emdash-emit 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -1
- package/README.md +235 -187
- package/dist/cli/commands/delete.d.ts.map +1 -1
- package/dist/cli/commands/delete.js +2 -0
- package/dist/cli/commands/delete.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +9 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/pull.d.ts +32 -0
- package/dist/cli/commands/pull.d.ts.map +1 -1
- package/dist/cli/commands/pull.js +108 -9
- package/dist/cli/commands/pull.js.map +1 -1
- package/dist/cli/commands/push.d.ts.map +1 -1
- package/dist/cli/commands/push.js +18 -0
- package/dist/cli/commands/push.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +15 -5
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/client/media.d.ts.map +1 -1
- package/dist/client/media.js +16 -5
- package/dist/client/media.js.map +1 -1
- package/dist/db/cache.d.ts +24 -5
- package/dist/db/cache.d.ts.map +1 -1
- package/dist/db/cache.js +308 -61
- package/dist/db/cache.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/sync-delete.d.ts.map +1 -1
- package/dist/mcp/tools/sync-delete.js +5 -0
- package/dist/mcp/tools/sync-delete.js.map +1 -1
- package/dist/mcp/tools/sync-doctor.d.ts.map +1 -1
- package/dist/mcp/tools/sync-doctor.js +12 -0
- package/dist/mcp/tools/sync-doctor.js.map +1 -1
- package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
- package/dist/mcp/tools/sync-pull.js +49 -12
- package/dist/mcp/tools/sync-pull.js.map +1 -1
- package/dist/mcp/tools/sync-push.d.ts.map +1 -1
- package/dist/mcp/tools/sync-push.js +5 -0
- package/dist/mcp/tools/sync-push.js.map +1 -1
- package/dist/mcp/tools/sync-status.d.ts.map +1 -1
- package/dist/mcp/tools/sync-status.js +6 -2
- package/dist/mcp/tools/sync-status.js.map +1 -1
- package/dist/sync/conflicts.d.ts.map +1 -1
- package/dist/sync/conflicts.js +44 -0
- package/dist/sync/conflicts.js.map +1 -1
- package/dist/sync/delete.d.ts +3 -1
- package/dist/sync/delete.d.ts.map +1 -1
- package/dist/sync/delete.js +7 -20
- package/dist/sync/delete.js.map +1 -1
- package/dist/sync/diff.d.ts.map +1 -1
- package/dist/sync/diff.js +18 -2
- package/dist/sync/diff.js.map +1 -1
- package/dist/sync/doctor.d.ts +28 -1
- package/dist/sync/doctor.d.ts.map +1 -1
- package/dist/sync/doctor.js +393 -45
- package/dist/sync/doctor.js.map +1 -1
- package/dist/sync/guards.d.ts +45 -0
- package/dist/sync/guards.d.ts.map +1 -0
- package/dist/sync/guards.js +235 -0
- package/dist/sync/guards.js.map +1 -0
- package/dist/sync/media/body_scan.d.ts +15 -0
- package/dist/sync/media/body_scan.d.ts.map +1 -0
- package/dist/sync/media/body_scan.js +132 -0
- package/dist/sync/media/body_scan.js.map +1 -0
- package/dist/sync/media/companions.d.ts +20 -0
- package/dist/sync/media/companions.d.ts.map +1 -0
- package/dist/sync/media/companions.js +111 -0
- package/dist/sync/media/companions.js.map +1 -0
- package/dist/sync/media/index.d.ts +9 -0
- package/dist/sync/media/index.d.ts.map +1 -0
- package/dist/sync/media/index.js +9 -0
- package/dist/sync/media/index.js.map +1 -0
- package/dist/sync/media/mirror_item.d.ts +27 -0
- package/dist/sync/media/mirror_item.d.ts.map +1 -0
- package/dist/sync/media/mirror_item.js +207 -0
- package/dist/sync/media/mirror_item.js.map +1 -0
- package/dist/sync/media/naming_mirror.d.ts +36 -0
- package/dist/sync/media/naming_mirror.d.ts.map +1 -0
- package/dist/sync/media/naming_mirror.js +143 -0
- package/dist/sync/media/naming_mirror.js.map +1 -0
- package/dist/sync/media/pull_article.d.ts +37 -0
- package/dist/sync/media/pull_article.d.ts.map +1 -0
- package/dist/sync/media/pull_article.js +273 -0
- package/dist/sync/media/pull_article.js.map +1 -0
- package/dist/sync/media/pull_library.d.ts +12 -0
- package/dist/sync/media/pull_library.d.ts.map +1 -0
- package/dist/sync/media/pull_library.js +207 -0
- package/dist/sync/media/pull_library.js.map +1 -0
- package/dist/sync/media/remote_index.d.ts +18 -0
- package/dist/sync/media/remote_index.d.ts.map +1 -0
- package/dist/sync/media/remote_index.js +94 -0
- package/dist/sync/media/remote_index.js.map +1 -0
- package/dist/sync/media/types.d.ts +191 -0
- package/dist/sync/media/types.d.ts.map +1 -0
- package/dist/sync/media/types.js +11 -0
- package/dist/sync/media/types.js.map +1 -0
- package/dist/sync/media.d.ts +1 -74
- package/dist/sync/media.d.ts.map +1 -1
- package/dist/sync/media.js +1 -592
- package/dist/sync/media.js.map +1 -1
- package/dist/sync/merge.d.ts +11 -0
- package/dist/sync/merge.d.ts.map +1 -1
- package/dist/sync/merge.js +53 -6
- package/dist/sync/merge.js.map +1 -1
- package/dist/sync/orchestrator.d.ts +23 -0
- package/dist/sync/orchestrator.d.ts.map +1 -1
- package/dist/sync/orchestrator.js +85 -1
- package/dist/sync/orchestrator.js.map +1 -1
- package/dist/sync/preflight.d.ts +1 -1
- package/dist/sync/preflight.d.ts.map +1 -1
- package/dist/sync/preflight.js +127 -15
- package/dist/sync/preflight.js.map +1 -1
- package/dist/sync/pull.d.ts +21 -0
- package/dist/sync/pull.d.ts.map +1 -1
- package/dist/sync/pull.js +278 -17
- package/dist/sync/pull.js.map +1 -1
- package/dist/sync/push.d.ts +7 -0
- package/dist/sync/push.d.ts.map +1 -1
- package/dist/sync/push.js +135 -58
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/status.d.ts +4 -0
- package/dist/sync/status.d.ts.map +1 -1
- package/dist/sync/status.js +68 -4
- package/dist/sync/status.js.map +1 -1
- package/dist/utils/config.d.ts +25 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +28 -2
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +15 -4
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/media-body-refs.d.ts +81 -0
- package/dist/utils/media-body-refs.d.ts.map +1 -0
- package/dist/utils/media-body-refs.js +240 -0
- package/dist/utils/media-body-refs.js.map +1 -0
- package/dist/utils/media-refs.d.ts +44 -0
- package/dist/utils/media-refs.d.ts.map +1 -0
- package/dist/utils/media-refs.js +101 -0
- package/dist/utils/media-refs.js.map +1 -0
- package/dist/utils/naming.d.ts +31 -0
- package/dist/utils/naming.d.ts.map +1 -1
- package/dist/utils/naming.js +58 -0
- package/dist/utils/naming.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { toRootRelativeMediaUrl } from '../../client/media.js';
|
|
5
|
+
import { MEDIA_FILE_ROUTE_PREFIX } from '../../utils/media-refs.js';
|
|
6
|
+
export function buildMediaMetaDoc(item, origin, existingMeta) {
|
|
7
|
+
const storageKey = (typeof item.storageKey === 'string' && item.storageKey ? item.storageKey : undefined) ??
|
|
8
|
+
(typeof existingMeta?.storageKey === 'string' && existingMeta.storageKey
|
|
9
|
+
? existingMeta.storageKey
|
|
10
|
+
: '');
|
|
11
|
+
const canonicalUrl = item.url ||
|
|
12
|
+
(storageKey
|
|
13
|
+
? `${MEDIA_FILE_ROUTE_PREFIX}${storageKey}`
|
|
14
|
+
: typeof existingMeta?.url === 'string'
|
|
15
|
+
? existingMeta.url
|
|
16
|
+
: '');
|
|
17
|
+
const rootRelativeUrl = canonicalUrl
|
|
18
|
+
? toRootRelativeMediaUrl(canonicalUrl, storageKey, origin)
|
|
19
|
+
: '';
|
|
20
|
+
const resolvedAlt = item.alt !== undefined && item.alt !== null && item.alt !== ''
|
|
21
|
+
? item.alt
|
|
22
|
+
: typeof existingMeta?.alt === 'string'
|
|
23
|
+
? existingMeta.alt
|
|
24
|
+
: '';
|
|
25
|
+
const resolvedCaption = item.caption !== undefined && item.caption !== null && item.caption !== ''
|
|
26
|
+
? item.caption
|
|
27
|
+
: typeof existingMeta?.caption === 'string'
|
|
28
|
+
? existingMeta.caption
|
|
29
|
+
: '';
|
|
30
|
+
const resolvedContentHash = typeof item.contentHash === 'string' && item.contentHash
|
|
31
|
+
? item.contentHash
|
|
32
|
+
: typeof existingMeta?.contentHash === 'string' && existingMeta.contentHash
|
|
33
|
+
? existingMeta.contentHash
|
|
34
|
+
: null;
|
|
35
|
+
return {
|
|
36
|
+
id: item.id,
|
|
37
|
+
filename: item.filename,
|
|
38
|
+
mimeType: item.mimeType,
|
|
39
|
+
size: item.size,
|
|
40
|
+
width: item.width ?? (typeof existingMeta?.width === 'number' ? existingMeta.width : null),
|
|
41
|
+
height: item.height ?? (typeof existingMeta?.height === 'number' ? existingMeta.height : null),
|
|
42
|
+
alt: resolvedAlt,
|
|
43
|
+
caption: resolvedCaption,
|
|
44
|
+
storageKey,
|
|
45
|
+
contentHash: resolvedContentHash,
|
|
46
|
+
status: item.status ?? (typeof existingMeta?.status === 'string' ? existingMeta.status : 'ready'),
|
|
47
|
+
url: rootRelativeUrl,
|
|
48
|
+
createdAt: item.createdAt || (typeof existingMeta?.createdAt === 'string' ? existingMeta.createdAt : ''),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Recovers the storageKey a companion YAML actually refers to. Older mirrors — most visibly the ones
|
|
53
|
+
* written before Emit persisted the field — carry no `storageKey:` key at all, yet their `url:` is the
|
|
54
|
+
* canonical `/_emdash/api/media/file/<storageKey>` route, which is confirmed metadata written from the
|
|
55
|
+
* server response and not an inference from a body reference. Absolute and root-relative forms are both
|
|
56
|
+
* accepted because either can appear depending on how the record was mirrored. `legacy_*` residue is
|
|
57
|
+
* never read here: it backs no real record, so it must not serve as verification evidence.
|
|
58
|
+
*/
|
|
59
|
+
export function storageKeyFromMediaMeta(doc) {
|
|
60
|
+
if (typeof doc.storageKey === 'string' && doc.storageKey.trim()) {
|
|
61
|
+
return doc.storageKey.trim();
|
|
62
|
+
}
|
|
63
|
+
if (typeof doc.url !== 'string' || !doc.url.trim())
|
|
64
|
+
return undefined;
|
|
65
|
+
const match = doc.url.match(/(?:^|\/)media\/file\/([^?#]+)/);
|
|
66
|
+
return match ? match[1] : undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Indexes every locally mirrored media asset (`media/<markdownRef>.yaml`, plus legacy companion names) so a body
|
|
70
|
+
* storageKey that the canonical id lookup failed to resolve can be re-checked against confirmed local
|
|
71
|
+
* metadata instead of being declared missing. Leftover `legacy_*` residue is skipped because it backs
|
|
72
|
+
* no real record. The result maps each ULID found in a mirror (record id or storageKey) to its entry,
|
|
73
|
+
* which is what lets Emit tell "the record exists under a different id" apart from "no record exists".
|
|
74
|
+
*/
|
|
75
|
+
export function collectLocalMediaMirrorCandidates(mediaDir) {
|
|
76
|
+
const byUlid = new Map();
|
|
77
|
+
if (!fs.existsSync(mediaDir))
|
|
78
|
+
return byUlid;
|
|
79
|
+
for (const file of fs.readdirSync(mediaDir)) {
|
|
80
|
+
if (!file.endsWith('.yaml') || file.startsWith('legacy_'))
|
|
81
|
+
continue;
|
|
82
|
+
let doc = null;
|
|
83
|
+
try {
|
|
84
|
+
doc = yaml.load(fs.readFileSync(path.join(mediaDir, file), 'utf8'));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!doc)
|
|
90
|
+
continue;
|
|
91
|
+
const storageKey = storageKeyFromMediaMeta(doc);
|
|
92
|
+
const stem = file.replace(/\.yaml$/, '');
|
|
93
|
+
const idMatch = stem.replace(/^\d{8}_/, '').match(/^([0-9A-HJ-NP-Z]{26})/i);
|
|
94
|
+
const docId = typeof doc.id === 'string' && /^[0-9A-HJ-NP-Z]{26}$/i.test(doc.id.trim())
|
|
95
|
+
? doc.id.trim().toUpperCase()
|
|
96
|
+
: undefined;
|
|
97
|
+
const mediaId = docId || (idMatch ? idMatch[1].toUpperCase() : '');
|
|
98
|
+
if (!mediaId)
|
|
99
|
+
continue;
|
|
100
|
+
const candidate = { mediaId, storageKey, metaFile: file };
|
|
101
|
+
if (!byUlid.has(mediaId))
|
|
102
|
+
byUlid.set(mediaId, candidate);
|
|
103
|
+
for (const ulid of storageKey?.matchAll(/[0-9A-HJ-NP-Z]{26}/gi) ?? []) {
|
|
104
|
+
const key = ulid[0].toUpperCase();
|
|
105
|
+
if (!byUlid.has(key))
|
|
106
|
+
byUlid.set(key, candidate);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return byUlid;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=companions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companions.js","sourceRoot":"","sources":["../../../src/sync/media/companions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './naming_mirror.js';
|
|
3
|
+
export * from './companions.js';
|
|
4
|
+
export * from './body_scan.js';
|
|
5
|
+
export * from './mirror_item.js';
|
|
6
|
+
export * from './pull_article.js';
|
|
7
|
+
export * from './pull_library.js';
|
|
8
|
+
export * from './remote_index.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sync/media/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './naming_mirror.js';
|
|
3
|
+
export * from './companions.js';
|
|
4
|
+
export * from './body_scan.js';
|
|
5
|
+
export * from './mirror_item.js';
|
|
6
|
+
export * from './pull_article.js';
|
|
7
|
+
export * from './pull_library.js';
|
|
8
|
+
export * from './remote_index.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sync/media/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CacheDatabase } from '../../db/cache.js';
|
|
2
|
+
import type { EmDashMediaItem, PromoteDraftMediaOptions, PromotedMediaFile } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Persists YAML companion metadata and downloads the binary file for a single media item.
|
|
5
|
+
*/
|
|
6
|
+
export declare function syncSingleMediaItem(options: {
|
|
7
|
+
item: EmDashMediaItem;
|
|
8
|
+
mediaDir: string;
|
|
9
|
+
origin: string;
|
|
10
|
+
token: string;
|
|
11
|
+
downloadFiles: boolean;
|
|
12
|
+
fetchFn: typeof fetch;
|
|
13
|
+
cache?: CacheDatabase;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
downloaded: boolean;
|
|
16
|
+
skipped: boolean;
|
|
17
|
+
baseName: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Promotes a local draft image from media/drafts/<name> into the canonical
|
|
21
|
+
* media/<markdownRef>.<ext> location alongside its matching media/<markdownRef>.yaml companion, where
|
|
22
|
+
* markdownRef is derived from the storageKey the article body will reference. Returns both the workspace-relative
|
|
23
|
+
* path of the promoted binary and the shared `media/<markdownRef>` stem, so the caller can record the mirror in
|
|
24
|
+
* `media_state.markdown_ref`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function promoteDraftMediaFile(options: PromoteDraftMediaOptions): PromotedMediaFile;
|
|
27
|
+
//# sourceMappingURL=mirror_item.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mirror_item.d.ts","sourceRoot":"","sources":["../../../src/sync/media/mirror_item.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvD,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/F;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,OAAO,KAAK,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAmKvE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAyD1F"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { MEDIA_FILE_ROUTE_PREFIX } from '../../utils/media-refs.js';
|
|
5
|
+
import { buildMediaMetaDoc } from './companions.js';
|
|
6
|
+
import { cleanupDuplicateMediaFilesForId, mediaMirrorNames, moveFileSafe, } from './naming_mirror.js';
|
|
7
|
+
/**
|
|
8
|
+
* Persists YAML companion metadata and downloads the binary file for a single media item.
|
|
9
|
+
*/
|
|
10
|
+
export async function syncSingleMediaItem(options) {
|
|
11
|
+
const { item, mediaDir, origin, token, downloadFiles, fetchFn, cache } = options;
|
|
12
|
+
const names = mediaMirrorNames(item, mediaDir);
|
|
13
|
+
const baseName = names.markdownRef;
|
|
14
|
+
const yamlPath = path.join(mediaDir, `${baseName}.yaml`);
|
|
15
|
+
// Read any existing companion for this record id first, whatever stem it currently occupies. The
|
|
16
|
+
// storageKey-named companion is the expected location; the legacy stems are where an older workspace still
|
|
17
|
+
// holds the same asset. `legacy_<storageKey>.yaml` is skipped unconditionally: it carries a matching ULID
|
|
18
|
+
// but fabricates its metadata from a file route, so it must never seed a real record.
|
|
19
|
+
let existingMeta = null;
|
|
20
|
+
let existingMetaPath = null;
|
|
21
|
+
const mediaFiles = fs.readdirSync(mediaDir);
|
|
22
|
+
const companionCandidates = [yamlPath];
|
|
23
|
+
for (const legacy of names.legacyBaseNames) {
|
|
24
|
+
companionCandidates.push(path.join(mediaDir, `${legacy}.yaml`));
|
|
25
|
+
}
|
|
26
|
+
for (const candidate of companionCandidates) {
|
|
27
|
+
if (!fs.existsSync(candidate))
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
existingMeta = yaml.load(fs.readFileSync(candidate, 'utf8'));
|
|
31
|
+
existingMetaPath = candidate;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
existingMeta = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!existingMeta) {
|
|
39
|
+
const matchingYaml = mediaFiles.find((f) => f.endsWith('.yaml') &&
|
|
40
|
+
!f.startsWith('legacy_') &&
|
|
41
|
+
f !== path.basename(yamlPath) &&
|
|
42
|
+
(f.includes(`_${item.id}-`) || f.startsWith(`${item.id}-`)));
|
|
43
|
+
if (matchingYaml) {
|
|
44
|
+
try {
|
|
45
|
+
existingMeta = yaml.load(fs.readFileSync(path.join(mediaDir, matchingYaml), 'utf8'));
|
|
46
|
+
existingMetaPath = path.join(mediaDir, matchingYaml);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
existingMeta = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 1. Write YAML metadata under the storageKey-derived stem
|
|
54
|
+
const metaDoc = buildMediaMetaDoc(item, origin, existingMeta);
|
|
55
|
+
fs.writeFileSync(yamlPath, yaml.dump(metaDoc, { indent: 2, quotingType: '"', forceQuotes: false, lineWidth: -1 }), 'utf8');
|
|
56
|
+
let downloaded = false;
|
|
57
|
+
let skipped = false;
|
|
58
|
+
// 2. Download binary file if enabled and url is present
|
|
59
|
+
const canonicalDownloadPath = item.url || (item.storageKey ? `${MEDIA_FILE_ROUTE_PREFIX}${item.storageKey}` : '');
|
|
60
|
+
const fileExt = names.ext || (item.mimeType.includes('png') ? '.png' : '.jpg');
|
|
61
|
+
const targetFilePath = path.join(mediaDir, `${baseName}${fileExt}`);
|
|
62
|
+
// Rename an existing mirror into the new name before considering a download, so migrating a workspace
|
|
63
|
+
// costs no bandwidth. Size must match: a same-stem file of a different size is not this asset.
|
|
64
|
+
if (!fs.existsSync(targetFilePath)) {
|
|
65
|
+
const candidates = names.legacyBaseNames
|
|
66
|
+
.map((legacy) => path.join(mediaDir, `${legacy}${fileExt}`))
|
|
67
|
+
.concat(mediaFiles
|
|
68
|
+
.filter((f) => !f.endsWith('.yaml') &&
|
|
69
|
+
!f.startsWith('legacy_') &&
|
|
70
|
+
(f.includes(`_${item.id}-`) || f.startsWith(`${item.id}-`)))
|
|
71
|
+
.map((f) => path.join(mediaDir, f)));
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
if (candidate === targetFilePath || !fs.existsSync(candidate))
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs.statSync(candidate);
|
|
77
|
+
if (stat.size === item.size) {
|
|
78
|
+
moveFileSafe(candidate, targetFilePath);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (downloadFiles && canonicalDownloadPath) {
|
|
86
|
+
let shouldDownload = true;
|
|
87
|
+
if (fs.existsSync(targetFilePath)) {
|
|
88
|
+
const stat = fs.statSync(targetFilePath);
|
|
89
|
+
if (stat.size === item.size) {
|
|
90
|
+
shouldDownload = false;
|
|
91
|
+
skipped = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (shouldDownload) {
|
|
95
|
+
try {
|
|
96
|
+
const downloadUrl = new URL(canonicalDownloadPath, origin).toString();
|
|
97
|
+
const fileRes = await fetchFn(downloadUrl, {
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${token}`,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (fileRes.ok) {
|
|
103
|
+
const buffer = Buffer.from(await fileRes.arrayBuffer());
|
|
104
|
+
fs.writeFileSync(targetFilePath, buffer);
|
|
105
|
+
downloaded = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Ignore individual download failures
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Register the mirrored asset in media_state. Before storageKey naming, only the push/upload path wrote
|
|
114
|
+
// this table, so a pulled workspace had no index at all and every lookup fell back to scanning YAML. With
|
|
115
|
+
// the mirror name no longer carrying the record id, that index is what makes a body reference resolvable
|
|
116
|
+
// without re-reading thousands of companions.
|
|
117
|
+
if (cache && item.storageKey) {
|
|
118
|
+
const mirrorUrl = typeof metaDoc.url === 'string' && metaDoc.url
|
|
119
|
+
? metaDoc.url
|
|
120
|
+
: `${MEDIA_FILE_ROUTE_PREFIX}${item.storageKey}`;
|
|
121
|
+
cache.saveMediaState({
|
|
122
|
+
localHash: `media-record:${item.id}`,
|
|
123
|
+
mediaId: item.id,
|
|
124
|
+
storageKey: item.storageKey,
|
|
125
|
+
url: mirrorUrl,
|
|
126
|
+
workspacePath: path.posix.join('media', `${baseName}${fileExt}`),
|
|
127
|
+
markdownRef: path.posix.join('media', baseName),
|
|
128
|
+
status: 'ready',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Drop the companion this asset occupied under a legacy stem. Done after the binary rename so an
|
|
132
|
+
// interrupted run never leaves a record whose metadata and bytes disagree about the name.
|
|
133
|
+
if (existingMetaPath && existingMetaPath !== yamlPath && fs.existsSync(existingMetaPath)) {
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(existingMetaPath);
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
}
|
|
139
|
+
for (const legacy of names.legacyBaseNames) {
|
|
140
|
+
const legacyYaml = path.join(mediaDir, `${legacy}.yaml`);
|
|
141
|
+
if (legacyYaml !== yamlPath && fs.existsSync(legacyYaml)) {
|
|
142
|
+
try {
|
|
143
|
+
fs.unlinkSync(legacyYaml);
|
|
144
|
+
}
|
|
145
|
+
catch { }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Clean up any duplicate files with the same media ID but a different datePrefix/baseName
|
|
149
|
+
cleanupDuplicateMediaFilesForId(mediaDir, item.id, baseName);
|
|
150
|
+
return { downloaded, skipped, baseName };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Promotes a local draft image from media/drafts/<name> into the canonical
|
|
154
|
+
* media/<markdownRef>.<ext> location alongside its matching media/<markdownRef>.yaml companion, where
|
|
155
|
+
* markdownRef is derived from the storageKey the article body will reference. Returns both the workspace-relative
|
|
156
|
+
* path of the promoted binary and the shared `media/<markdownRef>` stem, so the caller can record the mirror in
|
|
157
|
+
* `media_state.markdown_ref`.
|
|
158
|
+
*/
|
|
159
|
+
export function promoteDraftMediaFile(options) {
|
|
160
|
+
const { workspaceRoot, origin, draftAbsPath, item, removeDraft = true } = options;
|
|
161
|
+
const mediaDir = path.join(workspaceRoot, 'media');
|
|
162
|
+
fs.mkdirSync(mediaDir, { recursive: true });
|
|
163
|
+
const ext = path.extname(item.filename) ||
|
|
164
|
+
path.extname(draftAbsPath) ||
|
|
165
|
+
(item.mimeType.includes('png') ? '.png' : '.jpg');
|
|
166
|
+
const names = mediaMirrorNames({ ...item, filename: item.filename || path.basename(draftAbsPath) }, mediaDir);
|
|
167
|
+
const baseName = names.markdownRef;
|
|
168
|
+
const yamlPath = path.join(mediaDir, `${baseName}.yaml`);
|
|
169
|
+
let existingMeta = null;
|
|
170
|
+
if (fs.existsSync(yamlPath)) {
|
|
171
|
+
try {
|
|
172
|
+
existingMeta = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
existingMeta = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const metaDoc = buildMediaMetaDoc(item, origin, existingMeta);
|
|
179
|
+
fs.writeFileSync(yamlPath, yaml.dump(metaDoc, { indent: 2, quotingType: '"', forceQuotes: false, lineWidth: -1 }), 'utf8');
|
|
180
|
+
const targetFilePath = path.join(mediaDir, `${baseName}${ext}`);
|
|
181
|
+
if (fs.existsSync(draftAbsPath)) {
|
|
182
|
+
if (removeDraft) {
|
|
183
|
+
moveFileSafe(draftAbsPath, targetFilePath);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
fs.copyFileSync(draftAbsPath, targetFilePath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Retire any earlier id-named mirror of the same record so one asset never occupies two names.
|
|
190
|
+
for (const legacy of names.legacyBaseNames) {
|
|
191
|
+
for (const suffix of [ext, '.yaml']) {
|
|
192
|
+
const legacyPath = path.join(mediaDir, `${legacy}${suffix}`);
|
|
193
|
+
if (legacyPath !== yamlPath && legacyPath !== targetFilePath && fs.existsSync(legacyPath)) {
|
|
194
|
+
try {
|
|
195
|
+
fs.unlinkSync(legacyPath);
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
cleanupDuplicateMediaFilesForId(mediaDir, item.id, baseName);
|
|
202
|
+
return {
|
|
203
|
+
filePath: path.posix.join('media', `${baseName}${ext}`),
|
|
204
|
+
markdownRef: path.posix.join('media', baseName),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=mirror_item.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mirror_item.js","sourceRoot":"","sources":["../../../src/sync/media/mirror_item.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type EmDashMediaItem, type MediaMirrorNames } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Moves a file from src to dest, falling back to copy + unlink when src and dest
|
|
4
|
+
* reside on different filesystems (`EXDEV`).
|
|
5
|
+
*/
|
|
6
|
+
export declare function moveFileSafe(src: string, dest: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Reads the media record id a mirror belongs to from its companion YAML, accepting both the storageKey-named
|
|
9
|
+
* stem and the legacy `<yyyyMMdd>_<id>-<name>` form. Returns `null` when no companion exists or it names no
|
|
10
|
+
* valid ULID, which is how Emit refuses to prune a file it cannot attribute to a record. A companion whose
|
|
11
|
+
* id is explicitly null (or any non-ULID value) proves the mirror backs no real record, so the stem's own
|
|
12
|
+
* leading characters are never trusted as an id — that fallback would let fabricated fixture ids reach
|
|
13
|
+
* record-keyed logic.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mediaRecordIdForStem(mediaDir: string, stem: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Removes any stale duplicate files in `media/` that belong to the same 26-char `mediaId` but sit under a
|
|
18
|
+
* different stem — a leftover date-prefixed name from an earlier local promotion, for instance. Ownership is
|
|
19
|
+
* read from each file's companion YAML rather than from the filename, because a storageKey-named mirror
|
|
20
|
+
* carries no record id at all and must never be matched (or deleted) by a coincidental ULID prefix.
|
|
21
|
+
*/
|
|
22
|
+
export declare function cleanupDuplicateMediaFilesForId(mediaDir: string, mediaId: string, canonicalBaseName: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the storageKey-derived stem against the names already taken in `media/`. Two distinct keys can
|
|
25
|
+
* sanitize to the same basename — `a/sample.png` and `b/sample.png` both yield `sample` — and a second asset
|
|
26
|
+
* must never overwrite the first. A name already owned by this same record is reused as-is, so re-pulling is
|
|
27
|
+
* idempotent; any other collision gets the deterministic `<stem>--<mediaId>` suffix.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveMarkdownRefCollision(mediaDir: string, baseRef: string, item: EmDashMediaItem): string;
|
|
30
|
+
/**
|
|
31
|
+
* Computes the canonical mirror name for one remote media item. Falls back to the record-id based stem only
|
|
32
|
+
* when the storageKey carries no usable basename, so a malformed migrated key cannot produce an unnameable
|
|
33
|
+
* file. Pass `mediaDir` to resolve a stem collision against the mirrors already on disk.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mediaMirrorNames(item: EmDashMediaItem, mediaDir?: string): MediaMirrorNames;
|
|
36
|
+
//# sourceMappingURL=naming_mirror.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"naming_mirror.d.ts","sourceRoot":"","sources":["../../../src/sync/media/naming_mirror.ts"],"names":[],"mappings":"AASA,OAAO,EAA2B,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAElG;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAW5D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBlF;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,MAAM,GACxB,IAAI,CAwBN;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,GACpB,MAAM,CAgBR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAkB3F"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { formatDatePrefix, sanitizeTitleForFilename, storageKeyToMarkdownRef, collisionSuffixedMarkdownRef, } from '../../utils/frontmatter.js';
|
|
5
|
+
import { MEDIA_BINARY_EXTENSIONS } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Moves a file from src to dest, falling back to copy + unlink when src and dest
|
|
8
|
+
* reside on different filesystems (`EXDEV`).
|
|
9
|
+
*/
|
|
10
|
+
export function moveFileSafe(src, dest) {
|
|
11
|
+
try {
|
|
12
|
+
fs.renameSync(src, dest);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err?.code === 'EXDEV') {
|
|
16
|
+
fs.copyFileSync(src, dest);
|
|
17
|
+
fs.unlinkSync(src);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reads the media record id a mirror belongs to from its companion YAML, accepting both the storageKey-named
|
|
26
|
+
* stem and the legacy `<yyyyMMdd>_<id>-<name>` form. Returns `null` when no companion exists or it names no
|
|
27
|
+
* valid ULID, which is how Emit refuses to prune a file it cannot attribute to a record. A companion whose
|
|
28
|
+
* id is explicitly null (or any non-ULID value) proves the mirror backs no real record, so the stem's own
|
|
29
|
+
* leading characters are never trusted as an id — that fallback would let fabricated fixture ids reach
|
|
30
|
+
* record-keyed logic.
|
|
31
|
+
*/
|
|
32
|
+
export function mediaRecordIdForStem(mediaDir, stem) {
|
|
33
|
+
const candidates = [`${stem}.yaml`, `${stem.replace(/^\d{8}_/, '')}.yaml`];
|
|
34
|
+
for (const name of candidates) {
|
|
35
|
+
const candidatePath = path.join(mediaDir, name);
|
|
36
|
+
if (!fs.existsSync(candidatePath))
|
|
37
|
+
continue;
|
|
38
|
+
let doc = null;
|
|
39
|
+
try {
|
|
40
|
+
doc = yaml.load(fs.readFileSync(candidatePath, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!doc)
|
|
46
|
+
continue;
|
|
47
|
+
const rawId = typeof doc.id === 'string' ? doc.id.trim() : '';
|
|
48
|
+
if (/^[0-9A-Za-z_-]{26}$/.test(rawId)) {
|
|
49
|
+
return rawId.toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Removes any stale duplicate files in `media/` that belong to the same 26-char `mediaId` but sit under a
|
|
57
|
+
* different stem — a leftover date-prefixed name from an earlier local promotion, for instance. Ownership is
|
|
58
|
+
* read from each file's companion YAML rather than from the filename, because a storageKey-named mirror
|
|
59
|
+
* carries no record id at all and must never be matched (or deleted) by a coincidental ULID prefix.
|
|
60
|
+
*/
|
|
61
|
+
export function cleanupDuplicateMediaFilesForId(mediaDir, mediaId, canonicalBaseName) {
|
|
62
|
+
if (!fs.existsSync(mediaDir))
|
|
63
|
+
return;
|
|
64
|
+
const wanted = mediaId.toUpperCase();
|
|
65
|
+
for (const entry of fs.readdirSync(mediaDir)) {
|
|
66
|
+
if (entry.startsWith('legacy_'))
|
|
67
|
+
continue;
|
|
68
|
+
const entryBase = entry.replace(/\.yaml$/, '').replace(/\.[^.]+$/, '');
|
|
69
|
+
if (entryBase === canonicalBaseName)
|
|
70
|
+
continue;
|
|
71
|
+
// Only a companion-backed mirror can be attributed, and only when the companion itself is being
|
|
72
|
+
// retired together with its binary.
|
|
73
|
+
if (!entry.endsWith('.yaml'))
|
|
74
|
+
continue;
|
|
75
|
+
if (mediaRecordIdForStem(mediaDir, entryBase) !== wanted)
|
|
76
|
+
continue;
|
|
77
|
+
for (const orphan of [
|
|
78
|
+
`${entryBase}.yaml`,
|
|
79
|
+
...MEDIA_BINARY_EXTENSIONS.map((e) => `${entryBase}${e}`),
|
|
80
|
+
]) {
|
|
81
|
+
const orphanPath = path.join(mediaDir, orphan);
|
|
82
|
+
if (orphan === `${canonicalBaseName}.yaml`)
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(orphanPath))
|
|
86
|
+
fs.unlinkSync(orphanPath);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Ignore cleanup errors
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolves the storageKey-derived stem against the names already taken in `media/`. Two distinct keys can
|
|
96
|
+
* sanitize to the same basename — `a/sample.png` and `b/sample.png` both yield `sample` — and a second asset
|
|
97
|
+
* must never overwrite the first. A name already owned by this same record is reused as-is, so re-pulling is
|
|
98
|
+
* idempotent; any other collision gets the deterministic `<stem>--<mediaId>` suffix.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveMarkdownRefCollision(mediaDir, baseRef, item) {
|
|
101
|
+
const suffixed = collisionSuffixedMarkdownRef(baseRef, item.id);
|
|
102
|
+
const takenByOther = (stem) => {
|
|
103
|
+
for (const suffix of ['.yaml', ...MEDIA_BINARY_EXTENSIONS]) {
|
|
104
|
+
const candidatePath = path.join(mediaDir, `${stem}${suffix}`);
|
|
105
|
+
if (!fs.existsSync(candidatePath))
|
|
106
|
+
continue;
|
|
107
|
+
const ownerId = mediaRecordIdForStem(mediaDir, stem);
|
|
108
|
+
if (ownerId && ownerId !== item.id.toUpperCase())
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
if (!takenByOther(baseRef))
|
|
114
|
+
return baseRef;
|
|
115
|
+
if (!takenByOther(suffixed))
|
|
116
|
+
return suffixed;
|
|
117
|
+
// Both taken and both owned by this record: the suffixed form is already this asset's, so keep it.
|
|
118
|
+
return suffixed;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Computes the canonical mirror name for one remote media item. Falls back to the record-id based stem only
|
|
122
|
+
* when the storageKey carries no usable basename, so a malformed migrated key cannot produce an unnameable
|
|
123
|
+
* file. Pass `mediaDir` to resolve a stem collision against the mirrors already on disk.
|
|
124
|
+
*/
|
|
125
|
+
export function mediaMirrorNames(item, mediaDir) {
|
|
126
|
+
const ext = path.extname(item.filename) || '';
|
|
127
|
+
const sanitizedBase = sanitizeTitleForFilename(path.basename(item.filename, ext));
|
|
128
|
+
const legacyBaseNames = [
|
|
129
|
+
`${item.id}-${sanitizedBase}`,
|
|
130
|
+
`${formatDatePrefix(item.createdAt)}${item.id}-${sanitizedBase}`,
|
|
131
|
+
];
|
|
132
|
+
const fromKey = item.storageKey ? storageKeyToMarkdownRef(item.storageKey) : null;
|
|
133
|
+
if (fromKey) {
|
|
134
|
+
const markdownRef = mediaDir ? resolveMarkdownRefCollision(mediaDir, fromKey, item) : fromKey;
|
|
135
|
+
return { markdownRef, ext, legacyBaseNames };
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
markdownRef: `${formatDatePrefix(item.createdAt)}${item.id}-${sanitizedBase}`,
|
|
139
|
+
ext,
|
|
140
|
+
legacyBaseNames,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=naming_mirror.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"naming_mirror.js","sourceRoot":"","sources":["../../../src/sync/media/naming_mirror.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CacheDatabase } from '../../db/cache.js';
|
|
2
|
+
import type { MirroredAssetResult, PullMediaResult, RemoteStorageKeyIndex } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pulls media referenced by a specific article (by ULID or file path).
|
|
5
|
+
*/
|
|
6
|
+
export declare function pullArticleMedia(options: {
|
|
7
|
+
workspaceRoot: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
token: string;
|
|
10
|
+
refreshToken?: string;
|
|
11
|
+
article: string;
|
|
12
|
+
collection?: string;
|
|
13
|
+
downloadFiles?: boolean;
|
|
14
|
+
fetchFn?: typeof fetch;
|
|
15
|
+
cache?: CacheDatabase;
|
|
16
|
+
}): Promise<PullMediaResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Mirrors only the assets a batch of pulled articles actually references, reusing the records the lightweight
|
|
19
|
+
* index walk already fetched instead of paying for a per-asset `GET /_emdash/api/media/{id}`.
|
|
20
|
+
*
|
|
21
|
+
* This is the mirror half of the default-ON local-first pull: the walk phase builds `RemoteStorageKeyIndex`
|
|
22
|
+
* (JSON only, no binaries), and this phase downloads the binary + companion YAML for exactly the storageKeys
|
|
23
|
+
* found in article bodies. A key the walk did not report is NOT proof of a missing record — the record can
|
|
24
|
+
* exist under a different primary key while the body carries an R2 storageKey — so it is re-checked against
|
|
25
|
+
* confirmed evidence only (a walked record resolved under that exact ULID, or local mirror metadata) and what
|
|
26
|
+
* still fails is reported as `orphanStorageKeys`. Emit never fetches such an asset and never registers one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function mirrorReferencedAssetsFromIndex(referencedStorageKeys: Iterable<string>, index: RemoteStorageKeyIndex, options: {
|
|
29
|
+
workspaceRoot: string;
|
|
30
|
+
origin: string;
|
|
31
|
+
token: string;
|
|
32
|
+
refreshToken?: string;
|
|
33
|
+
downloadFiles?: boolean;
|
|
34
|
+
fetchFn?: typeof fetch;
|
|
35
|
+
cache?: CacheDatabase;
|
|
36
|
+
}): Promise<MirroredAssetResult>;
|
|
37
|
+
//# sourceMappingURL=pull_article.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull_article.d.ts","sourceRoot":"","sources":["../../../src/sync/media/pull_article.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAKvD,OAAO,KAAK,EAEV,mBAAmB,EAEnB,eAAe,EACf,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6L3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,+BAA+B,CACnD,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvC,KAAK,EAAE,qBAAqB,EAC5B,OAAO,EAAE;IACP,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,GACA,OAAO,CAAC,mBAAmB,CAAC,CAuE9B"}
|