1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2025-07-16 00:14:19 +02:00

New Feature

- Hidden file sync.
This commit is contained in:
vorotamoroz
2022-07-19 17:57:29 +09:00
parent 0e6a6dcd2a
commit fddc466b0f
5 changed files with 439 additions and 9 deletions

View File

@ -20,6 +20,7 @@ import {
MILSTONE_DOCID,
DatabaseConnectingStatus,
ChunkVersionRange,
NoteEntry,
} from "./lib/src/types";
import { RemoteDBSettings } from "./lib/src/types";
import { resolveWithIgnoreKnownError, runWithLock, shouldSplitAsPlainText, splitPieces2, enableEncryption } from "./lib/src/utils";
@ -304,7 +305,7 @@ export class LocalPouchDB {
} else {
obj = await this.localDatabase.get(id);
}
const deleted = "deleted" in obj ? obj.deleted : undefined;
if (obj.type && obj.type == "leaf") {
//do nothing for leaf;
return false;
@ -330,6 +331,8 @@ export class LocalPouchDB {
_conflicts: obj._conflicts,
children: children,
datatype: type,
deleted: deleted,
type: type
};
return doc;
}
@ -350,6 +353,7 @@ export class LocalPouchDB {
} else {
obj = await this.localDatabase.get(id);
}
const deleted = "deleted" in obj ? obj.deleted : undefined;
if (obj.type && obj.type == "leaf") {
//do nothing for leaf;
@ -358,7 +362,7 @@ export class LocalPouchDB {
//Check it out and fix docs to regular case
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 = {
data: note.data,
_id: note._id,
@ -370,6 +374,8 @@ export class LocalPouchDB {
_conflicts: obj._conflicts,
children: [],
datatype: "newnote",
deleted: deleted,
type: "newnote",
};
if (typeof this.corruptedEntries[doc._id] != "undefined") {
delete this.corruptedEntries[doc._id];
@ -414,6 +420,8 @@ export class LocalPouchDB {
children: obj.children,
datatype: obj.type,
_conflicts: obj._conflicts,
deleted: deleted,
type: obj.type
};
if (dump) {
Logger(`therefore:`);
@ -684,7 +692,7 @@ export class LocalPouchDB {
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") {
delete this.corruptedEntries[note._id];
}

View File

@ -765,9 +765,60 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
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", {
text: sanitizeHTMLToDom(`Advanced settings<br>
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
text: `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)
.setName("Batch size")

Submodule src/lib updated: 548265c701...1f67fb604c

View File

@ -1,8 +1,8 @@
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 { 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 { PluginDataEntry, PERIODIC_PLUGIN_SWEEP, PluginList, DevicePluginList } from "./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, InternalFileInfo } from "./types";
import {
base64ToString,
arrayBufferToBase64,
@ -18,6 +18,7 @@ import {
NewNotice,
getLocks,
Parallels,
WrappedNotice,
} from "./lib/src/utils";
import { Logger, setLogger } from "./lib/src/logger";
import { LocalPouchDB } from "./LocalPouchDB";
@ -500,6 +501,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.showPluginSyncModal();
},
});
this.addCommand({
id: "livesync-scaninternal",
name: "Sync hidden files",
callback: () => {
this.syncInternalFilesAndDatabase("safe", true);
},
});
}
pluginDialog: PluginDialogModal = null;
@ -531,6 +540,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
this.clearPeriodicSync();
this.clearPluginSweep();
this.clearInternalFileScan();
if (this.localDatabase != null) {
this.localDatabase.closeReplication();
this.localDatabase.close();
@ -1124,6 +1134,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const now = new Date().getTime();
if (queue.missingChildren.length == 0) {
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))) {
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
await this.handleDBChanged(queue.entry);
@ -1162,7 +1179,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
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));
if (info && info instanceof TFile) {
@ -1304,6 +1321,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.localDatabase.closeReplication();
this.clearPeriodicSync();
this.clearPluginSweep();
this.clearInternalFileScan();
await this.applyBatchChange();
// disable all sync temporary.
if (this.suspended) return;
@ -1314,8 +1332,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.refreshStatusText();
}
if (this.settings.syncInternalFiles) {
await this.syncInternalFilesAndDatabase("safe", false);
}
this.setPeriodicSync();
this.setPluginSweep();
this.setPeriodicInternalFileScan();
}
lastMessage = "";
@ -1414,6 +1436,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.sweepPlugin(false);
}
await this.loadQueuedFiles();
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication) {
await this.syncInternalFilesAndDatabase("push", showMessage);
}
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
}
@ -1877,6 +1902,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
size: file.stat.size,
children: [],
datatype: datatype,
type: datatype,
};
//upsert should locked
const msg = `DB <- STORAGE (${datatype}) `;
@ -2016,6 +2042,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
size: 0,
children: [],
datatype: "plain",
type: "plain"
};
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL.VERBOSE);
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");
}
}

View File

@ -22,3 +22,11 @@ export interface DevicePluginList {
[key: string]: PluginDataEntry;
}
export const PERIODIC_PLUGIN_SWEEP = 60;
export interface InternalFileInfo {
path: string;
mtime: number;
ctime: number;
size: number;
deleted?: boolean;
}