@im-nobsidian/core 0.17.0 → 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 +89 -30
- package/dist/index.js +558 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -196,9 +196,7 @@ var DEFAULT_TITLE_PROPERTY = "Name";
|
|
|
196
196
|
var DatabaseSyncSchema = z.object({
|
|
197
197
|
databaseId: z.string(),
|
|
198
198
|
localFolder: z.string(),
|
|
199
|
-
|
|
200
|
-
pullFilter: z.unknown().optional(),
|
|
201
|
-
titleProperty: z.string().default(DEFAULT_TITLE_PROPERTY)
|
|
199
|
+
pullFilter: z.unknown().optional()
|
|
202
200
|
});
|
|
203
201
|
var DEFAULTS = {
|
|
204
202
|
version: 1,
|
|
@@ -268,12 +266,36 @@ var RETIRED_SETTINGS = [
|
|
|
268
266
|
note: "\uAEBC\uB3C4 \uBC14\uB010 \uAC83\uC774 \uC5C6\uC5C8\uC2B5\uB2C8\uB2E4 \u2014 frontmatter \uB294 \uB298 Notion \uC18D\uC131\uC73C\uB85C \uC8FC\uACE0\uBC1B\uC2B5\uB2C8\uB2E4"
|
|
269
267
|
}
|
|
270
268
|
];
|
|
269
|
+
var RETIRED_DATABASE_SETTINGS = [
|
|
270
|
+
{
|
|
271
|
+
key: "propertyMapping",
|
|
272
|
+
meant: (value) => value !== void 0,
|
|
273
|
+
note: "\uC5B4\uB514\uC5D0\uB3C4 \uC4F0\uC774\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uC18D\uC131\uC740 frontmatter \uC758 \uC774\uB984 \uADF8\uB300\uB85C Notion \uC18D\uC131\uACFC \uC9DD\uC9D3\uC2B5\uB2C8\uB2E4"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
key: "titleProperty",
|
|
277
|
+
meant: (value) => value !== void 0 && value !== DEFAULT_TITLE_PROPERTY,
|
|
278
|
+
note: "\uC5B4\uB514\uC5D0\uB3C4 \uC4F0\uC774\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uC81C\uBAA9 \uC18D\uC131\uC740 Notion DB \uC758 title \uC18D\uC131\uC744 \uC77D\uC5B4 \uCC3E\uC2B5\uB2C8\uB2E4"
|
|
279
|
+
}
|
|
280
|
+
];
|
|
271
281
|
function retiredSettings(raw) {
|
|
272
|
-
|
|
273
|
-
|
|
282
|
+
const root = raw;
|
|
283
|
+
const lines = RETIRED_SETTINGS.flatMap(({ path: [section, key], meant, note }) => {
|
|
284
|
+
const group2 = root?.[section];
|
|
274
285
|
const value = group2 && typeof group2 === "object" ? group2[key] : void 0;
|
|
275
286
|
return meant(value) ? [`${section}.${key}: ${note}`] : [];
|
|
276
287
|
});
|
|
288
|
+
const notion = root?.notion;
|
|
289
|
+
const databases = Array.isArray(notion?.databases) ? notion.databases : [];
|
|
290
|
+
databases.forEach((database, index) => {
|
|
291
|
+
if (!database || typeof database !== "object") return;
|
|
292
|
+
for (const { key, meant, note } of RETIRED_DATABASE_SETTINGS) {
|
|
293
|
+
if (meant(database[key])) {
|
|
294
|
+
lines.push(`notion.databases[${index}].${key}: ${note}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
return lines;
|
|
277
299
|
}
|
|
278
300
|
var ConfigSchema = z.object({
|
|
279
301
|
version: z.literal(1),
|
|
@@ -12875,6 +12897,76 @@ function createDefaultPipeline(options) {
|
|
|
12875
12897
|
return pipeline;
|
|
12876
12898
|
}
|
|
12877
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
|
+
|
|
12878
12970
|
// src/utils/mime.ts
|
|
12879
12971
|
var EXTENSION_TO_BLOCK_TYPE = {
|
|
12880
12972
|
".png": "image",
|
|
@@ -12996,11 +13088,11 @@ function getMimeType(filename) {
|
|
|
12996
13088
|
}
|
|
12997
13089
|
|
|
12998
13090
|
// src/sync/uploaded-media.ts
|
|
12999
|
-
function
|
|
13091
|
+
function keyOf2(pageId) {
|
|
13000
13092
|
return `uploaded_media:${compactNotionId(pageId)}`;
|
|
13001
13093
|
}
|
|
13002
13094
|
function rememberUploadedMedia(stateDb, pageId, media) {
|
|
13003
|
-
const key =
|
|
13095
|
+
const key = keyOf2(pageId);
|
|
13004
13096
|
if (media.length === 0 && stateDb.getMeta(key) === null) return;
|
|
13005
13097
|
stateDb.setMeta(key, JSON.stringify(media.map(rowOf)));
|
|
13006
13098
|
}
|
|
@@ -13008,14 +13100,14 @@ function rememberReceivedMedia(stateDb, pageId, media) {
|
|
|
13008
13100
|
if (media.length === 0) return;
|
|
13009
13101
|
const merged = recallUploadedMedia(stateDb, pageId);
|
|
13010
13102
|
for (const item of media) merged.set(item.fileId, item);
|
|
13011
|
-
stateDb.setMeta(
|
|
13103
|
+
stateDb.setMeta(keyOf2(pageId), JSON.stringify([...merged.values()].map(rowOf)));
|
|
13012
13104
|
}
|
|
13013
13105
|
function rowOf({ fileId, target, localPath, fileHash }) {
|
|
13014
13106
|
return fileHash === void 0 ? { fileId, target, localPath } : { fileId, target, localPath, fileHash };
|
|
13015
13107
|
}
|
|
13016
13108
|
function recallUploadedMedia(stateDb, pageId) {
|
|
13017
13109
|
const known = /* @__PURE__ */ new Map();
|
|
13018
|
-
const value = stateDb?.getMeta(
|
|
13110
|
+
const value = stateDb?.getMeta(keyOf2(pageId));
|
|
13019
13111
|
if (!value) return known;
|
|
13020
13112
|
try {
|
|
13021
13113
|
for (const row of JSON.parse(value)) {
|
|
@@ -13204,8 +13296,11 @@ function nameHashOf(fullHash) {
|
|
|
13204
13296
|
var IMAGE_STEM_MAX = 50;
|
|
13205
13297
|
function withoutOwnHash(name, hash) {
|
|
13206
13298
|
if (hash.length !== NAME_HASH_LENGTH || !/^[0-9a-f]+$/.test(hash)) return name;
|
|
13207
|
-
const
|
|
13208
|
-
|
|
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;
|
|
13209
13304
|
}
|
|
13210
13305
|
function imageStemOf(uploadedName, pageTitle) {
|
|
13211
13306
|
const stem = safeImageStem(uploadedName.replace(/\.[^.]*$/, ""));
|
|
@@ -13248,7 +13343,7 @@ function localEmbedTargets(note) {
|
|
|
13248
13343
|
}
|
|
13249
13344
|
return targets;
|
|
13250
13345
|
}
|
|
13251
|
-
function
|
|
13346
|
+
function folderOf2(notePath) {
|
|
13252
13347
|
return notePath?.includes("/") ? notePath.slice(0, notePath.lastIndexOf("/")) : void 0;
|
|
13253
13348
|
}
|
|
13254
13349
|
function messageOf2(error) {
|
|
@@ -13349,7 +13444,7 @@ var ImageHandler = class {
|
|
|
13349
13444
|
if (!target || /^https?:\/\//.test(target) || target.includes("\n")) return null;
|
|
13350
13445
|
const vaultPath = stripAlias2(target);
|
|
13351
13446
|
if (baseName(vaultPath) !== uploadedName) return null;
|
|
13352
|
-
for (const candidate of this.vaultCandidates(vaultPath, noteFolder)) {
|
|
13447
|
+
for await (const candidate of this.vaultCandidates(vaultPath, noteFolder)) {
|
|
13353
13448
|
try {
|
|
13354
13449
|
if (await this.vaultFs.exists(candidate)) return target;
|
|
13355
13450
|
} catch {
|
|
@@ -13389,7 +13484,7 @@ var ImageHandler = class {
|
|
|
13389
13484
|
const spans = notionMediaSpans(markdown);
|
|
13390
13485
|
if (spans.length === 0) return markdown;
|
|
13391
13486
|
const known = pageId ? recallUploadedMedia(this.stateDb, pageId) : /* @__PURE__ */ new Map();
|
|
13392
|
-
const noteFolder =
|
|
13487
|
+
const noteFolder = folderOf2(notePath);
|
|
13393
13488
|
let result = markdown;
|
|
13394
13489
|
for (const span of spans) {
|
|
13395
13490
|
const uploadedName = this.uploadedNameOf(span.url);
|
|
@@ -13419,7 +13514,7 @@ var ImageHandler = class {
|
|
|
13419
13514
|
return null;
|
|
13420
13515
|
}
|
|
13421
13516
|
for (const target of targets) {
|
|
13422
|
-
for (const candidate of this.vaultCandidates(stripAlias2(target), noteFolder)) {
|
|
13517
|
+
for await (const candidate of this.vaultCandidates(stripAlias2(target), noteFolder)) {
|
|
13423
13518
|
let localHash;
|
|
13424
13519
|
try {
|
|
13425
13520
|
localHash = sha256Of(await this.vaultFs.readBinary(candidate));
|
|
@@ -13562,7 +13657,7 @@ var ImageHandler = class {
|
|
|
13562
13657
|
* @returns 보일 이름은 받을 때 붙인 내용 해시를 뗀 것이다 — 다시 받으면 같은 이름 · 같은 경로가 된다.
|
|
13563
13658
|
*/
|
|
13564
13659
|
async uploadNoteAttachment(target, notePath) {
|
|
13565
|
-
const uploaded = await this.uploadLocalImage(target,
|
|
13660
|
+
const uploaded = await this.uploadLocalImage(target, folderOf2(notePath));
|
|
13566
13661
|
const base = uploaded.localPath.split("/").pop() ?? uploaded.localPath;
|
|
13567
13662
|
const name = withoutOwnHash(base, nameHashOf(uploaded.fileHash));
|
|
13568
13663
|
const extension = /\.[^./]+$/.exec(base)?.[0] ?? "";
|
|
@@ -13575,7 +13670,7 @@ var ImageHandler = class {
|
|
|
13575
13670
|
if (!this.notionClient) return [];
|
|
13576
13671
|
const localImages = images.filter((img) => !img.isExternal && img.localPath);
|
|
13577
13672
|
if (localImages.length === 0) return [];
|
|
13578
|
-
const noteFolder =
|
|
13673
|
+
const noteFolder = folderOf2(notePath);
|
|
13579
13674
|
const results = [];
|
|
13580
13675
|
const imageBlocks = [];
|
|
13581
13676
|
for (const img of localImages) {
|
|
@@ -13662,7 +13757,7 @@ var ImageHandler = class {
|
|
|
13662
13757
|
}
|
|
13663
13758
|
return segment;
|
|
13664
13759
|
});
|
|
13665
|
-
const noteFolder =
|
|
13760
|
+
const noteFolder = folderOf2(notePath);
|
|
13666
13761
|
const kept = [];
|
|
13667
13762
|
let result = "";
|
|
13668
13763
|
let cursor = 0;
|
|
@@ -13753,7 +13848,7 @@ var ImageHandler = class {
|
|
|
13753
13848
|
if (kept.length > 0) this.rememberUploads(pageId, kept);
|
|
13754
13849
|
return empty;
|
|
13755
13850
|
}
|
|
13756
|
-
const noteFolder =
|
|
13851
|
+
const noteFolder = folderOf2(notePath);
|
|
13757
13852
|
const uploaded = [];
|
|
13758
13853
|
const remembered = [...kept];
|
|
13759
13854
|
const unsupported2 = [];
|
|
@@ -13911,29 +14006,37 @@ var ImageHandler = class {
|
|
|
13911
14006
|
/**
|
|
13912
14007
|
* 임베드 경로가 가리킬 수 있는 볼트 실제 파일 후보를 우선순위대로 만든다(F23).
|
|
13913
14008
|
* Obsidian 위키링크 임베드는 노트 옆 파일을 파일명만으로 참조하는 게 관례라
|
|
13914
|
-
* 노트 폴더 → 볼트 루트 → attachments/ 순.
|
|
14009
|
+
* 노트 폴더 → 볼트 루트 → attachments/ 순. 세 곳에 없으면 볼트 어디서든 이름으로 찾는다({@link VaultFileIndex}).
|
|
13915
14010
|
*
|
|
13916
14011
|
* push(업로드)와 pull(원본 복원)이 **같은 사다리**를 봐야 왕복이 닫힌다. 예전에는
|
|
13917
14012
|
* pull 쪽이 볼트 루트만 확인해서, `__e2e_probe__/노트.md` 안의 `![[t-img.png]]` 는
|
|
13918
14013
|
* 올라갈 때는 노트 폴더에서 잘 찾아 올리고도 내려올 때는 원본을 못 찾아 사본을
|
|
13919
14014
|
* 새로 받았다(실측) — 왕복할 때마다 첨부가 한 벌씩 늘어난다.
|
|
13920
14015
|
*/
|
|
13921
|
-
vaultCandidates(localPath, noteFolder) {
|
|
13922
|
-
const
|
|
14016
|
+
async *vaultCandidates(localPath, noteFolder) {
|
|
14017
|
+
const ladder = [];
|
|
13923
14018
|
if (noteFolder && !localPath.includes("/")) {
|
|
13924
|
-
|
|
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;
|
|
13925
14025
|
}
|
|
13926
|
-
|
|
13927
|
-
|
|
14026
|
+
}
|
|
14027
|
+
indexOfFiles = null;
|
|
14028
|
+
/** 볼트의 첨부 이름표 — 처음 쓸 때 만든다. */
|
|
14029
|
+
get fileIndex() {
|
|
14030
|
+
this.indexOfFiles ??= new VaultFileIndex(this.vaultFs);
|
|
14031
|
+
return this.indexOfFiles;
|
|
13928
14032
|
}
|
|
13929
14033
|
/**
|
|
13930
14034
|
* 임베드 경로를 볼트 실제 파일로 해석해 읽는다(F23).
|
|
13931
14035
|
* 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
|
|
13932
14036
|
*/
|
|
13933
14037
|
async readImageFromVault(localPath, noteFolder) {
|
|
13934
|
-
const candidates = this.vaultCandidates(localPath, noteFolder);
|
|
13935
14038
|
let lastError;
|
|
13936
|
-
for (const candidate of
|
|
14039
|
+
for await (const candidate of this.vaultCandidates(localPath, noteFolder)) {
|
|
13937
14040
|
try {
|
|
13938
14041
|
return { buffer: await this.vaultFs.readBinary(candidate), resolvedPath: candidate };
|
|
13939
14042
|
} catch (error) {
|
|
@@ -14126,12 +14229,20 @@ var ImageHandler = class {
|
|
|
14126
14229
|
const hash = nameHashOf(fileHash);
|
|
14127
14230
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
14128
14231
|
const name = withoutOwnHash(caption, hash);
|
|
14129
|
-
const
|
|
14130
|
-
|
|
14131
|
-
|
|
14132
|
-
)
|
|
14133
|
-
|
|
14134
|
-
|
|
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
|
+
}
|
|
14135
14246
|
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
14136
14247
|
await this.vaultFs.writeBinary(localPath, buffer);
|
|
14137
14248
|
return { originalUrl: url, localPath, hash, size: buffer.length, fileHash, name };
|
|
@@ -14141,6 +14252,14 @@ var ImageHandler = class {
|
|
|
14141
14252
|
}
|
|
14142
14253
|
throw lastError;
|
|
14143
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
|
+
}
|
|
14144
14263
|
isNotionImageUrl(url) {
|
|
14145
14264
|
return this.isNotionFileUrl(url);
|
|
14146
14265
|
}
|
|
@@ -15027,7 +15146,74 @@ function selectStaleDbArtifacts(files, localFolder, keepPaths) {
|
|
|
15027
15146
|
return stale;
|
|
15028
15147
|
}
|
|
15029
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
|
+
|
|
15030
15213
|
// src/sync/file-handler.ts
|
|
15214
|
+
function isHiddenPath(path) {
|
|
15215
|
+
return path.split("/").some((part) => part.startsWith("."));
|
|
15216
|
+
}
|
|
15031
15217
|
function getFolderPath(filePath) {
|
|
15032
15218
|
const parts = filePath.split("/");
|
|
15033
15219
|
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
@@ -15042,6 +15228,8 @@ var FileHandler = class {
|
|
|
15042
15228
|
this.customFetch = options?.fetch;
|
|
15043
15229
|
this.downloadTimeoutMs = options?.downloadTimeoutMs ?? DEFAULT_CONFIG.advanced.mediaDownloadTimeoutMs;
|
|
15044
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, "");
|
|
15045
15233
|
}
|
|
15046
15234
|
vaultFs;
|
|
15047
15235
|
notionClient;
|
|
@@ -15050,6 +15238,8 @@ var FileHandler = class {
|
|
|
15050
15238
|
customFetch;
|
|
15051
15239
|
downloadTimeoutMs;
|
|
15052
15240
|
itemTimeoutMs;
|
|
15241
|
+
attachmentsFolder;
|
|
15242
|
+
deleteSync;
|
|
15053
15243
|
get fetchFn() {
|
|
15054
15244
|
return this.customFetch ?? globalThis.fetch;
|
|
15055
15245
|
}
|
|
@@ -15059,7 +15249,9 @@ var FileHandler = class {
|
|
|
15059
15249
|
if (folderFiles.length === 0) return [];
|
|
15060
15250
|
const results = [];
|
|
15061
15251
|
for (const file of folderFiles) {
|
|
15062
|
-
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);
|
|
15063
15255
|
if (result) results.push(result);
|
|
15064
15256
|
}
|
|
15065
15257
|
return results;
|
|
@@ -15080,18 +15272,11 @@ var FileHandler = class {
|
|
|
15080
15272
|
* 내면 같은 말이 두 번 나온다). 없으면 경고 로그로만 남는다.
|
|
15081
15273
|
* @returns 업로드 결과. 변경 없음(스킵)이거나 실패했으면 null.
|
|
15082
15274
|
*/
|
|
15083
|
-
async pushSingleFile(file, folderPageId, failed) {
|
|
15084
|
-
const existing = this.stateDb.getFileRegistry(file.path);
|
|
15085
|
-
if (existing) {
|
|
15086
|
-
if (existing.fileSize === file.size) return null;
|
|
15087
|
-
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15088
|
-
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
15089
|
-
if (existing.fileHash === currentHash) return null;
|
|
15090
|
-
}
|
|
15275
|
+
async pushSingleFile(file, folderPageId, existing, failed) {
|
|
15091
15276
|
await this.sema.acquire();
|
|
15092
15277
|
try {
|
|
15093
15278
|
return await withDeadline(
|
|
15094
|
-
() => this.uploadFile(file, folderPageId),
|
|
15279
|
+
() => this.uploadFile(file, folderPageId, existing),
|
|
15095
15280
|
this.itemTimeoutMs,
|
|
15096
15281
|
`\uCCA8\uBD80 \uC5C5\uB85C\uB4DC ${file.path}`
|
|
15097
15282
|
);
|
|
@@ -15112,6 +15297,22 @@ var FileHandler = class {
|
|
|
15112
15297
|
this.sema.release();
|
|
15113
15298
|
}
|
|
15114
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
|
+
}
|
|
15115
15316
|
/**
|
|
15116
15317
|
* 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 아예 빼고, Notion 이 받지 않는
|
|
15117
15318
|
* 형식은 따로 모은다.
|
|
@@ -15138,7 +15339,7 @@ var FileHandler = class {
|
|
|
15138
15339
|
* @param failed 올리지 못한 첨부를 담을 곳 — push 결과의 `failed` 를 넘기면 CLI · 플러그인이 처리하지 못한 것으로
|
|
15139
15340
|
* 보인다. 예전엔 경고 로그로만 남아 push 가 「실패 0」 으로 끝났다.
|
|
15140
15341
|
*/
|
|
15141
|
-
async pushAllFiles(failed) {
|
|
15342
|
+
async pushAllFiles(failed, folders) {
|
|
15142
15343
|
const { uploadable: allFiles, unsupported: unsupported2 } = await this.listUploadableFiles();
|
|
15143
15344
|
if (unsupported2.length > 0) {
|
|
15144
15345
|
const names = unsupported2.slice(0, LOGGED_NAMES).map((f) => f.path);
|
|
@@ -15147,32 +15348,225 @@ var FileHandler = class {
|
|
|
15147
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` : ""}`
|
|
15148
15349
|
);
|
|
15149
15350
|
}
|
|
15150
|
-
if (
|
|
15151
|
-
const
|
|
15351
|
+
if (this.deleteSync) await this.removeDeletedFolderFiles();
|
|
15352
|
+
const pending = [];
|
|
15152
15353
|
for (const file of allFiles) {
|
|
15153
|
-
|
|
15154
|
-
const
|
|
15155
|
-
|
|
15156
|
-
|
|
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);
|
|
15157
15373
|
}
|
|
15158
15374
|
const results = [];
|
|
15159
|
-
|
|
15160
|
-
|
|
15161
|
-
const
|
|
15162
|
-
if (!
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
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)}`
|
|
15166
15407
|
);
|
|
15167
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);
|
|
15168
15477
|
continue;
|
|
15169
15478
|
}
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
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();
|
|
15173
15490
|
}
|
|
15174
15491
|
}
|
|
15175
|
-
|
|
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;
|
|
15176
15570
|
}
|
|
15177
15571
|
async downloadFileBlock(url, filename, targetFolder) {
|
|
15178
15572
|
const response = await fetchForDownload(this.fetchFn, url, this.downloadTimeoutMs);
|
|
@@ -15203,7 +15597,7 @@ var FileHandler = class {
|
|
|
15203
15597
|
}
|
|
15204
15598
|
return results;
|
|
15205
15599
|
}
|
|
15206
|
-
async uploadFile(file, parentPageId) {
|
|
15600
|
+
async uploadFile(file, parentPageId, previous) {
|
|
15207
15601
|
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15208
15602
|
const filename = file.path.split("/").pop() ?? "file";
|
|
15209
15603
|
const contentType = getMimeType(filename);
|
|
@@ -15211,8 +15605,12 @@ var FileHandler = class {
|
|
|
15211
15605
|
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
15212
15606
|
const blob = new Blob([buffer], { type: contentType });
|
|
15213
15607
|
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
15214
|
-
const block = this.createFileBlock(blockType, fileUploadId);
|
|
15215
|
-
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);
|
|
15216
15614
|
this.stateDb.registerFile({
|
|
15217
15615
|
localPath: file.path,
|
|
15218
15616
|
notionPageId: parentPageId,
|
|
@@ -15224,10 +15622,25 @@ var FileHandler = class {
|
|
|
15224
15622
|
getLogger().info(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC: ${file.path} \u2192 ${blockType} \uBE14\uB85D`);
|
|
15225
15623
|
return { localPath: file.path, fileUploadId, blockType };
|
|
15226
15624
|
}
|
|
15227
|
-
|
|
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) {
|
|
15228
15640
|
const fileRef = {
|
|
15229
15641
|
type: "file_upload",
|
|
15230
|
-
file_upload: { id: fileUploadId }
|
|
15642
|
+
file_upload: { id: fileUploadId },
|
|
15643
|
+
caption: [{ type: "text", text: { content: name } }]
|
|
15231
15644
|
};
|
|
15232
15645
|
switch (blockType) {
|
|
15233
15646
|
case "image":
|
|
@@ -19092,7 +19505,7 @@ var PageDecorations = class {
|
|
|
19092
19505
|
}
|
|
19093
19506
|
case "local":
|
|
19094
19507
|
try {
|
|
19095
|
-
const uploaded = await this.imageHandler.uploadLocalImage(source.target,
|
|
19508
|
+
const uploaded = await this.imageHandler.uploadLocalImage(source.target, folderOf3(path));
|
|
19096
19509
|
return { type: "file_upload", file_upload: { id: uploaded.fileUploadId } };
|
|
19097
19510
|
} catch (error) {
|
|
19098
19511
|
getLogger().warn(
|
|
@@ -19137,7 +19550,7 @@ function sameText(a, b) {
|
|
|
19137
19550
|
if (isEmptyFrontmatterValue(before) && isEmptyFrontmatterValue(after)) return true;
|
|
19138
19551
|
return String(before) === String(after);
|
|
19139
19552
|
}
|
|
19140
|
-
function
|
|
19553
|
+
function folderOf3(notePath) {
|
|
19141
19554
|
return notePath.includes("/") ? notePath.slice(0, notePath.lastIndexOf("/")) : void 0;
|
|
19142
19555
|
}
|
|
19143
19556
|
|
|
@@ -20941,7 +21354,7 @@ async function verifyPageCompleteness(remote, local, rootPageId) {
|
|
|
20941
21354
|
var ID_SUFFIX = new RegExp(
|
|
20942
21355
|
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((len) => `[0-9a-f]{${len}}`).join("|")})\\)$`
|
|
20943
21356
|
);
|
|
20944
|
-
function
|
|
21357
|
+
function folderOf4(record) {
|
|
20945
21358
|
if (record.fileType === "db-row") return null;
|
|
20946
21359
|
if (isFolderRecord(record)) return record.obsidianPath;
|
|
20947
21360
|
const slash = record.obsidianPath.lastIndexOf("/");
|
|
@@ -20958,10 +21371,10 @@ function findNestedFolderPages(records) {
|
|
|
20958
21371
|
const found2 = [];
|
|
20959
21372
|
for (const inner of records) {
|
|
20960
21373
|
if (inner.notionPageId === null || inner.notionParentId === null) continue;
|
|
20961
|
-
const folder =
|
|
21374
|
+
const folder = folderOf4(inner);
|
|
20962
21375
|
if (folder === null) continue;
|
|
20963
21376
|
const outer = byPage.get(compactNotionId(inner.notionParentId));
|
|
20964
|
-
if (!outer || outer === inner ||
|
|
21377
|
+
if (!outer || outer === inner || folderOf4(outer) !== folder) continue;
|
|
20965
21378
|
found2.push({
|
|
20966
21379
|
folder,
|
|
20967
21380
|
outer: { path: outer.obsidianPath, notionPageId: outer.notionPageId },
|
|
@@ -21336,7 +21749,7 @@ var VaultSchemaRenames = class {
|
|
|
21336
21749
|
const meta = await this.notionClient.getDatabaseMeta(databaseId);
|
|
21337
21750
|
for (const [from, to] of pairs) {
|
|
21338
21751
|
const carrying = rows.filter((row) => Object.hasOwn(row.current, to));
|
|
21339
|
-
const folder =
|
|
21752
|
+
const folder = folderOf5(carrying[0].record.obsidianPath);
|
|
21340
21753
|
const owners = meta.dataSources.filter(
|
|
21341
21754
|
(ds) => ds.properties?.[from] !== void 0
|
|
21342
21755
|
);
|
|
@@ -21502,7 +21915,7 @@ function sameRow(a, b) {
|
|
|
21502
21915
|
const keys = Object.keys(left.data);
|
|
21503
21916
|
return left.content === right.content && keys.length === Object.keys(right.data).length && keys.every((key) => sameFrontmatterValue(left.data[key], right.data[key]));
|
|
21504
21917
|
}
|
|
21505
|
-
function
|
|
21918
|
+
function folderOf5(path) {
|
|
21506
21919
|
const at = path.lastIndexOf("/");
|
|
21507
21920
|
return at === -1 ? "" : path.slice(0, at);
|
|
21508
21921
|
}
|
|
@@ -23354,8 +23767,7 @@ var DatabaseDiscovery = class {
|
|
|
23354
23767
|
kind: "ok",
|
|
23355
23768
|
config: {
|
|
23356
23769
|
databaseId: dbId,
|
|
23357
|
-
localFolder: `${parentFolder}/${safeName}
|
|
23358
|
-
titleProperty: DEFAULT_TITLE_PROPERTY
|
|
23770
|
+
localFolder: `${parentFolder}/${safeName}`
|
|
23359
23771
|
}
|
|
23360
23772
|
};
|
|
23361
23773
|
}
|
|
@@ -24429,7 +24841,7 @@ function rewriteTarget(target, source, context, kind) {
|
|
|
24429
24841
|
const shown = keepExtension ? destination : destination.slice(0, -".md".length);
|
|
24430
24842
|
let next;
|
|
24431
24843
|
if (resolved.relative) {
|
|
24432
|
-
next = relativePath(
|
|
24844
|
+
next = relativePath(folderOf6(source.to), shown);
|
|
24433
24845
|
if (!linkpath.startsWith("./") && next.startsWith("./")) next = next.slice(2);
|
|
24434
24846
|
} else if (style === "name") {
|
|
24435
24847
|
const name = baseName3(shown);
|
|
@@ -24451,14 +24863,14 @@ function resolveLinkpath(linkpath, sourcePath, files) {
|
|
|
24451
24863
|
const matches = byName(files, baseName3(candidate)).filter((file) => file.endsWith(suffix));
|
|
24452
24864
|
if (matches.length === 1) return found(matches[0], linkpath);
|
|
24453
24865
|
if (matches.length > 1) {
|
|
24454
|
-
const local = `${folderPrefix(
|
|
24866
|
+
const local = `${folderPrefix(folderOf6(sourcePath))}${candidate}`;
|
|
24455
24867
|
return matches.includes(local) ? found(local, linkpath) : null;
|
|
24456
24868
|
}
|
|
24457
24869
|
}
|
|
24458
24870
|
return null;
|
|
24459
24871
|
}
|
|
24460
24872
|
function resolveRelative(linkpath, sourcePath, files) {
|
|
24461
|
-
const joined = normalizeSegments(`${folderPrefix(
|
|
24873
|
+
const joined = normalizeSegments(`${folderPrefix(folderOf6(sourcePath))}${linkpath}`);
|
|
24462
24874
|
if (joined === null) return null;
|
|
24463
24875
|
for (const candidate of withExtension(joined)) {
|
|
24464
24876
|
if (files.has(candidate)) return { ...found(candidate, linkpath), relative: true };
|
|
@@ -24517,7 +24929,7 @@ function relativePath(from, to) {
|
|
|
24517
24929
|
function encodeMarkdownTarget(next, original) {
|
|
24518
24930
|
return original.includes("%") || next.includes(" ") ? next.replace(/ /g, "%20") : next;
|
|
24519
24931
|
}
|
|
24520
|
-
function
|
|
24932
|
+
function folderOf6(path) {
|
|
24521
24933
|
const slash = path.lastIndexOf("/");
|
|
24522
24934
|
return slash === -1 ? "" : path.slice(0, slash);
|
|
24523
24935
|
}
|
|
@@ -26820,7 +27232,7 @@ async function resolveBlockToPageId(notionClient, blockId) {
|
|
|
26820
27232
|
|
|
26821
27233
|
// src/sync/page-puller.ts
|
|
26822
27234
|
var PagePuller = class {
|
|
26823
|
-
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) {
|
|
26824
27236
|
this.config = config;
|
|
26825
27237
|
this.stateDb = stateDb;
|
|
26826
27238
|
this.notionClient = notionClient;
|
|
@@ -26834,6 +27246,7 @@ var PagePuller = class {
|
|
|
26834
27246
|
this.placement = placement;
|
|
26835
27247
|
this.detector = detector;
|
|
26836
27248
|
this.discovery = discovery;
|
|
27249
|
+
this.fileHandler = fileHandler;
|
|
26837
27250
|
}
|
|
26838
27251
|
config;
|
|
26839
27252
|
stateDb;
|
|
@@ -26848,6 +27261,7 @@ var PagePuller = class {
|
|
|
26848
27261
|
placement;
|
|
26849
27262
|
detector;
|
|
26850
27263
|
discovery;
|
|
27264
|
+
fileHandler;
|
|
26851
27265
|
dbSchemaLoaded = false;
|
|
26852
27266
|
_pullImageCount = 0;
|
|
26853
27267
|
_pullFileCount = 0;
|
|
@@ -27004,8 +27418,17 @@ var PagePuller = class {
|
|
|
27004
27418
|
if (excluded !== null) return { path: excluded, excluded: true };
|
|
27005
27419
|
const fetched = await this.fetchPageMarkdown(pageId);
|
|
27006
27420
|
const hasChildPages = this.hasChildContainers(pageId, fetched.children);
|
|
27007
|
-
|
|
27008
|
-
|
|
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;
|
|
27009
27432
|
}
|
|
27010
27433
|
const fileType = hasChildPages ? "folder-note" : "file";
|
|
27011
27434
|
const noteDir = hasChildPages ? parentPath ? `${parentPath}/${safeName}` : safeName : parentPath;
|
|
@@ -27468,7 +27891,8 @@ var PagePuller = class {
|
|
|
27468
27891
|
compact: isCompactExport(result.markdown),
|
|
27469
27892
|
children: extractChildTags(result.markdown),
|
|
27470
27893
|
fingerprint: remoteBodyFingerprint(result.markdown),
|
|
27471
|
-
hasBody: hasBodyBesidesChildren(result.markdown)
|
|
27894
|
+
hasBody: hasBodyBesidesChildren(result.markdown),
|
|
27895
|
+
folderFiles: folderFilesInBody(result.markdown)
|
|
27472
27896
|
};
|
|
27473
27897
|
} catch (error) {
|
|
27474
27898
|
getLogger().warn(
|
|
@@ -27487,9 +27911,34 @@ var PagePuller = class {
|
|
|
27487
27911
|
compact: isCompactExport(fallback.markdown),
|
|
27488
27912
|
children: extractChildTags(fallback.markdown),
|
|
27489
27913
|
fingerprint: null,
|
|
27490
|
-
hasBody: hasBodyBesidesChildren(fallback.markdown)
|
|
27914
|
+
hasBody: hasBodyBesidesChildren(fallback.markdown),
|
|
27915
|
+
folderFiles: folderFilesInBody(fallback.markdown)
|
|
27491
27916
|
};
|
|
27492
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
|
+
}
|
|
27493
27942
|
/**
|
|
27494
27943
|
* push 가 만든 폴더 페이지(폴더 레코드)의 원격 변경을 받는다(S-17). 폴더 레코드에는 볼트 파일이
|
|
27495
27944
|
* 없다 — 예전에는 폴더 경로를 파일로 읽어 «빈 로컬 파일» 과 원격 본문의 충돌로 남겼다.
|
|
@@ -27506,26 +27955,18 @@ var PagePuller = class {
|
|
|
27506
27955
|
* @throws 받을 자리가 없을 때 — 이유와 함께 실패로 남고, 다음 pull 이 다시 본다(기준 시각이
|
|
27507
27956
|
* 이 변경 앞에 묶인다).
|
|
27508
27957
|
*/
|
|
27509
|
-
/**
|
|
27510
|
-
* 이 폴더 레코드의 Notion 페이지에서 받을 본문이 있나 — dry-run 이 실제 pull({@link pullFolderRecord})과 같게 센다.
|
|
27511
|
-
* 본문이 하위 페이지뿐이면 실제 pull 은 본 것만 적고 고친 것으로 세지 않는다.
|
|
27512
|
-
*/
|
|
27513
|
-
async folderPageHasBody(record) {
|
|
27514
|
-
const page = await this.notionClient.getPage(record.notionPageId);
|
|
27515
|
-
if (isTrashedOrArchived(page)) return false;
|
|
27516
|
-
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
27517
|
-
return hasBodyBesidesChildren(markdown) && this.folderNoteRefusal(record.obsidianPath) === null;
|
|
27518
|
-
}
|
|
27519
|
-
/** 이 폴더에 폴더 노트를 둘 수 없는 까닭 — 둘 수 있으면 null. */
|
|
27520
|
-
folderNoteRefusal(folder) {
|
|
27521
|
-
if (isDatabaseMode(this.config)) return "DB \uBAA8\uB4DC\uB294 \uD3F4\uB354 \uB178\uD2B8\uB97C \uBC1B\uC9C0 \uC54A\uC74C";
|
|
27522
|
-
return this.placement.folderLookup().databaseAt(folder) ? "\uADF8 \uD3F4\uB354\uB294 DB \uB77C \uD3F4\uB354 \uB178\uD2B8\uB97C \uB458 \uC218 \uC5C6\uC74C" : null;
|
|
27523
|
-
}
|
|
27524
27958
|
async pullFolderRecord(record, page) {
|
|
27525
27959
|
const folder = record.obsidianPath;
|
|
27526
27960
|
if (isTrashedOrArchived(page)) return null;
|
|
27527
|
-
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
27961
|
+
const { markdown, truncated } = await this.notionClient.getPageMarkdown(page.id);
|
|
27528
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);
|
|
27529
27970
|
this.observation.record(record.id, page, remoteBodyFingerprint(markdown));
|
|
27530
27971
|
return null;
|
|
27531
27972
|
}
|
|
@@ -27714,6 +28155,10 @@ var PagePuller = class {
|
|
|
27714
28155
|
this.dbSchemaLoaded = true;
|
|
27715
28156
|
}
|
|
27716
28157
|
};
|
|
28158
|
+
function folderFilesInBody(markdown) {
|
|
28159
|
+
const files = folderFilesOf(markdown, false);
|
|
28160
|
+
return files ? { files, named: folderFilesOf(markdown, true) !== null } : null;
|
|
28161
|
+
}
|
|
27717
28162
|
|
|
27718
28163
|
// src/sync/frontmatter-link-resolver.ts
|
|
27719
28164
|
function resolveFrontmatterRelations(content, idToTitle) {
|
|
@@ -28823,7 +29268,9 @@ var SyncOrchestrator = class {
|
|
|
28823
29268
|
{
|
|
28824
29269
|
fetch: customFetch,
|
|
28825
29270
|
downloadTimeoutMs: config.advanced.mediaDownloadTimeoutMs,
|
|
28826
|
-
itemTimeoutMs: config.advanced.itemTimeoutMs
|
|
29271
|
+
itemTimeoutMs: config.advanced.itemTimeoutMs,
|
|
29272
|
+
attachmentsFolder: config.paths.attachments,
|
|
29273
|
+
deleteSync: config.sync.deleteSync
|
|
28827
29274
|
}
|
|
28828
29275
|
);
|
|
28829
29276
|
const propertyMapper = PropertyMapper.fromConfig(config);
|
|
@@ -28933,7 +29380,8 @@ var SyncOrchestrator = class {
|
|
|
28933
29380
|
this.drift,
|
|
28934
29381
|
this.placement,
|
|
28935
29382
|
this.detector,
|
|
28936
|
-
this.discovery
|
|
29383
|
+
this.discovery,
|
|
29384
|
+
this.fileHandler
|
|
28937
29385
|
);
|
|
28938
29386
|
this.pullPlanner = new PullPlanner(
|
|
28939
29387
|
config,
|
|
@@ -29053,14 +29501,16 @@ var SyncOrchestrator = class {
|
|
|
29053
29501
|
* 있든 없든 push 마다 부른다 — 바뀌지 않은 첨부는 파일 기록이 건너뛴다.
|
|
29054
29502
|
*/
|
|
29055
29503
|
async pushVaultFiles(failed) {
|
|
29056
|
-
if (this.config.sync.syncFiles === false) return;
|
|
29504
|
+
if (this.config.sync.syncFiles === false) return 0;
|
|
29057
29505
|
try {
|
|
29058
|
-
const fileResults = await this.fileHandler.pushAllFiles(failed);
|
|
29506
|
+
const fileResults = await this.fileHandler.pushAllFiles(failed, this.placement);
|
|
29059
29507
|
if (fileResults.length > 0) {
|
|
29060
29508
|
getLogger().info(`[Im-Nobsidian] ${fileResults.length}\uAC1C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC`);
|
|
29061
29509
|
}
|
|
29510
|
+
return fileResults.length;
|
|
29062
29511
|
} catch (error) {
|
|
29063
29512
|
getLogger().warn("[Im-Nobsidian] \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC911 \uC624\uB958:", error);
|
|
29513
|
+
return 0;
|
|
29064
29514
|
}
|
|
29065
29515
|
}
|
|
29066
29516
|
/**
|
|
@@ -29149,12 +29599,13 @@ var SyncOrchestrator = class {
|
|
|
29149
29599
|
...viewPlan.refused,
|
|
29150
29600
|
...viewed2.failed
|
|
29151
29601
|
];
|
|
29152
|
-
|
|
29602
|
+
const uploadedFiles2 = !options?.dryRun && !options?.signal?.aborted ? await this.pushVaultFiles(failed2) : 0;
|
|
29153
29603
|
return {
|
|
29154
29604
|
created: 0,
|
|
29155
29605
|
updated: 0,
|
|
29156
29606
|
deleted: 0,
|
|
29157
29607
|
moved: 0,
|
|
29608
|
+
...uploadedFiles2 > 0 ? { uploadedFiles: uploadedFiles2 } : {},
|
|
29158
29609
|
failed: failed2,
|
|
29159
29610
|
duration: Date.now() - startTime,
|
|
29160
29611
|
...edited2.applied.length > 0 ? { schemaEdits: edited2.applied } : {},
|
|
@@ -29433,7 +29884,7 @@ var SyncOrchestrator = class {
|
|
|
29433
29884
|
signal: options?.signal
|
|
29434
29885
|
});
|
|
29435
29886
|
}
|
|
29436
|
-
await this.pushVaultFiles(failed);
|
|
29887
|
+
const uploadedFiles = await this.pushVaultFiles(failed);
|
|
29437
29888
|
this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
29438
29889
|
this.stateDb.setMeta(LAST_SYNC_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
29439
29890
|
this.stateDb.setMeta(PUSH_IN_PROGRESS_META_KEY, "");
|
|
@@ -29445,6 +29896,7 @@ var SyncOrchestrator = class {
|
|
|
29445
29896
|
updated: counts.updated,
|
|
29446
29897
|
deleted: counts.deleted,
|
|
29447
29898
|
moved: counts.moved,
|
|
29899
|
+
...uploadedFiles > 0 ? { uploadedFiles } : {},
|
|
29448
29900
|
failed,
|
|
29449
29901
|
duration: Date.now() - startTime,
|
|
29450
29902
|
readOnlySkipped: this.pusher.takeReadOnlySkips(),
|