derive.mjs
114 lines 3.2 KB
Raw
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 10 days ago
1 /**
2 * Vault media/** walk for attachment derivation (Phase 2F-b-b).
3 *
4 * Extends mcp/resources/listing.mjs semantics to the full media tree with all
5 * supported attachment extensions (image, video, audio, document).
6 *
7 * @see docs/ATTACHMENT-STORE-CONTRACT-2F-b.md §3.1
8 */
9
10 import fs from 'fs';
11 import path from 'path';
12 import {
13 IMAGE_EXT_MIME,
14 VIDEO_EXT_MIME,
15 AUDIO_EXT_MIME,
16 DOC_EXT_MIME,
17 } from '../media-url-extract.mjs';
18
19 /** @type {Record<string, string>} */
20 const EXT_TO_MIME = {
21 ...IMAGE_EXT_MIME,
22 ...VIDEO_EXT_MIME,
23 ...AUDIO_EXT_MIME,
24 ...DOC_EXT_MIME,
25 };
26
27 export const ALL_MEDIA_EXTENSIONS = Object.keys(EXT_TO_MIME);
28
29 /**
30 * @typedef {Object} VaultMediaFile
31 * @property {string} relPath - vault-relative path under media/ (e.g. media/photos/x.png)
32 * @property {number} byteSize
33 * @property {string} mtimeIso
34 * @property {string} ext
35 */
36
37 /**
38 * Walk all files under vault `media/**` matching known attachment extensions.
39 *
40 * @param {string} vaultPath - absolute vault root
41 * @param {string} [mediaSubdir='media'] - vault-relative media root
42 * @returns {VaultMediaFile[]}
43 */
44 export function walkAllMediaFiles(vaultPath, mediaSubdir = 'media') {
45 const relRoot = mediaSubdir.replace(/\\/g, '/').replace(/\/$/, '') || 'media';
46 const dir = path.join(vaultPath, relRoot);
47 if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
48 return [];
49 }
50
51 /** @type {VaultMediaFile[]} */
52 const files = [];
53
54 /**
55 * @param {string} absDir
56 * @param {string} prefix - path under media/ (may be empty)
57 */
58 const walk = (absDir, prefix) => {
59 let entries;
60 try {
61 entries = fs.readdirSync(absDir, { withFileTypes: true });
62 } catch {
63 return;
64 }
65 for (const entry of entries) {
66 const full = path.join(absDir, entry.name);
67 const relUnderMedia = prefix ? `${prefix}/${entry.name}` : entry.name;
68 if (entry.isDirectory()) {
69 walk(full, relUnderMedia);
70 } else if (entry.isFile()) {
71 const ext = path.extname(entry.name).slice(1).toLowerCase();
72 if (!ALL_MEDIA_EXTENSIONS.includes(ext)) continue;
73 let stat;
74 try {
75 stat = fs.statSync(full);
76 } catch {
77 continue;
78 }
79 const vaultRel = `${relRoot}/${relUnderMedia}`.replace(/\\/g, '/');
80 files.push({
81 relPath: vaultRel,
82 byteSize: stat.size,
83 mtimeIso: stat.mtime.toISOString(),
84 ext,
85 });
86 }
87 }
88 };
89
90 walk(dir, '');
91 files.sort((a, b) => a.relPath.localeCompare(b.relPath));
92 return files;
93 }
94
95 /**
96 * @param {string} ext - extension without dot
97 * @returns {{ mimeClass: 'image'|'video'|'audio'|'document'|'unknown', mimeType: string|null }}
98 */
99 export function mimeFromExtension(ext) {
100 const lower = ext.toLowerCase();
101 if (lower in IMAGE_EXT_MIME) {
102 return { mimeClass: 'image', mimeType: IMAGE_EXT_MIME[lower] };
103 }
104 if (lower in VIDEO_EXT_MIME) {
105 return { mimeClass: 'video', mimeType: VIDEO_EXT_MIME[lower] };
106 }
107 if (lower in AUDIO_EXT_MIME) {
108 return { mimeClass: 'audio', mimeType: AUDIO_EXT_MIME[lower] };
109 }
110 if (lower in DOC_EXT_MIME) {
111 return { mimeClass: 'document', mimeType: DOC_EXT_MIME[lower] };
112 }
113 return { mimeClass: 'unknown', mimeType: null };
114 }
File History 1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 10 days ago