1// Directory metadata pass: readme contents, explicit sort order, friend
2// permissions, recursive size/date/hash aggregates, and the `.date`
3// override. Driven by the `dir_reindex` flag, which the scanner sets on
4// parents whenever children change. Ported from the tail of file-scan.ts.
5const console = log.scoped("indexer");
6
7export function run(p: progress.Ref): boolean {
8 const dirs = mediaFiles.getDirectoriesToReindex()
9 .sort((a, b) => b.path.length - a.path.length);
10 if (dirs.length === 0) return false;
11 using node = p.start("update directory metadata", { total: dirs.length });
12
13 for (const dir of dirs) {
14 using _ = node.start(dir.path);
15 try {
16 processDir(dir);
17 } catch (err) {
18 // one broken directory must not starve the rest of the pass; the
19 // dir_reindex flag stays set, so it retries on the next trigger
20 console.error(`directory metadata for ${dir.path} failed:`, err);
21 }
22 node.inc();
23 }
24 console.info(`updated metadata for ${dirs.length} directories`);
25 return true;
26}
27
28function processDir(dir: { id: number; path: string }) {
29 const children = mediaFiles.getChildren(dir);
30
31 // readme.txt
32 const readmeContent = children.find((x) => x.basename === "readme.txt")?.contents ?? "";
33
34 // dirsort
35 let dirsort: string[] | null = null;
36 const dirSortRaw = children.find((x) => x.basename === ".dirsort")?.contents ?? "";
37 if (dirSortRaw) {
38 dirsort = dirSortRaw
39 .split("\n")
40 .map((x) => x.trim())
41 .filter(Boolean);
42 }
43
44 // Permissions
45 if (children.some((x) => x.basename === ".friends")) {
46 permissions.set(dir.path, 1);
47 } else {
48 permissions.set(dir.path, 0);
49 }
50
51 // Recursive stats.
52 let totalSize = 0;
53 let newestDate = new Date(0);
54 let allHashes = "";
55 for (const child of children) {
56 totalSize += child.size;
57 allHashes += child.hash;
58
59 // readme.txt and hidden files don't render a date in the UI, so they
60 // must not contribute to the directory's date either.
61 const dateExempt = child.basename === "readme.txt" || child.basename.startsWith(".");
62 if (!dateExempt && child.date > newestDate) {
63 newestDate = child.date;
64 }
65 }
66
67 // Project Date
68 const dateFile = children.find((x) => x.basename === ".date");
69 if (dateFile) {
70 // a `YYYY-MM-DD` day at noon utc, which is the same day in pacific time, where file dates are shown
71 const date = new Date(`${dateFile.contents.trim()}T12:00:00Z`);
72 if (Number.isNaN(date.getTime())) {
73 // a fresh .date file has no extracted contents until the
74 // text-contents processor lands; leave dir_reindex set and let a
75 // later pass pick this directory back up. a genuinely malformed
76 // .date keeps warning here until it is fixed on disk.
77 console.warn(
78 `${dir.path}/.date is empty or unparseable; deferring date override`,
79 );
80 return;
81 }
82 newestDate = date;
83 mediaFiles.setConfig(dir, JSON.stringify({ hideChildrenDates: true }));
84 } else {
85 mediaFiles.setConfig(dir, "");
86 }
87
88 const dirHash = crypto
89 .createHash("sha1")
90 .update(dir.path + allHashes)
91 .digest("hex");
92
93 mediaFiles.markDirectoryProcessed({
94 id: dir.id,
95 timestamp: newestDate,
96 contents: readmeContent,
97 size: totalSize,
98 hash: dirHash,
99 dirsort,
100 });
101}
102
103import * as mediaFiles from "#src/file-viewer/models/media-files.ts";
104import * as permissions from "#src/file-viewer/models/permissions.ts";
105import * as crypto from "node:crypto";
106
107import * as log from "@clo/lib/log";
108import * as progress from "@clo/lib/progress";