@im-nobsidian/core 0.1.7 → 0.1.9
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 +82 -9
- package/dist/index.js +561 -208
- 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
|
|
@@ -788,12 +810,16 @@ var NotionClient = class _NotionClient {
|
|
|
788
810
|
sema;
|
|
789
811
|
token;
|
|
790
812
|
propertyMapper = new PropertyMapper();
|
|
813
|
+
customFetch;
|
|
791
814
|
constructor(options) {
|
|
792
815
|
this.client = new Client({
|
|
793
816
|
auth: options.token,
|
|
794
|
-
timeoutMs: options.timeoutMs ?? 3e4
|
|
817
|
+
timeoutMs: options.timeoutMs ?? 3e4,
|
|
818
|
+
logLevel: LogLevel.ERROR,
|
|
819
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
795
820
|
});
|
|
796
821
|
this.token = options.token;
|
|
822
|
+
this.customFetch = options.fetch;
|
|
797
823
|
this.sema = new Sema(options.concurrency ?? 3);
|
|
798
824
|
}
|
|
799
825
|
getInternalClient() {
|
|
@@ -886,7 +912,8 @@ var NotionClient = class _NotionClient {
|
|
|
886
912
|
const cleanId = databaseId.replace(/-/g, "");
|
|
887
913
|
const formatted = `${cleanId.slice(0, 8)}-${cleanId.slice(8, 12)}-${cleanId.slice(12, 16)}-${cleanId.slice(16, 20)}-${cleanId.slice(20)}`;
|
|
888
914
|
const resp = await this.withRateLimit(async () => {
|
|
889
|
-
const
|
|
915
|
+
const doFetch = this.customFetch ?? globalThis.fetch;
|
|
916
|
+
const r = await doFetch(`https://api.notion.com/v1/databases/${formatted}`, {
|
|
890
917
|
headers: {
|
|
891
918
|
Authorization: `Bearer ${this.token}`,
|
|
892
919
|
"Notion-Version": "2022-06-28",
|
|
@@ -1072,6 +1099,30 @@ var NotionClient = class _NotionClient {
|
|
|
1072
1099
|
nextCursor: response.next_cursor
|
|
1073
1100
|
};
|
|
1074
1101
|
}
|
|
1102
|
+
async searchRecentPages(since) {
|
|
1103
|
+
const results = [];
|
|
1104
|
+
let cursor;
|
|
1105
|
+
const sinceDate = new Date(since);
|
|
1106
|
+
outer: do {
|
|
1107
|
+
const response = await this.withRateLimit(
|
|
1108
|
+
() => this.client.search({
|
|
1109
|
+
filter: { property: "object", value: "page" },
|
|
1110
|
+
sort: { direction: "descending", timestamp: "last_edited_time" },
|
|
1111
|
+
start_cursor: cursor,
|
|
1112
|
+
page_size: 100
|
|
1113
|
+
})
|
|
1114
|
+
);
|
|
1115
|
+
for (const page of response.results) {
|
|
1116
|
+
const p = page;
|
|
1117
|
+
if (new Date(p.last_edited_time) <= sinceDate) {
|
|
1118
|
+
break outer;
|
|
1119
|
+
}
|
|
1120
|
+
results.push({ id: p.id, last_edited_time: p.last_edited_time });
|
|
1121
|
+
}
|
|
1122
|
+
cursor = response.next_cursor ?? void 0;
|
|
1123
|
+
} while (cursor);
|
|
1124
|
+
return results;
|
|
1125
|
+
}
|
|
1075
1126
|
async getChildPages(parentId) {
|
|
1076
1127
|
const blocks = await this.fetchAllChildrenDeep(parentId);
|
|
1077
1128
|
const childPageBlocks = blocks.filter((b) => b.type === "child_page");
|
|
@@ -1080,8 +1131,8 @@ var NotionClient = class _NotionClient {
|
|
|
1080
1131
|
return pages;
|
|
1081
1132
|
}
|
|
1082
1133
|
async getChildDatabaseIds(parentId) {
|
|
1083
|
-
const
|
|
1084
|
-
return
|
|
1134
|
+
const children = await this.fetchAllChildren(parentId);
|
|
1135
|
+
return children.filter((b) => b.type === "child_database").map((b) => b.id);
|
|
1085
1136
|
}
|
|
1086
1137
|
async getChildPagesRecursive(parentId) {
|
|
1087
1138
|
const all = [];
|
|
@@ -1629,6 +1680,52 @@ var ChangeDetector = class {
|
|
|
1629
1680
|
this.stateDb = stateDb;
|
|
1630
1681
|
}
|
|
1631
1682
|
stateDb;
|
|
1683
|
+
async detectLocalChangesFast(stats, readFile3) {
|
|
1684
|
+
const changes = [];
|
|
1685
|
+
const existingPaths = /* @__PURE__ */ new Set();
|
|
1686
|
+
for (const file of stats) {
|
|
1687
|
+
existingPaths.add(file.path);
|
|
1688
|
+
const record = this.stateDb.getByPath(file.path);
|
|
1689
|
+
if (!record) {
|
|
1690
|
+
const content2 = await readFile3(file.path);
|
|
1691
|
+
changes.push({
|
|
1692
|
+
path: file.path,
|
|
1693
|
+
type: "created",
|
|
1694
|
+
currentHash: computeHash(content2),
|
|
1695
|
+
previousHash: null
|
|
1696
|
+
});
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
if (record.localMtime === file.mtime && record.localFileSize === file.size) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
const content = await readFile3(file.path);
|
|
1703
|
+
const currentHash = computeHash(content);
|
|
1704
|
+
if (record.contentHash !== currentHash) {
|
|
1705
|
+
changes.push({
|
|
1706
|
+
path: file.path,
|
|
1707
|
+
type: "modified",
|
|
1708
|
+
currentHash,
|
|
1709
|
+
previousHash: record.contentHash
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const syncedRecords = this.stateDb.getByStatus("synced");
|
|
1714
|
+
for (const record of syncedRecords) {
|
|
1715
|
+
if (!existingPaths.has(record.obsidianPath)) {
|
|
1716
|
+
if (record.fileType === "folder-note" || record.fileType === "folder-only") {
|
|
1717
|
+
if (!record.obsidianPath.endsWith(".md")) continue;
|
|
1718
|
+
}
|
|
1719
|
+
changes.push({
|
|
1720
|
+
path: record.obsidianPath,
|
|
1721
|
+
type: "deleted",
|
|
1722
|
+
currentHash: "",
|
|
1723
|
+
previousHash: record.contentHash
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return this.detectMoves(changes);
|
|
1728
|
+
}
|
|
1632
1729
|
detectLocalChanges(currentFiles) {
|
|
1633
1730
|
const changes = [];
|
|
1634
1731
|
const existingPaths = /* @__PURE__ */ new Set();
|
|
@@ -1644,7 +1741,7 @@ var ChangeDetector = class {
|
|
|
1644
1741
|
});
|
|
1645
1742
|
continue;
|
|
1646
1743
|
}
|
|
1647
|
-
if (record.
|
|
1744
|
+
if (record.localMtime && record.localMtime === file.mtime) {
|
|
1648
1745
|
continue;
|
|
1649
1746
|
}
|
|
1650
1747
|
const currentHash = computeHash(file.content);
|
|
@@ -3049,14 +3146,19 @@ var EXTENSION_TO_MIME = {
|
|
|
3049
3146
|
".tiff": "image/tiff"
|
|
3050
3147
|
};
|
|
3051
3148
|
var ImageHandler = class {
|
|
3052
|
-
constructor(vaultFs, attachmentFolder = "attachments", notionClient) {
|
|
3149
|
+
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch) {
|
|
3053
3150
|
this.vaultFs = vaultFs;
|
|
3054
3151
|
this.attachmentFolder = attachmentFolder;
|
|
3055
3152
|
this.notionClient = notionClient;
|
|
3153
|
+
this.customFetch = customFetch;
|
|
3056
3154
|
}
|
|
3057
3155
|
vaultFs;
|
|
3058
3156
|
attachmentFolder;
|
|
3059
3157
|
notionClient;
|
|
3158
|
+
customFetch;
|
|
3159
|
+
get fetchFn() {
|
|
3160
|
+
return this.customFetch ?? globalThis.fetch;
|
|
3161
|
+
}
|
|
3060
3162
|
async downloadImage(url, pageTitle) {
|
|
3061
3163
|
const maxRetries = 3;
|
|
3062
3164
|
let lastError = null;
|
|
@@ -3068,7 +3170,7 @@ var ImageHandler = class {
|
|
|
3068
3170
|
`[Im-Nobsidian] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${pageTitle}`
|
|
3069
3171
|
);
|
|
3070
3172
|
}
|
|
3071
|
-
const response = await
|
|
3173
|
+
const response = await this.fetchFn(url);
|
|
3072
3174
|
if (!response.ok) {
|
|
3073
3175
|
throw new Error(`\uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3074
3176
|
}
|
|
@@ -3215,6 +3317,7 @@ var ImageHandler = class {
|
|
|
3215
3317
|
const url = match[2];
|
|
3216
3318
|
const caption = match[1]?.trim() || "file";
|
|
3217
3319
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
3320
|
+
if (!download.localPath) continue;
|
|
3218
3321
|
downloads.push(download);
|
|
3219
3322
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3220
3323
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3236,6 +3339,7 @@ var ImageHandler = class {
|
|
|
3236
3339
|
if (!realUrl) continue;
|
|
3237
3340
|
const caption = fileName || match[1]?.trim() || "file";
|
|
3238
3341
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3342
|
+
if (!download.localPath) continue;
|
|
3239
3343
|
downloads.push(download);
|
|
3240
3344
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3241
3345
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3251,6 +3355,7 @@ var ImageHandler = class {
|
|
|
3251
3355
|
const url = match[1];
|
|
3252
3356
|
const caption = match[2]?.trim() || "file";
|
|
3253
3357
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
3358
|
+
if (!download.localPath) continue;
|
|
3254
3359
|
downloads.push(download);
|
|
3255
3360
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3256
3361
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3272,6 +3377,7 @@ var ImageHandler = class {
|
|
|
3272
3377
|
if (!realUrl) continue;
|
|
3273
3378
|
const caption = fileName || match[2]?.trim() || "file";
|
|
3274
3379
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3380
|
+
if (!download.localPath) continue;
|
|
3275
3381
|
downloads.push(download);
|
|
3276
3382
|
const obsidianLink = `[[${download.localPath}|${caption}]]`;
|
|
3277
3383
|
result = result.replace(match[0], obsidianLink);
|
|
@@ -3307,10 +3413,17 @@ var ImageHandler = class {
|
|
|
3307
3413
|
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${caption}`
|
|
3308
3414
|
);
|
|
3309
3415
|
}
|
|
3310
|
-
const response = await
|
|
3416
|
+
const response = await this.fetchFn(url);
|
|
3311
3417
|
if (!response.ok) {
|
|
3312
3418
|
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3313
3419
|
}
|
|
3420
|
+
const contentLength = response.headers.get("content-length");
|
|
3421
|
+
const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
3422
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
3423
|
+
const sizeMB = Math.round(parseInt(contentLength, 10) / 1024 / 1024);
|
|
3424
|
+
getLogger().debug(`[Im-Nobsidian] \uD30C\uC77C \uC2A4\uD0B5 \u2014 \uB108\uBB34 \uD07C (${sizeMB}MB): ${caption}`);
|
|
3425
|
+
return { originalUrl: url, localPath: "", hash: "", size: 0 };
|
|
3426
|
+
}
|
|
3314
3427
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3315
3428
|
const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
|
|
3316
3429
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
@@ -3469,13 +3582,12 @@ var FileHandler = class {
|
|
|
3469
3582
|
if (folderFiles.length === 0) return [];
|
|
3470
3583
|
const results = [];
|
|
3471
3584
|
for (const file of folderFiles) {
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
if (existing)
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
}
|
|
3585
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3586
|
+
if (existing) {
|
|
3587
|
+
if (existing.fileSize === file.size) continue;
|
|
3588
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3589
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3590
|
+
if (existing.fileHash === currentHash) continue;
|
|
3479
3591
|
}
|
|
3480
3592
|
await this.sema.acquire();
|
|
3481
3593
|
try {
|
|
@@ -3504,19 +3616,20 @@ var FileHandler = class {
|
|
|
3504
3616
|
const folderRecord = folderPath ? this.stateDb.getByPath(folderPath) : this.stateDb.getByPath("__root__");
|
|
3505
3617
|
const folderPageId = folderRecord?.notionPageId;
|
|
3506
3618
|
if (!folderPageId) {
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3619
|
+
if (files.length > 0) {
|
|
3620
|
+
getLogger().debug(
|
|
3621
|
+
`\uD3F4\uB354 \uD398\uC774\uC9C0 \uC5C6\uC74C, \uD30C\uC77C \uC2A4\uD0B5: ${folderPath || "(\uB8E8\uD2B8)"} (${files.length}\uAC1C)`
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3510
3624
|
continue;
|
|
3511
3625
|
}
|
|
3512
3626
|
for (const file of files) {
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
if (existing)
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
}
|
|
3627
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3628
|
+
if (existing) {
|
|
3629
|
+
if (existing.fileSize === file.size) continue;
|
|
3630
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3631
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3632
|
+
if (existing.fileHash === currentHash) continue;
|
|
3520
3633
|
}
|
|
3521
3634
|
await this.sema.acquire();
|
|
3522
3635
|
try {
|
|
@@ -4209,13 +4322,21 @@ var DatabaseSyncer = class {
|
|
|
4209
4322
|
});
|
|
4210
4323
|
}
|
|
4211
4324
|
}
|
|
4212
|
-
getLogger().
|
|
4325
|
+
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4213
4326
|
return { created, updated, failed };
|
|
4214
4327
|
}
|
|
4215
|
-
async pullDatabaseViews(dbConfig) {
|
|
4328
|
+
async pullDatabaseViews(dbConfig, force = false) {
|
|
4216
4329
|
try {
|
|
4217
|
-
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4218
4330
|
const configPath = ".im-nobsidian/db-views.json";
|
|
4331
|
+
if (!force) {
|
|
4332
|
+
try {
|
|
4333
|
+
const existing = await this.vaultFs.readFile(configPath);
|
|
4334
|
+
const parsed = JSON.parse(existing);
|
|
4335
|
+
if (parsed[dbConfig.databaseId]) return;
|
|
4336
|
+
} catch {
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4219
4340
|
await this.vaultFs.ensureFolder(".im-nobsidian");
|
|
4220
4341
|
let allViewsConfigs = {};
|
|
4221
4342
|
try {
|
|
@@ -4225,7 +4346,7 @@ var DatabaseSyncer = class {
|
|
|
4225
4346
|
}
|
|
4226
4347
|
allViewsConfigs[dbConfig.databaseId] = viewsConfig;
|
|
4227
4348
|
await this.vaultFs.writeFile(configPath, JSON.stringify(allViewsConfigs, null, 2));
|
|
4228
|
-
getLogger().
|
|
4349
|
+
getLogger().debug(
|
|
4229
4350
|
`[DB Sync] \uBDF0 \uC124\uC815 ${viewsConfig.views.length}\uAC1C \uC800\uC7A5: ${dbConfig.databaseId}`
|
|
4230
4351
|
);
|
|
4231
4352
|
} catch (error) {
|
|
@@ -4267,15 +4388,32 @@ var DatabaseSyncer = class {
|
|
|
4267
4388
|
} catch {
|
|
4268
4389
|
}
|
|
4269
4390
|
if (this.config.conversion.imageDownload === "immediate" && markdown) {
|
|
4270
|
-
|
|
4271
|
-
|
|
4391
|
+
try {
|
|
4392
|
+
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
4393
|
+
markdown = imageResult.content;
|
|
4394
|
+
} catch (error) {
|
|
4395
|
+
getLogger().warn(`[DB Sync] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${title}):`, error);
|
|
4396
|
+
}
|
|
4272
4397
|
}
|
|
4273
4398
|
if (markdown) {
|
|
4274
|
-
|
|
4275
|
-
|
|
4399
|
+
try {
|
|
4400
|
+
const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
|
|
4401
|
+
markdown = fileResult.content;
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
getLogger().warn(`[DB Sync] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${title}):`, error);
|
|
4404
|
+
}
|
|
4276
4405
|
}
|
|
4277
4406
|
const existingRecord = this.stateDb.getByNotionId(page.id);
|
|
4278
|
-
|
|
4407
|
+
let filePath;
|
|
4408
|
+
if (existingRecord?.obsidianPath) {
|
|
4409
|
+
filePath = existingRecord.obsidianPath;
|
|
4410
|
+
} else {
|
|
4411
|
+
filePath = `${dbConfig.localFolder}/${safeName}.md`;
|
|
4412
|
+
const existingByPath = this.stateDb.getByPath(filePath);
|
|
4413
|
+
if (existingByPath && existingByPath.notionPageId !== page.id) {
|
|
4414
|
+
filePath = `${dbConfig.localFolder}/${safeName} (${page.id.slice(0, 8)}).md`;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4279
4417
|
const finalContent = this.pipeline.convertToMarkdown(
|
|
4280
4418
|
markdown,
|
|
4281
4419
|
{
|
|
@@ -4384,7 +4522,7 @@ var DatabaseSyncer = class {
|
|
|
4384
4522
|
});
|
|
4385
4523
|
}
|
|
4386
4524
|
}
|
|
4387
|
-
getLogger().
|
|
4525
|
+
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4388
4526
|
return { created, updated, failed };
|
|
4389
4527
|
}
|
|
4390
4528
|
};
|
|
@@ -4404,7 +4542,7 @@ function extractTitleFromPath(filePath) {
|
|
|
4404
4542
|
|
|
4405
4543
|
// src/sync/orchestrator.ts
|
|
4406
4544
|
var SyncOrchestrator = class {
|
|
4407
|
-
constructor(config, stateDb, notionClient, vaultFs) {
|
|
4545
|
+
constructor(config, stateDb, notionClient, vaultFs, customFetch) {
|
|
4408
4546
|
this.config = config;
|
|
4409
4547
|
this.stateDb = stateDb;
|
|
4410
4548
|
this.notionClient = notionClient;
|
|
@@ -4414,7 +4552,12 @@ var SyncOrchestrator = class {
|
|
|
4414
4552
|
wikilinkResolver: (text) => stateDb.resolveWikilink(text)
|
|
4415
4553
|
});
|
|
4416
4554
|
this.blockConverter = new BlockConverter();
|
|
4417
|
-
this.imageHandler = new ImageHandler(
|
|
4555
|
+
this.imageHandler = new ImageHandler(
|
|
4556
|
+
vaultFs,
|
|
4557
|
+
config.paths.attachments,
|
|
4558
|
+
notionClient,
|
|
4559
|
+
customFetch
|
|
4560
|
+
);
|
|
4418
4561
|
this.fileHandler = new FileHandler(vaultFs, notionClient, stateDb);
|
|
4419
4562
|
this.propertyMapper = new PropertyMapper();
|
|
4420
4563
|
this.databaseSyncer = new DatabaseSyncer(
|
|
@@ -4451,15 +4594,20 @@ var SyncOrchestrator = class {
|
|
|
4451
4594
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
4452
4595
|
}
|
|
4453
4596
|
this.cleanupInterruptedSync();
|
|
4454
|
-
|
|
4455
|
-
const
|
|
4597
|
+
this.repairFolderRecords();
|
|
4598
|
+
const stats = await this.vaultFs.listMarkdownFileStats();
|
|
4599
|
+
const changes = await this.changeDetector.detectLocalChangesFast(
|
|
4600
|
+
stats,
|
|
4601
|
+
(path) => this.vaultFs.readFile(path)
|
|
4602
|
+
);
|
|
4456
4603
|
const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
|
|
4457
4604
|
const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
|
|
4458
4605
|
const dbFolders = (this.config.notion.databases ?? []).map(
|
|
4459
4606
|
(d) => d.localFolder.endsWith("/") ? d.localFolder : d.localFolder + "/"
|
|
4460
4607
|
);
|
|
4461
4608
|
const isDbPath = (path) => dbFolders.some((prefix) => path.startsWith(prefix));
|
|
4462
|
-
const
|
|
4609
|
+
const excludeSet = options?.excludePaths ? new Set(options.excludePaths) : null;
|
|
4610
|
+
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
4611
|
const hasDbConfigs = (this.config.notion.databases?.length ?? 0) > 0;
|
|
4464
4612
|
if (filtered.length === 0 && !hasDbConfigs) {
|
|
4465
4613
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
@@ -4508,6 +4656,7 @@ var SyncOrchestrator = class {
|
|
|
4508
4656
|
const tasks = filtered.map((change) => async () => {
|
|
4509
4657
|
await sema.acquire();
|
|
4510
4658
|
try {
|
|
4659
|
+
if (options?.signal?.aborted) return;
|
|
4511
4660
|
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4512
4661
|
options?.onProgress?.(++completed, total, { path: change.path, operation: op });
|
|
4513
4662
|
switch (change.type) {
|
|
@@ -4630,11 +4779,37 @@ var SyncOrchestrator = class {
|
|
|
4630
4779
|
const conflicts = [];
|
|
4631
4780
|
const writtenPaths = [];
|
|
4632
4781
|
const failed = [];
|
|
4633
|
-
const
|
|
4782
|
+
const lastPull = this.stateDb.getMeta("last_pull_at") ?? this.stateDb.getMeta("last_sync_at");
|
|
4783
|
+
const trackedCount = this.stateDb.getAll().length;
|
|
4784
|
+
const remoteChanges = !this.isDatabaseMode && lastPull && trackedCount > 0 && !options?.force ? await this.detectRemoteChangesIncremental(lastPull) : await this.detectRemoteChanges();
|
|
4634
4785
|
const filtered = options?.paths ? remoteChanges.filter((c) => {
|
|
4635
4786
|
const record = this.stateDb.getByNotionId(c.pageId);
|
|
4636
4787
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
4637
4788
|
}) : remoteChanges;
|
|
4789
|
+
if (filtered.length === 0 && (this.config.notion.databases?.length ?? 0) === 0) {
|
|
4790
|
+
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
4791
|
+
if (!hasDbResults.created && !hasDbResults.updated) {
|
|
4792
|
+
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4793
|
+
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4794
|
+
this.stateDb.setMeta("pull_in_progress", "");
|
|
4795
|
+
return emptyResult;
|
|
4796
|
+
}
|
|
4797
|
+
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4798
|
+
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4799
|
+
this.stateDb.setMeta("pull_in_progress", "");
|
|
4800
|
+
return {
|
|
4801
|
+
created: hasDbResults.created,
|
|
4802
|
+
updated: hasDbResults.updated,
|
|
4803
|
+
deleted: 0,
|
|
4804
|
+
conflicts: [],
|
|
4805
|
+
writtenPaths,
|
|
4806
|
+
failed,
|
|
4807
|
+
duration: Date.now() - startTime,
|
|
4808
|
+
imageCount: 0,
|
|
4809
|
+
fileCount: 0,
|
|
4810
|
+
linkCount: 0
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4638
4813
|
if (filtered.length === 0) {
|
|
4639
4814
|
return emptyResult;
|
|
4640
4815
|
}
|
|
@@ -4670,15 +4845,14 @@ var SyncOrchestrator = class {
|
|
|
4670
4845
|
const tasks = filtered.map((change) => async () => {
|
|
4671
4846
|
await sema.acquire();
|
|
4672
4847
|
try {
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4676
|
-
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
4848
|
+
if (options?.signal?.aborted) return;
|
|
4849
|
+
let resultPath;
|
|
4677
4850
|
switch (change.type) {
|
|
4678
4851
|
case "created": {
|
|
4679
4852
|
const path = await this.pullCreate(change.pageId);
|
|
4680
4853
|
writtenPaths.push(path);
|
|
4681
4854
|
created++;
|
|
4855
|
+
resultPath = path;
|
|
4682
4856
|
break;
|
|
4683
4857
|
}
|
|
4684
4858
|
case "modified": {
|
|
@@ -4689,16 +4863,24 @@ var SyncOrchestrator = class {
|
|
|
4689
4863
|
writtenPaths.push(result.path);
|
|
4690
4864
|
updated++;
|
|
4691
4865
|
}
|
|
4866
|
+
resultPath = result.path;
|
|
4692
4867
|
break;
|
|
4693
4868
|
}
|
|
4694
4869
|
case "deleted": {
|
|
4870
|
+
const record2 = this.stateDb.getByNotionId(change.pageId);
|
|
4871
|
+
resultPath = record2?.obsidianPath;
|
|
4695
4872
|
const path = await this.pullDelete(change.pageId);
|
|
4696
4873
|
if (path) {
|
|
4697
4874
|
deleted++;
|
|
4875
|
+
resultPath = path;
|
|
4698
4876
|
}
|
|
4699
4877
|
break;
|
|
4700
4878
|
}
|
|
4701
4879
|
}
|
|
4880
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4881
|
+
const displayPath = resultPath ?? record?.obsidianPath ?? change.pageId;
|
|
4882
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
4883
|
+
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
4702
4884
|
} catch (error) {
|
|
4703
4885
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
4704
4886
|
failed.push({
|
|
@@ -4773,44 +4955,13 @@ var SyncOrchestrator = class {
|
|
|
4773
4955
|
getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
|
|
4774
4956
|
}
|
|
4775
4957
|
}
|
|
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
|
-
}
|
|
4958
|
+
{
|
|
4959
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
4960
|
+
created += dbDiscovery.created;
|
|
4961
|
+
updated += dbDiscovery.updated;
|
|
4811
4962
|
}
|
|
4812
|
-
const
|
|
4813
|
-
const linkCount = await this.resolveNotionLinks(
|
|
4963
|
+
const linkTargetPaths = writtenPaths.length > 0 ? writtenPaths : [];
|
|
4964
|
+
const linkCount = linkTargetPaths.length > 0 ? await this.resolveNotionLinks(linkTargetPaths) : 0;
|
|
4814
4965
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4815
4966
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4816
4967
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
@@ -4832,7 +4983,8 @@ var SyncOrchestrator = class {
|
|
|
4832
4983
|
const pullResult = await this.pull(options);
|
|
4833
4984
|
const pushResult = await this.push({
|
|
4834
4985
|
...options,
|
|
4835
|
-
paths: options?.paths
|
|
4986
|
+
paths: options?.paths,
|
|
4987
|
+
excludePaths: pullResult.writtenPaths
|
|
4836
4988
|
});
|
|
4837
4989
|
return {
|
|
4838
4990
|
pull: pullResult,
|
|
@@ -4844,9 +4996,9 @@ var SyncOrchestrator = class {
|
|
|
4844
4996
|
async status() {
|
|
4845
4997
|
const files = await this.vaultFs.listMarkdownFiles();
|
|
4846
4998
|
const localChanges = this.changeDetector.detectLocalChanges(files);
|
|
4847
|
-
const remoteChanges = await this.detectRemoteChanges();
|
|
4848
|
-
const conflictRecords = this.stateDb.getByStatus("conflict");
|
|
4849
4999
|
const lastSyncAt = this.stateDb.getMeta("last_sync_at");
|
|
5000
|
+
const remoteChanges = lastSyncAt ? await this.detectRemoteChangesIncremental(lastSyncAt) : await this.detectRemoteChanges();
|
|
5001
|
+
const conflictRecords = this.stateDb.getByStatus("conflict");
|
|
4850
5002
|
const conflicts = await this.buildConflictsFromRecords(
|
|
4851
5003
|
conflictRecords,
|
|
4852
5004
|
localChanges,
|
|
@@ -4861,6 +5013,34 @@ var SyncOrchestrator = class {
|
|
|
4861
5013
|
lastSyncAt
|
|
4862
5014
|
};
|
|
4863
5015
|
}
|
|
5016
|
+
async statusLocal() {
|
|
5017
|
+
const stats = await this.vaultFs.listMarkdownFileStats();
|
|
5018
|
+
const localChanges = await this.changeDetector.detectLocalChangesFast(
|
|
5019
|
+
stats,
|
|
5020
|
+
(path) => this.vaultFs.readFile(path)
|
|
5021
|
+
);
|
|
5022
|
+
const conflictRecords = this.stateDb.getByStatus("conflict");
|
|
5023
|
+
const lastSyncAt = this.stateDb.getMeta("last_sync_at");
|
|
5024
|
+
return {
|
|
5025
|
+
localChanges,
|
|
5026
|
+
remoteChanges: [],
|
|
5027
|
+
conflicts: [],
|
|
5028
|
+
conflictRecords,
|
|
5029
|
+
pendingOperations: conflictRecords.length,
|
|
5030
|
+
lastSyncAt
|
|
5031
|
+
};
|
|
5032
|
+
}
|
|
5033
|
+
async fetch() {
|
|
5034
|
+
const startTime = Date.now();
|
|
5035
|
+
const changes = await this.detectRemoteChanges();
|
|
5036
|
+
this.stateDb.setMeta("last_fetch_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5037
|
+
return {
|
|
5038
|
+
newPages: changes.filter((c) => c.type === "created").length,
|
|
5039
|
+
modifiedPages: changes.filter((c) => c.type === "modified").length,
|
|
5040
|
+
deletedPages: changes.filter((c) => c.type === "deleted").length,
|
|
5041
|
+
duration: Date.now() - startTime
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
4864
5044
|
async buildConflictsFromRecords(records, localChanges, remoteChanges) {
|
|
4865
5045
|
const conflicts = [];
|
|
4866
5046
|
for (const record of records) {
|
|
@@ -4903,25 +5083,97 @@ var SyncOrchestrator = class {
|
|
|
4903
5083
|
return this.config.notion.parentMode === "database" && !!this.config.notion.databaseId;
|
|
4904
5084
|
}
|
|
4905
5085
|
async discoverChildDatabases() {
|
|
4906
|
-
const rootId = this.config.notion.rootPageId;
|
|
4907
5086
|
const allDbs = [];
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5087
|
+
const trackedPages = this.stateDb.getAll().filter((r) => r.notionPageId);
|
|
5088
|
+
const pageIds = [this.config.notion.rootPageId, ...trackedPages.map((r) => r.notionPageId)];
|
|
5089
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5090
|
+
for (const parentId of pageIds) {
|
|
5091
|
+
if (seen.has(parentId)) continue;
|
|
5092
|
+
seen.add(parentId);
|
|
5093
|
+
try {
|
|
5094
|
+
const dbIds = await this.notionClient.getChildDatabaseIds(parentId);
|
|
5095
|
+
for (const dbId of dbIds) {
|
|
5096
|
+
allDbs.push({ dbId, parentPageId: parentId });
|
|
5097
|
+
}
|
|
5098
|
+
} catch {
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
return allDbs;
|
|
5102
|
+
}
|
|
5103
|
+
async pullDiscoveredDatabases(writtenPaths, failed) {
|
|
5104
|
+
if (this.isDatabaseMode) return { created: 0, updated: 0 };
|
|
5105
|
+
let created = 0;
|
|
5106
|
+
let updated = 0;
|
|
5107
|
+
try {
|
|
5108
|
+
const cachedRaw = this.stateDb.getMeta("discovered_dbs");
|
|
5109
|
+
let dbConfigs = [];
|
|
5110
|
+
if (cachedRaw) {
|
|
4912
5111
|
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));
|
|
5112
|
+
dbConfigs = JSON.parse(cachedRaw);
|
|
4919
5113
|
} catch {
|
|
5114
|
+
dbConfigs = [];
|
|
4920
5115
|
}
|
|
4921
5116
|
}
|
|
4922
|
-
|
|
5117
|
+
if (dbConfigs.length === 0) {
|
|
5118
|
+
const discoveredDbs = await this.discoverChildDatabases();
|
|
5119
|
+
const configuredIds = new Set(
|
|
5120
|
+
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
5121
|
+
);
|
|
5122
|
+
const newDbs = discoveredDbs.filter((db) => !configuredIds.has(db.dbId.replace(/-/g, "")));
|
|
5123
|
+
for (const { dbId, parentPageId } of newDbs) {
|
|
5124
|
+
try {
|
|
5125
|
+
const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
|
|
5126
|
+
const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
|
|
5127
|
+
let parentFolder = "";
|
|
5128
|
+
if (parentEntry?.obsidianPath) {
|
|
5129
|
+
const obsPath = parentEntry.obsidianPath;
|
|
5130
|
+
if (obsPath.endsWith(".md")) {
|
|
5131
|
+
const parts = obsPath.split("/");
|
|
5132
|
+
parts.pop();
|
|
5133
|
+
parentFolder = parts.length > 0 ? parts.join("/") : obsPath.replace(/\.md$/, "");
|
|
5134
|
+
} else {
|
|
5135
|
+
parentFolder = obsPath;
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
let safeName;
|
|
5139
|
+
if (dbTitle) {
|
|
5140
|
+
safeName = dbTitle.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
5141
|
+
} else {
|
|
5142
|
+
const parentName = parentFolder.split("/").pop() || "";
|
|
5143
|
+
safeName = parentName ? `${parentName}-DB` : `db-${dbId.slice(0, 8)}`;
|
|
5144
|
+
}
|
|
5145
|
+
if (!parentFolder) parentFolder = "databases";
|
|
5146
|
+
dbConfigs.push({
|
|
5147
|
+
databaseId: dbId,
|
|
5148
|
+
localFolder: `${parentFolder}/${safeName}`,
|
|
5149
|
+
titleProperty: "Name"
|
|
5150
|
+
});
|
|
5151
|
+
} catch (error) {
|
|
5152
|
+
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
if (dbConfigs.length > 0) {
|
|
5156
|
+
this.stateDb.setMeta("discovered_dbs", JSON.stringify(dbConfigs));
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
5159
|
+
for (const dbConfig of dbConfigs) {
|
|
5160
|
+
try {
|
|
5161
|
+
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
5162
|
+
created += dbResult.created;
|
|
5163
|
+
updated += dbResult.updated;
|
|
5164
|
+
failed.push(...dbResult.failed);
|
|
5165
|
+
if (dbResult.created + dbResult.updated > 0) {
|
|
5166
|
+
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
5167
|
+
writtenPaths.push(...dbPaths.slice(-dbResult.created - dbResult.updated));
|
|
5168
|
+
}
|
|
5169
|
+
} catch (error) {
|
|
5170
|
+
getLogger().warn(`[Im-Nobsidian] DB ${dbConfig.databaseId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
} catch (error) {
|
|
5174
|
+
getLogger().warn("[Im-Nobsidian] child_database \uC790\uB3D9 \uBC1C\uACAC \uC2E4\uD328:", error);
|
|
4923
5175
|
}
|
|
4924
|
-
return
|
|
5176
|
+
return { created, updated };
|
|
4925
5177
|
}
|
|
4926
5178
|
async resolveNotionLinks(paths) {
|
|
4927
5179
|
let totalResolved = 0;
|
|
@@ -5015,6 +5267,7 @@ var SyncOrchestrator = class {
|
|
|
5015
5267
|
);
|
|
5016
5268
|
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
5017
5269
|
const hash = computeHash(content);
|
|
5270
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
5018
5271
|
this.stateDb.transaction(() => {
|
|
5019
5272
|
this.stateDb.upsert({
|
|
5020
5273
|
obsidianPath: path,
|
|
@@ -5026,7 +5279,9 @@ var SyncOrchestrator = class {
|
|
|
5026
5279
|
syncDirection: "both",
|
|
5027
5280
|
fileType: this.isFolderNote(path) ? "folder-note" : "file",
|
|
5028
5281
|
status: "synced",
|
|
5029
|
-
baseSnapshot: Buffer.from(content, "utf-8")
|
|
5282
|
+
baseSnapshot: Buffer.from(content, "utf-8"),
|
|
5283
|
+
localMtime: fileStat?.mtime ?? null,
|
|
5284
|
+
localFileSize: fileStat?.size ?? null
|
|
5030
5285
|
});
|
|
5031
5286
|
const aliases = extractAliases2(conversionResult.properties);
|
|
5032
5287
|
this.stateDb.upsertWikilink({
|
|
@@ -5056,23 +5311,36 @@ var SyncOrchestrator = class {
|
|
|
5056
5311
|
});
|
|
5057
5312
|
await this.pushUpdatePage(record.notionPageId, conversionResult.content, record.baseSnapshot);
|
|
5058
5313
|
await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
|
|
5059
|
-
let propsToUpdate
|
|
5060
|
-
if (this.isDatabaseMode &&
|
|
5314
|
+
let propsToUpdate;
|
|
5315
|
+
if (this.isDatabaseMode && conversionResult.properties && Object.keys(conversionResult.properties).length > 0) {
|
|
5061
5316
|
await this.ensureDbSchema();
|
|
5062
5317
|
const title2 = extractTitle(path);
|
|
5063
|
-
propsToUpdate = this.propertyMapper.toNotionProperties(
|
|
5318
|
+
propsToUpdate = this.propertyMapper.toNotionProperties(conversionResult.properties, title2);
|
|
5319
|
+
} else if (conversionResult.properties?.title) {
|
|
5320
|
+
const titleStr = String(conversionResult.properties.title);
|
|
5321
|
+
propsToUpdate = {
|
|
5322
|
+
title: { title: [{ text: { content: titleStr } }] }
|
|
5323
|
+
};
|
|
5064
5324
|
}
|
|
5325
|
+
let lastEditedTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
5065
5326
|
if (propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
5066
|
-
await this.notionClient.updatePageProperties(
|
|
5327
|
+
const updatedPage = await this.notionClient.updatePageProperties(
|
|
5328
|
+
record.notionPageId,
|
|
5329
|
+
propsToUpdate
|
|
5330
|
+
);
|
|
5331
|
+
lastEditedTime = updatedPage.last_edited_time;
|
|
5067
5332
|
}
|
|
5068
|
-
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
5069
5333
|
const hash = computeHash(content);
|
|
5334
|
+
const fileStat = await this.vaultFs.getFileStat(path);
|
|
5070
5335
|
const title = extractTitle(path);
|
|
5071
5336
|
const aliases = extractAliases2(conversionResult.properties);
|
|
5072
5337
|
this.stateDb.transaction(() => {
|
|
5073
5338
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
5074
5339
|
this.stateDb.updateStatus(record.id, "synced");
|
|
5075
|
-
this.stateDb.setNotionLastEdited(record.id,
|
|
5340
|
+
this.stateDb.setNotionLastEdited(record.id, lastEditedTime);
|
|
5341
|
+
if (fileStat) {
|
|
5342
|
+
this.stateDb.updateStatCache(record.id, fileStat.mtime, fileStat.size);
|
|
5343
|
+
}
|
|
5076
5344
|
this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
|
|
5077
5345
|
this.stateDb.upsertWikilink({
|
|
5078
5346
|
obsidianPath: path,
|
|
@@ -5086,7 +5354,14 @@ var SyncOrchestrator = class {
|
|
|
5086
5354
|
const record = this.stateDb.getByPath(path);
|
|
5087
5355
|
if (!record?.notionPageId) return;
|
|
5088
5356
|
if (this.config.sync.deleteSync) {
|
|
5089
|
-
|
|
5357
|
+
try {
|
|
5358
|
+
await this.notionClient.archivePage(record.notionPageId);
|
|
5359
|
+
} catch (error) {
|
|
5360
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5361
|
+
if (msg.includes("archived ancestor")) ; else {
|
|
5362
|
+
throw error;
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5090
5365
|
this.stateDb.delete(record.id);
|
|
5091
5366
|
} else {
|
|
5092
5367
|
this.stateDb.updateStatus(record.id, "pending");
|
|
@@ -5166,13 +5441,49 @@ var SyncOrchestrator = class {
|
|
|
5166
5441
|
}
|
|
5167
5442
|
return changes;
|
|
5168
5443
|
}
|
|
5444
|
+
async detectRemoteChangesIncremental(since) {
|
|
5445
|
+
const changes = [];
|
|
5446
|
+
const recentPages = await this.notionClient.searchRecentPages(since);
|
|
5447
|
+
for (const page of recentPages) {
|
|
5448
|
+
const record = this.stateDb.getByNotionId(page.id);
|
|
5449
|
+
if (!record) {
|
|
5450
|
+
try {
|
|
5451
|
+
const fullPage = await this.notionClient.getPage(page.id);
|
|
5452
|
+
const parentId = await this.extractParentId(fullPage);
|
|
5453
|
+
if (parentId && this.isTrackedParent(parentId)) {
|
|
5454
|
+
changes.push({
|
|
5455
|
+
pageId: page.id,
|
|
5456
|
+
type: "created",
|
|
5457
|
+
lastEdited: page.last_edited_time,
|
|
5458
|
+
previousEdited: null
|
|
5459
|
+
});
|
|
5460
|
+
}
|
|
5461
|
+
} catch {
|
|
5462
|
+
}
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (page.last_edited_time !== record.notionLastEdited) {
|
|
5466
|
+
changes.push({
|
|
5467
|
+
pageId: page.id,
|
|
5468
|
+
type: "modified",
|
|
5469
|
+
lastEdited: page.last_edited_time,
|
|
5470
|
+
previousEdited: record.notionLastEdited
|
|
5471
|
+
});
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
return changes;
|
|
5475
|
+
}
|
|
5476
|
+
isTrackedParent(parentId) {
|
|
5477
|
+
if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return true;
|
|
5478
|
+
return !!this.stateDb.getByNotionId(parentId);
|
|
5479
|
+
}
|
|
5169
5480
|
async pullCreate(pageId) {
|
|
5170
5481
|
const page = await this.notionClient.getPage(pageId);
|
|
5171
5482
|
const title = this.notionClient.extractTitle(page);
|
|
5172
5483
|
const safeName = sanitizeFileName(title);
|
|
5173
5484
|
const parentPath = await this.resolveParentPath(page);
|
|
5174
|
-
const
|
|
5175
|
-
const hasChildPages =
|
|
5485
|
+
const firstChildren = await this.notionClient.listChildren(pageId, { pageSize: 100 });
|
|
5486
|
+
const hasChildPages = firstChildren.results.some((b) => "type" in b && b.type === "child_page");
|
|
5176
5487
|
const markdown = await this.fetchPageMarkdown(pageId);
|
|
5177
5488
|
const hasContent = markdown.trim().length > 0;
|
|
5178
5489
|
let filePath;
|
|
@@ -5223,6 +5534,7 @@ var SyncOrchestrator = class {
|
|
|
5223
5534
|
);
|
|
5224
5535
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
5225
5536
|
const hash = computeHash(finalContent);
|
|
5537
|
+
const pullStat = await this.vaultFs.getFileStat(filePath);
|
|
5226
5538
|
const resolvedParentId = await this.extractParentId(page);
|
|
5227
5539
|
this.stateDb.transaction(() => {
|
|
5228
5540
|
this.stateDb.upsert({
|
|
@@ -5235,7 +5547,9 @@ var SyncOrchestrator = class {
|
|
|
5235
5547
|
syncDirection: "both",
|
|
5236
5548
|
fileType,
|
|
5237
5549
|
status: "synced",
|
|
5238
|
-
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
5550
|
+
baseSnapshot: Buffer.from(finalContent, "utf-8"),
|
|
5551
|
+
localMtime: pullStat?.mtime ?? null,
|
|
5552
|
+
localFileSize: pullStat?.size ?? null
|
|
5239
5553
|
});
|
|
5240
5554
|
const aliases = extractAliases2(properties);
|
|
5241
5555
|
this.stateDb.upsertWikilink({
|
|
@@ -5313,6 +5627,7 @@ var SyncOrchestrator = class {
|
|
|
5313
5627
|
}
|
|
5314
5628
|
}
|
|
5315
5629
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
5630
|
+
const updateStat = await this.vaultFs.getFileStat(record.obsidianPath);
|
|
5316
5631
|
const newHash = computeHash(remoteContent);
|
|
5317
5632
|
this.stateDb.upsert({
|
|
5318
5633
|
obsidianPath: record.obsidianPath,
|
|
@@ -5324,7 +5639,9 @@ var SyncOrchestrator = class {
|
|
|
5324
5639
|
syncDirection: record.syncDirection,
|
|
5325
5640
|
fileType: record.fileType,
|
|
5326
5641
|
status: "synced",
|
|
5327
|
-
baseSnapshot: Buffer.from(remoteContent, "utf-8")
|
|
5642
|
+
baseSnapshot: Buffer.from(remoteContent, "utf-8"),
|
|
5643
|
+
localMtime: updateStat?.mtime ?? null,
|
|
5644
|
+
localFileSize: updateStat?.size ?? null
|
|
5328
5645
|
});
|
|
5329
5646
|
return { path: record.obsidianPath };
|
|
5330
5647
|
}
|
|
@@ -5341,31 +5658,27 @@ var SyncOrchestrator = class {
|
|
|
5341
5658
|
return record.obsidianPath;
|
|
5342
5659
|
}
|
|
5343
5660
|
async ensureFolderPage(folderPath) {
|
|
5344
|
-
const existing = this.stateDb.getByPath(folderPath);
|
|
5345
|
-
if (existing?.notionPageId) return;
|
|
5346
5661
|
const folderNotePath = `${folderPath}/${folderPath.split("/").pop()}.md`;
|
|
5347
5662
|
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
|
-
});
|
|
5663
|
+
if (folderNoteRecord?.notionPageId) {
|
|
5664
|
+
const existing2 = this.stateDb.getByPath(folderPath);
|
|
5665
|
+
if (existing2) this.stateDb.delete(existing2.id);
|
|
5360
5666
|
return;
|
|
5361
5667
|
}
|
|
5668
|
+
const existing = this.stateDb.getByPath(folderPath);
|
|
5669
|
+
if (existing?.notionPageId) return;
|
|
5362
5670
|
const parts = folderPath.split("/");
|
|
5363
5671
|
const folderName = parts[parts.length - 1];
|
|
5364
5672
|
let parentId = this.config.notion.rootPageId;
|
|
5365
5673
|
if (parts.length > 1) {
|
|
5366
5674
|
const parentPath = parts.slice(0, -1).join("/");
|
|
5675
|
+
await this.ensureFolderPage(parentPath);
|
|
5676
|
+
const parentNotePath = `${parentPath}/${parentPath.split("/").pop()}.md`;
|
|
5677
|
+
const parentNote = this.stateDb.getByPath(parentNotePath);
|
|
5367
5678
|
const parentRecord = this.stateDb.getByPath(parentPath);
|
|
5368
|
-
if (
|
|
5679
|
+
if (parentNote?.notionPageId) {
|
|
5680
|
+
parentId = parentNote.notionPageId;
|
|
5681
|
+
} else if (parentRecord?.notionPageId) {
|
|
5369
5682
|
parentId = parentRecord.notionPageId;
|
|
5370
5683
|
}
|
|
5371
5684
|
}
|
|
@@ -5392,8 +5705,26 @@ var SyncOrchestrator = class {
|
|
|
5392
5705
|
const folderPath = parts.slice(0, -1).join("/");
|
|
5393
5706
|
const folderRecord = this.stateDb.getByPath(folderPath);
|
|
5394
5707
|
if (folderRecord?.notionPageId) return folderRecord.notionPageId;
|
|
5708
|
+
const folderName = parts[parts.length - 2];
|
|
5709
|
+
const folderNotePath = `${folderPath}/${folderName}.md`;
|
|
5710
|
+
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5711
|
+
if (folderNoteRecord?.notionPageId) return folderNoteRecord.notionPageId;
|
|
5395
5712
|
return this.config.notion.rootPageId;
|
|
5396
5713
|
}
|
|
5714
|
+
repairFolderRecords() {
|
|
5715
|
+
const allRecords = this.stateDb.getAll();
|
|
5716
|
+
const folderRecords = allRecords.filter(
|
|
5717
|
+
(r) => r.fileType === "folder-note" && !r.obsidianPath.endsWith(".md")
|
|
5718
|
+
);
|
|
5719
|
+
for (const folder of folderRecords) {
|
|
5720
|
+
const folderName = folder.obsidianPath.split("/").pop();
|
|
5721
|
+
const folderNotePath = `${folder.obsidianPath}/${folderName}.md`;
|
|
5722
|
+
const noteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5723
|
+
if (noteRecord?.notionPageId) {
|
|
5724
|
+
this.stateDb.delete(folder.id);
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5397
5728
|
isFolderNote(filePath) {
|
|
5398
5729
|
const parts = filePath.split("/");
|
|
5399
5730
|
if (parts.length < 2) return false;
|
|
@@ -5428,17 +5759,14 @@ var SyncOrchestrator = class {
|
|
|
5428
5759
|
}
|
|
5429
5760
|
async pushCreatePage(parentId, parentType, title, markdownContent, properties) {
|
|
5430
5761
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
});
|
|
5440
|
-
} catch {
|
|
5441
|
-
}
|
|
5762
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5763
|
+
return await this.notionClient.createPageWithMarkdown({
|
|
5764
|
+
parentId,
|
|
5765
|
+
parentType,
|
|
5766
|
+
title,
|
|
5767
|
+
markdown: enhanced,
|
|
5768
|
+
properties
|
|
5769
|
+
});
|
|
5442
5770
|
}
|
|
5443
5771
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5444
5772
|
const page = await this.notionClient.createPage({
|
|
@@ -5452,27 +5780,11 @@ var SyncOrchestrator = class {
|
|
|
5452
5780
|
}
|
|
5453
5781
|
return page;
|
|
5454
5782
|
}
|
|
5455
|
-
async pushUpdatePage(pageId, markdownContent,
|
|
5783
|
+
async pushUpdatePage(pageId, markdownContent, _baseSnapshot) {
|
|
5456
5784
|
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
|
-
}
|
|
5785
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
5786
|
+
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
5787
|
+
return;
|
|
5476
5788
|
}
|
|
5477
5789
|
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
5478
5790
|
const existingBlocks = await this.notionClient.fetchAllChildren(pageId);
|
|
@@ -5491,51 +5803,6 @@ var SyncOrchestrator = class {
|
|
|
5491
5803
|
})
|
|
5492
5804
|
);
|
|
5493
5805
|
}
|
|
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
5806
|
async fetchPageMarkdown(pageId) {
|
|
5540
5807
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
5541
5808
|
try {
|
|
@@ -5673,6 +5940,20 @@ var NodeVaultFS = class {
|
|
|
5673
5940
|
await this.walkDir(this.rootPath, files);
|
|
5674
5941
|
return files;
|
|
5675
5942
|
}
|
|
5943
|
+
async listMarkdownFileStats() {
|
|
5944
|
+
const stats = [];
|
|
5945
|
+
await this.walkDirStats(this.rootPath, stats);
|
|
5946
|
+
return stats;
|
|
5947
|
+
}
|
|
5948
|
+
async getFileStat(path) {
|
|
5949
|
+
try {
|
|
5950
|
+
const fullPath = join(this.rootPath, path);
|
|
5951
|
+
const fileStat = await stat(fullPath);
|
|
5952
|
+
return { path, mtime: fileStat.mtime.toISOString(), size: fileStat.size };
|
|
5953
|
+
} catch {
|
|
5954
|
+
return null;
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5676
5957
|
async listNonMarkdownFiles() {
|
|
5677
5958
|
const files = [];
|
|
5678
5959
|
await this.walkDirNonMd(this.rootPath, files);
|
|
@@ -5719,6 +6000,25 @@ var NodeVaultFS = class {
|
|
|
5719
6000
|
}
|
|
5720
6001
|
}
|
|
5721
6002
|
}
|
|
6003
|
+
async walkDirStats(dir, result) {
|
|
6004
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
6005
|
+
for (const entry of entries) {
|
|
6006
|
+
const fullPath = join(dir, entry.name);
|
|
6007
|
+
if (entry.name.startsWith(".")) continue;
|
|
6008
|
+
const relativePath = relative(this.rootPath, fullPath);
|
|
6009
|
+
if (this.isExcluded(relativePath)) continue;
|
|
6010
|
+
if (entry.isDirectory()) {
|
|
6011
|
+
await this.walkDirStats(fullPath, result);
|
|
6012
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
6013
|
+
const fileStat = await stat(fullPath);
|
|
6014
|
+
result.push({
|
|
6015
|
+
path: relativePath,
|
|
6016
|
+
mtime: fileStat.mtime.toISOString(),
|
|
6017
|
+
size: fileStat.size
|
|
6018
|
+
});
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
5722
6022
|
async walkDir(dir, result) {
|
|
5723
6023
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
5724
6024
|
for (const entry of entries) {
|
|
@@ -6307,6 +6607,59 @@ function extractGroupName(val) {
|
|
|
6307
6607
|
if (typeof val === "object" && "start" in val) return val.start;
|
|
6308
6608
|
return null;
|
|
6309
6609
|
}
|
|
6610
|
+
function filterEntries(entries, searchQuery, filters) {
|
|
6611
|
+
let result = entries;
|
|
6612
|
+
if (searchQuery && searchQuery.trim()) {
|
|
6613
|
+
const q = searchQuery.trim().toLowerCase();
|
|
6614
|
+
result = result.filter((entry) => {
|
|
6615
|
+
if (entry.title.toLowerCase().includes(q)) return true;
|
|
6616
|
+
return Object.values(entry.properties).some((val) => {
|
|
6617
|
+
if (val === null || val === void 0) return false;
|
|
6618
|
+
if (typeof val === "string") return val.toLowerCase().includes(q);
|
|
6619
|
+
if (Array.isArray(val)) return val.some((v) => v.toLowerCase().includes(q));
|
|
6620
|
+
if (typeof val === "object" && "start" in val) return val.start.includes(q);
|
|
6621
|
+
return String(val).toLowerCase().includes(q);
|
|
6622
|
+
});
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
if (filters && filters.length > 0) {
|
|
6626
|
+
for (const filter of filters) {
|
|
6627
|
+
result = result.filter((entry) => matchFilter(entry, filter));
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
return result;
|
|
6631
|
+
}
|
|
6632
|
+
function matchFilter(entry, filter) {
|
|
6633
|
+
const val = filter.property === "title" ? entry.title : entry.properties[filter.property] ?? null;
|
|
6634
|
+
switch (filter.operator) {
|
|
6635
|
+
case "isEmpty":
|
|
6636
|
+
return val === null || val === void 0 || val === "" || Array.isArray(val) && val.length === 0;
|
|
6637
|
+
case "isNotEmpty":
|
|
6638
|
+
return val !== null && val !== void 0 && val !== "" && !(Array.isArray(val) && val.length === 0);
|
|
6639
|
+
case "equals":
|
|
6640
|
+
if (typeof val === "boolean") return val === filter.value;
|
|
6641
|
+
return String(val ?? "") === String(filter.value ?? "");
|
|
6642
|
+
case "contains": {
|
|
6643
|
+
const needle = String(filter.value ?? "").toLowerCase();
|
|
6644
|
+
if (typeof val === "string") return val.toLowerCase().includes(needle);
|
|
6645
|
+
if (Array.isArray(val)) return val.some((v) => v.toLowerCase().includes(needle));
|
|
6646
|
+
return String(val ?? "").toLowerCase().includes(needle);
|
|
6647
|
+
}
|
|
6648
|
+
case "startsWith":
|
|
6649
|
+
return String(val ?? "").startsWith(String(filter.value ?? ""));
|
|
6650
|
+
case "gt":
|
|
6651
|
+
return typeof val === "number" && typeof filter.value === "number" && val > filter.value;
|
|
6652
|
+
case "lt":
|
|
6653
|
+
return typeof val === "number" && typeof filter.value === "number" && val < filter.value;
|
|
6654
|
+
case "between": {
|
|
6655
|
+
if (typeof val !== "number" || typeof filter.value !== "number" || typeof filter.valueTo !== "number")
|
|
6656
|
+
return false;
|
|
6657
|
+
return val >= filter.value && val <= filter.valueTo;
|
|
6658
|
+
}
|
|
6659
|
+
default:
|
|
6660
|
+
return true;
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6310
6663
|
function parseDate(value) {
|
|
6311
6664
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value);
|
|
6312
6665
|
return match?.[1] ?? null;
|
|
@@ -6518,6 +6871,6 @@ ${lines.join("\n")}
|
|
|
6518
6871
|
}`;
|
|
6519
6872
|
}
|
|
6520
6873
|
|
|
6521
|
-
export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
|
|
6874
|
+
export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
|
|
6522
6875
|
//# sourceMappingURL=index.js.map
|
|
6523
6876
|
//# sourceMappingURL=index.js.map
|