mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-12-16 03:05:57 +00:00
New Feature
- Hidden file sync.
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
MILSTONE_DOCID,
|
MILSTONE_DOCID,
|
||||||
DatabaseConnectingStatus,
|
DatabaseConnectingStatus,
|
||||||
ChunkVersionRange,
|
ChunkVersionRange,
|
||||||
|
NoteEntry,
|
||||||
} from "./lib/src/types";
|
} from "./lib/src/types";
|
||||||
import { RemoteDBSettings } from "./lib/src/types";
|
import { RemoteDBSettings } from "./lib/src/types";
|
||||||
import { resolveWithIgnoreKnownError, runWithLock, shouldSplitAsPlainText, splitPieces2, enableEncryption } from "./lib/src/utils";
|
import { resolveWithIgnoreKnownError, runWithLock, shouldSplitAsPlainText, splitPieces2, enableEncryption } from "./lib/src/utils";
|
||||||
@@ -304,7 +305,7 @@ export class LocalPouchDB {
|
|||||||
} else {
|
} else {
|
||||||
obj = await this.localDatabase.get(id);
|
obj = await this.localDatabase.get(id);
|
||||||
}
|
}
|
||||||
|
const deleted = "deleted" in obj ? obj.deleted : undefined;
|
||||||
if (obj.type && obj.type == "leaf") {
|
if (obj.type && obj.type == "leaf") {
|
||||||
//do nothing for leaf;
|
//do nothing for leaf;
|
||||||
return false;
|
return false;
|
||||||
@@ -330,6 +331,8 @@ export class LocalPouchDB {
|
|||||||
_conflicts: obj._conflicts,
|
_conflicts: obj._conflicts,
|
||||||
children: children,
|
children: children,
|
||||||
datatype: type,
|
datatype: type,
|
||||||
|
deleted: deleted,
|
||||||
|
type: type
|
||||||
};
|
};
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
@@ -350,6 +353,7 @@ export class LocalPouchDB {
|
|||||||
} else {
|
} else {
|
||||||
obj = await this.localDatabase.get(id);
|
obj = await this.localDatabase.get(id);
|
||||||
}
|
}
|
||||||
|
const deleted = "deleted" in obj ? obj.deleted : undefined;
|
||||||
|
|
||||||
if (obj.type && obj.type == "leaf") {
|
if (obj.type && obj.type == "leaf") {
|
||||||
//do nothing for leaf;
|
//do nothing for leaf;
|
||||||
@@ -358,7 +362,7 @@ export class LocalPouchDB {
|
|||||||
|
|
||||||
//Check it out and fix docs to regular case
|
//Check it out and fix docs to regular case
|
||||||
if (!obj.type || (obj.type && obj.type == "notes")) {
|
if (!obj.type || (obj.type && obj.type == "notes")) {
|
||||||
const note = obj as Entry;
|
const note = obj as NoteEntry;
|
||||||
const doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
|
const doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
|
||||||
data: note.data,
|
data: note.data,
|
||||||
_id: note._id,
|
_id: note._id,
|
||||||
@@ -370,6 +374,8 @@ export class LocalPouchDB {
|
|||||||
_conflicts: obj._conflicts,
|
_conflicts: obj._conflicts,
|
||||||
children: [],
|
children: [],
|
||||||
datatype: "newnote",
|
datatype: "newnote",
|
||||||
|
deleted: deleted,
|
||||||
|
type: "newnote",
|
||||||
};
|
};
|
||||||
if (typeof this.corruptedEntries[doc._id] != "undefined") {
|
if (typeof this.corruptedEntries[doc._id] != "undefined") {
|
||||||
delete this.corruptedEntries[doc._id];
|
delete this.corruptedEntries[doc._id];
|
||||||
@@ -414,6 +420,8 @@ export class LocalPouchDB {
|
|||||||
children: obj.children,
|
children: obj.children,
|
||||||
datatype: obj.type,
|
datatype: obj.type,
|
||||||
_conflicts: obj._conflicts,
|
_conflicts: obj._conflicts,
|
||||||
|
deleted: deleted,
|
||||||
|
type: obj.type
|
||||||
};
|
};
|
||||||
if (dump) {
|
if (dump) {
|
||||||
Logger(`therefore:`);
|
Logger(`therefore:`);
|
||||||
@@ -684,7 +692,7 @@ export class LocalPouchDB {
|
|||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const r = await this.localDatabase.put(newDoc, { force: true });
|
const r = await this.localDatabase.put<PlainEntry | NewEntry>(newDoc, { force: true });
|
||||||
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
if (typeof this.corruptedEntries[note._id] != "undefined") {
|
||||||
delete this.corruptedEntries[note._id];
|
delete this.corruptedEntries[note._id];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -765,9 +765,60 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
containerSyncSettingEl.createEl("h3", {
|
||||||
|
text: sanitizeHTMLToDom(`Experimental`),
|
||||||
|
});
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Sync hidden files.")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.syncInternalFiles).onChange(async (value) => {
|
||||||
|
this.plugin.settings.syncInternalFiles = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Scan hidden files before replication.")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
||||||
|
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Scan hidden files periodicaly.")
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder("")
|
||||||
|
.setValue(this.plugin.settings.syncInternalFilesInterval + "")
|
||||||
|
.onChange(async (value) => {
|
||||||
|
let v = Number(value);
|
||||||
|
if (isNaN(v) || v < 10) {
|
||||||
|
v = 10;
|
||||||
|
}
|
||||||
|
this.plugin.settings.syncInternalFilesInterval = v;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
text.inputEl.setAttribute("type", "number");
|
||||||
|
});
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Skip patterns")
|
||||||
|
.setDesc(
|
||||||
|
"Regular expression"
|
||||||
|
)
|
||||||
|
.addTextArea((text) =>
|
||||||
|
text
|
||||||
|
.setValue(this.plugin.settings.syncInternalFilesIgnorePatterns)
|
||||||
|
.setPlaceholder("\\/node_modules\\/, \\/\\.git\\/")
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.syncInternalFilesIgnorePatterns = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
containerSyncSettingEl.createEl("h3", {
|
||||||
|
text: sanitizeHTMLToDom(`Advanced settings`),
|
||||||
|
});
|
||||||
containerSyncSettingEl.createEl("div", {
|
containerSyncSettingEl.createEl("div", {
|
||||||
text: sanitizeHTMLToDom(`Advanced settings<br>
|
text: `If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`,
|
||||||
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
|
|
||||||
});
|
});
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Batch size")
|
.setName("Batch size")
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 548265c701...1f67fb604c
369
src/main.ts
369
src/main.ts
@@ -1,8 +1,8 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian";
|
||||||
import { diff_match_patch } from "diff-match-patch";
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
|
||||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types";
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, InternalFileEntry } from "./lib/src/types";
|
||||||
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList } from "./types";
|
import { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList, InternalFileInfo } from "./types";
|
||||||
import {
|
import {
|
||||||
base64ToString,
|
base64ToString,
|
||||||
arrayBufferToBase64,
|
arrayBufferToBase64,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
NewNotice,
|
NewNotice,
|
||||||
getLocks,
|
getLocks,
|
||||||
Parallels,
|
Parallels,
|
||||||
|
WrappedNotice,
|
||||||
} from "./lib/src/utils";
|
} from "./lib/src/utils";
|
||||||
import { Logger, setLogger } from "./lib/src/logger";
|
import { Logger, setLogger } from "./lib/src/logger";
|
||||||
import { LocalPouchDB } from "./LocalPouchDB";
|
import { LocalPouchDB } from "./LocalPouchDB";
|
||||||
@@ -500,6 +501,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.showPluginSyncModal();
|
this.showPluginSyncModal();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "livesync-scaninternal",
|
||||||
|
name: "Sync hidden files",
|
||||||
|
callback: () => {
|
||||||
|
this.syncInternalFilesAndDatabase("safe", true);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginDialog: PluginDialogModal = null;
|
pluginDialog: PluginDialogModal = null;
|
||||||
@@ -531,6 +540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
this.clearPeriodicSync();
|
this.clearPeriodicSync();
|
||||||
this.clearPluginSweep();
|
this.clearPluginSweep();
|
||||||
|
this.clearInternalFileScan();
|
||||||
if (this.localDatabase != null) {
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.closeReplication();
|
this.localDatabase.closeReplication();
|
||||||
this.localDatabase.close();
|
this.localDatabase.close();
|
||||||
@@ -1124,6 +1134,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (queue.missingChildren.length == 0) {
|
if (queue.missingChildren.length == 0) {
|
||||||
queue.done = true;
|
queue.done = true;
|
||||||
|
if (queue.entry._id.startsWith("i:")) {
|
||||||
|
//system file
|
||||||
|
const filename = id2path(queue.entry._id.substring("i:".length));
|
||||||
|
Logger(`Applying hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||||
|
await this.syncInternalFilesAndDatabase("pull", false, false, [filename])
|
||||||
|
Logger(`Applied hidden file, ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||||
|
}
|
||||||
if (isValidPath(id2path(queue.entry._id))) {
|
if (isValidPath(id2path(queue.entry._id))) {
|
||||||
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
|
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||||
await this.handleDBChanged(queue.entry);
|
await this.handleDBChanged(queue.entry);
|
||||||
@@ -1162,7 +1179,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||||
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
if (skipOldFile) {
|
if ((!doc._id.startsWith("i:")) && skipOldFile) {
|
||||||
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
||||||
|
|
||||||
if (info && info instanceof TFile) {
|
if (info && info instanceof TFile) {
|
||||||
@@ -1304,6 +1321,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.localDatabase.closeReplication();
|
this.localDatabase.closeReplication();
|
||||||
this.clearPeriodicSync();
|
this.clearPeriodicSync();
|
||||||
this.clearPluginSweep();
|
this.clearPluginSweep();
|
||||||
|
this.clearInternalFileScan();
|
||||||
await this.applyBatchChange();
|
await this.applyBatchChange();
|
||||||
// disable all sync temporary.
|
// disable all sync temporary.
|
||||||
if (this.suspended) return;
|
if (this.suspended) return;
|
||||||
@@ -1314,8 +1332,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
|
||||||
this.refreshStatusText();
|
this.refreshStatusText();
|
||||||
}
|
}
|
||||||
|
if (this.settings.syncInternalFiles) {
|
||||||
|
await this.syncInternalFilesAndDatabase("safe", false);
|
||||||
|
}
|
||||||
this.setPeriodicSync();
|
this.setPeriodicSync();
|
||||||
this.setPluginSweep();
|
this.setPluginSweep();
|
||||||
|
this.setPeriodicInternalFileScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
@@ -1414,6 +1436,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.sweepPlugin(false);
|
await this.sweepPlugin(false);
|
||||||
}
|
}
|
||||||
await this.loadQueuedFiles();
|
await this.loadQueuedFiles();
|
||||||
|
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication) {
|
||||||
|
await this.syncInternalFilesAndDatabase("push", showMessage);
|
||||||
|
}
|
||||||
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1877,6 +1902,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
size: file.stat.size,
|
size: file.stat.size,
|
||||||
children: [],
|
children: [],
|
||||||
datatype: datatype,
|
datatype: datatype,
|
||||||
|
type: datatype,
|
||||||
};
|
};
|
||||||
//upsert should locked
|
//upsert should locked
|
||||||
const msg = `DB <- STORAGE (${datatype}) `;
|
const msg = `DB <- STORAGE (${datatype}) `;
|
||||||
@@ -2016,6 +2042,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
size: 0,
|
size: 0,
|
||||||
children: [],
|
children: [],
|
||||||
datatype: "plain",
|
datatype: "plain",
|
||||||
|
type: "plain"
|
||||||
};
|
};
|
||||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
|
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
|
||||||
await runWithLock("plugin-" + m.id, false, async () => {
|
await runWithLock("plugin-" + m.id, false, async () => {
|
||||||
@@ -2091,4 +2118,340 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
periodicInternalFileScanHandler: number = null;
|
||||||
|
|
||||||
|
clearInternalFileScan() {
|
||||||
|
if (this.periodicInternalFileScanHandler != null) {
|
||||||
|
clearInterval(this.periodicInternalFileScanHandler);
|
||||||
|
this.periodicInternalFileScanHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeriodicInternalFileScan() {
|
||||||
|
if (this.periodicInternalFileScanHandler != null) {
|
||||||
|
this.clearInternalFileScan();
|
||||||
|
}
|
||||||
|
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0) {
|
||||||
|
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicInternalFileScan() {
|
||||||
|
await this.syncInternalFilesAndDatabase("push", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFiles(
|
||||||
|
path: string,
|
||||||
|
ignoreList: string[],
|
||||||
|
filter: RegExp[]
|
||||||
|
) {
|
||||||
|
const w = await this.app.vault.adapter.list(path);
|
||||||
|
let files = [
|
||||||
|
...w.files
|
||||||
|
.filter((e) => !ignoreList.some((ee) => e.endsWith(ee)))
|
||||||
|
.filter((e) => !filter || filter.some((ee) => e.match(ee))),
|
||||||
|
];
|
||||||
|
L1: for (const v of w.folders) {
|
||||||
|
for (const ignore of ignoreList) {
|
||||||
|
if (v.endsWith(ignore)) {
|
||||||
|
continue L1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = files.concat(await this.getFiles(v, ignoreList, filter));
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
||||||
|
const ignoreFiles = ["node_modules", ".git", "obsidian-pouch"];
|
||||||
|
const root = this.app.vault.getRoot();
|
||||||
|
const findRoot = root.path;
|
||||||
|
const filenames = (await this.getFiles(findRoot, ignoreFiles, null)).filter(e => e.startsWith(".")).filter(e => !e.startsWith(".trash"));
|
||||||
|
const files = filenames.map(async e => {
|
||||||
|
return {
|
||||||
|
path: e,
|
||||||
|
stat: await this.app.vault.adapter.stat(e)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result: InternalFileInfo[] = [];
|
||||||
|
for (const f of files) {
|
||||||
|
const w = await f;
|
||||||
|
result.push({
|
||||||
|
...w,
|
||||||
|
...w.stat
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeInternaFileToDatabase(file: InternalFileInfo, forceWrite = false) {
|
||||||
|
const id = "i:" + path2id(file.path);
|
||||||
|
const contentBin = await this.app.vault.adapter.readBinary(file.path);
|
||||||
|
const content = await arrayBufferToBase64(contentBin);
|
||||||
|
const mtime = file.mtime;
|
||||||
|
await runWithLock("file-" + id, false, async () => {
|
||||||
|
const old = await this.localDatabase.getDBEntry(id, null, false, false);
|
||||||
|
let saveData: LoadedEntry;
|
||||||
|
if (old === false) {
|
||||||
|
saveData = {
|
||||||
|
_id: id,
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
ctime: mtime,
|
||||||
|
datatype: "newnote",
|
||||||
|
size: file.size,
|
||||||
|
children: [],
|
||||||
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (old.data == content && !forceWrite) {
|
||||||
|
// Logger(`internal files STORAGE --> DB:${file.path}: Not changed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveData =
|
||||||
|
{
|
||||||
|
...old,
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
size: file.size,
|
||||||
|
datatype: "newnote",
|
||||||
|
children: [],
|
||||||
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.localDatabase.putDBEntry(saveData, true);
|
||||||
|
Logger(`internal files STORAGE --> DB:${file.path}: Done`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteInternaFileOnDatabase(filename: string, forceWrite = false) {
|
||||||
|
const id = "i:" + path2id(filename);
|
||||||
|
const mtime = new Date().getTime();
|
||||||
|
await runWithLock("file-" + id, false, async () => {
|
||||||
|
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
|
||||||
|
let saveData: InternalFileEntry;
|
||||||
|
if (old === false) {
|
||||||
|
saveData = {
|
||||||
|
_id: id,
|
||||||
|
mtime,
|
||||||
|
ctime: mtime,
|
||||||
|
size: 0,
|
||||||
|
children: [],
|
||||||
|
deleted: true,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (old.deleted) {
|
||||||
|
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveData =
|
||||||
|
{
|
||||||
|
...old,
|
||||||
|
mtime,
|
||||||
|
size: 0,
|
||||||
|
children: [],
|
||||||
|
deleted: true,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.localDatabase.localDatabase.put(saveData);
|
||||||
|
Logger(`STORAGE -x> DB:${filename}: (hidden) Done`);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async ensureDirectoryEx(fullpath: string) {
|
||||||
|
const pathElements = fullpath.split("/");
|
||||||
|
pathElements.pop();
|
||||||
|
let c = "";
|
||||||
|
for (const v of pathElements) {
|
||||||
|
c += v;
|
||||||
|
try {
|
||||||
|
await this.app.vault.adapter.mkdir(c);
|
||||||
|
} catch (ex) {
|
||||||
|
// basically skip exceptions.
|
||||||
|
if (ex.message && ex.message == "Folder already exists.") {
|
||||||
|
// especialy this message is.
|
||||||
|
} else {
|
||||||
|
Logger("Folder Create Error");
|
||||||
|
Logger(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c += "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async extractInternaFileFromDatabase(filename: string, force = false) {
|
||||||
|
const isExists = await this.app.vault.adapter.exists(filename);
|
||||||
|
const id = "i:" + path2id(filename);
|
||||||
|
|
||||||
|
return await runWithLock("file-" + id, false, async () => {
|
||||||
|
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
|
||||||
|
if (fileOnDB === false) throw new Error(`File not found on database.:${id}`);
|
||||||
|
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false;
|
||||||
|
if (deleted) {
|
||||||
|
if (!isExists) {
|
||||||
|
Logger(`STORAGE <x- DB:${filename}: deleted (hidden) Deleted on DB, but the file is already not found on storage.`);
|
||||||
|
} else {
|
||||||
|
Logger(`STORAGE <x- DB:${filename}: deleted (hidden).`);
|
||||||
|
await this.app.vault.adapter.remove(filename);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!isExists) {
|
||||||
|
await this.ensureDirectoryEx(filename);
|
||||||
|
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||||
|
Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// const stat = await this.app.vault.adapter.stat(filename);
|
||||||
|
// const fileMTime = ~~(stat.mtime/1000);
|
||||||
|
// const docMtime = ~~(old.mtime/1000);
|
||||||
|
const contentBin = await this.app.vault.adapter.readBinary(filename);
|
||||||
|
const content = await arrayBufferToBase64(contentBin);
|
||||||
|
if (content == fileOnDB.data && !force) {
|
||||||
|
Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||||
|
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""})`);
|
||||||
|
return true;
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmPopup: WrappedNotice = null;
|
||||||
|
confirmPopupTimer: number = null;
|
||||||
|
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||||
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
|
Logger("Scanning hidden files.", logLevel, "sync_internal");
|
||||||
|
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase()
|
||||||
|
.replace(/\n| /g, "")
|
||||||
|
.split(",").filter(e => e).map(e => new RegExp(e));
|
||||||
|
if (!files) files = await this.scanInternalFiles();
|
||||||
|
const filesOnDB = (await this.localDatabase.localDatabase.allDocs({ startkey: "i:", endkey: "i;", include_docs: true })).rows.map(e => e.doc) as InternalFileEntry[];
|
||||||
|
const allFileNames = [...new Set([...files.map(e => normalizePath(e.path)), ...filesOnDB.map(e => normalizePath(id2path(e._id.substring("i:".length))))])];
|
||||||
|
function compareMTime(a: number, b: number) {
|
||||||
|
const wa = ~~(a / 1000);
|
||||||
|
const wb = ~~(b / 1000);
|
||||||
|
const diff = wa - wb;
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileCount = allFileNames.length;
|
||||||
|
let processed = 0;
|
||||||
|
let filesChanged = 0;
|
||||||
|
const p = Parallels();
|
||||||
|
const limit = 10;
|
||||||
|
|
||||||
|
for (const filename of allFileNames) {
|
||||||
|
// Logger(`Processing:${filename}`, LOG_LEVEL.VERBOSE);
|
||||||
|
processed++;
|
||||||
|
if (processed % 100 == 0) Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal");
|
||||||
|
if (ignorePatterns.some(e => filename.match(e))) continue;
|
||||||
|
if (targetFiles !== false && targetFiles.indexOf(filename) == -1) continue;
|
||||||
|
|
||||||
|
const fileOnStorage = files.find(e => e.path == filename);
|
||||||
|
const fileOnDatabase = filesOnDB.find(e => e._id == "i:" + id2path(filename));
|
||||||
|
let proc: () => Promise<void> | null = null;
|
||||||
|
if (fileOnStorage && fileOnDatabase) {
|
||||||
|
// Both => Synchronize
|
||||||
|
|
||||||
|
const nw = compareMTime(fileOnStorage.mtime, fileOnDatabase.mtime);
|
||||||
|
if (nw == 0) continue;
|
||||||
|
|
||||||
|
if (nw > 0) {
|
||||||
|
proc = async () => {
|
||||||
|
await this.storeInternaFileToDatabase(fileOnStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (nw < 0) {
|
||||||
|
proc = async () => {
|
||||||
|
if (await this.extractInternaFileFromDatabase(filename)) {
|
||||||
|
filesChanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if (!fileOnStorage && fileOnDatabase) {
|
||||||
|
if (direction == "push") {
|
||||||
|
if (fileOnDatabase.deleted) {
|
||||||
|
// await this.storeInternaFileToDatabase(fileOnStorage);
|
||||||
|
} else {
|
||||||
|
proc = async () => {
|
||||||
|
await this.deleteInternaFileOnDatabase(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (direction == "pull") {
|
||||||
|
proc = async () => {
|
||||||
|
if (await this.extractInternaFileFromDatabase(filename)) {
|
||||||
|
filesChanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (direction == "safe") {
|
||||||
|
if (fileOnDatabase.deleted) {
|
||||||
|
// await this.storeInternaFileToDatabase(fileOnStorage);
|
||||||
|
} else {
|
||||||
|
proc = async () => {
|
||||||
|
if (await this.extractInternaFileFromDatabase(filename)) {
|
||||||
|
filesChanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (fileOnStorage && !fileOnDatabase) {
|
||||||
|
proc = async () => {
|
||||||
|
await this.storeInternaFileToDatabase(fileOnStorage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid state on hidden file sync");
|
||||||
|
// Something corrupted?
|
||||||
|
}
|
||||||
|
if (proc) p.add(proc());
|
||||||
|
proc = null;
|
||||||
|
await p.wait(limit);
|
||||||
|
}
|
||||||
|
await p.all();
|
||||||
|
// Show notification to restart obsidian.
|
||||||
|
if (direction == "pull" && filesChanged != 0) {
|
||||||
|
|
||||||
|
const fragment = createFragment((doc) => {
|
||||||
|
doc.createEl("span", null, (a) => {
|
||||||
|
a.appendText(`Hidden files have been synchronized, Press `)
|
||||||
|
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||||
|
anchor.text = "HERE";
|
||||||
|
anchor.addEventListener("click", () => {
|
||||||
|
// @ts-ignore
|
||||||
|
this.app.commands.executeCommandById("app:reload")
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
|
||||||
|
a.appendText(` to reload obsidian, or press elsewhere to dismiss this message.`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//@ts-ignore
|
||||||
|
const isShown = this.confirmPopup?.noticeEl?.isShown();
|
||||||
|
if (!isShown) {
|
||||||
|
this.confirmPopup = new Notice(fragment, 0);
|
||||||
|
}
|
||||||
|
if (this.confirmPopupTimer != null) {
|
||||||
|
clearTimeout(this.confirmPopupTimer);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.confirmPopup?.hide();
|
||||||
|
this.confirmPopup = null;
|
||||||
|
}, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger(`Hidden files scanned`, logLevel, "sync_internal");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,3 +22,11 @@ export interface DevicePluginList {
|
|||||||
[key: string]: PluginDataEntry;
|
[key: string]: PluginDataEntry;
|
||||||
}
|
}
|
||||||
export const PERIODIC_PLUGIN_SWEEP = 60;
|
export const PERIODIC_PLUGIN_SWEEP = 60;
|
||||||
|
|
||||||
|
export interface InternalFileInfo {
|
||||||
|
path: string;
|
||||||
|
mtime: number;
|
||||||
|
ctime: number;
|
||||||
|
size: number;
|
||||||
|
deleted?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user