|
|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView } from "obsidian";
|
|
|
|
|
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest } from "obsidian";
|
|
|
|
|
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
|
|
|
|
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
|
|
|
|
|
import xxhash from "xxhash-wasm";
|
|
|
|
|
@@ -51,6 +51,8 @@ interface ObsidianLiveSyncSettings {
|
|
|
|
|
doNotDeleteFolder: boolean;
|
|
|
|
|
resolveConflictsByNewerFile: boolean;
|
|
|
|
|
batchSave: boolean;
|
|
|
|
|
deviceAndVaultName: string;
|
|
|
|
|
usePluginSettings: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
|
|
|
|
@@ -80,7 +82,10 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
|
|
|
|
|
doNotDeleteFolder: false,
|
|
|
|
|
resolveConflictsByNewerFile: false,
|
|
|
|
|
batchSave: false,
|
|
|
|
|
deviceAndVaultName: "",
|
|
|
|
|
usePluginSettings: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface Entry {
|
|
|
|
|
_id: string;
|
|
|
|
|
data: string;
|
|
|
|
|
@@ -121,6 +126,22 @@ type LoadedEntry = Entry & {
|
|
|
|
|
datatype: "plain" | "newnote";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface PluginDataEntry {
|
|
|
|
|
_id: string;
|
|
|
|
|
deviceVaultName: string;
|
|
|
|
|
mtime: number;
|
|
|
|
|
manifest: PluginManifest;
|
|
|
|
|
mainJs: string;
|
|
|
|
|
manifestJson: string;
|
|
|
|
|
styleCss?: string;
|
|
|
|
|
// it must be encrypted.
|
|
|
|
|
dataJson?: string;
|
|
|
|
|
_rev?: string;
|
|
|
|
|
_deleted?: boolean;
|
|
|
|
|
_conflicts?: string[];
|
|
|
|
|
type: "plugin";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface EntryLeaf {
|
|
|
|
|
_id: string;
|
|
|
|
|
data: string;
|
|
|
|
|
@@ -156,7 +177,7 @@ interface EntryNodeInfo {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EntryBody = Entry | NewEntry | PlainEntry;
|
|
|
|
|
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
|
|
|
|
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo | PluginDataEntry;
|
|
|
|
|
|
|
|
|
|
type diff_result_leaf = {
|
|
|
|
|
rev: string;
|
|
|
|
|
@@ -264,8 +285,8 @@ const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
|
|
|
|
if (uri.startsWith("http://")) return true;
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise<false | { db: PouchDB.Database; info: any }> => {
|
|
|
|
|
if (!isValidRemoteCouchDBURI(uri)) return false;
|
|
|
|
|
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }): Promise<string | { db: PouchDB.Database; info: any }> => {
|
|
|
|
|
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
|
|
|
|
let db = new PouchDB(uri, {
|
|
|
|
|
auth,
|
|
|
|
|
});
|
|
|
|
|
@@ -273,8 +294,12 @@ const connectRemoteCouchDB = async (uri: string, auth: { username: string; passw
|
|
|
|
|
let info = await db.info();
|
|
|
|
|
return { db: db, info: info };
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
let msg = `${ex.name}:${ex.message}`;
|
|
|
|
|
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
|
|
|
|
|
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
|
|
|
|
|
}
|
|
|
|
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
|
|
|
return false;
|
|
|
|
|
return msg;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// check the version of remote.
|
|
|
|
|
@@ -324,6 +349,7 @@ const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): P
|
|
|
|
|
await db.put(vi);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function isValidPath(filename: string): boolean {
|
|
|
|
|
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g;
|
|
|
|
|
let x = filename.replace(regex, "_");
|
|
|
|
|
@@ -332,10 +358,22 @@ function isValidPath(filename: string): boolean {
|
|
|
|
|
return sx == filename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For backward compatibility, using the path for determining id.
|
|
|
|
|
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
|
|
|
|
|
// The first slash will be deleted when the path is normalized.
|
|
|
|
|
function path2id(filename: string): string {
|
|
|
|
|
let x = normalizePath(filename);
|
|
|
|
|
if (x.startsWith("_")) x = "/" + x;
|
|
|
|
|
return x;
|
|
|
|
|
}
|
|
|
|
|
function id2path(filename: string): string {
|
|
|
|
|
return normalizePath(filename);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default Logger.
|
|
|
|
|
let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
|
|
|
|
let timestamp = new Date().toLocaleString();
|
|
|
|
|
let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2);
|
|
|
|
|
let messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
|
|
|
|
let newmessage = timestamp + "->" + messagecontent;
|
|
|
|
|
console.log(newmessage);
|
|
|
|
|
};
|
|
|
|
|
@@ -546,6 +584,7 @@ class LocalPouchDB {
|
|
|
|
|
// this.initializeDatabase();
|
|
|
|
|
}
|
|
|
|
|
close() {
|
|
|
|
|
Logger("Database closed (by close)");
|
|
|
|
|
this.isReady = false;
|
|
|
|
|
if (this.changeHandler != null) {
|
|
|
|
|
this.changeHandler.cancel();
|
|
|
|
|
@@ -607,6 +646,7 @@ class LocalPouchDB {
|
|
|
|
|
await this.localDatabase.put(nodeinfo);
|
|
|
|
|
}
|
|
|
|
|
this.localDatabase.on("close", () => {
|
|
|
|
|
Logger("Database closed.");
|
|
|
|
|
this.isReady = false;
|
|
|
|
|
});
|
|
|
|
|
this.nodeid = nodeinfo.nodeid;
|
|
|
|
|
@@ -625,6 +665,7 @@ class LocalPouchDB {
|
|
|
|
|
});
|
|
|
|
|
this.changeHandler = changes;
|
|
|
|
|
this.isReady = true;
|
|
|
|
|
Logger("Database is now ready.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async prepareHashFunctions() {
|
|
|
|
|
@@ -650,7 +691,7 @@ class LocalPouchDB {
|
|
|
|
|
waitForLeafReady(id: string): Promise<boolean> {
|
|
|
|
|
return new Promise((res, rej) => {
|
|
|
|
|
// Set timeout.
|
|
|
|
|
let timer = setTimeout(() => rej(false), LEAF_WAIT_TIMEOUT);
|
|
|
|
|
let timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT);
|
|
|
|
|
if (typeof this.leafArrivedCallbacks[id] == "undefined") {
|
|
|
|
|
this.leafArrivedCallbacks[id] = [];
|
|
|
|
|
}
|
|
|
|
|
@@ -661,7 +702,7 @@ class LocalPouchDB {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDBLeaf(id: string): Promise<string> {
|
|
|
|
|
async getDBLeaf(id: string, waitForReady: boolean): Promise<string> {
|
|
|
|
|
// when in cache, use that.
|
|
|
|
|
if (this.hashCacheRev[id]) {
|
|
|
|
|
return this.hashCacheRev[id];
|
|
|
|
|
@@ -683,7 +724,7 @@ class LocalPouchDB {
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`retrive leaf, but it was not leaf.`);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
if (ex.status && ex.status == 404) {
|
|
|
|
|
if (ex.status && ex.status == 404 && waitForReady) {
|
|
|
|
|
// just leaf is not ready.
|
|
|
|
|
// wait for on
|
|
|
|
|
if ((await this.waitForLeafReady(id)) === false) {
|
|
|
|
|
@@ -720,7 +761,8 @@ class LocalPouchDB {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDBEntryMeta(id: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
|
|
|
|
|
async getDBEntryMeta(path: string, opt?: PouchDB.Core.GetOptions): Promise<false | LoadedEntry> {
|
|
|
|
|
let id = path2id(path);
|
|
|
|
|
try {
|
|
|
|
|
let obj: EntryDocResponse = null;
|
|
|
|
|
if (opt) {
|
|
|
|
|
@@ -737,6 +779,10 @@ class LocalPouchDB {
|
|
|
|
|
// retrieve metadata only
|
|
|
|
|
if (!obj.type || (obj.type && obj.type == "notes") || obj.type == "newnote" || obj.type == "plain") {
|
|
|
|
|
let note = obj as Entry;
|
|
|
|
|
let children: string[] = [];
|
|
|
|
|
if (obj.type == "newnote" || obj.type == "plain") {
|
|
|
|
|
children = obj.children;
|
|
|
|
|
}
|
|
|
|
|
let doc: LoadedEntry & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = {
|
|
|
|
|
data: "",
|
|
|
|
|
_id: note._id,
|
|
|
|
|
@@ -746,7 +792,7 @@ class LocalPouchDB {
|
|
|
|
|
_deleted: obj._deleted,
|
|
|
|
|
_rev: obj._rev,
|
|
|
|
|
_conflicts: obj._conflicts,
|
|
|
|
|
children: [],
|
|
|
|
|
children: children,
|
|
|
|
|
datatype: "newnote",
|
|
|
|
|
};
|
|
|
|
|
return doc;
|
|
|
|
|
@@ -759,7 +805,8 @@ class LocalPouchDB {
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
async getDBEntry(id: string, opt?: PouchDB.Core.GetOptions, dump = false): Promise<false | LoadedEntry> {
|
|
|
|
|
async getDBEntry(path: string, opt?: PouchDB.Core.GetOptions, dump = false, waitForReady = true): Promise<false | LoadedEntry> {
|
|
|
|
|
let id = path2id(path);
|
|
|
|
|
try {
|
|
|
|
|
let obj: EntryDocResponse = null;
|
|
|
|
|
if (opt) {
|
|
|
|
|
@@ -808,13 +855,14 @@ class LocalPouchDB {
|
|
|
|
|
}
|
|
|
|
|
let childrens: string[];
|
|
|
|
|
try {
|
|
|
|
|
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e)));
|
|
|
|
|
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
|
|
|
|
|
if (dump) {
|
|
|
|
|
Logger(`childrens:`);
|
|
|
|
|
Logger(childrens);
|
|
|
|
|
}
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
Logger(`Something went wrong on reading elements of ${obj._id} from database.`, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
|
|
|
this.corruptedEntries[obj._id] = obj;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
@@ -856,7 +904,8 @@ class LocalPouchDB {
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
async deleteDBEntry(id: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
|
|
|
|
|
async deleteDBEntry(path: string, opt?: PouchDB.Core.GetOptions): Promise<boolean> {
|
|
|
|
|
let id = path2id(path);
|
|
|
|
|
try {
|
|
|
|
|
let obj: EntryDocResponse = null;
|
|
|
|
|
if (opt) {
|
|
|
|
|
@@ -897,12 +946,13 @@ class LocalPouchDB {
|
|
|
|
|
throw ex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async deleteDBEntryPrefix(prefix: string): Promise<boolean> {
|
|
|
|
|
async deleteDBEntryPrefix(prefixSrc: string): Promise<boolean> {
|
|
|
|
|
// delete database entries by prefix.
|
|
|
|
|
// it called from folder deletion.
|
|
|
|
|
let c = 0;
|
|
|
|
|
let readCount = 0;
|
|
|
|
|
let delDocs: string[] = [];
|
|
|
|
|
let prefix = path2id(prefixSrc);
|
|
|
|
|
do {
|
|
|
|
|
let result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true });
|
|
|
|
|
readCount = result.rows.length;
|
|
|
|
|
@@ -1119,7 +1169,7 @@ class LocalPouchDB {
|
|
|
|
|
} else {
|
|
|
|
|
Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger(item);
|
|
|
|
|
this.disposeHashCache();
|
|
|
|
|
// this.disposeHashCache();
|
|
|
|
|
saved = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1195,10 +1245,10 @@ class LocalPouchDB {
|
|
|
|
|
password: setting.couchDB_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (dbret === false) {
|
|
|
|
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (typeof dbret === "string") {
|
|
|
|
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (notice != null) notice.hide();
|
|
|
|
|
return rej(`could not connect to ${uri}`);
|
|
|
|
|
return rej(`could not connect to ${uri}:${dbret}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let syncOptionBase: PouchDB.Replication.SyncOptions = {
|
|
|
|
|
@@ -1267,8 +1317,8 @@ class LocalPouchDB {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (dbret === false) {
|
|
|
|
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (typeof dbret === "string") {
|
|
|
|
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1434,6 +1484,7 @@ class LocalPouchDB {
|
|
|
|
|
this.changeHandler.cancel();
|
|
|
|
|
}
|
|
|
|
|
await this.closeReplication();
|
|
|
|
|
Logger("Database closed for reset Database.");
|
|
|
|
|
this.isReady = false;
|
|
|
|
|
await this.localDatabase.destroy();
|
|
|
|
|
this.localDatabase = null;
|
|
|
|
|
@@ -1449,7 +1500,7 @@ class LocalPouchDB {
|
|
|
|
|
password: setting.couchDB_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
let con = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (con === false) return;
|
|
|
|
|
if (typeof con == "string") return;
|
|
|
|
|
try {
|
|
|
|
|
await con.db.destroy();
|
|
|
|
|
Logger("Remote Database Destroyed", LOG_LEVEL.NOTICE);
|
|
|
|
|
@@ -1466,7 +1517,7 @@ class LocalPouchDB {
|
|
|
|
|
password: setting.couchDB_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
let con2 = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (con2 === false) return;
|
|
|
|
|
if (typeof con2 === "string") return;
|
|
|
|
|
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) {
|
|
|
|
|
@@ -1476,8 +1527,8 @@ class LocalPouchDB {
|
|
|
|
|
password: setting.couchDB_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (dbret === false) {
|
|
|
|
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (typeof dbret === "string") {
|
|
|
|
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1510,8 +1561,8 @@ class LocalPouchDB {
|
|
|
|
|
password: setting.couchDB_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
|
|
|
|
if (dbret === false) {
|
|
|
|
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (typeof dbret === "string") {
|
|
|
|
|
Logger(`could not connect to ${uri}:${dbret}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1621,6 +1672,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
this.settings.liveSync = false;
|
|
|
|
|
this.settings.syncOnSave = false;
|
|
|
|
|
this.settings.syncOnStart = false;
|
|
|
|
|
this.settings.syncOnFileOpen = false;
|
|
|
|
|
this.settings.periodicReplication = false;
|
|
|
|
|
this.settings.versionUpFlash = "I changed specifications incompatiblly, so when you enable sync again, be sure to made version up all nother devides.";
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
@@ -1698,7 +1750,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
name: "Dump informations of this doc ",
|
|
|
|
|
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
|
|
|
//this.replicate();
|
|
|
|
|
this.localDatabase.getDBEntry(view.file.path, {}, true);
|
|
|
|
|
this.localDatabase.getDBEntry(view.file.path, {}, true, false);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
this.addCommand({
|
|
|
|
|
@@ -1850,6 +1902,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
// If batchsave is enabled, queue all changes and do nothing.
|
|
|
|
|
if (this.settings.batchSave) {
|
|
|
|
|
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
|
|
|
|
|
this.refreshStatusText();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.watchVaultChangeAsync(file, ...args);
|
|
|
|
|
@@ -1872,6 +1925,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.refreshStatusText();
|
|
|
|
|
return Promise.all(promises);
|
|
|
|
|
}
|
|
|
|
|
batchFileChange: string[] = [];
|
|
|
|
|
@@ -1957,7 +2011,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}
|
|
|
|
|
let valutName = this.app.vault.getName();
|
|
|
|
|
let timestamp = new Date().toLocaleString();
|
|
|
|
|
let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2);
|
|
|
|
|
let messagecontent = typeof message == "string" ? message : message instanceof Error ? `${message.name}:${message.message}` : JSON.stringify(message, null, 2);
|
|
|
|
|
let newmessage = timestamp + "->" + messagecontent;
|
|
|
|
|
|
|
|
|
|
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
|
|
|
|
|
@@ -1993,37 +2047,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async doc2storage_create(docEntry: EntryBody, force?: boolean) {
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(docEntry._id, { rev: docEntry._rev });
|
|
|
|
|
let pathSrc = id2path(docEntry._id);
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(pathSrc, { rev: docEntry._rev });
|
|
|
|
|
if (doc === false) return;
|
|
|
|
|
let path = id2path(doc._id);
|
|
|
|
|
if (doc.datatype == "newnote") {
|
|
|
|
|
let bin = base64ToArrayBuffer(doc.data);
|
|
|
|
|
if (bin != null) {
|
|
|
|
|
if (!isValidPath(doc._id)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (!isValidPath(path)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.ensureDirectory(doc._id);
|
|
|
|
|
await this.ensureDirectory(path);
|
|
|
|
|
try {
|
|
|
|
|
let newfile = await this.app.vault.createBinary(normalizePath(doc._id), bin, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger("live : write to local (newfile:b) " + doc._id);
|
|
|
|
|
let newfile = await this.app.vault.createBinary(normalizePath(path), bin, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger("live : write to local (newfile:b) " + path);
|
|
|
|
|
await this.app.vault.trigger("create", newfile);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
Logger("could not write to local (newfile:bin) " + doc._id, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger("could not write to local (newfile:bin) " + path, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (doc.datatype == "plain") {
|
|
|
|
|
if (!isValidPath(doc._id)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (!isValidPath(path)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.ensureDirectory(doc._id);
|
|
|
|
|
await this.ensureDirectory(path);
|
|
|
|
|
try {
|
|
|
|
|
let newfile = await this.app.vault.create(normalizePath(doc._id), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger("live : write to local (newfile:p) " + doc._id);
|
|
|
|
|
let newfile = await this.app.vault.create(normalizePath(path), doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger("live : write to local (newfile:p) " + path);
|
|
|
|
|
await this.app.vault.trigger("create", newfile);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
Logger("could not write to local (newfile:plain) " + doc._id, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger("could not write to local (newfile:plain) " + path, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
@@ -2048,16 +2104,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async doc2storate_modify(docEntry: EntryBody, file: TFile, force?: boolean) {
|
|
|
|
|
let pathSrc = id2path(docEntry._id);
|
|
|
|
|
if (docEntry._deleted) {
|
|
|
|
|
//basically pass.
|
|
|
|
|
//but if there're no docs left, delete file.
|
|
|
|
|
let lastDocs = await this.localDatabase.getDBEntry(docEntry._id);
|
|
|
|
|
let lastDocs = await this.localDatabase.getDBEntry(pathSrc);
|
|
|
|
|
if (lastDocs === false) {
|
|
|
|
|
await this.deleteVaultItem(file);
|
|
|
|
|
} else {
|
|
|
|
|
// it perhaps delete some revisions.
|
|
|
|
|
// may be we have to reload this
|
|
|
|
|
await this.pullFile(docEntry._id, null, true);
|
|
|
|
|
await this.pullFile(pathSrc, null, true);
|
|
|
|
|
Logger(`delete skipped:${lastDocs._id}`);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
@@ -2065,38 +2122,39 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
let localMtime = ~~(file.stat.mtime / 1000);
|
|
|
|
|
let docMtime = ~~(docEntry.mtime / 1000);
|
|
|
|
|
if (localMtime < docMtime || force) {
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(docEntry._id);
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(pathSrc);
|
|
|
|
|
let msg = "livesync : newer local files so write to local:" + file.path;
|
|
|
|
|
if (force) msg = "livesync : force write to local:" + file.path;
|
|
|
|
|
if (doc === false) return;
|
|
|
|
|
let path = id2path(doc._id);
|
|
|
|
|
if (doc.datatype == "newnote") {
|
|
|
|
|
let bin = base64ToArrayBuffer(doc.data);
|
|
|
|
|
if (bin != null) {
|
|
|
|
|
if (!isValidPath(doc._id)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (!isValidPath(path)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.ensureDirectory(doc._id);
|
|
|
|
|
await this.ensureDirectory(path);
|
|
|
|
|
try {
|
|
|
|
|
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger(msg);
|
|
|
|
|
await this.app.vault.trigger("modify", file);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
Logger("could not write to local (modify:bin) " + doc._id, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger("could not write to local (modify:bin) " + path, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (doc.datatype == "plain") {
|
|
|
|
|
if (!isValidPath(doc._id)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${doc._id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (!isValidPath(path)) {
|
|
|
|
|
Logger(`The file that having platform dependent name has been arrived. This file has skipped: ${path}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.ensureDirectory(doc._id);
|
|
|
|
|
await this.ensureDirectory(path);
|
|
|
|
|
try {
|
|
|
|
|
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
|
|
|
|
Logger(msg);
|
|
|
|
|
await this.app.vault.trigger("modify", file);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
Logger("could not write to local (modify:plain) " + doc._id, LOG_LEVEL.NOTICE);
|
|
|
|
|
Logger("could not write to local (modify:plain) " + path, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Logger("live : New data imcoming, but we cound't parse that.:" + doc.datatype + "-", LOG_LEVEL.NOTICE);
|
|
|
|
|
@@ -2111,7 +2169,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}
|
|
|
|
|
async handleDBChanged(change: EntryBody) {
|
|
|
|
|
let allfiles = this.app.vault.getFiles();
|
|
|
|
|
let targetFiles = allfiles.filter((e) => e.path == change._id);
|
|
|
|
|
let targetFiles = allfiles.filter((e) => e.path == id2path(change._id));
|
|
|
|
|
if (targetFiles.length == 0) {
|
|
|
|
|
if (change._deleted) {
|
|
|
|
|
return;
|
|
|
|
|
@@ -2133,10 +2191,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
this.refreshStatusText();
|
|
|
|
|
for (var change of docs) {
|
|
|
|
|
if (this.localDatabase.isSelfModified(change._id, change._rev)) {
|
|
|
|
|
return;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
|
|
|
|
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
|
|
|
|
if (change._id.startsWith("ps:")) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (change._id.startsWith("h:")) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo" && change.type != "plugin") {
|
|
|
|
|
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
|
|
|
|
await this.handleDBChanged(change);
|
|
|
|
|
}
|
|
|
|
|
if (change.type == "versioninfo") {
|
|
|
|
|
@@ -2199,7 +2263,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
w = "?";
|
|
|
|
|
}
|
|
|
|
|
this.statusBar.title = this.localDatabase.syncStatus;
|
|
|
|
|
this.statusBar.setText(`Sync:${w} ↑${sent} ↓${arrived}`);
|
|
|
|
|
let waiting = "";
|
|
|
|
|
if (this.settings.batchSave) {
|
|
|
|
|
waiting = " " + this.batchFileChange.map((e) => "🚀").join("");
|
|
|
|
|
}
|
|
|
|
|
this.statusBar.setText(`Sync:${w} ↑${sent} ↓${arrived}${waiting}`);
|
|
|
|
|
}
|
|
|
|
|
async replicate(showMessage?: boolean) {
|
|
|
|
|
if (this.settings.versionUpFlash != "") {
|
|
|
|
|
@@ -2235,7 +2303,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
const filesStorage = this.app.vault.getFiles();
|
|
|
|
|
const filesStorageName = filesStorage.map((e) => e.path);
|
|
|
|
|
const wf = await this.localDatabase.localDatabase.allDocs();
|
|
|
|
|
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && e.id != "obsydian_livesync_version").map((e) => normalizePath(e.id));
|
|
|
|
|
const filesDatabase = wf.rows.filter((e) => !e.id.startsWith("h:") && !e.id.startsWith("ps:") && e.id != "obsydian_livesync_version").map((e) => id2path(e.id));
|
|
|
|
|
|
|
|
|
|
const onlyInStorage = filesStorage.filter((e) => filesDatabase.indexOf(e.path) == -1);
|
|
|
|
|
const onlyInDatabase = filesDatabase.filter((e) => filesStorageName.indexOf(e) == -1);
|
|
|
|
|
@@ -2250,18 +2318,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
const count = objects.length;
|
|
|
|
|
Logger(procedurename);
|
|
|
|
|
let i = 0;
|
|
|
|
|
let lastTicks = performance.now() + 2000;
|
|
|
|
|
// let lastTicks = performance.now() + 2000;
|
|
|
|
|
let procs = objects.map(async (e) => {
|
|
|
|
|
try {
|
|
|
|
|
// debugger;
|
|
|
|
|
// Logger("hello?")
|
|
|
|
|
await callback(e);
|
|
|
|
|
i++;
|
|
|
|
|
if (lastTicks < performance.now()) {
|
|
|
|
|
if (i % 25 == 0) {
|
|
|
|
|
const notify = `${procedurename} : ${i}/${count}`;
|
|
|
|
|
if (notice != null) notice.setMessage(notify);
|
|
|
|
|
Logger(notify);
|
|
|
|
|
lastTicks = performance.now() + 2000;
|
|
|
|
|
// lastTicks = performance.now() + 2000;
|
|
|
|
|
// this.statusBar.setText(notify);
|
|
|
|
|
}
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
@@ -2293,7 +2361,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
});
|
|
|
|
|
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
|
|
|
|
Logger(`Pull from db:${e}`);
|
|
|
|
|
await this.pullFile(e, filesStorage);
|
|
|
|
|
await this.pullFile(e, filesStorage, false, null, false);
|
|
|
|
|
});
|
|
|
|
|
await runAll("CHECK FILE STATUS", syncFiles, async (e) => {
|
|
|
|
|
await this.syncFileBetweenDBandStorage(e, filesStorage);
|
|
|
|
|
@@ -2435,7 +2503,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}
|
|
|
|
|
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
|
|
|
|
await this.pullFile(path, null, true);
|
|
|
|
|
Logger(`automaticaly merged (newerFileResolve) :${path}`);
|
|
|
|
|
Logger(`Automaticaly merged (newerFileResolve) :${path}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// make diff.
|
|
|
|
|
@@ -2494,20 +2562,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
}).open();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string) {
|
|
|
|
|
async pullFile(filename: string, fileList?: TFile[], force?: boolean, rev?: string, waitForReady: boolean = true) {
|
|
|
|
|
if (!fileList) {
|
|
|
|
|
fileList = this.app.vault.getFiles();
|
|
|
|
|
}
|
|
|
|
|
let targetFiles = fileList.filter((e) => e.path == normalizePath(filename));
|
|
|
|
|
let targetFiles = fileList.filter((e) => e.path == id2path(filename));
|
|
|
|
|
if (targetFiles.length == 0) {
|
|
|
|
|
//have to create;
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
|
|
|
|
if (doc === false) return;
|
|
|
|
|
await this.doc2storage_create(doc, force);
|
|
|
|
|
} else if (targetFiles.length == 1) {
|
|
|
|
|
//normal case
|
|
|
|
|
let file = targetFiles[0];
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null);
|
|
|
|
|
let doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
|
|
|
|
if (doc === false) return;
|
|
|
|
|
await this.doc2storate_modify(doc, file, force);
|
|
|
|
|
} else {
|
|
|
|
|
@@ -2519,18 +2587,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
async syncFileBetweenDBandStorage(file: TFile, fileList?: TFile[]) {
|
|
|
|
|
let doc = await this.localDatabase.getDBEntryMeta(file.path);
|
|
|
|
|
if (doc === false) return;
|
|
|
|
|
|
|
|
|
|
let storageMtime = ~~(file.stat.mtime / 1000);
|
|
|
|
|
let docMtime = ~~(doc.mtime / 1000);
|
|
|
|
|
if (storageMtime > docMtime) {
|
|
|
|
|
//newer local file.
|
|
|
|
|
Logger("DB -> STORAGE :" + file.path);
|
|
|
|
|
Logger("STORAGE -> DB :" + file.path);
|
|
|
|
|
Logger(`${storageMtime} > ${docMtime}`);
|
|
|
|
|
await this.updateIntoDB(file);
|
|
|
|
|
} else if (storageMtime < docMtime) {
|
|
|
|
|
//newer database file.
|
|
|
|
|
Logger("STORAGE <- DB :" + file.path);
|
|
|
|
|
Logger(`${storageMtime} < ${docMtime}`);
|
|
|
|
|
let docx = await this.localDatabase.getDBEntry(file.path);
|
|
|
|
|
let docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
|
|
|
|
if (docx != false) {
|
|
|
|
|
await this.doc2storate_modify(docx, file);
|
|
|
|
|
}
|
|
|
|
|
@@ -2552,7 +2621,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
content = await this.app.vault.read(file);
|
|
|
|
|
datatype = "plain";
|
|
|
|
|
}
|
|
|
|
|
let fullpath = file.path;
|
|
|
|
|
let fullpath = path2id(file.path);
|
|
|
|
|
let d: LoadedEntry = {
|
|
|
|
|
_id: fullpath,
|
|
|
|
|
data: content,
|
|
|
|
|
@@ -2563,7 +2632,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|
|
|
|
datatype: datatype,
|
|
|
|
|
};
|
|
|
|
|
//From here
|
|
|
|
|
let old = await this.localDatabase.getDBEntry(fullpath);
|
|
|
|
|
let old = await this.localDatabase.getDBEntry(fullpath, null, false, false);
|
|
|
|
|
if (old !== false) {
|
|
|
|
|
let oldData = { data: old.data, deleted: old._deleted };
|
|
|
|
|
let newData = { data: d.data, deleted: d._deleted };
|
|
|
|
|
@@ -2721,8 +2790,8 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
username: this.plugin.settings.couchDB_USER,
|
|
|
|
|
password: this.plugin.settings.couchDB_PASSWORD,
|
|
|
|
|
});
|
|
|
|
|
if (db === false) {
|
|
|
|
|
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
if (typeof db === "string") {
|
|
|
|
|
this.plugin.addLog(`could not connect to ${this.plugin.settings.couchDB_URI} : ${this.plugin.settings.couchDB_DBNAME} \n(${db})`, LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.plugin.addLog(`Connected to ${db.info.db_name}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
@@ -2877,7 +2946,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("End to End Encryption (beta)")
|
|
|
|
|
.setName("End to End Encryption")
|
|
|
|
|
.setDesc("Encrypting contents on the database.")
|
|
|
|
|
.addToggle((toggle) =>
|
|
|
|
|
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
|
|
|
|
@@ -2918,6 +2987,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
this.plugin.settings.periodicReplication = false;
|
|
|
|
|
this.plugin.settings.syncOnSave = false;
|
|
|
|
|
this.plugin.settings.syncOnStart = false;
|
|
|
|
|
this.plugin.settings.syncOnFileOpen = false;
|
|
|
|
|
this.plugin.settings.encrypt = this.plugin.settings.workingEncrypt;
|
|
|
|
|
this.plugin.settings.passphrase = this.plugin.settings.workingPassphrase;
|
|
|
|
|
|
|
|
|
|
@@ -3096,6 +3166,15 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("Use newer file if conflicted (beta)")
|
|
|
|
|
.setDesc("Resolve conflicts by newer files automatically.")
|
|
|
|
|
.addToggle((toggle) =>
|
|
|
|
|
toggle.setValue(this.plugin.settings.resolveConflictsByNewerFile).onChange(async (value) => {
|
|
|
|
|
this.plugin.settings.resolveConflictsByNewerFile = value;
|
|
|
|
|
await this.plugin.saveSettings();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("Minimum chunk size")
|
|
|
|
|
.setDesc("(letters), minimum chunk size.")
|
|
|
|
|
@@ -3162,6 +3241,7 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
this.plugin.settings.periodicReplication = false;
|
|
|
|
|
this.plugin.settings.syncOnSave = false;
|
|
|
|
|
this.plugin.settings.syncOnStart = false;
|
|
|
|
|
this.plugin.settings.syncOnFileOpen = false;
|
|
|
|
|
|
|
|
|
|
await this.plugin.saveSettings();
|
|
|
|
|
applyDisplayEnabled();
|
|
|
|
|
@@ -3256,6 +3336,251 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
await this.plugin.initializeDatabase();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// With great respect, thank you TfTHacker!
|
|
|
|
|
// refered: https://github.com/TfTHacker/obsidian42-brat/blob/main/src/features/BetaPlugins.ts
|
|
|
|
|
containerEl.createEl("h3", { text: "Plugins and settings (bleeding edge)" });
|
|
|
|
|
|
|
|
|
|
// new Setting(containerEl)
|
|
|
|
|
// .setName("Use Plugins and settings")
|
|
|
|
|
// .setDesc("It's on the bleeding edge. If you change this option, close setting dialog once,")
|
|
|
|
|
// .addToggle((toggle) =>
|
|
|
|
|
// toggle.setValue(this.plugin.settings.usePluginSettings).onChange(async (value) => {
|
|
|
|
|
// this.plugin.settings.usePluginSettings = value;
|
|
|
|
|
// await this.plugin.saveSettings();
|
|
|
|
|
// })
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("Device and Vault name")
|
|
|
|
|
.setDesc("")
|
|
|
|
|
.addText((text) => {
|
|
|
|
|
text.setPlaceholder("desktop-main")
|
|
|
|
|
.setValue(this.plugin.settings.deviceAndVaultName)
|
|
|
|
|
.onChange(async (value) => {
|
|
|
|
|
this.plugin.settings.deviceAndVaultName = value;
|
|
|
|
|
await this.plugin.saveSettings();
|
|
|
|
|
});
|
|
|
|
|
// text.inputEl.setAttribute("type", "password");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sweepPlugin = async () => {
|
|
|
|
|
// delete old database plugin entries
|
|
|
|
|
// TODO: don't delete always.
|
|
|
|
|
const db = this.plugin.localDatabase.localDatabase;
|
|
|
|
|
let oldDocs = await db.allDocs({ startkey: `ps:${this.plugin.settings.deviceAndVaultName}-`, endkey: `ps:${this.plugin.settings.deviceAndVaultName}.`, include_docs: true });
|
|
|
|
|
let delDocs = oldDocs.rows.map((e) => {
|
|
|
|
|
e.doc._deleted = true;
|
|
|
|
|
return e.doc;
|
|
|
|
|
});
|
|
|
|
|
await db.bulkDocs(delDocs);
|
|
|
|
|
|
|
|
|
|
// sweep current plugin.
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const pl = this.plugin.app.plugins;
|
|
|
|
|
const manifests: PluginManifest[] = Object.values(pl.manifests);
|
|
|
|
|
for (let m of manifests) {
|
|
|
|
|
let path = normalizePath(m.dir) + "/";
|
|
|
|
|
const adapter = this.plugin.app.vault.adapter;
|
|
|
|
|
let files = ["manifest.json", "main.js", "style.css", "data.json"];
|
|
|
|
|
let pluginData: { [key: string]: string } = {};
|
|
|
|
|
for (let file of files) {
|
|
|
|
|
let thePath = path + file;
|
|
|
|
|
if (await adapter.exists(thePath)) {
|
|
|
|
|
pluginData[file] = await adapter.read(thePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mtime = 0;
|
|
|
|
|
if (await adapter.exists(path + "/data.json")) {
|
|
|
|
|
mtime = (await adapter.stat(path + "/data.json")).mtime;
|
|
|
|
|
}
|
|
|
|
|
let p: PluginDataEntry = {
|
|
|
|
|
_id: `ps:${this.plugin.settings.deviceAndVaultName}-${m.id}`,
|
|
|
|
|
dataJson: pluginData["data.json"],
|
|
|
|
|
deviceVaultName: this.plugin.settings.deviceAndVaultName,
|
|
|
|
|
mainJs: pluginData["main.js"],
|
|
|
|
|
styleCss: pluginData["style.css"],
|
|
|
|
|
manifest: m,
|
|
|
|
|
manifestJson: pluginData["manifest.json"],
|
|
|
|
|
mtime: mtime,
|
|
|
|
|
type: "plugin",
|
|
|
|
|
};
|
|
|
|
|
let d: LoadedEntry = {
|
|
|
|
|
_id: p._id,
|
|
|
|
|
data: JSON.stringify(p),
|
|
|
|
|
ctime: mtime,
|
|
|
|
|
mtime: mtime,
|
|
|
|
|
size: 0,
|
|
|
|
|
children: [],
|
|
|
|
|
datatype: "plain",
|
|
|
|
|
};
|
|
|
|
|
await this.plugin.localDatabase.putDBEntry(d);
|
|
|
|
|
}
|
|
|
|
|
await this.plugin.replicate(true);
|
|
|
|
|
updatePluginPane();
|
|
|
|
|
};
|
|
|
|
|
const updatePluginPane = async () => {
|
|
|
|
|
const db = this.plugin.localDatabase.localDatabase;
|
|
|
|
|
let docList = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: false });
|
|
|
|
|
let oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.plugin.localDatabase.getDBEntry(e.id)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(e.data));
|
|
|
|
|
let plugins: { [key: string]: PluginDataEntry[] } = {};
|
|
|
|
|
let allPlugins: { [key: string]: PluginDataEntry } = {};
|
|
|
|
|
let thisDevicePlugins: { [key: string]: PluginDataEntry } = {};
|
|
|
|
|
for (let v of oldDocs) {
|
|
|
|
|
if (typeof plugins[v.deviceVaultName] === "undefined") {
|
|
|
|
|
plugins[v.deviceVaultName] = [];
|
|
|
|
|
}
|
|
|
|
|
plugins[v.deviceVaultName].push(v);
|
|
|
|
|
allPlugins[v._id] = v;
|
|
|
|
|
if (v.deviceVaultName == this.plugin.settings.deviceAndVaultName) {
|
|
|
|
|
thisDevicePlugins[v.manifest.id] = v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let html = `
|
|
|
|
|
<div class='sls-plugins-wrap'>
|
|
|
|
|
<table class='sls-plugins-tbl'>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>vault</th>
|
|
|
|
|
<th>plugin</th>
|
|
|
|
|
<th>version</th>
|
|
|
|
|
<th>modified</th>
|
|
|
|
|
<th>plugin</th>
|
|
|
|
|
<th>setting</th>
|
|
|
|
|
</tr>`;
|
|
|
|
|
for (let vaults in plugins) {
|
|
|
|
|
if (vaults == this.plugin.settings.deviceAndVaultName) continue;
|
|
|
|
|
for (let v of plugins[vaults]) {
|
|
|
|
|
let mtime = v.mtime == 0 ? "-" : new Date(v.mtime).toLocaleString();
|
|
|
|
|
let settingApplyable: boolean | string = "-";
|
|
|
|
|
let settingFleshness: string = "";
|
|
|
|
|
let isSameVersion = false;
|
|
|
|
|
if (thisDevicePlugins[v.manifest.id]) {
|
|
|
|
|
if (thisDevicePlugins[v.manifest.id].manifest.version == v.manifest.version) {
|
|
|
|
|
isSameVersion = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (thisDevicePlugins[v.manifest.id] && thisDevicePlugins[v.manifest.id].dataJson && v.dataJson) {
|
|
|
|
|
// have this plugin.
|
|
|
|
|
let localSetting = thisDevicePlugins[v.manifest.id].dataJson;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let remoteSetting = v.dataJson;
|
|
|
|
|
if (localSetting == remoteSetting) {
|
|
|
|
|
settingApplyable = "even";
|
|
|
|
|
} else {
|
|
|
|
|
if (v.mtime > thisDevicePlugins[v.manifest.id].mtime) {
|
|
|
|
|
settingFleshness = "newer";
|
|
|
|
|
} else {
|
|
|
|
|
settingFleshness = "older";
|
|
|
|
|
}
|
|
|
|
|
settingApplyable = true;
|
|
|
|
|
}
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
settingApplyable = "could not decrypt";
|
|
|
|
|
}
|
|
|
|
|
} else if (!v.dataJson) {
|
|
|
|
|
settingApplyable = "N/A";
|
|
|
|
|
}
|
|
|
|
|
// very ugly way.
|
|
|
|
|
let piece = `<tr>
|
|
|
|
|
<th>${escapeStringToHTML(v.deviceVaultName)}</th>
|
|
|
|
|
<td>${escapeStringToHTML(v.manifest.name)}</td>
|
|
|
|
|
<td class="tcenter">${escapeStringToHTML(v.manifest.version)}</td>
|
|
|
|
|
<td class="tcenter">${escapeStringToHTML(mtime)}</td>
|
|
|
|
|
<td class="tcenter">${isSameVersion ? "even" : "<button data-key='" + v._id + "' class='apply-plugin-version'>Use</button>"}</td>
|
|
|
|
|
<td class="tcenter">${settingApplyable === true ? "<button data-key='" + v._id + "' class='apply-plugin-data'>Apply (" + settingFleshness + ")</button>" : settingApplyable}</td>
|
|
|
|
|
</tr>`;
|
|
|
|
|
html += piece;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
html += "</table></div>";
|
|
|
|
|
pluginConfig.innerHTML = html;
|
|
|
|
|
pluginConfig.querySelectorAll(".apply-plugin-data").forEach((e) =>
|
|
|
|
|
e.addEventListener("click", async (evt) => {
|
|
|
|
|
console.dir("pluginData:" + e.attributes.getNamedItem("data-key").value);
|
|
|
|
|
let plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
|
|
|
|
|
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
|
|
|
|
const adapter = this.plugin.app.vault.adapter;
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
let stat = this.plugin.app.plugins.enabledPlugins[plugin.manifest.id];
|
|
|
|
|
if (stat) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
await this.plugin.app.plugins.unloadPlugin(plugin.manifest.id);
|
|
|
|
|
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
|
|
|
|
|
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL.NOTICE);
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
if (stat) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
await this.plugin.app.plugins.loadPlugin(plugin.manifest.id);
|
|
|
|
|
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
sweepPlugin();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
pluginConfig.querySelectorAll(".apply-plugin-version").forEach((e) =>
|
|
|
|
|
e.addEventListener("click", async (evt) => {
|
|
|
|
|
console.dir("pluginVersion:" + e.attributes.getNamedItem("data-key").value);
|
|
|
|
|
|
|
|
|
|
let plugin = allPlugins[e.attributes.getNamedItem("data-key").value];
|
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
let stat = this.plugin.app.plugins.enabledPlugins[plugin.manifest.id];
|
|
|
|
|
if (stat) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
await this.plugin.app.plugins.unloadPlugin(plugin.manifest.id);
|
|
|
|
|
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
|
|
|
|
const adapter = this.plugin.app.vault.adapter;
|
|
|
|
|
if ((await adapter.exists(pluginTargetFolderPath)) === false) {
|
|
|
|
|
await adapter.mkdir(pluginTargetFolderPath);
|
|
|
|
|
}
|
|
|
|
|
await adapter.write(pluginTargetFolderPath + "main.js", plugin.mainJs);
|
|
|
|
|
await adapter.write(pluginTargetFolderPath + "manifest.json", plugin.manifestJson);
|
|
|
|
|
if (plugin.styleCss) await adapter.write(pluginTargetFolderPath + "styles.css", plugin.styleCss);
|
|
|
|
|
if (plugin.dataJson) await adapter.write(pluginTargetFolderPath + "data.json", plugin.dataJson);
|
|
|
|
|
if (stat) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
await this.plugin.app.plugins.loadPlugin(plugin.manifest.id);
|
|
|
|
|
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL.NOTICE);
|
|
|
|
|
}
|
|
|
|
|
sweepPlugin();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let pluginConfig = containerEl.createEl("div");
|
|
|
|
|
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("Reload")
|
|
|
|
|
.setDesc("Reload List")
|
|
|
|
|
.addButton((button) =>
|
|
|
|
|
button
|
|
|
|
|
.setButtonText("Reload")
|
|
|
|
|
.setDisabled(false)
|
|
|
|
|
.onClick(async () => {
|
|
|
|
|
await updatePluginPane();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
new Setting(containerEl)
|
|
|
|
|
.setName("Save plugins into the database")
|
|
|
|
|
.setDesc("Now, it wouldn't work automatically")
|
|
|
|
|
.addButton((button) =>
|
|
|
|
|
button
|
|
|
|
|
.setButtonText("Save plugins")
|
|
|
|
|
.setDisabled(false)
|
|
|
|
|
.onClick(async () => {
|
|
|
|
|
if (!this.plugin.settings.encrypt) {
|
|
|
|
|
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await sweepPlugin();
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
updatePluginPane();
|
|
|
|
|
containerEl.createEl("h3", { text: "Corrupted data" });
|
|
|
|
|
|
|
|
|
|
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
|
|
|
|
@@ -3269,6 +3594,17 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|
|
|
|
xx.remove();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
xx.createEl("button", { text: `Restore from file` }, (e) => {
|
|
|
|
|
e.addEventListener("click", async () => {
|
|
|
|
|
let f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k);
|
|
|
|
|
if (f.length == 0) {
|
|
|
|
|
Logger("Not found in vault", LOG_LEVEL.NOTICE);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await this.plugin.updateIntoDB(f[0]);
|
|
|
|
|
xx.remove();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let cx = containerEl.createEl("div", { text: "There's no collupted data." });
|
|
|
|
|
|