Compare commits

...

3 Commits

Author SHA1 Message Date
vorotamoroz
8126bb6c02 Implemented:
- Plugins and settings sync (bleeding edge, not tested well)
2021-11-25 23:50:46 +09:00
vorotamoroz
12753262fd bumped 2021-11-25 12:32:35 +09:00
vorotamoroz
97b34cff47 Fixed:
- the issue of filenames.

Improved:
- logging (error detail are now logged.)
- Testing remote DB.
2021-11-25 03:13:08 +09:00
5 changed files with 340 additions and 57 deletions

386
main.ts
View File

@@ -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);
};
@@ -720,7 +758,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) {
@@ -759,7 +798,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): Promise<false | LoadedEntry> {
let id = path2id(path);
try {
let obj: EntryDocResponse = null;
if (opt) {
@@ -856,7 +896,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 +938,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;
@@ -1195,10 +1237,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 +1309,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;
}
@@ -1449,7 +1491,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 +1508,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 +1518,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 +1552,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;
}
@@ -1960,7 +2002,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);
@@ -1996,37 +2038,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 {
@@ -2051,16 +2095,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;
@@ -2068,38 +2113,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);
@@ -2114,7 +2160,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;
@@ -2139,7 +2185,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return;
}
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo" && change.type != "plugin") {
await this.handleDBChanged(change);
}
if (change.type == "versioninfo") {
@@ -2242,7 +2288,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);
@@ -2505,7 +2551,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
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);
@@ -2559,7 +2605,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,
@@ -2728,8 +2774,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);
@@ -3274,6 +3320,240 @@ 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);
console.dir(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 arrayBufferToBase64(await adapter.readBinary(thePath));
pluginData[file] = await adapter.read(thePath);
}
}
console.dir(m.id);
console.dir(pluginData);
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"] ? await encrypt(pluginData["data.json"], this.plugin.settings.passphrase) : undefined,
deviceVaultName: this.plugin.settings.deviceAndVaultName,
mainJs: pluginData["main.js"],
styleCss: pluginData["style.css"],
manifest: m,
manifestJson: pluginData["manifest.json"],
mtime: mtime,
type: "plugin",
};
await db.put(p);
}
await this.plugin.replicate(true);
updatePluginPane();
};
const updatePluginPane = async () => {
const db = this.plugin.localDatabase.localDatabase;
let oldDocs = await db.allDocs<PluginDataEntry>({ startkey: `ps:`, endkey: `ps;`, include_docs: true });
let plugins: { [key: string]: PluginDataEntry[] } = {};
let allPlugins: { [key: string]: PluginDataEntry } = {};
let thisDevicePlugins: { [key: string]: PluginDataEntry } = {};
for (let v of oldDocs.rows) {
if (typeof plugins[v.doc.deviceVaultName] === "undefined") {
plugins[v.doc.deviceVaultName] = [];
}
plugins[v.doc.deviceVaultName].push(v.doc);
allPlugins[v.doc._id] = v.doc;
if (v.doc.deviceVaultName == this.plugin.settings.deviceAndVaultName) {
thisDevicePlugins[v.doc.manifest.id] = v.doc;
}
}
let html = `
<table>
<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 = await decrypt(thisDevicePlugins[v.manifest.id].dataJson, this.plugin.settings.passphrase);
try {
let remoteSetting = await decrypt(v.dataJson, this.plugin.settings.passphrase);
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>";
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", await decrypt(plugin.dataJson, this.plugin.settings.passphrase));
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", await decrypt(plugin.dataJson, this.plugin.settings.passphrase));
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 () => {
await sweepPlugin();
})
);
updatePluginPane();
containerEl.createEl("h3", { text: "Corrupted data" });
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.1.18",
"version": "0.1.20",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.1.18",
"version": "0.1.20",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.1.18",
"version": "0.1.20",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.1.18",
"version": "0.1.20",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"scripts": {

View File

@@ -28,3 +28,6 @@
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.tcenter {
text-align: center;
}