@im-nobsidian/core 0.1.7 → 0.1.8
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 +33 -2
- package/dist/index.js +475 -198
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, mkdir, writeFile, unlink, rename, stat, readdir } from 'fs/pr
|
|
|
3
3
|
import { join, dirname, relative } from 'path';
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import { randomUUID, createHash } from 'crypto';
|
|
6
|
-
import { Client } from '@notionhq/client';
|
|
6
|
+
import { Client, LogLevel } from '@notionhq/client';
|
|
7
7
|
import { Sema } from 'async-sema';
|
|
8
8
|
import matter7 from 'gray-matter';
|
|
9
9
|
import { markdownToBlocks } from '@tryfabric/martian';
|
|
@@ -290,6 +290,14 @@ CREATE INDEX IF NOT EXISTS idx_file_registry_page ON file_registry(notion_page_i
|
|
|
290
290
|
UPDATE sync_metadata SET value = '2' WHERE key = 'schema_version';
|
|
291
291
|
`;
|
|
292
292
|
|
|
293
|
+
// src/state/migrations/003-stat-cache.ts
|
|
294
|
+
var STAT_CACHE_MIGRATION = `
|
|
295
|
+
ALTER TABLE sync_state ADD COLUMN local_mtime TEXT;
|
|
296
|
+
ALTER TABLE sync_state ADD COLUMN local_file_size INTEGER;
|
|
297
|
+
|
|
298
|
+
UPDATE sync_metadata SET value = '3' WHERE key = 'schema_version';
|
|
299
|
+
`;
|
|
300
|
+
|
|
293
301
|
// src/state/state-db.ts
|
|
294
302
|
var StateDB = class _StateDB {
|
|
295
303
|
db;
|
|
@@ -312,6 +320,9 @@ var StateDB = class _StateDB {
|
|
|
312
320
|
if (version < 2) {
|
|
313
321
|
this.db.exec(FILE_REGISTRY_MIGRATION);
|
|
314
322
|
}
|
|
323
|
+
if (version < 3) {
|
|
324
|
+
this.db.exec(STAT_CACHE_MIGRATION);
|
|
325
|
+
}
|
|
315
326
|
}
|
|
316
327
|
getSchemaVersion() {
|
|
317
328
|
try {
|
|
@@ -349,8 +360,8 @@ var StateDB = class _StateDB {
|
|
|
349
360
|
notion_page_id = ?, notion_parent_id = ?, content_hash = ?,
|
|
350
361
|
notion_last_edited = ?, local_last_modified = ?,
|
|
351
362
|
sync_direction = ?, file_type = ?, status = ?,
|
|
352
|
-
base_snapshot = ?,
|
|
353
|
-
updated_at = datetime('now')
|
|
363
|
+
base_snapshot = ?, local_mtime = ?, local_file_size = ?,
|
|
364
|
+
version = version + 1, updated_at = datetime('now')
|
|
354
365
|
WHERE id = ?`
|
|
355
366
|
).run(
|
|
356
367
|
record.notionPageId ?? null,
|
|
@@ -362,6 +373,8 @@ var StateDB = class _StateDB {
|
|
|
362
373
|
record.fileType,
|
|
363
374
|
record.status,
|
|
364
375
|
record.baseSnapshot ?? null,
|
|
376
|
+
record.localMtime ?? null,
|
|
377
|
+
record.localFileSize ?? null,
|
|
365
378
|
existing.id
|
|
366
379
|
);
|
|
367
380
|
return this.getByPath(record.obsidianPath);
|
|
@@ -371,8 +384,8 @@ var StateDB = class _StateDB {
|
|
|
371
384
|
`INSERT INTO sync_state
|
|
372
385
|
(id, obsidian_path, notion_page_id, notion_parent_id, content_hash,
|
|
373
386
|
notion_last_edited, local_last_modified, sync_direction, file_type,
|
|
374
|
-
status, base_snapshot, version)
|
|
375
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
|
|
387
|
+
status, base_snapshot, local_mtime, local_file_size, version)
|
|
388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
|
|
376
389
|
).run(
|
|
377
390
|
id,
|
|
378
391
|
record.obsidianPath,
|
|
@@ -384,7 +397,9 @@ var StateDB = class _StateDB {
|
|
|
384
397
|
record.syncDirection,
|
|
385
398
|
record.fileType,
|
|
386
399
|
record.status,
|
|
387
|
-
record.baseSnapshot ?? null
|
|
400
|
+
record.baseSnapshot ?? null,
|
|
401
|
+
record.localMtime ?? null,
|
|
402
|
+
record.localFileSize ?? null
|
|
388
403
|
);
|
|
389
404
|
return this.getByPath(record.obsidianPath);
|
|
390
405
|
}
|
|
@@ -407,6 +422,11 @@ var StateDB = class _StateDB {
|
|
|
407
422
|
"UPDATE sync_state SET notion_last_edited = ?, updated_at = datetime('now') WHERE id = ?"
|
|
408
423
|
).run(lastEdited, id);
|
|
409
424
|
}
|
|
425
|
+
updateStatCache(id, mtime, fileSize) {
|
|
426
|
+
this.db.prepare(
|
|
427
|
+
"UPDATE sync_state SET local_mtime = ?, local_file_size = ?, updated_at = datetime('now') WHERE id = ?"
|
|
428
|
+
).run(mtime, fileSize, id);
|
|
429
|
+
}
|
|
410
430
|
setNotionParentId(id, parentId) {
|
|
411
431
|
this.db.prepare(
|
|
412
432
|
"UPDATE sync_state SET notion_parent_id = ?, updated_at = datetime('now') WHERE id = ?"
|
|
@@ -520,6 +540,8 @@ var StateDB = class _StateDB {
|
|
|
520
540
|
fileType: row.file_type,
|
|
521
541
|
status: row.status,
|
|
522
542
|
baseSnapshot: row.base_snapshot,
|
|
543
|
+
localMtime: row.local_mtime,
|
|
544
|
+
localFileSize: row.local_file_size,
|
|
523
545
|
version: row.version,
|
|
524
546
|
createdAt: row.created_at,
|
|
525
547
|
updatedAt: row.updated_at
|
|
@@ -791,7 +813,8 @@ var NotionClient = class _NotionClient {
|
|
|
791
813
|
constructor(options) {
|
|
792
814
|
this.client = new Client({
|
|
793
815
|
auth: options.token,
|
|
794
|
-
timeoutMs: options.timeoutMs ?? 3e4
|
|
816
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
817
|
+
logLevel: LogLevel.ERROR
|
|
795
818
|
});
|
|
796
819
|
this.token = options.token;
|
|
797
820
|
this.sema = new Sema(options.concurrency ?? 3);
|
|
@@ -1072,6 +1095,30 @@ var NotionClient = class _NotionClient {
|
|
|
1072
1095
|
nextCursor: response.next_cursor
|
|
1073
1096
|
};
|
|
1074
1097
|
}
|
|
1098
|
+
async searchRecentPages(since) {
|
|
1099
|
+
const results = [];
|
|
1100
|
+
let cursor;
|
|
1101
|
+
const sinceDate = new Date(since);
|
|
1102
|
+
outer: do {
|
|
1103
|
+
const response = await this.withRateLimit(
|
|
1104
|
+
() => this.client.search({
|
|
1105
|
+
filter: { property: "object", value: "page" },
|
|
1106
|
+
sort: { direction: "descending", timestamp: "last_edited_time" },
|
|
1107
|
+
start_cursor: cursor,
|
|
1108
|
+
page_size: 100
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
for (const page of response.results) {
|
|
1112
|
+
const p = page;
|
|
1113
|
+
if (new Date(p.last_edited_time) <= sinceDate) {
|
|
1114
|
+
break outer;
|
|
1115
|
+
}
|
|
1116
|
+
results.push({ id: p.id, last_edited_time: p.last_edited_time });
|
|
1117
|
+
}
|
|
1118
|
+
cursor = response.next_cursor ?? void 0;
|
|
1119
|
+
} while (cursor);
|
|
1120
|
+
return results;
|
|
1121
|
+
}
|
|
1075
1122
|
async getChildPages(parentId) {
|
|
1076
1123
|
const blocks = await this.fetchAllChildrenDeep(parentId);
|
|
1077
1124
|
const childPageBlocks = blocks.filter((b) => b.type === "child_page");
|
|
@@ -1080,8 +1127,8 @@ var NotionClient = class _NotionClient {
|
|
|
1080
1127
|
return pages;
|
|
1081
1128
|
}
|
|
1082
1129
|
async getChildDatabaseIds(parentId) {
|
|
1083
|
-
const
|
|
1084
|
-
return
|
|
1130
|
+
const children = await this.fetchAllChildren(parentId);
|
|
1131
|
+
return children.filter((b) => b.type === "child_database").map((b) => b.id);
|
|
1085
1132
|
}
|
|
1086
1133
|
async getChildPagesRecursive(parentId) {
|
|
1087
1134
|
const all = [];
|
|
@@ -1629,6 +1676,52 @@ var ChangeDetector = class {
|
|
|
1629
1676
|
this.stateDb = stateDb;
|
|
1630
1677
|
}
|
|
1631
1678
|
stateDb;
|
|
1679
|
+
async detectLocalChangesFast(stats, readFile3) {
|
|
1680
|
+
const changes = [];
|
|
1681
|
+
const existingPaths = /* @__PURE__ */ new Set();
|
|
1682
|
+
for (const file of stats) {
|
|
1683
|
+
existingPaths.add(file.path);
|
|
1684
|
+
const record = this.stateDb.getByPath(file.path);
|
|
1685
|
+
if (!record) {
|
|
1686
|
+
const content2 = await readFile3(file.path);
|
|
1687
|
+
changes.push({
|
|
1688
|
+
path: file.path,
|
|
1689
|
+
type: "created",
|
|
1690
|
+
currentHash: computeHash(content2),
|
|
1691
|
+
previousHash: null
|
|
1692
|
+
});
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (record.localMtime === file.mtime && record.localFileSize === file.size) {
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const content = await readFile3(file.path);
|
|
1699
|
+
const currentHash = computeHash(content);
|
|
1700
|
+
if (record.contentHash !== currentHash) {
|
|
1701
|
+
changes.push({
|
|
1702
|
+
path: file.path,
|
|
1703
|
+
type: "modified",
|
|
1704
|
+
currentHash,
|
|
1705
|
+
previousHash: record.contentHash
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
const syncedRecords = this.stateDb.getByStatus("synced");
|
|
1710
|
+
for (const record of syncedRecords) {
|
|
1711
|
+
if (!existingPaths.has(record.obsidianPath)) {
|
|
1712
|
+
if (record.fileType === "folder-note" || record.fileType === "folder-only") {
|
|
1713
|
+
if (!record.obsidianPath.endsWith(".md")) continue;
|
|
1714
|
+
}
|
|
1715
|
+
changes.push({
|
|
1716
|
+
path: record.obsidianPath,
|
|
1717
|
+
type: "deleted",
|
|
1718
|
+
currentHash: "",
|
|
1719
|
+
previousHash: record.contentHash
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return this.detectMoves(changes);
|
|
1724
|
+
}
|
|
1632
1725
|
detectLocalChanges(currentFiles) {
|
|
1633
1726
|
const changes = [];
|
|
1634
1727
|
const existingPaths = /* @__PURE__ */ new Set();
|
|
@@ -1644,7 +1737,7 @@ var ChangeDetector = class {
|
|
|
1644
1737
|
});
|
|
1645
1738
|
continue;
|
|
1646
1739
|
}
|
|
1647
|
-
if (record.
|
|
1740
|
+
if (record.localMtime && record.localMtime === file.mtime) {
|
|
1648
1741
|
continue;
|
|
1649
1742
|
}
|
|
1650
1743
|
const currentHash = computeHash(file.content);
|
|
@@ -3215,6 +3308,7 @@ var ImageHandler = class {
|
|
|
3215
3308
|
const url = match[2];
|
|
3216
3309
|
const caption = match[1]?.trim() || "file";
|
|
3217
3310
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
3311
|
+
if (!download.localPath) continue;
|
|
3218
3312
|
downloads.push(download);
|
|
3219
3313
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3220
3314
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3236,6 +3330,7 @@ var ImageHandler = class {
|
|
|
3236
3330
|
if (!realUrl) continue;
|
|
3237
3331
|
const caption = fileName || match[1]?.trim() || "file";
|
|
3238
3332
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3333
|
+
if (!download.localPath) continue;
|
|
3239
3334
|
downloads.push(download);
|
|
3240
3335
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3241
3336
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3251,6 +3346,7 @@ var ImageHandler = class {
|
|
|
3251
3346
|
const url = match[1];
|
|
3252
3347
|
const caption = match[2]?.trim() || "file";
|
|
3253
3348
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
3349
|
+
if (!download.localPath) continue;
|
|
3254
3350
|
downloads.push(download);
|
|
3255
3351
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3256
3352
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3272,6 +3368,7 @@ var ImageHandler = class {
|
|
|
3272
3368
|
if (!realUrl) continue;
|
|
3273
3369
|
const caption = fileName || match[2]?.trim() || "file";
|
|
3274
3370
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3371
|
+
if (!download.localPath) continue;
|
|
3275
3372
|
downloads.push(download);
|
|
3276
3373
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3277
3374
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3311,6 +3408,13 @@ var ImageHandler = class {
|
|
|
3311
3408
|
if (!response.ok) {
|
|
3312
3409
|
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3313
3410
|
}
|
|
3411
|
+
const contentLength = response.headers.get("content-length");
|
|
3412
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
3413
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
3414
|
+
const sizeMB = Math.round(parseInt(contentLength, 10) / 1024 / 1024);
|
|
3415
|
+
getLogger().debug(`[Im-Nobsidian] \uD30C\uC77C \uC2A4\uD0B5 \u2014 \uB108\uBB34 \uD07C (${sizeMB}MB): ${caption}`);
|
|
3416
|
+
return { originalUrl: url, localPath: "", hash: "", size: 0 };
|
|
3417
|
+
}
|
|
3314
3418
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3315
3419
|
const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
|
|
3316
3420
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
@@ -3469,13 +3573,12 @@ var FileHandler = class {
|
|
|
3469
3573
|
if (folderFiles.length === 0) return [];
|
|
3470
3574
|
const results = [];
|
|
3471
3575
|
for (const file of folderFiles) {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
if (existing)
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
}
|
|
3576
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3577
|
+
if (existing) {
|
|
3578
|
+
if (existing.fileSize === file.size) continue;
|
|
3579
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3580
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3581
|
+
if (existing.fileHash === currentHash) continue;
|
|
3479
3582
|
}
|
|
3480
3583
|
await this.sema.acquire();
|
|
3481
3584
|
try {
|
|
@@ -3504,19 +3607,20 @@ var FileHandler = class {
|
|
|
3504
3607
|
const folderRecord = folderPath ? this.stateDb.getByPath(folderPath) : this.stateDb.getByPath("__root__");
|
|
3505
3608
|
const folderPageId = folderRecord?.notionPageId;
|
|
3506
3609
|
if (!folderPageId) {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3610
|
+
if (files.length > 0) {
|
|
3611
|
+
getLogger().debug(
|
|
3612
|
+
`\uD3F4\uB354 \uD398\uC774\uC9C0 \uC5C6\uC74C, \uD30C\uC77C \uC2A4\uD0B5: ${folderPath || "(\uB8E8\uD2B8)"} (${files.length}\uAC1C)`
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3510
3615
|
continue;
|
|
3511
3616
|
}
|
|
3512
3617
|
for (const file of files) {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
if (existing)
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
}
|
|
3618
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3619
|
+
if (existing) {
|
|
3620
|
+
if (existing.fileSize === file.size) continue;
|
|
3621
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3622
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3623
|
+
if (existing.fileHash === currentHash) continue;
|
|
3520
3624
|
}
|
|
3521
3625
|
await this.sema.acquire();
|
|
3522
3626
|
try {
|
|
@@ -4209,13 +4313,21 @@ var DatabaseSyncer = class {
|
|
|
4209
4313
|
});
|
|
4210
4314
|
}
|
|
4211
4315
|
}
|
|
4212
|
-
getLogger().
|
|
4316
|
+
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4213
4317
|
return { created, updated, failed };
|
|
4214
4318
|
}
|
|
4215
|
-
async pullDatabaseViews(dbConfig) {
|
|
4319
|
+
async pullDatabaseViews(dbConfig, force = false) {
|
|
4216
4320
|
try {
|
|
4217
|
-
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4218
4321
|
const configPath = ".im-nobsidian/db-views.json";
|
|
4322
|
+
if (!force) {
|
|
4323
|
+
try {
|
|
4324
|
+
const existing = await this.vaultFs.readFile(configPath);
|
|
4325
|
+
const parsed = JSON.parse(existing);
|
|
4326
|
+
if (parsed[dbConfig.databaseId]) return;
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4219
4331
|
await this.vaultFs.ensureFolder(".im-nobsidian");
|
|
4220
4332
|
let allViewsConfigs = {};
|
|
4221
4333
|
try {
|
|
@@ -4225,7 +4337,7 @@ var DatabaseSyncer = class {
|
|
|
4225
4337
|
}
|
|
4226
4338
|
allViewsConfigs[dbConfig.databaseId] = viewsConfig;
|
|
4227
4339
|
await this.vaultFs.writeFile(configPath, JSON.stringify(allViewsConfigs, null, 2));
|
|
4228
|
-
getLogger().
|
|
4340
|
+
getLogger().debug(
|
|
4229
4341
|
`[DB Sync] \uBDF0 \uC124\uC815 ${viewsConfig.views.length}\uAC1C \uC800\uC7A5: ${dbConfig.databaseId}`
|
|
4230
4342
|
);
|
|
4231
4343
|
} catch (error) {
|
|
@@ -4267,15 +4379,32 @@ var DatabaseSyncer = class {
|
|
|
4267
4379
|
} catch {
|
|
4268
4380
|
}
|
|
4269
4381
|
if (this.config.conversion.imageDownload === "immediate" && markdown) {
|
|
4270
|
-
|
|
4271
|
-
|
|
4382
|
+
try {
|
|
4383
|
+
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
4384
|
+
markdown = imageResult.content;
|
|
4385
|
+
} catch (error) {
|
|
4386
|
+
getLogger().warn(`[DB Sync] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${title}):`, error);
|
|
4387
|
+
}
|
|
4272
4388
|
}
|
|
4273
4389
|
if (markdown) {
|
|
4274
|
-
|
|
4275
|
-
|
|
4390
|
+
try {
|
|
4391
|
+
const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
|
|
4392
|
+
markdown = fileResult.content;
|
|
4393
|
+
} catch (error) {
|
|
4394
|
+
getLogger().warn(`[DB Sync] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${title}):`, error);
|
|
4395
|
+
}
|
|
4276
4396
|
}
|
|
4277
4397
|
const existingRecord = this.stateDb.getByNotionId(page.id);
|
|
4278
|
-
|
|
4398
|
+
let filePath;
|
|
4399
|
+
if (existingRecord?.obsidianPath) {
|
|
4400
|
+
filePath = existingRecord.obsidianPath;
|
|
4401
|
+
} else {
|
|
4402
|
+
filePath = `${dbConfig.localFolder}/${safeName}.md`;
|
|
4403
|
+
const existingByPath = this.stateDb.getByPath(filePath);
|
|
4404
|
+
if (existingByPath && existingByPath.notionPageId !== page.id) {
|
|
4405
|
+
filePath = `${dbConfig.localFolder}/${safeName} (${page.id.slice(0, 8)}).md`;
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4279
4408
|
const finalContent = this.pipeline.convertToMarkdown(
|
|
4280
4409
|
markdown,
|
|
4281
4410
|
{
|
|
@@ -4384,7 +4513,7 @@ var DatabaseSyncer = class {
|
|
|
4384
4513
|
});
|
|
4385
4514
|
}
|
|
4386
4515
|
}
|
|
4387
|
-
getLogger().
|
|
4516
|
+
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4388
4517
|
return { created, updated, failed };
|
|
4389
4518
|
}
|
|
4390
4519
|
};
|
|
@@ -4451,15 +4580,20 @@ var SyncOrchestrator = class {
|
|
|
4451
4580
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
4452
4581
|
}
|
|
4453
4582
|
this.cleanupInterruptedSync();
|
|
4454
|
-
|
|
4455
|
-
const
|
|
4583
|
+
this.repairFolderRecords();
|
|
4584
|
+
const stats = await this.vaultFs.listMarkdownFileStats();
|
|
4585
|
+
const changes = await this.changeDetector.detectLocalChangesFast(
|
|
4586
|
+
stats,
|
|
4587
|
+
(path) => this.vaultFs.readFile(path)
|
|
4588
|
+
);
|
|
4456
4589
|
const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
|
|
4457
4590
|
const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
|
|
4458
4591
|
const dbFolders = (this.config.notion.databases ?? []).map(
|
|
4459
4592
|
(d) => d.localFolder.endsWith("/") ? d.localFolder : d.localFolder + "/"
|
|
4460
4593
|
);
|
|
4461
4594
|
const isDbPath = (path) => dbFolders.some((prefix) => path.startsWith(prefix));
|
|
4462
|
-
const
|
|
4595
|
+
const excludeSet = options?.excludePaths ? new Set(options.excludePaths) : null;
|
|
4596
|
+
const filtered = (options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible).filter((c) => !isDbPath(c.path) && (!excludeSet || !excludeSet.has(c.path)));
|
|
4463
4597
|
const hasDbConfigs = (this.config.notion.databases?.length ?? 0) > 0;
|
|
4464
4598
|
if (filtered.length === 0 && !hasDbConfigs) {
|
|
4465
4599
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
@@ -4630,11 +4764,37 @@ var SyncOrchestrator = class {
|
|
|
4630
4764
|
const conflicts = [];
|
|
4631
4765
|
const writtenPaths = [];
|
|
4632
4766
|
const failed = [];
|
|
4633
|
-
const
|
|
4767
|
+
const lastPull = this.stateDb.getMeta("last_pull_at") ?? this.stateDb.getMeta("last_sync_at");
|
|
4768
|
+
const trackedCount = this.stateDb.getAll().length;
|
|
4769
|
+
const remoteChanges = !this.isDatabaseMode && lastPull && trackedCount > 0 && !options?.force ? await this.detectRemoteChangesIncremental(lastPull) : await this.detectRemoteChanges();
|
|
4634
4770
|
const filtered = options?.paths ? remoteChanges.filter((c) => {
|
|
4635
4771
|
const record = this.stateDb.getByNotionId(c.pageId);
|
|
4636
4772
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
4637
4773
|
}) : remoteChanges;
|
|
4774
|
+
if (filtered.length === 0 && (this.config.notion.databases?.length ?? 0) === 0) {
|
|
4775
|
+
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
4776
|
+
if (!hasDbResults.created && !hasDbResults.updated) {
|
|
4777
|
+
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4778
|
+
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4779
|
+
this.stateDb.setMeta("pull_in_progress", "");
|
|
4780
|
+
return emptyResult;
|
|
4781
|
+
}
|
|
4782
|
+
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4783
|
+
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4784
|
+
this.stateDb.setMeta("pull_in_progress", "");
|
|
4785
|
+
return {
|
|
4786
|
+
created: hasDbResults.created,
|
|
4787
|
+
updated: hasDbResults.updated,
|
|
4788
|
+
deleted: 0,
|
|
4789
|
+
conflicts: [],
|
|
4790
|
+
writtenPaths,
|
|
4791
|
+
failed,
|
|
4792
|
+
duration: Date.now() - startTime,
|
|
4793
|
+
imageCount: 0,
|
|
4794
|
+
fileCount: 0,
|
|
4795
|
+
linkCount: 0
|
|
4796
|
+
};
|
|
4797
|
+
}
|
|
4638
4798
|
if (filtered.length === 0) {
|
|
4639
4799
|
return emptyResult;
|
|
4640
4800
|
}
|
|
@@ -4670,15 +4830,13 @@ var SyncOrchestrator = class {
|
|
|
4670
4830
|
const tasks = filtered.map((change) => async () => {
|
|
4671
4831
|
await sema.acquire();
|
|
4672
4832
|
try {
|
|
4673
|
-
|
|
4674
|
-
const displayPath = record?.obsidianPath ?? change.pageId;
|
|
4675
|
-
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4676
|
-
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
4833
|
+
let resultPath;
|
|
4677
4834
|
switch (change.type) {
|
|
4678
4835
|
case "created": {
|
|
4679
4836
|
const path = await this.pullCreate(change.pageId);
|
|
4680
4837
|
writtenPaths.push(path);
|
|
4681
4838
|
created++;
|
|
4839
|
+
resultPath = path;
|
|
4682
4840
|
break;
|
|
4683
4841
|
}
|
|
4684
4842
|
case "modified": {
|
|
@@ -4689,16 +4847,24 @@ var SyncOrchestrator = class {
|
|
|
4689
4847
|
writtenPaths.push(result.path);
|
|
4690
4848
|
updated++;
|
|
4691
4849
|
}
|
|
4850
|
+
resultPath = result.path;
|
|
4692
4851
|
break;
|
|
4693
4852
|
}
|
|
4694
4853
|
case "deleted": {
|
|
4854
|
+
const record2 = this.stateDb.getByNotionId(change.pageId);
|
|
4855
|
+
resultPath = record2?.obsidianPath;
|
|
4695
4856
|
const path = await this.pullDelete(change.pageId);
|
|
4696
4857
|
if (path) {
|
|
4697
4858
|
deleted++;
|
|
4859
|
+
resultPath = path;
|
|
4698
4860
|
}
|
|
4699
4861
|
break;
|
|
4700
4862
|
}
|
|
4701
4863
|
}
|
|
4864
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4865
|
+
const displayPath = resultPath ?? record?.obsidianPath ?? change.pageId;
|
|
4866
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4867
|
+
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
4702
4868
|
} catch (error) {
|
|
4703
4869
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4704
4870
|
failed.push({
|
|
@@ -4773,44 +4939,13 @@ var SyncOrchestrator = class {
|
|
|
4773
4939
|
getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
|
|
4774
4940
|
}
|
|
4775
4941
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
4781
|
-
);
|
|
4782
|
-
const newDbs = discoveredDbs.filter((db) => !configuredIds.has(db.dbId.replace(/-/g, "")));
|
|
4783
|
-
for (const { dbId, parentPageId } of newDbs) {
|
|
4784
|
-
try {
|
|
4785
|
-
const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
|
|
4786
|
-
const safeName = (dbTitle || dbId.slice(0, 8)).replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
4787
|
-
const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
|
|
4788
|
-
let parentFolder = "databases";
|
|
4789
|
-
if (parentEntry?.obsidianPath) {
|
|
4790
|
-
const parts = parentEntry.obsidianPath.split("/");
|
|
4791
|
-
parts.pop();
|
|
4792
|
-
parentFolder = parts.length > 0 ? parts.join("/") : "databases";
|
|
4793
|
-
}
|
|
4794
|
-
const folderName = `${parentFolder}/${safeName}`;
|
|
4795
|
-
const dbConfig = {
|
|
4796
|
-
databaseId: dbId,
|
|
4797
|
-
localFolder: folderName,
|
|
4798
|
-
titleProperty: "Name"
|
|
4799
|
-
};
|
|
4800
|
-
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
4801
|
-
created += dbResult.created;
|
|
4802
|
-
updated += dbResult.updated;
|
|
4803
|
-
failed.push(...dbResult.failed);
|
|
4804
|
-
} catch (error) {
|
|
4805
|
-
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
4806
|
-
}
|
|
4807
|
-
}
|
|
4808
|
-
} catch (error) {
|
|
4809
|
-
getLogger().warn("[Im-Nobsidian] child_database \uC790\uB3D9 \uBC1C\uACAC \uC2E4\uD328:", error);
|
|
4810
|
-
}
|
|
4942
|
+
{
|
|
4943
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
4944
|
+
created += dbDiscovery.created;
|
|
4945
|
+
updated += dbDiscovery.updated;
|
|
4811
4946
|
}
|
|
4812
|
-
const
|
|
4813
|
-
const linkCount = await this.resolveNotionLinks(
|
|
4947
|
+
const linkTargetPaths = writtenPaths.length > 0 ? writtenPaths : [];
|
|
4948
|
+
const linkCount = linkTargetPaths.length > 0 ? await this.resolveNotionLinks(linkTargetPaths) : 0;
|
|
4814
4949
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4815
4950
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4816
4951
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
@@ -4832,7 +4967,8 @@ var SyncOrchestrator = class {
|
|
|
4832
4967
|
const pullResult = await this.pull(options);
|
|
4833
4968
|
const pushResult = await this.push({
|
|
4834
4969
|
...options,
|
|
4835
|
-
paths: options?.paths
|
|
4970
|
+
paths: options?.paths,
|
|
4971
|
+
excludePaths: pullResult.writtenPaths
|
|
4836
4972
|
});
|
|
4837
4973
|
return {
|
|
4838
4974
|
pull: pullResult,
|
|
@@ -4861,6 +4997,34 @@ var SyncOrchestrator = class {
|
|
|
4861
4997
|
lastSyncAt
|
|
4862
4998
|
};
|
|
4863
4999
|
}
|
|
5000
|
+
async statusLocal() {
|
|
5001
|
+
const stats = await this.vaultFs.listMarkdownFileStats();
|
|
5002
|
+
const localChanges = await this.changeDetector.detectLocalChangesFast(
|
|
5003
|
+
stats,
|
|
5004
|
+
(path) => this.vaultFs.readFile(path)
|
|
5005
|
+
);
|
|
5006
|
+
const conflictRecords = this.stateDb.getByStatus("conflict");
|
|
5007
|
+
const lastSyncAt = this.stateDb.getMeta("last_sync_at");
|
|
5008
|
+
return {
|
|
5009
|
+
localChanges,
|
|
5010
|
+
remoteChanges: [],
|
|
5011
|
+
conflicts: [],
|
|
5012
|
+
conflictRecords,
|
|
5013
|
+
pendingOperations: conflictRecords.length,
|
|
5014
|
+
lastSyncAt
|
|
5015
|
+
};
|
|
5016
|
+
}
|
|
5017
|
+
async fetch() {
|
|
5018
|
+
const startTime = Date.now();
|
|
5019
|
+
const changes = await this.detectRemoteChanges();
|
|
5020
|
+
this.stateDb.setMeta("last_fetch_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5021
|
+
return {
|
|
5022
|
+
newPages: changes.filter((c) => c.type === "created").length,
|
|
5023
|
+
modifiedPages: changes.filter((c) => c.type === "modified").length,
|
|
5024
|
+
deletedPages: changes.filter((c) => c.type === "deleted").length,
|
|
5025
|
+
duration: Date.now() - startTime
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
4864
5028
|
async buildConflictsFromRecords(records, localChanges, remoteChanges) {
|
|
4865
5029
|
const conflicts = [];
|
|
4866
5030
|
for (const record of records) {
|
|
@@ -4903,25 +5067,97 @@ var SyncOrchestrator = class {
|
|
|
4903
5067
|
return this.config.notion.parentMode === "database" && !!this.config.notion.databaseId;
|
|
4904
5068
|
}
|
|
4905
5069
|
async discoverChildDatabases() {
|
|
4906
|
-
const rootId = this.config.notion.rootPageId;
|
|
4907
5070
|
const allDbs = [];
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5071
|
+
const trackedPages = this.stateDb.getAll().filter((r) => r.notionPageId);
|
|
5072
|
+
const pageIds = [this.config.notion.rootPageId, ...trackedPages.map((r) => r.notionPageId)];
|
|
5073
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5074
|
+
for (const parentId of pageIds) {
|
|
5075
|
+
if (seen.has(parentId)) continue;
|
|
5076
|
+
seen.add(parentId);
|
|
5077
|
+
try {
|
|
5078
|
+
const dbIds = await this.notionClient.getChildDatabaseIds(parentId);
|
|
5079
|
+
for (const dbId of dbIds) {
|
|
5080
|
+
allDbs.push({ dbId, parentPageId: parentId });
|
|
5081
|
+
}
|
|
5082
|
+
} catch {
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
return allDbs;
|
|
5086
|
+
}
|
|
5087
|
+
async pullDiscoveredDatabases(writtenPaths, failed) {
|
|
5088
|
+
if (this.isDatabaseMode) return { created: 0, updated: 0 };
|
|
5089
|
+
let created = 0;
|
|
5090
|
+
let updated = 0;
|
|
5091
|
+
try {
|
|
5092
|
+
const cachedRaw = this.stateDb.getMeta("discovered_dbs");
|
|
5093
|
+
let dbConfigs = [];
|
|
5094
|
+
if (cachedRaw) {
|
|
4912
5095
|
try {
|
|
4913
|
-
|
|
4914
|
-
for (const dbId of dbIds) {
|
|
4915
|
-
allDbs.push({ dbId, parentPageId: parentId });
|
|
4916
|
-
}
|
|
4917
|
-
const childPages = await this.notionClient.getChildPages(parentId);
|
|
4918
|
-
nextLevel.push(...childPages.map((p) => p.id));
|
|
5096
|
+
dbConfigs = JSON.parse(cachedRaw);
|
|
4919
5097
|
} catch {
|
|
5098
|
+
dbConfigs = [];
|
|
4920
5099
|
}
|
|
4921
5100
|
}
|
|
4922
|
-
|
|
5101
|
+
if (dbConfigs.length === 0) {
|
|
5102
|
+
const discoveredDbs = await this.discoverChildDatabases();
|
|
5103
|
+
const configuredIds = new Set(
|
|
5104
|
+
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
5105
|
+
);
|
|
5106
|
+
const newDbs = discoveredDbs.filter((db) => !configuredIds.has(db.dbId.replace(/-/g, "")));
|
|
5107
|
+
for (const { dbId, parentPageId } of newDbs) {
|
|
5108
|
+
try {
|
|
5109
|
+
const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
|
|
5110
|
+
const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
|
|
5111
|
+
let parentFolder = "";
|
|
5112
|
+
if (parentEntry?.obsidianPath) {
|
|
5113
|
+
const obsPath = parentEntry.obsidianPath;
|
|
5114
|
+
if (obsPath.endsWith(".md")) {
|
|
5115
|
+
const parts = obsPath.split("/");
|
|
5116
|
+
parts.pop();
|
|
5117
|
+
parentFolder = parts.length > 0 ? parts.join("/") : obsPath.replace(/\.md$/, "");
|
|
5118
|
+
} else {
|
|
5119
|
+
parentFolder = obsPath;
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
let safeName;
|
|
5123
|
+
if (dbTitle) {
|
|
5124
|
+
safeName = dbTitle.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
5125
|
+
} else {
|
|
5126
|
+
const parentName = parentFolder.split("/").pop() || "";
|
|
5127
|
+
safeName = parentName ? `${parentName}-DB` : `db-${dbId.slice(0, 8)}`;
|
|
5128
|
+
}
|
|
5129
|
+
if (!parentFolder) parentFolder = "databases";
|
|
5130
|
+
dbConfigs.push({
|
|
5131
|
+
databaseId: dbId,
|
|
5132
|
+
localFolder: `${parentFolder}/${safeName}`,
|
|
5133
|
+
titleProperty: "Name"
|
|
5134
|
+
});
|
|
5135
|
+
} catch (error) {
|
|
5136
|
+
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5139
|
+
if (dbConfigs.length > 0) {
|
|
5140
|
+
this.stateDb.setMeta("discovered_dbs", JSON.stringify(dbConfigs));
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
for (const dbConfig of dbConfigs) {
|
|
5144
|
+
try {
|
|
5145
|
+
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
5146
|
+
created += dbResult.created;
|
|
5147
|
+
updated += dbResult.updated;
|
|
5148
|
+
failed.push(...dbResult.failed);
|
|
5149
|
+
if (dbResult.created + dbResult.updated > 0) {
|
|
5150
|
+
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
5151
|
+
writtenPaths.push(...dbPaths.slice(-dbResult.created - dbResult.updated));
|
|
5152
|
+
}
|
|
5153
|
+
} catch (error) {
|
|
5154
|
+
getLogger().warn(`[Im-Nobsidian] DB ${dbConfig.databaseId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
} catch (error) {
|
|
5158
|
+
getLogger().warn("[Im-Nobsidian] child_database \uC790\uB3D9 \uBC1C\uACAC \uC2E4\uD328:", error);
|
|
4923
5159
|
}
|
|
4924
|
-
return
|
|
5160
|
+
return { created, updated };
|
|
4925
5161
|
}
|
|
4926
5162
|
async resolveNotionLinks(paths) {
|
|
4927
5163
|
let totalResolved = 0;
|
|
@@ -5015,6 +5251,7 @@ var SyncOrchestrator = class {
|
|
|
5015
5251
|
);
|
|
5016
5252
|
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
5017
5253
|
const hash = computeHash(content);
|
|
5254
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
5018
5255
|
this.stateDb.transaction(() => {
|
|
5019
5256
|
this.stateDb.upsert({
|
|
5020
5257
|
obsidianPath: path,
|
|
@@ -5026,7 +5263,9 @@ var SyncOrchestrator = class {
|
|
|
5026
5263
|
syncDirection: "both",
|
|
5027
5264
|
fileType: this.isFolderNote(path) ? "folder-note" : "file",
|
|
5028
5265
|
status: "synced",
|
|
5029
|
-
baseSnapshot: Buffer.from(content, "utf-8")
|
|
5266
|
+
baseSnapshot: Buffer.from(content, "utf-8"),
|
|
5267
|
+
localMtime: fileStat?.mtime ?? null,
|
|
5268
|
+
localFileSize: fileStat?.size ?? null
|
|
5030
5269
|
});
|
|
5031
5270
|
const aliases = extractAliases2(conversionResult.properties);
|
|
5032
5271
|
this.stateDb.upsertWikilink({
|
|
@@ -5056,23 +5295,36 @@ var SyncOrchestrator = class {
|
|
|
5056
5295
|
});
|
|
5057
5296
|
await this.pushUpdatePage(record.notionPageId, conversionResult.content, record.baseSnapshot);
|
|
5058
5297
|
await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
|
|
5059
|
-
let propsToUpdate
|
|
5060
|
-
if (this.isDatabaseMode &&
|
|
5298
|
+
let propsToUpdate;
|
|
5299
|
+
if (this.isDatabaseMode && conversionResult.properties && Object.keys(conversionResult.properties).length > 0) {
|
|
5061
5300
|
await this.ensureDbSchema();
|
|
5062
5301
|
const title2 = extractTitle(path);
|
|
5063
|
-
propsToUpdate = this.propertyMapper.toNotionProperties(
|
|
5302
|
+
propsToUpdate = this.propertyMapper.toNotionProperties(conversionResult.properties, title2);
|
|
5303
|
+
} else if (conversionResult.properties?.title) {
|
|
5304
|
+
const titleStr = String(conversionResult.properties.title);
|
|
5305
|
+
propsToUpdate = {
|
|
5306
|
+
title: { title: [{ text: { content: titleStr } }] }
|
|
5307
|
+
};
|
|
5064
5308
|
}
|
|
5309
|
+
let lastEditedTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5065
5310
|
if (propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
5066
|
-
await this.notionClient.updatePageProperties(
|
|
5311
|
+
const updatedPage = await this.notionClient.updatePageProperties(
|
|
5312
|
+
record.notionPageId,
|
|
5313
|
+
propsToUpdate
|
|
5314
|
+
);
|
|
5315
|
+
lastEditedTime = updatedPage.last_edited_time;
|
|
5067
5316
|
}
|
|
5068
|
-
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
5069
5317
|
const hash = computeHash(content);
|
|
5318
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
5070
5319
|
const title = extractTitle(path);
|
|
5071
5320
|
const aliases = extractAliases2(conversionResult.properties);
|
|
5072
5321
|
this.stateDb.transaction(() => {
|
|
5073
5322
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
5074
5323
|
this.stateDb.updateStatus(record.id, "synced");
|
|
5075
|
-
this.stateDb.setNotionLastEdited(record.id,
|
|
5324
|
+
this.stateDb.setNotionLastEdited(record.id, lastEditedTime);
|
|
5325
|
+
if (fileStat) {
|
|
5326
|
+
this.stateDb.updateStatCache(record.id, fileStat.mtime, fileStat.size);
|
|
5327
|
+
}
|
|
5076
5328
|
this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
|
|
5077
5329
|
this.stateDb.upsertWikilink({
|
|
5078
5330
|
obsidianPath: path,
|
|
@@ -5166,13 +5418,49 @@ var SyncOrchestrator = class {
|
|
|
5166
5418
|
}
|
|
5167
5419
|
return changes;
|
|
5168
5420
|
}
|
|
5421
|
+
async detectRemoteChangesIncremental(since) {
|
|
5422
|
+
const changes = [];
|
|
5423
|
+
const recentPages = await this.notionClient.searchRecentPages(since);
|
|
5424
|
+
for (const page of recentPages) {
|
|
5425
|
+
const record = this.stateDb.getByNotionId(page.id);
|
|
5426
|
+
if (!record) {
|
|
5427
|
+
try {
|
|
5428
|
+
const fullPage = await this.notionClient.getPage(page.id);
|
|
5429
|
+
const parentId = await this.extractParentId(fullPage);
|
|
5430
|
+
if (parentId && this.isTrackedParent(parentId)) {
|
|
5431
|
+
changes.push({
|
|
5432
|
+
pageId: page.id,
|
|
5433
|
+
type: "created",
|
|
5434
|
+
lastEdited: page.last_edited_time,
|
|
5435
|
+
previousEdited: null
|
|
5436
|
+
});
|
|
5437
|
+
}
|
|
5438
|
+
} catch {
|
|
5439
|
+
}
|
|
5440
|
+
continue;
|
|
5441
|
+
}
|
|
5442
|
+
if (page.last_edited_time !== record.notionLastEdited) {
|
|
5443
|
+
changes.push({
|
|
5444
|
+
pageId: page.id,
|
|
5445
|
+
type: "modified",
|
|
5446
|
+
lastEdited: page.last_edited_time,
|
|
5447
|
+
previousEdited: record.notionLastEdited
|
|
5448
|
+
});
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
return changes;
|
|
5452
|
+
}
|
|
5453
|
+
isTrackedParent(parentId) {
|
|
5454
|
+
if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return true;
|
|
5455
|
+
return !!this.stateDb.getByNotionId(parentId);
|
|
5456
|
+
}
|
|
5169
5457
|
async pullCreate(pageId) {
|
|
5170
5458
|
const page = await this.notionClient.getPage(pageId);
|
|
5171
5459
|
const title = this.notionClient.extractTitle(page);
|
|
5172
5460
|
const safeName = sanitizeFileName(title);
|
|
5173
5461
|
const parentPath = await this.resolveParentPath(page);
|
|
5174
|
-
const
|
|
5175
|
-
const hasChildPages =
|
|
5462
|
+
const firstChildren = await this.notionClient.listChildren(pageId, { pageSize: 100 });
|
|
5463
|
+
const hasChildPages = firstChildren.results.some((b) => "type" in b && b.type === "child_page");
|
|
5176
5464
|
const markdown = await this.fetchPageMarkdown(pageId);
|
|
5177
5465
|
const hasContent = markdown.trim().length > 0;
|
|
5178
5466
|
let filePath;
|
|
@@ -5223,6 +5511,7 @@ var SyncOrchestrator = class {
|
|
|
5223
5511
|
);
|
|
5224
5512
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
5225
5513
|
const hash = computeHash(finalContent);
|
|
5514
|
+
const pullStat = await this.vaultFs.getFileStat(filePath);
|
|
5226
5515
|
const resolvedParentId = await this.extractParentId(page);
|
|
5227
5516
|
this.stateDb.transaction(() => {
|
|
5228
5517
|
this.stateDb.upsert({
|
|
@@ -5235,7 +5524,9 @@ var SyncOrchestrator = class {
|
|
|
5235
5524
|
syncDirection: "both",
|
|
5236
5525
|
fileType,
|
|
5237
5526
|
status: "synced",
|
|
5238
|
-
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
5527
|
+
baseSnapshot: Buffer.from(finalContent, "utf-8"),
|
|
5528
|
+
localMtime: pullStat?.mtime ?? null,
|
|
5529
|
+
localFileSize: pullStat?.size ?? null
|
|
5239
5530
|
});
|
|
5240
5531
|
const aliases = extractAliases2(properties);
|
|
5241
5532
|
this.stateDb.upsertWikilink({
|
|
@@ -5313,6 +5604,7 @@ var SyncOrchestrator = class {
|
|
|
5313
5604
|
}
|
|
5314
5605
|
}
|
|
5315
5606
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
5607
|
+
const updateStat = await this.vaultFs.getFileStat(record.obsidianPath);
|
|
5316
5608
|
const newHash = computeHash(remoteContent);
|
|
5317
5609
|
this.stateDb.upsert({
|
|
5318
5610
|
obsidianPath: record.obsidianPath,
|
|
@@ -5324,7 +5616,9 @@ var SyncOrchestrator = class {
|
|
|
5324
5616
|
syncDirection: record.syncDirection,
|
|
5325
5617
|
fileType: record.fileType,
|
|
5326
5618
|
status: "synced",
|
|
5327
|
-
baseSnapshot: Buffer.from(remoteContent, "utf-8")
|
|
5619
|
+
baseSnapshot: Buffer.from(remoteContent, "utf-8"),
|
|
5620
|
+
localMtime: updateStat?.mtime ?? null,
|
|
5621
|
+
localFileSize: updateStat?.size ?? null
|
|
5328
5622
|
});
|
|
5329
5623
|
return { path: record.obsidianPath };
|
|
5330
5624
|
}
|
|
@@ -5341,31 +5635,27 @@ var SyncOrchestrator = class {
|
|
|
5341
5635
|
return record.obsidianPath;
|
|
5342
5636
|
}
|
|
5343
5637
|
async ensureFolderPage(folderPath) {
|
|
5344
|
-
const existing = this.stateDb.getByPath(folderPath);
|
|
5345
|
-
if (existing?.notionPageId) return;
|
|
5346
5638
|
const folderNotePath = `${folderPath}/${folderPath.split("/").pop()}.md`;
|
|
5347
5639
|
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5348
|
-
if (folderNoteRecord?.
|
|
5349
|
-
this.stateDb.
|
|
5350
|
-
|
|
5351
|
-
notionPageId: folderNoteRecord.notionParentId,
|
|
5352
|
-
notionParentId: "",
|
|
5353
|
-
contentHash: "",
|
|
5354
|
-
notionLastEdited: folderNoteRecord.notionLastEdited,
|
|
5355
|
-
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5356
|
-
syncDirection: "both",
|
|
5357
|
-
fileType: "folder-note",
|
|
5358
|
-
status: "synced"
|
|
5359
|
-
});
|
|
5640
|
+
if (folderNoteRecord?.notionPageId) {
|
|
5641
|
+
const existing2 = this.stateDb.getByPath(folderPath);
|
|
5642
|
+
if (existing2) this.stateDb.delete(existing2.id);
|
|
5360
5643
|
return;
|
|
5361
5644
|
}
|
|
5645
|
+
const existing = this.stateDb.getByPath(folderPath);
|
|
5646
|
+
if (existing?.notionPageId) return;
|
|
5362
5647
|
const parts = folderPath.split("/");
|
|
5363
5648
|
const folderName = parts[parts.length - 1];
|
|
5364
5649
|
let parentId = this.config.notion.rootPageId;
|
|
5365
5650
|
if (parts.length > 1) {
|
|
5366
5651
|
const parentPath = parts.slice(0, -1).join("/");
|
|
5652
|
+
await this.ensureFolderPage(parentPath);
|
|
5653
|
+
const parentNotePath = `${parentPath}/${parentPath.split("/").pop()}.md`;
|
|
5654
|
+
const parentNote = this.stateDb.getByPath(parentNotePath);
|
|
5367
5655
|
const parentRecord = this.stateDb.getByPath(parentPath);
|
|
5368
|
-
if (
|
|
5656
|
+
if (parentNote?.notionPageId) {
|
|
5657
|
+
parentId = parentNote.notionPageId;
|
|
5658
|
+
} else if (parentRecord?.notionPageId) {
|
|
5369
5659
|
parentId = parentRecord.notionPageId;
|
|
5370
5660
|
}
|
|
5371
5661
|
}
|
|
@@ -5392,8 +5682,26 @@ var SyncOrchestrator = class {
|
|
|
5392
5682
|
const folderPath = parts.slice(0, -1).join("/");
|
|
5393
5683
|
const folderRecord = this.stateDb.getByPath(folderPath);
|
|
5394
5684
|
if (folderRecord?.notionPageId) return folderRecord.notionPageId;
|
|
5685
|
+
const folderName = parts[parts.length - 2];
|
|
5686
|
+
const folderNotePath = `${folderPath}/${folderName}.md`;
|
|
5687
|
+
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5688
|
+
if (folderNoteRecord?.notionPageId) return folderNoteRecord.notionPageId;
|
|
5395
5689
|
return this.config.notion.rootPageId;
|
|
5396
5690
|
}
|
|
5691
|
+
repairFolderRecords() {
|
|
5692
|
+
const allRecords = this.stateDb.getAll();
|
|
5693
|
+
const folderRecords = allRecords.filter(
|
|
5694
|
+
(r) => r.fileType === "folder-note" && !r.obsidianPath.endsWith(".md")
|
|
5695
|
+
);
|
|
5696
|
+
for (const folder of folderRecords) {
|
|
5697
|
+
const folderName = folder.obsidianPath.split("/").pop();
|
|
5698
|
+
const folderNotePath = `${folder.obsidianPath}/${folderName}.md`;
|
|
5699
|
+
const noteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5700
|
+
if (noteRecord?.notionPageId) {
|
|
5701
|
+
this.stateDb.delete(folder.id);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5397
5705
|
isFolderNote(filePath) {
|
|
5398
5706
|
const parts = filePath.split("/");
|
|
5399
5707
|
if (parts.length < 2) return false;
|
|
@@ -5428,17 +5736,14 @@ var SyncOrchestrator = class {
|
|
|
5428
5736
|
}
|
|
5429
5737
|
async pushCreatePage(parentId, parentType, title, markdownContent, properties) {
|
|
5430
5738
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
});
|
|
5440
|
-
} catch {
|
|
5441
|
-
}
|
|
5739
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5740
|
+
return await this.notionClient.createPageWithMarkdown({
|
|
5741
|
+
parentId,
|
|
5742
|
+
parentType,
|
|
5743
|
+
title,
|
|
5744
|
+
markdown: enhanced,
|
|
5745
|
+
properties
|
|
5746
|
+
});
|
|
5442
5747
|
}
|
|
5443
5748
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5444
5749
|
const page = await this.notionClient.createPage({
|
|
@@ -5452,27 +5757,11 @@ var SyncOrchestrator = class {
|
|
|
5452
5757
|
}
|
|
5453
5758
|
return page;
|
|
5454
5759
|
}
|
|
5455
|
-
async pushUpdatePage(pageId, markdownContent,
|
|
5760
|
+
async pushUpdatePage(pageId, markdownContent, _baseSnapshot) {
|
|
5456
5761
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
const baseEnhanced = obsidianToNotionEnhanced(baseSnapshot.toString("utf-8"));
|
|
5461
|
-
const patches = this.computePatches(baseEnhanced, enhanced);
|
|
5462
|
-
if (patches.length > 0 && patches.length <= 20) {
|
|
5463
|
-
try {
|
|
5464
|
-
await this.notionClient.updatePageMarkdownPartial(pageId, patches);
|
|
5465
|
-
return;
|
|
5466
|
-
} catch {
|
|
5467
|
-
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5468
|
-
return;
|
|
5469
|
-
}
|
|
5470
|
-
}
|
|
5471
|
-
}
|
|
5472
|
-
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5473
|
-
return;
|
|
5474
|
-
} catch {
|
|
5475
|
-
}
|
|
5762
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5763
|
+
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5764
|
+
return;
|
|
5476
5765
|
}
|
|
5477
5766
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5478
5767
|
const existingBlocks = await this.notionClient.fetchAllChildren(pageId);
|
|
@@ -5491,51 +5780,6 @@ var SyncOrchestrator = class {
|
|
|
5491
5780
|
})
|
|
5492
5781
|
);
|
|
5493
5782
|
}
|
|
5494
|
-
computePatches(oldContent, newContent) {
|
|
5495
|
-
const oldLines = oldContent.split("\n");
|
|
5496
|
-
const newLines = newContent.split("\n");
|
|
5497
|
-
const patches = [];
|
|
5498
|
-
let i = 0;
|
|
5499
|
-
let j = 0;
|
|
5500
|
-
while (i < oldLines.length && j < newLines.length) {
|
|
5501
|
-
if (oldLines[i] === newLines[j]) {
|
|
5502
|
-
i++;
|
|
5503
|
-
j++;
|
|
5504
|
-
continue;
|
|
5505
|
-
}
|
|
5506
|
-
let oldEnd = i + 1;
|
|
5507
|
-
let newEnd = j + 1;
|
|
5508
|
-
const lookahead = Math.min(10, oldLines.length - i, newLines.length - j);
|
|
5509
|
-
for (let k = 1; k <= lookahead; k++) {
|
|
5510
|
-
if (i + k < oldLines.length && newLines[j] === oldLines[i + k]) {
|
|
5511
|
-
oldEnd = i + k;
|
|
5512
|
-
newEnd = j;
|
|
5513
|
-
break;
|
|
5514
|
-
}
|
|
5515
|
-
if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
|
|
5516
|
-
oldEnd = i;
|
|
5517
|
-
newEnd = j + k;
|
|
5518
|
-
break;
|
|
5519
|
-
}
|
|
5520
|
-
oldEnd = i + k;
|
|
5521
|
-
newEnd = j + k;
|
|
5522
|
-
}
|
|
5523
|
-
const oldChunk = oldLines.slice(i, oldEnd).join("\n");
|
|
5524
|
-
const newChunk = newLines.slice(j, newEnd).join("\n");
|
|
5525
|
-
if (oldChunk || newChunk) {
|
|
5526
|
-
patches.push({ oldStr: oldChunk, newStr: newChunk });
|
|
5527
|
-
}
|
|
5528
|
-
i = oldEnd;
|
|
5529
|
-
j = newEnd;
|
|
5530
|
-
}
|
|
5531
|
-
if (i < oldLines.length || j < newLines.length) {
|
|
5532
|
-
patches.push({
|
|
5533
|
-
oldStr: oldLines.slice(i).join("\n"),
|
|
5534
|
-
newStr: newLines.slice(j).join("\n")
|
|
5535
|
-
});
|
|
5536
|
-
}
|
|
5537
|
-
return patches;
|
|
5538
|
-
}
|
|
5539
5783
|
async fetchPageMarkdown(pageId) {
|
|
5540
5784
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5541
5785
|
try {
|
|
@@ -5673,6 +5917,20 @@ var NodeVaultFS = class {
|
|
|
5673
5917
|
await this.walkDir(this.rootPath, files);
|
|
5674
5918
|
return files;
|
|
5675
5919
|
}
|
|
5920
|
+
async listMarkdownFileStats() {
|
|
5921
|
+
const stats = [];
|
|
5922
|
+
await this.walkDirStats(this.rootPath, stats);
|
|
5923
|
+
return stats;
|
|
5924
|
+
}
|
|
5925
|
+
async getFileStat(path) {
|
|
5926
|
+
try {
|
|
5927
|
+
const fullPath = join(this.rootPath, path);
|
|
5928
|
+
const fileStat = await stat(fullPath);
|
|
5929
|
+
return { path, mtime: fileStat.mtime.toISOString(), size: fileStat.size };
|
|
5930
|
+
} catch {
|
|
5931
|
+
return null;
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5676
5934
|
async listNonMarkdownFiles() {
|
|
5677
5935
|
const files = [];
|
|
5678
5936
|
await this.walkDirNonMd(this.rootPath, files);
|
|
@@ -5719,6 +5977,25 @@ var NodeVaultFS = class {
|
|
|
5719
5977
|
}
|
|
5720
5978
|
}
|
|
5721
5979
|
}
|
|
5980
|
+
async walkDirStats(dir, result) {
|
|
5981
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
5982
|
+
for (const entry of entries) {
|
|
5983
|
+
const fullPath = join(dir, entry.name);
|
|
5984
|
+
if (entry.name.startsWith(".")) continue;
|
|
5985
|
+
const relativePath = relative(this.rootPath, fullPath);
|
|
5986
|
+
if (this.isExcluded(relativePath)) continue;
|
|
5987
|
+
if (entry.isDirectory()) {
|
|
5988
|
+
await this.walkDirStats(fullPath, result);
|
|
5989
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5990
|
+
const fileStat = await stat(fullPath);
|
|
5991
|
+
result.push({
|
|
5992
|
+
path: relativePath,
|
|
5993
|
+
mtime: fileStat.mtime.toISOString(),
|
|
5994
|
+
size: fileStat.size
|
|
5995
|
+
});
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5722
5999
|
async walkDir(dir, result) {
|
|
5723
6000
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
5724
6001
|
for (const entry of entries) {
|