Implemented:

- Explicit types
- Path obfuscation.
- ... and minor changes.
This commit is contained in:
vorotamoroz
2023-04-10 12:04:30 +09:00
parent 97f91b1eb0
commit e395e53248
14 changed files with 448 additions and 468 deletions

View File

@@ -2,7 +2,7 @@ const isDebug = false;
import { Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, RequestUrlParam, RequestUrlResponse, requestUrl } from "./deps";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, DatabaseConnectingStatus } 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, SALT_OF_PASSPHRASE, ConfigPassphraseStore, CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, DatabaseConnectingStatus, EntryHasPath, DocumentID, FilePathWithPrefix, FilePath, AnyEntry } from "./lib/src/types";
import { InternalFileInfo, queueItem, CacheData, FileEventItem, FileWatchEventQueueMax } from "./types";
import { delay, getDocData, isDocContentSame } from "./lib/src/utils";
import { Logger } from "./lib/src/logger";
@@ -11,14 +11,14 @@ import { LogDisplayModal } from "./LogDisplayModal";
import { ConflictResolveModal } from "./ConflictResolveModal";
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
import { DocumentHistoryModal } from "./DocumentHistoryModal";
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isInternalMetadata, isPluginMetadata, id2filenameInternalMetadata, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched } from "./utils";
import { applyPatch, cancelAllPeriodicTask, cancelAllTasks, cancelTask, generatePatchObj, id2path, isObjectMargeApplicable, isSensibleMargeApplicable, flattenObject, path2id, scheduleTask, tryParseJSON, createFile, modifyFile, isValidPath, getAbstractFileByPath, touch, recentlyTouched, isIdOfInternalMetadata, isPluginMetadata, stripInternalMetadataPrefix, isChunk, askSelectString, askYesNo, askString, PeriodicProcessor, clearTouched, getPath, getPathWithoutPrefix, getPathFromTFile } from "./utils";
import { encrypt, tryDecrypt } from "./lib/src/e2ee_v2";
import { enableEncryption, isCloudantURI, isErrorOfMissingDoc, isValidRemoteCouchDBURI } from "./lib/src/utils_couchdb";
import { getGlobalStore, ObservableStore, observeStores } from "./lib/src/store";
import { lockStore, logMessageStore, logStore } from "./lib/src/stores";
import { setNoticeClass } from "./lib/src/wrapper";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
import { addPrefix, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
import { runWithLock } from "./lib/src/lock";
import { Semaphore } from "./lib/src/semaphore";
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
@@ -32,8 +32,6 @@ import { SetupLiveSync } from "./CmdSetupLiveSync";
setNoticeClass(Notice);
export default class ObsidianLiveSyncPlugin extends Plugin
implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv {
@@ -131,7 +129,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
delete transformedHeaders["content-length"];
delete transformedHeaders["Content-Length"];
const requestParam: RequestUrlParam = {
url: url as string,
url,
method: opts.method,
body: body,
headers: transformedHeaders,
@@ -205,12 +203,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
id2path(filename: string): string {
return id2path(filename);
id2path(id: DocumentID, entry: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
const tempId = id2path(id, entry);
if (stripPrefix && isIdOfInternalMetadata(tempId)) {
const out = stripInternalMetadataPrefix(tempId);
return out;
}
return tempId;
}
path2id(filename: string): string {
return path2id(filename);
getPath(entry: AnyEntry) {
return getPath(entry);
}
getPathWithoutPrefix(entry: AnyEntry) {
return getPathWithoutPrefix(entry);
}
async path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise<DocumentID> {
const destPath = addPrefix(filename, prefix);
return await path2id(destPath, this.settings.usePathObfuscation ? this.settings.passphrase : "");
}
createPouchDBInstance<T>(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database<T> {
if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb";
@@ -291,44 +302,46 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
showHistory(file: TFile | string) {
new DocumentHistoryModal(this.app, this, file).open();
showHistory(file: TFile | FilePathWithPrefix, id: DocumentID) {
new DocumentHistoryModal(this.app, this, file, id).open();
}
async fileHistory() {
const notes: { path: string, mtime: number }[] = [];
const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs()) {
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
notes.push({ id: doc._id, path: this.getPath(doc), dispPath: this.getPathWithoutPrefix(doc), mtime: doc.mtime });
}
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map(e => e.path);
const notesList = notes.map(e => e.dispPath);
const target = await askSelectString(this.app, "File to view History", notesList);
if (target) {
this.showHistory(target);
const targetId = notes.find(e => e.dispPath == target);
this.showHistory(targetId.path, undefined);
}
}
async pickFileForResolve() {
const notes: { path: string, mtime: number }[] = [];
const notes: { id: DocumentID, path: FilePathWithPrefix, dispPath: string, mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
notes.push({ id: doc._id, path: this.getPath(doc), dispPath: this.getPathWithoutPrefix(doc), mtime: doc.mtime });
}
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map(e => e.path);
const notesList = notes.map(e => e.dispPath);
if (notesList.length == 0) {
Logger("There are no conflicted documents", LOG_LEVEL.NOTICE);
return false;
}
const target = await askSelectString(this.app, "File to view History", notesList);
if (target) {
await this.resolveConflicted(target);
const targetItem = notes.find(e => e.dispPath == target);
await this.resolveConflicted(targetItem.path);
return true;
}
return false;
}
async resolveConflicted(target: string) {
if (isInternalMetadata(target)) {
async resolveConflicted(target: FilePathWithPrefix) {
if (isIdOfInternalMetadata(target)) {
await this.addOnHiddenFileSync.resolveConflictOnInternalFile(target);
} else if (isPluginMetadata(target)) {
await this.resolveConflictByNewerEntry(target);
@@ -346,7 +359,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (doc.type == "newnote" || doc.type == "plain") {
if (doc.deleted && (doc.mtime - limit) < 0) {
notes.push({ path: id2path(doc._id), mtime: doc.mtime, ttl: (doc.mtime - limit) / 1000 / 86400, doc: doc });
notes.push({ path: this.getPath(doc), mtime: doc.mtime, ttl: (doc.mtime - limit) / 1000 / 86400, doc: doc });
}
}
}
@@ -360,8 +373,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger(`Deletion history expired: ${v.path}`);
const delDoc = v.doc;
delDoc._deleted = true;
// console.dir(delDoc);
await this.localDatabase.localDatabase.put(delDoc);
await this.localDatabase.putRaw(delDoc);
}
Logger(`Checking expired file history done`);
}
@@ -453,7 +465,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger(`Additional safety scan..`, LOG_LEVEL.VERBOSE);
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: id2path(doc._id), mtime: doc.mtime });
notes.push({ path: this.getPath(doc), mtime: doc.mtime });
}
if (notes.length > 0) {
Logger(`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`, LOG_LEVEL.NOTICE);
@@ -555,14 +567,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
id: "livesync-dump",
name: "Dump information of this doc ",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
this.localDatabase.getDBEntry(getPathFromTFile(view.file), {}, true, false);
},
});
this.addCommand({
id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.",
editorCallback: async (editor: Editor, view: MarkdownView) => {
await this.showIfConflicted(view.file.path);
await this.showIfConflicted(getPathFromTFile(view.file));
},
});
@@ -600,7 +612,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
id: "livesync-history",
name: "Show history",
editorCallback: (editor: Editor, view: MarkdownView) => {
this.showHistory(view.file);
this.showHistory(view.file, null);
},
});
this.addCommand({
@@ -766,6 +778,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
this.settings = settings;
if ("workingEncrypt" in this.settings) delete this.settings.workingEncrypt;
if ("workingPassphrase" in this.settings) delete this.settings.workingPassphrase;
@@ -953,7 +966,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (this.settings.syncOnFileOpen && !this.suspended) {
await this.replicate();
}
await this.showIfConflicted(file.path);
await this.showIfConflicted(getPathFromTFile(file));
}
async applyBatchChange() {
@@ -1012,6 +1025,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const messageContent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
if (message instanceof Error) {
// debugger;
console.dir(message.stack);
}
const newMessage = timestamp + "->" + messageContent;
@@ -1091,21 +1105,21 @@ export default class ObsidianLiveSyncPlugin extends Plugin
async doc2storage(docEntry: EntryBody, file?: TFile, force?: boolean) {
const mode = file == undefined ? "create" : "modify";
const pathSrc = id2path(docEntry._id);
if (shouldBeIgnored(pathSrc)) {
const path = this.getPath(docEntry);
if (shouldBeIgnored(path)) {
return;
}
if (!this.isTargetFile(pathSrc)) return;
if (!this.isTargetFile(path)) return;
if (docEntry._deleted || docEntry.deleted) {
// This occurs not only when files are deleted, but also when conflicts are resolved.
// We have to check no other revisions are left.
const lastDocs = await this.localDatabase.getDBEntry(pathSrc);
const lastDocs = await this.localDatabase.getDBEntry(path);
if (lastDocs === false) {
await this.deleteVaultItem(file);
} else {
// it perhaps delete some revisions.
// may be we have to reload this
await this.pullFile(pathSrc, null, true);
await this.pullFile(path, null, true);
Logger(`delete skipped:${lastDocs._id}`, LOG_LEVEL.VERBOSE);
}
return;
@@ -1113,9 +1127,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const localMtime = ~~((file?.stat?.mtime || 0) / 1000);
const docMtime = ~~(docEntry.mtime / 1000);
const doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev });
const doc = await this.localDatabase.getDBEntry(path, { rev: docEntry._rev });
if (doc === false) return;
const path = id2path(doc._id);
const msg = `DB -> STORAGE (${mode}${force ? ",force" : ""},${doc.datatype}) `;
if (doc.datatype != "newnote" && doc.datatype != "plain") {
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL.NOTICE);
@@ -1134,7 +1147,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
outFile = await createFile(normalizePath(path), writeData, { ctime: doc.ctime, mtime: doc.mtime, });
} else {
await modifyFile(file, writeData, { ctime: doc.ctime, mtime: doc.mtime });
outFile = getAbstractFileByPath(file.path) as TFile;
outFile = getAbstractFileByPath(getPathFromTFile(file)) as TFile;
}
Logger(msg + path);
touch(outFile);
@@ -1172,7 +1185,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
handleDBChanged(change: EntryBody) {
// If the file is opened, we have to apply immediately
const af = app.workspace.getActiveFile();
if (af && af.path == id2path(change._id)) {
if (af && af.path == this.getPath(change)) {
this.queuedEntries = this.queuedEntries.filter(e => e._id != change._id);
return this.handleDBChangedAsync(change);
}
@@ -1188,15 +1201,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const entry = this.queuedEntries.shift();
// If the same file is to be manipulated, leave it to the last process.
if (this.queuedEntries.some(e => e._id == entry._id)) continue;
const path = getPath(entry);
try {
const releaser = await semaphore.acquire(1);
runWithLock(`dbchanged-${entry._id}`, false, async () => {
Logger(`Applying ${entry._id} (${entry._rev}) change...`, LOG_LEVEL.VERBOSE);
runWithLock(`dbchanged-${path}`, false, async () => {
Logger(`Applying ${path} (${entry._id}: ${entry._rev}) change...`, LOG_LEVEL.VERBOSE);
await this.handleDBChangedAsync(entry);
Logger(`Applied ${entry._id} (${entry._rev}) change...`);
Logger(`Applied ${path} (${entry._id}:${entry._rev}) change...`);
}).finally(() => { releaser(); });
} catch (ex) {
Logger(`Failed to apply the change of ${entry._id} (${entry._rev})`);
Logger(`Failed to apply the change of ${path} (${entry._id}:${entry._rev})`);
}
} while (this.queuedEntries.length > 0);
} finally {
@@ -1205,7 +1219,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
async handleDBChangedAsync(change: EntryBody) {
const targetFile = getAbstractFileByPath(id2path(change._id));
const targetFile = getAbstractFileByPath(this.getPathWithoutPrefix(change));
if (targetFile == null) {
if (change._deleted || change.deleted) {
return;
@@ -1232,17 +1246,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.doc2storage(doc, file);
queueConflictCheck();
} else {
const d = await this.localDatabase.getDBEntryMeta(id2path(change._id), { conflicts: true }, true);
const d = await this.localDatabase.getDBEntryMeta(this.getPath(change), { conflicts: true }, true);
if (d && !d._conflicts) {
await this.doc2storage(doc, file);
} else {
if (!queueConflictCheck()) {
Logger(`${id2path(change._id)} is conflicted, write to the storage has been pended.`, LOG_LEVEL.NOTICE);
Logger(`${this.getPath(change)} is conflicted, write to the storage has been pended.`, LOG_LEVEL.NOTICE);
}
}
}
} else {
Logger(`${id2path(change._id)} is already exist as the folder`);
Logger(`${this.getPath(change)} is already exist as the folder`);
}
}
@@ -1258,7 +1272,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
async loadQueuedFiles() {
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
const ids = JSON.parse(localStorage.getItem(lsKey) || "[]") as string[];
const ret = await this.localDatabase.localDatabase.allDocs({ keys: ids, include_docs: true });
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: ids, include_docs: true });
for (const doc of ret.rows) {
if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument<EntryBody & PouchDB.Core.AllDocsMeta>);
@@ -1274,11 +1288,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const now = new Date().getTime();
if (queue.missingChildren.length == 0) {
queue.done = true;
if (isInternalMetadata(queue.entry._id)) {
if (isIdOfInternalMetadata(queue.entry._id)) {
//system file
const filename = id2path(id2filenameInternalMetadata(queue.entry._id));
const filename = this.getPathWithoutPrefix(queue.entry);
this.addOnHiddenFileSync.procInternalFile(filename);
} else if (isValidPath(id2path(queue.entry._id))) {
} else if (isValidPath(this.getPath(queue.entry))) {
this.handleDBChanged(queue.entry);
} else {
Logger(`Skipped: ${queue.entry._id}`, LOG_LEVEL.VERBOSE);
@@ -1315,10 +1329,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (isNewFileCompleted) this.procQueuedFiles();
}
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
if (!this.isTargetFile(id2path(doc._id))) return;
const path = this.getPath(doc);
if (!this.isTargetFile(path)) return;
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
// Do not handle internal files if the feature has not been enabled.
if (isInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
if (isIdOfInternalMetadata(doc._id) && !this.settings.syncInternalFiles) return;
// It is better for your own safety, not to handle the following files
const ignoreFiles = [
"_design/replicate",
@@ -1326,15 +1341,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin
FLAGMD_REDFLAG2,
FLAGMD_REDFLAG3
];
if (!isInternalMetadata(doc._id) && ignoreFiles.contains(id2path(doc._id))) {
if (!isIdOfInternalMetadata(doc._id) && ignoreFiles.contains(path)) {
return;
}
if ((!isInternalMetadata(doc._id)) && skipOldFile) {
const info = getAbstractFileByPath(id2path(doc._id));
if ((!isIdOfInternalMetadata(doc._id)) && skipOldFile) {
const info = getAbstractFileByPath(stripAllPrefixes(path));
if (info && info instanceof TFile) {
const localMtime = ~~((info as TFile).stat.mtime / 1000);
const localMtime = ~~(info.stat.mtime / 1000);
const docMtime = ~~(doc.mtime / 1000);
//TODO: some margin required.
if (localMtime >= docMtime) {
@@ -1351,7 +1366,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
};
// If `Read chunks online` is enabled, retrieve chunks from the remote CouchDB directly.
if ((!this.settings.readChunksOnline) && "children" in doc) {
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
const c = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: doc.children, include_docs: false });
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
// fetch from remote
if (missing.length > 0) Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
@@ -1590,10 +1605,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const filesStorage = this.app.vault.getFiles().filter(e => this.isTargetFile(e));
const filesStorageName = filesStorage.map((e) => e.path);
Logger("Collecting local files on the DB", LOG_LEVEL.VERBOSE);
const filesDatabase = [] as string[]
for await (const docId of this.localDatabase.findAllDocNames()) {
const path = id2path(docId);
if (isValidPath(docId) && this.isTargetFile(path)) {
const filesDatabase = [] as FilePathWithPrefix[]
for await (const doc of this.localDatabase.findAllNormalDocs()) {
const path = getPath(doc);
if (isValidPath(path) && this.isTargetFile(path)) {
filesDatabase.push(path);
}
}
@@ -1605,7 +1620,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
initialScan = true;
Logger("Database looks empty, save files as initial sync data");
}
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1);
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(getPathFromTFile(e)) == -1);
const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1);
const onlyInStorageNames = onlyInStorage.map((e) => e.path);
@@ -1659,9 +1674,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin
caches = await this.kvDB.get<{ [key: string]: { storageMtime: number; docMtime: number } }>("diff-caches") || {};
const docsCount = syncFiles.length;
do {
const syncFilesX = syncFiles.splice(0, 100);
const docs = await this.localDatabase.localDatabase.allDocs({ keys: syncFilesX.map(e => path2id(e.path)), include_docs: true })
const syncFilesToSync = syncFilesX.map((e) => ({ file: e, doc: docs.rows.find(ee => ee.id == path2id(e.path)).doc as LoadedEntry }));
const syncFilesXSrc = syncFiles.splice(0, 100);
const syncFilesX = [] as { file: TFile, id: DocumentID }[];
for (const file of syncFilesXSrc) {
const id = await this.path2id(getPathFromTFile(file));
syncFilesX.push({ file: file, id: id });
}
const docs = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: syncFilesX.map(e => e.id), include_docs: true })
const docsMap = docs.rows.reduce((p, c) => ({ ...p, [c.id]: c.doc }), {} as Record<DocumentID, EntryDoc>)
const syncFilesToSync = syncFilesX.map((e) => ({ file: e.file, doc: docsMap[e.id] as LoadedEntry }));
await runAll(`CHECK FILE STATUS:${syncFiles.length}/${docsCount}`, syncFilesToSync, async (e) => {
caches = await this.syncFileBetweenDBandStorage(e.file, e.doc, initialScan, caches);
});
@@ -1680,7 +1702,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
// --> conflict resolving
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
async getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise<false | diff_result_leaf> {
try {
const doc = await this.localDatabase.getDBEntry(path, { rev: rev }, false, false, true);
if (doc === false) return false;
@@ -1705,7 +1727,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
return false;
}
//TODO: TIDY UP
async mergeSensibly(path: string, baseRev: string, currentRev: string, conflictedRev: string): Promise<Diff[] | false> {
async mergeSensibly(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise<Diff[] | false> {
const baseLeaf = await this.getConflictedDoc(path, baseRev);
const leftLeaf = await this.getConflictedDoc(path, currentRev);
const rightLeaf = await this.getConflictedDoc(path, conflictedRev);
@@ -1862,7 +1884,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
async mergeObject(path: string, baseRev: string, currentRev: string, conflictedRev: string): Promise<string | false> {
async mergeObject(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise<string | false> {
try {
const baseLeaf = await this.getConflictedDoc(path, baseRev);
const leftLeaf = await this.getConflictedDoc(path, currentRev);
@@ -1917,7 +1939,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
* @param path the file location
* @returns true -> resolved, false -> nothing to do, or check result.
*/
async getConflictedStatus(path: string): Promise<diff_check_result> {
async getConflictedStatus(path: FilePathWithPrefix): Promise<diff_check_result> {
const test = await this.localDatabase.getDBEntry(path, { conflicts: true, revs_info: true }, false, false, true);
if (test === false) return false;
if (test == null) return false;
@@ -1928,7 +1950,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
const conflictedRev = conflicts[0];
const conflictedRevNo = Number(conflictedRev.split("-")[0]);
//Search
const revFrom = (await this.localDatabase.localDatabase.get<EntryDoc>(path2id(path), { revs_info: true }));
const revFrom = (await this.localDatabase.getRaw<EntryDoc>(await this.path2id(path), { revs_info: true }));
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
let p = undefined;
if (commonBase) {
@@ -1956,7 +1978,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
// remove conflicted revision.
await this.localDatabase.deleteDBEntry(path, { rev: conflictedRev });
const file = getAbstractFileByPath(path) as TFile;
const file = getAbstractFileByPath(stripAllPrefixes(path)) as TFile;
if (file) {
await this.app.vault.modify(file, p);
await this.updateIntoDB(file);
@@ -2020,7 +2042,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
};
}
showMergeDialog(filename: string, conflictCheckResult: diff_result): Promise<boolean> {
showMergeDialog(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean> {
return new Promise((res, rej) => {
Logger("open conflict dialog", LOG_LEVEL.VERBOSE);
new ConflictResolveModal(this.app, filename, conflictCheckResult, async (selected) => {
@@ -2040,7 +2062,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
// delete conflicted revision and write a new file, store it again.
const p = conflictCheckResult.diff.map((e) => e[1]).join("");
await this.localDatabase.deleteDBEntry(filename, { rev: testDoc._conflicts[0] });
const file = getAbstractFileByPath(filename) as TFile;
const file = getAbstractFileByPath(stripAllPrefixes(filename)) as TFile;
if (file) {
await this.app.vault.modify(file, p);
await this.updateIntoDB(file);
@@ -2076,25 +2098,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}).open();
});
}
conflictedCheckFiles: string[] = [];
conflictedCheckFiles: FilePath[] = [];
// queueing the conflicted file check
conflictedCheckTimer: number;
queueConflictedCheck(file: TFile) {
this.conflictedCheckFiles = this.conflictedCheckFiles.filter((e) => e != file.path);
this.conflictedCheckFiles.push(file.path);
this.conflictedCheckFiles.push(getPathFromTFile(file));
if (this.conflictedCheckTimer != null) {
window.clearTimeout(this.conflictedCheckTimer);
}
this.conflictedCheckTimer = window.setTimeout(async () => {
this.conflictedCheckTimer = null;
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as string[];
const checkFiles = JSON.parse(JSON.stringify(this.conflictedCheckFiles)) as FilePath[];
for (const filename of checkFiles) {
try {
const file = getAbstractFileByPath(filename);
if (file != null && file instanceof TFile) {
await this.showIfConflicted(file.path);
await this.showIfConflicted(getPathFromTFile(file));
}
} catch (ex) {
Logger(ex);
@@ -2103,7 +2125,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}, 100);
}
async showIfConflicted(filename: string) {
async showIfConflicted(filename: FilePathWithPrefix) {
await runWithLock("conflicted", false, async () => {
const conflictCheckResult = await this.getConflictedStatus(filename);
if (conflictCheckResult === false) {
@@ -2126,9 +2148,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
});
}
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
const targetFile = getAbstractFileByPath(id2path(filename));
if (!this.isTargetFile(id2path(filename))) return;
async pullFile(filename: FilePathWithPrefix, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
const targetFile = getAbstractFileByPath(stripAllPrefixes(filename));
if (!this.isTargetFile(filename)) return;
if (targetFile == null) {
//have to create;
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
@@ -2186,7 +2208,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
//newer database file.
Logger("STORAGE <- DB :" + file.path);
Logger(`${storageMtime} < ${docMtime}`);
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
const docx = await this.localDatabase.getDBEntry(getPathFromTFile(file), null, false, false);
if (docx != false) {
await this.doc2storage(docx, file);
} else {
@@ -2241,9 +2263,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin
datatype = "plain";
}
}
const fullPath = path2id(file.path);
const fullPath = getPathFromTFile(file);
const id = await this.path2id(fullPath);
const d: LoadedEntry = {
_id: fullPath,
_id: id,
path: getPathFromTFile(file),
data: content,
ctime: file.stat.ctime,
mtime: file.stat.mtime,
@@ -2292,7 +2316,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
async deleteFromDB(file: TFile) {
if (!this.isTargetFile(file)) return;
const fullPath = file.path;
const fullPath = getPathFromTFile(file);
Logger(`deleteDB By path:${fullPath}`);
await this.deleteFromDBbyPath(fullPath);
if (this.settings.syncOnSave && !this.suspended) {
@@ -2300,7 +2324,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
}
}
async deleteFromDBbyPath(fullPath: string) {
async deleteFromDBbyPath(fullPath: FilePath) {
await this.localDatabase.deleteDBEntry(fullPath);
if (this.settings.syncOnSave && !this.suspended) {
await this.replicate();
@@ -2352,24 +2376,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin
await this.app.vault.adapter.append(file.path, "", { ctime: file.ctime, mtime: file.mtime });
}
async resolveConflictByNewerEntry(id: string) {
const doc = await this.localDatabase.localDatabase.get(id, { conflicts: true });
async resolveConflictByNewerEntry(path: FilePathWithPrefix) {
const id = await this.path2id(path);
const doc = await this.localDatabase.getRaw<AnyEntry>(id, { conflicts: true });
// If there is no conflict, return with false.
if (!("_conflicts" in doc)) return false;
if (doc._conflicts.length == 0) return false;
Logger(`Hidden file conflicted:${id2filenameInternalMetadata(id)}`);
Logger(`Hidden file conflicted:${this.getPath(doc)}`);
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
const revA = doc._rev;
const revB = conflicts[0];
const revBDoc = await this.localDatabase.localDatabase.get(id, { rev: revB });
const revBDoc = await this.localDatabase.getRaw<EntryDoc>(id, { rev: revB });
// determine which revision should been deleted.
// simply check modified time
const mtimeA = ("mtime" in doc && doc.mtime) || 0;
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
const delRev = mtimeA < mtimeB ? revA : revB;
// delete older one.
await this.localDatabase.localDatabase.remove(id, delRev);
Logger(`Older one has been deleted:${id2filenameInternalMetadata(id)}`);
await this.localDatabase.removeRaw(id, delRev);
Logger(`Older one has been deleted:${this.getPath(doc)}`);
return true;
}