@im-nobsidian/core 0.1.2 → 0.1.4
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 +174 -5
- package/dist/index.js +936 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,13 @@ import { readFileSync } from 'fs';
|
|
|
12
12
|
import { watch } from 'chokidar';
|
|
13
13
|
|
|
14
14
|
// src/types/config.ts
|
|
15
|
+
var DatabaseSyncSchema = z.object({
|
|
16
|
+
databaseId: z.string(),
|
|
17
|
+
localFolder: z.string(),
|
|
18
|
+
propertyMapping: z.record(z.string()).optional(),
|
|
19
|
+
pullFilter: z.unknown().optional(),
|
|
20
|
+
titleProperty: z.string().default("Name")
|
|
21
|
+
});
|
|
15
22
|
var ConfigSchema = z.object({
|
|
16
23
|
version: z.literal(1),
|
|
17
24
|
notion: z.object({
|
|
@@ -19,14 +26,16 @@ var ConfigSchema = z.object({
|
|
|
19
26
|
rootPageId: z.string(),
|
|
20
27
|
workspaceId: z.string().optional(),
|
|
21
28
|
parentMode: z.enum(["page", "database"]).default("page"),
|
|
22
|
-
databaseId: z.string().optional()
|
|
29
|
+
databaseId: z.string().optional(),
|
|
30
|
+
databases: z.array(DatabaseSyncSchema).default([])
|
|
23
31
|
}),
|
|
24
32
|
sync: z.object({
|
|
25
33
|
direction: z.enum(["push", "pull", "both"]).default("both"),
|
|
26
34
|
conflictStrategy: z.enum(["local-first", "remote-first", "manual", "duplicate"]).default("manual"),
|
|
27
35
|
autoSync: z.boolean().default(false),
|
|
28
36
|
autoSyncInterval: z.number().min(30).max(3600).default(300),
|
|
29
|
-
deleteSync: z.boolean().default(false)
|
|
37
|
+
deleteSync: z.boolean().default(false),
|
|
38
|
+
syncFiles: z.boolean().default(true)
|
|
30
39
|
}),
|
|
31
40
|
paths: z.object({
|
|
32
41
|
include: z.array(z.string()).default(["**/*"]),
|
|
@@ -51,14 +60,16 @@ var DEFAULT_CONFIG = {
|
|
|
51
60
|
notion: {
|
|
52
61
|
token: "",
|
|
53
62
|
rootPageId: "",
|
|
54
|
-
parentMode: "page"
|
|
63
|
+
parentMode: "page",
|
|
64
|
+
databases: []
|
|
55
65
|
},
|
|
56
66
|
sync: {
|
|
57
67
|
direction: "both",
|
|
58
68
|
conflictStrategy: "manual",
|
|
59
69
|
autoSync: false,
|
|
60
70
|
autoSyncInterval: 300,
|
|
61
|
-
deleteSync: false
|
|
71
|
+
deleteSync: false,
|
|
72
|
+
syncFiles: true
|
|
62
73
|
},
|
|
63
74
|
paths: {
|
|
64
75
|
include: ["**/*"],
|
|
@@ -257,6 +268,28 @@ CREATE TABLE IF NOT EXISTS sync_metadata (
|
|
|
257
268
|
INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('schema_version', '1');
|
|
258
269
|
`;
|
|
259
270
|
|
|
271
|
+
// src/state/migrations/002-file-registry.ts
|
|
272
|
+
var FILE_REGISTRY_MIGRATION = `
|
|
273
|
+
CREATE TABLE IF NOT EXISTS file_registry (
|
|
274
|
+
id TEXT PRIMARY KEY,
|
|
275
|
+
local_path TEXT NOT NULL,
|
|
276
|
+
notion_page_id TEXT NOT NULL,
|
|
277
|
+
file_upload_id TEXT NOT NULL,
|
|
278
|
+
file_type TEXT NOT NULL DEFAULT 'file',
|
|
279
|
+
file_hash TEXT NOT NULL,
|
|
280
|
+
file_size INTEGER NOT NULL DEFAULT 0,
|
|
281
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
282
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
283
|
+
|
|
284
|
+
CONSTRAINT chk_file_type CHECK (file_type IN ('image', 'pdf', 'video', 'audio', 'file'))
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_file_registry_path ON file_registry(local_path);
|
|
288
|
+
CREATE INDEX IF NOT EXISTS idx_file_registry_page ON file_registry(notion_page_id);
|
|
289
|
+
|
|
290
|
+
UPDATE sync_metadata SET value = '2' WHERE key = 'schema_version';
|
|
291
|
+
`;
|
|
292
|
+
|
|
260
293
|
// src/state/state-db.ts
|
|
261
294
|
var StateDB = class _StateDB {
|
|
262
295
|
db;
|
|
@@ -276,6 +309,9 @@ var StateDB = class _StateDB {
|
|
|
276
309
|
if (version < 1) {
|
|
277
310
|
this.db.exec(INITIAL_MIGRATION);
|
|
278
311
|
}
|
|
312
|
+
if (version < 2) {
|
|
313
|
+
this.db.exec(FILE_REGISTRY_MIGRATION);
|
|
314
|
+
}
|
|
279
315
|
}
|
|
280
316
|
getSchemaVersion() {
|
|
281
317
|
try {
|
|
@@ -371,6 +407,11 @@ var StateDB = class _StateDB {
|
|
|
371
407
|
"UPDATE sync_state SET notion_last_edited = ?, updated_at = datetime('now') WHERE id = ?"
|
|
372
408
|
).run(lastEdited, id);
|
|
373
409
|
}
|
|
410
|
+
setNotionParentId(id, parentId) {
|
|
411
|
+
this.db.prepare(
|
|
412
|
+
"UPDATE sync_state SET notion_parent_id = ?, updated_at = datetime('now') WHERE id = ?"
|
|
413
|
+
).run(parentId, id);
|
|
414
|
+
}
|
|
374
415
|
delete(id) {
|
|
375
416
|
this.db.prepare("DELETE FROM sync_state WHERE id = ?").run(id);
|
|
376
417
|
}
|
|
@@ -401,6 +442,49 @@ var StateDB = class _StateDB {
|
|
|
401
442
|
setMeta(key, value) {
|
|
402
443
|
this.db.prepare("INSERT OR REPLACE INTO sync_metadata (key, value) VALUES (?, ?)").run(key, value);
|
|
403
444
|
}
|
|
445
|
+
// --- file_registry ---
|
|
446
|
+
isFileRegistered(localPath) {
|
|
447
|
+
const row = this.db.prepare("SELECT 1 FROM file_registry WHERE local_path = ?").get(localPath);
|
|
448
|
+
return !!row;
|
|
449
|
+
}
|
|
450
|
+
getFileRegistry(localPath) {
|
|
451
|
+
const row = this.db.prepare("SELECT * FROM file_registry WHERE local_path = ?").get(localPath);
|
|
452
|
+
return row ? this.mapFileRegistryRow(row) : null;
|
|
453
|
+
}
|
|
454
|
+
getFilesByPageId(notionPageId) {
|
|
455
|
+
const rows = this.db.prepare("SELECT * FROM file_registry WHERE notion_page_id = ?").all(notionPageId);
|
|
456
|
+
return rows.map((r) => this.mapFileRegistryRow(r));
|
|
457
|
+
}
|
|
458
|
+
registerFile(entry) {
|
|
459
|
+
const id = generateId();
|
|
460
|
+
this.db.prepare(
|
|
461
|
+
`INSERT OR REPLACE INTO file_registry
|
|
462
|
+
(id, local_path, notion_page_id, file_upload_id, file_type, file_hash, file_size)
|
|
463
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
464
|
+
).run(
|
|
465
|
+
id,
|
|
466
|
+
entry.localPath,
|
|
467
|
+
entry.notionPageId,
|
|
468
|
+
entry.fileUploadId,
|
|
469
|
+
entry.fileType,
|
|
470
|
+
entry.fileHash,
|
|
471
|
+
entry.fileSize
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
deleteFileRegistry(localPath) {
|
|
475
|
+
this.db.prepare("DELETE FROM file_registry WHERE local_path = ?").run(localPath);
|
|
476
|
+
}
|
|
477
|
+
mapFileRegistryRow(row) {
|
|
478
|
+
return {
|
|
479
|
+
id: row.id,
|
|
480
|
+
localPath: row.local_path,
|
|
481
|
+
notionPageId: row.notion_page_id,
|
|
482
|
+
fileUploadId: row.file_upload_id,
|
|
483
|
+
fileType: row.file_type,
|
|
484
|
+
fileHash: row.file_hash,
|
|
485
|
+
fileSize: row.file_size
|
|
486
|
+
};
|
|
487
|
+
}
|
|
404
488
|
// --- preserve markers ---
|
|
405
489
|
storePreserveMarkers(path, markers) {
|
|
406
490
|
if (markers.length === 0) {
|
|
@@ -568,6 +652,15 @@ var PropertyMapper = class {
|
|
|
568
652
|
});
|
|
569
653
|
return { files: filesArr };
|
|
570
654
|
}
|
|
655
|
+
case "created_time":
|
|
656
|
+
case "last_edited_time":
|
|
657
|
+
case "created_by":
|
|
658
|
+
case "last_edited_by":
|
|
659
|
+
case "formula":
|
|
660
|
+
case "rollup":
|
|
661
|
+
case "unique_id":
|
|
662
|
+
case "verification":
|
|
663
|
+
return null;
|
|
571
664
|
default:
|
|
572
665
|
return { rich_text: [{ text: { content: String(value) } }] };
|
|
573
666
|
}
|
|
@@ -760,6 +853,32 @@ var NotionClient = class {
|
|
|
760
853
|
})
|
|
761
854
|
);
|
|
762
855
|
}
|
|
856
|
+
async updatePageMarkdownPartial(pageId, patches) {
|
|
857
|
+
return this.withRateLimit(
|
|
858
|
+
() => this.client.pages.updateMarkdown({
|
|
859
|
+
page_id: pageId,
|
|
860
|
+
type: "update_content",
|
|
861
|
+
update_content: {
|
|
862
|
+
content_updates: patches.map((p) => ({
|
|
863
|
+
old_str: p.oldStr,
|
|
864
|
+
new_str: p.newStr,
|
|
865
|
+
replace_all_matches: p.replaceAll ?? false
|
|
866
|
+
})),
|
|
867
|
+
allow_deleting_content: true
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
// ─── Page Move ───
|
|
873
|
+
async movePage(pageId, newParentId, newParentType) {
|
|
874
|
+
const parent = newParentType === "database" ? { database_id: newParentId } : { page_id: newParentId };
|
|
875
|
+
return this.withRateLimit(
|
|
876
|
+
() => this.client.pages.update({
|
|
877
|
+
page_id: pageId,
|
|
878
|
+
parent
|
|
879
|
+
})
|
|
880
|
+
);
|
|
881
|
+
}
|
|
763
882
|
// ─── Database / DataSource ───
|
|
764
883
|
async getDatabaseSchema(databaseId) {
|
|
765
884
|
const db = await this.withRateLimit(
|
|
@@ -777,7 +896,8 @@ var NotionClient = class {
|
|
|
777
896
|
() => this.client.dataSources.query({
|
|
778
897
|
data_source_id: databaseId,
|
|
779
898
|
start_cursor: options?.startCursor,
|
|
780
|
-
page_size: options?.pageSize ?? 100
|
|
899
|
+
page_size: options?.pageSize ?? 100,
|
|
900
|
+
filter: options?.filter
|
|
781
901
|
})
|
|
782
902
|
);
|
|
783
903
|
return {
|
|
@@ -785,6 +905,27 @@ var NotionClient = class {
|
|
|
785
905
|
nextCursor: response.next_cursor
|
|
786
906
|
};
|
|
787
907
|
}
|
|
908
|
+
async queryAllDatabasePages(databaseId, filter) {
|
|
909
|
+
const all = [];
|
|
910
|
+
let cursor;
|
|
911
|
+
do {
|
|
912
|
+
const response = await this.queryDatabase(databaseId, {
|
|
913
|
+
startCursor: cursor,
|
|
914
|
+
pageSize: 100,
|
|
915
|
+
filter
|
|
916
|
+
});
|
|
917
|
+
all.push(...response.results);
|
|
918
|
+
cursor = response.nextCursor ?? void 0;
|
|
919
|
+
} while (cursor);
|
|
920
|
+
return all;
|
|
921
|
+
}
|
|
922
|
+
async getDataSourceId(databaseId) {
|
|
923
|
+
const db = await this.withRateLimit(
|
|
924
|
+
() => this.client.databases.retrieve({ database_id: databaseId })
|
|
925
|
+
);
|
|
926
|
+
const dataSources = db.data_sources;
|
|
927
|
+
return dataSources?.[0]?.id ?? databaseId;
|
|
928
|
+
}
|
|
788
929
|
async archivePage(pageId) {
|
|
789
930
|
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, archived: true }));
|
|
790
931
|
}
|
|
@@ -833,15 +974,18 @@ var NotionClient = class {
|
|
|
833
974
|
() => this.client.fileUploads.create({ filename, content_type: contentType })
|
|
834
975
|
);
|
|
835
976
|
const fileUploadId = upload.id;
|
|
836
|
-
await this.withRateLimit(
|
|
977
|
+
const sendResult = await this.withRateLimit(
|
|
837
978
|
() => this.client.fileUploads.send({
|
|
838
979
|
file_upload_id: fileUploadId,
|
|
839
980
|
file: { data: fileData, filename }
|
|
840
981
|
})
|
|
841
982
|
);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
983
|
+
const status = sendResult.status;
|
|
984
|
+
if (status === "pending") {
|
|
985
|
+
await this.withRateLimit(
|
|
986
|
+
() => this.client.fileUploads.complete({ file_upload_id: fileUploadId })
|
|
987
|
+
);
|
|
988
|
+
}
|
|
845
989
|
return fileUploadId;
|
|
846
990
|
}
|
|
847
991
|
// ─── Search & Navigation ───
|
|
@@ -1854,7 +1998,10 @@ var FrontmatterGenerator = class {
|
|
|
1854
1998
|
function normalizeProperties(props) {
|
|
1855
1999
|
const result = {};
|
|
1856
2000
|
for (const [key, value] of Object.entries(props)) {
|
|
1857
|
-
|
|
2001
|
+
const normalized = normalizeValue(value);
|
|
2002
|
+
if (normalized !== void 0) {
|
|
2003
|
+
result[key] = normalized;
|
|
2004
|
+
}
|
|
1858
2005
|
}
|
|
1859
2006
|
return result;
|
|
1860
2007
|
}
|
|
@@ -1862,6 +2009,10 @@ function normalizeValue(value) {
|
|
|
1862
2009
|
if (typeof value === "string") {
|
|
1863
2010
|
return normalizeDate(value);
|
|
1864
2011
|
}
|
|
2012
|
+
if (Array.isArray(value)) {
|
|
2013
|
+
if (value.length === 0) return void 0;
|
|
2014
|
+
return value;
|
|
2015
|
+
}
|
|
1865
2016
|
if (typeof value === "object" && value !== null && "start" in value) {
|
|
1866
2017
|
const dateObj = value;
|
|
1867
2018
|
const normalized = {
|
|
@@ -1872,12 +2023,10 @@ function normalizeValue(value) {
|
|
|
1872
2023
|
}
|
|
1873
2024
|
return value;
|
|
1874
2025
|
}
|
|
2026
|
+
var MIDNIGHT_RE = /T00:00:00(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})/;
|
|
1875
2027
|
function normalizeDate(dateStr) {
|
|
1876
2028
|
if (typeof dateStr !== "string") return dateStr;
|
|
1877
|
-
if (
|
|
1878
|
-
return dateStr.split("T")[0] ?? dateStr;
|
|
1879
|
-
}
|
|
1880
|
-
if (dateStr.match(/T00:00:00[Z+]/)) {
|
|
2029
|
+
if (MIDNIGHT_RE.test(dateStr)) {
|
|
1881
2030
|
return dateStr.split("T")[0] ?? dateStr;
|
|
1882
2031
|
}
|
|
1883
2032
|
return dateStr;
|
|
@@ -2810,6 +2959,265 @@ var ImageHandler = class {
|
|
|
2810
2959
|
return ext ? `.${ext[1].toLowerCase()}` : null;
|
|
2811
2960
|
}
|
|
2812
2961
|
};
|
|
2962
|
+
var EXTENSION_TO_BLOCK_TYPE = {
|
|
2963
|
+
".png": "image",
|
|
2964
|
+
".jpg": "image",
|
|
2965
|
+
".jpeg": "image",
|
|
2966
|
+
".gif": "image",
|
|
2967
|
+
".svg": "image",
|
|
2968
|
+
".webp": "image",
|
|
2969
|
+
".ico": "image",
|
|
2970
|
+
".bmp": "image",
|
|
2971
|
+
".tiff": "image",
|
|
2972
|
+
".tif": "image",
|
|
2973
|
+
".avif": "image",
|
|
2974
|
+
".apng": "image",
|
|
2975
|
+
".heic": "image",
|
|
2976
|
+
".pdf": "pdf",
|
|
2977
|
+
".mp4": "video",
|
|
2978
|
+
".mov": "video",
|
|
2979
|
+
".webm": "video",
|
|
2980
|
+
".avi": "video",
|
|
2981
|
+
".mkv": "video",
|
|
2982
|
+
".flv": "video",
|
|
2983
|
+
".wmv": "video",
|
|
2984
|
+
".m4v": "video",
|
|
2985
|
+
".mpeg": "video",
|
|
2986
|
+
".ogv": "video",
|
|
2987
|
+
".3gp": "video",
|
|
2988
|
+
".mp3": "audio",
|
|
2989
|
+
".wav": "audio",
|
|
2990
|
+
".ogg": "audio",
|
|
2991
|
+
".m4a": "audio",
|
|
2992
|
+
".flac": "audio",
|
|
2993
|
+
".aac": "audio",
|
|
2994
|
+
".wma": "audio",
|
|
2995
|
+
".opus": "audio",
|
|
2996
|
+
".weba": "audio"
|
|
2997
|
+
};
|
|
2998
|
+
var EXTENSION_TO_MIME2 = {
|
|
2999
|
+
".png": "image/png",
|
|
3000
|
+
".jpg": "image/jpeg",
|
|
3001
|
+
".jpeg": "image/jpeg",
|
|
3002
|
+
".gif": "image/gif",
|
|
3003
|
+
".webp": "image/webp",
|
|
3004
|
+
".svg": "image/svg+xml",
|
|
3005
|
+
".bmp": "image/bmp",
|
|
3006
|
+
".ico": "image/x-icon",
|
|
3007
|
+
".tiff": "image/tiff",
|
|
3008
|
+
".tif": "image/tiff",
|
|
3009
|
+
".avif": "image/avif",
|
|
3010
|
+
".heic": "image/heic",
|
|
3011
|
+
".pdf": "application/pdf",
|
|
3012
|
+
".mp4": "video/mp4",
|
|
3013
|
+
".mov": "video/quicktime",
|
|
3014
|
+
".webm": "video/webm",
|
|
3015
|
+
".avi": "video/x-msvideo",
|
|
3016
|
+
".mkv": "video/x-matroska",
|
|
3017
|
+
".mp3": "audio/mpeg",
|
|
3018
|
+
".wav": "audio/wav",
|
|
3019
|
+
".ogg": "audio/ogg",
|
|
3020
|
+
".m4a": "audio/mp4",
|
|
3021
|
+
".flac": "audio/flac",
|
|
3022
|
+
".aac": "audio/aac",
|
|
3023
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3024
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3025
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
3026
|
+
".zip": "application/zip",
|
|
3027
|
+
".gz": "application/gzip",
|
|
3028
|
+
".tar": "application/x-tar",
|
|
3029
|
+
".rar": "application/vnd.rar",
|
|
3030
|
+
".7z": "application/x-7z-compressed",
|
|
3031
|
+
".py": "text/x-python",
|
|
3032
|
+
".js": "text/javascript",
|
|
3033
|
+
".ts": "text/typescript",
|
|
3034
|
+
".json": "application/json",
|
|
3035
|
+
".csv": "text/csv",
|
|
3036
|
+
".txt": "text/plain",
|
|
3037
|
+
".html": "text/html",
|
|
3038
|
+
".xml": "application/xml",
|
|
3039
|
+
".hwp": "application/x-hwp",
|
|
3040
|
+
".xls": "application/vnd.ms-excel",
|
|
3041
|
+
".doc": "application/msword",
|
|
3042
|
+
".ppt": "application/vnd.ms-powerpoint"
|
|
3043
|
+
};
|
|
3044
|
+
function getBlockType(filename) {
|
|
3045
|
+
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
3046
|
+
if (!ext) return "file";
|
|
3047
|
+
return EXTENSION_TO_BLOCK_TYPE[ext] ?? "file";
|
|
3048
|
+
}
|
|
3049
|
+
function getMimeType(filename) {
|
|
3050
|
+
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
3051
|
+
if (!ext) return "application/octet-stream";
|
|
3052
|
+
return EXTENSION_TO_MIME2[ext] ?? "application/octet-stream";
|
|
3053
|
+
}
|
|
3054
|
+
function getFolderPath(filePath) {
|
|
3055
|
+
const parts = filePath.split("/");
|
|
3056
|
+
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
3057
|
+
}
|
|
3058
|
+
var FileHandler = class {
|
|
3059
|
+
constructor(vaultFs, notionClient, stateDb) {
|
|
3060
|
+
this.vaultFs = vaultFs;
|
|
3061
|
+
this.notionClient = notionClient;
|
|
3062
|
+
this.stateDb = stateDb;
|
|
3063
|
+
}
|
|
3064
|
+
vaultFs;
|
|
3065
|
+
notionClient;
|
|
3066
|
+
stateDb;
|
|
3067
|
+
sema = new Sema(2);
|
|
3068
|
+
async pushFilesForFolder(folderPageId, folderPath) {
|
|
3069
|
+
const allFiles = await this.vaultFs.listNonMarkdownFiles();
|
|
3070
|
+
const folderFiles = allFiles.filter((f) => getFolderPath(f.path) === folderPath);
|
|
3071
|
+
if (folderFiles.length === 0) return [];
|
|
3072
|
+
const results = [];
|
|
3073
|
+
for (const file of folderFiles) {
|
|
3074
|
+
if (this.stateDb.isFileRegistered(file.path)) {
|
|
3075
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3076
|
+
if (existing) {
|
|
3077
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3078
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3079
|
+
if (existing.fileHash === currentHash) continue;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
await this.sema.acquire();
|
|
3083
|
+
try {
|
|
3084
|
+
const result = await this.uploadFile(file, folderPageId);
|
|
3085
|
+
if (result) results.push(result);
|
|
3086
|
+
} catch (error) {
|
|
3087
|
+
getLogger().warn(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${file.path}):`, error);
|
|
3088
|
+
} finally {
|
|
3089
|
+
this.sema.release();
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
return results;
|
|
3093
|
+
}
|
|
3094
|
+
async pushAllFiles() {
|
|
3095
|
+
const allFiles = await this.vaultFs.listNonMarkdownFiles();
|
|
3096
|
+
if (allFiles.length === 0) return [];
|
|
3097
|
+
const filesByFolder = /* @__PURE__ */ new Map();
|
|
3098
|
+
for (const file of allFiles) {
|
|
3099
|
+
const folder = getFolderPath(file.path);
|
|
3100
|
+
const list = filesByFolder.get(folder) ?? [];
|
|
3101
|
+
list.push(file);
|
|
3102
|
+
filesByFolder.set(folder, list);
|
|
3103
|
+
}
|
|
3104
|
+
const results = [];
|
|
3105
|
+
for (const [folderPath, files] of filesByFolder) {
|
|
3106
|
+
const folderRecord = folderPath ? this.stateDb.getByPath(folderPath) : this.stateDb.getByPath("__root__");
|
|
3107
|
+
const folderPageId = folderRecord?.notionPageId;
|
|
3108
|
+
if (!folderPageId) {
|
|
3109
|
+
getLogger().warn(
|
|
3110
|
+
`\uD3F4\uB354 \uD398\uC774\uC9C0 \uC5C6\uC74C, \uD30C\uC77C \uC2A4\uD0B5: ${folderPath || "(\uB8E8\uD2B8)"} (${files.length}\uAC1C)`
|
|
3111
|
+
);
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
for (const file of files) {
|
|
3115
|
+
if (this.stateDb.isFileRegistered(file.path)) {
|
|
3116
|
+
const existing = this.stateDb.getFileRegistry(file.path);
|
|
3117
|
+
if (existing) {
|
|
3118
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3119
|
+
const currentHash = createHash("sha256").update(buffer).digest("hex");
|
|
3120
|
+
if (existing.fileHash === currentHash) continue;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
await this.sema.acquire();
|
|
3124
|
+
try {
|
|
3125
|
+
const result = await this.uploadFile(file, folderPageId);
|
|
3126
|
+
if (result) results.push(result);
|
|
3127
|
+
} catch (error) {
|
|
3128
|
+
getLogger().warn(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${file.path}):`, error);
|
|
3129
|
+
} finally {
|
|
3130
|
+
this.sema.release();
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
return results;
|
|
3135
|
+
}
|
|
3136
|
+
async downloadFileBlock(url, filename, targetFolder) {
|
|
3137
|
+
const response = await fetch(url);
|
|
3138
|
+
if (!response.ok) {
|
|
3139
|
+
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3140
|
+
}
|
|
3141
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3142
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
3143
|
+
const localPath = targetFolder ? `${targetFolder}/${filename}` : filename;
|
|
3144
|
+
await this.vaultFs.ensureFolder(targetFolder || ".");
|
|
3145
|
+
await this.vaultFs.writeBinary(localPath, buffer);
|
|
3146
|
+
return { url, localPath, hash, size: buffer.length };
|
|
3147
|
+
}
|
|
3148
|
+
async downloadFileBlocks(blocks, targetFolder) {
|
|
3149
|
+
const results = [];
|
|
3150
|
+
for (const block of blocks) {
|
|
3151
|
+
const filename = this.extractFilename(block.url, block.caption);
|
|
3152
|
+
if (this.stateDb.isFileRegistered(`${targetFolder}/${filename}`)) continue;
|
|
3153
|
+
await this.sema.acquire();
|
|
3154
|
+
try {
|
|
3155
|
+
const result = await this.downloadFileBlock(block.url, filename, targetFolder);
|
|
3156
|
+
results.push(result);
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${block.url}):`, error);
|
|
3159
|
+
} finally {
|
|
3160
|
+
this.sema.release();
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
return results;
|
|
3164
|
+
}
|
|
3165
|
+
async uploadFile(file, parentPageId) {
|
|
3166
|
+
const buffer = await this.vaultFs.readBinary(file.path);
|
|
3167
|
+
const filename = file.path.split("/").pop() ?? "file";
|
|
3168
|
+
const contentType = getMimeType(filename);
|
|
3169
|
+
const blockType = getBlockType(filename);
|
|
3170
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
3171
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
3172
|
+
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
3173
|
+
const block = this.createFileBlock(blockType, fileUploadId);
|
|
3174
|
+
await this.notionClient.appendChildren(parentPageId, [block]);
|
|
3175
|
+
this.stateDb.registerFile({
|
|
3176
|
+
localPath: file.path,
|
|
3177
|
+
notionPageId: parentPageId,
|
|
3178
|
+
fileUploadId,
|
|
3179
|
+
fileType: blockType,
|
|
3180
|
+
fileHash: hash,
|
|
3181
|
+
fileSize: file.size
|
|
3182
|
+
});
|
|
3183
|
+
getLogger().info(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC: ${file.path} \u2192 ${blockType} \uBE14\uB85D`);
|
|
3184
|
+
return { localPath: file.path, fileUploadId, blockType };
|
|
3185
|
+
}
|
|
3186
|
+
createFileBlock(blockType, fileUploadId) {
|
|
3187
|
+
const fileRef = {
|
|
3188
|
+
type: "file_upload",
|
|
3189
|
+
file_upload: { id: fileUploadId }
|
|
3190
|
+
};
|
|
3191
|
+
switch (blockType) {
|
|
3192
|
+
case "image":
|
|
3193
|
+
return { type: "image", image: fileRef };
|
|
3194
|
+
case "pdf":
|
|
3195
|
+
return { type: "pdf", pdf: fileRef };
|
|
3196
|
+
case "video":
|
|
3197
|
+
return { type: "video", video: fileRef };
|
|
3198
|
+
case "audio":
|
|
3199
|
+
return { type: "audio", audio: fileRef };
|
|
3200
|
+
case "file":
|
|
3201
|
+
default:
|
|
3202
|
+
return { type: "file", file: fileRef };
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
extractFilename(url, caption) {
|
|
3206
|
+
if (caption) {
|
|
3207
|
+
const safe = caption.replace(/[^a-zA-Z0-9가-힣._\s-]/g, "").trim();
|
|
3208
|
+
if (safe) return safe;
|
|
3209
|
+
}
|
|
3210
|
+
try {
|
|
3211
|
+
const pathname = new URL(url).pathname;
|
|
3212
|
+
const segments = pathname.split("/");
|
|
3213
|
+
const last = segments[segments.length - 1];
|
|
3214
|
+
if (last && last.includes(".")) return decodeURIComponent(last);
|
|
3215
|
+
} catch {
|
|
3216
|
+
}
|
|
3217
|
+
const ext = url.match(/\.(png|jpg|jpeg|gif|pdf|mp4|mp3|docx|xlsx|zip|webp)(\?|$)/i);
|
|
3218
|
+
return `file-${Date.now()}${ext ? `.${ext[1]}` : ""}`;
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
2813
3221
|
|
|
2814
3222
|
// src/utils/sanitize.ts
|
|
2815
3223
|
var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
@@ -2855,7 +3263,13 @@ var NOTION_TOGGLE_RE = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/d
|
|
|
2855
3263
|
var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
|
|
2856
3264
|
var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
|
|
2857
3265
|
var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
|
|
2858
|
-
var NOTION_UNKNOWN_RE = /<unknown id="[^"]*"[^>]
|
|
3266
|
+
var NOTION_UNKNOWN_RE = /<unknown id="([^"]*)"([^>]*)\/>/g;
|
|
3267
|
+
var NOTION_AUDIO_RE = /<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
|
|
3268
|
+
var NOTION_VIDEO_RE = /<video src="([^"]*)">([\s\S]*?)<\/video>/g;
|
|
3269
|
+
var NOTION_PDF_RE = /<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
|
|
3270
|
+
var NOTION_FILE_RE = /<file src="([^"]*)">([\s\S]*?)<\/file>/g;
|
|
3271
|
+
var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
|
|
3272
|
+
var NOTION_UNDERLINE_RE = /<span underline="true">([\s\S]*?)<\/span>/g;
|
|
2859
3273
|
var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
|
|
2860
3274
|
var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
|
|
2861
3275
|
function notionEnhancedToObsidian(enhanced) {
|
|
@@ -2866,10 +3280,13 @@ function notionEnhancedToObsidian(enhanced) {
|
|
|
2866
3280
|
result = convertPageLinks(result);
|
|
2867
3281
|
result = convertUserMentions(result);
|
|
2868
3282
|
result = convertDateMentions(result);
|
|
2869
|
-
result =
|
|
3283
|
+
result = convertMediaTags(result);
|
|
3284
|
+
result = convertTabBlocks(result);
|
|
3285
|
+
result = preserveUnknownBlocks(result);
|
|
2870
3286
|
result = convertNotionMath(result);
|
|
2871
3287
|
result = convertNotionTables(result);
|
|
2872
3288
|
result = convertColorSpans(result);
|
|
3289
|
+
result = convertUnderlineSpans(result);
|
|
2873
3290
|
result = removeEmptyBlocks(result);
|
|
2874
3291
|
result = unescapePipes(result);
|
|
2875
3292
|
return result;
|
|
@@ -2877,7 +3294,12 @@ function notionEnhancedToObsidian(enhanced) {
|
|
|
2877
3294
|
function obsidianToNotionEnhanced(obsidian) {
|
|
2878
3295
|
let result = obsidian;
|
|
2879
3296
|
result = convertTogglesToHtml(result);
|
|
3297
|
+
result = restoreTabBlocks(result);
|
|
2880
3298
|
result = convertObsidianCallouts(result);
|
|
3299
|
+
result = restoreMediaTags(result);
|
|
3300
|
+
result = restoreUnknownBlocks(result);
|
|
3301
|
+
result = restoreColorSpans(result);
|
|
3302
|
+
result = restoreUnderlineSpans(result);
|
|
2881
3303
|
return result;
|
|
2882
3304
|
}
|
|
2883
3305
|
function convertToggles(content) {
|
|
@@ -2922,8 +3344,46 @@ function convertDateMentions(content) {
|
|
|
2922
3344
|
return end ? `${start} \u2192 ${end}` : start;
|
|
2923
3345
|
});
|
|
2924
3346
|
}
|
|
2925
|
-
function
|
|
2926
|
-
return content.replace(NOTION_UNKNOWN_RE,
|
|
3347
|
+
function preserveUnknownBlocks(content) {
|
|
3348
|
+
return content.replace(NOTION_UNKNOWN_RE, (_match, id, attrs) => {
|
|
3349
|
+
const typeMatch = /type="([^"]*)"/.exec(attrs);
|
|
3350
|
+
const altMatch = /alt="([^"]*)"/.exec(attrs);
|
|
3351
|
+
const blockType = altMatch?.[1] ?? typeMatch?.[1] ?? "unknown";
|
|
3352
|
+
return `%%im-nobsidian:unknown:id=${id}&type=${blockType}%%`;
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
function convertMediaTags(content) {
|
|
3356
|
+
let result = content;
|
|
3357
|
+
result = result.replace(NOTION_AUDIO_RE, (_match, src, caption) => {
|
|
3358
|
+
const cap = caption.trim();
|
|
3359
|
+
return cap ? `[\u{1F50A} ${cap}](${src})` : `[\u{1F50A} audio](${src})`;
|
|
3360
|
+
});
|
|
3361
|
+
result = result.replace(NOTION_VIDEO_RE, (_match, src, caption) => {
|
|
3362
|
+
const cap = caption.trim();
|
|
3363
|
+
return cap ? `[\u{1F3AC} ${cap}](${src})` : `[\u{1F3AC} video](${src})`;
|
|
3364
|
+
});
|
|
3365
|
+
result = result.replace(NOTION_PDF_RE, (_match, src, caption) => {
|
|
3366
|
+
const cap = caption.trim();
|
|
3367
|
+
return cap ? `[\u{1F4C4} ${cap}](${src})` : `[\u{1F4C4} pdf](${src})`;
|
|
3368
|
+
});
|
|
3369
|
+
result = result.replace(NOTION_FILE_RE, (_match, src, caption) => {
|
|
3370
|
+
const cap = caption.trim();
|
|
3371
|
+
return cap ? `[\u{1F4CE} ${cap}](${src})` : `[\u{1F4CE} file](${src})`;
|
|
3372
|
+
});
|
|
3373
|
+
return result;
|
|
3374
|
+
}
|
|
3375
|
+
function convertTabBlocks(content) {
|
|
3376
|
+
return content.replace(NOTION_TAB_RE, (_match, title, body) => {
|
|
3377
|
+
const trimmed = body.trim();
|
|
3378
|
+
const indented = trimmed.split("\n").map((line) => `> ${line}`).join("\n");
|
|
3379
|
+
return `> [!tab] ${title}
|
|
3380
|
+
${indented}`;
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
function convertUnderlineSpans(content) {
|
|
3384
|
+
return content.replace(NOTION_UNDERLINE_RE, (_match, text) => {
|
|
3385
|
+
return `%%im-nobsidian:underline%%${text}%%/underline%%`;
|
|
3386
|
+
});
|
|
2927
3387
|
}
|
|
2928
3388
|
function convertTogglesToHtml(content) {
|
|
2929
3389
|
const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
|
|
@@ -3033,7 +3493,10 @@ function convertPageLinks(content) {
|
|
|
3033
3493
|
});
|
|
3034
3494
|
}
|
|
3035
3495
|
function convertColorSpans(content) {
|
|
3036
|
-
return content.replace(
|
|
3496
|
+
return content.replace(
|
|
3497
|
+
NOTION_COLOR_SPAN_RE,
|
|
3498
|
+
(_match, color, text) => `%%im-nobsidian:color:${color}%%${text}%%/color%%`
|
|
3499
|
+
);
|
|
3037
3500
|
}
|
|
3038
3501
|
function removeEmptyBlocks(content) {
|
|
3039
3502
|
return content.replace(NOTION_EMPTY_BLOCK_RE, "\n");
|
|
@@ -3080,6 +3543,51 @@ function convertNotionTables(content) {
|
|
|
3080
3543
|
return lines.join("\n");
|
|
3081
3544
|
});
|
|
3082
3545
|
}
|
|
3546
|
+
var OBSIDIAN_MEDIA_AUDIO_RE = /\[🔊\s*([^\]]*)\]\(([^)]+)\)/g;
|
|
3547
|
+
var OBSIDIAN_MEDIA_VIDEO_RE = /\[🎬\s*([^\]]*)\]\(([^)]+)\)/g;
|
|
3548
|
+
var OBSIDIAN_MEDIA_PDF_RE = /\[📄\s*([^\]]*)\]\(([^)]+)\)/g;
|
|
3549
|
+
var OBSIDIAN_MEDIA_FILE_RE = /\[📎\s*([^\]]*)\]\(([^)]+)\)/g;
|
|
3550
|
+
function restoreMediaTags(content) {
|
|
3551
|
+
let result = content;
|
|
3552
|
+
result = result.replace(OBSIDIAN_MEDIA_AUDIO_RE, (_match, caption, src) => {
|
|
3553
|
+
return `<audio src="${src}">${caption}</audio>`;
|
|
3554
|
+
});
|
|
3555
|
+
result = result.replace(OBSIDIAN_MEDIA_VIDEO_RE, (_match, caption, src) => {
|
|
3556
|
+
return `<video src="${src}">${caption}</video>`;
|
|
3557
|
+
});
|
|
3558
|
+
result = result.replace(OBSIDIAN_MEDIA_PDF_RE, (_match, caption, src) => {
|
|
3559
|
+
return `<pdf src="${src}">${caption}</pdf>`;
|
|
3560
|
+
});
|
|
3561
|
+
result = result.replace(OBSIDIAN_MEDIA_FILE_RE, (_match, caption, src) => {
|
|
3562
|
+
return `<file src="${src}">${caption}</file>`;
|
|
3563
|
+
});
|
|
3564
|
+
return result;
|
|
3565
|
+
}
|
|
3566
|
+
var OBSIDIAN_TAB_RE = /^> \[!tab\]\s*(.+)\n((?:> .*\n?)*)/gm;
|
|
3567
|
+
function restoreTabBlocks(content) {
|
|
3568
|
+
return content.replace(OBSIDIAN_TAB_RE, (_match, title, body) => {
|
|
3569
|
+
const unquoted = body.split("\n").map((line) => line.replace(/^> /, "")).join("\n").trim();
|
|
3570
|
+
return `<tab title="${title}">${unquoted}</tab>`;
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
var OBSIDIAN_UNKNOWN_RE = /%%im-nobsidian:unknown:id=([^&]+)&type=([^%]+)%%/g;
|
|
3574
|
+
function restoreUnknownBlocks(content) {
|
|
3575
|
+
return content.replace(OBSIDIAN_UNKNOWN_RE, (_match, id, type) => {
|
|
3576
|
+
return `<unknown id="${id}" type="${type}"/>`;
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
var OBSIDIAN_COLOR_RE = /%%im-nobsidian:color:([^%]+)%%([\s\S]*?)%%\/color%%/g;
|
|
3580
|
+
function restoreColorSpans(content) {
|
|
3581
|
+
return content.replace(OBSIDIAN_COLOR_RE, (_match, color, text) => {
|
|
3582
|
+
return `<span color="${color}">${text}</span>`;
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
var OBSIDIAN_UNDERLINE_RE = /%%im-nobsidian:underline%%([\s\S]*?)%%\/underline%%/g;
|
|
3586
|
+
function restoreUnderlineSpans(content) {
|
|
3587
|
+
return content.replace(OBSIDIAN_UNDERLINE_RE, (_match, text) => {
|
|
3588
|
+
return `<span underline="true">${text}</span>`;
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3083
3591
|
function unescapePipes(content) {
|
|
3084
3592
|
const lines = content.split("\n");
|
|
3085
3593
|
const result = [];
|
|
@@ -3099,6 +3607,253 @@ function unescapePipes(content) {
|
|
|
3099
3607
|
}
|
|
3100
3608
|
return result.join("\n");
|
|
3101
3609
|
}
|
|
3610
|
+
var DatabaseSyncer = class {
|
|
3611
|
+
constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler) {
|
|
3612
|
+
this.config = config;
|
|
3613
|
+
this.stateDb = stateDb;
|
|
3614
|
+
this.notionClient = notionClient;
|
|
3615
|
+
this.vaultFs = vaultFs;
|
|
3616
|
+
this.pipeline = pipeline;
|
|
3617
|
+
this.imageHandler = imageHandler;
|
|
3618
|
+
this.propertyMapper.setWikilinkResolver({
|
|
3619
|
+
resolve: (title) => stateDb.resolveWikilink(title)?.notionPageId ?? null,
|
|
3620
|
+
resolvePageId: (pageId) => stateDb.resolvePageId(pageId)?.title ?? null
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
config;
|
|
3624
|
+
stateDb;
|
|
3625
|
+
notionClient;
|
|
3626
|
+
vaultFs;
|
|
3627
|
+
pipeline;
|
|
3628
|
+
imageHandler;
|
|
3629
|
+
propertyMapper = new PropertyMapper();
|
|
3630
|
+
async pullAll() {
|
|
3631
|
+
const databases = this.config.notion.databases;
|
|
3632
|
+
if (!databases || databases.length === 0) {
|
|
3633
|
+
return { created: 0, updated: 0, failed: [] };
|
|
3634
|
+
}
|
|
3635
|
+
let created = 0;
|
|
3636
|
+
let updated = 0;
|
|
3637
|
+
const failed = [];
|
|
3638
|
+
for (const dbConfig of databases) {
|
|
3639
|
+
try {
|
|
3640
|
+
const result = await this.pullDatabase(dbConfig);
|
|
3641
|
+
created += result.created;
|
|
3642
|
+
updated += result.updated;
|
|
3643
|
+
failed.push(...result.failed);
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} pull \uC2E4\uD328:`, error);
|
|
3646
|
+
failed.push({
|
|
3647
|
+
path: dbConfig.localFolder,
|
|
3648
|
+
operation: "create",
|
|
3649
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return { created, updated, failed };
|
|
3654
|
+
}
|
|
3655
|
+
async pushAll() {
|
|
3656
|
+
const databases = this.config.notion.databases;
|
|
3657
|
+
if (!databases || databases.length === 0) {
|
|
3658
|
+
return { created: 0, updated: 0, failed: [] };
|
|
3659
|
+
}
|
|
3660
|
+
let created = 0;
|
|
3661
|
+
let updated = 0;
|
|
3662
|
+
const failed = [];
|
|
3663
|
+
for (const dbConfig of databases) {
|
|
3664
|
+
try {
|
|
3665
|
+
const result = await this.pushDatabase(dbConfig);
|
|
3666
|
+
created += result.created;
|
|
3667
|
+
updated += result.updated;
|
|
3668
|
+
failed.push(...result.failed);
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} push \uC2E4\uD328:`, error);
|
|
3671
|
+
failed.push({
|
|
3672
|
+
path: dbConfig.localFolder,
|
|
3673
|
+
operation: "create",
|
|
3674
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
return { created, updated, failed };
|
|
3679
|
+
}
|
|
3680
|
+
async pullDatabase(dbConfig) {
|
|
3681
|
+
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
3682
|
+
this.propertyMapper.loadSchema(schema);
|
|
3683
|
+
await this.vaultFs.ensureFolder(dbConfig.localFolder);
|
|
3684
|
+
const pages = await this.notionClient.queryAllDatabasePages(
|
|
3685
|
+
dbConfig.databaseId,
|
|
3686
|
+
dbConfig.pullFilter
|
|
3687
|
+
);
|
|
3688
|
+
let created = 0;
|
|
3689
|
+
let updated = 0;
|
|
3690
|
+
const failed = [];
|
|
3691
|
+
for (const page of pages) {
|
|
3692
|
+
try {
|
|
3693
|
+
const record = this.stateDb.getByNotionId(page.id);
|
|
3694
|
+
if (record) {
|
|
3695
|
+
if (page.last_edited_time === record.notionLastEdited) continue;
|
|
3696
|
+
await this.pullDatabasePage(page, dbConfig);
|
|
3697
|
+
updated++;
|
|
3698
|
+
} else {
|
|
3699
|
+
await this.pullDatabasePage(page, dbConfig);
|
|
3700
|
+
created++;
|
|
3701
|
+
}
|
|
3702
|
+
} catch (error) {
|
|
3703
|
+
failed.push({
|
|
3704
|
+
path: `${dbConfig.localFolder}/${page.id}`,
|
|
3705
|
+
operation: "create",
|
|
3706
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
getLogger().info(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
3711
|
+
return { created, updated, failed };
|
|
3712
|
+
}
|
|
3713
|
+
async pullDatabasePage(page, dbConfig) {
|
|
3714
|
+
const title = this.notionClient.extractTitle(page);
|
|
3715
|
+
const safeName = sanitizeFileName(title);
|
|
3716
|
+
const properties = this.propertyMapper.fromNotionProperties(
|
|
3717
|
+
page.properties
|
|
3718
|
+
);
|
|
3719
|
+
properties.title = title;
|
|
3720
|
+
let markdown = "";
|
|
3721
|
+
try {
|
|
3722
|
+
const mdResult = await this.notionClient.getPageMarkdown(page.id);
|
|
3723
|
+
markdown = notionEnhancedToObsidian(mdResult.markdown);
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
if (this.config.conversion.imageDownload === "immediate" && markdown) {
|
|
3727
|
+
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
3728
|
+
markdown = imageResult.content;
|
|
3729
|
+
}
|
|
3730
|
+
const existingRecord = this.stateDb.getByNotionId(page.id);
|
|
3731
|
+
const filePath = existingRecord?.obsidianPath ?? `${dbConfig.localFolder}/${safeName}.md`;
|
|
3732
|
+
const finalContent = this.pipeline.convertToMarkdown(
|
|
3733
|
+
markdown,
|
|
3734
|
+
{
|
|
3735
|
+
direction: "pull",
|
|
3736
|
+
path: "markdown-api",
|
|
3737
|
+
filePath,
|
|
3738
|
+
parentMode: "database"
|
|
3739
|
+
},
|
|
3740
|
+
{ properties }
|
|
3741
|
+
);
|
|
3742
|
+
await this.vaultFs.ensureFolder(dbConfig.localFolder);
|
|
3743
|
+
await this.vaultFs.writeFile(filePath, finalContent);
|
|
3744
|
+
const hash = computeHash(finalContent);
|
|
3745
|
+
this.stateDb.transaction(() => {
|
|
3746
|
+
this.stateDb.upsert({
|
|
3747
|
+
obsidianPath: filePath,
|
|
3748
|
+
notionPageId: page.id,
|
|
3749
|
+
notionParentId: dbConfig.databaseId,
|
|
3750
|
+
contentHash: hash,
|
|
3751
|
+
notionLastEdited: page.last_edited_time,
|
|
3752
|
+
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3753
|
+
syncDirection: "both",
|
|
3754
|
+
fileType: "db-row",
|
|
3755
|
+
status: "synced",
|
|
3756
|
+
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
3757
|
+
});
|
|
3758
|
+
this.stateDb.upsertWikilink({
|
|
3759
|
+
obsidianPath: filePath,
|
|
3760
|
+
notionPageId: page.id,
|
|
3761
|
+
title,
|
|
3762
|
+
aliases: extractAliases(properties)
|
|
3763
|
+
});
|
|
3764
|
+
});
|
|
3765
|
+
return filePath;
|
|
3766
|
+
}
|
|
3767
|
+
async pushDatabase(dbConfig) {
|
|
3768
|
+
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
3769
|
+
this.propertyMapper.loadSchema(schema);
|
|
3770
|
+
const allFiles = await this.vaultFs.listMarkdownFiles();
|
|
3771
|
+
const prefix = dbConfig.localFolder.endsWith("/") ? dbConfig.localFolder : dbConfig.localFolder + "/";
|
|
3772
|
+
const dbFiles = allFiles.filter((f) => f.path.startsWith(prefix));
|
|
3773
|
+
let created = 0;
|
|
3774
|
+
let updated = 0;
|
|
3775
|
+
const failed = [];
|
|
3776
|
+
for (const file of dbFiles) {
|
|
3777
|
+
try {
|
|
3778
|
+
const content = await this.vaultFs.readFile(file.path);
|
|
3779
|
+
const hash = computeHash(content);
|
|
3780
|
+
const record = this.stateDb.getByPath(file.path);
|
|
3781
|
+
if (record?.contentHash === hash) continue;
|
|
3782
|
+
const parsed = matter(content);
|
|
3783
|
+
const frontmatter = parsed.data;
|
|
3784
|
+
const body = parsed.content;
|
|
3785
|
+
const title = frontmatter.title ?? extractTitleFromPath(file.path);
|
|
3786
|
+
delete frontmatter.title;
|
|
3787
|
+
const notionProps = this.propertyMapper.toNotionProperties(frontmatter, title);
|
|
3788
|
+
const enhanced = obsidianToNotionEnhanced(body.trim());
|
|
3789
|
+
if (record?.notionPageId) {
|
|
3790
|
+
await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
|
|
3791
|
+
try {
|
|
3792
|
+
await this.notionClient.replacePageMarkdown(record.notionPageId, enhanced);
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3795
|
+
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
3796
|
+
this.stateDb.transaction(() => {
|
|
3797
|
+
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
3798
|
+
this.stateDb.updateStatus(record.id, "synced");
|
|
3799
|
+
this.stateDb.setNotionLastEdited(record.id, updatedPage.last_edited_time);
|
|
3800
|
+
});
|
|
3801
|
+
updated++;
|
|
3802
|
+
} else {
|
|
3803
|
+
const page = await this.notionClient.createPageWithMarkdown({
|
|
3804
|
+
parentId: dbConfig.databaseId,
|
|
3805
|
+
parentType: "database",
|
|
3806
|
+
title,
|
|
3807
|
+
markdown: enhanced,
|
|
3808
|
+
properties: notionProps
|
|
3809
|
+
});
|
|
3810
|
+
this.stateDb.transaction(() => {
|
|
3811
|
+
this.stateDb.upsert({
|
|
3812
|
+
obsidianPath: file.path,
|
|
3813
|
+
notionPageId: page.id,
|
|
3814
|
+
notionParentId: dbConfig.databaseId,
|
|
3815
|
+
contentHash: hash,
|
|
3816
|
+
notionLastEdited: page.last_edited_time,
|
|
3817
|
+
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3818
|
+
syncDirection: "both",
|
|
3819
|
+
fileType: "db-row",
|
|
3820
|
+
status: "synced",
|
|
3821
|
+
baseSnapshot: Buffer.from(content, "utf-8")
|
|
3822
|
+
});
|
|
3823
|
+
this.stateDb.upsertWikilink({
|
|
3824
|
+
obsidianPath: file.path,
|
|
3825
|
+
notionPageId: page.id,
|
|
3826
|
+
title,
|
|
3827
|
+
aliases: extractAliases(frontmatter)
|
|
3828
|
+
});
|
|
3829
|
+
});
|
|
3830
|
+
created++;
|
|
3831
|
+
}
|
|
3832
|
+
} catch (error) {
|
|
3833
|
+
failed.push({
|
|
3834
|
+
path: file.path,
|
|
3835
|
+
operation: "create",
|
|
3836
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
getLogger().info(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
3841
|
+
return { created, updated, failed };
|
|
3842
|
+
}
|
|
3843
|
+
};
|
|
3844
|
+
function extractAliases(properties) {
|
|
3845
|
+
const raw = properties.aliases ?? properties.alias;
|
|
3846
|
+
if (!raw) return [];
|
|
3847
|
+
if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
|
|
3848
|
+
if (typeof raw === "string")
|
|
3849
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3850
|
+
return [];
|
|
3851
|
+
}
|
|
3852
|
+
function extractTitleFromPath(filePath) {
|
|
3853
|
+
const parts = filePath.split("/");
|
|
3854
|
+
const filename = parts[parts.length - 1] ?? "";
|
|
3855
|
+
return filename.replace(/\.md$/, "");
|
|
3856
|
+
}
|
|
3102
3857
|
|
|
3103
3858
|
// src/sync/orchestrator.ts
|
|
3104
3859
|
var SyncOrchestrator = class {
|
|
@@ -3113,7 +3868,16 @@ var SyncOrchestrator = class {
|
|
|
3113
3868
|
});
|
|
3114
3869
|
this.blockConverter = new BlockConverter();
|
|
3115
3870
|
this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments, notionClient);
|
|
3871
|
+
this.fileHandler = new FileHandler(vaultFs, notionClient, stateDb);
|
|
3116
3872
|
this.propertyMapper = new PropertyMapper();
|
|
3873
|
+
this.databaseSyncer = new DatabaseSyncer(
|
|
3874
|
+
config,
|
|
3875
|
+
stateDb,
|
|
3876
|
+
notionClient,
|
|
3877
|
+
vaultFs,
|
|
3878
|
+
this.pipeline,
|
|
3879
|
+
this.imageHandler
|
|
3880
|
+
);
|
|
3117
3881
|
this.propertyMapper.setWikilinkResolver({
|
|
3118
3882
|
resolve: (title) => stateDb.resolveWikilink(title)?.notionPageId ?? null,
|
|
3119
3883
|
resolvePageId: (pageId) => stateDb.resolvePageId(pageId)?.title ?? null
|
|
@@ -3128,6 +3892,8 @@ var SyncOrchestrator = class {
|
|
|
3128
3892
|
pipeline;
|
|
3129
3893
|
blockConverter;
|
|
3130
3894
|
imageHandler;
|
|
3895
|
+
fileHandler;
|
|
3896
|
+
databaseSyncer;
|
|
3131
3897
|
propertyMapper;
|
|
3132
3898
|
dbSchemaLoaded = false;
|
|
3133
3899
|
async push(options) {
|
|
@@ -3189,10 +3955,13 @@ var SyncOrchestrator = class {
|
|
|
3189
3955
|
created++;
|
|
3190
3956
|
break;
|
|
3191
3957
|
case "modified":
|
|
3192
|
-
case "moved":
|
|
3193
3958
|
await this.pushUpdate(change.path);
|
|
3194
3959
|
updated++;
|
|
3195
3960
|
break;
|
|
3961
|
+
case "moved":
|
|
3962
|
+
await this.pushMove(change.path);
|
|
3963
|
+
updated++;
|
|
3964
|
+
break;
|
|
3196
3965
|
case "deleted":
|
|
3197
3966
|
await this.pushDelete(change.path);
|
|
3198
3967
|
deleted++;
|
|
@@ -3209,6 +3978,26 @@ var SyncOrchestrator = class {
|
|
|
3209
3978
|
}
|
|
3210
3979
|
});
|
|
3211
3980
|
await Promise.all(tasks.map((t) => t()));
|
|
3981
|
+
if (this.config.sync.syncFiles !== false) {
|
|
3982
|
+
try {
|
|
3983
|
+
const fileResults = await this.fileHandler.pushAllFiles();
|
|
3984
|
+
if (fileResults.length > 0) {
|
|
3985
|
+
getLogger().info(`[Im-Nobsidian] ${fileResults.length}\uAC1C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC`);
|
|
3986
|
+
}
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
getLogger().warn("[Im-Nobsidian] \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC911 \uC624\uB958:", error);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
3992
|
+
try {
|
|
3993
|
+
const dbResult = await this.databaseSyncer.pushAll();
|
|
3994
|
+
created += dbResult.created;
|
|
3995
|
+
updated += dbResult.updated;
|
|
3996
|
+
failed.push(...dbResult.failed);
|
|
3997
|
+
} catch (error) {
|
|
3998
|
+
getLogger().warn("[Im-Nobsidian] DB Push \uC911 \uC624\uB958:", error);
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
3212
4001
|
this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
3213
4002
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
3214
4003
|
this.stateDb.setMeta("push_in_progress", "");
|
|
@@ -3302,6 +4091,20 @@ var SyncOrchestrator = class {
|
|
|
3302
4091
|
}
|
|
3303
4092
|
});
|
|
3304
4093
|
await Promise.all(tasks.map((t) => t()));
|
|
4094
|
+
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
4095
|
+
try {
|
|
4096
|
+
const dbResult = await this.databaseSyncer.pullAll();
|
|
4097
|
+
created += dbResult.created;
|
|
4098
|
+
updated += dbResult.updated;
|
|
4099
|
+
failed.push(...dbResult.failed);
|
|
4100
|
+
if (dbResult.created + dbResult.updated > 0) {
|
|
4101
|
+
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
4102
|
+
writtenPaths.push(...dbPaths.slice(-dbResult.created - dbResult.updated));
|
|
4103
|
+
}
|
|
4104
|
+
} catch (error) {
|
|
4105
|
+
getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
3305
4108
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
3306
4109
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
3307
4110
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
@@ -3369,13 +4172,20 @@ var SyncOrchestrator = class {
|
|
|
3369
4172
|
localContent = await this.vaultFs.readFile(record.obsidianPath);
|
|
3370
4173
|
} catch {
|
|
3371
4174
|
}
|
|
4175
|
+
let remoteContent = "";
|
|
4176
|
+
if (record.notionPageId) {
|
|
4177
|
+
try {
|
|
4178
|
+
remoteContent = await this.fetchPageMarkdown(record.notionPageId);
|
|
4179
|
+
} catch {
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
3372
4182
|
conflicts.push({
|
|
3373
4183
|
syncRecord: record,
|
|
3374
4184
|
localChange,
|
|
3375
4185
|
remoteChange,
|
|
3376
4186
|
baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
|
|
3377
4187
|
localContent,
|
|
3378
|
-
remoteContent
|
|
4188
|
+
remoteContent
|
|
3379
4189
|
});
|
|
3380
4190
|
}
|
|
3381
4191
|
return conflicts;
|
|
@@ -3439,7 +4249,7 @@ var SyncOrchestrator = class {
|
|
|
3439
4249
|
status: "synced",
|
|
3440
4250
|
baseSnapshot: Buffer.from(content, "utf-8")
|
|
3441
4251
|
});
|
|
3442
|
-
const aliases =
|
|
4252
|
+
const aliases = extractAliases2(conversionResult.properties);
|
|
3443
4253
|
this.stateDb.upsertWikilink({
|
|
3444
4254
|
obsidianPath: path,
|
|
3445
4255
|
notionPageId: page.id,
|
|
@@ -3465,7 +4275,7 @@ var SyncOrchestrator = class {
|
|
|
3465
4275
|
filePath: path,
|
|
3466
4276
|
parentMode: this.config.notion.parentMode
|
|
3467
4277
|
});
|
|
3468
|
-
await this.pushUpdatePage(record.notionPageId, conversionResult.content);
|
|
4278
|
+
await this.pushUpdatePage(record.notionPageId, conversionResult.content, record.baseSnapshot);
|
|
3469
4279
|
await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
|
|
3470
4280
|
let propsToUpdate = conversionResult.properties;
|
|
3471
4281
|
if (this.isDatabaseMode && propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
@@ -3479,7 +4289,7 @@ var SyncOrchestrator = class {
|
|
|
3479
4289
|
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
3480
4290
|
const hash = computeHash(content);
|
|
3481
4291
|
const title = extractTitle(path);
|
|
3482
|
-
const aliases =
|
|
4292
|
+
const aliases = extractAliases2(conversionResult.properties);
|
|
3483
4293
|
this.stateDb.transaction(() => {
|
|
3484
4294
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
3485
4295
|
this.stateDb.updateStatus(record.id, "synced");
|
|
@@ -3503,6 +4313,22 @@ var SyncOrchestrator = class {
|
|
|
3503
4313
|
this.stateDb.updateStatus(record.id, "pending");
|
|
3504
4314
|
}
|
|
3505
4315
|
}
|
|
4316
|
+
async pushMove(path) {
|
|
4317
|
+
const record = this.stateDb.getByPath(path);
|
|
4318
|
+
if (!record?.notionPageId) return;
|
|
4319
|
+
const newParentId = await this.resolveNotionParent(path);
|
|
4320
|
+
const currentParentId = record.notionParentId;
|
|
4321
|
+
if (currentParentId && !notionIdsEqual(newParentId, currentParentId)) {
|
|
4322
|
+
try {
|
|
4323
|
+
const parentType = this.isDatabaseMode ? "database" : "page";
|
|
4324
|
+
await this.notionClient.movePage(record.notionPageId, newParentId, parentType);
|
|
4325
|
+
this.stateDb.setNotionParentId(record.id, newParentId);
|
|
4326
|
+
} catch {
|
|
4327
|
+
getLogger().warn(`[Im-Nobsidian] Move API \uC2E4\uD328 \u2014 "${path}" \uB0B4\uC6A9\uB9CC \uC5C5\uB370\uC774\uD2B8\uD569\uB2C8\uB2E4`);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
await this.pushUpdate(path);
|
|
4331
|
+
}
|
|
3506
4332
|
async detectRemoteChanges() {
|
|
3507
4333
|
const changes = [];
|
|
3508
4334
|
const lastPull = this.stateDb.getMeta("last_pull_at");
|
|
@@ -3627,7 +4453,7 @@ var SyncOrchestrator = class {
|
|
|
3627
4453
|
status: "synced",
|
|
3628
4454
|
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
3629
4455
|
});
|
|
3630
|
-
const aliases =
|
|
4456
|
+
const aliases = extractAliases2(properties);
|
|
3631
4457
|
this.stateDb.upsertWikilink({
|
|
3632
4458
|
obsidianPath: filePath,
|
|
3633
4459
|
notionPageId: pageId,
|
|
@@ -3838,10 +4664,23 @@ var SyncOrchestrator = class {
|
|
|
3838
4664
|
}
|
|
3839
4665
|
return page;
|
|
3840
4666
|
}
|
|
3841
|
-
async pushUpdatePage(pageId, markdownContent) {
|
|
4667
|
+
async pushUpdatePage(pageId, markdownContent, baseSnapshot) {
|
|
3842
4668
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
3843
4669
|
try {
|
|
3844
4670
|
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
4671
|
+
if (baseSnapshot) {
|
|
4672
|
+
const baseEnhanced = obsidianToNotionEnhanced(baseSnapshot.toString("utf-8"));
|
|
4673
|
+
const patches = this.computePatches(baseEnhanced, enhanced);
|
|
4674
|
+
if (patches.length > 0 && patches.length <= 20) {
|
|
4675
|
+
try {
|
|
4676
|
+
await this.notionClient.updatePageMarkdownPartial(pageId, patches);
|
|
4677
|
+
return;
|
|
4678
|
+
} catch {
|
|
4679
|
+
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
3845
4684
|
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
3846
4685
|
return;
|
|
3847
4686
|
} catch {
|
|
@@ -3864,6 +4703,51 @@ var SyncOrchestrator = class {
|
|
|
3864
4703
|
})
|
|
3865
4704
|
);
|
|
3866
4705
|
}
|
|
4706
|
+
computePatches(oldContent, newContent) {
|
|
4707
|
+
const oldLines = oldContent.split("\n");
|
|
4708
|
+
const newLines = newContent.split("\n");
|
|
4709
|
+
const patches = [];
|
|
4710
|
+
let i = 0;
|
|
4711
|
+
let j = 0;
|
|
4712
|
+
while (i < oldLines.length && j < newLines.length) {
|
|
4713
|
+
if (oldLines[i] === newLines[j]) {
|
|
4714
|
+
i++;
|
|
4715
|
+
j++;
|
|
4716
|
+
continue;
|
|
4717
|
+
}
|
|
4718
|
+
let oldEnd = i + 1;
|
|
4719
|
+
let newEnd = j + 1;
|
|
4720
|
+
const lookahead = Math.min(10, oldLines.length - i, newLines.length - j);
|
|
4721
|
+
for (let k = 1; k <= lookahead; k++) {
|
|
4722
|
+
if (i + k < oldLines.length && newLines[j] === oldLines[i + k]) {
|
|
4723
|
+
oldEnd = i + k;
|
|
4724
|
+
newEnd = j;
|
|
4725
|
+
break;
|
|
4726
|
+
}
|
|
4727
|
+
if (j + k < newLines.length && oldLines[i] === newLines[j + k]) {
|
|
4728
|
+
oldEnd = i;
|
|
4729
|
+
newEnd = j + k;
|
|
4730
|
+
break;
|
|
4731
|
+
}
|
|
4732
|
+
oldEnd = i + k;
|
|
4733
|
+
newEnd = j + k;
|
|
4734
|
+
}
|
|
4735
|
+
const oldChunk = oldLines.slice(i, oldEnd).join("\n");
|
|
4736
|
+
const newChunk = newLines.slice(j, newEnd).join("\n");
|
|
4737
|
+
if (oldChunk || newChunk) {
|
|
4738
|
+
patches.push({ oldStr: oldChunk, newStr: newChunk });
|
|
4739
|
+
}
|
|
4740
|
+
i = oldEnd;
|
|
4741
|
+
j = newEnd;
|
|
4742
|
+
}
|
|
4743
|
+
if (i < oldLines.length || j < newLines.length) {
|
|
4744
|
+
patches.push({
|
|
4745
|
+
oldStr: oldLines.slice(i).join("\n"),
|
|
4746
|
+
newStr: newLines.slice(j).join("\n")
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
return patches;
|
|
4750
|
+
}
|
|
3867
4751
|
async fetchPageMarkdown(pageId) {
|
|
3868
4752
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
3869
4753
|
try {
|
|
@@ -3909,7 +4793,7 @@ function extractTitle(filePath) {
|
|
|
3909
4793
|
const filename = parts[parts.length - 1] ?? "";
|
|
3910
4794
|
return filename.replace(/\.md$/, "");
|
|
3911
4795
|
}
|
|
3912
|
-
function
|
|
4796
|
+
function extractAliases2(properties) {
|
|
3913
4797
|
const raw = properties.aliases ?? properties.alias;
|
|
3914
4798
|
if (!raw) return [];
|
|
3915
4799
|
if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
|
|
@@ -3981,6 +4865,11 @@ var NodeVaultFS = class {
|
|
|
3981
4865
|
await this.walkDir(this.rootPath, files);
|
|
3982
4866
|
return files;
|
|
3983
4867
|
}
|
|
4868
|
+
async listNonMarkdownFiles() {
|
|
4869
|
+
const files = [];
|
|
4870
|
+
await this.walkDirNonMd(this.rootPath, files);
|
|
4871
|
+
return files;
|
|
4872
|
+
}
|
|
3984
4873
|
matchesPattern(relativePath, pattern) {
|
|
3985
4874
|
if (pattern === "**/*" || pattern === "**") return true;
|
|
3986
4875
|
if (pattern.endsWith("/**")) {
|
|
@@ -4003,6 +4892,25 @@ var NodeVaultFS = class {
|
|
|
4003
4892
|
}
|
|
4004
4893
|
return this.excludePatterns.some((p) => this.matchesPattern(relativePath, p));
|
|
4005
4894
|
}
|
|
4895
|
+
async walkDirNonMd(dir, result) {
|
|
4896
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
4897
|
+
for (const entry of entries) {
|
|
4898
|
+
const fullPath = join(dir, entry.name);
|
|
4899
|
+
if (entry.name.startsWith(".")) continue;
|
|
4900
|
+
const relativePath = relative(this.rootPath, fullPath);
|
|
4901
|
+
if (this.isExcluded(relativePath)) continue;
|
|
4902
|
+
if (entry.isDirectory()) {
|
|
4903
|
+
await this.walkDirNonMd(fullPath, result);
|
|
4904
|
+
} else if (entry.isFile() && !entry.name.endsWith(".md")) {
|
|
4905
|
+
const fileStat = await stat(fullPath);
|
|
4906
|
+
result.push({
|
|
4907
|
+
path: relativePath,
|
|
4908
|
+
size: fileStat.size,
|
|
4909
|
+
mtime: fileStat.mtime.toISOString()
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4006
4914
|
async walkDir(dir, result) {
|
|
4007
4915
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
4008
4916
|
for (const entry of entries) {
|
|
@@ -4483,6 +5391,6 @@ var WatchSyncService = class {
|
|
|
4483
5391
|
}
|
|
4484
5392
|
};
|
|
4485
5393
|
|
|
4486
|
-
export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, EmbedResolver, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, generateId, getLogger, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, threeWayMerge };
|
|
5394
|
+
export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, generateId, getLogger, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, threeWayMerge };
|
|
4487
5395
|
//# sourceMappingURL=index.js.map
|
|
4488
5396
|
//# sourceMappingURL=index.js.map
|