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 = MediaFile.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: MediaFile) {
29 const children = dir.getChildren();
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 FilePermissions.setPermissions(dir.path, 1);
47 } else {
48 FilePermissions.setPermissions(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 const date = new Date(dateFile.contents);
71 if (Number.isNaN(date.getTime())) {
72 // a fresh .date file has no extracted contents until the
73 // text-contents processor lands; leave dir_reindex set and let a
74 // later pass pick this directory back up. a genuinely malformed
75 // .date keeps warning here until it is fixed on disk.
76 console.warn(
77 `${dir.path}/.date is empty or unparseable; deferring date override`,
78 );
79 return;
80 }
81 newestDate = date;
82 dir.setConfig(JSON.stringify({ hideChildrenDates: true }));
83 } else {
84 dir.setConfig("");
85 }
86
87 const dirHash = crypto
88 .createHash("sha1")
89 .update(dir.path + allHashes)
90 .digest("hex");
91
92 MediaFile.markDirectoryProcessed({
93 id: dir.id,
94 timestamp: newestDate,
95 contents: readmeContent,
96 size: totalSize,
97 hash: dirHash,
98 dirsort,
99 });
100}
101
102import * as crypto from "node:crypto";
103
104import * as log from "@clo/lib/log";
105import * as progress from "@clo/lib/progress";
106
107import { FilePermissions } from "#src/file-viewer/models/FilePermissions.ts";
108import { MediaFile } from "#src/file-viewer/models/MediaFile.ts";