@im-nobsidian/core 0.20.0 → 0.21.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 +52 -6
- package/dist/index.js +449 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15184,12 +15184,56 @@ function folderFilesOf(markdown, namedOnly) {
|
|
|
15184
15184
|
for (const span of notionMediaSpans(rest)) {
|
|
15185
15185
|
if (!/^https?:\/\//.test(span.url)) return null;
|
|
15186
15186
|
const name = uploadedFileNameFromUrl(span.url);
|
|
15187
|
-
if (!name
|
|
15187
|
+
if (!name) return null;
|
|
15188
|
+
if (namedOnly && unescapeCaption(span.label) !== name && !isMarkedCaption(span.label, name)) {
|
|
15189
|
+
return null;
|
|
15190
|
+
}
|
|
15188
15191
|
rest = rest.replace(span.full, "");
|
|
15189
15192
|
files.push({ url: span.url, name });
|
|
15190
15193
|
}
|
|
15191
15194
|
return files.length > 0 && rest.trim() === "" ? files : null;
|
|
15192
15195
|
}
|
|
15196
|
+
var FOLDER_FILE_MARK = "\u{1F4C1}";
|
|
15197
|
+
function folderFileCaption(name) {
|
|
15198
|
+
return `${FOLDER_FILE_MARK} ${name}`;
|
|
15199
|
+
}
|
|
15200
|
+
function isMarkedCaption(label, name) {
|
|
15201
|
+
return unescapeCaption(label) === folderFileCaption(name);
|
|
15202
|
+
}
|
|
15203
|
+
var FENCE_RE = /^(`{3,}|~{3,})/;
|
|
15204
|
+
function folderFilesInNote(markdown, options = {}) {
|
|
15205
|
+
const all = markdown.split("\n");
|
|
15206
|
+
const drop = /* @__PURE__ */ new Set();
|
|
15207
|
+
const files = [];
|
|
15208
|
+
const lines = [];
|
|
15209
|
+
let fence = null;
|
|
15210
|
+
all.forEach((line, i) => {
|
|
15211
|
+
const opener = FENCE_RE.exec(line)?.[1];
|
|
15212
|
+
if (opener !== void 0 && (fence === null || opener.startsWith(fence))) {
|
|
15213
|
+
fence = fence === null ? opener : null;
|
|
15214
|
+
return;
|
|
15215
|
+
}
|
|
15216
|
+
if (fence !== null) return;
|
|
15217
|
+
const full = line.trimEnd();
|
|
15218
|
+
const [span, ...more] = notionMediaSpans(full);
|
|
15219
|
+
if (!span || more.length > 0 || span.full !== full || !/^https?:\/\//.test(span.url)) return;
|
|
15220
|
+
const name = uploadedFileNameFromUrl(span.url);
|
|
15221
|
+
if (!name) return;
|
|
15222
|
+
const fileId = notionFileIdOf(span.url);
|
|
15223
|
+
const marked = isMarkedCaption(span.label, name) || fileId !== null && options.knownIds?.has(fileId) === true;
|
|
15224
|
+
const file = { url: span.url, name };
|
|
15225
|
+
if (!marked || options.exclude?.(file)) return;
|
|
15226
|
+
drop.add(i);
|
|
15227
|
+
files.push(file);
|
|
15228
|
+
lines.push(full);
|
|
15229
|
+
});
|
|
15230
|
+
if (files.length === 0) return null;
|
|
15231
|
+
return {
|
|
15232
|
+
markdown: all.filter((_, i) => !drop.has(i)).join("\n"),
|
|
15233
|
+
files,
|
|
15234
|
+
lines
|
|
15235
|
+
};
|
|
15236
|
+
}
|
|
15193
15237
|
var WIKI_EMBED_RE = /!\[\[([^\]|#^\n]+)/g;
|
|
15194
15238
|
var MD_EMBED_RE = /!\[[^\]\n]*\]\(<?([^)>\s]+)/g;
|
|
15195
15239
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
@@ -15217,8 +15261,8 @@ function embeddedNamesOf(notes) {
|
|
|
15217
15261
|
function keyOf3(pageId) {
|
|
15218
15262
|
return `folder_files:${compactNotionId(pageId)}`;
|
|
15219
15263
|
}
|
|
15220
|
-
function
|
|
15221
|
-
const raw = stateDb.getMeta(
|
|
15264
|
+
function readPathMap(stateDb, key) {
|
|
15265
|
+
const raw = stateDb.getMeta(key);
|
|
15222
15266
|
if (!raw) return /* @__PURE__ */ new Map();
|
|
15223
15267
|
try {
|
|
15224
15268
|
return new Map(Object.entries(JSON.parse(raw)));
|
|
@@ -15226,16 +15270,39 @@ function folderFileIdsOf(stateDb, pageId) {
|
|
|
15226
15270
|
return /* @__PURE__ */ new Map();
|
|
15227
15271
|
}
|
|
15228
15272
|
}
|
|
15273
|
+
function writePathMap(stateDb, key, map) {
|
|
15274
|
+
stateDb.setMeta(key, JSON.stringify(Object.fromEntries(map)));
|
|
15275
|
+
}
|
|
15276
|
+
function folderFileIdsOf(stateDb, pageId) {
|
|
15277
|
+
return readPathMap(stateDb, keyOf3(pageId));
|
|
15278
|
+
}
|
|
15229
15279
|
function forgetFolderFileId(stateDb, pageId, localPath) {
|
|
15280
|
+
rememberFolderFileStamp(stateDb, pageId, localPath, null);
|
|
15230
15281
|
const ids = folderFileIdsOf(stateDb, pageId);
|
|
15231
15282
|
if (!ids.delete(localPath)) return;
|
|
15232
|
-
stateDb
|
|
15283
|
+
writePathMap(stateDb, keyOf3(pageId), ids);
|
|
15233
15284
|
}
|
|
15234
15285
|
function rememberFolderFileId(stateDb, pageId, localPath, fileId) {
|
|
15235
15286
|
const ids = folderFileIdsOf(stateDb, pageId);
|
|
15236
15287
|
if (ids.get(localPath) === fileId) return;
|
|
15237
15288
|
ids.set(localPath, fileId);
|
|
15238
|
-
stateDb
|
|
15289
|
+
writePathMap(stateDb, keyOf3(pageId), ids);
|
|
15290
|
+
}
|
|
15291
|
+
function fileStampOf(file) {
|
|
15292
|
+
return `${file.size}:${file.mtime}`;
|
|
15293
|
+
}
|
|
15294
|
+
function stampKeyOf(pageId) {
|
|
15295
|
+
return `folder_file_stamps:${compactNotionId(pageId)}`;
|
|
15296
|
+
}
|
|
15297
|
+
function folderFileStampsOf(stateDb, pageId) {
|
|
15298
|
+
return readPathMap(stateDb, stampKeyOf(pageId));
|
|
15299
|
+
}
|
|
15300
|
+
function rememberFolderFileStamp(stateDb, pageId, localPath, stamp) {
|
|
15301
|
+
const stamps = folderFileStampsOf(stateDb, pageId);
|
|
15302
|
+
if ((stamps.get(localPath) ?? null) === stamp) return;
|
|
15303
|
+
if (stamp === null) stamps.delete(localPath);
|
|
15304
|
+
else stamps.set(localPath, stamp);
|
|
15305
|
+
writePathMap(stateDb, stampKeyOf(pageId), stamps);
|
|
15239
15306
|
}
|
|
15240
15307
|
|
|
15241
15308
|
// src/sync/file-handler.ts
|
|
@@ -15246,6 +15313,12 @@ function getFolderPath(filePath) {
|
|
|
15246
15313
|
const parts = filePath.split("/");
|
|
15247
15314
|
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
15248
15315
|
}
|
|
15316
|
+
function filesFolderOfNote(record) {
|
|
15317
|
+
if (record.fileType === "folder-note" && isFolderNotePath(record.obsidianPath)) {
|
|
15318
|
+
return getFolderPath(record.obsidianPath);
|
|
15319
|
+
}
|
|
15320
|
+
return record.fileType === "file" ? stripMarkdownExtension(record.obsidianPath) : null;
|
|
15321
|
+
}
|
|
15249
15322
|
var LOGGED_NAMES = 5;
|
|
15250
15323
|
var FileHandler = class {
|
|
15251
15324
|
constructor(vaultFs, notionClient, stateDb, concurrency = DEFAULT_CONFIG.advanced.fileConcurrency, options) {
|
|
@@ -15325,21 +15398,55 @@ var FileHandler = class {
|
|
|
15325
15398
|
this.sema.release();
|
|
15326
15399
|
}
|
|
15327
15400
|
}
|
|
15328
|
-
/**
|
|
15401
|
+
/**
|
|
15402
|
+
* 올리거나 받은 뒤 바뀌지 않았나 — 그때의 표(크기 · 수정 시각, {@link fileStampOf})와 같으면 읽지 않는다. 크기가 다르면 바뀌었다.
|
|
15403
|
+
* 크기만 같으면 내용으로 견주고, 같으면 지금 표를 적어 다음에는 읽지 않는다. 예전에는 크기가 같으면 바뀌지 않았다고 봐, 같은
|
|
15404
|
+
* 크기로 고친 파일을 올리지 않았다.
|
|
15405
|
+
*/
|
|
15329
15406
|
async isUnchanged(existing, file) {
|
|
15330
|
-
|
|
15407
|
+
const stamp = fileStampOf(file);
|
|
15408
|
+
if (folderFileStampsOf(this.stateDb, existing.notionPageId).get(file.path) === stamp) {
|
|
15409
|
+
return true;
|
|
15410
|
+
}
|
|
15411
|
+
if (existing.fileSize !== file.size) return false;
|
|
15331
15412
|
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15332
|
-
|
|
15413
|
+
const same = existing.fileHash === createHash("sha256").update(buffer).digest("hex");
|
|
15414
|
+
if (same) rememberFolderFileStamp(this.stateDb, existing.notionPageId, file.path, stamp);
|
|
15415
|
+
return same;
|
|
15333
15416
|
}
|
|
15334
15417
|
/** pull 이 받은 것을 두는 폴더 안인가. */
|
|
15335
15418
|
inAttachmentsFolder(path) {
|
|
15336
15419
|
return this.attachmentsFolder !== "" && path.startsWith(`${this.attachmentsFolder}/`);
|
|
15337
15420
|
}
|
|
15338
|
-
/**
|
|
15421
|
+
/**
|
|
15422
|
+
* 폴더 첨부를 둘 Notion 페이지 — 그 폴더의 Notion 자리({@link folderContainer} 와 같은 차례). 폴더 노트(`X/X.md`)나 노트
|
|
15423
|
+
* 이름의 폴더(`X.md` 옆 `X/`)면 그 노트의 페이지 끝(ADR-056), 아니면 push 가 만든 폴더 페이지. 맨 위는 루트 기록이 있을 때만.
|
|
15424
|
+
* 행 · DB 폴더는 둘 곳이 없다 — 행의 본문은 행의 것이다.
|
|
15425
|
+
*/
|
|
15339
15426
|
folderPageOf(folder) {
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15427
|
+
if (folder === "") return this.stateDb.getByPath("__root__")?.notionPageId ?? null;
|
|
15428
|
+
return this.notePageOf(folder) ?? this.folderRecordPageOf(folder);
|
|
15429
|
+
}
|
|
15430
|
+
/** 이 폴더를 품은 노트의 페이지 — 폴더 노트 · 노트 이름의 폴더. 없거나 행이면 null. */
|
|
15431
|
+
notePageOf(folder) {
|
|
15432
|
+
const note = this.stateDb.getByPath(folderNoteOf(folder));
|
|
15433
|
+
if (note?.notionPageId && note.fileType === "folder-note") return note.notionPageId;
|
|
15434
|
+
const named = this.stateDb.getByPath(`${folder}.md`);
|
|
15435
|
+
return named?.notionPageId && named.fileType === "file" ? named.notionPageId : null;
|
|
15436
|
+
}
|
|
15437
|
+
folderRecordPageOf(folder) {
|
|
15438
|
+
const record = this.stateDb.getByPath(folder);
|
|
15439
|
+
return record?.notionPageId && isFolderRecord(record) ? record.notionPageId : null;
|
|
15440
|
+
}
|
|
15441
|
+
/**
|
|
15442
|
+
* 이 기록이 그 폴더의 폴더 첨부인가. 노트의 페이지에는 그 노트가 임베드해 올린 파일도 같은 페이지로 적혀 있다 — 폴더
|
|
15443
|
+
* 첨부로 적어 둔 것({@link folderFileIdsOf})만이다.
|
|
15444
|
+
*/
|
|
15445
|
+
isFolderFileEntry(entry, folder) {
|
|
15446
|
+
const pageId = this.folderPageOf(folder);
|
|
15447
|
+
if (!pageId || entry.notionPageId !== pageId) return false;
|
|
15448
|
+
if (pageId !== this.notePageOf(folder)) return true;
|
|
15449
|
+
return folderFileIdsOf(this.stateDb, pageId).has(entry.localPath);
|
|
15343
15450
|
}
|
|
15344
15451
|
/**
|
|
15345
15452
|
* 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 아예 빼고, Notion 이 받지 않는
|
|
@@ -15367,7 +15474,7 @@ var FileHandler = class {
|
|
|
15367
15474
|
* @param failed 올리지 못한 첨부를 담을 곳 — push 결과의 `failed` 를 넘기면 CLI · 플러그인이 처리하지 못한 것으로
|
|
15368
15475
|
* 보인다. 예전엔 경고 로그로만 남아 push 가 「실패 0」 으로 끝났다.
|
|
15369
15476
|
*/
|
|
15370
|
-
async pushAllFiles(failed, folders) {
|
|
15477
|
+
async pushAllFiles(failed, folders, noteWrites) {
|
|
15371
15478
|
const { uploadable: allFiles, unsupported: unsupported2 } = await this.listUploadableFiles();
|
|
15372
15479
|
if (unsupported2.length > 0) {
|
|
15373
15480
|
const names = unsupported2.slice(0, LOGGED_NAMES).map((f) => f.path);
|
|
@@ -15376,13 +15483,13 @@ var FileHandler = class {
|
|
|
15376
15483
|
`[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` : ""}`
|
|
15377
15484
|
);
|
|
15378
15485
|
}
|
|
15379
|
-
if (this.deleteSync) await this.removeDeletedFolderFiles();
|
|
15486
|
+
if (this.deleteSync) await this.removeDeletedFolderFiles(noteWrites);
|
|
15380
15487
|
const pending = [];
|
|
15381
15488
|
for (const file of allFiles) {
|
|
15382
15489
|
if (this.inAttachmentsFolder(file.path) || isHiddenPath(file.path)) continue;
|
|
15383
15490
|
const existing = this.stateDb.getFileRegistry(file.path);
|
|
15384
15491
|
if (existing) {
|
|
15385
|
-
if (
|
|
15492
|
+
if (!this.isFolderFileEntry(existing, getFolderPath(file.path))) continue;
|
|
15386
15493
|
if (await this.isUnchanged(existing, file)) continue;
|
|
15387
15494
|
}
|
|
15388
15495
|
pending.push({ file, existing });
|
|
@@ -15404,10 +15511,23 @@ var FileHandler = class {
|
|
|
15404
15511
|
for (const [folder, items] of byFolder) {
|
|
15405
15512
|
const pageId = await this.provideFolderPage(folder, folders, items, stranded);
|
|
15406
15513
|
if (!pageId) continue;
|
|
15407
|
-
|
|
15408
|
-
|
|
15409
|
-
|
|
15410
|
-
|
|
15514
|
+
await this.writingNotePage(
|
|
15515
|
+
pageId === this.notePageOf(folder),
|
|
15516
|
+
pageId,
|
|
15517
|
+
noteWrites,
|
|
15518
|
+
async () => {
|
|
15519
|
+
for (const { file, existing } of items) {
|
|
15520
|
+
let previous = existing;
|
|
15521
|
+
if (!previous) {
|
|
15522
|
+
const moved = await this.movedHere(file, pageId);
|
|
15523
|
+
if (moved?.same) continue;
|
|
15524
|
+
previous = moved?.entry ?? null;
|
|
15525
|
+
}
|
|
15526
|
+
const result = await this.pushSingleFile(file, pageId, previous, failed);
|
|
15527
|
+
if (result) results.push(result);
|
|
15528
|
+
}
|
|
15529
|
+
}
|
|
15530
|
+
);
|
|
15411
15531
|
}
|
|
15412
15532
|
this.reportStranded(stranded);
|
|
15413
15533
|
return results;
|
|
@@ -15416,25 +15536,37 @@ var FileHandler = class {
|
|
|
15416
15536
|
* 볼트에서 지운 폴더 첨부를 Notion 에서도 지운다(deleteSync) — 폴더 페이지에 적힌 파일 가운데 볼트에 없는 것의 블록과 기록.
|
|
15417
15537
|
* 포함 · 제외 패턴으로 빠진 파일은 볼트에 있어 지우지 않는다.
|
|
15418
15538
|
*/
|
|
15419
|
-
async removeDeletedFolderFiles() {
|
|
15539
|
+
async removeDeletedFolderFiles(noteWrites) {
|
|
15420
15540
|
const removed = [];
|
|
15421
15541
|
for (const record of this.stateDb.getAll()) {
|
|
15422
|
-
if (!record.notionPageId
|
|
15542
|
+
if (!record.notionPageId) continue;
|
|
15543
|
+
const folder = isFolderRecord(record) ? record.obsidianPath : filesFolderOfNote(record);
|
|
15544
|
+
if (folder === null) continue;
|
|
15545
|
+
const ids = folderFileIdsOf(this.stateDb, record.notionPageId);
|
|
15546
|
+
if (!isFolderRecord(record) && ids.size === 0) continue;
|
|
15547
|
+
const gone = [];
|
|
15423
15548
|
for (const entry of this.stateDb.getFilesByPageId(record.notionPageId)) {
|
|
15424
|
-
if (getFolderPath(entry.localPath) !==
|
|
15425
|
-
if (
|
|
15426
|
-
|
|
15427
|
-
const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
|
|
15428
|
-
await this.removeOlderBlocks(record.notionPageId, name, void 0);
|
|
15429
|
-
this.stateDb.deleteFileRegistry(entry.localPath);
|
|
15430
|
-
forgetFolderFileId(this.stateDb, record.notionPageId, entry.localPath);
|
|
15431
|
-
removed.push(entry.localPath);
|
|
15432
|
-
} catch (error) {
|
|
15433
|
-
getLogger().warn(
|
|
15434
|
-
`[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)}`
|
|
15435
|
-
);
|
|
15436
|
-
}
|
|
15549
|
+
if (getFolderPath(entry.localPath) !== folder) continue;
|
|
15550
|
+
if (!isFolderRecord(record) && !ids.has(entry.localPath)) continue;
|
|
15551
|
+
if (!await this.vaultFs.exists(entry.localPath)) gone.push(entry);
|
|
15437
15552
|
}
|
|
15553
|
+
if (gone.length === 0) continue;
|
|
15554
|
+
const pageId = record.notionPageId;
|
|
15555
|
+
await this.writingNotePage(!isFolderRecord(record), pageId, noteWrites, async () => {
|
|
15556
|
+
for (const entry of gone) {
|
|
15557
|
+
try {
|
|
15558
|
+
const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
|
|
15559
|
+
await this.removeOlderBlocks(pageId, entry.localPath, name, void 0);
|
|
15560
|
+
this.stateDb.deleteFileRegistry(entry.localPath);
|
|
15561
|
+
forgetFolderFileId(this.stateDb, pageId, entry.localPath);
|
|
15562
|
+
removed.push(entry.localPath);
|
|
15563
|
+
} catch (error) {
|
|
15564
|
+
getLogger().warn(
|
|
15565
|
+
`[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)}`
|
|
15566
|
+
);
|
|
15567
|
+
}
|
|
15568
|
+
}
|
|
15569
|
+
});
|
|
15438
15570
|
}
|
|
15439
15571
|
if (removed.length > 0) {
|
|
15440
15572
|
getLogger().info(
|
|
@@ -15442,10 +15574,20 @@ var FileHandler = class {
|
|
|
15442
15574
|
);
|
|
15443
15575
|
}
|
|
15444
15576
|
}
|
|
15577
|
+
/** 노트의 페이지에 쓰는 동안을 알린다({@link NotePageWrites}) — 폴더 페이지는 노트가 없어 알릴 것이 없다. */
|
|
15578
|
+
async writingNotePage(isNotePage, pageId, noteWrites, write) {
|
|
15579
|
+
if (!isNotePage || !noteWrites) return write();
|
|
15580
|
+
await noteWrites.begin(pageId);
|
|
15581
|
+
try {
|
|
15582
|
+
await write();
|
|
15583
|
+
} finally {
|
|
15584
|
+
await noteWrites.end(pageId);
|
|
15585
|
+
}
|
|
15586
|
+
}
|
|
15445
15587
|
/** 폴더 첨부를 둘 페이지 — 노트 없는 폴더면 마련한다. 둘 곳이 없으면 그 파일들을 `stranded` 에 담고 null. */
|
|
15446
15588
|
async provideFolderPage(folder, folders, items, stranded) {
|
|
15447
15589
|
let pageId = this.folderPageOf(folder);
|
|
15448
|
-
let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704" : "\uD3F4\uB354 \
|
|
15590
|
+
let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704" : "DB \uD3F4\uB354 \xB7 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uD3F4\uB354 \uB178\uD2B8";
|
|
15449
15591
|
if (!pageId && folder !== "" && folders && !this.stateDb.getByPath(folder)) {
|
|
15450
15592
|
try {
|
|
15451
15593
|
await folders.ensureFolderPage(folder);
|
|
@@ -15594,6 +15736,8 @@ var FileHandler = class {
|
|
|
15594
15736
|
fileHash: hash,
|
|
15595
15737
|
fileSize: buffer.length
|
|
15596
15738
|
});
|
|
15739
|
+
const stat2 = await this.vaultFs.getFileStat(localPath);
|
|
15740
|
+
if (stat2) rememberFolderFileStamp(this.stateDb, pageId, localPath, fileStampOf(stat2));
|
|
15597
15741
|
return wrote;
|
|
15598
15742
|
}
|
|
15599
15743
|
async downloadFileBlock(url, filename, targetFolder) {
|
|
@@ -15635,7 +15779,9 @@ var FileHandler = class {
|
|
|
15635
15779
|
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
15636
15780
|
const block = this.createFileBlock(blockType, fileUploadId, filename);
|
|
15637
15781
|
const [created] = await this.notionClient.appendChildBlocks(parentPageId, [block]);
|
|
15638
|
-
if (previous)
|
|
15782
|
+
if (previous) {
|
|
15783
|
+
await this.removeOlderBlocks(parentPageId, file.path, filename, created?.id);
|
|
15784
|
+
}
|
|
15639
15785
|
const url = created ? fileUrlOfBlock(created) : null;
|
|
15640
15786
|
const fileId = url ? notionFileIdOf(url) : null;
|
|
15641
15787
|
if (fileId) rememberFolderFileId(this.stateDb, parentPageId, file.path, fileId);
|
|
@@ -15647,28 +15793,78 @@ var FileHandler = class {
|
|
|
15647
15793
|
fileHash: hash,
|
|
15648
15794
|
fileSize: file.size
|
|
15649
15795
|
});
|
|
15796
|
+
rememberFolderFileStamp(this.stateDb, parentPageId, file.path, fileStampOf(file));
|
|
15650
15797
|
getLogger().info(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC: ${file.path} \u2192 ${blockType} \uBE14\uB85D`);
|
|
15651
15798
|
return { localPath: file.path, fileUploadId, blockType };
|
|
15652
15799
|
}
|
|
15653
|
-
/**
|
|
15654
|
-
|
|
15800
|
+
/**
|
|
15801
|
+
* 처음 보는 파일이 이 페이지에 적어 둔 같은 이름의 폴더 첨부를 옮긴 것인가 — 그 기록의 경로가 볼트에서 사라졌으면 옮겼다(폴더 ·
|
|
15802
|
+
* 폴더 노트 · 노트의 이름 바꾸기). 기록 · 파일 id · 표를 새 경로로 옮긴다. `same` 이면 내용이 같아 올리지 않는다. 다르면 옮긴
|
|
15803
|
+
* 기록을 옛 것으로 올려 옛 블록을 지운다. 예전에는 새 파일로 올려 노트 페이지에 같은 파일의 블록이 둘 남았다.
|
|
15804
|
+
*/
|
|
15805
|
+
async movedHere(file, pageId) {
|
|
15806
|
+
const name = file.path.slice(file.path.lastIndexOf("/") + 1);
|
|
15807
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
15808
|
+
const ids = folderFileIdsOf(this.stateDb, pageId);
|
|
15809
|
+
for (const entry of this.stateDb.getFilesByPageId(pageId)) {
|
|
15810
|
+
if (entry.localPath === file.path) continue;
|
|
15811
|
+
if (entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1) !== name) continue;
|
|
15812
|
+
if (record && !isFolderRecord(record) && !ids.has(entry.localPath)) continue;
|
|
15813
|
+
if (await this.vaultFs.exists(entry.localPath)) continue;
|
|
15814
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
15815
|
+
const same = entry.fileSize === file.size && entry.fileHash === createHash("sha256").update(buffer).digest("hex");
|
|
15816
|
+
const fileId = ids.get(entry.localPath);
|
|
15817
|
+
this.stateDb.deleteFileRegistry(entry.localPath);
|
|
15818
|
+
forgetFolderFileId(this.stateDb, pageId, entry.localPath);
|
|
15819
|
+
this.stateDb.registerFile({
|
|
15820
|
+
localPath: file.path,
|
|
15821
|
+
notionPageId: pageId,
|
|
15822
|
+
fileUploadId: entry.fileUploadId,
|
|
15823
|
+
fileType: entry.fileType,
|
|
15824
|
+
fileHash: entry.fileHash,
|
|
15825
|
+
fileSize: entry.fileSize
|
|
15826
|
+
});
|
|
15827
|
+
if (fileId) rememberFolderFileId(this.stateDb, pageId, file.path, fileId);
|
|
15828
|
+
if (same) rememberFolderFileStamp(this.stateDb, pageId, file.path, fileStampOf(file));
|
|
15829
|
+
return { entry: { ...entry, localPath: file.path }, same };
|
|
15830
|
+
}
|
|
15831
|
+
return null;
|
|
15832
|
+
}
|
|
15833
|
+
/**
|
|
15834
|
+
* 이 페이지에서 그 폴더 첨부의 블록 중 방금 붙인 것(`keep`) 말고를 지운다. 폴더 페이지는 같은 이름의 파일 블록 모두(id 를
|
|
15835
|
+
* 적기 전에 올린 블록도). 노트의 페이지는 노트가 임베드한 같은 이름의 파일이 있을 수 있어, 적어 둔 Notion 파일 id 의 블록과
|
|
15836
|
+
* 표시를 단 캡션({@link folderFileCaption})의 블록만 — 다른 볼트가 같은 파일을 먼저 고쳐 올린 블록은 id 를 모르지만 표시가 있다.
|
|
15837
|
+
* 예전에는 id 로만 지워, 두 볼트가 따로 고쳐 올리면 블록이 둘 남았다.
|
|
15838
|
+
*/
|
|
15839
|
+
async removeOlderBlocks(pageId, localPath, name, keep) {
|
|
15840
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
15841
|
+
const onNote = record !== null && !isFolderRecord(record);
|
|
15842
|
+
const fileId = folderFileIdsOf(this.stateDb, pageId).get(localPath);
|
|
15655
15843
|
for (const block of await this.notionClient.fetchAllChildren(pageId)) {
|
|
15656
15844
|
if (block.id === keep) continue;
|
|
15657
15845
|
const media = block[block.type];
|
|
15658
15846
|
const url = media?.type === "file" ? media.file?.url : void 0;
|
|
15659
15847
|
if (!url || uploadedFileNameFromUrl(url) !== name) continue;
|
|
15848
|
+
if (keep === void 0 && fileId !== void 0) {
|
|
15849
|
+
if (notionFileIdOf(url) !== fileId) continue;
|
|
15850
|
+
} else if (onNote) {
|
|
15851
|
+
const caption = (media?.caption ?? []).map((part) => part.plain_text ?? "").join("");
|
|
15852
|
+
const ours = fileId !== void 0 && notionFileIdOf(url) === fileId || caption === folderFileCaption(name);
|
|
15853
|
+
if (!ours) continue;
|
|
15854
|
+
}
|
|
15660
15855
|
await this.notionClient.deleteBlock(block.id);
|
|
15661
15856
|
}
|
|
15662
15857
|
}
|
|
15663
15858
|
/**
|
|
15664
|
-
* 폴더 첨부의 파일 블록 — 캡션에 파일 이름을
|
|
15665
|
-
* 자식 페이지가 없는 폴더 페이지는 PDF 하나만 붙인 노트와 모양이
|
|
15859
|
+
* 폴더 첨부의 파일 블록 — 캡션에 표시를 단 파일 이름을 적는다({@link folderFileCaption}). 받는 쪽이 그 표시로 폴더 첨부를
|
|
15860
|
+
* 알아본다 — 자식 페이지가 없는 폴더 페이지는 PDF 하나만 붙인 노트와 모양이 같고, 노트 페이지의 폴더 첨부는 그 노트의
|
|
15861
|
+
* 임베드와 같은 블록이다.
|
|
15666
15862
|
*/
|
|
15667
15863
|
createFileBlock(blockType, fileUploadId, name) {
|
|
15668
15864
|
const fileRef = {
|
|
15669
15865
|
type: "file_upload",
|
|
15670
15866
|
file_upload: { id: fileUploadId },
|
|
15671
|
-
caption: [{ type: "text", text: { content: name } }]
|
|
15867
|
+
caption: [{ type: "text", text: { content: folderFileCaption(name) } }]
|
|
15672
15868
|
};
|
|
15673
15869
|
switch (blockType) {
|
|
15674
15870
|
case "image":
|
|
@@ -19167,7 +19363,9 @@ var NoteRelocator = class {
|
|
|
19167
19363
|
if (isFolderRecord(record)) return [record.obsidianPath];
|
|
19168
19364
|
if (record.fileType === "folder-note") return [parentFolderOf(record.obsidianPath)];
|
|
19169
19365
|
const children = stripMarkdownExtension(record.obsidianPath);
|
|
19170
|
-
const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children))
|
|
19366
|
+
const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children)) || record.notionPageId !== null && [...folderFileIdsOf(this.stateDb, record.notionPageId).keys()].some(
|
|
19367
|
+
(path) => within(path, children)
|
|
19368
|
+
);
|
|
19171
19369
|
return hasChildren ? [record.obsidianPath, children] : [record.obsidianPath];
|
|
19172
19370
|
}
|
|
19173
19371
|
/** 옮기지 못할 까닭 — 없으면 null. */
|
|
@@ -19400,7 +19598,7 @@ async function notionBodyToObsidian(client, markdown, pageId) {
|
|
|
19400
19598
|
const bookmarks = bookmarkIds.length > 0 ? await client.getBookmarks(bookmarkIds) : void 0;
|
|
19401
19599
|
return notionEnhancedToObsidian(markdown, { userNames, codeTexts, bookmarks });
|
|
19402
19600
|
}
|
|
19403
|
-
async function pageBlocksToObsidian(client, pageId) {
|
|
19601
|
+
async function pageBlocksToObsidian(client, pageId, bodyOf5 = (markdown) => markdown) {
|
|
19404
19602
|
const tree = await client.getBlockTree(pageId);
|
|
19405
19603
|
const markdown = blocksToNfm(tree, { pageId });
|
|
19406
19604
|
const content = await notionBodyToObsidian(
|
|
@@ -19409,7 +19607,7 @@ async function pageBlocksToObsidian(client, pageId) {
|
|
|
19409
19607
|
getCodeBlockTexts: () => Promise.resolve(blocksOfType(tree, "code").map(plainTextOf)),
|
|
19410
19608
|
getBookmarks: () => Promise.resolve(bookmarksOf(tree))
|
|
19411
19609
|
},
|
|
19412
|
-
markdown,
|
|
19610
|
+
bodyOf5(markdown),
|
|
19413
19611
|
pageId
|
|
19414
19612
|
);
|
|
19415
19613
|
return { markdown, content };
|
|
@@ -23492,6 +23690,7 @@ function dropEmbeddedBaseMarkers(content, basePaths) {
|
|
|
23492
23690
|
|
|
23493
23691
|
// src/sync/database-discovery.ts
|
|
23494
23692
|
var DISCOVERY_RETRY_META_KEY = "discovery_retry";
|
|
23693
|
+
var PREVIEW_BODY_READ_BUDGET = 100;
|
|
23495
23694
|
function parseDiscoveryRetry(raw) {
|
|
23496
23695
|
if (!raw) return { parents: [], dbs: [] };
|
|
23497
23696
|
try {
|
|
@@ -23889,6 +24088,27 @@ var DatabaseDiscovery = class {
|
|
|
23889
24088
|
)
|
|
23890
24089
|
};
|
|
23891
24090
|
}
|
|
24091
|
+
/**
|
|
24092
|
+
* dry-run — 새로 받을 페이지의 본문을 읽어 그 안의 DB 를 찾는다({@link plannedNewDatabases} 가 그 행을 센다). 실제 pull 은
|
|
24093
|
+
* 받으며 찾는다. 예전에는 읽지 않고 «미리 세지 않았습니다» 만 알렸다. {@link PREVIEW_BODY_READ_BUDGET} 개까지만 읽는다 —
|
|
24094
|
+
* 넘거나 읽지 못한(다 받지 못한) 페이지는 {@link bodyRead} 가 아니어서 알림으로 남는다.
|
|
24095
|
+
*/
|
|
24096
|
+
async readNewPageBodies(pageIds, signal) {
|
|
24097
|
+
if (isDatabaseMode(this.config)) return;
|
|
24098
|
+
const unread = pageIds.filter((pageId) => !this.bodyRead(pageId));
|
|
24099
|
+
for (const pageId of unread.slice(0, PREVIEW_BODY_READ_BUDGET)) {
|
|
24100
|
+
if (signal?.aborted) return;
|
|
24101
|
+
try {
|
|
24102
|
+
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
24103
|
+
if (result.truncated || (result.unknown_block_ids ?? []).length > 0) continue;
|
|
24104
|
+
this.collectInlineDbRefs(pageId, result.markdown);
|
|
24105
|
+
} catch (error) {
|
|
24106
|
+
getLogger().debug(
|
|
24107
|
+
`[Im-Nobsidian] \uBBF8\uB9AC \uBCF4\uAE30: \uC0C8 \uD398\uC774\uC9C0 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD574 \uC548\uC758 DB \uB294 \uBC1B\uC73C\uBA70 \uCC3E\uC74C (${pageId}): ${error instanceof Error ? error.message : String(error)}`
|
|
24108
|
+
);
|
|
24109
|
+
}
|
|
24110
|
+
}
|
|
24111
|
+
}
|
|
23892
24112
|
/** 이번 pull 에서 본문을 받은 페이지인가 — 그 본문의 `<database>` 로 새 DB 를 찾았다. */
|
|
23893
24113
|
bodyRead(pageId) {
|
|
23894
24114
|
return this._bodyReadPages.has(normalizeNotionId(pageId));
|
|
@@ -24725,7 +24945,14 @@ var LocalPlanner = class {
|
|
|
24725
24945
|
live.add(target);
|
|
24726
24946
|
}
|
|
24727
24947
|
}
|
|
24728
|
-
const
|
|
24948
|
+
const filePairs = await this.movedFolderFilePairs(
|
|
24949
|
+
[...folderRecords].filter((folder) => !live.has(folder))
|
|
24950
|
+
);
|
|
24951
|
+
for (const { to } of filePairs) for (const folder of foldersOf([to])) live.add(folder);
|
|
24952
|
+
const pairs = [
|
|
24953
|
+
...scan.adoptions.map((a) => ({ from: a.record.obsidianPath, to: a.to })),
|
|
24954
|
+
...filePairs
|
|
24955
|
+
];
|
|
24729
24956
|
const folderMoves = [];
|
|
24730
24957
|
const databaseFolderMoves = [];
|
|
24731
24958
|
for (const move of deriveFolderMoves(tracked, live, pairs, hints)) {
|
|
@@ -24734,6 +24961,29 @@ var LocalPlanner = class {
|
|
|
24734
24961
|
}
|
|
24735
24962
|
return { folderMoves, databaseFolderMoves };
|
|
24736
24963
|
}
|
|
24964
|
+
/**
|
|
24965
|
+
* 사라진 폴더 페이지에 올린 폴더 첨부의 짝 — 노트 없이 파일만 든 폴더는 노트의 짝이 없어 이름을 바꿔도 옮긴 줄 몰랐다. 그
|
|
24966
|
+
* 폴더 페이지에 올린 파일과 이름 · 크기가 같은 파일이 볼트에 하나뿐이면 그리로 옮겼다. 예전에는 새 폴더 페이지를 만들어 다시
|
|
24967
|
+
* 올렸고, 삭제 동기화가 꺼져 있으면 옛 폴더 페이지가 남아 다른 볼트가 두 폴더를 받았다.
|
|
24968
|
+
*/
|
|
24969
|
+
async movedFolderFilePairs(missing) {
|
|
24970
|
+
const entries = missing.flatMap((folder) => {
|
|
24971
|
+
const pageId = this.stateDb.getByPath(folder)?.notionPageId;
|
|
24972
|
+
if (!pageId) return [];
|
|
24973
|
+
return this.stateDb.getFilesByPageId(pageId).filter((entry) => entry.localPath.startsWith(`${folder}/`));
|
|
24974
|
+
});
|
|
24975
|
+
if (entries.length === 0) return [];
|
|
24976
|
+
const files = await this.vaultFs.listNonMarkdownFiles();
|
|
24977
|
+
const nameOf = (path) => path.slice(path.lastIndexOf("/") + 1);
|
|
24978
|
+
const pairs = [];
|
|
24979
|
+
for (const entry of entries) {
|
|
24980
|
+
const found2 = files.filter(
|
|
24981
|
+
(file) => nameOf(file.path) === nameOf(entry.localPath) && file.size === entry.fileSize
|
|
24982
|
+
);
|
|
24983
|
+
if (found2.length === 1) pairs.push({ from: entry.localPath, to: found2[0].path });
|
|
24984
|
+
}
|
|
24985
|
+
return pairs;
|
|
24986
|
+
}
|
|
24737
24987
|
/** 레코드를 새 경로로 옮겨 적는다 — 이동 WAL · 위키링크 · 보존 마커도 함께. */
|
|
24738
24988
|
moveRecord(record, to) {
|
|
24739
24989
|
const from = record.obsidianPath;
|
|
@@ -27355,11 +27605,14 @@ var PagePusher = class {
|
|
|
27355
27605
|
async pushUpdatePage(pageId, markdownContent, notePath) {
|
|
27356
27606
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
27357
27607
|
const deferred = deferSyncedReferences(obsidianToNotionEnhanced(markdownContent));
|
|
27358
|
-
const { markdown, kept } = await this.imageHandler.keepUploadedMedia(
|
|
27608
|
+
const { markdown: body, kept } = await this.imageHandler.keepUploadedMedia(
|
|
27359
27609
|
pageId,
|
|
27360
27610
|
deferred.nfm,
|
|
27361
27611
|
notePath
|
|
27362
27612
|
);
|
|
27613
|
+
const folderFiles = await this.folderFileLines(pageId);
|
|
27614
|
+
const markdown = folderFiles ? `${body}
|
|
27615
|
+
${folderFiles}` : body;
|
|
27363
27616
|
const written = await replacePageBody(this.notionClient, pageId, markdown, {
|
|
27364
27617
|
databaseIdsOfBase: (basePath) => this.databaseIdsOfBase(basePath),
|
|
27365
27618
|
unknownTagFallbacks: unknownTagFallbacks(markdownContent)
|
|
@@ -27379,6 +27632,7 @@ var PagePusher = class {
|
|
|
27379
27632
|
"\uC790\uC2DD \uD398\uC774\uC9C0 \xB7 DB \uAC00 \uC788\uB294 \uD398\uC774\uC9C0\uB294 \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBCF4\uB0B4\uBA74 \uC790\uC2DD\uAE4C\uC9C0 \uC9C0\uC6CC\uC838 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C \u2014 \uC124\uC815 conversion.preferMarkdownApi \uB97C \uAE30\uBCF8\uAC12(true)\uC73C\uB85C \uB450\uACE0 \uB2E4\uC2DC push \uD558\uC138\uC694"
|
|
27380
27633
|
);
|
|
27381
27634
|
}
|
|
27635
|
+
this.forgetFolderFilesForBlockMode(pageId, notePath);
|
|
27382
27636
|
const { nfm, blocks } = blockModeBody(markdownContent);
|
|
27383
27637
|
if (blocks.length > 0) {
|
|
27384
27638
|
await this.notionClient.appendBlockTree(pageId, blocks);
|
|
@@ -27396,6 +27650,32 @@ var PagePusher = class {
|
|
|
27396
27650
|
);
|
|
27397
27651
|
return { fingerprint: null, kept: [], sent: nfm, syncedRefs: [], syncedRefText: "" };
|
|
27398
27652
|
}
|
|
27653
|
+
/**
|
|
27654
|
+
* 노트 페이지의 폴더 첨부 줄(ADR-056) — 본문을 통째로 바꿀 때 받은 줄 그대로 끝에 되실어야 Notion 이 그 블록을 둔다
|
|
27655
|
+
* ({@link ImageHandler.keepUploadedMedia} 와 같은 실측). 이 페이지에 적어 둔 폴더 첨부가 없으면 읽지 않는다. 읽지 못하면
|
|
27656
|
+
* 던진다 — 모르고 바꾸면 폴더 첨부가 지워진다.
|
|
27657
|
+
*/
|
|
27658
|
+
async folderFileLines(pageId) {
|
|
27659
|
+
const ids = new Set(folderFileIdsOf(this.stateDb, pageId).values());
|
|
27660
|
+
if (ids.size === 0) return "";
|
|
27661
|
+
const remote = (await this.notionClient.getPageMarkdown(pageId)).markdown;
|
|
27662
|
+
return (folderFilesInNote(remote, { knownIds: ids })?.lines ?? []).join("\n");
|
|
27663
|
+
}
|
|
27664
|
+
/**
|
|
27665
|
+
* 블록 방식은 본문 블록을 모두 지우고 새로 붙인다 — 폴더 첨부 블록을 남기면 새 본문이 그 뒤에 붙는다.
|
|
27666
|
+
* 함께 지우고 기록을 놓는다 — 이어지는 첨부 push 가 볼트의 파일을 다시 끝에 올린다.
|
|
27667
|
+
*/
|
|
27668
|
+
forgetFolderFilesForBlockMode(pageId, notePath) {
|
|
27669
|
+
const ids = folderFileIdsOf(this.stateDb, pageId);
|
|
27670
|
+
if (ids.size === 0) return;
|
|
27671
|
+
for (const localPath of ids.keys()) {
|
|
27672
|
+
this.stateDb.deleteFileRegistry(localPath);
|
|
27673
|
+
forgetFolderFileId(this.stateDb, pageId, localPath);
|
|
27674
|
+
}
|
|
27675
|
+
getLogger().info(
|
|
27676
|
+
`[Im-Nobsidian] \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBC14\uAFD4 \uD3F4\uB354 \uCCA8\uBD80 ${ids.size}\uAC1C\uB97C \uB2E4\uC2DC \uC62C\uB9BC (${notePath})`
|
|
27677
|
+
);
|
|
27678
|
+
}
|
|
27399
27679
|
/**
|
|
27400
27680
|
* 자리표시로 보낸 코드를 채우고(S-22) 동기화 블록 참조를 붙인다({@link deferSyncedReferences}) — 본문을
|
|
27401
27681
|
* 바꾼 뒤(블록 방식이면 옛 블록을 지운 뒤) 부른다.
|
|
@@ -27773,7 +28053,10 @@ var PagePuller = class {
|
|
|
27773
28053
|
const local = await readLocalNote(this.vaultFs, filePath);
|
|
27774
28054
|
rendered = await render2(filePath, local);
|
|
27775
28055
|
const relinked = local === void 0 ? null : this.relinkExistingNote(filePath, claim.existing, local, rendered, note);
|
|
27776
|
-
if (relinked)
|
|
28056
|
+
if (relinked) {
|
|
28057
|
+
await this.receiveNoteFiles(filePath, fileType, pageId, fetched);
|
|
28058
|
+
return relinked;
|
|
28059
|
+
}
|
|
27777
28060
|
this.claimedPaths.delete(pathKey(filePath));
|
|
27778
28061
|
filePath = (await this.resolveUniqueFilePath(noteDir, safeName, pageId)).path;
|
|
27779
28062
|
rendered = await render2(filePath);
|
|
@@ -27786,8 +28069,19 @@ var PagePuller = class {
|
|
|
27786
28069
|
properties: rendered.properties,
|
|
27787
28070
|
status: "synced"
|
|
27788
28071
|
});
|
|
28072
|
+
await this.receiveNoteFiles(filePath, fileType, pageId, fetched);
|
|
27789
28073
|
return { path: filePath };
|
|
27790
28074
|
}
|
|
28075
|
+
/** 새로 받은 노트 페이지의 폴더 첨부를 그 노트의 폴더에 받는다(ADR-056) — 폴더 노트는 그 폴더, 노트는 이름의 폴더. */
|
|
28076
|
+
async receiveNoteFiles(filePath, fileType, pageId, fetched) {
|
|
28077
|
+
if (fetched.noteFiles.length === 0) return;
|
|
28078
|
+
await this.receiveFolderFiles(
|
|
28079
|
+
childFolderOf({ fileType, obsidianPath: filePath }, this.placement.folderLookup()),
|
|
28080
|
+
pageId,
|
|
28081
|
+
fetched.noteFiles,
|
|
28082
|
+
fetched.noteFilesComplete
|
|
28083
|
+
);
|
|
28084
|
+
}
|
|
27791
28085
|
/**
|
|
27792
28086
|
* 상태 기록에 없는 노트가 이 페이지가 놓일 자리에 이미 있다 — 상태 DB 를 잃었거나 처음 잇는 볼트다(P7).
|
|
27793
28087
|
* 예전에는 남의 노트로 보고 id 를 붙인 이름으로 또 받아 모든 노트가 두 벌이 됐다(push 는 모든 페이지를 또 만들었다).
|
|
@@ -27946,6 +28240,14 @@ var PagePuller = class {
|
|
|
27946
28240
|
}
|
|
27947
28241
|
const rendered = await this.renderRemotePage(record, change.pageId, page);
|
|
27948
28242
|
const remoteContent = rendered.content;
|
|
28243
|
+
if (rendered.folderFiles && !remoteGone) {
|
|
28244
|
+
await this.receiveFolderFiles(
|
|
28245
|
+
childFolderOf(record, this.placement.folderLookup()),
|
|
28246
|
+
change.pageId,
|
|
28247
|
+
rendered.folderFiles.files,
|
|
28248
|
+
rendered.folderFiles.complete
|
|
28249
|
+
);
|
|
28250
|
+
}
|
|
27949
28251
|
let localContent;
|
|
27950
28252
|
let localExists = true;
|
|
27951
28253
|
try {
|
|
@@ -28089,7 +28391,10 @@ var PagePuller = class {
|
|
|
28089
28391
|
if (record.fileType === "db-row") {
|
|
28090
28392
|
return this.databaseSyncer.renderRow(page, record.obsidianPath, { downloadMedia });
|
|
28091
28393
|
}
|
|
28092
|
-
const
|
|
28394
|
+
const local = await readLocalNote(this.vaultFs, record.obsidianPath);
|
|
28395
|
+
const fetched = await this.fetchPageMarkdown(pageId, {
|
|
28396
|
+
embedded: embeddedNamesOf(local === void 0 ? [] : [local])
|
|
28397
|
+
});
|
|
28093
28398
|
const title = this.notionClient.extractTitle(page);
|
|
28094
28399
|
const { content, properties } = await this.renderPageNote({
|
|
28095
28400
|
pageId,
|
|
@@ -28100,11 +28405,17 @@ var PagePuller = class {
|
|
|
28100
28405
|
titleFollowsName: this.titleFollowsName(record, title),
|
|
28101
28406
|
downloadMedia,
|
|
28102
28407
|
local: {
|
|
28103
|
-
content:
|
|
28408
|
+
content: local,
|
|
28104
28409
|
markers: this.stateDb.getPreserveMarkers(record.obsidianPath)
|
|
28105
28410
|
}
|
|
28106
28411
|
});
|
|
28107
|
-
return {
|
|
28412
|
+
return {
|
|
28413
|
+
content,
|
|
28414
|
+
title,
|
|
28415
|
+
properties,
|
|
28416
|
+
bodyFingerprint: fetched.fingerprint,
|
|
28417
|
+
folderFiles: { files: fetched.noteFiles, complete: fetched.noteFilesComplete }
|
|
28418
|
+
};
|
|
28108
28419
|
}
|
|
28109
28420
|
/**
|
|
28110
28421
|
* 받은 페이지 본문을 볼트에 쓸 노트로 — 새로 받기(`createPulledPage`)와 다시 받기({@link renderRemotePage})가
|
|
@@ -28172,19 +28483,27 @@ var PagePuller = class {
|
|
|
28172
28483
|
/**
|
|
28173
28484
|
* 페이지 본문을 볼트 쪽 markdown 으로 받는다.
|
|
28174
28485
|
*
|
|
28486
|
+
* @param options.embedded 로컬 노트가 임베드한 파일 이름 — 폴더 첨부({@link folderFilesInNote})를 노트에서 떼어
|
|
28487
|
+
* `noteFiles` 로 돌려줄 때, 이 이름 · 이 볼트가 노트 첨부로 올린 파일은 노트의 것으로 둔다(ADR-056).
|
|
28175
28488
|
* @returns `fingerprint` 는 받은 원격 본문의 지문({@link remoteBodyFingerprint}). 블록 API 로
|
|
28176
28489
|
* 받았으면 null — 지문은 Markdown API 의 본문으로만 견준다.
|
|
28177
28490
|
*/
|
|
28178
|
-
async fetchPageMarkdown(pageId) {
|
|
28491
|
+
async fetchPageMarkdown(pageId, options = {}) {
|
|
28492
|
+
const noteFilesOf = (markdown) => this.noteFolderFilesOf(pageId, markdown, options);
|
|
28179
28493
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
28180
28494
|
try {
|
|
28181
28495
|
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
28182
28496
|
this.discovery.collectInlineDbRefs(pageId, result.markdown);
|
|
28183
28497
|
this.detector.noteBodyChildren(pageId, result.markdown);
|
|
28498
|
+
const found2 = noteFilesOf(result.markdown);
|
|
28184
28499
|
return {
|
|
28185
28500
|
content: await this.resolveBlockLinks(
|
|
28186
28501
|
this.resolveNotionIdWikilinks(
|
|
28187
|
-
await notionBodyToObsidian(
|
|
28502
|
+
await notionBodyToObsidian(
|
|
28503
|
+
this.notionClient,
|
|
28504
|
+
found2?.markdown ?? result.markdown,
|
|
28505
|
+
pageId
|
|
28506
|
+
)
|
|
28188
28507
|
),
|
|
28189
28508
|
pageId
|
|
28190
28509
|
),
|
|
@@ -28194,7 +28513,9 @@ var PagePuller = class {
|
|
|
28194
28513
|
children: extractChildTags(result.markdown),
|
|
28195
28514
|
fingerprint: remoteBodyFingerprint(result.markdown),
|
|
28196
28515
|
hasBody: hasBodyBesidesChildren(result.markdown),
|
|
28197
|
-
folderFiles: folderFilesInBody(result.markdown)
|
|
28516
|
+
folderFiles: folderFilesInBody(result.markdown),
|
|
28517
|
+
noteFiles: found2?.files ?? [],
|
|
28518
|
+
noteFilesComplete: !result.truncated && this.noteFilesHoldAll(pageId, found2?.markdown ?? result.markdown)
|
|
28198
28519
|
};
|
|
28199
28520
|
} catch (error) {
|
|
28200
28521
|
getLogger().warn(
|
|
@@ -28202,7 +28523,14 @@ var PagePuller = class {
|
|
|
28202
28523
|
);
|
|
28203
28524
|
}
|
|
28204
28525
|
}
|
|
28205
|
-
|
|
28526
|
+
let noteFiles = [];
|
|
28527
|
+
let noteFilesComplete = true;
|
|
28528
|
+
const fallback = await pageBlocksToObsidian(this.notionClient, pageId, (markdown) => {
|
|
28529
|
+
const found2 = noteFilesOf(markdown);
|
|
28530
|
+
noteFiles = found2?.files ?? [];
|
|
28531
|
+
noteFilesComplete = this.noteFilesHoldAll(pageId, found2?.markdown ?? markdown);
|
|
28532
|
+
return found2?.markdown ?? markdown;
|
|
28533
|
+
});
|
|
28206
28534
|
this.discovery.collectInlineDbRefs(pageId, fallback.markdown);
|
|
28207
28535
|
this.detector.noteBodyChildren(pageId, fallback.markdown);
|
|
28208
28536
|
return {
|
|
@@ -28214,9 +28542,28 @@ var PagePuller = class {
|
|
|
28214
28542
|
children: extractChildTags(fallback.markdown),
|
|
28215
28543
|
fingerprint: null,
|
|
28216
28544
|
hasBody: hasBodyBesidesChildren(fallback.markdown),
|
|
28217
|
-
folderFiles: folderFilesInBody(fallback.markdown)
|
|
28545
|
+
folderFiles: folderFilesInBody(fallback.markdown),
|
|
28546
|
+
noteFiles,
|
|
28547
|
+
noteFilesComplete
|
|
28218
28548
|
};
|
|
28219
28549
|
}
|
|
28550
|
+
/** 노트 페이지의 폴더 첨부({@link folderFilesInNote}) — 없으면 null. */
|
|
28551
|
+
noteFolderFilesOf(pageId, markdown, options) {
|
|
28552
|
+
const media = recallUploadedMedia(this.stateDb, pageId);
|
|
28553
|
+
return folderFilesInNote(markdown, {
|
|
28554
|
+
knownIds: new Set(folderFileIdsOf(this.stateDb, pageId).values()),
|
|
28555
|
+
exclude: (file) => {
|
|
28556
|
+
const fileId = notionFileIdOf(file.url);
|
|
28557
|
+
return options.embedded?.has(file.name) === true || fileId !== null && media.has(fileId);
|
|
28558
|
+
}
|
|
28559
|
+
});
|
|
28560
|
+
}
|
|
28561
|
+
/** 폴더 첨부를 뗀 본문에 적어 둔 폴더 첨부가 남지 않았나 — {@link fetchPageMarkdown} 의 `noteFilesComplete`. */
|
|
28562
|
+
noteFilesHoldAll(pageId, rest) {
|
|
28563
|
+
const ids = new Set(folderFileIdsOf(this.stateDb, pageId).values());
|
|
28564
|
+
if (ids.size === 0) return true;
|
|
28565
|
+
return !notionMediaSpans(rest).some((span) => ids.has(notionFileIdOf(span.url) ?? ""));
|
|
28566
|
+
}
|
|
28220
28567
|
/** 폴더 첨부를 받아 받은 파일 수에 더한다. */
|
|
28221
28568
|
async receiveFolderFiles(folder, pageId, files, complete2 = false) {
|
|
28222
28569
|
this._pullFileCount += await this.fileHandler.receiveFolderFiles(
|
|
@@ -28688,6 +29035,10 @@ var PullPlanner = class {
|
|
|
28688
29035
|
const relinkPaths = new Map(
|
|
28689
29036
|
[...relinks.byPage].map(([pageId, relink]) => [compactNotionId(pageId), relink.path])
|
|
28690
29037
|
);
|
|
29038
|
+
await this.discovery.readNewPageBodies(
|
|
29039
|
+
planned.filter((change) => change.type === "created").map((change) => change.pageId),
|
|
29040
|
+
options.signal
|
|
29041
|
+
);
|
|
28691
29042
|
const newDatabases = await this.discovery.plannedNewDatabases(failed, {
|
|
28692
29043
|
rescanRoot: this.detector.rootChanged(),
|
|
28693
29044
|
notePathOf: (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? null
|
|
@@ -29880,7 +30231,11 @@ var SyncOrchestrator = class {
|
|
|
29880
30231
|
async pushVaultFiles(failed) {
|
|
29881
30232
|
if (this.config.sync.syncFiles === false) return 0;
|
|
29882
30233
|
try {
|
|
29883
|
-
const fileResults = await this.fileHandler.pushAllFiles(
|
|
30234
|
+
const fileResults = await this.fileHandler.pushAllFiles(
|
|
30235
|
+
failed,
|
|
30236
|
+
this.placement,
|
|
30237
|
+
this.notePageWrites()
|
|
30238
|
+
);
|
|
29884
30239
|
if (fileResults.length > 0) {
|
|
29885
30240
|
getLogger().info(`[Im-Nobsidian] ${fileResults.length}\uAC1C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC`);
|
|
29886
30241
|
}
|
|
@@ -29890,6 +30245,42 @@ var SyncOrchestrator = class {
|
|
|
29890
30245
|
return 0;
|
|
29891
30246
|
}
|
|
29892
30247
|
}
|
|
30248
|
+
/**
|
|
30249
|
+
* 폴더 첨부를 노트의 페이지에 붙이거나 지울 때({@link NotePageWrites}) — 쓰기 전에 원격과 맞던 노트만 쓴 뒤의 원격을 본
|
|
30250
|
+
* 것으로 적는다. 받지 않은 Notion 편집이 걸린 노트를 적으면 다음 pull 이 그 편집을 받지 않는다. 읽지 못하면 적지 않는다 —
|
|
30251
|
+
* 다음 push 가 그 노트를 올리지 않고 pull 을 먼저 하라고 알릴 뿐이다.
|
|
30252
|
+
*/
|
|
30253
|
+
notePageWrites() {
|
|
30254
|
+
const inSync = /* @__PURE__ */ new Map();
|
|
30255
|
+
return {
|
|
30256
|
+
begin: async (pageId) => {
|
|
30257
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
30258
|
+
if (!record || isFolderRecord(record)) return;
|
|
30259
|
+
try {
|
|
30260
|
+
const page = await this.notionClient.getPage(pageId);
|
|
30261
|
+
if (await this.drift.remoteDrift(record, page) === "none")
|
|
30262
|
+
inSync.set(pageId, record.id);
|
|
30263
|
+
} catch (error) {
|
|
30264
|
+
getLogger().warn(
|
|
30265
|
+
`[Im-Nobsidian] \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBD99\uC77C \uB178\uD2B8\uC758 \uC6D0\uACA9\uC744 \uC77D\uC9C0 \uBABB\uD568 (${record.obsidianPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
30266
|
+
);
|
|
30267
|
+
}
|
|
30268
|
+
},
|
|
30269
|
+
end: async (pageId) => {
|
|
30270
|
+
const recordId = inSync.get(pageId);
|
|
30271
|
+
if (recordId === void 0) return;
|
|
30272
|
+
inSync.delete(pageId);
|
|
30273
|
+
try {
|
|
30274
|
+
const page = await this.notionClient.getPage(pageId);
|
|
30275
|
+
this.observation.record(recordId, page, await this.drift.remoteBodyFingerprintOf(pageId));
|
|
30276
|
+
} catch (error) {
|
|
30277
|
+
getLogger().warn(
|
|
30278
|
+
`[Im-Nobsidian] \uD3F4\uB354 \uCCA8\uBD80\uB97C \uBD99\uC778 \uB178\uD2B8\uC758 \uC6D0\uACA9\uC744 \uC801\uC9C0 \uBABB\uD568 (${pageId}): ${error instanceof Error ? error.message : String(error)}`
|
|
30279
|
+
);
|
|
30280
|
+
}
|
|
30281
|
+
}
|
|
30282
|
+
};
|
|
30283
|
+
}
|
|
29893
30284
|
/**
|
|
29894
30285
|
* @param mergedByPull sync dry-run 만 — 앞선 pull dry-run 이 합칠 노트. 실제 sync 는 합친 뒤 원격을 본 것으로
|
|
29895
30286
|
* 적어 push 가 올린다 — dry-run 은 받지 않아 원격이 바뀐 그대로라, 원격을 보고 거절하지 않는다.
|
|
@@ -32468,7 +32859,7 @@ function hasColumnLayout(text) {
|
|
|
32468
32859
|
var START_RE = new RegExp(`^[\\t ]*${COLUMN_LIST_START}[\\t ]*$`);
|
|
32469
32860
|
var END_RE = new RegExp(`^[\\t ]*${COLUMN_LIST_END}[\\t ]*$`);
|
|
32470
32861
|
var SEP_RE = new RegExp(`^[\\t ]*${COLUMN_SEP_SOURCE}[\\t ]*$`);
|
|
32471
|
-
var
|
|
32862
|
+
var FENCE_RE2 = /^[\t ]*(`{3,}|~{3,})/;
|
|
32472
32863
|
function readColumnLayout(lines) {
|
|
32473
32864
|
const places = new Array(lines.length);
|
|
32474
32865
|
let group2 = -1;
|
|
@@ -32492,7 +32883,7 @@ function readColumnLayout(lines) {
|
|
|
32492
32883
|
open = null;
|
|
32493
32884
|
};
|
|
32494
32885
|
lines.forEach((line, i) => {
|
|
32495
|
-
const fenceMatch =
|
|
32886
|
+
const fenceMatch = FENCE_RE2.exec(line);
|
|
32496
32887
|
if (fence) {
|
|
32497
32888
|
if (fenceMatch && fenceMatch[1].startsWith(fence)) fence = null;
|
|
32498
32889
|
return;
|