@im-nobsidian/core 0.1.6 → 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 +43 -3
- package/dist/index.js +516 -200
- 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
|
};
|
|
@@ -4443,21 +4572,28 @@ var SyncOrchestrator = class {
|
|
|
4443
4572
|
databaseSyncer;
|
|
4444
4573
|
propertyMapper;
|
|
4445
4574
|
dbSchemaLoaded = false;
|
|
4575
|
+
_pullImageCount = 0;
|
|
4576
|
+
_pullFileCount = 0;
|
|
4446
4577
|
async push(options) {
|
|
4447
4578
|
const startTime = Date.now();
|
|
4448
4579
|
if (this.config.sync.direction === "pull") {
|
|
4449
4580
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
4450
4581
|
}
|
|
4451
4582
|
this.cleanupInterruptedSync();
|
|
4452
|
-
|
|
4453
|
-
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
|
+
);
|
|
4454
4589
|
const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
|
|
4455
4590
|
const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
|
|
4456
4591
|
const dbFolders = (this.config.notion.databases ?? []).map(
|
|
4457
4592
|
(d) => d.localFolder.endsWith("/") ? d.localFolder : d.localFolder + "/"
|
|
4458
4593
|
);
|
|
4459
4594
|
const isDbPath = (path) => dbFolders.some((prefix) => path.startsWith(prefix));
|
|
4460
|
-
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)));
|
|
4461
4597
|
const hasDbConfigs = (this.config.notion.databases?.length ?? 0) > 0;
|
|
4462
4598
|
if (filtered.length === 0 && !hasDbConfigs) {
|
|
4463
4599
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
@@ -4466,6 +4602,12 @@ var SyncOrchestrator = class {
|
|
|
4466
4602
|
const dryCreated = filtered.filter((c) => c.type === "created").length;
|
|
4467
4603
|
const dryUpdated = filtered.filter((c) => c.type === "modified" || c.type === "moved").length;
|
|
4468
4604
|
const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
|
|
4605
|
+
let dryProgress = 0;
|
|
4606
|
+
const dryTotal = filtered.length;
|
|
4607
|
+
for (const change of filtered) {
|
|
4608
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4609
|
+
options?.onProgress?.(++dryProgress, dryTotal, { path: change.path, operation: op });
|
|
4610
|
+
}
|
|
4469
4611
|
return {
|
|
4470
4612
|
created: dryCreated,
|
|
4471
4613
|
updated: dryUpdated,
|
|
@@ -4500,7 +4642,8 @@ var SyncOrchestrator = class {
|
|
|
4500
4642
|
const tasks = filtered.map((change) => async () => {
|
|
4501
4643
|
await sema.acquire();
|
|
4502
4644
|
try {
|
|
4503
|
-
|
|
4645
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4646
|
+
options?.onProgress?.(++completed, total, { path: change.path, operation: op });
|
|
4504
4647
|
switch (change.type) {
|
|
4505
4648
|
case "created":
|
|
4506
4649
|
await this.pushCreate(change.path);
|
|
@@ -4604,7 +4747,10 @@ var SyncOrchestrator = class {
|
|
|
4604
4747
|
conflicts: [],
|
|
4605
4748
|
writtenPaths: [],
|
|
4606
4749
|
failed: [],
|
|
4607
|
-
duration: Date.now() - startTime
|
|
4750
|
+
duration: Date.now() - startTime,
|
|
4751
|
+
imageCount: 0,
|
|
4752
|
+
fileCount: 0,
|
|
4753
|
+
linkCount: 0
|
|
4608
4754
|
};
|
|
4609
4755
|
if (this.config.sync.direction === "push") {
|
|
4610
4756
|
return emptyResult;
|
|
@@ -4613,14 +4759,42 @@ var SyncOrchestrator = class {
|
|
|
4613
4759
|
let created = 0;
|
|
4614
4760
|
let updated = 0;
|
|
4615
4761
|
let deleted = 0;
|
|
4762
|
+
this._pullImageCount = 0;
|
|
4763
|
+
this._pullFileCount = 0;
|
|
4616
4764
|
const conflicts = [];
|
|
4617
4765
|
const writtenPaths = [];
|
|
4618
4766
|
const failed = [];
|
|
4619
|
-
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();
|
|
4620
4770
|
const filtered = options?.paths ? remoteChanges.filter((c) => {
|
|
4621
4771
|
const record = this.stateDb.getByNotionId(c.pageId);
|
|
4622
4772
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
4623
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
|
+
}
|
|
4624
4798
|
if (filtered.length === 0) {
|
|
4625
4799
|
return emptyResult;
|
|
4626
4800
|
}
|
|
@@ -4628,6 +4802,14 @@ var SyncOrchestrator = class {
|
|
|
4628
4802
|
const dryCreated = filtered.filter((c) => c.type === "created").length;
|
|
4629
4803
|
const dryUpdated = filtered.filter((c) => c.type === "modified").length;
|
|
4630
4804
|
const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
|
|
4805
|
+
let dryProgress = 0;
|
|
4806
|
+
const dryTotal = filtered.length;
|
|
4807
|
+
for (const change of filtered) {
|
|
4808
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4809
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4810
|
+
const displayPath = record?.obsidianPath ?? change.pageId;
|
|
4811
|
+
options?.onProgress?.(++dryProgress, dryTotal, { path: displayPath, operation: op });
|
|
4812
|
+
}
|
|
4631
4813
|
return {
|
|
4632
4814
|
created: dryCreated,
|
|
4633
4815
|
updated: dryUpdated,
|
|
@@ -4635,7 +4817,10 @@ var SyncOrchestrator = class {
|
|
|
4635
4817
|
conflicts: [],
|
|
4636
4818
|
writtenPaths: [],
|
|
4637
4819
|
failed: [],
|
|
4638
|
-
duration: Date.now() - startTime
|
|
4820
|
+
duration: Date.now() - startTime,
|
|
4821
|
+
imageCount: 0,
|
|
4822
|
+
fileCount: 0,
|
|
4823
|
+
linkCount: 0
|
|
4639
4824
|
};
|
|
4640
4825
|
}
|
|
4641
4826
|
this.stateDb.setMeta("pull_in_progress", "true");
|
|
@@ -4645,12 +4830,13 @@ var SyncOrchestrator = class {
|
|
|
4645
4830
|
const tasks = filtered.map((change) => async () => {
|
|
4646
4831
|
await sema.acquire();
|
|
4647
4832
|
try {
|
|
4648
|
-
|
|
4833
|
+
let resultPath;
|
|
4649
4834
|
switch (change.type) {
|
|
4650
4835
|
case "created": {
|
|
4651
4836
|
const path = await this.pullCreate(change.pageId);
|
|
4652
4837
|
writtenPaths.push(path);
|
|
4653
4838
|
created++;
|
|
4839
|
+
resultPath = path;
|
|
4654
4840
|
break;
|
|
4655
4841
|
}
|
|
4656
4842
|
case "modified": {
|
|
@@ -4661,16 +4847,24 @@ var SyncOrchestrator = class {
|
|
|
4661
4847
|
writtenPaths.push(result.path);
|
|
4662
4848
|
updated++;
|
|
4663
4849
|
}
|
|
4850
|
+
resultPath = result.path;
|
|
4664
4851
|
break;
|
|
4665
4852
|
}
|
|
4666
4853
|
case "deleted": {
|
|
4854
|
+
const record2 = this.stateDb.getByNotionId(change.pageId);
|
|
4855
|
+
resultPath = record2?.obsidianPath;
|
|
4667
4856
|
const path = await this.pullDelete(change.pageId);
|
|
4668
4857
|
if (path) {
|
|
4669
4858
|
deleted++;
|
|
4859
|
+
resultPath = path;
|
|
4670
4860
|
}
|
|
4671
4861
|
break;
|
|
4672
4862
|
}
|
|
4673
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 });
|
|
4674
4868
|
} catch (error) {
|
|
4675
4869
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4676
4870
|
failed.push({
|
|
@@ -4745,44 +4939,13 @@ var SyncOrchestrator = class {
|
|
|
4745
4939
|
getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
|
|
4746
4940
|
}
|
|
4747
4941
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
4753
|
-
);
|
|
4754
|
-
const newDbs = discoveredDbs.filter((db) => !configuredIds.has(db.dbId.replace(/-/g, "")));
|
|
4755
|
-
for (const { dbId, parentPageId } of newDbs) {
|
|
4756
|
-
try {
|
|
4757
|
-
const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
|
|
4758
|
-
const safeName = (dbTitle || dbId.slice(0, 8)).replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
4759
|
-
const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
|
|
4760
|
-
let parentFolder = "databases";
|
|
4761
|
-
if (parentEntry?.obsidianPath) {
|
|
4762
|
-
const parts = parentEntry.obsidianPath.split("/");
|
|
4763
|
-
parts.pop();
|
|
4764
|
-
parentFolder = parts.length > 0 ? parts.join("/") : "databases";
|
|
4765
|
-
}
|
|
4766
|
-
const folderName = `${parentFolder}/${safeName}`;
|
|
4767
|
-
const dbConfig = {
|
|
4768
|
-
databaseId: dbId,
|
|
4769
|
-
localFolder: folderName,
|
|
4770
|
-
titleProperty: "Name"
|
|
4771
|
-
};
|
|
4772
|
-
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
4773
|
-
created += dbResult.created;
|
|
4774
|
-
updated += dbResult.updated;
|
|
4775
|
-
failed.push(...dbResult.failed);
|
|
4776
|
-
} catch (error) {
|
|
4777
|
-
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
4778
|
-
}
|
|
4779
|
-
}
|
|
4780
|
-
} catch (error) {
|
|
4781
|
-
getLogger().warn("[Im-Nobsidian] child_database \uC790\uB3D9 \uBC1C\uACAC \uC2E4\uD328:", error);
|
|
4782
|
-
}
|
|
4942
|
+
{
|
|
4943
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
4944
|
+
created += dbDiscovery.created;
|
|
4945
|
+
updated += dbDiscovery.updated;
|
|
4783
4946
|
}
|
|
4784
|
-
const
|
|
4785
|
-
await this.resolveNotionLinks(
|
|
4947
|
+
const linkTargetPaths = writtenPaths.length > 0 ? writtenPaths : [];
|
|
4948
|
+
const linkCount = linkTargetPaths.length > 0 ? await this.resolveNotionLinks(linkTargetPaths) : 0;
|
|
4786
4949
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4787
4950
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4788
4951
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
@@ -4793,7 +4956,10 @@ var SyncOrchestrator = class {
|
|
|
4793
4956
|
conflicts,
|
|
4794
4957
|
writtenPaths,
|
|
4795
4958
|
failed,
|
|
4796
|
-
duration: Date.now() - startTime
|
|
4959
|
+
duration: Date.now() - startTime,
|
|
4960
|
+
imageCount: this._pullImageCount,
|
|
4961
|
+
fileCount: this._pullFileCount,
|
|
4962
|
+
linkCount
|
|
4797
4963
|
};
|
|
4798
4964
|
}
|
|
4799
4965
|
async sync(options) {
|
|
@@ -4801,7 +4967,8 @@ var SyncOrchestrator = class {
|
|
|
4801
4967
|
const pullResult = await this.pull(options);
|
|
4802
4968
|
const pushResult = await this.push({
|
|
4803
4969
|
...options,
|
|
4804
|
-
paths: options?.paths
|
|
4970
|
+
paths: options?.paths,
|
|
4971
|
+
excludePaths: pullResult.writtenPaths
|
|
4805
4972
|
});
|
|
4806
4973
|
return {
|
|
4807
4974
|
pull: pullResult,
|
|
@@ -4830,6 +4997,34 @@ var SyncOrchestrator = class {
|
|
|
4830
4997
|
lastSyncAt
|
|
4831
4998
|
};
|
|
4832
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
|
+
}
|
|
4833
5028
|
async buildConflictsFromRecords(records, localChanges, remoteChanges) {
|
|
4834
5029
|
const conflicts = [];
|
|
4835
5030
|
for (const record of records) {
|
|
@@ -4872,27 +5067,100 @@ var SyncOrchestrator = class {
|
|
|
4872
5067
|
return this.config.notion.parentMode === "database" && !!this.config.notion.databaseId;
|
|
4873
5068
|
}
|
|
4874
5069
|
async discoverChildDatabases() {
|
|
4875
|
-
const rootId = this.config.notion.rootPageId;
|
|
4876
5070
|
const allDbs = [];
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
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) {
|
|
4881
5095
|
try {
|
|
4882
|
-
|
|
4883
|
-
for (const dbId of dbIds) {
|
|
4884
|
-
allDbs.push({ dbId, parentPageId: parentId });
|
|
4885
|
-
}
|
|
4886
|
-
const childPages = await this.notionClient.getChildPages(parentId);
|
|
4887
|
-
nextLevel.push(...childPages.map((p) => p.id));
|
|
5096
|
+
dbConfigs = JSON.parse(cachedRaw);
|
|
4888
5097
|
} catch {
|
|
5098
|
+
dbConfigs = [];
|
|
4889
5099
|
}
|
|
4890
5100
|
}
|
|
4891
|
-
|
|
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);
|
|
4892
5159
|
}
|
|
4893
|
-
return
|
|
5160
|
+
return { created, updated };
|
|
4894
5161
|
}
|
|
4895
5162
|
async resolveNotionLinks(paths) {
|
|
5163
|
+
let totalResolved = 0;
|
|
4896
5164
|
const allRecords = this.stateDb.getAll();
|
|
4897
5165
|
const idToTitle = /* @__PURE__ */ new Map();
|
|
4898
5166
|
for (const r of allRecords) {
|
|
@@ -4903,7 +5171,7 @@ var SyncOrchestrator = class {
|
|
|
4903
5171
|
idToTitle.set(r.notionPageId, title);
|
|
4904
5172
|
}
|
|
4905
5173
|
}
|
|
4906
|
-
if (idToTitle.size === 0) return;
|
|
5174
|
+
if (idToTitle.size === 0) return 0;
|
|
4907
5175
|
for (const filePath of paths) {
|
|
4908
5176
|
if (!filePath.endsWith(".md")) continue;
|
|
4909
5177
|
try {
|
|
@@ -4912,6 +5180,7 @@ var SyncOrchestrator = class {
|
|
|
4912
5180
|
const resolved = content.replace(/\[\[notion:([a-f0-9-]+)\]\]/g, (_match, id) => {
|
|
4913
5181
|
const title = idToTitle.get(id.replace(/-/g, ""));
|
|
4914
5182
|
if (title) {
|
|
5183
|
+
totalResolved++;
|
|
4915
5184
|
changed = true;
|
|
4916
5185
|
return `[[${title}]]`;
|
|
4917
5186
|
}
|
|
@@ -4923,6 +5192,7 @@ var SyncOrchestrator = class {
|
|
|
4923
5192
|
(_match, text, id) => {
|
|
4924
5193
|
const title = idToTitle.get(id);
|
|
4925
5194
|
if (title) {
|
|
5195
|
+
totalResolved++;
|
|
4926
5196
|
changed = true;
|
|
4927
5197
|
return `[[${title}|${text}]]`;
|
|
4928
5198
|
}
|
|
@@ -4936,6 +5206,7 @@ var SyncOrchestrator = class {
|
|
|
4936
5206
|
} catch {
|
|
4937
5207
|
}
|
|
4938
5208
|
}
|
|
5209
|
+
return totalResolved;
|
|
4939
5210
|
}
|
|
4940
5211
|
async ensureDbSchema() {
|
|
4941
5212
|
if (this.dbSchemaLoaded || !this.isDatabaseMode) return;
|
|
@@ -4980,6 +5251,7 @@ var SyncOrchestrator = class {
|
|
|
4980
5251
|
);
|
|
4981
5252
|
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
4982
5253
|
const hash = computeHash(content);
|
|
5254
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
4983
5255
|
this.stateDb.transaction(() => {
|
|
4984
5256
|
this.stateDb.upsert({
|
|
4985
5257
|
obsidianPath: path,
|
|
@@ -4991,7 +5263,9 @@ var SyncOrchestrator = class {
|
|
|
4991
5263
|
syncDirection: "both",
|
|
4992
5264
|
fileType: this.isFolderNote(path) ? "folder-note" : "file",
|
|
4993
5265
|
status: "synced",
|
|
4994
|
-
baseSnapshot: Buffer.from(content, "utf-8")
|
|
5266
|
+
baseSnapshot: Buffer.from(content, "utf-8"),
|
|
5267
|
+
localMtime: fileStat?.mtime ?? null,
|
|
5268
|
+
localFileSize: fileStat?.size ?? null
|
|
4995
5269
|
});
|
|
4996
5270
|
const aliases = extractAliases2(conversionResult.properties);
|
|
4997
5271
|
this.stateDb.upsertWikilink({
|
|
@@ -5021,23 +5295,36 @@ var SyncOrchestrator = class {
|
|
|
5021
5295
|
});
|
|
5022
5296
|
await this.pushUpdatePage(record.notionPageId, conversionResult.content, record.baseSnapshot);
|
|
5023
5297
|
await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
|
|
5024
|
-
let propsToUpdate
|
|
5025
|
-
if (this.isDatabaseMode &&
|
|
5298
|
+
let propsToUpdate;
|
|
5299
|
+
if (this.isDatabaseMode && conversionResult.properties && Object.keys(conversionResult.properties).length > 0) {
|
|
5026
5300
|
await this.ensureDbSchema();
|
|
5027
5301
|
const title2 = extractTitle(path);
|
|
5028
|
-
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
|
+
};
|
|
5029
5308
|
}
|
|
5309
|
+
let lastEditedTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5030
5310
|
if (propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
5031
|
-
await this.notionClient.updatePageProperties(
|
|
5311
|
+
const updatedPage = await this.notionClient.updatePageProperties(
|
|
5312
|
+
record.notionPageId,
|
|
5313
|
+
propsToUpdate
|
|
5314
|
+
);
|
|
5315
|
+
lastEditedTime = updatedPage.last_edited_time;
|
|
5032
5316
|
}
|
|
5033
|
-
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
5034
5317
|
const hash = computeHash(content);
|
|
5318
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
5035
5319
|
const title = extractTitle(path);
|
|
5036
5320
|
const aliases = extractAliases2(conversionResult.properties);
|
|
5037
5321
|
this.stateDb.transaction(() => {
|
|
5038
5322
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
5039
5323
|
this.stateDb.updateStatus(record.id, "synced");
|
|
5040
|
-
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
|
+
}
|
|
5041
5328
|
this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
|
|
5042
5329
|
this.stateDb.upsertWikilink({
|
|
5043
5330
|
obsidianPath: path,
|
|
@@ -5131,13 +5418,49 @@ var SyncOrchestrator = class {
|
|
|
5131
5418
|
}
|
|
5132
5419
|
return changes;
|
|
5133
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
|
+
}
|
|
5134
5457
|
async pullCreate(pageId) {
|
|
5135
5458
|
const page = await this.notionClient.getPage(pageId);
|
|
5136
5459
|
const title = this.notionClient.extractTitle(page);
|
|
5137
5460
|
const safeName = sanitizeFileName(title);
|
|
5138
5461
|
const parentPath = await this.resolveParentPath(page);
|
|
5139
|
-
const
|
|
5140
|
-
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");
|
|
5141
5464
|
const markdown = await this.fetchPageMarkdown(pageId);
|
|
5142
5465
|
const hasContent = markdown.trim().length > 0;
|
|
5143
5466
|
let filePath;
|
|
@@ -5171,9 +5494,11 @@ var SyncOrchestrator = class {
|
|
|
5171
5494
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
5172
5495
|
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
5173
5496
|
processedMarkdown = imageResult.content;
|
|
5497
|
+
this._pullImageCount += imageResult.downloads.length;
|
|
5174
5498
|
}
|
|
5175
5499
|
const fileResult = await this.imageHandler.downloadAllFiles(processedMarkdown, title);
|
|
5176
5500
|
processedMarkdown = fileResult.content;
|
|
5501
|
+
this._pullFileCount += fileResult.downloads.length;
|
|
5177
5502
|
const finalContent = this.pipeline.convertToMarkdown(
|
|
5178
5503
|
processedMarkdown,
|
|
5179
5504
|
{
|
|
@@ -5186,6 +5511,7 @@ var SyncOrchestrator = class {
|
|
|
5186
5511
|
);
|
|
5187
5512
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
5188
5513
|
const hash = computeHash(finalContent);
|
|
5514
|
+
const pullStat = await this.vaultFs.getFileStat(filePath);
|
|
5189
5515
|
const resolvedParentId = await this.extractParentId(page);
|
|
5190
5516
|
this.stateDb.transaction(() => {
|
|
5191
5517
|
this.stateDb.upsert({
|
|
@@ -5198,7 +5524,9 @@ var SyncOrchestrator = class {
|
|
|
5198
5524
|
syncDirection: "both",
|
|
5199
5525
|
fileType,
|
|
5200
5526
|
status: "synced",
|
|
5201
|
-
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
5527
|
+
baseSnapshot: Buffer.from(finalContent, "utf-8"),
|
|
5528
|
+
localMtime: pullStat?.mtime ?? null,
|
|
5529
|
+
localFileSize: pullStat?.size ?? null
|
|
5202
5530
|
});
|
|
5203
5531
|
const aliases = extractAliases2(properties);
|
|
5204
5532
|
this.stateDb.upsertWikilink({
|
|
@@ -5229,9 +5557,11 @@ var SyncOrchestrator = class {
|
|
|
5229
5557
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
5230
5558
|
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
5231
5559
|
markdown = imageResult.content;
|
|
5560
|
+
this._pullImageCount += imageResult.downloads.length;
|
|
5232
5561
|
}
|
|
5233
5562
|
const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
|
|
5234
5563
|
markdown = fileResult.content;
|
|
5564
|
+
this._pullFileCount += fileResult.downloads.length;
|
|
5235
5565
|
const savedMarkers = this.stateDb.getPreserveMarkers(record.obsidianPath);
|
|
5236
5566
|
const remoteContent = this.pipeline.convertToMarkdown(
|
|
5237
5567
|
markdown,
|
|
@@ -5274,6 +5604,7 @@ var SyncOrchestrator = class {
|
|
|
5274
5604
|
}
|
|
5275
5605
|
}
|
|
5276
5606
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
5607
|
+
const updateStat = await this.vaultFs.getFileStat(record.obsidianPath);
|
|
5277
5608
|
const newHash = computeHash(remoteContent);
|
|
5278
5609
|
this.stateDb.upsert({
|
|
5279
5610
|
obsidianPath: record.obsidianPath,
|
|
@@ -5285,7 +5616,9 @@ var SyncOrchestrator = class {
|
|
|
5285
5616
|
syncDirection: record.syncDirection,
|
|
5286
5617
|
fileType: record.fileType,
|
|
5287
5618
|
status: "synced",
|
|
5288
|
-
baseSnapshot: Buffer.from(remoteContent, "utf-8")
|
|
5619
|
+
baseSnapshot: Buffer.from(remoteContent, "utf-8"),
|
|
5620
|
+
localMtime: updateStat?.mtime ?? null,
|
|
5621
|
+
localFileSize: updateStat?.size ?? null
|
|
5289
5622
|
});
|
|
5290
5623
|
return { path: record.obsidianPath };
|
|
5291
5624
|
}
|
|
@@ -5302,31 +5635,27 @@ var SyncOrchestrator = class {
|
|
|
5302
5635
|
return record.obsidianPath;
|
|
5303
5636
|
}
|
|
5304
5637
|
async ensureFolderPage(folderPath) {
|
|
5305
|
-
const existing = this.stateDb.getByPath(folderPath);
|
|
5306
|
-
if (existing?.notionPageId) return;
|
|
5307
5638
|
const folderNotePath = `${folderPath}/${folderPath.split("/").pop()}.md`;
|
|
5308
5639
|
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5309
|
-
if (folderNoteRecord?.
|
|
5310
|
-
this.stateDb.
|
|
5311
|
-
|
|
5312
|
-
notionPageId: folderNoteRecord.notionParentId,
|
|
5313
|
-
notionParentId: "",
|
|
5314
|
-
contentHash: "",
|
|
5315
|
-
notionLastEdited: folderNoteRecord.notionLastEdited,
|
|
5316
|
-
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5317
|
-
syncDirection: "both",
|
|
5318
|
-
fileType: "folder-note",
|
|
5319
|
-
status: "synced"
|
|
5320
|
-
});
|
|
5640
|
+
if (folderNoteRecord?.notionPageId) {
|
|
5641
|
+
const existing2 = this.stateDb.getByPath(folderPath);
|
|
5642
|
+
if (existing2) this.stateDb.delete(existing2.id);
|
|
5321
5643
|
return;
|
|
5322
5644
|
}
|
|
5645
|
+
const existing = this.stateDb.getByPath(folderPath);
|
|
5646
|
+
if (existing?.notionPageId) return;
|
|
5323
5647
|
const parts = folderPath.split("/");
|
|
5324
5648
|
const folderName = parts[parts.length - 1];
|
|
5325
5649
|
let parentId = this.config.notion.rootPageId;
|
|
5326
5650
|
if (parts.length > 1) {
|
|
5327
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);
|
|
5328
5655
|
const parentRecord = this.stateDb.getByPath(parentPath);
|
|
5329
|
-
if (
|
|
5656
|
+
if (parentNote?.notionPageId) {
|
|
5657
|
+
parentId = parentNote.notionPageId;
|
|
5658
|
+
} else if (parentRecord?.notionPageId) {
|
|
5330
5659
|
parentId = parentRecord.notionPageId;
|
|
5331
5660
|
}
|
|
5332
5661
|
}
|
|
@@ -5353,8 +5682,26 @@ var SyncOrchestrator = class {
|
|
|
5353
5682
|
const folderPath = parts.slice(0, -1).join("/");
|
|
5354
5683
|
const folderRecord = this.stateDb.getByPath(folderPath);
|
|
5355
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;
|
|
5356
5689
|
return this.config.notion.rootPageId;
|
|
5357
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
|
+
}
|
|
5358
5705
|
isFolderNote(filePath) {
|
|
5359
5706
|
const parts = filePath.split("/");
|
|
5360
5707
|
if (parts.length < 2) return false;
|
|
@@ -5389,17 +5736,14 @@ var SyncOrchestrator = class {
|
|
|
5389
5736
|
}
|
|
5390
5737
|
async pushCreatePage(parentId, parentType, title, markdownContent, properties) {
|
|
5391
5738
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
});
|
|
5401
|
-
} catch {
|
|
5402
|
-
}
|
|
5739
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5740
|
+
return await this.notionClient.createPageWithMarkdown({
|
|
5741
|
+
parentId,
|
|
5742
|
+
parentType,
|
|
5743
|
+
title,
|
|
5744
|
+
markdown: enhanced,
|
|
5745
|
+
properties
|
|
5746
|
+
});
|
|
5403
5747
|
}
|
|
5404
5748
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5405
5749
|
const page = await this.notionClient.createPage({
|
|
@@ -5413,27 +5757,11 @@ var SyncOrchestrator = class {
|
|
|
5413
5757
|
}
|
|
5414
5758
|
return page;
|
|
5415
5759
|
}
|
|
5416
|
-
async pushUpdatePage(pageId, markdownContent,
|
|
5760
|
+
async pushUpdatePage(pageId, markdownContent, _baseSnapshot) {
|
|
5417
5761
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
const baseEnhanced = obsidianToNotionEnhanced(baseSnapshot.toString("utf-8"));
|
|
5422
|
-
const patches = this.computePatches(baseEnhanced, enhanced);
|
|
5423
|
-
if (patches.length > 0 && patches.length <= 20) {
|
|
5424
|
-
try {
|
|
5425
|
-
await this.notionClient.updatePageMarkdownPartial(pageId, patches);
|
|
5426
|
-
return;
|
|
5427
|
-
} catch {
|
|
5428
|
-
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5429
|
-
return;
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5432
|
-
}
|
|
5433
|
-
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5434
|
-
return;
|
|
5435
|
-
} catch {
|
|
5436
|
-
}
|
|
5762
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5763
|
+
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5764
|
+
return;
|
|
5437
5765
|
}
|
|
5438
5766
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5439
5767
|
const existingBlocks = await this.notionClient.fetchAllChildren(pageId);
|
|
@@ -5452,51 +5780,6 @@ var SyncOrchestrator = class {
|
|
|
5452
5780
|
})
|
|
5453
5781
|
);
|
|
5454
5782
|
}
|
|
5455
|
-
computePatches(oldContent, newContent) {
|
|
5456
|
-
const oldLines = oldContent.split("\n");
|
|
5457
|
-
const newLines = newContent.split("\n");
|
|
5458
|
-
const patches = [];
|
|
5459
|
-
let i = 0;
|
|
5460
|
-
let j = 0;
|
|
5461
|
-
while (i < oldLines.length && j < newLines.length) {
|
|
5462
|
-
if (oldLines[i] === newLines[j]) {
|
|
5463
|
-
i++;
|
|
5464
|
-
j++;
|
|
5465
|
-
continue;
|
|
5466
|
-
}
|
|
5467
|
-
let oldEnd = i + 1;
|
|
5468
|
-
let newEnd = j + 1;
|
|
5469
|
-
const lookahead = Math.min(10, oldLines.length - i, newLines.length - j);
|
|
5470
|
-
for (let k = 1; k <= lookahead; k++) {
|
|
5471
|
-
if (i + k < oldLines.length && newLines[j] === oldLines[i + k]) {
|
|
5472
|
-
oldEnd = i + k;
|
|
5473
|
-
newEnd = j;
|
|
5474
|
-
break;
|
|
5475
|
-
}
|
|
5476
|
-
if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
|
|
5477
|
-
oldEnd = i;
|
|
5478
|
-
newEnd = j + k;
|
|
5479
|
-
break;
|
|
5480
|
-
}
|
|
5481
|
-
oldEnd = i + k;
|
|
5482
|
-
newEnd = j + k;
|
|
5483
|
-
}
|
|
5484
|
-
const oldChunk = oldLines.slice(i, oldEnd).join("\n");
|
|
5485
|
-
const newChunk = newLines.slice(j, newEnd).join("\n");
|
|
5486
|
-
if (oldChunk || newChunk) {
|
|
5487
|
-
patches.push({ oldStr: oldChunk, newStr: newChunk });
|
|
5488
|
-
}
|
|
5489
|
-
i = oldEnd;
|
|
5490
|
-
j = newEnd;
|
|
5491
|
-
}
|
|
5492
|
-
if (i < oldLines.length || j < newLines.length) {
|
|
5493
|
-
patches.push({
|
|
5494
|
-
oldStr: oldLines.slice(i).join("\n"),
|
|
5495
|
-
newStr: newLines.slice(j).join("\n")
|
|
5496
|
-
});
|
|
5497
|
-
}
|
|
5498
|
-
return patches;
|
|
5499
|
-
}
|
|
5500
5783
|
async fetchPageMarkdown(pageId) {
|
|
5501
5784
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5502
5785
|
try {
|
|
@@ -5634,6 +5917,20 @@ var NodeVaultFS = class {
|
|
|
5634
5917
|
await this.walkDir(this.rootPath, files);
|
|
5635
5918
|
return files;
|
|
5636
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
|
+
}
|
|
5637
5934
|
async listNonMarkdownFiles() {
|
|
5638
5935
|
const files = [];
|
|
5639
5936
|
await this.walkDirNonMd(this.rootPath, files);
|
|
@@ -5680,6 +5977,25 @@ var NodeVaultFS = class {
|
|
|
5680
5977
|
}
|
|
5681
5978
|
}
|
|
5682
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
|
+
}
|
|
5683
5999
|
async walkDir(dir, result) {
|
|
5684
6000
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
5685
6001
|
for (const entry of entries) {
|