You've already forked obsidian-livesync
mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2025-07-16 00:14:19 +02:00
fix some,
- Add Utility functions. - Database reset opeartion and corrupt preventions. - Fixing file deleting. - Tidy up setting dialog. - Add notice about the file that having platform dependant name. - Add webclip on readme
This commit is contained in:
@ -16,6 +16,7 @@ Limitations: Folder deletion handling is not completed.
|
|||||||
- Live Sync
|
- Live Sync
|
||||||
- Self-Hosted data synchronization with conflict detection and resolving in Obsidian.
|
- Self-Hosted data synchronization with conflict detection and resolving in Obsidian.
|
||||||
- Off-line sync is also available.
|
- Off-line sync is also available.
|
||||||
|
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||||
|
|
||||||
## IMPORTANT NOTICE
|
## IMPORTANT NOTICE
|
||||||
|
|
||||||
@ -33,11 +34,14 @@ If you want to synchronize to both backend, sync one by one, please.
|
|||||||
5. Setup LiveSync or SyncOnSave or SyncOnStart as you like.
|
5. Setup LiveSync or SyncOnSave or SyncOnStart as you like.
|
||||||
|
|
||||||
## Test Server
|
## Test Server
|
||||||
|
|
||||||
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of obsidian-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of obsidian-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
||||||
Note: Please read "Limitations" carefully. Do not send your private vault.
|
Note: Please read "Limitations" carefully. Do not send your private vault.
|
||||||
|
|
||||||
## WebClipper is also available.
|
## WebClipper is also available.
|
||||||
See [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
|
||||||
|
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||||
|
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
||||||
|
|
||||||
## When your database looks corrupted
|
## When your database looks corrupted
|
||||||
|
|
||||||
|
378
main.ts
378
main.ts
@ -21,6 +21,8 @@ const LOG_LEVEL = {
|
|||||||
type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
|
type LOG_LEVEL = typeof LOG_LEVEL[keyof typeof LOG_LEVEL];
|
||||||
|
|
||||||
const VERSIONINFO_DOCID = "obsydian_livesync_version";
|
const VERSIONINFO_DOCID = "obsydian_livesync_version";
|
||||||
|
const MILSTONE_DOCID = "_local/obsydian_livesync_milestone";
|
||||||
|
const NODEINFO_DOCID = "_local/obsydian_livesync_nodeinfo";
|
||||||
|
|
||||||
interface ObsidianLiveSyncSettings {
|
interface ObsidianLiveSyncSettings {
|
||||||
couchDB_URI: string;
|
couchDB_URI: string;
|
||||||
@ -110,8 +112,27 @@ interface EntryVersionInfo {
|
|||||||
version: number;
|
version: number;
|
||||||
_deleted?: boolean;
|
_deleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EntryMilestoneInfo {
|
||||||
|
_id: typeof MILSTONE_DOCID;
|
||||||
|
_rev?: string;
|
||||||
|
type: "milestoneinfo";
|
||||||
|
_deleted?: boolean;
|
||||||
|
created: number;
|
||||||
|
accepted_nodes: string[];
|
||||||
|
locked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntryNodeInfo {
|
||||||
|
_id: typeof NODEINFO_DOCID;
|
||||||
|
_rev?: string;
|
||||||
|
_deleted?: boolean;
|
||||||
|
type: "nodeinfo";
|
||||||
|
nodeid: string;
|
||||||
|
}
|
||||||
|
|
||||||
type EntryBody = Entry | NewEntry | PlainEntry;
|
type EntryBody = Entry | NewEntry | PlainEntry;
|
||||||
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo;
|
type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo;
|
||||||
|
|
||||||
type diff_result_leaf = {
|
type diff_result_leaf = {
|
||||||
rev: string;
|
rev: string;
|
||||||
@ -195,6 +216,13 @@ const escapeStringToHTML = (str: string) => {
|
|||||||
return escape[match];
|
return escape[match];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveWithIgnoreKnownError<T>(p: Promise<T>, def: T): Promise<T> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
p.then(res).catch((ex) => (ex.status && ex.status == 404 ? res(def) : rej(ex)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
const isValidRemoteCouchDBURI = (uri: string): boolean => {
|
||||||
if (uri.startsWith("https://")) return true;
|
if (uri.startsWith("https://")) return true;
|
||||||
if (uri.startsWith("http://")) return true;
|
if (uri.startsWith("http://")) return true;
|
||||||
@ -251,22 +279,21 @@ const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): P
|
|||||||
version: barrier,
|
version: barrier,
|
||||||
type: "versioninfo",
|
type: "versioninfo",
|
||||||
};
|
};
|
||||||
try {
|
let versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo;
|
||||||
let versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo;
|
|
||||||
if (versionInfo.type != "versioninfo") {
|
if (versionInfo.type != "versioninfo") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
vi._rev = versionInfo._rev;
|
vi._rev = versionInfo._rev;
|
||||||
} catch (ex) {
|
|
||||||
if (ex.status && ex.status == 404) {
|
|
||||||
// no op.
|
|
||||||
} else {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await db.put(vi);
|
await db.put(vi);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
function isValidPath(filename: string): boolean {
|
||||||
|
let regex = /[\u0000-\u001f]|[\\"':?<>|*$]/g;
|
||||||
|
let x = filename.replace(regex, "_");
|
||||||
|
let win = /(\\|\/)(COM\d|LPT\d|CON|PRN|AUX|NUL|CLOCK$)($|\.)/gi;
|
||||||
|
let sx = (x = x.replace(win, "/_"));
|
||||||
|
return sx == filename;
|
||||||
|
}
|
||||||
|
|
||||||
// Default Logger.
|
// Default Logger.
|
||||||
let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
let Logger: (message: any, levlel?: LOG_LEVEL) => Promise<void> = async (message, _) => {
|
||||||
@ -282,6 +309,7 @@ class LocalPouchDB {
|
|||||||
dbname: string;
|
dbname: string;
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: ObsidianLiveSyncSettings;
|
||||||
localDatabase: PouchDB.Database<EntryDoc>;
|
localDatabase: PouchDB.Database<EntryDoc>;
|
||||||
|
nodeid: string = "";
|
||||||
|
|
||||||
recentModifiedDocs: string[] = [];
|
recentModifiedDocs: string[] = [];
|
||||||
h32: (input: string, seed?: number) => string;
|
h32: (input: string, seed?: number) => string;
|
||||||
@ -294,6 +322,8 @@ class LocalPouchDB {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
corruptedEntries: { [key: string]: EntryDoc } = {};
|
corruptedEntries: { [key: string]: EntryDoc } = {};
|
||||||
|
remoteLocked = false;
|
||||||
|
remoteLockedAndDeviceNotAccepted = false;
|
||||||
|
|
||||||
constructor(settings: ObsidianLiveSyncSettings, dbname: string) {
|
constructor(settings: ObsidianLiveSyncSettings, dbname: string) {
|
||||||
this.auth = {
|
this.auth = {
|
||||||
@ -345,6 +375,17 @@ class LocalPouchDB {
|
|||||||
revs_limit: 100,
|
revs_limit: 100,
|
||||||
deterministic_revs: true,
|
deterministic_revs: true,
|
||||||
});
|
});
|
||||||
|
// initialize local node information.
|
||||||
|
let nodeinfo: EntryNodeInfo = await resolveWithIgnoreKnownError<EntryNodeInfo>(this.localDatabase.get(NODEINFO_DOCID), {
|
||||||
|
_id: NODEINFO_DOCID,
|
||||||
|
type: "nodeinfo",
|
||||||
|
nodeid: "",
|
||||||
|
});
|
||||||
|
if (nodeinfo.nodeid == "") {
|
||||||
|
nodeinfo.nodeid = Math.random().toString(36).slice(-10);
|
||||||
|
await this.localDatabase.put(nodeinfo);
|
||||||
|
}
|
||||||
|
this.nodeid = nodeinfo.nodeid;
|
||||||
|
|
||||||
// Traceing the leaf id
|
// Traceing the leaf id
|
||||||
let changes = this.localDatabase
|
let changes = this.localDatabase
|
||||||
@ -560,7 +601,54 @@ class LocalPouchDB {
|
|||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async deleteDBEntryPrefix(prefix: string): Promise<boolean> {
|
||||||
|
// delete database entries by prefix.
|
||||||
|
// it called from folder deletion.
|
||||||
|
let c = 0;
|
||||||
|
let readCount = 0;
|
||||||
|
let delDocs: string[] = [];
|
||||||
|
do {
|
||||||
|
let result = await this.localDatabase.allDocs({ include_docs: false, skip: c, limit: 100, conflicts: true });
|
||||||
|
readCount = result.rows.length;
|
||||||
|
if (readCount > 0) {
|
||||||
|
//there are some result
|
||||||
|
for (let v of result.rows) {
|
||||||
|
// let doc = v.doc;
|
||||||
|
if (v.id.startsWith(prefix) || v.id.startsWith("/" + prefix)) {
|
||||||
|
delDocs.push(v.id);
|
||||||
|
console.log("!" + v.id);
|
||||||
|
} else {
|
||||||
|
if (!v.id.startsWith("h:")) {
|
||||||
|
console.log("?" + v.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c += readCount;
|
||||||
|
} while (readCount != 0);
|
||||||
|
// items collected.
|
||||||
|
//bulk docs to delete?
|
||||||
|
let deleteCount = 0;
|
||||||
|
let notfound = 0;
|
||||||
|
for (let v of delDocs) {
|
||||||
|
try {
|
||||||
|
let item = await this.localDatabase.get(v);
|
||||||
|
item._deleted = true;
|
||||||
|
await this.localDatabase.put(item);
|
||||||
|
this.updateRecentModifiedDocs(item._id, item._rev, true);
|
||||||
|
deleteCount++;
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex.status && ex.status == 404) {
|
||||||
|
notfound++;
|
||||||
|
// NO OP. It should be timing problem.
|
||||||
|
} else {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger(`deleteDBEntryPrefix:deleted ${deleteCount} items, skipped ${notfound}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
async putDBEntry(note: LoadedEntry) {
|
async putDBEntry(note: LoadedEntry) {
|
||||||
let leftData = note.data;
|
let leftData = note.data;
|
||||||
let savenNotes = [];
|
let savenNotes = [];
|
||||||
@ -727,6 +815,47 @@ class LocalPouchDB {
|
|||||||
// no op now,
|
// no op now,
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
replicateAllToServer(setting: ObsidianLiveSyncSettings) {
|
||||||
|
return new Promise(async (res, rej) => {
|
||||||
|
this.closeReplication();
|
||||||
|
Logger("send all data to server", LOG_LEVEL.NOTICE);
|
||||||
|
let uri = setting.couchDB_URI;
|
||||||
|
let auth: Credential = {
|
||||||
|
username: setting.couchDB_USER,
|
||||||
|
password: setting.couchDB_PASSWORD,
|
||||||
|
};
|
||||||
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
||||||
|
if (dbret === false) {
|
||||||
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
||||||
|
return rej(`could not connect to ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let syncOptionBase: PouchDB.Replication.SyncOptions = {
|
||||||
|
batch_size: 250,
|
||||||
|
batches_limit: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = dbret.db;
|
||||||
|
//replicate once
|
||||||
|
let replicate = this.localDatabase.replicate.to(db, syncOptionBase);
|
||||||
|
replicate
|
||||||
|
.on("change", async (e) => {
|
||||||
|
// no op.
|
||||||
|
Logger(`sending..:${e.docs.length}`);
|
||||||
|
})
|
||||||
|
.on("complete", async (info) => {
|
||||||
|
Logger("Completed", LOG_LEVEL.NOTICE);
|
||||||
|
replicate.cancel();
|
||||||
|
replicate.removeAllListeners();
|
||||||
|
res(true);
|
||||||
|
})
|
||||||
|
.on("error", (e) => {
|
||||||
|
Logger("Pulling Replication error", LOG_LEVEL.NOTICE);
|
||||||
|
Logger(e);
|
||||||
|
rej(e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<{}>[]) => Promise<void>) {
|
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<{}>[]) => Promise<void>) {
|
||||||
if (setting.versionUpFlash != "") {
|
if (setting.versionUpFlash != "") {
|
||||||
new Notice("Open settings and check message, please.");
|
new Notice("Open settings and check message, please.");
|
||||||
@ -752,6 +881,26 @@ class LocalPouchDB {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defMilestonePoint: EntryMilestoneInfo = {
|
||||||
|
_id: MILSTONE_DOCID,
|
||||||
|
type: "milestoneinfo",
|
||||||
|
created: (new Date() as any) / 1,
|
||||||
|
locked: false,
|
||||||
|
accepted_nodes: [this.nodeid],
|
||||||
|
};
|
||||||
|
|
||||||
|
let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defMilestonePoint);
|
||||||
|
this.remoteLocked = remoteMilestone.locked;
|
||||||
|
this.remoteLockedAndDeviceNotAccepted = remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1;
|
||||||
|
|
||||||
|
if (remoteMilestone.locked && remoteMilestone.accepted_nodes.indexOf(this.nodeid) == -1) {
|
||||||
|
Logger("Remote database marked as 'Auto Sync Locked'. And this devide does not marked as resolved device. see settings dialog.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof remoteMilestone._rev == "undefined") {
|
||||||
|
await dbret.db.put(remoteMilestone);
|
||||||
|
}
|
||||||
|
|
||||||
let syncOptionBase: PouchDB.Replication.SyncOptions = {
|
let syncOptionBase: PouchDB.Replication.SyncOptions = {
|
||||||
batch_size: 250,
|
batch_size: 250,
|
||||||
batches_limit: 40,
|
batches_limit: 40,
|
||||||
@ -869,6 +1018,71 @@ class LocalPouchDB {
|
|||||||
if (con2 === false) return;
|
if (con2 === false) return;
|
||||||
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
|
Logger("Remote Database Created or Connected", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
|
async markRemoteLocked(setting: ObsidianLiveSyncSettings, locked: boolean) {
|
||||||
|
let uri = setting.couchDB_URI;
|
||||||
|
let auth: Credential = {
|
||||||
|
username: setting.couchDB_USER,
|
||||||
|
password: setting.couchDB_PASSWORD,
|
||||||
|
};
|
||||||
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
||||||
|
if (dbret === false) {
|
||||||
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) {
|
||||||
|
Logger("Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let defInitPoint: EntryMilestoneInfo = {
|
||||||
|
_id: MILSTONE_DOCID,
|
||||||
|
type: "milestoneinfo",
|
||||||
|
created: (new Date() as any) / 1,
|
||||||
|
locked: locked,
|
||||||
|
accepted_nodes: [this.nodeid],
|
||||||
|
};
|
||||||
|
|
||||||
|
let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint);
|
||||||
|
remoteMilestone.accepted_nodes = [this.nodeid];
|
||||||
|
remoteMilestone.locked = locked;
|
||||||
|
if (locked) {
|
||||||
|
Logger("Lock remote database to prevent data corruption", LOG_LEVEL.NOTICE);
|
||||||
|
} else {
|
||||||
|
Logger("Unlock remote database to prevent data corruption", LOG_LEVEL.NOTICE);
|
||||||
|
}
|
||||||
|
await dbret.db.put(remoteMilestone);
|
||||||
|
}
|
||||||
|
async markRemoteResolved(setting: ObsidianLiveSyncSettings) {
|
||||||
|
let uri = setting.couchDB_URI;
|
||||||
|
let auth: Credential = {
|
||||||
|
username: setting.couchDB_USER,
|
||||||
|
password: setting.couchDB_PASSWORD,
|
||||||
|
};
|
||||||
|
let dbret = await connectRemoteCouchDB(uri, auth);
|
||||||
|
if (dbret === false) {
|
||||||
|
Logger(`could not connect to ${uri}`, LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await checkRemoteVersion(dbret.db, this.migrate.bind(this), VER))) {
|
||||||
|
Logger("Remote database is newer or corrupted, make sure to latest version of obsidian-livesync installed", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let defInitPoint: EntryMilestoneInfo = {
|
||||||
|
_id: MILSTONE_DOCID,
|
||||||
|
type: "milestoneinfo",
|
||||||
|
created: (new Date() as any) / 1,
|
||||||
|
locked: false,
|
||||||
|
accepted_nodes: [this.nodeid],
|
||||||
|
};
|
||||||
|
// check local database hash status and remote replicate hash status
|
||||||
|
let remoteMilestone: EntryMilestoneInfo = await resolveWithIgnoreKnownError(dbret.db.get(MILSTONE_DOCID), defInitPoint);
|
||||||
|
// remoteMilestone.locked = false;
|
||||||
|
remoteMilestone.accepted_nodes = Array.from(new Set([...remoteMilestone.accepted_nodes, this.nodeid]));
|
||||||
|
// this.remoteLocked = false;
|
||||||
|
Logger("Mark this device as 'resolved'.", LOG_LEVEL.NOTICE);
|
||||||
|
await dbret.db.put(remoteMilestone);
|
||||||
|
}
|
||||||
|
|
||||||
async garbageCollect() {
|
async garbageCollect() {
|
||||||
// get all documents of NewEntry2
|
// get all documents of NewEntry2
|
||||||
@ -1202,12 +1416,20 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (doc.datatype == "newnote") {
|
if (doc.datatype == "newnote") {
|
||||||
let bin = base64ToArrayBuffer(doc.data);
|
let bin = base64ToArrayBuffer(doc.data);
|
||||||
if (bin != null) {
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ensureDirectory(doc._id);
|
await this.ensureDirectory(doc._id);
|
||||||
let newfile = await this.app.vault.createBinary(doc._id, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
let newfile = await this.app.vault.createBinary(doc._id, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger("live : write to local (newfile:b) " + doc._id);
|
Logger("live : write to local (newfile:b) " + doc._id);
|
||||||
await this.app.vault.trigger("create", newfile);
|
await this.app.vault.trigger("create", newfile);
|
||||||
}
|
}
|
||||||
} else if (doc.datatype == "plain") {
|
} 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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ensureDirectory(doc._id);
|
await this.ensureDirectory(doc._id);
|
||||||
let newfile = await this.app.vault.create(doc._id, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
let newfile = await this.app.vault.create(doc._id, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger("live : write to local (newfile:p) " + doc._id);
|
Logger("live : write to local (newfile:p) " + doc._id);
|
||||||
@ -1251,6 +1473,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (doc.datatype == "newnote") {
|
if (doc.datatype == "newnote") {
|
||||||
let bin = base64ToArrayBuffer(doc.data);
|
let bin = base64ToArrayBuffer(doc.data);
|
||||||
if (bin != null) {
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ensureDirectory(doc._id);
|
await this.ensureDirectory(doc._id);
|
||||||
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modifyBinary(file, bin, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger(msg);
|
Logger(msg);
|
||||||
@ -1258,6 +1484,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (doc.datatype == "plain") {
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.ensureDirectory(doc._id);
|
await this.ensureDirectory(doc._id);
|
||||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||||
Logger(msg);
|
Logger(msg);
|
||||||
@ -1298,7 +1528,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
|
||||||
if (change.type != "leaf" && change.type != "versioninfo") {
|
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
|
||||||
await this.handleDBChanged(change);
|
await this.handleDBChanged(change);
|
||||||
}
|
}
|
||||||
if (change.type == "versioninfo") {
|
if (change.type == "versioninfo") {
|
||||||
@ -1333,6 +1563,18 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.openDatabase();
|
await this.openDatabase();
|
||||||
await this.syncAllFiles();
|
await this.syncAllFiles();
|
||||||
}
|
}
|
||||||
|
async replicateAllToServer() {
|
||||||
|
return await this.localDatabase.replicateAllToServer(this.settings);
|
||||||
|
}
|
||||||
|
async markRemoteLocked() {
|
||||||
|
return await this.localDatabase.markRemoteLocked(this.settings, true);
|
||||||
|
}
|
||||||
|
async markRemoteUnlocked() {
|
||||||
|
return await this.localDatabase.markRemoteLocked(this.settings, false);
|
||||||
|
}
|
||||||
|
async markRemoteResolved() {
|
||||||
|
return await this.localDatabase.markRemoteResolved(this.settings);
|
||||||
|
}
|
||||||
async syncAllFiles() {
|
async syncAllFiles() {
|
||||||
// synchronize all files between database and storage.
|
// synchronize all files between database and storage.
|
||||||
const filesStorage = this.app.vault.getFiles();
|
const filesStorage = this.app.vault.getFiles();
|
||||||
@ -1362,6 +1604,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
async deleteFolderOnDB(folder: TFolder) {
|
async deleteFolderOnDB(folder: TFolder) {
|
||||||
Logger(`delete folder:${folder.path}`);
|
Logger(`delete folder:${folder.path}`);
|
||||||
|
await this.localDatabase.deleteDBEntryPrefix(folder.path + "/");
|
||||||
for (var v of folder.children) {
|
for (var v of folder.children) {
|
||||||
let entry = v as TFile & TFolder;
|
let entry = v as TFile & TFolder;
|
||||||
Logger(`->entry:${entry.path}`, LOG_LEVEL.VERBOSE);
|
Logger(`->entry:${entry.path}`, LOG_LEVEL.VERBOSE);
|
||||||
@ -1374,7 +1617,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (ex.code && ex.code == "ENOENT") {
|
if (ex.code && ex.code == "ENOENT") {
|
||||||
//NO OP.
|
//NO OP.
|
||||||
} else {
|
} else {
|
||||||
Logger(`error while delete filder:${entry.path}`, LOG_LEVEL.NOTICE);
|
Logger(`error while delete folder:${entry.path}`, LOG_LEVEL.NOTICE);
|
||||||
Logger(ex);
|
Logger(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1788,9 +2031,12 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
text.inputEl.setAttribute("type", "password");
|
text.inputEl.setAttribute("type", "password");
|
||||||
});
|
});
|
||||||
new Setting(containerEl).setName("Test DB").addButton((button) =>
|
new Setting(containerEl)
|
||||||
|
.setName("Test Database Connection")
|
||||||
|
.setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.")
|
||||||
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Test Database Connection")
|
.setButtonText("Test")
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.testConnection();
|
await this.testConnection();
|
||||||
@ -1931,24 +2177,6 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
text.inputEl.setAttribute("type", "number");
|
text.inputEl.setAttribute("type", "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl).setName("Local Database Operations").addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Reset local database")
|
|
||||||
.setDisabled(false)
|
|
||||||
.onClick(async () => {
|
|
||||||
await this.plugin.resetLocalDatabase();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
new Setting(containerEl).setName("Re-init").addButton((button) =>
|
|
||||||
button
|
|
||||||
.setButtonText("Init Database again")
|
|
||||||
.setDisabled(false)
|
|
||||||
.onClick(async () => {
|
|
||||||
await this.plugin.resetLocalDatabase();
|
|
||||||
await this.plugin.initializeDatabase();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
new Setting(containerEl).setName("Garbage Collect").addButton((button) =>
|
new Setting(containerEl).setName("Garbage Collect").addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Garbage Collection")
|
.setButtonText("Garbage Collection")
|
||||||
@ -1960,6 +2188,60 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
containerEl.createEl("h3", { text: "Hatch" });
|
containerEl.createEl("h3", { text: "Hatch" });
|
||||||
|
|
||||||
|
if (this.plugin.localDatabase.remoteLockedAndDeviceNotAccepted) {
|
||||||
|
let c = containerEl.createEl("div", {
|
||||||
|
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization, and this device was not marked as 'resolved'. it caused by some operations like this. re-initialized. Local database initialization should be required. please back your vault up, reset local database, and press 'Mark this device as resolved'. ",
|
||||||
|
});
|
||||||
|
c.createEl("button", { text: "I'm ready, mark this device 'resolved'" }, (e) => {
|
||||||
|
e.addEventListener("click", async () => {
|
||||||
|
await this.plugin.markRemoteResolved();
|
||||||
|
c.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
c.addClass("op-warn");
|
||||||
|
} else {
|
||||||
|
if (this.plugin.localDatabase.remoteLocked) {
|
||||||
|
let c = containerEl.createEl("div", {
|
||||||
|
text: "To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database.",
|
||||||
|
});
|
||||||
|
c.createEl("button", { text: "I'm ready, unlock the database" }, (e) => {
|
||||||
|
e.addEventListener("click", async () => {
|
||||||
|
await this.plugin.markRemoteUnlocked();
|
||||||
|
c.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
c.addClass("op-warn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Drop History")
|
||||||
|
.setDesc("Initialize local and remote database, and create local database from storage and put all into server. And also, lock the database to prevent data corruption.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Execute")
|
||||||
|
.setWarning()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.resetLocalDatabase();
|
||||||
|
await this.plugin.initializeDatabase();
|
||||||
|
await this.plugin.tryResetRemoteDatabase();
|
||||||
|
await this.plugin.markRemoteLocked();
|
||||||
|
await this.plugin.replicateAllToServer();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Lock remote database")
|
||||||
|
.setDesc("Lock remote database for synchronize")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("Lock")
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.markRemoteLocked();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Suspend file watching")
|
.setName("Suspend file watching")
|
||||||
.setDesc("if enables it, all file operations are ignored.")
|
.setDesc("if enables it, all file operations are ignored.")
|
||||||
@ -1970,23 +2252,41 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerEl).setName("Remote Database Operations").addButton((button) =>
|
new Setting(containerEl)
|
||||||
|
.setName("Reset remote database")
|
||||||
|
.setDesc("Reset remote database, this affects only database. If you replicate again, remote database will restored by local database.")
|
||||||
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Reset remote database")
|
.setButtonText("Reset")
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin.tryResetRemoteDatabase();
|
await this.plugin.tryResetRemoteDatabase();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(containerEl).setName("Remote Database Operations").addButton((button) =>
|
new Setting(containerEl)
|
||||||
|
.setName("Reset local database")
|
||||||
|
.setDesc("Reset local database, this affects only database. If you replicate again, local database will restored by remote database.")
|
||||||
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText("Create remote database")
|
.setButtonText("Reset")
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.plugin.tryResetRemoteDatabase();
|
await this.plugin.resetLocalDatabase();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Initialize local database again")
|
||||||
|
.setDesc("WARNING: Reset local database and reconstruct by storage data. It affects local database, but if you replicate remote as is, remote data will be merged or corrupted.")
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText("INITIALIZE")
|
||||||
|
.setWarning()
|
||||||
|
.setDisabled(false)
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.resetLocalDatabase();
|
||||||
|
await this.plugin.initializeDatabase();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
containerEl.createEl("h3", { text: "Corrupted data" });
|
containerEl.createEl("h3", { text: "Corrupted data" });
|
||||||
|
|
||||||
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Obsidian Live sync",
|
"name": "Obsidian Live sync",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"minAppVersion": "0.9.12",
|
"minAppVersion": "0.9.12",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "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",
|
"author": "vorotamoroz",
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"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",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Reference in New Issue
Block a user