@im-nobsidian/core 0.21.0 → 0.22.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 +47 -12
- package/dist/index.js +275 -108
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3753,7 +3753,7 @@ var NotionClient2 = class _NotionClient {
|
|
|
3753
3753
|
return blocks;
|
|
3754
3754
|
}
|
|
3755
3755
|
/**
|
|
3756
|
-
* 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로
|
|
3756
|
+
* 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로 뒤**에, `options.atStart` 면 맨 앞에 삽입한다
|
|
3757
3757
|
* (기본은 맨 끝). 자리표시자를 실제 미디어 블록으로 제자리 교체할 때 필요하다.
|
|
3758
3758
|
*
|
|
3759
3759
|
* 실측(2025-09-03): position 삽입 응답의 `results` 는 방금 넣은 블록뿐 아니라
|
|
@@ -3771,19 +3771,24 @@ var NotionClient2 = class _NotionClient {
|
|
|
3771
3771
|
const batchSize = this.batchSize;
|
|
3772
3772
|
const created = [];
|
|
3773
3773
|
let after = options?.after;
|
|
3774
|
+
let atStart = after === void 0 && options?.atStart === true;
|
|
3774
3775
|
for (let i = 0; i < children.length; i += batchSize) {
|
|
3775
3776
|
const batch = children.slice(i, i + batchSize);
|
|
3777
|
+
const position = after ? { position: { type: "after_block", after_block: { id: after } } } : atStart ? { position: { type: "start" } } : {};
|
|
3776
3778
|
const response = await this.withRateLimit(
|
|
3777
3779
|
() => this.client.blocks.children.append({
|
|
3778
3780
|
block_id: blockId,
|
|
3779
3781
|
children: batch,
|
|
3780
|
-
...
|
|
3782
|
+
...position
|
|
3781
3783
|
}),
|
|
3782
3784
|
CREATES_BLOCKS
|
|
3783
3785
|
);
|
|
3784
3786
|
const blocks = response.results.slice(0, batch.length);
|
|
3785
3787
|
created.push(...blocks);
|
|
3786
|
-
if (after && blocks.length > 0)
|
|
3788
|
+
if ((after || atStart) && blocks.length > 0) {
|
|
3789
|
+
after = blocks[blocks.length - 1].id;
|
|
3790
|
+
atStart = false;
|
|
3791
|
+
}
|
|
3787
3792
|
}
|
|
3788
3793
|
return created;
|
|
3789
3794
|
}
|
|
@@ -3792,9 +3797,11 @@ var NotionClient2 = class _NotionClient {
|
|
|
3792
3797
|
* 자식은 부모를 만든 뒤 그 아래에 마저 붙인다({@link shapeAppendRequests}). 더 깊은 블록의 id 는
|
|
3793
3798
|
* 응답에 없어 부모의 자식 목록을 읽어 찾는다.
|
|
3794
3799
|
*
|
|
3800
|
+
* `options.atStart` 면 맨 위 블록을 맨 앞에 넣는다 — 본문 뒤에 둘 블록(폴더 첨부)을 남기고 본문만 바꿀 때.
|
|
3801
|
+
*
|
|
3795
3802
|
* 중간에 실패하면 그때까지 만든 블록은 남는다 — 호출측이 실패로 알린다.
|
|
3796
3803
|
*/
|
|
3797
|
-
async appendBlockTree(parentId, blocks) {
|
|
3804
|
+
async appendBlockTree(parentId, blocks, options) {
|
|
3798
3805
|
const listed = /* @__PURE__ */ new Map();
|
|
3799
3806
|
const childIds = async (id) => {
|
|
3800
3807
|
let ids = listed.get(id);
|
|
@@ -3805,15 +3812,19 @@ var NotionClient2 = class _NotionClient {
|
|
|
3805
3812
|
return ids;
|
|
3806
3813
|
};
|
|
3807
3814
|
const queue = [
|
|
3808
|
-
{ parentId, blocks }
|
|
3815
|
+
{ parentId, blocks, top: true }
|
|
3809
3816
|
];
|
|
3817
|
+
let topAfter;
|
|
3810
3818
|
while (queue.length > 0) {
|
|
3811
3819
|
const next = queue.shift();
|
|
3812
3820
|
for (const request of shapeAppendRequests(next.blocks)) {
|
|
3821
|
+
const position = next.top && options?.atStart ? topAfter ? { after: topAfter } : { atStart: true } : void 0;
|
|
3813
3822
|
const created = await this.appendChildren(
|
|
3814
3823
|
next.parentId,
|
|
3815
|
-
request.map((shaped) => shaped.body)
|
|
3824
|
+
request.map((shaped) => shaped.body),
|
|
3825
|
+
position
|
|
3816
3826
|
);
|
|
3827
|
+
if (position) topAfter = created[created.length - 1] ?? topAfter;
|
|
3817
3828
|
for (const [k, shaped] of request.entries()) {
|
|
3818
3829
|
for (const pending of shaped.pending) {
|
|
3819
3830
|
let id = created[k];
|
|
@@ -15174,6 +15185,21 @@ function selectStaleDbArtifacts(files, localFolder, keepPaths) {
|
|
|
15174
15185
|
return stale;
|
|
15175
15186
|
}
|
|
15176
15187
|
|
|
15188
|
+
// src/sync/parent-mode.ts
|
|
15189
|
+
function isDatabaseMode(config) {
|
|
15190
|
+
return config.notion.parentMode === "database" && !!config.notion.databaseId;
|
|
15191
|
+
}
|
|
15192
|
+
function rowDatabaseOf(config, record) {
|
|
15193
|
+
if (record.fileType === "db-row") {
|
|
15194
|
+
if (!record.notionParentId) {
|
|
15195
|
+
throw new Error(`DB \uD589\uC778\uB370 \uC18C\uC18D DB \uB97C \uC54C \uC218 \uC5C6\uC74C: ${record.obsidianPath}`);
|
|
15196
|
+
}
|
|
15197
|
+
return record.notionParentId;
|
|
15198
|
+
}
|
|
15199
|
+
if (isDatabaseMode(config)) return config.notion.databaseId;
|
|
15200
|
+
return null;
|
|
15201
|
+
}
|
|
15202
|
+
|
|
15177
15203
|
// src/sync/folder-files.ts
|
|
15178
15204
|
function unescapeCaption(caption) {
|
|
15179
15205
|
return caption.replace(/\\([\\`*_{}[\]()#+\-.!|~<>$&])/g, "$1").trim();
|
|
@@ -15200,7 +15226,20 @@ function folderFileCaption(name) {
|
|
|
15200
15226
|
function isMarkedCaption(label, name) {
|
|
15201
15227
|
return unescapeCaption(label) === folderFileCaption(name);
|
|
15202
15228
|
}
|
|
15203
|
-
|
|
15229
|
+
function uploadedFileOfBlock(block) {
|
|
15230
|
+
const media = block[block.type];
|
|
15231
|
+
const url = media?.type === "file" ? media.file?.url : void 0;
|
|
15232
|
+
if (!url) return null;
|
|
15233
|
+
return { url, caption: (media?.caption ?? []).map((part) => part.plain_text ?? "").join("") };
|
|
15234
|
+
}
|
|
15235
|
+
function isFolderFileBlock(block, knownIds) {
|
|
15236
|
+
const file = uploadedFileOfBlock(block);
|
|
15237
|
+
if (!file) return false;
|
|
15238
|
+
const name = uploadedFileNameFromUrl(file.url);
|
|
15239
|
+
const fileId = notionFileIdOf(file.url);
|
|
15240
|
+
return fileId !== null && knownIds.has(fileId) || name !== null && file.caption === folderFileCaption(name);
|
|
15241
|
+
}
|
|
15242
|
+
var FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
15204
15243
|
function folderFilesInNote(markdown, options = {}) {
|
|
15205
15244
|
const all = markdown.split("\n");
|
|
15206
15245
|
const drop = /* @__PURE__ */ new Set();
|
|
@@ -15214,7 +15253,7 @@ function folderFilesInNote(markdown, options = {}) {
|
|
|
15214
15253
|
return;
|
|
15215
15254
|
}
|
|
15216
15255
|
if (fence !== null) return;
|
|
15217
|
-
const full = line.
|
|
15256
|
+
const full = line.trim();
|
|
15218
15257
|
const [span, ...more] = notionMediaSpans(full);
|
|
15219
15258
|
if (!span || more.length > 0 || span.full !== full || !/^https?:\/\//.test(span.url)) return;
|
|
15220
15259
|
const name = uploadedFileNameFromUrl(span.url);
|
|
@@ -15234,6 +15273,26 @@ function folderFilesInNote(markdown, options = {}) {
|
|
|
15234
15273
|
lines
|
|
15235
15274
|
};
|
|
15236
15275
|
}
|
|
15276
|
+
function noteFolderFilesOfPage(stateDb, pageId, markdown, embedded) {
|
|
15277
|
+
const ids = new Set(folderFileIdsOf(stateDb, pageId).values());
|
|
15278
|
+
const media = recallUploadedMedia(stateDb, pageId);
|
|
15279
|
+
const found2 = folderFilesInNote(markdown, {
|
|
15280
|
+
knownIds: ids,
|
|
15281
|
+
exclude: (file) => {
|
|
15282
|
+
const fileId = notionFileIdOf(file.url);
|
|
15283
|
+
return embedded?.has(file.name) === true || fileId !== null && media.has(fileId);
|
|
15284
|
+
}
|
|
15285
|
+
});
|
|
15286
|
+
const rest = found2?.markdown ?? markdown;
|
|
15287
|
+
return {
|
|
15288
|
+
found: found2,
|
|
15289
|
+
holdsAll: ids.size === 0 || !notionMediaSpans(rest).some((span) => ids.has(notionFileIdOf(span.url) ?? ""))
|
|
15290
|
+
};
|
|
15291
|
+
}
|
|
15292
|
+
function rootFilesPageOf(config) {
|
|
15293
|
+
if (isDatabaseMode(config) || !config.notion.rootPageId) return null;
|
|
15294
|
+
return normalizeNotionId(config.notion.rootPageId);
|
|
15295
|
+
}
|
|
15237
15296
|
var WIKI_EMBED_RE = /!\[\[([^\]|#^\n]+)/g;
|
|
15238
15297
|
var MD_EMBED_RE = /!\[[^\]\n]*\]\(<?([^)>\s]+)/g;
|
|
15239
15298
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
@@ -15306,6 +15365,9 @@ function rememberFolderFileStamp(stateDb, pageId, localPath, stamp) {
|
|
|
15306
15365
|
}
|
|
15307
15366
|
|
|
15308
15367
|
// src/sync/file-handler.ts
|
|
15368
|
+
function flattenBlockTree2(blocks) {
|
|
15369
|
+
return blocks.flatMap((block) => [block, ...flattenBlockTree2(block.children ?? [])]);
|
|
15370
|
+
}
|
|
15309
15371
|
function isHiddenPath(path) {
|
|
15310
15372
|
return path.split("/").some((part) => part.startsWith("."));
|
|
15311
15373
|
}
|
|
@@ -15317,7 +15379,10 @@ function filesFolderOfNote(record) {
|
|
|
15317
15379
|
if (record.fileType === "folder-note" && isFolderNotePath(record.obsidianPath)) {
|
|
15318
15380
|
return getFolderPath(record.obsidianPath);
|
|
15319
15381
|
}
|
|
15320
|
-
return record
|
|
15382
|
+
return isNamedNote(record) ? stripMarkdownExtension(record.obsidianPath) : null;
|
|
15383
|
+
}
|
|
15384
|
+
function isNamedNote(record) {
|
|
15385
|
+
return record.fileType === "file" || record.fileType === "db-row";
|
|
15321
15386
|
}
|
|
15322
15387
|
var LOGGED_NAMES = 5;
|
|
15323
15388
|
var FileHandler = class {
|
|
@@ -15330,6 +15395,7 @@ var FileHandler = class {
|
|
|
15330
15395
|
this.downloadTimeoutMs = options?.downloadTimeoutMs ?? DEFAULT_CONFIG.advanced.mediaDownloadTimeoutMs;
|
|
15331
15396
|
this.itemTimeoutMs = options?.itemTimeoutMs ?? DEFAULT_CONFIG.advanced.itemTimeoutMs;
|
|
15332
15397
|
this.deleteSync = options?.deleteSync ?? false;
|
|
15398
|
+
this.rootPageId = options?.rootPageId ?? null;
|
|
15333
15399
|
this.attachmentsFolder = (options?.attachmentsFolder ?? DEFAULT_CONFIG.paths.attachments).replace(/^\/+|\/+$/g, "");
|
|
15334
15400
|
}
|
|
15335
15401
|
vaultFs;
|
|
@@ -15341,6 +15407,7 @@ var FileHandler = class {
|
|
|
15341
15407
|
itemTimeoutMs;
|
|
15342
15408
|
attachmentsFolder;
|
|
15343
15409
|
deleteSync;
|
|
15410
|
+
rootPageId;
|
|
15344
15411
|
get fetchFn() {
|
|
15345
15412
|
return this.customFetch ?? globalThis.fetch;
|
|
15346
15413
|
}
|
|
@@ -15420,19 +15487,22 @@ var FileHandler = class {
|
|
|
15420
15487
|
}
|
|
15421
15488
|
/**
|
|
15422
15489
|
* 폴더 첨부를 둘 Notion 페이지 — 그 폴더의 Notion 자리({@link folderContainer} 와 같은 차례). 폴더 노트(`X/X.md`)나 노트
|
|
15423
|
-
* 이름의 폴더(`X.md` 옆 `X/`)면 그 노트의 페이지 끝(ADR-056), 아니면 push 가 만든 폴더 페이지. 맨 위는 루트
|
|
15424
|
-
* 행
|
|
15490
|
+
* 이름의 폴더(`X.md` 옆 `X/`)면 그 노트의 페이지 끝(ADR-056), 아니면 push 가 만든 폴더 페이지. 맨 위는 루트 페이지 끝.
|
|
15491
|
+
* 행 이름의 폴더(`DB/행/`)는 그 행의 페이지 끝. DB 폴더는 둘 곳이 없다 — Notion 의 DB 는 본문이 없다.
|
|
15425
15492
|
*/
|
|
15426
15493
|
folderPageOf(folder) {
|
|
15427
|
-
if (folder === "") return this.stateDb.getByPath("__root__")?.notionPageId ?? null;
|
|
15428
15494
|
return this.notePageOf(folder) ?? this.folderRecordPageOf(folder);
|
|
15429
15495
|
}
|
|
15430
|
-
/**
|
|
15496
|
+
/**
|
|
15497
|
+
* 이 폴더를 품은 노트의 페이지 — 폴더 노트 · 노트(행 포함) 이름의 폴더 · 볼트 맨 위는 루트 페이지. 본문이 이 도구의 것이
|
|
15498
|
+
* 아니거나(루트) 노트의 것이라, 적어 둔 폴더 첨부만 제 것으로 본다. 없으면 null.
|
|
15499
|
+
*/
|
|
15431
15500
|
notePageOf(folder) {
|
|
15501
|
+
if (folder === "") return this.rootPageId;
|
|
15432
15502
|
const note = this.stateDb.getByPath(folderNoteOf(folder));
|
|
15433
15503
|
if (note?.notionPageId && note.fileType === "folder-note") return note.notionPageId;
|
|
15434
15504
|
const named = this.stateDb.getByPath(`${folder}.md`);
|
|
15435
|
-
return named?.notionPageId && named
|
|
15505
|
+
return named?.notionPageId && isNamedNote(named) ? named.notionPageId : null;
|
|
15436
15506
|
}
|
|
15437
15507
|
folderRecordPageOf(folder) {
|
|
15438
15508
|
const record = this.stateDb.getByPath(folder);
|
|
@@ -15538,21 +15608,26 @@ var FileHandler = class {
|
|
|
15538
15608
|
*/
|
|
15539
15609
|
async removeDeletedFolderFiles(noteWrites) {
|
|
15540
15610
|
const removed = [];
|
|
15611
|
+
const pages = [];
|
|
15541
15612
|
for (const record of this.stateDb.getAll()) {
|
|
15542
15613
|
if (!record.notionPageId) continue;
|
|
15543
15614
|
const folder = isFolderRecord(record) ? record.obsidianPath : filesFolderOfNote(record);
|
|
15544
|
-
if (folder
|
|
15545
|
-
|
|
15546
|
-
|
|
15615
|
+
if (folder !== null) {
|
|
15616
|
+
pages.push({ pageId: record.notionPageId, folder, onNote: !isFolderRecord(record) });
|
|
15617
|
+
}
|
|
15618
|
+
}
|
|
15619
|
+
if (this.rootPageId) pages.push({ pageId: this.rootPageId, folder: "", onNote: true });
|
|
15620
|
+
for (const { pageId, folder, onNote } of pages) {
|
|
15621
|
+
const ids = folderFileIdsOf(this.stateDb, pageId);
|
|
15622
|
+
if (onNote && ids.size === 0) continue;
|
|
15547
15623
|
const gone = [];
|
|
15548
|
-
for (const entry of this.stateDb.getFilesByPageId(
|
|
15624
|
+
for (const entry of this.stateDb.getFilesByPageId(pageId)) {
|
|
15549
15625
|
if (getFolderPath(entry.localPath) !== folder) continue;
|
|
15550
|
-
if (
|
|
15626
|
+
if (onNote && !ids.has(entry.localPath)) continue;
|
|
15551
15627
|
if (!await this.vaultFs.exists(entry.localPath)) gone.push(entry);
|
|
15552
15628
|
}
|
|
15553
15629
|
if (gone.length === 0) continue;
|
|
15554
|
-
|
|
15555
|
-
await this.writingNotePage(!isFolderRecord(record), pageId, noteWrites, async () => {
|
|
15630
|
+
await this.writingNotePage(onNote, pageId, noteWrites, async () => {
|
|
15556
15631
|
for (const entry of gone) {
|
|
15557
15632
|
try {
|
|
15558
15633
|
const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
|
|
@@ -15587,7 +15662,7 @@ var FileHandler = class {
|
|
|
15587
15662
|
/** 폴더 첨부를 둘 페이지 — 노트 없는 폴더면 마련한다. 둘 곳이 없으면 그 파일들을 `stranded` 에 담고 null. */
|
|
15588
15663
|
async provideFolderPage(folder, folders, items, stranded) {
|
|
15589
15664
|
let pageId = this.folderPageOf(folder);
|
|
15590
|
-
let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704" : "DB \uD3F4\uB354 \xB7 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uD3F4\uB354 \uB178\uD2B8";
|
|
15665
|
+
let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704 \u2014 DB \uBAA8\uB4DC\uC758 \uB8E8\uD2B8\uB294 DB \uB77C \uBCF8\uBB38\uC774 \uC5C6\uB2E4" : "DB \uD3F4\uB354 \xB7 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uD3F4\uB354 \uB178\uD2B8";
|
|
15591
15666
|
if (!pageId && folder !== "" && folders && !this.stateDb.getByPath(folder)) {
|
|
15592
15667
|
try {
|
|
15593
15668
|
await folders.ensureFolderPage(folder);
|
|
@@ -15838,22 +15913,23 @@ var FileHandler = class {
|
|
|
15838
15913
|
*/
|
|
15839
15914
|
async removeOlderBlocks(pageId, localPath, name, keep) {
|
|
15840
15915
|
const record = this.stateDb.getByNotionId(pageId);
|
|
15841
|
-
const onNote = record !== null && !isFolderRecord(record);
|
|
15916
|
+
const onNote = record !== null && !isFolderRecord(record) || this.rootPageId !== null && notionIdsEqual(pageId, this.rootPageId);
|
|
15842
15917
|
const fileId = folderFileIdsOf(this.stateDb, pageId).get(localPath);
|
|
15843
|
-
|
|
15844
|
-
if (block.id === keep)
|
|
15845
|
-
const media = block
|
|
15846
|
-
|
|
15847
|
-
if (!url || uploadedFileNameFromUrl(url) !== name) continue;
|
|
15918
|
+
const isOlder = (block) => {
|
|
15919
|
+
if (block.id === keep) return false;
|
|
15920
|
+
const media = uploadedFileOfBlock(block);
|
|
15921
|
+
if (!media || uploadedFileNameFromUrl(media.url) !== name) return false;
|
|
15848
15922
|
if (keep === void 0 && fileId !== void 0) {
|
|
15849
|
-
|
|
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;
|
|
15923
|
+
return notionFileIdOf(media.url) === fileId;
|
|
15854
15924
|
}
|
|
15855
|
-
|
|
15925
|
+
if (!onNote) return true;
|
|
15926
|
+
return fileId !== void 0 && notionFileIdOf(media.url) === fileId || media.caption === folderFileCaption(name);
|
|
15927
|
+
};
|
|
15928
|
+
let older = (await this.notionClient.fetchAllChildren(pageId)).filter(isOlder);
|
|
15929
|
+
if (older.length === 0 && onNote) {
|
|
15930
|
+
older = flattenBlockTree2(await this.notionClient.getBlockTree(pageId)).filter(isOlder);
|
|
15856
15931
|
}
|
|
15932
|
+
for (const block of older) await this.notionClient.deleteBlock(block.id);
|
|
15857
15933
|
}
|
|
15858
15934
|
/**
|
|
15859
15935
|
* 폴더 첨부의 파일 블록 — 캡션에 표시를 단 파일 이름을 적는다({@link folderFileCaption}). 받는 쪽이 그 표시로 폴더 첨부를
|
|
@@ -16889,21 +16965,6 @@ function loadLinkedDbMap(stateDb) {
|
|
|
16889
16965
|
}
|
|
16890
16966
|
}
|
|
16891
16967
|
|
|
16892
|
-
// src/sync/parent-mode.ts
|
|
16893
|
-
function isDatabaseMode(config) {
|
|
16894
|
-
return config.notion.parentMode === "database" && !!config.notion.databaseId;
|
|
16895
|
-
}
|
|
16896
|
-
function rowDatabaseOf(config, record) {
|
|
16897
|
-
if (record.fileType === "db-row") {
|
|
16898
|
-
if (!record.notionParentId) {
|
|
16899
|
-
throw new Error(`DB \uD589\uC778\uB370 \uC18C\uC18D DB \uB97C \uC54C \uC218 \uC5C6\uC74C: ${record.obsidianPath}`);
|
|
16900
|
-
}
|
|
16901
|
-
return record.notionParentId;
|
|
16902
|
-
}
|
|
16903
|
-
if (isDatabaseMode(config)) return config.notion.databaseId;
|
|
16904
|
-
return null;
|
|
16905
|
-
}
|
|
16906
|
-
|
|
16907
16968
|
// src/sync/remote-scan.ts
|
|
16908
16969
|
var LAST_FULL_PULL_META_KEY = "last_full_pull_at";
|
|
16909
16970
|
var LAST_PULL_META_KEY = "last_pull_at";
|
|
@@ -19874,6 +19935,7 @@ function emptyDatabaseSyncResult() {
|
|
|
19874
19935
|
}
|
|
19875
19936
|
var DatabaseSyncer = class {
|
|
19876
19937
|
constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler, observation = () => NO_OBSERVATION, noteRowBody = () => {
|
|
19938
|
+
}, receiveRowFiles = async () => {
|
|
19877
19939
|
}) {
|
|
19878
19940
|
this.config = config;
|
|
19879
19941
|
this.stateDb = stateDb;
|
|
@@ -19883,6 +19945,7 @@ var DatabaseSyncer = class {
|
|
|
19883
19945
|
this.imageHandler = imageHandler;
|
|
19884
19946
|
this.observation = observation;
|
|
19885
19947
|
this.noteRowBody = noteRowBody;
|
|
19948
|
+
this.receiveRowFiles = receiveRowFiles;
|
|
19886
19949
|
this.baseFileInfo = new DbBaseFiles(stateDb);
|
|
19887
19950
|
this.journal = new UpdateJournal(stateDb);
|
|
19888
19951
|
this.sidecarBases = new SidecarBases(stateDb);
|
|
@@ -19907,6 +19970,7 @@ var DatabaseSyncer = class {
|
|
|
19907
19970
|
imageHandler;
|
|
19908
19971
|
observation;
|
|
19909
19972
|
noteRowBody;
|
|
19973
|
+
receiveRowFiles;
|
|
19910
19974
|
propertyMapper;
|
|
19911
19975
|
decorationsCache;
|
|
19912
19976
|
/**
|
|
@@ -20989,7 +21053,18 @@ var DatabaseSyncer = class {
|
|
|
20989
21053
|
this.noteRowBody(page.id, mdResult.markdown);
|
|
20990
21054
|
const bodyFingerprint = remoteBodyFingerprint(mdResult.markdown);
|
|
20991
21055
|
const exportCompact = isCompactExport(mdResult.markdown);
|
|
20992
|
-
|
|
21056
|
+
const localContent = await readLocalNote(this.vaultFs, options?.localPath ?? filePath);
|
|
21057
|
+
const noteFiles = noteFolderFilesOfPage(
|
|
21058
|
+
this.stateDb,
|
|
21059
|
+
page.id,
|
|
21060
|
+
mdResult.markdown,
|
|
21061
|
+
embeddedNamesOf(localContent === void 0 ? [] : [localContent])
|
|
21062
|
+
);
|
|
21063
|
+
let markdown = await notionBodyToObsidian(
|
|
21064
|
+
this.notionClient,
|
|
21065
|
+
noteFiles.found?.markdown ?? mdResult.markdown,
|
|
21066
|
+
page.id
|
|
21067
|
+
);
|
|
20993
21068
|
markdown = await this.imageHandler.restoreUploadedMedia(
|
|
20994
21069
|
markdown,
|
|
20995
21070
|
page.id,
|
|
@@ -21023,7 +21098,7 @@ var DatabaseSyncer = class {
|
|
|
21023
21098
|
{
|
|
21024
21099
|
properties,
|
|
21025
21100
|
notionExportCompact: exportCompact,
|
|
21026
|
-
localContent
|
|
21101
|
+
localContent,
|
|
21027
21102
|
// 이 렌더러가 적는 키 — 속성과 제목 · 커버 · 아이콘. 나머지는 로컬 키다(F-08).
|
|
21028
21103
|
notionKeys: [...this.propertyMapper.ownedKeys(rawProperties), "title", ...DECORATION_KEYS]
|
|
21029
21104
|
}
|
|
@@ -21033,7 +21108,11 @@ var DatabaseSyncer = class {
|
|
|
21033
21108
|
title,
|
|
21034
21109
|
properties,
|
|
21035
21110
|
bodyFingerprint,
|
|
21036
|
-
propertiesBlock: hasPropertiesBlock(mdResult.markdown)
|
|
21111
|
+
propertiesBlock: hasPropertiesBlock(mdResult.markdown),
|
|
21112
|
+
folderFiles: {
|
|
21113
|
+
files: noteFiles.found?.files ?? [],
|
|
21114
|
+
complete: !mdResult.truncated && noteFiles.holdsAll
|
|
21115
|
+
}
|
|
21037
21116
|
};
|
|
21038
21117
|
}
|
|
21039
21118
|
/**
|
|
@@ -21330,8 +21409,19 @@ var DatabaseSyncer = class {
|
|
|
21330
21409
|
*/
|
|
21331
21410
|
adoptedChildrenOf(record, rowPath) {
|
|
21332
21411
|
const from = record.fileType === "folder-note" ? parentFolderOf(record.obsidianPath) : stripMarkdownExtension(record.obsidianPath);
|
|
21333
|
-
|
|
21334
|
-
|
|
21412
|
+
return this.holdsChildren(record, from) ? { from, to: stripMarkdownExtension(rowPath) } : null;
|
|
21413
|
+
}
|
|
21414
|
+
/**
|
|
21415
|
+
* 이 노트 · 행 옆의 폴더(`from`)가 그것의 것인가 — 하위 페이지가 있거나, 그 페이지에 올린 폴더 첨부(ADR-056)가 든 폴더.
|
|
21416
|
+
* 예전에는 하위 페이지만 보아 행의 이름을 따라 옮길 때 폴더 첨부만 든 폴더가 옛 이름에 남았다.
|
|
21417
|
+
*/
|
|
21418
|
+
holdsChildren(record, from) {
|
|
21419
|
+
if (this.stateDb.getAll().some((r) => r.id !== record.id && within(r.obsidianPath, from))) {
|
|
21420
|
+
return true;
|
|
21421
|
+
}
|
|
21422
|
+
return record.notionPageId !== null && [...folderFileIdsOf(this.stateDb, record.notionPageId).keys()].some(
|
|
21423
|
+
(path) => within(path, from)
|
|
21424
|
+
);
|
|
21335
21425
|
}
|
|
21336
21426
|
/**
|
|
21337
21427
|
* Notion 에서 바꾼 제목으로 행 이름을 바꿀 자리(`sync.renameOnTitleChange`) — 바꿀 것이 없으면 null.
|
|
@@ -21353,14 +21443,32 @@ var DatabaseSyncer = class {
|
|
|
21353
21443
|
);
|
|
21354
21444
|
if (to === recordPath) return null;
|
|
21355
21445
|
const from = stripMarkdownExtension(recordPath);
|
|
21356
|
-
if (!this.
|
|
21446
|
+
if (!this.holdsChildren(record, from)) {
|
|
21357
21447
|
return { to, children: null };
|
|
21358
21448
|
}
|
|
21359
21449
|
const children = { from, to: stripMarkdownExtension(to) };
|
|
21360
21450
|
const blocked = await this.relocator.blocked(children);
|
|
21361
21451
|
return blocked ? { reason: `\uD558\uC704 \uD398\uC774\uC9C0 \uD3F4\uB354 \u2014 ${blocked}` } : { to, children };
|
|
21362
21452
|
}
|
|
21453
|
+
/**
|
|
21454
|
+
* 행 하나를 받고 그 행 페이지의 폴더 첨부를 행 이름의 폴더에 받는다(ADR-056) — 노트가 충돌로 남거나 로컬을 지켜도 파일은
|
|
21455
|
+
* 제 규칙대로 받는다(페이지와 같다). 행을 옮겼으면 옮긴 자리로.
|
|
21456
|
+
*/
|
|
21363
21457
|
async pullDatabasePage(page, dbConfig) {
|
|
21458
|
+
const rendered = {};
|
|
21459
|
+
const outcome = await this.pullDatabaseRow(page, dbConfig, rendered);
|
|
21460
|
+
const path = outcome.path ?? this.stateDb.getByNotionId(page.id)?.obsidianPath;
|
|
21461
|
+
if (rendered.folderFiles && path) {
|
|
21462
|
+
await this.receiveRowFiles(
|
|
21463
|
+
stripMarkdownExtension(path),
|
|
21464
|
+
page.id,
|
|
21465
|
+
rendered.folderFiles.files,
|
|
21466
|
+
rendered.folderFiles.complete
|
|
21467
|
+
);
|
|
21468
|
+
}
|
|
21469
|
+
return outcome;
|
|
21470
|
+
}
|
|
21471
|
+
async pullDatabaseRow(page, dbConfig, seen) {
|
|
21364
21472
|
const safeName = sanitizeFileName(this.notionClient.extractTitle(page));
|
|
21365
21473
|
const existingRecord = this.stateDb.getByNotionId(page.id);
|
|
21366
21474
|
const recordPath = existingRecord?.obsidianPath;
|
|
@@ -21414,6 +21522,7 @@ var DatabaseSyncer = class {
|
|
|
21414
21522
|
localPath: recordPath ?? filePath,
|
|
21415
21523
|
...local !== void 0 ? { adoptFrom: local } : {}
|
|
21416
21524
|
});
|
|
21525
|
+
seen.folderFiles = rendered.folderFiles;
|
|
21417
21526
|
let relinked = false;
|
|
21418
21527
|
if (adopt) {
|
|
21419
21528
|
const outcome = local === void 0 ? null : await this.relinkExistingRow(
|
|
@@ -21436,6 +21545,7 @@ var DatabaseSyncer = class {
|
|
|
21436
21545
|
await this.rowPathOwners(dbConfig.localFolder, safeName, page.id)
|
|
21437
21546
|
);
|
|
21438
21547
|
rendered = await this.renderRow(page, filePath, { localPath: filePath });
|
|
21548
|
+
seen.folderFiles = rendered.folderFiles;
|
|
21439
21549
|
}
|
|
21440
21550
|
}
|
|
21441
21551
|
const { title, properties, bodyFingerprint } = rendered;
|
|
@@ -23690,7 +23800,6 @@ function dropEmbeddedBaseMarkers(content, basePaths) {
|
|
|
23690
23800
|
|
|
23691
23801
|
// src/sync/database-discovery.ts
|
|
23692
23802
|
var DISCOVERY_RETRY_META_KEY = "discovery_retry";
|
|
23693
|
-
var PREVIEW_BODY_READ_BUDGET = 100;
|
|
23694
23803
|
function parseDiscoveryRetry(raw) {
|
|
23695
23804
|
if (!raw) return { parents: [], dbs: [] };
|
|
23696
23805
|
try {
|
|
@@ -23908,7 +24017,8 @@ var DatabaseDiscovery = class {
|
|
|
23908
24017
|
* `markScanned` 일 때 적는다.
|
|
23909
24018
|
*
|
|
23910
24019
|
* @param failed 읽지 못한 페이지 · DB — 이유와 함께 싣는다.
|
|
23911
|
-
* @param options.notePathOf 페이지 → 볼트 경로. 새 DB 폴더를 부모 노트 옆에 둔다 — dry-run 은 다시 이을
|
|
24020
|
+
* @param options.notePathOf 페이지 → 볼트 경로. 새 DB 폴더를 부모 노트 옆에 둔다 — dry-run 은 다시 이을 노트 · 새로 받을 페이지의
|
|
24021
|
+
* 자리를 준다.
|
|
23912
24022
|
*/
|
|
23913
24023
|
async openRegistry(failed, options) {
|
|
23914
24024
|
const { forceRediscovery, signal } = options;
|
|
@@ -24090,18 +24200,22 @@ var DatabaseDiscovery = class {
|
|
|
24090
24200
|
}
|
|
24091
24201
|
/**
|
|
24092
24202
|
* dry-run — 새로 받을 페이지의 본문을 읽어 그 안의 DB 를 찾는다({@link plannedNewDatabases} 가 그 행을 센다). 실제 pull 은
|
|
24093
|
-
* 받으며
|
|
24094
|
-
*
|
|
24203
|
+
* 받으며 찾는다 — 같은 길로 읽는다: Markdown API(잘린 본문은 채운 것 · 권한 없는 자리표시는 그대로), 실패하거나 블록 방식이면
|
|
24204
|
+
* 블록 트리. 예전에는 100개까지만 읽고 넘는 페이지 안의 DB 를 «미리 세지 않았습니다» 로만 알렸다 — 미리 보기는 사용자가
|
|
24205
|
+
* 부르고 멈출 수 있어(`signal`) 모두 읽는다. 둘 다 읽지 못한 페이지만 {@link bodyRead} 가 아니어서 알림으로 남는다.
|
|
24095
24206
|
*/
|
|
24096
24207
|
async readNewPageBodies(pageIds, signal) {
|
|
24097
24208
|
if (isDatabaseMode(this.config)) return;
|
|
24098
24209
|
const unread = pageIds.filter((pageId) => !this.bodyRead(pageId));
|
|
24099
|
-
|
|
24210
|
+
if (unread.length > 0) {
|
|
24211
|
+
getLogger().info(
|
|
24212
|
+
`[Im-Nobsidian] \uBBF8\uB9AC \uBCF4\uAE30: \uC0C8 \uD398\uC774\uC9C0 ${unread.length}\uAC1C\uC758 \uBCF8\uBB38\uC5D0\uC11C DB \uB97C \uCC3E\uC74C`
|
|
24213
|
+
);
|
|
24214
|
+
}
|
|
24215
|
+
for (const pageId of unread) {
|
|
24100
24216
|
if (signal?.aborted) return;
|
|
24101
24217
|
try {
|
|
24102
|
-
|
|
24103
|
-
if (result.truncated || (result.unknown_block_ids ?? []).length > 0) continue;
|
|
24104
|
-
this.collectInlineDbRefs(pageId, result.markdown);
|
|
24218
|
+
this.collectInlineDbRefs(pageId, await this.previewBodyOf(pageId));
|
|
24105
24219
|
} catch (error) {
|
|
24106
24220
|
getLogger().debug(
|
|
24107
24221
|
`[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)}`
|
|
@@ -24109,6 +24223,16 @@ var DatabaseDiscovery = class {
|
|
|
24109
24223
|
}
|
|
24110
24224
|
}
|
|
24111
24225
|
}
|
|
24226
|
+
/** 미리 보기가 읽는 새 페이지 본문 — 실제 pull 의 `PagePuller.fetchPageMarkdown` 과 같은 차례. */
|
|
24227
|
+
async previewBodyOf(pageId) {
|
|
24228
|
+
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
24229
|
+
try {
|
|
24230
|
+
return (await this.notionClient.getPageMarkdown(pageId)).markdown;
|
|
24231
|
+
} catch {
|
|
24232
|
+
}
|
|
24233
|
+
}
|
|
24234
|
+
return blocksToNfm(await this.notionClient.getBlockTree(pageId), { pageId });
|
|
24235
|
+
}
|
|
24112
24236
|
/** 이번 pull 에서 본문을 받은 페이지인가 — 그 본문의 `<database>` 로 새 DB 를 찾았다. */
|
|
24113
24237
|
bodyRead(pageId) {
|
|
24114
24238
|
return this._bodyReadPages.has(normalizeNotionId(pageId));
|
|
@@ -24191,7 +24315,7 @@ var DatabaseDiscovery = class {
|
|
|
24191
24315
|
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uC124\uC815 \uC0DD\uC131 \uC2E4\uD328:`, error);
|
|
24192
24316
|
return { kind: "error", error: error instanceof Error ? error.message : String(error) };
|
|
24193
24317
|
}
|
|
24194
|
-
const obsPath = parentPageId ? notePathOf(parentPageId) : null;
|
|
24318
|
+
const obsPath = parentPageId ? await notePathOf(parentPageId) : null;
|
|
24195
24319
|
let parentFolder = "";
|
|
24196
24320
|
if (obsPath) {
|
|
24197
24321
|
if (isMarkdownPath(obsPath)) {
|
|
@@ -27598,7 +27722,8 @@ var PagePusher = class {
|
|
|
27598
27722
|
* 실패로 알린다. 예전처럼 조용히 건너뛰면 동기화됨으로 기록돼 편집이 영영 가지 않는다.
|
|
27599
27723
|
*
|
|
27600
27724
|
* 바뀌지 않은 미디어는 다시 올리지 않고 Notion 의 그 블록을 둔다({@link ImageHandler.keepUploadedMedia}).
|
|
27601
|
-
* 블록 방식은
|
|
27725
|
+
* 블록 방식은 노트의 미디어를 둘 수 없다 — 예전처럼 모두 다시 올린다. 페이지 끝의 폴더 첨부(ADR-056)는 노트 밖의 것이라
|
|
27726
|
+
* 블록째 두고 새 본문을 그 앞(맨 앞)에 넣는다.
|
|
27602
27727
|
*
|
|
27603
27728
|
* @returns 보낸 본문 · 그 지문 · 그대로 둔 미디어({@link PushedBody})
|
|
27604
27729
|
*/
|
|
@@ -27610,9 +27735,9 @@ var PagePusher = class {
|
|
|
27610
27735
|
deferred.nfm,
|
|
27611
27736
|
notePath
|
|
27612
27737
|
);
|
|
27613
|
-
const
|
|
27614
|
-
const markdown =
|
|
27615
|
-
${
|
|
27738
|
+
const folderFiles2 = await this.folderFileLines(pageId);
|
|
27739
|
+
const markdown = folderFiles2 ? `${body}
|
|
27740
|
+
${folderFiles2}` : body;
|
|
27616
27741
|
const written = await replacePageBody(this.notionClient, pageId, markdown, {
|
|
27617
27742
|
databaseIdsOfBase: (basePath) => this.databaseIdsOfBase(basePath),
|
|
27618
27743
|
unknownTagFallbacks: unknownTagFallbacks(markdownContent)
|
|
@@ -27632,14 +27757,19 @@ ${folderFiles}` : body;
|
|
|
27632
27757
|
"\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"
|
|
27633
27758
|
);
|
|
27634
27759
|
}
|
|
27635
|
-
this.
|
|
27760
|
+
const folderFiles = this.folderFileBlocks(existingBlocks, pageId, notePath);
|
|
27636
27761
|
const { nfm, blocks } = blockModeBody(markdownContent);
|
|
27637
27762
|
if (blocks.length > 0) {
|
|
27638
|
-
await this.notionClient.appendBlockTree(
|
|
27763
|
+
await this.notionClient.appendBlockTree(
|
|
27764
|
+
pageId,
|
|
27765
|
+
blocks,
|
|
27766
|
+
folderFiles.size > 0 ? { atStart: true } : void 0
|
|
27767
|
+
);
|
|
27639
27768
|
}
|
|
27640
27769
|
const deleteSema = new Sema(this.config.advanced.concurrency);
|
|
27641
27770
|
await Promise.all(
|
|
27642
27771
|
existingBlocks.map(async (block) => {
|
|
27772
|
+
if (folderFiles.has(block.id)) return;
|
|
27643
27773
|
await deleteSema.acquire();
|
|
27644
27774
|
try {
|
|
27645
27775
|
await this.notionClient.deleteBlock(block.id);
|
|
@@ -27662,19 +27792,28 @@ ${folderFiles}` : body;
|
|
|
27662
27792
|
return (folderFilesInNote(remote, { knownIds: ids })?.lines ?? []).join("\n");
|
|
27663
27793
|
}
|
|
27664
27794
|
/**
|
|
27665
|
-
* 블록
|
|
27666
|
-
*
|
|
27667
|
-
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27795
|
+
* 블록 방식으로 바꿀 본문에서 남길 폴더 첨부 블록(ADR-056) — 맨 위 블록 가운데 {@link isFolderFileBlock}. 예전에는 함께 지우고
|
|
27796
|
+
* 기록을 놓아 노트를 고칠 때마다 폴더 첨부를 다시 올렸다.
|
|
27797
|
+
*
|
|
27798
|
+
* 맨 위에 없는 폴더 첨부(Notion 에서 토글 안으로 옮긴 것)는 그 토글과 함께 지워진다 — 기록을 놓아 이어지는 첨부 push 가
|
|
27799
|
+
* 페이지 끝에 다시 올리게 한다. 두면 볼트의 기록만 남아 다음 pull 이 Notion 에서 지운 파일로 본다.
|
|
27800
|
+
*/
|
|
27801
|
+
folderFileBlocks(blocks, pageId, notePath) {
|
|
27802
|
+
const recorded = folderFileIdsOf(this.stateDb, pageId);
|
|
27803
|
+
const ids = new Set(recorded.values());
|
|
27804
|
+
const kept = blocks.filter((block) => isFolderFileBlock(block, ids));
|
|
27805
|
+
const keptIds = new Set(kept.map((block) => notionFileIdOf(fileUrlOfBlock(block) ?? "")));
|
|
27806
|
+
const lost = [...recorded].filter(([, fileId]) => !keptIds.has(fileId));
|
|
27807
|
+
for (const [localPath] of lost) {
|
|
27672
27808
|
this.stateDb.deleteFileRegistry(localPath);
|
|
27673
27809
|
forgetFolderFileId(this.stateDb, pageId, localPath);
|
|
27674
27810
|
}
|
|
27675
|
-
|
|
27676
|
-
|
|
27677
|
-
|
|
27811
|
+
if (lost.length > 0) {
|
|
27812
|
+
getLogger().info(
|
|
27813
|
+
`[Im-Nobsidian] \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBC14\uAFB8\uBA70 \uB9E8 \uC704\uC5D0 \uC5C6\uB358 \uD3F4\uB354 \uCCA8\uBD80 ${lost.length}\uAC1C\uB97C \uB2E4\uC2DC \uC62C\uB9BC (${notePath})`
|
|
27814
|
+
);
|
|
27815
|
+
}
|
|
27816
|
+
return new Set(kept.map((block) => block.id));
|
|
27678
27817
|
}
|
|
27679
27818
|
/**
|
|
27680
27819
|
* 자리표시로 보낸 코드를 채우고(S-22) 동기화 블록 참조를 붙인다({@link deferSyncedReferences}) — 본문을
|
|
@@ -27914,6 +28053,17 @@ var PagePuller = class {
|
|
|
27914
28053
|
sanitizeFileName(this.notionClient.extractTitle(page))
|
|
27915
28054
|
);
|
|
27916
28055
|
}
|
|
28056
|
+
/**
|
|
28057
|
+
* dry-run — 새 페이지를 실제 pull 이 받을 자리(부모의 자리 + 제목). 그 안에서 찾은 DB 의 폴더를 실제처럼 그 옆에 둔다 — 예전에는
|
|
28058
|
+
* 자리를 몰라 `databases/` 아래로 보였다. 같은 이름이 있으면 실제 pull 은 접미사를 붙이지만 여기서는 붙이지 않는다(선점하지
|
|
28059
|
+
* 않는다). 볼트도 상태 DB 도 바꾸지 않는다.
|
|
28060
|
+
*/
|
|
28061
|
+
async plannedNotePath(pageId) {
|
|
28062
|
+
const page = this.detector.discoveredPage(pageId) ?? await this.notionClient.getPage(pageId);
|
|
28063
|
+
const parent = await this.resolveParentPath(page);
|
|
28064
|
+
const name = `${sanitizeFileName(this.notionClient.extractTitle(page))}.md`;
|
|
28065
|
+
return parent ? `${parent}/${name}` : name;
|
|
28066
|
+
}
|
|
27917
28067
|
/**
|
|
27918
28068
|
* dry-run — 실제 pull({@link createPulledPage})이 이 새 페이지를 새로 받지 않고 상태 기록에 없던 노트 · 폴더와 다시 잇는가(P7).
|
|
27919
28069
|
* 다시 이을 노트(`note` — 충돌로 남길 것이면 `conflict`) · 이미 있는 폴더(`folder` — 폴더로만 받는 페이지), 아니면 null.
|
|
@@ -28489,13 +28639,13 @@ var PagePuller = class {
|
|
|
28489
28639
|
* 받았으면 null — 지문은 Markdown API 의 본문으로만 견준다.
|
|
28490
28640
|
*/
|
|
28491
28641
|
async fetchPageMarkdown(pageId, options = {}) {
|
|
28492
|
-
const noteFilesOf = (markdown) => this.
|
|
28642
|
+
const noteFilesOf = (markdown) => noteFolderFilesOfPage(this.stateDb, pageId, markdown, options.embedded);
|
|
28493
28643
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
28494
28644
|
try {
|
|
28495
28645
|
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
28496
28646
|
this.discovery.collectInlineDbRefs(pageId, result.markdown);
|
|
28497
28647
|
this.detector.noteBodyChildren(pageId, result.markdown);
|
|
28498
|
-
const found2 = noteFilesOf(result.markdown);
|
|
28648
|
+
const { found: found2, holdsAll } = noteFilesOf(result.markdown);
|
|
28499
28649
|
return {
|
|
28500
28650
|
content: await this.resolveBlockLinks(
|
|
28501
28651
|
this.resolveNotionIdWikilinks(
|
|
@@ -28515,7 +28665,7 @@ var PagePuller = class {
|
|
|
28515
28665
|
hasBody: hasBodyBesidesChildren(result.markdown),
|
|
28516
28666
|
folderFiles: folderFilesInBody(result.markdown),
|
|
28517
28667
|
noteFiles: found2?.files ?? [],
|
|
28518
|
-
noteFilesComplete: !result.truncated &&
|
|
28668
|
+
noteFilesComplete: !result.truncated && holdsAll
|
|
28519
28669
|
};
|
|
28520
28670
|
} catch (error) {
|
|
28521
28671
|
getLogger().warn(
|
|
@@ -28526,9 +28676,9 @@ var PagePuller = class {
|
|
|
28526
28676
|
let noteFiles = [];
|
|
28527
28677
|
let noteFilesComplete = true;
|
|
28528
28678
|
const fallback = await pageBlocksToObsidian(this.notionClient, pageId, (markdown) => {
|
|
28529
|
-
const found2 = noteFilesOf(markdown);
|
|
28679
|
+
const { found: found2, holdsAll } = noteFilesOf(markdown);
|
|
28530
28680
|
noteFiles = found2?.files ?? [];
|
|
28531
|
-
noteFilesComplete =
|
|
28681
|
+
noteFilesComplete = holdsAll;
|
|
28532
28682
|
return found2?.markdown ?? markdown;
|
|
28533
28683
|
});
|
|
28534
28684
|
this.discovery.collectInlineDbRefs(pageId, fallback.markdown);
|
|
@@ -28547,24 +28697,39 @@ var PagePuller = class {
|
|
|
28547
28697
|
noteFilesComplete
|
|
28548
28698
|
};
|
|
28549
28699
|
}
|
|
28550
|
-
/**
|
|
28551
|
-
|
|
28552
|
-
|
|
28553
|
-
|
|
28554
|
-
|
|
28555
|
-
|
|
28556
|
-
|
|
28557
|
-
|
|
28700
|
+
/**
|
|
28701
|
+
* 볼트 맨 위 파일 — 루트 페이지 끝의 폴더 첨부(「📁 이름」 · 적어 둔 것)를 볼트 맨 위에 받는다({@link rootFilesPageOf}).
|
|
28702
|
+
* 루트의 다른 블록(사용자가 붙인 파일 포함)은 받지 않는다. Markdown API 로 읽지 못하면 맨 위 블록에서 찾되 빠진 파일을
|
|
28703
|
+
* 지운 것으로 보지 않는다 — 토글 안으로 옮긴 것은 보이지 않는다. 읽지 못하면 경고만 남긴다(다음 pull 이 다시 읽는다).
|
|
28704
|
+
*/
|
|
28705
|
+
async receiveRootFiles() {
|
|
28706
|
+
const rootId = rootFilesPageOf(this.config);
|
|
28707
|
+
if (!rootId) return;
|
|
28708
|
+
try {
|
|
28709
|
+
let files;
|
|
28710
|
+
let complete2;
|
|
28711
|
+
try {
|
|
28712
|
+
const result = await this.notionClient.getPageMarkdown(rootId);
|
|
28713
|
+
const { found: found2, holdsAll } = noteFolderFilesOfPage(this.stateDb, rootId, result.markdown);
|
|
28714
|
+
files = found2?.files ?? [];
|
|
28715
|
+
complete2 = !result.truncated && holdsAll;
|
|
28716
|
+
} catch {
|
|
28717
|
+
const ids = new Set(folderFileIdsOf(this.stateDb, rootId).values());
|
|
28718
|
+
files = (await this.notionClient.fetchAllChildren(rootId)).flatMap((block) => {
|
|
28719
|
+
const url = isFolderFileBlock(block, ids) ? fileUrlOfBlock(block) : null;
|
|
28720
|
+
const name = url ? uploadedFileNameFromUrl(url) : null;
|
|
28721
|
+
return url && name ? [{ url, name }] : [];
|
|
28722
|
+
});
|
|
28723
|
+
complete2 = false;
|
|
28558
28724
|
}
|
|
28559
|
-
|
|
28560
|
-
|
|
28561
|
-
|
|
28562
|
-
|
|
28563
|
-
|
|
28564
|
-
|
|
28565
|
-
return !notionMediaSpans(rest).some((span) => ids.has(notionFileIdOf(span.url) ?? ""));
|
|
28725
|
+
await this.receiveFolderFiles("", rootId, files, complete2);
|
|
28726
|
+
} catch (error) {
|
|
28727
|
+
getLogger().warn(
|
|
28728
|
+
`[Im-Nobsidian] \uBCFC\uD2B8 \uB9E8 \uC704 \uD30C\uC77C\uC744 \uB8E8\uD2B8 \uD398\uC774\uC9C0\uC5D0\uC11C \uBC1B\uC9C0 \uBABB\uD568: ${error instanceof Error ? error.message : String(error)}`
|
|
28729
|
+
);
|
|
28730
|
+
}
|
|
28566
28731
|
}
|
|
28567
|
-
/** 폴더 첨부를 받아 받은 파일 수에
|
|
28732
|
+
/** 폴더 첨부를 받아 받은 파일 수에 더한다 — 행의 폴더 첨부도 여기로 받는다(`DatabaseSyncer`). */
|
|
28568
28733
|
async receiveFolderFiles(folder, pageId, files, complete2 = false) {
|
|
28569
28734
|
this._pullFileCount += await this.fileHandler.receiveFolderFiles(
|
|
28570
28735
|
folder,
|
|
@@ -29035,13 +29200,12 @@ var PullPlanner = class {
|
|
|
29035
29200
|
const relinkPaths = new Map(
|
|
29036
29201
|
[...relinks.byPage].map(([pageId, relink]) => [compactNotionId(pageId), relink.path])
|
|
29037
29202
|
);
|
|
29038
|
-
|
|
29039
|
-
|
|
29040
|
-
|
|
29041
|
-
);
|
|
29203
|
+
const created = planned.filter((change) => change.type === "created").map((change) => change.pageId);
|
|
29204
|
+
await this.discovery.readNewPageBodies(created, options.signal);
|
|
29205
|
+
const createdIds = new Set(created.map(compactNotionId));
|
|
29042
29206
|
const newDatabases = await this.discovery.plannedNewDatabases(failed, {
|
|
29043
29207
|
rescanRoot: this.detector.rootChanged(),
|
|
29044
|
-
notePathOf: (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? null
|
|
29208
|
+
notePathOf: async (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? (createdIds.has(compactNotionId(pageId)) ? await this.puller.plannedNotePath(pageId) : null)
|
|
29045
29209
|
});
|
|
29046
29210
|
const rows = [];
|
|
29047
29211
|
for (const row of await this.planDatabaseRows(
|
|
@@ -29996,7 +30160,8 @@ var SyncOrchestrator = class {
|
|
|
29996
30160
|
downloadTimeoutMs: config.advanced.mediaDownloadTimeoutMs,
|
|
29997
30161
|
itemTimeoutMs: config.advanced.itemTimeoutMs,
|
|
29998
30162
|
attachmentsFolder: config.paths.attachments,
|
|
29999
|
-
deleteSync: config.sync.deleteSync
|
|
30163
|
+
deleteSync: config.sync.deleteSync,
|
|
30164
|
+
rootPageId: rootFilesPageOf(config)
|
|
30000
30165
|
}
|
|
30001
30166
|
);
|
|
30002
30167
|
const propertyMapper = PropertyMapper.fromConfig(config);
|
|
@@ -30008,7 +30173,8 @@ var SyncOrchestrator = class {
|
|
|
30008
30173
|
pipeline,
|
|
30009
30174
|
imageHandler,
|
|
30010
30175
|
() => this.observation.context,
|
|
30011
|
-
(pageId, rawMarkdown) => this.detector.noteBodyChildren(pageId, rawMarkdown)
|
|
30176
|
+
(pageId, rawMarkdown) => this.detector.noteBodyChildren(pageId, rawMarkdown),
|
|
30177
|
+
(folder, pageId, files, complete2) => this.puller.receiveFolderFiles(folder, pageId, files, complete2)
|
|
30012
30178
|
);
|
|
30013
30179
|
this.discovery = new DatabaseDiscovery(
|
|
30014
30180
|
config,
|
|
@@ -30848,6 +31014,7 @@ var SyncOrchestrator = class {
|
|
|
30848
31014
|
pendingDatabases
|
|
30849
31015
|
);
|
|
30850
31016
|
const finalize = async (created, updated, deleted, restored = 0) => {
|
|
31017
|
+
if (!options?.dryRun && this.detector.rootChanged()) await this.puller.receiveRootFiles();
|
|
30851
31018
|
await this.puller.removeVacatedFolders();
|
|
30852
31019
|
const renamedRows = this.databaseSyncer.takeRenamedRows();
|
|
30853
31020
|
restoredPaths.push(...this.databaseSyncer.takeRestoredRows());
|