@im-nobsidian/core 0.17.1 → 0.18.0
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/dist/index.d.ts +81 -2
- package/dist/index.js +530 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12897,6 +12897,76 @@ function createDefaultPipeline(options) {
|
|
|
12897
12897
|
return pipeline;
|
|
12898
12898
|
}
|
|
12899
12899
|
|
|
12900
|
+
// src/sync/vault-file-index.ts
|
|
12901
|
+
var INDEX_MAX_AGE_MS = 5e3;
|
|
12902
|
+
function keyOf(path) {
|
|
12903
|
+
return path.normalize("NFC").toLowerCase();
|
|
12904
|
+
}
|
|
12905
|
+
function baseNameOf(path) {
|
|
12906
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
12907
|
+
}
|
|
12908
|
+
function folderOf(path) {
|
|
12909
|
+
const slash = path.lastIndexOf("/");
|
|
12910
|
+
return slash === -1 ? "" : path.slice(0, slash);
|
|
12911
|
+
}
|
|
12912
|
+
var VaultFileIndex = class {
|
|
12913
|
+
constructor(vaultFs, now = Date.now) {
|
|
12914
|
+
this.vaultFs = vaultFs;
|
|
12915
|
+
this.now = now;
|
|
12916
|
+
}
|
|
12917
|
+
vaultFs;
|
|
12918
|
+
now;
|
|
12919
|
+
byName = null;
|
|
12920
|
+
builtAt = 0;
|
|
12921
|
+
/**
|
|
12922
|
+
* `linkpath`(이름 또는 경로 끝)로 끝나는 볼트 파일 — 가까운 것부터: 노트 폴더 안(얕은 것 먼저) → 짧은 경로 → 가나다. 지금
|
|
12923
|
+
* 있는 것만 돌려준다.
|
|
12924
|
+
*/
|
|
12925
|
+
async find(linkpath, noteFolder) {
|
|
12926
|
+
if (this.byName === null || this.now() - this.builtAt > INDEX_MAX_AGE_MS) await this.rebuild();
|
|
12927
|
+
const key = keyOf(linkpath.replace(/^\/+/, ""));
|
|
12928
|
+
const suffix = `/${key}`;
|
|
12929
|
+
const matches = (this.byName?.get(keyOf(baseNameOf(key))) ?? []).filter((path) => {
|
|
12930
|
+
const k = keyOf(path);
|
|
12931
|
+
return k === key || k.endsWith(suffix);
|
|
12932
|
+
});
|
|
12933
|
+
const existing = [];
|
|
12934
|
+
for (const path of rank(matches, noteFolder)) {
|
|
12935
|
+
if (await this.vaultFs.exists(path)) existing.push(path);
|
|
12936
|
+
}
|
|
12937
|
+
return existing;
|
|
12938
|
+
}
|
|
12939
|
+
async rebuild() {
|
|
12940
|
+
const byName2 = /* @__PURE__ */ new Map();
|
|
12941
|
+
let files = [];
|
|
12942
|
+
try {
|
|
12943
|
+
files = await this.vaultFs.listNonMarkdownFiles() ?? [];
|
|
12944
|
+
} catch {
|
|
12945
|
+
}
|
|
12946
|
+
for (const file of files) {
|
|
12947
|
+
const key = keyOf(baseNameOf(file.path));
|
|
12948
|
+
const list = byName2.get(key) ?? [];
|
|
12949
|
+
list.push(file.path);
|
|
12950
|
+
byName2.set(key, list);
|
|
12951
|
+
}
|
|
12952
|
+
this.byName = byName2;
|
|
12953
|
+
this.builtAt = this.now();
|
|
12954
|
+
}
|
|
12955
|
+
};
|
|
12956
|
+
function rank(paths, noteFolder) {
|
|
12957
|
+
const inside2 = (path) => {
|
|
12958
|
+
if (noteFolder === void 0) return Number.POSITIVE_INFINITY;
|
|
12959
|
+
const folder = folderOf(path);
|
|
12960
|
+
if (noteFolder === "") return folder.split("/").filter(Boolean).length;
|
|
12961
|
+
if (folder !== noteFolder && !folder.startsWith(`${noteFolder}/`))
|
|
12962
|
+
return Number.POSITIVE_INFINITY;
|
|
12963
|
+
return folder.slice(noteFolder.length).split("/").filter(Boolean).length;
|
|
12964
|
+
};
|
|
12965
|
+
return [...paths].sort(
|
|
12966
|
+
(a, b) => inside2(a) - inside2(b) || a.length - b.length || a.localeCompare(b)
|
|
12967
|
+
);
|
|
12968
|
+
}
|
|
12969
|
+
|
|
12900
12970
|
// src/utils/mime.ts
|
|
12901
12971
|
var EXTENSION_TO_BLOCK_TYPE = {
|
|
12902
12972
|
".png": "image",
|
|
@@ -13018,11 +13088,11 @@ function getMimeType(filename) {
|
|
|
13018
13088
|
}
|
|
13019
13089
|
|
|
13020
13090
|
// src/sync/uploaded-media.ts
|
|
13021
|
-
function
|
|
13091
|
+
function keyOf2(pageId) {
|
|
13022
13092
|
return `uploaded_media:${compactNotionId(pageId)}`;
|
|
13023
13093
|
}
|
|
13024
13094
|
function rememberUploadedMedia(stateDb, pageId, media) {
|
|
13025
|
-
const key =
|
|
13095
|
+
const key = keyOf2(pageId);
|
|
13026
13096
|
if (media.length === 0 && stateDb.getMeta(key) === null) return;
|
|
13027
13097
|
stateDb.setMeta(key, JSON.stringify(media.map(rowOf)));
|
|
13028
13098
|
}
|
|
@@ -13030,14 +13100,14 @@ function rememberReceivedMedia(stateDb, pageId, media) {
|
|
|
13030
13100
|
if (media.length === 0) return;
|
|
13031
13101
|
const merged = recallUploadedMedia(stateDb, pageId);
|
|
13032
13102
|
for (const item of media) merged.set(item.fileId, item);
|
|
13033
|
-
stateDb.setMeta(
|
|
13103
|
+
stateDb.setMeta(keyOf2(pageId), JSON.stringify([...merged.values()].map(rowOf)));
|
|
13034
13104
|
}
|
|
13035
13105
|
function rowOf({ fileId, target, localPath, fileHash }) {
|
|
13036
13106
|
return fileHash === void 0 ? { fileId, target, localPath } : { fileId, target, localPath, fileHash };
|
|
13037
13107
|
}
|
|
13038
13108
|
function recallUploadedMedia(stateDb, pageId) {
|
|
13039
13109
|
const known = /* @__PURE__ */ new Map();
|
|
13040
|
-
const value = stateDb?.getMeta(
|
|
13110
|
+
const value = stateDb?.getMeta(keyOf2(pageId));
|
|
13041
13111
|
if (!value) return known;
|
|
13042
13112
|
try {
|
|
13043
13113
|
for (const row of JSON.parse(value)) {
|
|
@@ -13226,8 +13296,11 @@ function nameHashOf(fullHash) {
|
|
|
13226
13296
|
var IMAGE_STEM_MAX = 50;
|
|
13227
13297
|
function withoutOwnHash(name, hash) {
|
|
13228
13298
|
if (hash.length !== NAME_HASH_LENGTH || !/^[0-9a-f]+$/.test(hash)) return name;
|
|
13229
|
-
const
|
|
13230
|
-
|
|
13299
|
+
const tail = new RegExp(`-${hash}(\\.[^./]*)?$`).exec(name);
|
|
13300
|
+
const stem = tail ? name.slice(0, tail.index) : "";
|
|
13301
|
+
if (stem === "") return name;
|
|
13302
|
+
const extension = tail?.[1] ?? "";
|
|
13303
|
+
return extension && !stem.toLowerCase().endsWith(extension.toLowerCase()) ? `${stem}${extension}` : stem;
|
|
13231
13304
|
}
|
|
13232
13305
|
function imageStemOf(uploadedName, pageTitle) {
|
|
13233
13306
|
const stem = safeImageStem(uploadedName.replace(/\.[^.]*$/, ""));
|
|
@@ -13270,7 +13343,7 @@ function localEmbedTargets(note) {
|
|
|
13270
13343
|
}
|
|
13271
13344
|
return targets;
|
|
13272
13345
|
}
|
|
13273
|
-
function
|
|
13346
|
+
function folderOf2(notePath) {
|
|
13274
13347
|
return notePath?.includes("/") ? notePath.slice(0, notePath.lastIndexOf("/")) : void 0;
|
|
13275
13348
|
}
|
|
13276
13349
|
function messageOf2(error) {
|
|
@@ -13371,7 +13444,7 @@ var ImageHandler = class {
|
|
|
13371
13444
|
if (!target || /^https?:\/\//.test(target) || target.includes("\n")) return null;
|
|
13372
13445
|
const vaultPath = stripAlias2(target);
|
|
13373
13446
|
if (baseName(vaultPath) !== uploadedName) return null;
|
|
13374
|
-
for (const candidate of this.vaultCandidates(vaultPath, noteFolder)) {
|
|
13447
|
+
for await (const candidate of this.vaultCandidates(vaultPath, noteFolder)) {
|
|
13375
13448
|
try {
|
|
13376
13449
|
if (await this.vaultFs.exists(candidate)) return target;
|
|
13377
13450
|
} catch {
|
|
@@ -13411,7 +13484,7 @@ var ImageHandler = class {
|
|
|
13411
13484
|
const spans = notionMediaSpans(markdown);
|
|
13412
13485
|
if (spans.length === 0) return markdown;
|
|
13413
13486
|
const known = pageId ? recallUploadedMedia(this.stateDb, pageId) : /* @__PURE__ */ new Map();
|
|
13414
|
-
const noteFolder =
|
|
13487
|
+
const noteFolder = folderOf2(notePath);
|
|
13415
13488
|
let result = markdown;
|
|
13416
13489
|
for (const span of spans) {
|
|
13417
13490
|
const uploadedName = this.uploadedNameOf(span.url);
|
|
@@ -13441,7 +13514,7 @@ var ImageHandler = class {
|
|
|
13441
13514
|
return null;
|
|
13442
13515
|
}
|
|
13443
13516
|
for (const target of targets) {
|
|
13444
|
-
for (const candidate of this.vaultCandidates(stripAlias2(target), noteFolder)) {
|
|
13517
|
+
for await (const candidate of this.vaultCandidates(stripAlias2(target), noteFolder)) {
|
|
13445
13518
|
let localHash;
|
|
13446
13519
|
try {
|
|
13447
13520
|
localHash = sha256Of(await this.vaultFs.readBinary(candidate));
|
|
@@ -13584,7 +13657,7 @@ var ImageHandler = class {
|
|
|
13584
13657
|
* @returns 보일 이름은 받을 때 붙인 내용 해시를 뗀 것이다 — 다시 받으면 같은 이름 · 같은 경로가 된다.
|
|
13585
13658
|
*/
|
|
13586
13659
|
async uploadNoteAttachment(target, notePath) {
|
|
13587
|
-
const uploaded = await this.uploadLocalImage(target,
|
|
13660
|
+
const uploaded = await this.uploadLocalImage(target, folderOf2(notePath));
|
|
13588
13661
|
const base = uploaded.localPath.split("/").pop() ?? uploaded.localPath;
|
|
13589
13662
|
const name = withoutOwnHash(base, nameHashOf(uploaded.fileHash));
|
|
13590
13663
|
const extension = /\.[^./]+$/.exec(base)?.[0] ?? "";
|
|
@@ -13597,7 +13670,7 @@ var ImageHandler = class {
|
|
|
13597
13670
|
if (!this.notionClient) return [];
|
|
13598
13671
|
const localImages = images.filter((img) => !img.isExternal && img.localPath);
|
|
13599
13672
|
if (localImages.length === 0) return [];
|
|
13600
|
-
const noteFolder =
|
|
13673
|
+
const noteFolder = folderOf2(notePath);
|
|
13601
13674
|
const results = [];
|
|
13602
13675
|
const imageBlocks = [];
|
|
13603
13676
|
for (const img of localImages) {
|
|
@@ -13684,7 +13757,7 @@ var ImageHandler = class {
|
|
|
13684
13757
|
}
|
|
13685
13758
|
return segment;
|
|
13686
13759
|
});
|
|
13687
|
-
const noteFolder =
|
|
13760
|
+
const noteFolder = folderOf2(notePath);
|
|
13688
13761
|
const kept = [];
|
|
13689
13762
|
let result = "";
|
|
13690
13763
|
let cursor = 0;
|
|
@@ -13775,7 +13848,7 @@ var ImageHandler = class {
|
|
|
13775
13848
|
if (kept.length > 0) this.rememberUploads(pageId, kept);
|
|
13776
13849
|
return empty;
|
|
13777
13850
|
}
|
|
13778
|
-
const noteFolder =
|
|
13851
|
+
const noteFolder = folderOf2(notePath);
|
|
13779
13852
|
const uploaded = [];
|
|
13780
13853
|
const remembered = [...kept];
|
|
13781
13854
|
const unsupported2 = [];
|
|
@@ -13933,29 +14006,37 @@ var ImageHandler = class {
|
|
|
13933
14006
|
/**
|
|
13934
14007
|
* 임베드 경로가 가리킬 수 있는 볼트 실제 파일 후보를 우선순위대로 만든다(F23).
|
|
13935
14008
|
* Obsidian 위키링크 임베드는 노트 옆 파일을 파일명만으로 참조하는 게 관례라
|
|
13936
|
-
* 노트 폴더 → 볼트 루트 → attachments/ 순.
|
|
14009
|
+
* 노트 폴더 → 볼트 루트 → attachments/ 순. 세 곳에 없으면 볼트 어디서든 이름으로 찾는다({@link VaultFileIndex}).
|
|
13937
14010
|
*
|
|
13938
14011
|
* push(업로드)와 pull(원본 복원)이 **같은 사다리**를 봐야 왕복이 닫힌다. 예전에는
|
|
13939
14012
|
* pull 쪽이 볼트 루트만 확인해서, `__e2e_probe__/노트.md` 안의 `![[t-img.png]]` 는
|
|
13940
14013
|
* 올라갈 때는 노트 폴더에서 잘 찾아 올리고도 내려올 때는 원본을 못 찾아 사본을
|
|
13941
14014
|
* 새로 받았다(실측) — 왕복할 때마다 첨부가 한 벌씩 늘어난다.
|
|
13942
14015
|
*/
|
|
13943
|
-
vaultCandidates(localPath, noteFolder) {
|
|
13944
|
-
const
|
|
14016
|
+
async *vaultCandidates(localPath, noteFolder) {
|
|
14017
|
+
const ladder = [];
|
|
13945
14018
|
if (noteFolder && !localPath.includes("/")) {
|
|
13946
|
-
|
|
14019
|
+
ladder.push(`${noteFolder}/${localPath}`);
|
|
14020
|
+
}
|
|
14021
|
+
ladder.push(localPath, `${this.attachmentFolder}/${localPath}`);
|
|
14022
|
+
yield* ladder;
|
|
14023
|
+
for (const found2 of await this.fileIndex.find(localPath, noteFolder)) {
|
|
14024
|
+
if (!ladder.includes(found2)) yield found2;
|
|
13947
14025
|
}
|
|
13948
|
-
|
|
13949
|
-
|
|
14026
|
+
}
|
|
14027
|
+
indexOfFiles = null;
|
|
14028
|
+
/** 볼트의 첨부 이름표 — 처음 쓸 때 만든다. */
|
|
14029
|
+
get fileIndex() {
|
|
14030
|
+
this.indexOfFiles ??= new VaultFileIndex(this.vaultFs);
|
|
14031
|
+
return this.indexOfFiles;
|
|
13950
14032
|
}
|
|
13951
14033
|
/**
|
|
13952
14034
|
* 임베드 경로를 볼트 실제 파일로 해석해 읽는다(F23).
|
|
13953
14035
|
* 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
|
|
13954
14036
|
*/
|
|
13955
14037
|
async readImageFromVault(localPath, noteFolder) {
|
|
13956
|
-
const candidates = this.vaultCandidates(localPath, noteFolder);
|
|
13957
14038
|
let lastError;
|
|
13958
|
-
for (const candidate of
|
|
14039
|
+
for await (const candidate of this.vaultCandidates(localPath, noteFolder)) {
|
|
13959
14040
|
try {
|
|
13960
14041
|
return { buffer: await this.vaultFs.readBinary(candidate), resolvedPath: candidate };
|
|
13961
14042
|
} catch (error) {
|
|
@@ -14148,12 +14229,20 @@ var ImageHandler = class {
|
|
|
14148
14229
|
const hash = nameHashOf(fileHash);
|
|
14149
14230
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
14150
14231
|
const name = withoutOwnHash(caption, hash);
|
|
14151
|
-
const
|
|
14152
|
-
|
|
14153
|
-
|
|
14154
|
-
)
|
|
14155
|
-
|
|
14156
|
-
|
|
14232
|
+
const hasExtension2 = extension !== "" && name.toLowerCase().endsWith(extension.toLowerCase());
|
|
14233
|
+
const stem = hasExtension2 ? name.slice(0, -extension.length) : name;
|
|
14234
|
+
const localPath = `${this.attachmentFolder}/${this.fileNameOf(stem, hash, extension)}`;
|
|
14235
|
+
const legacyPath = `${this.attachmentFolder}/${this.fileNameOf(name, hash, extension)}`;
|
|
14236
|
+
if (hasExtension2 && await this.vaultFs.exists(legacyPath)) {
|
|
14237
|
+
return {
|
|
14238
|
+
originalUrl: url,
|
|
14239
|
+
localPath: legacyPath,
|
|
14240
|
+
hash,
|
|
14241
|
+
size: buffer.length,
|
|
14242
|
+
fileHash,
|
|
14243
|
+
name
|
|
14244
|
+
};
|
|
14245
|
+
}
|
|
14157
14246
|
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
14158
14247
|
await this.vaultFs.writeBinary(localPath, buffer);
|
|
14159
14248
|
return { originalUrl: url, localPath, hash, size: buffer.length, fileHash, name };
|
|
@@ -14163,6 +14252,14 @@ var ImageHandler = class {
|
|
|
14163
14252
|
}
|
|
14164
14253
|
throw lastError;
|
|
14165
14254
|
}
|
|
14255
|
+
/** 받은 파일의 이름 — `<줄기>-<해시><확장자>`. */
|
|
14256
|
+
fileNameOf(stem, hash, extension) {
|
|
14257
|
+
const safeName = truncateBytes(
|
|
14258
|
+
stem.replace(/[^a-zA-Z0-9가-힣\s._-]/g, "").replace(/\s+/g, "-").slice(0, 80),
|
|
14259
|
+
FILE_SYSTEM_NAME_MAX_BYTES - utf8Bytes(`-${hash}${extension}`)
|
|
14260
|
+
);
|
|
14261
|
+
return `${safeName}-${hash}${extension}`;
|
|
14262
|
+
}
|
|
14166
14263
|
isNotionImageUrl(url) {
|
|
14167
14264
|
return this.isNotionFileUrl(url);
|
|
14168
14265
|
}
|
|
@@ -15049,7 +15146,74 @@ function selectStaleDbArtifacts(files, localFolder, keepPaths) {
|
|
|
15049
15146
|
return stale;
|
|
15050
15147
|
}
|
|
15051
15148
|
|
|
15149
|
+
// src/sync/folder-files.ts
|
|
15150
|
+
function unescapeCaption(caption) {
|
|
15151
|
+
return caption.replace(/\\([\\`*_{}[\]()#+\-.!|~<>$&])/g, "$1").trim();
|
|
15152
|
+
}
|
|
15153
|
+
function folderFilesOf(markdown, namedOnly) {
|
|
15154
|
+
let rest = markdown.replace(CHILD_PAGE_TAG_RE, "").replace(CHILD_DATABASE_TAG_RE, "").replace(/<empty-block\/>/g, "");
|
|
15155
|
+
const files = [];
|
|
15156
|
+
for (const span of notionMediaSpans(rest)) {
|
|
15157
|
+
if (!/^https?:\/\//.test(span.url)) return null;
|
|
15158
|
+
const name = uploadedFileNameFromUrl(span.url);
|
|
15159
|
+
if (!name || namedOnly && unescapeCaption(span.label) !== name) return null;
|
|
15160
|
+
rest = rest.replace(span.full, "");
|
|
15161
|
+
files.push({ url: span.url, name });
|
|
15162
|
+
}
|
|
15163
|
+
return files.length > 0 && rest.trim() === "" ? files : null;
|
|
15164
|
+
}
|
|
15165
|
+
var WIKI_EMBED_RE = /!\[\[([^\]|#^\n]+)/g;
|
|
15166
|
+
var MD_EMBED_RE = /!\[[^\]\n]*\]\(<?([^)>\s]+)/g;
|
|
15167
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
15168
|
+
var WIKILINK_TARGET_RE = /\[\[([^\]|#^\n]+)/g;
|
|
15169
|
+
function baseNameOf2(target) {
|
|
15170
|
+
let decoded = target.trim();
|
|
15171
|
+
try {
|
|
15172
|
+
decoded = decodeURI(decoded);
|
|
15173
|
+
} catch {
|
|
15174
|
+
}
|
|
15175
|
+
return decoded.slice(decoded.lastIndexOf("/") + 1).normalize("NFC");
|
|
15176
|
+
}
|
|
15177
|
+
function embeddedNamesOf(notes) {
|
|
15178
|
+
const names = /* @__PURE__ */ new Set();
|
|
15179
|
+
for (const text of notes) {
|
|
15180
|
+
for (const m of text.matchAll(WIKI_EMBED_RE)) names.add(baseNameOf2(m[1]));
|
|
15181
|
+
for (const m of text.matchAll(MD_EMBED_RE)) names.add(baseNameOf2(m[1]));
|
|
15182
|
+
const frontmatter = FRONTMATTER_RE.exec(text)?.[1];
|
|
15183
|
+
if (frontmatter) {
|
|
15184
|
+
for (const m of frontmatter.matchAll(WIKILINK_TARGET_RE)) names.add(baseNameOf2(m[1]));
|
|
15185
|
+
}
|
|
15186
|
+
}
|
|
15187
|
+
return names;
|
|
15188
|
+
}
|
|
15189
|
+
function keyOf3(pageId) {
|
|
15190
|
+
return `folder_files:${compactNotionId(pageId)}`;
|
|
15191
|
+
}
|
|
15192
|
+
function folderFileIdsOf(stateDb, pageId) {
|
|
15193
|
+
const raw = stateDb.getMeta(keyOf3(pageId));
|
|
15194
|
+
if (!raw) return /* @__PURE__ */ new Map();
|
|
15195
|
+
try {
|
|
15196
|
+
return new Map(Object.entries(JSON.parse(raw)));
|
|
15197
|
+
} catch {
|
|
15198
|
+
return /* @__PURE__ */ new Map();
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
function forgetFolderFileId(stateDb, pageId, localPath) {
|
|
15202
|
+
const ids = folderFileIdsOf(stateDb, pageId);
|
|
15203
|
+
if (!ids.delete(localPath)) return;
|
|
15204
|
+
stateDb.setMeta(keyOf3(pageId), JSON.stringify(Object.fromEntries(ids)));
|
|
15205
|
+
}
|
|
15206
|
+
function rememberFolderFileId(stateDb, pageId, localPath, fileId) {
|
|
15207
|
+
const ids = folderFileIdsOf(stateDb, pageId);
|
|
15208
|
+
if (ids.get(localPath) === fileId) return;
|
|
15209
|
+
ids.set(localPath, fileId);
|
|
15210
|
+
stateDb.setMeta(keyOf3(pageId), JSON.stringify(Object.fromEntries(ids)));
|
|
15211
|
+
}
|
|
15212
|
+
|
|
15052
15213
|
// src/sync/file-handler.ts
|
|
15214
|
+
function isHiddenPath(path) {
|
|
15215
|
+
return path.split("/").some((part) => part.startsWith("."));
|
|
15216
|
+
}
|
|
15053
15217
|
function getFolderPath(filePath) {
|
|
15054
15218
|
const parts = filePath.split("/");
|
|
15055
15219
|
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
@@ -15064,6 +15228,8 @@ var FileHandler = class {
|
|
|
15064
15228
|
this.customFetch = options?.fetch;
|
|
15065
15229
|
this.downloadTimeoutMs = options?.downloadTimeoutMs ?? DEFAULT_CONFIG.advanced.mediaDownloadTimeoutMs;
|
|
15066
15230
|
this.itemTimeoutMs = options?.itemTimeoutMs ?? DEFAULT_CONFIG.advanced.itemTimeoutMs;
|
|
15231
|
+
this.deleteSync = options?.deleteSync ?? false;
|
|
15232
|
+
this.attachmentsFolder = (options?.attachmentsFolder ?? DEFAULT_CONFIG.paths.attachments).replace(/^\/+|\/+$/g, "");
|
|
15067
15233
|
}
|
|
15068
15234
|
vaultFs;
|
|
15069
15235
|
notionClient;
|
|
@@ -15072,6 +15238,8 @@ var FileHandler = class {
|
|
|
15072
15238
|
customFetch;
|
|
15073
15239
|
downloadTimeoutMs;
|
|
15074
15240
|
itemTimeoutMs;
|
|
15241
|
+
attachmentsFolder;
|
|
15242
|
+
deleteSync;
|
|
15075
15243
|
get fetchFn() {
|
|
15076
15244
|
return this.customFetch ?? globalThis.fetch;
|
|
15077
15245
|
}
|
|
@@ -15081,7 +15249,9 @@ var FileHandler = class {
|
|
|
15081
15249
|
if (folderFiles.length === 0) return [];
|
|
15082
15250
|
const results = [];
|
|
15083
15251
|
for (const file of folderFiles) {
|
|
15084
|
-
const
|
|
15252
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
15253
|
+
if (existing && await this.isUnchanged(existing, file)) continue;
|
|
15254
|
+
const result = await this.pushSingleFile(file, folderPageId, existing);
|
|
15085
15255
|
if (result) results.push(result);
|
|
15086
15256
|
}
|
|
15087
15257
|
return results;
|
|
@@ -15102,18 +15272,11 @@ var FileHandler = class {
|
|
|
15102
15272
|
* 내면 같은 말이 두 번 나온다). 없으면 경고 로그로만 남는다.
|
|
15103
15273
|
* @returns 업로드 결과. 변경 없음(스킵)이거나 실패했으면 null.
|
|
15104
15274
|
*/
|
|
15105
|
-
async pushSingleFile(file, folderPageId, failed) {
|
|
15106
|
-
const existing = this.stateDb.getFileRegistry(file.path);
|
|
15107
|
-
if (existing) {
|
|
15108
|
-
if (existing.fileSize === file.size) return null;
|
|
15109
|
-
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15110
|
-
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
15111
|
-
if (existing.fileHash === currentHash) return null;
|
|
15112
|
-
}
|
|
15275
|
+
async pushSingleFile(file, folderPageId, existing, failed) {
|
|
15113
15276
|
await this.sema.acquire();
|
|
15114
15277
|
try {
|
|
15115
15278
|
return await withDeadline(
|
|
15116
|
-
() => this.uploadFile(file, folderPageId),
|
|
15279
|
+
() => this.uploadFile(file, folderPageId, existing),
|
|
15117
15280
|
this.itemTimeoutMs,
|
|
15118
15281
|
`\uCCA8\uBD80 \uC5C5\uB85C\uB4DC ${file.path}`
|
|
15119
15282
|
);
|
|
@@ -15134,6 +15297,22 @@ var FileHandler = class {
|
|
|
15134
15297
|
this.sema.release();
|
|
15135
15298
|
}
|
|
15136
15299
|
}
|
|
15300
|
+
/** 올린 뒤 바뀌지 않았나 — 크기가 같으면 읽지 않는다. */
|
|
15301
|
+
async isUnchanged(existing, file) {
|
|
15302
|
+
if (existing.fileSize === file.size) return true;
|
|
15303
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15304
|
+
return existing.fileHash === createHash("sha256").update(buffer).digest("hex");
|
|
15305
|
+
}
|
|
15306
|
+
/** pull 이 받은 것을 두는 폴더 안인가. */
|
|
15307
|
+
inAttachmentsFolder(path) {
|
|
15308
|
+
return this.attachmentsFolder !== "" && path.startsWith(`${this.attachmentsFolder}/`);
|
|
15309
|
+
}
|
|
15310
|
+
/** 폴더 첨부를 둘 Notion 페이지 — 폴더 노트 없는 폴더의 페이지(폴더 레코드)만. 맨 위는 루트 기록이 있을 때만. */
|
|
15311
|
+
folderPageOf(folder) {
|
|
15312
|
+
const record = this.stateDb.getByPath(folder || "__root__");
|
|
15313
|
+
if (!record?.notionPageId) return null;
|
|
15314
|
+
return folder === "" || isFolderRecord(record) ? record.notionPageId : null;
|
|
15315
|
+
}
|
|
15137
15316
|
/**
|
|
15138
15317
|
* 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 아예 빼고, Notion 이 받지 않는
|
|
15139
15318
|
* 형식은 따로 모은다.
|
|
@@ -15160,7 +15339,7 @@ var FileHandler = class {
|
|
|
15160
15339
|
* @param failed 올리지 못한 첨부를 담을 곳 — push 결과의 `failed` 를 넘기면 CLI · 플러그인이 처리하지 못한 것으로
|
|
15161
15340
|
* 보인다. 예전엔 경고 로그로만 남아 push 가 「실패 0」 으로 끝났다.
|
|
15162
15341
|
*/
|
|
15163
|
-
async pushAllFiles(failed) {
|
|
15342
|
+
async pushAllFiles(failed, folders) {
|
|
15164
15343
|
const { uploadable: allFiles, unsupported: unsupported2 } = await this.listUploadableFiles();
|
|
15165
15344
|
if (unsupported2.length > 0) {
|
|
15166
15345
|
const names = unsupported2.slice(0, LOGGED_NAMES).map((f) => f.path);
|
|
@@ -15169,32 +15348,225 @@ var FileHandler = class {
|
|
|
15169
15348
|
`[Im-Nobsidian] Notion \uC774 \uBC1B\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC774\uB77C \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uCCA8\uBD80 ${unsupported2.length}\uAC1C: ${names.join(", ")}${rest > 0 ? ` \uC678 ${rest}\uAC1C` : ""}`
|
|
15170
15349
|
);
|
|
15171
15350
|
}
|
|
15172
|
-
if (
|
|
15173
|
-
const
|
|
15351
|
+
if (this.deleteSync) await this.removeDeletedFolderFiles();
|
|
15352
|
+
const pending = [];
|
|
15174
15353
|
for (const file of allFiles) {
|
|
15175
|
-
|
|
15176
|
-
const
|
|
15177
|
-
|
|
15178
|
-
|
|
15354
|
+
if (this.inAttachmentsFolder(file.path) || isHiddenPath(file.path)) continue;
|
|
15355
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
15356
|
+
if (existing) {
|
|
15357
|
+
if (existing.notionPageId !== this.folderPageOf(getFolderPath(file.path))) continue;
|
|
15358
|
+
if (await this.isUnchanged(existing, file)) continue;
|
|
15359
|
+
}
|
|
15360
|
+
pending.push({ file, existing });
|
|
15361
|
+
}
|
|
15362
|
+
if (pending.length === 0) return [];
|
|
15363
|
+
const embedded = pending.some((p) => !p.existing) ? await this.embeddedNames() : /* @__PURE__ */ new Set();
|
|
15364
|
+
const byFolder = /* @__PURE__ */ new Map();
|
|
15365
|
+
for (const item of pending) {
|
|
15366
|
+
if (!item.existing && embedded.has(item.file.path.slice(item.file.path.lastIndexOf("/") + 1).normalize("NFC"))) {
|
|
15367
|
+
continue;
|
|
15368
|
+
}
|
|
15369
|
+
const folder = getFolderPath(item.file.path);
|
|
15370
|
+
const list = byFolder.get(folder) ?? [];
|
|
15371
|
+
list.push(item);
|
|
15372
|
+
byFolder.set(folder, list);
|
|
15179
15373
|
}
|
|
15180
15374
|
const results = [];
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
const
|
|
15184
|
-
if (!
|
|
15185
|
-
|
|
15186
|
-
|
|
15187
|
-
|
|
15375
|
+
const stranded = [];
|
|
15376
|
+
for (const [folder, items] of byFolder) {
|
|
15377
|
+
const pageId = await this.provideFolderPage(folder, folders, items, stranded);
|
|
15378
|
+
if (!pageId) continue;
|
|
15379
|
+
for (const { file, existing } of items) {
|
|
15380
|
+
const result = await this.pushSingleFile(file, pageId, existing, failed);
|
|
15381
|
+
if (result) results.push(result);
|
|
15382
|
+
}
|
|
15383
|
+
}
|
|
15384
|
+
this.reportStranded(stranded);
|
|
15385
|
+
return results;
|
|
15386
|
+
}
|
|
15387
|
+
/**
|
|
15388
|
+
* 볼트에서 지운 폴더 첨부를 Notion 에서도 지운다(deleteSync) — 폴더 페이지에 적힌 파일 가운데 볼트에 없는 것의 블록과 기록.
|
|
15389
|
+
* 포함 · 제외 패턴으로 빠진 파일은 볼트에 있어 지우지 않는다.
|
|
15390
|
+
*/
|
|
15391
|
+
async removeDeletedFolderFiles() {
|
|
15392
|
+
const removed = [];
|
|
15393
|
+
for (const record of this.stateDb.getAll()) {
|
|
15394
|
+
if (!record.notionPageId || !isFolderRecord(record)) continue;
|
|
15395
|
+
for (const entry of this.stateDb.getFilesByPageId(record.notionPageId)) {
|
|
15396
|
+
if (getFolderPath(entry.localPath) !== record.obsidianPath) continue;
|
|
15397
|
+
if (await this.vaultFs.exists(entry.localPath)) continue;
|
|
15398
|
+
try {
|
|
15399
|
+
const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
|
|
15400
|
+
await this.removeOlderBlocks(record.notionPageId, name, void 0);
|
|
15401
|
+
this.stateDb.deleteFileRegistry(entry.localPath);
|
|
15402
|
+
forgetFolderFileId(this.stateDb, record.notionPageId, entry.localPath);
|
|
15403
|
+
removed.push(entry.localPath);
|
|
15404
|
+
} catch (error) {
|
|
15405
|
+
getLogger().warn(
|
|
15406
|
+
`[Im-Nobsidian] \uBCFC\uD2B8\uC5D0\uC11C \uC9C0\uC6B4 \uD3F4\uB354 \uCCA8\uBD80\uB97C Notion \uC5D0\uC11C \uC9C0\uC6B0\uC9C0 \uBABB\uD568 (${entry.localPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
15188
15407
|
);
|
|
15189
15408
|
}
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
if (removed.length > 0) {
|
|
15412
|
+
getLogger().info(
|
|
15413
|
+
`[Im-Nobsidian] \uBCFC\uD2B8\uC5D0\uC11C \uC9C0\uC6B4 \uD3F4\uB354 \uCCA8\uBD80 ${removed.length}\uAC1C\uB97C Notion \uC5D0\uC11C \uC9C0\uC6C0`
|
|
15414
|
+
);
|
|
15415
|
+
}
|
|
15416
|
+
}
|
|
15417
|
+
/** 폴더 첨부를 둘 페이지 — 노트 없는 폴더면 마련한다. 둘 곳이 없으면 그 파일들을 `stranded` 에 담고 null. */
|
|
15418
|
+
async provideFolderPage(folder, folders, items, stranded) {
|
|
15419
|
+
let pageId = this.folderPageOf(folder);
|
|
15420
|
+
let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704" : "\uD3F4\uB354 \uB178\uD2B8\uAC00 \uC788\uB294 \uD3F4\uB354 \xB7 DB \uD3F4\uB354";
|
|
15421
|
+
if (!pageId && folder !== "" && folders && !this.stateDb.getByPath(folder)) {
|
|
15422
|
+
try {
|
|
15423
|
+
await folders.ensureFolderPage(folder);
|
|
15424
|
+
pageId = this.folderPageOf(folder);
|
|
15425
|
+
} catch (error) {
|
|
15426
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
15427
|
+
}
|
|
15428
|
+
}
|
|
15429
|
+
if (!pageId) stranded.push(...items.map(({ file }) => ({ path: file.path, reason })));
|
|
15430
|
+
return pageId;
|
|
15431
|
+
}
|
|
15432
|
+
/**
|
|
15433
|
+
* 둘 곳이 없어 올리지 않은 폴더 첨부 — 경고로 알린다. 예전에는 디버그 로그로만 남아 파일이 Notion 에 가지 않은 것을 알 수
|
|
15434
|
+
* 없었다. 노트에 붙이면 그 노트와 함께 올라간다.
|
|
15435
|
+
*/
|
|
15436
|
+
reportStranded(stranded) {
|
|
15437
|
+
if (stranded.length === 0) return;
|
|
15438
|
+
const names = stranded.slice(0, LOGGED_NAMES).map((f) => `${f.path}(${f.reason})`);
|
|
15439
|
+
const rest = stranded.length - names.length;
|
|
15440
|
+
getLogger().warn(
|
|
15441
|
+
`[Im-Nobsidian] \uD3F4\uB354\uC5D0 Notion \uC790\uB9AC\uAC00 \uC5C6\uC5B4 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uCCA8\uBD80 ${stranded.length}\uAC1C \u2014 \uB178\uD2B8\uC5D0 \uBD99\uC774\uBA74 \uD568\uAED8 \uC62C\uB77C\uAC04\uB2E4: ${names.join(", ")}${rest > 0 ? ` \uC678 ${rest}\uAC1C` : ""}`
|
|
15442
|
+
);
|
|
15443
|
+
}
|
|
15444
|
+
/** 볼트의 노트가 붙인 파일 이름 — {@link embeddedNamesOf}. 읽지 못한 노트는 건너뛴다. */
|
|
15445
|
+
async embeddedNames() {
|
|
15446
|
+
const texts = [];
|
|
15447
|
+
for (const note of await this.vaultFs.listMarkdownFiles()) {
|
|
15448
|
+
try {
|
|
15449
|
+
texts.push(await this.vaultFs.readFile(note.path));
|
|
15450
|
+
} catch {
|
|
15451
|
+
}
|
|
15452
|
+
}
|
|
15453
|
+
return embeddedNamesOf(texts);
|
|
15454
|
+
}
|
|
15455
|
+
/**
|
|
15456
|
+
* 폴더 페이지의 폴더 첨부를 그 폴더에 받는다 — 올린 이름 그대로. Notion 파일 id 가 적어 둔 것과 같으면(올린 볼트 · 앞서
|
|
15457
|
+
* 받은 볼트) 받지 않는다. Notion 에서 바뀐 파일은 볼트 쪽이 그대로일 때만 덮는다 — 볼트에서도 고쳤으면 두고 알린다(다음
|
|
15458
|
+
* push 가 볼트의 것을 올린다).
|
|
15459
|
+
*
|
|
15460
|
+
* @param complete 본문을 끝까지 받았나 — 그럴 때만 Notion 에서 지운 파일을 볼트에서 지운다(deleteSync). 잘린 본문에 없는
|
|
15461
|
+
* 파일은 지운 것이 아니다.
|
|
15462
|
+
* @returns 새로 받거나 바꾼 파일 수
|
|
15463
|
+
*/
|
|
15464
|
+
async receiveFolderFiles(folder, pageId, files, complete2 = false) {
|
|
15465
|
+
const known = new Map(
|
|
15466
|
+
this.stateDb.getFilesByPageId(pageId).map((entry) => [entry.localPath, entry])
|
|
15467
|
+
);
|
|
15468
|
+
const ids = folderFileIdsOf(this.stateDb, pageId);
|
|
15469
|
+
let received = 0;
|
|
15470
|
+
for (const file of files) {
|
|
15471
|
+
const localPath = folder ? `${folder}/${file.name}` : file.name;
|
|
15472
|
+
const fileId = notionFileIdOf(file.url);
|
|
15473
|
+
const entry = known.get(localPath);
|
|
15474
|
+
if (entry && fileId && ids.get(localPath) === fileId) continue;
|
|
15475
|
+
if (entry && fileId && !ids.has(localPath)) {
|
|
15476
|
+
rememberFolderFileId(this.stateDb, pageId, localPath, fileId);
|
|
15190
15477
|
continue;
|
|
15191
15478
|
}
|
|
15192
|
-
|
|
15193
|
-
|
|
15194
|
-
|
|
15479
|
+
if (entry && !fileId) continue;
|
|
15480
|
+
await this.sema.acquire();
|
|
15481
|
+
try {
|
|
15482
|
+
if (await this.receiveOne(localPath, folder, pageId, file, entry ?? null)) received++;
|
|
15483
|
+
if (fileId) rememberFolderFileId(this.stateDb, pageId, localPath, fileId);
|
|
15484
|
+
} catch (error) {
|
|
15485
|
+
getLogger().warn(
|
|
15486
|
+
`[Im-Nobsidian] \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBC1B\uC9C0 \uBABB\uD568 (${localPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
15487
|
+
);
|
|
15488
|
+
} finally {
|
|
15489
|
+
this.sema.release();
|
|
15195
15490
|
}
|
|
15196
15491
|
}
|
|
15197
|
-
|
|
15492
|
+
if (complete2 && this.deleteSync) {
|
|
15493
|
+
const names = new Set(files.map((file) => file.name));
|
|
15494
|
+
for (const entry of known.values()) {
|
|
15495
|
+
const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
|
|
15496
|
+
if (getFolderPath(entry.localPath) !== folder || names.has(name)) continue;
|
|
15497
|
+
if (!ids.has(entry.localPath)) continue;
|
|
15498
|
+
await this.removeLocalFolderFile(pageId, entry);
|
|
15499
|
+
}
|
|
15500
|
+
}
|
|
15501
|
+
return received;
|
|
15502
|
+
}
|
|
15503
|
+
/**
|
|
15504
|
+
* Notion 에서 지운 폴더 첨부를 볼트에서 지운다 — 지난 동기화 그대로일 때만. 볼트에서 고쳤으면 두고 기록만 놓는다 — 다음 push
|
|
15505
|
+
* 가 다시 올린다(노트의 원격 삭제와 같은 규칙).
|
|
15506
|
+
*/
|
|
15507
|
+
async removeLocalFolderFile(pageId, entry) {
|
|
15508
|
+
try {
|
|
15509
|
+
if (await this.vaultFs.exists(entry.localPath)) {
|
|
15510
|
+
const local = createHash("sha256").update(await this.vaultFs.readBinary(entry.localPath)).digest("hex");
|
|
15511
|
+
if (local === entry.fileHash) {
|
|
15512
|
+
await this.vaultFs.deleteFile(entry.localPath);
|
|
15513
|
+
getLogger().info(
|
|
15514
|
+
`[Im-Nobsidian] Notion \uC5D0\uC11C \uC9C0\uC6B4 \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC9C0\uC6C0: ${entry.localPath}`
|
|
15515
|
+
);
|
|
15516
|
+
} else {
|
|
15517
|
+
getLogger().warn(
|
|
15518
|
+
`[Im-Nobsidian] Notion \uC5D0\uC11C \uC9C0\uC6B4 \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uACE0\uCCD0 \uB450\uC5C8\uC74C \u2014 \uB2E4\uC74C push \uAC00 \uB2E4\uC2DC \uC62C\uB9B0\uB2E4: ${entry.localPath}`
|
|
15519
|
+
);
|
|
15520
|
+
}
|
|
15521
|
+
}
|
|
15522
|
+
this.stateDb.deleteFileRegistry(entry.localPath);
|
|
15523
|
+
forgetFolderFileId(this.stateDb, pageId, entry.localPath);
|
|
15524
|
+
} catch (error) {
|
|
15525
|
+
getLogger().warn(
|
|
15526
|
+
`[Im-Nobsidian] Notion \uC5D0\uC11C \uC9C0\uC6B4 \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC9C0\uC6B0\uC9C0 \uBABB\uD568 (${entry.localPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
15527
|
+
);
|
|
15528
|
+
}
|
|
15529
|
+
}
|
|
15530
|
+
/**
|
|
15531
|
+
* 폴더 첨부 하나를 받는다. 볼트에 같은 경로가 있으면: 같은 내용이면 적기만 하고, 지난 동기화 뒤 볼트에서 그대로인 파일은
|
|
15532
|
+
* 덮고(`entry`), 그 밖(볼트에서 고친 것 · 처음 보는 다른 파일)은 두고 알린다.
|
|
15533
|
+
*
|
|
15534
|
+
* @returns 볼트에 썼는가
|
|
15535
|
+
*/
|
|
15536
|
+
async receiveOne(localPath, folder, pageId, file, entry) {
|
|
15537
|
+
const response = await fetchForDownload(this.fetchFn, file.url, this.downloadTimeoutMs);
|
|
15538
|
+
if (!response.ok) {
|
|
15539
|
+
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
15540
|
+
}
|
|
15541
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
15542
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
15543
|
+
let wrote = false;
|
|
15544
|
+
if (await this.vaultFs.exists(localPath)) {
|
|
15545
|
+
const localHash = createHash("sha256").update(await this.vaultFs.readBinary(localPath)).digest("hex");
|
|
15546
|
+
if (localHash !== hash) {
|
|
15547
|
+
if (entry?.fileHash !== localHash) {
|
|
15548
|
+
getLogger().warn(
|
|
15549
|
+
`[Im-Nobsidian] \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBC1B\uC9C0 \uC54A\uC74C \u2014 \uBCFC\uD2B8\uC758 ${localPath} \uAC00 Notion \uC758 \uAC83\uACFC \uB2E4\uB974\uB2E4${entry ? "(\uC591\uCABD\uC5D0\uC11C \uACE0\uCE68 \u2014 \uB2E4\uC74C push \uAC00 \uBCFC\uD2B8\uC758 \uAC83\uC744 \uC62C\uB9B0\uB2E4)" : "(\uAC19\uC740 \uC774\uB984\uC758 \uB2E4\uB978 \uD30C\uC77C)"}`
|
|
15550
|
+
);
|
|
15551
|
+
return false;
|
|
15552
|
+
}
|
|
15553
|
+
await this.vaultFs.writeBinary(localPath, buffer);
|
|
15554
|
+
wrote = true;
|
|
15555
|
+
}
|
|
15556
|
+
} else {
|
|
15557
|
+
await this.vaultFs.ensureFolder(folder || ".");
|
|
15558
|
+
await this.vaultFs.writeBinary(localPath, buffer);
|
|
15559
|
+
wrote = true;
|
|
15560
|
+
}
|
|
15561
|
+
this.stateDb.registerFile({
|
|
15562
|
+
localPath,
|
|
15563
|
+
notionPageId: pageId,
|
|
15564
|
+
fileUploadId: "",
|
|
15565
|
+
fileType: getBlockType(file.name),
|
|
15566
|
+
fileHash: hash,
|
|
15567
|
+
fileSize: buffer.length
|
|
15568
|
+
});
|
|
15569
|
+
return wrote;
|
|
15198
15570
|
}
|
|
15199
15571
|
async downloadFileBlock(url, filename, targetFolder) {
|
|
15200
15572
|
const response = await fetchForDownload(this.fetchFn, url, this.downloadTimeoutMs);
|
|
@@ -15225,7 +15597,7 @@ var FileHandler = class {
|
|
|
15225
15597
|
}
|
|
15226
15598
|
return results;
|
|
15227
15599
|
}
|
|
15228
|
-
async uploadFile(file, parentPageId) {
|
|
15600
|
+
async uploadFile(file, parentPageId, previous) {
|
|
15229
15601
|
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15230
15602
|
const filename = file.path.split("/").pop() ?? "file";
|
|
15231
15603
|
const contentType = getMimeType(filename);
|
|
@@ -15233,8 +15605,12 @@ var FileHandler = class {
|
|
|
15233
15605
|
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
15234
15606
|
const blob = new Blob([buffer], { type: contentType });
|
|
15235
15607
|
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
15236
|
-
const block = this.createFileBlock(blockType, fileUploadId);
|
|
15237
|
-
await this.notionClient.
|
|
15608
|
+
const block = this.createFileBlock(blockType, fileUploadId, filename);
|
|
15609
|
+
const [created] = await this.notionClient.appendChildBlocks(parentPageId, [block]);
|
|
15610
|
+
if (previous) await this.removeOlderBlocks(parentPageId, filename, created?.id);
|
|
15611
|
+
const url = created ? fileUrlOfBlock(created) : null;
|
|
15612
|
+
const fileId = url ? notionFileIdOf(url) : null;
|
|
15613
|
+
if (fileId) rememberFolderFileId(this.stateDb, parentPageId, file.path, fileId);
|
|
15238
15614
|
this.stateDb.registerFile({
|
|
15239
15615
|
localPath: file.path,
|
|
15240
15616
|
notionPageId: parentPageId,
|
|
@@ -15246,10 +15622,25 @@ var FileHandler = class {
|
|
|
15246
15622
|
getLogger().info(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC: ${file.path} \u2192 ${blockType} \uBE14\uB85D`);
|
|
15247
15623
|
return { localPath: file.path, fileUploadId, blockType };
|
|
15248
15624
|
}
|
|
15249
|
-
|
|
15625
|
+
/** 이 페이지에서 같은 이름의 파일 블록 중 방금 붙인 것 말고를 지운다. */
|
|
15626
|
+
async removeOlderBlocks(pageId, name, keep) {
|
|
15627
|
+
for (const block of await this.notionClient.fetchAllChildren(pageId)) {
|
|
15628
|
+
if (block.id === keep) continue;
|
|
15629
|
+
const media = block[block.type];
|
|
15630
|
+
const url = media?.type === "file" ? media.file?.url : void 0;
|
|
15631
|
+
if (!url || uploadedFileNameFromUrl(url) !== name) continue;
|
|
15632
|
+
await this.notionClient.deleteBlock(block.id);
|
|
15633
|
+
}
|
|
15634
|
+
}
|
|
15635
|
+
/**
|
|
15636
|
+
* 폴더 첨부의 파일 블록 — 캡션에 파일 이름을 적는다. 받는 쪽이 그 표시로 폴더 첨부를 알아본다({@link folderFilesOf}) —
|
|
15637
|
+
* 자식 페이지가 없는 폴더 페이지는 PDF 하나만 붙인 노트와 모양이 같다.
|
|
15638
|
+
*/
|
|
15639
|
+
createFileBlock(blockType, fileUploadId, name) {
|
|
15250
15640
|
const fileRef = {
|
|
15251
15641
|
type: "file_upload",
|
|
15252
|
-
file_upload: { id: fileUploadId }
|
|
15642
|
+
file_upload: { id: fileUploadId },
|
|
15643
|
+
caption: [{ type: "text", text: { content: name } }]
|
|
15253
15644
|
};
|
|
15254
15645
|
switch (blockType) {
|
|
15255
15646
|
case "image":
|
|
@@ -19114,7 +19505,7 @@ var PageDecorations = class {
|
|
|
19114
19505
|
}
|
|
19115
19506
|
case "local":
|
|
19116
19507
|
try {
|
|
19117
|
-
const uploaded = await this.imageHandler.uploadLocalImage(source.target,
|
|
19508
|
+
const uploaded = await this.imageHandler.uploadLocalImage(source.target, folderOf3(path));
|
|
19118
19509
|
return { type: "file_upload", file_upload: { id: uploaded.fileUploadId } };
|
|
19119
19510
|
} catch (error) {
|
|
19120
19511
|
getLogger().warn(
|
|
@@ -19159,7 +19550,7 @@ function sameText(a, b) {
|
|
|
19159
19550
|
if (isEmptyFrontmatterValue(before) && isEmptyFrontmatterValue(after)) return true;
|
|
19160
19551
|
return String(before) === String(after);
|
|
19161
19552
|
}
|
|
19162
|
-
function
|
|
19553
|
+
function folderOf3(notePath) {
|
|
19163
19554
|
return notePath.includes("/") ? notePath.slice(0, notePath.lastIndexOf("/")) : void 0;
|
|
19164
19555
|
}
|
|
19165
19556
|
|
|
@@ -20963,7 +21354,7 @@ async function verifyPageCompleteness(remote, local, rootPageId) {
|
|
|
20963
21354
|
var ID_SUFFIX = new RegExp(
|
|
20964
21355
|
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((len) => `[0-9a-f]{${len}}`).join("|")})\\)$`
|
|
20965
21356
|
);
|
|
20966
|
-
function
|
|
21357
|
+
function folderOf4(record) {
|
|
20967
21358
|
if (record.fileType === "db-row") return null;
|
|
20968
21359
|
if (isFolderRecord(record)) return record.obsidianPath;
|
|
20969
21360
|
const slash = record.obsidianPath.lastIndexOf("/");
|
|
@@ -20980,10 +21371,10 @@ function findNestedFolderPages(records) {
|
|
|
20980
21371
|
const found2 = [];
|
|
20981
21372
|
for (const inner of records) {
|
|
20982
21373
|
if (inner.notionPageId === null || inner.notionParentId === null) continue;
|
|
20983
|
-
const folder =
|
|
21374
|
+
const folder = folderOf4(inner);
|
|
20984
21375
|
if (folder === null) continue;
|
|
20985
21376
|
const outer = byPage.get(compactNotionId(inner.notionParentId));
|
|
20986
|
-
if (!outer || outer === inner ||
|
|
21377
|
+
if (!outer || outer === inner || folderOf4(outer) !== folder) continue;
|
|
20987
21378
|
found2.push({
|
|
20988
21379
|
folder,
|
|
20989
21380
|
outer: { path: outer.obsidianPath, notionPageId: outer.notionPageId },
|
|
@@ -21358,7 +21749,7 @@ var VaultSchemaRenames = class {
|
|
|
21358
21749
|
const meta = await this.notionClient.getDatabaseMeta(databaseId);
|
|
21359
21750
|
for (const [from, to] of pairs) {
|
|
21360
21751
|
const carrying = rows.filter((row) => Object.hasOwn(row.current, to));
|
|
21361
|
-
const folder =
|
|
21752
|
+
const folder = folderOf5(carrying[0].record.obsidianPath);
|
|
21362
21753
|
const owners = meta.dataSources.filter(
|
|
21363
21754
|
(ds) => ds.properties?.[from] !== void 0
|
|
21364
21755
|
);
|
|
@@ -21524,7 +21915,7 @@ function sameRow(a, b) {
|
|
|
21524
21915
|
const keys = Object.keys(left.data);
|
|
21525
21916
|
return left.content === right.content && keys.length === Object.keys(right.data).length && keys.every((key) => sameFrontmatterValue(left.data[key], right.data[key]));
|
|
21526
21917
|
}
|
|
21527
|
-
function
|
|
21918
|
+
function folderOf5(path) {
|
|
21528
21919
|
const at = path.lastIndexOf("/");
|
|
21529
21920
|
return at === -1 ? "" : path.slice(0, at);
|
|
21530
21921
|
}
|
|
@@ -24450,7 +24841,7 @@ function rewriteTarget(target, source, context, kind) {
|
|
|
24450
24841
|
const shown = keepExtension ? destination : destination.slice(0, -".md".length);
|
|
24451
24842
|
let next;
|
|
24452
24843
|
if (resolved.relative) {
|
|
24453
|
-
next = relativePath(
|
|
24844
|
+
next = relativePath(folderOf6(source.to), shown);
|
|
24454
24845
|
if (!linkpath.startsWith("./") && next.startsWith("./")) next = next.slice(2);
|
|
24455
24846
|
} else if (style === "name") {
|
|
24456
24847
|
const name = baseName3(shown);
|
|
@@ -24472,14 +24863,14 @@ function resolveLinkpath(linkpath, sourcePath, files) {
|
|
|
24472
24863
|
const matches = byName(files, baseName3(candidate)).filter((file) => file.endsWith(suffix));
|
|
24473
24864
|
if (matches.length === 1) return found(matches[0], linkpath);
|
|
24474
24865
|
if (matches.length > 1) {
|
|
24475
|
-
const local = `${folderPrefix(
|
|
24866
|
+
const local = `${folderPrefix(folderOf6(sourcePath))}${candidate}`;
|
|
24476
24867
|
return matches.includes(local) ? found(local, linkpath) : null;
|
|
24477
24868
|
}
|
|
24478
24869
|
}
|
|
24479
24870
|
return null;
|
|
24480
24871
|
}
|
|
24481
24872
|
function resolveRelative(linkpath, sourcePath, files) {
|
|
24482
|
-
const joined = normalizeSegments(`${folderPrefix(
|
|
24873
|
+
const joined = normalizeSegments(`${folderPrefix(folderOf6(sourcePath))}${linkpath}`);
|
|
24483
24874
|
if (joined === null) return null;
|
|
24484
24875
|
for (const candidate of withExtension(joined)) {
|
|
24485
24876
|
if (files.has(candidate)) return { ...found(candidate, linkpath), relative: true };
|
|
@@ -24538,7 +24929,7 @@ function relativePath(from, to) {
|
|
|
24538
24929
|
function encodeMarkdownTarget(next, original) {
|
|
24539
24930
|
return original.includes("%") || next.includes(" ") ? next.replace(/ /g, "%20") : next;
|
|
24540
24931
|
}
|
|
24541
|
-
function
|
|
24932
|
+
function folderOf6(path) {
|
|
24542
24933
|
const slash = path.lastIndexOf("/");
|
|
24543
24934
|
return slash === -1 ? "" : path.slice(0, slash);
|
|
24544
24935
|
}
|
|
@@ -26841,7 +27232,7 @@ async function resolveBlockToPageId(notionClient, blockId) {
|
|
|
26841
27232
|
|
|
26842
27233
|
// src/sync/page-puller.ts
|
|
26843
27234
|
var PagePuller = class {
|
|
26844
|
-
constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler, propertyMapper, databaseSyncer, observation, drift, placement, detector, discovery) {
|
|
27235
|
+
constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler, propertyMapper, databaseSyncer, observation, drift, placement, detector, discovery, fileHandler) {
|
|
26845
27236
|
this.config = config;
|
|
26846
27237
|
this.stateDb = stateDb;
|
|
26847
27238
|
this.notionClient = notionClient;
|
|
@@ -26855,6 +27246,7 @@ var PagePuller = class {
|
|
|
26855
27246
|
this.placement = placement;
|
|
26856
27247
|
this.detector = detector;
|
|
26857
27248
|
this.discovery = discovery;
|
|
27249
|
+
this.fileHandler = fileHandler;
|
|
26858
27250
|
}
|
|
26859
27251
|
config;
|
|
26860
27252
|
stateDb;
|
|
@@ -26869,6 +27261,7 @@ var PagePuller = class {
|
|
|
26869
27261
|
placement;
|
|
26870
27262
|
detector;
|
|
26871
27263
|
discovery;
|
|
27264
|
+
fileHandler;
|
|
26872
27265
|
dbSchemaLoaded = false;
|
|
26873
27266
|
_pullImageCount = 0;
|
|
26874
27267
|
_pullFileCount = 0;
|
|
@@ -27025,8 +27418,17 @@ var PagePuller = class {
|
|
|
27025
27418
|
if (excluded !== null) return { path: excluded, excluded: true };
|
|
27026
27419
|
const fetched = await this.fetchPageMarkdown(pageId);
|
|
27027
27420
|
const hasChildPages = this.hasChildContainers(pageId, fetched.children);
|
|
27028
|
-
|
|
27029
|
-
|
|
27421
|
+
const folderFiles = fetched.folderFiles && (hasChildPages || fetched.folderFiles.named) ? fetched.folderFiles.files : null;
|
|
27422
|
+
if (hasChildPages && !fetched.hasBody || folderFiles) {
|
|
27423
|
+
const created = await this.createFolderOnly(
|
|
27424
|
+
pageId,
|
|
27425
|
+
page,
|
|
27426
|
+
fetched.fingerprint,
|
|
27427
|
+
parentPath,
|
|
27428
|
+
safeName
|
|
27429
|
+
);
|
|
27430
|
+
if (folderFiles) await this.receiveFolderFiles(created.path, pageId, folderFiles);
|
|
27431
|
+
return created;
|
|
27030
27432
|
}
|
|
27031
27433
|
const fileType = hasChildPages ? "folder-note" : "file";
|
|
27032
27434
|
const noteDir = hasChildPages ? parentPath ? `${parentPath}/${safeName}` : safeName : parentPath;
|
|
@@ -27489,7 +27891,8 @@ var PagePuller = class {
|
|
|
27489
27891
|
compact: isCompactExport(result.markdown),
|
|
27490
27892
|
children: extractChildTags(result.markdown),
|
|
27491
27893
|
fingerprint: remoteBodyFingerprint(result.markdown),
|
|
27492
|
-
hasBody: hasBodyBesidesChildren(result.markdown)
|
|
27894
|
+
hasBody: hasBodyBesidesChildren(result.markdown),
|
|
27895
|
+
folderFiles: folderFilesInBody(result.markdown)
|
|
27493
27896
|
};
|
|
27494
27897
|
} catch (error) {
|
|
27495
27898
|
getLogger().warn(
|
|
@@ -27508,9 +27911,34 @@ var PagePuller = class {
|
|
|
27508
27911
|
compact: isCompactExport(fallback.markdown),
|
|
27509
27912
|
children: extractChildTags(fallback.markdown),
|
|
27510
27913
|
fingerprint: null,
|
|
27511
|
-
hasBody: hasBodyBesidesChildren(fallback.markdown)
|
|
27914
|
+
hasBody: hasBodyBesidesChildren(fallback.markdown),
|
|
27915
|
+
folderFiles: folderFilesInBody(fallback.markdown)
|
|
27512
27916
|
};
|
|
27513
27917
|
}
|
|
27918
|
+
/** 폴더 첨부를 받아 받은 파일 수에 더한다. */
|
|
27919
|
+
async receiveFolderFiles(folder, pageId, files, complete2 = false) {
|
|
27920
|
+
this._pullFileCount += await this.fileHandler.receiveFolderFiles(
|
|
27921
|
+
folder,
|
|
27922
|
+
pageId,
|
|
27923
|
+
files,
|
|
27924
|
+
complete2
|
|
27925
|
+
);
|
|
27926
|
+
}
|
|
27927
|
+
/**
|
|
27928
|
+
* 이 폴더 레코드의 Notion 페이지에서 받을 본문이 있나 — dry-run 이 실제 pull({@link pullFolderRecord})과 같게 센다.
|
|
27929
|
+
* 본문이 하위 페이지뿐이면 실제 pull 은 본 것만 적고 고친 것으로 세지 않는다.
|
|
27930
|
+
*/
|
|
27931
|
+
async folderPageHasBody(record) {
|
|
27932
|
+
const page = await this.notionClient.getPage(record.notionPageId);
|
|
27933
|
+
if (isTrashedOrArchived(page)) return false;
|
|
27934
|
+
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
27935
|
+
return hasBodyBesidesChildren(markdown) && folderFilesOf(markdown, false) === null && this.folderNoteRefusal(record.obsidianPath) === null;
|
|
27936
|
+
}
|
|
27937
|
+
/** 이 폴더에 폴더 노트를 둘 수 없는 까닭 — 둘 수 있으면 null. */
|
|
27938
|
+
folderNoteRefusal(folder) {
|
|
27939
|
+
if (isDatabaseMode(this.config)) return "DB \uBAA8\uB4DC\uB294 \uD3F4\uB354 \uB178\uD2B8\uB97C \uBC1B\uC9C0 \uC54A\uC74C";
|
|
27940
|
+
return this.placement.folderLookup().databaseAt(folder) ? "\uADF8 \uD3F4\uB354\uB294 DB \uB77C \uD3F4\uB354 \uB178\uD2B8\uB97C \uB458 \uC218 \uC5C6\uC74C" : null;
|
|
27941
|
+
}
|
|
27514
27942
|
/**
|
|
27515
27943
|
* push 가 만든 폴더 페이지(폴더 레코드)의 원격 변경을 받는다(S-17). 폴더 레코드에는 볼트 파일이
|
|
27516
27944
|
* 없다 — 예전에는 폴더 경로를 파일로 읽어 «빈 로컬 파일» 과 원격 본문의 충돌로 남겼다.
|
|
@@ -27527,26 +27955,18 @@ var PagePuller = class {
|
|
|
27527
27955
|
* @throws 받을 자리가 없을 때 — 이유와 함께 실패로 남고, 다음 pull 이 다시 본다(기준 시각이
|
|
27528
27956
|
* 이 변경 앞에 묶인다).
|
|
27529
27957
|
*/
|
|
27530
|
-
/**
|
|
27531
|
-
* 이 폴더 레코드의 Notion 페이지에서 받을 본문이 있나 — dry-run 이 실제 pull({@link pullFolderRecord})과 같게 센다.
|
|
27532
|
-
* 본문이 하위 페이지뿐이면 실제 pull 은 본 것만 적고 고친 것으로 세지 않는다.
|
|
27533
|
-
*/
|
|
27534
|
-
async folderPageHasBody(record) {
|
|
27535
|
-
const page = await this.notionClient.getPage(record.notionPageId);
|
|
27536
|
-
if (isTrashedOrArchived(page)) return false;
|
|
27537
|
-
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
27538
|
-
return hasBodyBesidesChildren(markdown) && this.folderNoteRefusal(record.obsidianPath) === null;
|
|
27539
|
-
}
|
|
27540
|
-
/** 이 폴더에 폴더 노트를 둘 수 없는 까닭 — 둘 수 있으면 null. */
|
|
27541
|
-
folderNoteRefusal(folder) {
|
|
27542
|
-
if (isDatabaseMode(this.config)) return "DB \uBAA8\uB4DC\uB294 \uD3F4\uB354 \uB178\uD2B8\uB97C \uBC1B\uC9C0 \uC54A\uC74C";
|
|
27543
|
-
return this.placement.folderLookup().databaseAt(folder) ? "\uADF8 \uD3F4\uB354\uB294 DB \uB77C \uD3F4\uB354 \uB178\uD2B8\uB97C \uB458 \uC218 \uC5C6\uC74C" : null;
|
|
27544
|
-
}
|
|
27545
27958
|
async pullFolderRecord(record, page) {
|
|
27546
27959
|
const folder = record.obsidianPath;
|
|
27547
27960
|
if (isTrashedOrArchived(page)) return null;
|
|
27548
|
-
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
27961
|
+
const { markdown, truncated } = await this.notionClient.getPageMarkdown(page.id);
|
|
27549
27962
|
if (!hasBodyBesidesChildren(markdown)) {
|
|
27963
|
+
await this.receiveFolderFiles(folder, page.id, [], !truncated);
|
|
27964
|
+
this.observation.record(record.id, page, remoteBodyFingerprint(markdown));
|
|
27965
|
+
return null;
|
|
27966
|
+
}
|
|
27967
|
+
const files = folderFilesOf(markdown, false);
|
|
27968
|
+
if (files) {
|
|
27969
|
+
await this.receiveFolderFiles(folder, page.id, files, !truncated);
|
|
27550
27970
|
this.observation.record(record.id, page, remoteBodyFingerprint(markdown));
|
|
27551
27971
|
return null;
|
|
27552
27972
|
}
|
|
@@ -27735,6 +28155,10 @@ var PagePuller = class {
|
|
|
27735
28155
|
this.dbSchemaLoaded = true;
|
|
27736
28156
|
}
|
|
27737
28157
|
};
|
|
28158
|
+
function folderFilesInBody(markdown) {
|
|
28159
|
+
const files = folderFilesOf(markdown, false);
|
|
28160
|
+
return files ? { files, named: folderFilesOf(markdown, true) !== null } : null;
|
|
28161
|
+
}
|
|
27738
28162
|
|
|
27739
28163
|
// src/sync/frontmatter-link-resolver.ts
|
|
27740
28164
|
function resolveFrontmatterRelations(content, idToTitle) {
|
|
@@ -28844,7 +29268,9 @@ var SyncOrchestrator = class {
|
|
|
28844
29268
|
{
|
|
28845
29269
|
fetch: customFetch,
|
|
28846
29270
|
downloadTimeoutMs: config.advanced.mediaDownloadTimeoutMs,
|
|
28847
|
-
itemTimeoutMs: config.advanced.itemTimeoutMs
|
|
29271
|
+
itemTimeoutMs: config.advanced.itemTimeoutMs,
|
|
29272
|
+
attachmentsFolder: config.paths.attachments,
|
|
29273
|
+
deleteSync: config.sync.deleteSync
|
|
28848
29274
|
}
|
|
28849
29275
|
);
|
|
28850
29276
|
const propertyMapper = PropertyMapper.fromConfig(config);
|
|
@@ -28954,7 +29380,8 @@ var SyncOrchestrator = class {
|
|
|
28954
29380
|
this.drift,
|
|
28955
29381
|
this.placement,
|
|
28956
29382
|
this.detector,
|
|
28957
|
-
this.discovery
|
|
29383
|
+
this.discovery,
|
|
29384
|
+
this.fileHandler
|
|
28958
29385
|
);
|
|
28959
29386
|
this.pullPlanner = new PullPlanner(
|
|
28960
29387
|
config,
|
|
@@ -29074,14 +29501,16 @@ var SyncOrchestrator = class {
|
|
|
29074
29501
|
* 있든 없든 push 마다 부른다 — 바뀌지 않은 첨부는 파일 기록이 건너뛴다.
|
|
29075
29502
|
*/
|
|
29076
29503
|
async pushVaultFiles(failed) {
|
|
29077
|
-
if (this.config.sync.syncFiles === false) return;
|
|
29504
|
+
if (this.config.sync.syncFiles === false) return 0;
|
|
29078
29505
|
try {
|
|
29079
|
-
const fileResults = await this.fileHandler.pushAllFiles(failed);
|
|
29506
|
+
const fileResults = await this.fileHandler.pushAllFiles(failed, this.placement);
|
|
29080
29507
|
if (fileResults.length > 0) {
|
|
29081
29508
|
getLogger().info(`[Im-Nobsidian] ${fileResults.length}\uAC1C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC`);
|
|
29082
29509
|
}
|
|
29510
|
+
return fileResults.length;
|
|
29083
29511
|
} catch (error) {
|
|
29084
29512
|
getLogger().warn("[Im-Nobsidian] \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC911 \uC624\uB958:", error);
|
|
29513
|
+
return 0;
|
|
29085
29514
|
}
|
|
29086
29515
|
}
|
|
29087
29516
|
/**
|
|
@@ -29170,12 +29599,13 @@ var SyncOrchestrator = class {
|
|
|
29170
29599
|
...viewPlan.refused,
|
|
29171
29600
|
...viewed2.failed
|
|
29172
29601
|
];
|
|
29173
|
-
|
|
29602
|
+
const uploadedFiles2 = !options?.dryRun && !options?.signal?.aborted ? await this.pushVaultFiles(failed2) : 0;
|
|
29174
29603
|
return {
|
|
29175
29604
|
created: 0,
|
|
29176
29605
|
updated: 0,
|
|
29177
29606
|
deleted: 0,
|
|
29178
29607
|
moved: 0,
|
|
29608
|
+
...uploadedFiles2 > 0 ? { uploadedFiles: uploadedFiles2 } : {},
|
|
29179
29609
|
failed: failed2,
|
|
29180
29610
|
duration: Date.now() - startTime,
|
|
29181
29611
|
...edited2.applied.length > 0 ? { schemaEdits: edited2.applied } : {},
|
|
@@ -29454,7 +29884,7 @@ var SyncOrchestrator = class {
|
|
|
29454
29884
|
signal: options?.signal
|
|
29455
29885
|
});
|
|
29456
29886
|
}
|
|
29457
|
-
await this.pushVaultFiles(failed);
|
|
29887
|
+
const uploadedFiles = await this.pushVaultFiles(failed);
|
|
29458
29888
|
this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
29459
29889
|
this.stateDb.setMeta(LAST_SYNC_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
29460
29890
|
this.stateDb.setMeta(PUSH_IN_PROGRESS_META_KEY, "");
|
|
@@ -29466,6 +29896,7 @@ var SyncOrchestrator = class {
|
|
|
29466
29896
|
updated: counts.updated,
|
|
29467
29897
|
deleted: counts.deleted,
|
|
29468
29898
|
moved: counts.moved,
|
|
29899
|
+
...uploadedFiles > 0 ? { uploadedFiles } : {},
|
|
29469
29900
|
failed,
|
|
29470
29901
|
duration: Date.now() - startTime,
|
|
29471
29902
|
readOnlySkipped: this.pusher.takeReadOnlySkips(),
|