@im-nobsidian/core 0.19.0 → 0.20.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 +24 -3
- package/dist/index.js +494 -263
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13397,6 +13397,29 @@ var ImageHandler = class {
|
|
|
13397
13397
|
get fetchFn() {
|
|
13398
13398
|
return this.customFetch ?? globalThis.fetch;
|
|
13399
13399
|
}
|
|
13400
|
+
/** {@link preview} 안에서 도는 중인 렌더 수 — 0 보다 크면 받은 미디어를 볼트에 쓰지 않고 상태 DB 에 적지 않는다. */
|
|
13401
|
+
previewDepth = 0;
|
|
13402
|
+
/**
|
|
13403
|
+
* 받기를 미리 본다(dry-run) — 미디어를 실제 pull 처럼 받아 이름(내용 해시)과 임베드를 정하되 볼트에 쓰지 않고 상태 DB 에
|
|
13404
|
+
* 적지 않는다. 받지 않고 렌더하면 미디어가 원격 주소로 남아, 받은 글을 볼트의 노트와 견줄 수 없다.
|
|
13405
|
+
*/
|
|
13406
|
+
async preview(run) {
|
|
13407
|
+
this.previewDepth++;
|
|
13408
|
+
try {
|
|
13409
|
+
return await run();
|
|
13410
|
+
} finally {
|
|
13411
|
+
this.previewDepth--;
|
|
13412
|
+
}
|
|
13413
|
+
}
|
|
13414
|
+
get previewing() {
|
|
13415
|
+
return this.previewDepth > 0;
|
|
13416
|
+
}
|
|
13417
|
+
/** 받은 파일을 볼트에 쓴다 — 미리 보기에서는 쓰지 않는다({@link preview}). */
|
|
13418
|
+
async writeDownload(localPath, buffer) {
|
|
13419
|
+
if (this.previewing) return;
|
|
13420
|
+
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
13421
|
+
await this.vaultFs.writeBinary(localPath, buffer);
|
|
13422
|
+
}
|
|
13400
13423
|
/** 상한 로직은 첨부 경로와 공유한다 — 한쪽만 빠뜨리지 않도록(utils/download-fetch). */
|
|
13401
13424
|
fetchForDownload(url) {
|
|
13402
13425
|
return fetchForDownload(this.fetchFn, url, this.downloadTimeoutMs);
|
|
@@ -13460,8 +13483,7 @@ var ImageHandler = class {
|
|
|
13460
13483
|
return null;
|
|
13461
13484
|
}
|
|
13462
13485
|
async persistImage(fetched) {
|
|
13463
|
-
await this.
|
|
13464
|
-
await this.vaultFs.writeBinary(fetched.localPath, fetched.buffer);
|
|
13486
|
+
await this.writeDownload(fetched.localPath, fetched.buffer);
|
|
13465
13487
|
return {
|
|
13466
13488
|
originalUrl: fetched.originalUrl,
|
|
13467
13489
|
localPath: fetched.localPath,
|
|
@@ -13530,7 +13552,7 @@ var ImageHandler = class {
|
|
|
13530
13552
|
}
|
|
13531
13553
|
if (localHash !== remoteHash) continue;
|
|
13532
13554
|
const fileId = notionFileIdOf(span.url);
|
|
13533
|
-
if (fileId && this.stateDb) {
|
|
13555
|
+
if (fileId && this.stateDb && !this.previewing) {
|
|
13534
13556
|
rememberReceivedMedia(this.stateDb, pageId, [
|
|
13535
13557
|
{ fileId, target, localPath: candidate, fileHash: remoteHash }
|
|
13536
13558
|
]);
|
|
@@ -14001,7 +14023,7 @@ var ImageHandler = class {
|
|
|
14001
14023
|
*/
|
|
14002
14024
|
rememberReceived(pageId, received) {
|
|
14003
14025
|
const media = received.filter((item) => item !== null);
|
|
14004
|
-
if (!pageId || !this.stateDb || media.length === 0) return;
|
|
14026
|
+
if (!pageId || !this.stateDb || media.length === 0 || this.previewing) return;
|
|
14005
14027
|
try {
|
|
14006
14028
|
rememberReceivedMedia(this.stateDb, pageId, media);
|
|
14007
14029
|
} catch (error) {
|
|
@@ -14250,8 +14272,7 @@ var ImageHandler = class {
|
|
|
14250
14272
|
name
|
|
14251
14273
|
};
|
|
14252
14274
|
}
|
|
14253
|
-
await this.
|
|
14254
|
-
await this.vaultFs.writeBinary(localPath, buffer);
|
|
14275
|
+
await this.writeDownload(localPath, buffer);
|
|
14255
14276
|
return { originalUrl: url, localPath, hash, size: buffer.length, fileHash, name };
|
|
14256
14277
|
} catch (error) {
|
|
14257
14278
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -15760,8 +15781,59 @@ async function findUntrackedNote(stateDb, vaultFs, dir, safeName, pageId, taken)
|
|
|
15760
15781
|
}
|
|
15761
15782
|
return null;
|
|
15762
15783
|
}
|
|
15763
|
-
function
|
|
15764
|
-
|
|
15784
|
+
function relinkOutcome(existing, same, unlinkedVault, strategy) {
|
|
15785
|
+
if (same) return "relink";
|
|
15786
|
+
if (existing === "name" && !unlinkedVault) return null;
|
|
15787
|
+
const chosen = existing === "own" ? strategy : "manual";
|
|
15788
|
+
if (chosen === "local-first") return "relink";
|
|
15789
|
+
if (chosen === "remote-first") return "overwrite";
|
|
15790
|
+
return "conflict";
|
|
15791
|
+
}
|
|
15792
|
+
function relinkConflict(input) {
|
|
15793
|
+
return {
|
|
15794
|
+
syncRecord: input.record,
|
|
15795
|
+
localChange: {
|
|
15796
|
+
path: input.path,
|
|
15797
|
+
type: "modified",
|
|
15798
|
+
currentHash: computeHash(input.local),
|
|
15799
|
+
previousHash: input.record.contentHash
|
|
15800
|
+
},
|
|
15801
|
+
remoteChange: {
|
|
15802
|
+
pageId: input.pageId,
|
|
15803
|
+
type: "modified",
|
|
15804
|
+
lastEdited: input.lastEdited,
|
|
15805
|
+
previousEdited: null,
|
|
15806
|
+
path: input.path,
|
|
15807
|
+
title: input.title
|
|
15808
|
+
},
|
|
15809
|
+
baseContent: null,
|
|
15810
|
+
localContent: input.local,
|
|
15811
|
+
remoteContent: input.remote
|
|
15812
|
+
};
|
|
15813
|
+
}
|
|
15814
|
+
function plannedRelinkRecord(input) {
|
|
15815
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15816
|
+
return {
|
|
15817
|
+
id: `planned:${input.pageId}`,
|
|
15818
|
+
obsidianPath: input.path,
|
|
15819
|
+
notionPageId: input.pageId,
|
|
15820
|
+
notionParentId: input.parentId,
|
|
15821
|
+
contentHash: computeHash(input.remote),
|
|
15822
|
+
notionLastEdited: input.lastEdited,
|
|
15823
|
+
notionLastEditedBy: null,
|
|
15824
|
+
notionSeenAt: null,
|
|
15825
|
+
notionBodyFingerprint: null,
|
|
15826
|
+
localLastModified: now,
|
|
15827
|
+
syncDirection: "both",
|
|
15828
|
+
fileType: input.fileType,
|
|
15829
|
+
status: "conflict",
|
|
15830
|
+
baseSnapshot: null,
|
|
15831
|
+
localMtime: null,
|
|
15832
|
+
localFileSize: null,
|
|
15833
|
+
version: 0,
|
|
15834
|
+
createdAt: now,
|
|
15835
|
+
updatedAt: now
|
|
15836
|
+
};
|
|
15765
15837
|
}
|
|
15766
15838
|
|
|
15767
15839
|
// src/conflict/merger.ts
|
|
@@ -15949,20 +16021,27 @@ var PENDING_LINK_RE = new RegExp(
|
|
|
15949
16021
|
"g"
|
|
15950
16022
|
);
|
|
15951
16023
|
function sameAsPendingLinks(local, remote) {
|
|
15952
|
-
|
|
15953
|
-
|
|
16024
|
+
const pending = new RegExp(`${NOTION_BLOCK_LINK_RE.source}|${PENDING_LINK_RE.source}`, "g");
|
|
16025
|
+
if (!pending.test(remote)) return false;
|
|
16026
|
+
pending.lastIndex = 0;
|
|
15954
16027
|
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15955
16028
|
let pattern = "";
|
|
15956
16029
|
let last = 0;
|
|
15957
|
-
for (const match of remote.matchAll(
|
|
16030
|
+
for (const match of remote.matchAll(pending)) {
|
|
15958
16031
|
pattern += escape(remote.slice(last, match.index));
|
|
15959
|
-
const
|
|
15960
|
-
|
|
16032
|
+
const blockLabel = match[1];
|
|
16033
|
+
const alias = match[6];
|
|
16034
|
+
pattern += blockLabel ? pendingBlockLinkPattern(blockLabel, escape) : match[7] ? `"?\\[\\[[^\\]\\n]+\\]\\]"?` : alias ? `(?:\\[\\[(?:[^\\]\\n|]*/)?${escape(alias)}(?:\\\\?\\|[^\\]\\n]*)?\\]\\]|\\[\\[[^\\]\\n|]+\\\\?\\|${escape(alias)}\\]\\]|\\[${escape(alias)}\\]\\([^)\\n]+\\))` : `(?:\\[\\[[^\\]\\n]+\\]\\]|\\[[^\\]\\n]+\\]\\([^)\\n]+\\))`;
|
|
15961
16035
|
last = match.index + match[0].length;
|
|
15962
16036
|
}
|
|
15963
16037
|
pattern += escape(remote.slice(last).replace(/\n+$/, ""));
|
|
15964
16038
|
return new RegExp(`^${pattern}\\n*$`).test(local.replace(/\n+$/, ""));
|
|
15965
16039
|
}
|
|
16040
|
+
function pendingBlockLinkPattern(label, escape) {
|
|
16041
|
+
const text = label.replace(/\\([>^])/g, "$1");
|
|
16042
|
+
const written = escape(text.split(" > ").join("#"));
|
|
16043
|
+
return `(?:\\[\\[[^\\]\\n|]*#[^\\]\\n|]*\\\\?\\|${escape(text)}\\]\\]|\\[\\[(?:[^\\]\\n|#]*/)?${written}\\]\\]|\\[\\[#${written}\\]\\]|\\[(?:${escape(text)}|${escape(label)})\\]\\([^)\\n]+\\))`;
|
|
16044
|
+
}
|
|
15966
16045
|
function resolveNotionIdWikilinks(markdown, lookup) {
|
|
15967
16046
|
let resolved = 0;
|
|
15968
16047
|
const out = markdown.replace(
|
|
@@ -16578,6 +16657,10 @@ function decodeId(id) {
|
|
|
16578
16657
|
// src/sync/discovered-databases.ts
|
|
16579
16658
|
var ROOT_DATABASES_FOLDER = "databases";
|
|
16580
16659
|
var DISCOVERED_DBS_META_KEY = "discovered_dbs";
|
|
16660
|
+
var DISCOVERY_SCANNED_META_KEY = "discovery_scanned_at";
|
|
16661
|
+
function discoveryScanPending(stateDb) {
|
|
16662
|
+
return parseDiscoveredDbs(stateDb.getMeta(DISCOVERED_DBS_META_KEY)).length === 0 && !stateDb.getMeta(DISCOVERY_SCANNED_META_KEY);
|
|
16663
|
+
}
|
|
16581
16664
|
var INACCESSIBLE_DBS_META_KEY = "inaccessible_dbs";
|
|
16582
16665
|
var LINKED_DBS_META_KEY = "linked_dbs";
|
|
16583
16666
|
function parseDiscoveredDbs(raw) {
|
|
@@ -19985,13 +20068,15 @@ var DatabaseSyncer = class {
|
|
|
19985
20068
|
page.id,
|
|
19986
20069
|
(key) => relinkTaken.has(key) || claimed.has(key)
|
|
19987
20070
|
);
|
|
19988
|
-
|
|
20071
|
+
const relink = untracked && await this.plannedRowRelink(page, dbConfig, untracked);
|
|
20072
|
+
if (untracked && relink) {
|
|
19989
20073
|
relinkTaken.add(pathKey(untracked.path));
|
|
19990
20074
|
planned.push({
|
|
19991
20075
|
pageId: page.id,
|
|
19992
20076
|
path: untracked.path,
|
|
19993
20077
|
operation: "create",
|
|
19994
|
-
relink: true
|
|
20078
|
+
relink: true,
|
|
20079
|
+
...relink.conflict ? { relinkConflict: relink.conflict } : {}
|
|
19995
20080
|
});
|
|
19996
20081
|
continue;
|
|
19997
20082
|
}
|
|
@@ -20822,16 +20907,20 @@ var DatabaseSyncer = class {
|
|
|
20822
20907
|
return found2;
|
|
20823
20908
|
}
|
|
20824
20909
|
/**
|
|
20825
|
-
* 상태 기록에 없던 행 노트를 이 행과 다시 잇는다(P7) —
|
|
20826
|
-
*
|
|
20827
|
-
* remote-first 는 `"write"` — 호출측이 받은 행으로 덮는다.
|
|
20910
|
+
* 상태 기록에 없던 행 노트를 이 행과 다시 잇는다(P7) — 받은 글과 견줘 {@link relinkOutcome} 대로. 잇지 않으면 null.
|
|
20911
|
+
* 원격으로 덮을 것은 `"write"` — 호출측이 받은 행으로 덮는다.
|
|
20828
20912
|
*/
|
|
20829
20913
|
async relinkExistingRow(filePath, existing, local, finalContent, page, dbConfig, rendered, bodyFingerprint) {
|
|
20830
20914
|
const same = sameNoteContent(local, finalContent) || sameAsPendingLinks(local, finalContent);
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
|
|
20915
|
+
const outcome = relinkOutcome(
|
|
20916
|
+
existing,
|
|
20917
|
+
same,
|
|
20918
|
+
this.unlinkedVault,
|
|
20919
|
+
this.config.sync.conflictStrategy
|
|
20920
|
+
);
|
|
20921
|
+
if (outcome === null) return null;
|
|
20922
|
+
if (outcome === "overwrite") return "write";
|
|
20923
|
+
const conflicted = outcome === "conflict";
|
|
20835
20924
|
const stat2 = same ? await this.vaultFs.getFileStat(filePath) : null;
|
|
20836
20925
|
this.stateDb.transaction(() => {
|
|
20837
20926
|
this.stateDb.upsert({
|
|
@@ -20858,31 +20947,57 @@ var DatabaseSyncer = class {
|
|
|
20858
20947
|
});
|
|
20859
20948
|
});
|
|
20860
20949
|
if (!conflicted) return { action: "unchanged", path: filePath, relinked: true };
|
|
20861
|
-
const record = this.stateDb.getByPath(filePath);
|
|
20862
20950
|
return {
|
|
20863
20951
|
action: "conflict",
|
|
20864
20952
|
path: filePath,
|
|
20865
20953
|
relinked: true,
|
|
20866
|
-
conflict: {
|
|
20867
|
-
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
|
|
20873
|
-
|
|
20874
|
-
|
|
20954
|
+
conflict: relinkConflict({
|
|
20955
|
+
record: this.stateDb.getByPath(filePath),
|
|
20956
|
+
path: filePath,
|
|
20957
|
+
local,
|
|
20958
|
+
remote: finalContent,
|
|
20959
|
+
pageId: page.id,
|
|
20960
|
+
lastEdited: page.last_edited_time,
|
|
20961
|
+
title: rendered.title
|
|
20962
|
+
})
|
|
20963
|
+
};
|
|
20964
|
+
}
|
|
20965
|
+
/**
|
|
20966
|
+
* dry-run — 실제 pull 이 상태 기록에 없던 이 행 노트를 다시 잇나(P7). 받은 행을 실제 pull 처럼 렌더하되 볼트 · 상태 DB 는
|
|
20967
|
+
* 바꾸지 않고({@link ImageHandler.preview}) 견준다. 잇지 않으면 null(새 이름으로 받는다), 충돌로 남길 것이면 그 충돌.
|
|
20968
|
+
*/
|
|
20969
|
+
async plannedRowRelink(page, dbConfig, untracked) {
|
|
20970
|
+
const local = await readLocalNote(this.vaultFs, untracked.path);
|
|
20971
|
+
if (local === void 0) return null;
|
|
20972
|
+
const rendered = await this.imageHandler.preview(
|
|
20973
|
+
() => this.renderRow(page, untracked.path, { localPath: untracked.path, adoptFrom: local })
|
|
20974
|
+
);
|
|
20975
|
+
const same = sameNoteContent(local, rendered.content) || sameAsPendingLinks(local, rendered.content);
|
|
20976
|
+
const outcome = relinkOutcome(
|
|
20977
|
+
untracked.existing,
|
|
20978
|
+
same,
|
|
20979
|
+
this.unlinkedVault,
|
|
20980
|
+
this.config.sync.conflictStrategy
|
|
20981
|
+
);
|
|
20982
|
+
if (outcome === null) return null;
|
|
20983
|
+
if (outcome !== "conflict") return {};
|
|
20984
|
+
return {
|
|
20985
|
+
conflict: relinkConflict({
|
|
20986
|
+
record: plannedRelinkRecord({
|
|
20987
|
+
path: untracked.path,
|
|
20875
20988
|
pageId: page.id,
|
|
20876
|
-
|
|
20989
|
+
parentId: dbConfig.databaseId,
|
|
20990
|
+
remote: rendered.content,
|
|
20877
20991
|
lastEdited: page.last_edited_time,
|
|
20878
|
-
|
|
20879
|
-
|
|
20880
|
-
|
|
20881
|
-
|
|
20882
|
-
|
|
20883
|
-
|
|
20884
|
-
|
|
20885
|
-
|
|
20992
|
+
fileType: "db-row"
|
|
20993
|
+
}),
|
|
20994
|
+
path: untracked.path,
|
|
20995
|
+
local,
|
|
20996
|
+
remote: rendered.content,
|
|
20997
|
+
pageId: page.id,
|
|
20998
|
+
lastEdited: page.last_edited_time,
|
|
20999
|
+
title: rendered.title
|
|
21000
|
+
})
|
|
20886
21001
|
};
|
|
20887
21002
|
}
|
|
20888
21003
|
/** 이번 받기에서 로컬 편집을 합친 행의 경로를 가져가고 비운다. */
|
|
@@ -23377,7 +23492,6 @@ function dropEmbeddedBaseMarkers(content, basePaths) {
|
|
|
23377
23492
|
|
|
23378
23493
|
// src/sync/database-discovery.ts
|
|
23379
23494
|
var DISCOVERY_RETRY_META_KEY = "discovery_retry";
|
|
23380
|
-
var DISCOVERY_SCANNED_META_KEY = "discovery_scanned_at";
|
|
23381
23495
|
function parseDiscoveryRetry(raw) {
|
|
23382
23496
|
if (!raw) return { parents: [], dbs: [] };
|
|
23383
23497
|
try {
|
|
@@ -23443,144 +23557,25 @@ var DatabaseDiscovery = class {
|
|
|
23443
23557
|
let deleted = 0;
|
|
23444
23558
|
let restored = 0;
|
|
23445
23559
|
try {
|
|
23446
|
-
const
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23456
|
-
|
|
23457
|
-
|
|
23458
|
-
|
|
23459
|
-
|
|
23460
|
-
|
|
23461
|
-
|
|
23462
|
-
|
|
23463
|
-
|
|
23464
|
-
}
|
|
23465
|
-
const folderKeys = new Map([...folderOwner].map(([folder, id]) => [pathKey(folder), id]));
|
|
23466
|
-
const claimFolder = (config) => {
|
|
23467
|
-
config.localFolder = resolveDbFolderPath(
|
|
23468
|
-
config.localFolder,
|
|
23469
|
-
config.databaseId,
|
|
23470
|
-
(f) => folderOwner.get(f) ?? folderKeys.get(pathKey(f)) ?? null
|
|
23471
|
-
);
|
|
23472
|
-
folderOwner.set(config.localFolder, config.databaseId);
|
|
23473
|
-
folderKeys.set(pathKey(config.localFolder), config.databaseId);
|
|
23474
|
-
};
|
|
23475
|
-
const inaccessibleIds = loadInaccessibleDbIds(this.stateDb);
|
|
23476
|
-
let inaccessibleChanged = false;
|
|
23477
|
-
const degrade = (rawId) => {
|
|
23478
|
-
const nohyph = rawId.replace(/-/g, "");
|
|
23479
|
-
if (!inaccessibleIds.has(nohyph)) {
|
|
23480
|
-
inaccessibleIds.add(nohyph);
|
|
23481
|
-
inaccessibleChanged = true;
|
|
23482
|
-
}
|
|
23483
|
-
getLogger().warn(
|
|
23484
|
-
`[Im-Nobsidian] DB ${rawId}: \uC811\uADFC \uAC00\uB2A5\uD55C data source \uC5C6\uC74C(\uB9C1\uD06C\uB4DC/\uBBF8\uACF5\uC720/\uC0AD\uC81C \uCD94\uC815) \u2014 \uB3D9\uAE30\uD654 \uB300\uC0C1\uC5D0\uC11C \uC81C\uC678(\uC81C\uBAA9\uC740 \uBD80\uBAA8 \uD398\uC774\uC9C0\uC5D0 \uBCF4\uC874)`
|
|
23485
|
-
);
|
|
23486
|
-
};
|
|
23487
|
-
if (forceRediscovery && inaccessibleIds.size > 0) {
|
|
23488
|
-
getLogger().info(
|
|
23489
|
-
`[Im-Nobsidian] --force: \uC811\uADFC \uBD88\uAC00 DB ${inaccessibleIds.size}\uAC1C denylist \uC7AC\uAC80\uC99D`
|
|
23490
|
-
);
|
|
23491
|
-
inaccessibleIds.clear();
|
|
23492
|
-
inaccessibleChanged = true;
|
|
23493
|
-
}
|
|
23494
|
-
const linkedMap = loadLinkedDbMap(this.stateDb);
|
|
23495
|
-
let linkedChanged = false;
|
|
23496
|
-
const linkedNow = /* @__PURE__ */ new Set();
|
|
23497
|
-
const retry = parseDiscoveryRetry(this.stateDb.getMeta(DISCOVERY_RETRY_META_KEY));
|
|
23498
|
-
const nextRetry = { parents: [], dbs: [] };
|
|
23499
|
-
const pathOf = (pageId) => notionIdsEqual(pageId, this.config.notion.rootPageId) ? "" : this.stateDb.getByNotionId(pageId)?.obsidianPath ?? pageId;
|
|
23500
|
-
const unreadParent = (pageId, error) => {
|
|
23501
|
-
nextRetry.parents.push(pageId);
|
|
23502
|
-
failed.push({
|
|
23503
|
-
path: pathOf(pageId),
|
|
23504
|
-
operation: "update",
|
|
23505
|
-
error: `\uD558\uC704 DB \uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uD655\uC778\uD55C\uB2E4: ${error}`
|
|
23506
|
-
});
|
|
23507
|
-
};
|
|
23508
|
-
const retryLater = (dbId, parentPageId, error) => {
|
|
23509
|
-
nextRetry.dbs.push({ dbId, parentPageId });
|
|
23510
|
-
failed.push({
|
|
23511
|
-
path: pathOf(parentPageId),
|
|
23512
|
-
operation: "create",
|
|
23513
|
-
error: `DB ${shortNotionId(dbId)} \uB97C \uC77D\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uBCF8\uB2E4: ${error}`
|
|
23514
|
-
});
|
|
23515
|
-
};
|
|
23516
|
-
const register = async (dbId, parentPageId) => {
|
|
23517
|
-
const nohyph = dbId.replace(/-/g, "");
|
|
23518
|
-
if (knownIds.has(nohyph) || inaccessibleIds.has(nohyph)) return;
|
|
23519
|
-
const registerOriginal = async (origNohyph) => {
|
|
23520
|
-
if (knownIds.has(origNohyph) || inaccessibleIds.has(origNohyph)) return;
|
|
23521
|
-
const origId = normalizeNotionId(origNohyph);
|
|
23522
|
-
const outcome2 = await this.buildDiscoveredDbConfig(origId, parentPageId);
|
|
23523
|
-
knownIds.add(origNohyph);
|
|
23524
|
-
if (outcome2.kind === "ok") {
|
|
23525
|
-
claimFolder(outcome2.config);
|
|
23526
|
-
dbConfigs.push(outcome2.config);
|
|
23527
|
-
registeredNow.add(origNohyph);
|
|
23528
|
-
changed = true;
|
|
23529
|
-
} else if (outcome2.kind === "error") {
|
|
23530
|
-
retryLater(origId, parentPageId, outcome2.error);
|
|
23531
|
-
} else {
|
|
23532
|
-
degrade(origId);
|
|
23533
|
-
}
|
|
23534
|
-
};
|
|
23535
|
-
const knownOriginal = linkedMap.get(nohyph);
|
|
23536
|
-
if (knownOriginal) {
|
|
23537
|
-
await registerOriginal(knownOriginal);
|
|
23538
|
-
return;
|
|
23539
|
-
}
|
|
23540
|
-
const outcome = await this.buildDiscoveredDbConfig(dbId, parentPageId);
|
|
23541
|
-
knownIds.add(nohyph);
|
|
23542
|
-
if (outcome.kind === "ok") {
|
|
23543
|
-
claimFolder(outcome.config);
|
|
23544
|
-
dbConfigs.push(outcome.config);
|
|
23545
|
-
registeredNow.add(nohyph);
|
|
23546
|
-
changed = true;
|
|
23547
|
-
} else if (outcome.kind === "linked") {
|
|
23548
|
-
const origNohyph = outcome.originalDbId.replace(/-/g, "");
|
|
23549
|
-
linkedMap.set(nohyph, origNohyph);
|
|
23550
|
-
linkedNow.add(nohyph);
|
|
23551
|
-
linkedChanged = true;
|
|
23552
|
-
await registerOriginal(origNohyph);
|
|
23553
|
-
} else if (outcome.kind === "inaccessible") {
|
|
23554
|
-
degrade(dbId);
|
|
23555
|
-
} else if (outcome.kind === "error") {
|
|
23556
|
-
retryLater(dbId, parentPageId, outcome.error);
|
|
23557
|
-
}
|
|
23558
|
-
};
|
|
23559
|
-
const scanAll = forceRediscovery || dbConfigs.length === 0 && !this.stateDb.getMeta(DISCOVERY_SCANNED_META_KEY);
|
|
23560
|
-
if (signal?.aborted) {
|
|
23561
|
-
nextRetry.parents.push(...retry.parents);
|
|
23562
|
-
nextRetry.dbs.push(...retry.dbs);
|
|
23563
|
-
} else {
|
|
23564
|
-
const root = this.config.notion.rootPageId;
|
|
23565
|
-
const scanIds = scanAll ? [
|
|
23566
|
-
root,
|
|
23567
|
-
...this.stateDb.getAll().filter((r) => r.notionPageId).map((r) => r.notionPageId)
|
|
23568
|
-
].filter((id) => !this._bodyReadPages.has(normalizeNotionId(id))) : options.rescanRoot && !retry.parents.some((id) => notionIdsEqual(id, root)) ? [root, ...retry.parents] : retry.parents;
|
|
23569
|
-
if (scanIds.length > 0) {
|
|
23570
|
-
const { found: found2, unread } = await this.discoverChildDatabases(scanIds);
|
|
23571
|
-
for (const { dbId, parentPageId } of found2) {
|
|
23572
|
-
await register(dbId, parentPageId);
|
|
23573
|
-
}
|
|
23574
|
-
for (const { pageId, error } of unread) unreadParent(pageId, error);
|
|
23575
|
-
}
|
|
23576
|
-
for (const { dbId, parentPageId } of retry.dbs) {
|
|
23577
|
-
await register(dbId, parentPageId);
|
|
23578
|
-
}
|
|
23579
|
-
if (scanAll) this.stateDb.setMeta(DISCOVERY_SCANNED_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
23580
|
-
}
|
|
23581
|
-
for (const [nohyph, parentPageId] of this._inlineDbRefs) {
|
|
23582
|
-
await register(normalizeNotionId(nohyph), parentPageId);
|
|
23583
|
-
}
|
|
23560
|
+
const registry = await this.openRegistry(failed, {
|
|
23561
|
+
forceRediscovery,
|
|
23562
|
+
...options.rescanRoot !== void 0 ? { rescanRoot: options.rescanRoot } : {},
|
|
23563
|
+
...signal ? { signal } : {},
|
|
23564
|
+
markScanned: true
|
|
23565
|
+
});
|
|
23566
|
+
const {
|
|
23567
|
+
dbConfigs,
|
|
23568
|
+
configuredIds,
|
|
23569
|
+
registeredNow,
|
|
23570
|
+
inaccessibleIds,
|
|
23571
|
+
linkedMap,
|
|
23572
|
+
linkedNow,
|
|
23573
|
+
retry,
|
|
23574
|
+
nextRetry,
|
|
23575
|
+
dirty,
|
|
23576
|
+
register,
|
|
23577
|
+
degrade
|
|
23578
|
+
} = registry;
|
|
23584
23579
|
const stillSyncable = [];
|
|
23585
23580
|
const pulledWhole = /* @__PURE__ */ new Set();
|
|
23586
23581
|
let queue = [...dbConfigs];
|
|
@@ -23618,14 +23613,14 @@ var DatabaseDiscovery = class {
|
|
|
23618
23613
|
config.localFolder = move.to + config.localFolder.slice(move.from.length);
|
|
23619
23614
|
}
|
|
23620
23615
|
}
|
|
23621
|
-
|
|
23616
|
+
dirty.list = true;
|
|
23622
23617
|
}
|
|
23623
23618
|
});
|
|
23624
23619
|
if (dbResult.linkedOriginalDbId) {
|
|
23625
23620
|
linkedMap.set(nohyph, dbResult.linkedOriginalDbId.replace(/-/g, ""));
|
|
23626
23621
|
linkedNow.add(nohyph);
|
|
23627
|
-
|
|
23628
|
-
|
|
23622
|
+
dirty.linked = true;
|
|
23623
|
+
dirty.list = true;
|
|
23629
23624
|
ledger?.settle(dbConfig.databaseId);
|
|
23630
23625
|
getLogger().info(
|
|
23631
23626
|
`[Im-Nobsidian] DB ${dbConfig.databaseId}: linked view \uCEE8\uD14C\uC774\uB108 \uAC10\uC9C0 \u2192 \uD589\uC740 \uC6D0\uBCF8 ${dbResult.linkedOriginalDbId} \uD3F4\uB354\uAC00 \uB2E8\uC77C \uC18C\uC720(F25)`
|
|
@@ -23680,18 +23675,18 @@ var DatabaseDiscovery = class {
|
|
|
23680
23675
|
}
|
|
23681
23676
|
}
|
|
23682
23677
|
if (!signal?.aborted) {
|
|
23683
|
-
|
|
23678
|
+
dirty.linked = await this.pullLinkedViews(linkedMap, stillSyncable, {
|
|
23684
23679
|
pulled: (container, original) => forceRediscovery || linkedNow.has(container) || pulledWhole.has(original)
|
|
23685
|
-
}) ||
|
|
23680
|
+
}) || dirty.linked;
|
|
23686
23681
|
}
|
|
23687
23682
|
ledger?.keepOnly([...configuredIds, ...stillSyncable.map((c) => c.databaseId)]);
|
|
23688
|
-
if (
|
|
23683
|
+
if (dirty.list || dirty.inaccessible || stillSyncable.length !== dbConfigs.length) {
|
|
23689
23684
|
this.stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(stillSyncable));
|
|
23690
23685
|
}
|
|
23691
|
-
if (
|
|
23686
|
+
if (dirty.inaccessible) {
|
|
23692
23687
|
this.stateDb.setMeta(INACCESSIBLE_DBS_META_KEY, JSON.stringify([...inaccessibleIds]));
|
|
23693
23688
|
}
|
|
23694
|
-
if (
|
|
23689
|
+
if (dirty.linked) {
|
|
23695
23690
|
this.stateDb.setMeta(LINKED_DBS_META_KEY, JSON.stringify(Object.fromEntries(linkedMap)));
|
|
23696
23691
|
}
|
|
23697
23692
|
if (retry.parents.length + retry.dbs.length + nextRetry.parents.length + nextRetry.dbs.length > 0) {
|
|
@@ -23708,6 +23703,196 @@ var DatabaseDiscovery = class {
|
|
|
23708
23703
|
}
|
|
23709
23704
|
return { created, updated, deleted, restored };
|
|
23710
23705
|
}
|
|
23706
|
+
/**
|
|
23707
|
+
* 이번 pull 이 찾을 DB 를 등록부에 올린다 — 블록을 훑고({@link discoverChildDatabases}) 받은 본문의 `<database>` 를
|
|
23708
|
+
* 더한다. 상태 DB 에는 쓰지 않는다 — 등록부를 적는 것은 실제 pull 의 마감이고 dry-run 은 버린다. 다 훑은 표시만
|
|
23709
|
+
* `markScanned` 일 때 적는다.
|
|
23710
|
+
*
|
|
23711
|
+
* @param failed 읽지 못한 페이지 · DB — 이유와 함께 싣는다.
|
|
23712
|
+
* @param options.notePathOf 페이지 → 볼트 경로. 새 DB 폴더를 부모 노트 옆에 둔다 — dry-run 은 다시 이을 노트의 자리를 준다.
|
|
23713
|
+
*/
|
|
23714
|
+
async openRegistry(failed, options) {
|
|
23715
|
+
const { forceRediscovery, signal } = options;
|
|
23716
|
+
const { notePathOf } = options;
|
|
23717
|
+
const dbConfigs = parseDiscoveredDbs(this.stateDb.getMeta(DISCOVERED_DBS_META_KEY));
|
|
23718
|
+
const configuredIds = new Set(
|
|
23719
|
+
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
23720
|
+
);
|
|
23721
|
+
const knownIds = /* @__PURE__ */ new Set([
|
|
23722
|
+
...configuredIds,
|
|
23723
|
+
...dbConfigs.map((c) => c.databaseId.replace(/-/g, ""))
|
|
23724
|
+
]);
|
|
23725
|
+
const dirty = { list: false, inaccessible: false, linked: false };
|
|
23726
|
+
const registeredNow = /* @__PURE__ */ new Set();
|
|
23727
|
+
const folderOwner = new Map(
|
|
23728
|
+
(this.config.notion.databases ?? []).map((d) => [d.localFolder, d.databaseId])
|
|
23729
|
+
);
|
|
23730
|
+
const repairedCount = repairDbFolderCollisions(dbConfigs, folderOwner);
|
|
23731
|
+
for (const c of dbConfigs) folderOwner.set(c.localFolder, c.databaseId);
|
|
23732
|
+
if (repairedCount > 0) {
|
|
23733
|
+
dirty.list = true;
|
|
23734
|
+
getLogger().info(`[Im-Nobsidian] \uB3D9\uBA85 DB \uD3F4\uB354 \uCDA9\uB3CC ${repairedCount}\uAC74 \uBD84\uB9AC(F24)`);
|
|
23735
|
+
}
|
|
23736
|
+
const folderKeys = new Map([...folderOwner].map(([folder, id]) => [pathKey(folder), id]));
|
|
23737
|
+
const claimFolder = (config) => {
|
|
23738
|
+
config.localFolder = resolveDbFolderPath(
|
|
23739
|
+
config.localFolder,
|
|
23740
|
+
config.databaseId,
|
|
23741
|
+
(f) => folderOwner.get(f) ?? folderKeys.get(pathKey(f)) ?? null
|
|
23742
|
+
);
|
|
23743
|
+
folderOwner.set(config.localFolder, config.databaseId);
|
|
23744
|
+
folderKeys.set(pathKey(config.localFolder), config.databaseId);
|
|
23745
|
+
};
|
|
23746
|
+
const inaccessibleIds = loadInaccessibleDbIds(this.stateDb);
|
|
23747
|
+
const degrade = (rawId) => {
|
|
23748
|
+
const nohyph = rawId.replace(/-/g, "");
|
|
23749
|
+
if (!inaccessibleIds.has(nohyph)) {
|
|
23750
|
+
inaccessibleIds.add(nohyph);
|
|
23751
|
+
dirty.inaccessible = true;
|
|
23752
|
+
}
|
|
23753
|
+
getLogger().warn(
|
|
23754
|
+
`[Im-Nobsidian] DB ${rawId}: \uC811\uADFC \uAC00\uB2A5\uD55C data source \uC5C6\uC74C(\uB9C1\uD06C\uB4DC/\uBBF8\uACF5\uC720/\uC0AD\uC81C \uCD94\uC815) \u2014 \uB3D9\uAE30\uD654 \uB300\uC0C1\uC5D0\uC11C \uC81C\uC678(\uC81C\uBAA9\uC740 \uBD80\uBAA8 \uD398\uC774\uC9C0\uC5D0 \uBCF4\uC874)`
|
|
23755
|
+
);
|
|
23756
|
+
};
|
|
23757
|
+
if (forceRediscovery && inaccessibleIds.size > 0) {
|
|
23758
|
+
getLogger().info(
|
|
23759
|
+
`[Im-Nobsidian] --force: \uC811\uADFC \uBD88\uAC00 DB ${inaccessibleIds.size}\uAC1C denylist \uC7AC\uAC80\uC99D`
|
|
23760
|
+
);
|
|
23761
|
+
inaccessibleIds.clear();
|
|
23762
|
+
dirty.inaccessible = true;
|
|
23763
|
+
}
|
|
23764
|
+
const linkedMap = loadLinkedDbMap(this.stateDb);
|
|
23765
|
+
const linkedNow = /* @__PURE__ */ new Set();
|
|
23766
|
+
const retry = parseDiscoveryRetry(this.stateDb.getMeta(DISCOVERY_RETRY_META_KEY));
|
|
23767
|
+
const nextRetry = { parents: [], dbs: [] };
|
|
23768
|
+
const pathOf = (pageId) => notionIdsEqual(pageId, this.config.notion.rootPageId) ? "" : this.stateDb.getByNotionId(pageId)?.obsidianPath ?? pageId;
|
|
23769
|
+
const unreadParent = (pageId, error) => {
|
|
23770
|
+
nextRetry.parents.push(pageId);
|
|
23771
|
+
failed.push({
|
|
23772
|
+
path: pathOf(pageId),
|
|
23773
|
+
operation: "update",
|
|
23774
|
+
error: `\uD558\uC704 DB \uB97C \uD655\uC778\uD558\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uD655\uC778\uD55C\uB2E4: ${error}`
|
|
23775
|
+
});
|
|
23776
|
+
};
|
|
23777
|
+
const retryLater = (dbId, parentPageId, error) => {
|
|
23778
|
+
nextRetry.dbs.push({ dbId, parentPageId });
|
|
23779
|
+
failed.push({
|
|
23780
|
+
path: pathOf(parentPageId),
|
|
23781
|
+
operation: "create",
|
|
23782
|
+
error: `DB ${shortNotionId(dbId)} \uB97C \uC77D\uC9C0 \uBABB\uD568 \u2014 \uB2E4\uC74C pull \uC774 \uB2E4\uC2DC \uBCF8\uB2E4: ${error}`
|
|
23783
|
+
});
|
|
23784
|
+
};
|
|
23785
|
+
const register = async (dbId, parentPageId) => {
|
|
23786
|
+
const nohyph = dbId.replace(/-/g, "");
|
|
23787
|
+
if (knownIds.has(nohyph) || inaccessibleIds.has(nohyph)) return;
|
|
23788
|
+
const registerOriginal = async (origNohyph) => {
|
|
23789
|
+
if (knownIds.has(origNohyph) || inaccessibleIds.has(origNohyph)) return;
|
|
23790
|
+
const origId = normalizeNotionId(origNohyph);
|
|
23791
|
+
const outcome2 = await this.buildDiscoveredDbConfig(origId, parentPageId, notePathOf);
|
|
23792
|
+
knownIds.add(origNohyph);
|
|
23793
|
+
if (outcome2.kind === "ok") {
|
|
23794
|
+
claimFolder(outcome2.config);
|
|
23795
|
+
dbConfigs.push(outcome2.config);
|
|
23796
|
+
registeredNow.add(origNohyph);
|
|
23797
|
+
dirty.list = true;
|
|
23798
|
+
} else if (outcome2.kind === "error") {
|
|
23799
|
+
retryLater(origId, parentPageId, outcome2.error);
|
|
23800
|
+
} else {
|
|
23801
|
+
degrade(origId);
|
|
23802
|
+
}
|
|
23803
|
+
};
|
|
23804
|
+
const knownOriginal = linkedMap.get(nohyph);
|
|
23805
|
+
if (knownOriginal) {
|
|
23806
|
+
await registerOriginal(knownOriginal);
|
|
23807
|
+
return;
|
|
23808
|
+
}
|
|
23809
|
+
const outcome = await this.buildDiscoveredDbConfig(dbId, parentPageId, notePathOf);
|
|
23810
|
+
knownIds.add(nohyph);
|
|
23811
|
+
if (outcome.kind === "ok") {
|
|
23812
|
+
claimFolder(outcome.config);
|
|
23813
|
+
dbConfigs.push(outcome.config);
|
|
23814
|
+
registeredNow.add(nohyph);
|
|
23815
|
+
dirty.list = true;
|
|
23816
|
+
} else if (outcome.kind === "linked") {
|
|
23817
|
+
const origNohyph = outcome.originalDbId.replace(/-/g, "");
|
|
23818
|
+
linkedMap.set(nohyph, origNohyph);
|
|
23819
|
+
linkedNow.add(nohyph);
|
|
23820
|
+
dirty.linked = true;
|
|
23821
|
+
await registerOriginal(origNohyph);
|
|
23822
|
+
} else if (outcome.kind === "inaccessible") {
|
|
23823
|
+
degrade(dbId);
|
|
23824
|
+
} else if (outcome.kind === "error") {
|
|
23825
|
+
retryLater(dbId, parentPageId, outcome.error);
|
|
23826
|
+
}
|
|
23827
|
+
};
|
|
23828
|
+
const scanAll = forceRediscovery || discoveryScanPending(this.stateDb);
|
|
23829
|
+
if (signal?.aborted) {
|
|
23830
|
+
nextRetry.parents.push(...retry.parents);
|
|
23831
|
+
nextRetry.dbs.push(...retry.dbs);
|
|
23832
|
+
} else {
|
|
23833
|
+
const root = this.config.notion.rootPageId;
|
|
23834
|
+
const scanIds = scanAll ? [
|
|
23835
|
+
root,
|
|
23836
|
+
...this.stateDb.getAll().filter((r) => r.notionPageId).map((r) => r.notionPageId)
|
|
23837
|
+
].filter((id) => !this._bodyReadPages.has(normalizeNotionId(id))) : options.rescanRoot && !retry.parents.some((id) => notionIdsEqual(id, root)) ? [root, ...retry.parents] : retry.parents;
|
|
23838
|
+
if (scanIds.length > 0) {
|
|
23839
|
+
const { found: found2, unread } = await this.discoverChildDatabases(scanIds);
|
|
23840
|
+
for (const { dbId, parentPageId } of found2) {
|
|
23841
|
+
await register(dbId, parentPageId);
|
|
23842
|
+
}
|
|
23843
|
+
for (const { pageId, error } of unread) unreadParent(pageId, error);
|
|
23844
|
+
}
|
|
23845
|
+
for (const { dbId, parentPageId } of retry.dbs) {
|
|
23846
|
+
await register(dbId, parentPageId);
|
|
23847
|
+
}
|
|
23848
|
+
if (scanAll && options.markScanned) {
|
|
23849
|
+
this.stateDb.setMeta(DISCOVERY_SCANNED_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
23850
|
+
}
|
|
23851
|
+
}
|
|
23852
|
+
for (const [nohyph, parentPageId] of this._inlineDbRefs) {
|
|
23853
|
+
await register(normalizeNotionId(nohyph), parentPageId);
|
|
23854
|
+
}
|
|
23855
|
+
return {
|
|
23856
|
+
dbConfigs,
|
|
23857
|
+
configuredIds,
|
|
23858
|
+
registeredNow,
|
|
23859
|
+
inaccessibleIds,
|
|
23860
|
+
linkedMap,
|
|
23861
|
+
linkedNow,
|
|
23862
|
+
retry,
|
|
23863
|
+
nextRetry,
|
|
23864
|
+
dirty,
|
|
23865
|
+
register,
|
|
23866
|
+
degrade
|
|
23867
|
+
};
|
|
23868
|
+
}
|
|
23869
|
+
/**
|
|
23870
|
+
* dry-run — 이번 pull 이 새로 찾을 DB(실제 pull 의 {@link openRegistry} 와 같은 규칙). 볼트 · 상태 DB 를 바꾸지 않는다. 받은
|
|
23871
|
+
* 본문은 dry-run 이 이미 읽은 것(다시 이을 노트 · 로컬도 고친 노트)만 본다 — {@link bodyRead}.
|
|
23872
|
+
*
|
|
23873
|
+
* @returns 새로 찾을 DB 와 그 폴더를 가를 전체 목록(발견해 둔 DB 포함).
|
|
23874
|
+
*/
|
|
23875
|
+
async plannedNewDatabases(failed, options) {
|
|
23876
|
+
if (isDatabaseMode(this.config)) return { found: [], all: [] };
|
|
23877
|
+
const registry = await this.openRegistry(failed, {
|
|
23878
|
+
forceRediscovery: false,
|
|
23879
|
+
rescanRoot: options.rescanRoot,
|
|
23880
|
+
markScanned: false,
|
|
23881
|
+
notePathOf: options.notePathOf
|
|
23882
|
+
});
|
|
23883
|
+
return {
|
|
23884
|
+
found: registry.dbConfigs.filter(
|
|
23885
|
+
(config) => registry.registeredNow.has(config.databaseId.replace(/-/g, ""))
|
|
23886
|
+
),
|
|
23887
|
+
all: registry.dbConfigs.filter(
|
|
23888
|
+
(config) => !registry.inaccessibleIds.has(config.databaseId.replace(/-/g, ""))
|
|
23889
|
+
)
|
|
23890
|
+
};
|
|
23891
|
+
}
|
|
23892
|
+
/** 이번 pull 에서 본문을 받은 페이지인가 — 그 본문의 `<database>` 로 새 DB 를 찾았다. */
|
|
23893
|
+
bodyRead(pageId) {
|
|
23894
|
+
return this._bodyReadPages.has(normalizeNotionId(pageId));
|
|
23895
|
+
}
|
|
23711
23896
|
/**
|
|
23712
23897
|
* 링크드 뷰 컨테이너마다 그 보기로 `.base` 를 쓴다({@link DatabaseSyncer.pullLinkedView}). 원본 DB 를 받은 pull ·
|
|
23713
23898
|
* 이번에 해소한 컨테이너 · 아직 `.base` 가 없는 컨테이너만 — 보기만 고친 것은 DB 수정 시각을 바꾸지 않아 원본처럼
|
|
@@ -23760,7 +23945,7 @@ var DatabaseDiscovery = class {
|
|
|
23760
23945
|
* (예: 부모 "AI Engineer (1).md" + DB "Minirecord Project" → "AI Engineer (1)/Minirecord-Project")
|
|
23761
23946
|
* - 제목 취득 실패 시 부모 폴더명 기반 안전 이름으로 폴백
|
|
23762
23947
|
*/
|
|
23763
|
-
async buildDiscoveredDbConfig(dbId, parentPageId) {
|
|
23948
|
+
async buildDiscoveredDbConfig(dbId, parentPageId, notePathOf = (pageId) => this.stateDb.getByNotionId(pageId)?.obsidianPath ?? null) {
|
|
23764
23949
|
let dbTitle;
|
|
23765
23950
|
try {
|
|
23766
23951
|
const info = await this.notionClient.getDatabaseSyncability(dbId);
|
|
@@ -23786,10 +23971,9 @@ var DatabaseDiscovery = class {
|
|
|
23786
23971
|
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uC124\uC815 \uC0DD\uC131 \uC2E4\uD328:`, error);
|
|
23787
23972
|
return { kind: "error", error: error instanceof Error ? error.message : String(error) };
|
|
23788
23973
|
}
|
|
23789
|
-
const
|
|
23974
|
+
const obsPath = parentPageId ? notePathOf(parentPageId) : null;
|
|
23790
23975
|
let parentFolder = "";
|
|
23791
|
-
if (
|
|
23792
|
-
const obsPath = parentEntry.obsidianPath;
|
|
23976
|
+
if (obsPath) {
|
|
23793
23977
|
if (isMarkdownPath(obsPath)) {
|
|
23794
23978
|
const parts = obsPath.split("/");
|
|
23795
23979
|
const fileBase = stripMarkdownExtension(parts.pop() ?? "");
|
|
@@ -27451,34 +27635,80 @@ var PagePuller = class {
|
|
|
27451
27635
|
);
|
|
27452
27636
|
}
|
|
27453
27637
|
/**
|
|
27454
|
-
* dry-run — 실제 pull 이 이 새 페이지를 새로 받지 않고 상태 기록에 없던 노트 · 폴더와 다시 잇는가(P7).
|
|
27455
|
-
* 이미 있는 폴더(`folder` —
|
|
27456
|
-
*
|
|
27457
|
-
* 더한다. 볼트도 상태 DB 도
|
|
27638
|
+
* dry-run — 실제 pull({@link createPulledPage})이 이 새 페이지를 새로 받지 않고 상태 기록에 없던 노트 · 폴더와 다시 잇는가(P7).
|
|
27639
|
+
* 다시 이을 노트(`note` — 충돌로 남길 것이면 `conflict`) · 이미 있는 폴더(`folder` — 폴더로만 받는 페이지), 아니면 null.
|
|
27640
|
+
* 그 자리에 무엇이 있을 때만 본문을 받아 실제 pull 처럼 렌더해 견준다 — 미디어는 받되 볼트에 쓰지 않는다
|
|
27641
|
+
* ({@link ImageHandler.preview}). `taken` 은 이번 미리 보기가 이미 이은 자리({@link pathKey}) — 고르면 더한다. 볼트도 상태 DB 도
|
|
27642
|
+
* 바꾸지 않는다.
|
|
27458
27643
|
*/
|
|
27459
27644
|
async plannedRelink(pageId, taken) {
|
|
27460
27645
|
const page = this.detector.discoveredPage(pageId) ?? await this.notionClient.getPage(pageId);
|
|
27461
|
-
const
|
|
27646
|
+
const title = this.notionClient.extractTitle(page);
|
|
27647
|
+
const safeName = sanitizeFileName(title);
|
|
27462
27648
|
const parentPath = await this.resolveParentPath(page);
|
|
27649
|
+
if (this.excludedCreatePath(parentPath, safeName) !== null) return null;
|
|
27463
27650
|
const folder = parentPath ? `${parentPath}/${safeName}` : safeName;
|
|
27464
|
-
|
|
27465
|
-
|
|
27466
|
-
|
|
27467
|
-
|
|
27468
|
-
dir,
|
|
27469
|
-
safeName,
|
|
27470
|
-
pageId,
|
|
27471
|
-
(key) => taken.has(key)
|
|
27472
|
-
);
|
|
27473
|
-
if (found2 && relinksWithoutContent(found2, this.unlinkedVault)) {
|
|
27474
|
-
taken.add(pathKey(found2.path));
|
|
27475
|
-
return { path: found2.path, kind: "note" };
|
|
27476
|
-
}
|
|
27651
|
+
const untrackedIn = (dir) => findUntrackedNote(this.stateDb, this.vaultFs, dir, safeName, pageId, (key) => taken.has(key));
|
|
27652
|
+
const folderExists = async () => !taken.has(pathKey(folder)) && this.stateDb.getByPath(folder) === null && await this.vaultFs.exists(folder);
|
|
27653
|
+
if (!await untrackedIn(parentPath) && !await untrackedIn(folder) && !await folderExists()) {
|
|
27654
|
+
return null;
|
|
27477
27655
|
}
|
|
27478
|
-
const
|
|
27479
|
-
|
|
27480
|
-
|
|
27481
|
-
|
|
27656
|
+
const fetched = await this.fetchPageMarkdown(pageId);
|
|
27657
|
+
const hasChildPages = this.hasChildContainers(pageId, fetched.children);
|
|
27658
|
+
const folderOnly = hasChildPages && !fetched.hasBody || fetched.folderFiles !== null && (hasChildPages || fetched.folderFiles.named);
|
|
27659
|
+
if (folderOnly) {
|
|
27660
|
+
if (!await folderExists()) return null;
|
|
27661
|
+
taken.add(pathKey(folder));
|
|
27662
|
+
return { path: folder, kind: "folder" };
|
|
27663
|
+
}
|
|
27664
|
+
const found2 = await untrackedIn(hasChildPages ? folder : parentPath);
|
|
27665
|
+
if (!found2) return null;
|
|
27666
|
+
const local = await readLocalNote(this.vaultFs, found2.path);
|
|
27667
|
+
if (local === void 0) return null;
|
|
27668
|
+
const rendered = await this.imageHandler.preview(
|
|
27669
|
+
() => this.renderPageNote({
|
|
27670
|
+
pageId,
|
|
27671
|
+
page,
|
|
27672
|
+
fetched,
|
|
27673
|
+
filePath: found2.path,
|
|
27674
|
+
title,
|
|
27675
|
+
titleFollowsName: extractTitle(found2.path) === title,
|
|
27676
|
+
downloadMedia: true,
|
|
27677
|
+
local: { content: local, markers: [] },
|
|
27678
|
+
adoptLocalMedia: true
|
|
27679
|
+
})
|
|
27680
|
+
);
|
|
27681
|
+
const same = sameNoteContent(local, rendered.content) || sameAsPendingLinks(local, rendered.content);
|
|
27682
|
+
const outcome = relinkOutcome(
|
|
27683
|
+
found2.existing,
|
|
27684
|
+
same,
|
|
27685
|
+
this.unlinkedVault,
|
|
27686
|
+
this.config.sync.conflictStrategy
|
|
27687
|
+
);
|
|
27688
|
+
if (outcome === null) return null;
|
|
27689
|
+
taken.add(pathKey(found2.path));
|
|
27690
|
+
if (outcome !== "conflict") return { path: found2.path, kind: "note" };
|
|
27691
|
+
const parentId = await extractParentId(this.notionClient, page);
|
|
27692
|
+
return {
|
|
27693
|
+
path: found2.path,
|
|
27694
|
+
kind: "note",
|
|
27695
|
+
conflict: relinkConflict({
|
|
27696
|
+
record: plannedRelinkRecord({
|
|
27697
|
+
path: found2.path,
|
|
27698
|
+
pageId,
|
|
27699
|
+
parentId,
|
|
27700
|
+
remote: rendered.content,
|
|
27701
|
+
lastEdited: page.last_edited_time,
|
|
27702
|
+
fileType: hasChildPages ? "folder-note" : "file"
|
|
27703
|
+
}),
|
|
27704
|
+
path: found2.path,
|
|
27705
|
+
local,
|
|
27706
|
+
remote: rendered.content,
|
|
27707
|
+
pageId,
|
|
27708
|
+
lastEdited: page.last_edited_time,
|
|
27709
|
+
title
|
|
27710
|
+
})
|
|
27711
|
+
};
|
|
27482
27712
|
}
|
|
27483
27713
|
/**
|
|
27484
27714
|
* 새 페이지가 놓일 자리가 뺀 자리인가 — 부모 폴더 · 노트 이름 · 폴더 노트가 될 때의 폴더를 본다. 받기 전에는 폴더 노트가
|
|
@@ -27561,19 +27791,19 @@ var PagePuller = class {
|
|
|
27561
27791
|
/**
|
|
27562
27792
|
* 상태 기록에 없는 노트가 이 페이지가 놓일 자리에 이미 있다 — 상태 DB 를 잃었거나 처음 잇는 볼트다(P7).
|
|
27563
27793
|
* 예전에는 남의 노트로 보고 id 를 붙인 이름으로 또 받아 모든 노트가 두 벌이 됐다(push 는 모든 페이지를 또 만들었다).
|
|
27564
|
-
*
|
|
27565
|
-
* - 글이 같으면(끝 개행만 달라도) 그 노트를 이 페이지와 다시 잇는다 — 쓰지 않는다.
|
|
27566
|
-
* - 다르면 이 페이지 id 를 단 이름(지난번 이 페이지를 받은 이름)이면 충돌 전략을 따른다: remote-first 는 원격으로 덮고 ·
|
|
27567
|
-
* local-first 는 로컬을 두어 다음 push 가 올리고 · 그 밖은 충돌로 남긴다(지난 사본이 없어 merge 도 합칠 수 없다).
|
|
27568
|
-
* - 자연 이름에 있는 다른 글은 이은 노트가 하나도 없는 볼트에서만 잇고 전략과 상관없이 충돌로 남긴다 — 다른 노트일 수 있어
|
|
27569
|
-
* 덮거나 올리면 잃는다. 이은 노트가 있는 볼트에서는 잇지 않는다(null) — 그 볼트의 새 노트일 수 있다.
|
|
27794
|
+
* 받은 글과 견줘 {@link relinkOutcome} 대로 잇는다 — 잇지 않으면 null(id 를 단 새 이름으로 받는다).
|
|
27570
27795
|
*/
|
|
27571
27796
|
relinkExistingNote(filePath, existing, local, rendered, note) {
|
|
27572
27797
|
const same = sameNoteContent(local, rendered.content) || sameAsPendingLinks(local, rendered.content);
|
|
27573
|
-
|
|
27798
|
+
const outcome = relinkOutcome(
|
|
27799
|
+
existing,
|
|
27800
|
+
same,
|
|
27801
|
+
this.unlinkedVault,
|
|
27802
|
+
this.config.sync.conflictStrategy
|
|
27803
|
+
);
|
|
27804
|
+
if (outcome === null) return null;
|
|
27574
27805
|
return (async () => {
|
|
27575
|
-
|
|
27576
|
-
if (same || strategy === "local-first") {
|
|
27806
|
+
if (outcome === "relink") {
|
|
27577
27807
|
await this.recordPulledNote(filePath, note, {
|
|
27578
27808
|
content: rendered.content,
|
|
27579
27809
|
properties: rendered.properties,
|
|
@@ -27584,7 +27814,7 @@ var PagePuller = class {
|
|
|
27584
27814
|
});
|
|
27585
27815
|
return { path: filePath, relinked: true };
|
|
27586
27816
|
}
|
|
27587
|
-
if (
|
|
27817
|
+
if (outcome === "overwrite") {
|
|
27588
27818
|
await this.vaultFs.writeFile(filePath, rendered.content);
|
|
27589
27819
|
await this.recordPulledNote(filePath, note, {
|
|
27590
27820
|
content: rendered.content,
|
|
@@ -27600,27 +27830,15 @@ var PagePuller = class {
|
|
|
27600
27830
|
snapshot: null,
|
|
27601
27831
|
differsOnDisk: true
|
|
27602
27832
|
});
|
|
27603
|
-
const
|
|
27604
|
-
|
|
27605
|
-
|
|
27606
|
-
|
|
27607
|
-
|
|
27608
|
-
|
|
27609
|
-
|
|
27610
|
-
|
|
27611
|
-
|
|
27612
|
-
remoteChange: {
|
|
27613
|
-
pageId: note.pageId,
|
|
27614
|
-
type: "modified",
|
|
27615
|
-
lastEdited: note.page.last_edited_time,
|
|
27616
|
-
previousEdited: null,
|
|
27617
|
-
path: filePath,
|
|
27618
|
-
title: note.title
|
|
27619
|
-
},
|
|
27620
|
-
baseContent: null,
|
|
27621
|
-
localContent: local,
|
|
27622
|
-
remoteContent: rendered.content
|
|
27623
|
-
};
|
|
27833
|
+
const conflict = relinkConflict({
|
|
27834
|
+
record: this.stateDb.getByPath(filePath),
|
|
27835
|
+
path: filePath,
|
|
27836
|
+
local,
|
|
27837
|
+
remote: rendered.content,
|
|
27838
|
+
pageId: note.pageId,
|
|
27839
|
+
lastEdited: note.page.last_edited_time,
|
|
27840
|
+
title: note.title
|
|
27841
|
+
});
|
|
27624
27842
|
return { path: filePath, relinked: true, conflict };
|
|
27625
27843
|
})();
|
|
27626
27844
|
}
|
|
@@ -28410,7 +28628,7 @@ function resolvesNow(stateDb, record, marker, pushed) {
|
|
|
28410
28628
|
|
|
28411
28629
|
// src/sync/pull-planner.ts
|
|
28412
28630
|
var PullPlanner = class {
|
|
28413
|
-
constructor(config, stateDb, vaultFs, databaseSyncer, drift, planner, puller) {
|
|
28631
|
+
constructor(config, stateDb, vaultFs, databaseSyncer, drift, planner, puller, discovery, detector) {
|
|
28414
28632
|
this.config = config;
|
|
28415
28633
|
this.stateDb = stateDb;
|
|
28416
28634
|
this.vaultFs = vaultFs;
|
|
@@ -28418,6 +28636,8 @@ var PullPlanner = class {
|
|
|
28418
28636
|
this.drift = drift;
|
|
28419
28637
|
this.planner = planner;
|
|
28420
28638
|
this.puller = puller;
|
|
28639
|
+
this.discovery = discovery;
|
|
28640
|
+
this.detector = detector;
|
|
28421
28641
|
}
|
|
28422
28642
|
config;
|
|
28423
28643
|
stateDb;
|
|
@@ -28426,11 +28646,13 @@ var PullPlanner = class {
|
|
|
28426
28646
|
drift;
|
|
28427
28647
|
planner;
|
|
28428
28648
|
puller;
|
|
28649
|
+
discovery;
|
|
28650
|
+
detector;
|
|
28429
28651
|
/**
|
|
28430
28652
|
* pull 할 것을 세기만 한다(dry-run) — 볼트 · 상태 DB · Notion 을 바꾸지 않는다. 페이지와 DB 행을
|
|
28431
|
-
* 같이 센다. DB 는 설정한 DB 와 이미 발견해 둔 DB 가운데 실제 pull 이 조회할 것(`ledger`)
|
|
28432
|
-
*
|
|
28433
|
-
*
|
|
28653
|
+
* 같이 센다. DB 는 설정한 DB 와 이미 발견해 둔 DB 가운데 실제 pull 이 조회할 것(`ledger`)과, 이번 pull 이 새로 찾을
|
|
28654
|
+
* DB(루트 · 미리 보며 읽은 본문 안)를 센다. 세지 못한 DB 는 이유와 함께 `failed` 에 싣는다(세지 못한 것을 없다고 하지
|
|
28655
|
+
* 않는다).
|
|
28434
28656
|
*/
|
|
28435
28657
|
async planPull(filtered, restoreChanges, restoreIds, localPlan, options, startTime, scan, ledger) {
|
|
28436
28658
|
const unchangedDropped = await this.drift.withoutUnchangedRemotes(filtered, options.signal);
|
|
@@ -28456,20 +28678,32 @@ var PullPlanner = class {
|
|
|
28456
28678
|
planned.push(change);
|
|
28457
28679
|
}
|
|
28458
28680
|
const relinks = await this.plannedRelinks(planned);
|
|
28681
|
+
for (const relink of relinks.byPage.values()) {
|
|
28682
|
+
if (relink.conflict) conflicts.push(relink.conflict);
|
|
28683
|
+
}
|
|
28459
28684
|
const queued = new Set(
|
|
28460
28685
|
[...filtered, ...restoreChanges].map((change) => compactNotionId(change.pageId))
|
|
28461
28686
|
);
|
|
28462
28687
|
const movingInVault = new Set(localPlan?.scan.adoptions.map((adoption) => adoption.record.id));
|
|
28688
|
+
const relinkPaths = new Map(
|
|
28689
|
+
[...relinks.byPage].map(([pageId, relink]) => [compactNotionId(pageId), relink.path])
|
|
28690
|
+
);
|
|
28691
|
+
const newDatabases = await this.discovery.plannedNewDatabases(failed, {
|
|
28692
|
+
rescanRoot: this.detector.rootChanged(),
|
|
28693
|
+
notePathOf: (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? null
|
|
28694
|
+
});
|
|
28463
28695
|
const rows = [];
|
|
28464
28696
|
for (const row of await this.planDatabaseRows(
|
|
28465
28697
|
options.paths,
|
|
28466
28698
|
failed,
|
|
28467
28699
|
ledger,
|
|
28468
28700
|
movingInVault,
|
|
28469
|
-
relinks.taken
|
|
28701
|
+
relinks.taken,
|
|
28702
|
+
newDatabases
|
|
28470
28703
|
)) {
|
|
28471
28704
|
if (queued.has(compactNotionId(row.pageId))) continue;
|
|
28472
28705
|
if (row.resolution?.action === "conflict") conflicts.push(row.resolution.conflict);
|
|
28706
|
+
if (row.relinkConflict) conflicts.push(row.relinkConflict);
|
|
28473
28707
|
else if (row.resolution?.action === "merge") mergedPaths.push(row.path);
|
|
28474
28708
|
if (row.resolution?.action === "conflict" || row.resolution?.action === "skip") continue;
|
|
28475
28709
|
rows.push(row);
|
|
@@ -28511,6 +28745,7 @@ var PullPlanner = class {
|
|
|
28511
28745
|
const rowCount = (operation, restore = false) => rows.filter(
|
|
28512
28746
|
(row) => row.operation === operation && (row.restore ?? false) === restore && !row.relink
|
|
28513
28747
|
).length;
|
|
28748
|
+
const undiscoveredDatabases = !isDatabaseMode(this.config) && planned.some((c) => c.type === "created" && !this.discovery.bodyRead(c.pageId));
|
|
28514
28749
|
const relinkedPaths = [
|
|
28515
28750
|
...[...relinks.byPage.values()].filter((relink) => relink.kind === "note").map((relink) => relink.path),
|
|
28516
28751
|
...rows.filter((row) => row.relink).map((row) => row.path)
|
|
@@ -28527,6 +28762,7 @@ var PullPlanner = class {
|
|
|
28527
28762
|
conflicts,
|
|
28528
28763
|
...mergedPaths.length > 0 ? { mergedPaths } : {},
|
|
28529
28764
|
...relinkedPaths.length > 0 ? { relinkedPaths, relinkPending: true } : {},
|
|
28765
|
+
...undiscoveredDatabases ? { undiscoveredDatabases: true } : {},
|
|
28530
28766
|
writtenPaths: [],
|
|
28531
28767
|
failed,
|
|
28532
28768
|
duration: Date.now() - startTime,
|
|
@@ -28612,11 +28848,12 @@ var PullPlanner = class {
|
|
|
28612
28848
|
* (`DatabaseDiscovery.pullDiscoveredDatabases`)를 같은 규칙으로 고른다. 세지 못한 DB 는 이유와
|
|
28613
28849
|
* 함께 `failed` 에 싣는다 — 세지 못한 것을 없다고 하지 않는다.
|
|
28614
28850
|
*/
|
|
28615
|
-
async planDatabaseRows(paths, failed, ledger, movingInVault, relinkTaken) {
|
|
28851
|
+
async planDatabaseRows(paths, failed, ledger, movingInVault, relinkTaken, databases) {
|
|
28616
28852
|
const configured = this.config.notion.databases ?? [];
|
|
28617
28853
|
const rows = [];
|
|
28854
|
+
const found2 = new Set(databases.found);
|
|
28618
28855
|
const plan = async (dbConfig, resolveDbFolder2) => {
|
|
28619
|
-
if (!ledger.selects(dbConfig.databaseId)) {
|
|
28856
|
+
if (!found2.has(dbConfig) && !ledger.selects(dbConfig.databaseId)) {
|
|
28620
28857
|
ledger.skip();
|
|
28621
28858
|
return;
|
|
28622
28859
|
}
|
|
@@ -28641,17 +28878,9 @@ var PullPlanner = class {
|
|
|
28641
28878
|
};
|
|
28642
28879
|
for (const dbConfig of configured) await plan(dbConfig);
|
|
28643
28880
|
if (isDatabaseMode(this.config)) return rows;
|
|
28644
|
-
const discovered =
|
|
28645
|
-
repairDbFolderCollisions(
|
|
28646
|
-
discovered,
|
|
28647
|
-
new Map(configured.map((d) => [d.localFolder, d.databaseId]))
|
|
28648
|
-
);
|
|
28649
|
-
const inaccessible = loadInaccessibleDbIds(this.stateDb);
|
|
28881
|
+
const discovered = databases.all;
|
|
28650
28882
|
const resolveDbFolder = (dbId) => [...discovered, ...configured].find((c) => notionIdsEqual(c.databaseId, dbId))?.localFolder ?? null;
|
|
28651
|
-
for (const dbConfig of discovered)
|
|
28652
|
-
if (inaccessible.has(dbConfig.databaseId.replace(/-/g, ""))) continue;
|
|
28653
|
-
await plan(dbConfig, resolveDbFolder);
|
|
28654
|
-
}
|
|
28883
|
+
for (const dbConfig of discovered) await plan(dbConfig, resolveDbFolder);
|
|
28655
28884
|
return rows;
|
|
28656
28885
|
}
|
|
28657
28886
|
};
|
|
@@ -29536,7 +29765,9 @@ var SyncOrchestrator = class {
|
|
|
29536
29765
|
this.databaseSyncer,
|
|
29537
29766
|
this.drift,
|
|
29538
29767
|
this.planner,
|
|
29539
|
-
this.puller
|
|
29768
|
+
this.puller,
|
|
29769
|
+
this.discovery,
|
|
29770
|
+
this.detector
|
|
29540
29771
|
);
|
|
29541
29772
|
this.pusher = new PagePusher(
|
|
29542
29773
|
config,
|