mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-21 14:51:34 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aec0b2986b | ||
|
|
e6025b92d8 | ||
|
|
fad9fed5ca |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.17.17",
|
"version": "0.17.19",
|
||||||
"minAppVersion": "0.9.12",
|
"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.",
|
"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",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.17.17",
|
"version": "0.17.19",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.17.17",
|
"version": "0.17.19",
|
||||||
"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.17.17",
|
"version": "0.17.19",
|
||||||
"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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
291
src/main.ts
291
src/main.ts
@@ -1920,10 +1920,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.syncAllFiles(showingNotice);
|
await this.syncAllFiles(showingNotice);
|
||||||
}
|
}
|
||||||
if (this.settings.syncInternalFiles) {
|
if (this.settings.syncInternalFiles) {
|
||||||
await this.syncInternalFilesAndDatabase("push", showingNotice);
|
try {
|
||||||
|
Logger("Synchronizing hidden files...");
|
||||||
|
await this.syncInternalFilesAndDatabase("push", showingNotice);
|
||||||
|
Logger("Synchronizing hidden files done");
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Synchronizing hidden files failed");
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.settings.usePluginSync) {
|
if (this.settings.usePluginSync) {
|
||||||
await this.sweepPlugin(showingNotice);
|
try {
|
||||||
|
Logger("Scanning plugins...");
|
||||||
|
await this.sweepPlugin(showingNotice);
|
||||||
|
Logger("Scanning plugins done");
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Scanning plugins failed");
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
this.isReady = true;
|
this.isReady = true;
|
||||||
// run queued event once.
|
// run queued event once.
|
||||||
@@ -2990,41 +3005,47 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const content = await arrayBufferToBase64(contentBin);
|
const content = await arrayBufferToBase64(contentBin);
|
||||||
const mtime = file.mtime;
|
const mtime = file.mtime;
|
||||||
return await runWithLock("file-" + id, false, async () => {
|
return await runWithLock("file-" + id, false, async () => {
|
||||||
const old = await this.localDatabase.getDBEntry(id, null, false, false);
|
try {
|
||||||
let saveData: LoadedEntry;
|
const old = await this.localDatabase.getDBEntry(id, null, false, false);
|
||||||
if (old === false) {
|
let saveData: LoadedEntry;
|
||||||
saveData = {
|
if (old === false) {
|
||||||
_id: id,
|
saveData = {
|
||||||
data: content,
|
_id: id,
|
||||||
mtime,
|
data: content,
|
||||||
ctime: mtime,
|
mtime,
|
||||||
datatype: "newnote",
|
ctime: mtime,
|
||||||
size: file.size,
|
datatype: "newnote",
|
||||||
children: [],
|
size: file.size,
|
||||||
deleted: false,
|
children: [],
|
||||||
type: "newnote",
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isDocContentSame(old.data, content) && !forceWrite) {
|
||||||
|
// Logger(`internal files STORAGE --> DB:${file.path}: Not changed`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveData =
|
||||||
|
{
|
||||||
|
...old,
|
||||||
|
data: content,
|
||||||
|
mtime,
|
||||||
|
size: file.size,
|
||||||
|
datatype: "newnote",
|
||||||
|
children: [],
|
||||||
|
deleted: false,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (isDocContentSame(old.data, content) && !forceWrite) {
|
|
||||||
// Logger(`internal files STORAGE --> DB:${file.path}: Not changed`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveData =
|
|
||||||
{
|
|
||||||
...old,
|
|
||||||
data: content,
|
|
||||||
mtime,
|
|
||||||
size: file.size,
|
|
||||||
datatype: "newnote",
|
|
||||||
children: [],
|
|
||||||
deleted: false,
|
|
||||||
type: "newnote",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = await this.localDatabase.putDBEntry(saveData, true);
|
const ret = await this.localDatabase.putDBEntry(saveData, true);
|
||||||
Logger(`STORAGE --> DB:${file.path}: (hidden) Done`);
|
Logger(`STORAGE --> DB:${file.path}: (hidden) Done`);
|
||||||
return ret;
|
return ret;
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`STORAGE --> DB:${file.path}: (hidden) Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3032,36 +3053,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const id = filename2idInternalMetadata(path2id(filename));
|
const id = filename2idInternalMetadata(path2id(filename));
|
||||||
const mtime = new Date().getTime();
|
const mtime = new Date().getTime();
|
||||||
await runWithLock("file-" + id, false, async () => {
|
await runWithLock("file-" + id, false, async () => {
|
||||||
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
|
try {
|
||||||
let saveData: InternalFileEntry;
|
const old = await this.localDatabase.getDBEntry(id, null, false, false) as InternalFileEntry | false;
|
||||||
if (old === false) {
|
let saveData: InternalFileEntry;
|
||||||
saveData = {
|
if (old === false) {
|
||||||
_id: id,
|
saveData = {
|
||||||
mtime,
|
_id: id,
|
||||||
ctime: mtime,
|
mtime,
|
||||||
size: 0,
|
ctime: mtime,
|
||||||
children: [],
|
size: 0,
|
||||||
deleted: true,
|
children: [],
|
||||||
type: "newnote",
|
deleted: true,
|
||||||
}
|
type: "newnote",
|
||||||
} else {
|
}
|
||||||
if (old.deleted) {
|
} else {
|
||||||
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
|
if (old.deleted) {
|
||||||
return;
|
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
|
||||||
}
|
return;
|
||||||
saveData =
|
}
|
||||||
{
|
saveData =
|
||||||
...old,
|
{
|
||||||
mtime,
|
...old,
|
||||||
size: 0,
|
mtime,
|
||||||
children: [],
|
size: 0,
|
||||||
deleted: true,
|
children: [],
|
||||||
type: "newnote",
|
deleted: true,
|
||||||
|
type: "newnote",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
await this.localDatabase.localDatabase.put(saveData);
|
||||||
|
Logger(`STORAGE -x> DB:${filename}: (hidden) Done`);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`STORAGE -x> DB:${filename}: (hidden) Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
await this.localDatabase.localDatabase.put(saveData);
|
|
||||||
Logger(`STORAGE -x> DB:${filename}: (hidden) Done`);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async ensureDirectoryEx(fullPath: string) {
|
async ensureDirectoryEx(fullPath: string) {
|
||||||
@@ -3089,28 +3115,25 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const id = filename2idInternalMetadata(path2id(filename));
|
const id = filename2idInternalMetadata(path2id(filename));
|
||||||
|
|
||||||
return await runWithLock("file-" + id, false, async () => {
|
return await runWithLock("file-" + id, false, async () => {
|
||||||
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
|
try {
|
||||||
if (fileOnDB === false) throw new Error(`File not found on database.:${id}`);
|
const fileOnDB = await this.localDatabase.getDBEntry(id, null, false, false) as false | LoadedEntry;
|
||||||
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false;
|
if (fileOnDB === false) throw new Error(`File not found on database.:${id}`);
|
||||||
if (deleted) {
|
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false;
|
||||||
if (!isExists) {
|
if (deleted) {
|
||||||
Logger(`STORAGE <x- DB:${filename}: deleted (hidden) Deleted on DB, but the file is already not found on storage.`);
|
if (!isExists) {
|
||||||
} else {
|
Logger(`STORAGE <x- DB:${filename}: deleted (hidden) Deleted on DB, but the file is already not found on storage.`);
|
||||||
Logger(`STORAGE <x- DB:${filename}: deleted (hidden).`);
|
} else {
|
||||||
await this.app.vault.adapter.remove(filename);
|
Logger(`STORAGE <x- DB:${filename}: deleted (hidden).`);
|
||||||
|
await this.app.vault.adapter.remove(filename);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
if (!isExists) {
|
||||||
}
|
await this.ensureDirectoryEx(filename);
|
||||||
if (!isExists) {
|
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||||
await this.ensureDirectoryEx(filename);
|
Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`);
|
||||||
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
return true;
|
||||||
Logger(`STORAGE <-- DB:${filename}: written (hidden,new${force ? ", force" : ""})`);
|
} else {
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// const stat = await this.app.vault.adapter.stat(filename);
|
|
||||||
// const fileMTime = ~~(stat.mtime/1000);
|
|
||||||
// const docMtime = ~~(old.mtime/1000);
|
|
||||||
const contentBin = await this.app.vault.adapter.readBinary(filename);
|
const contentBin = await this.app.vault.adapter.readBinary(filename);
|
||||||
const content = await arrayBufferToBase64(contentBin);
|
const content = await arrayBufferToBase64(contentBin);
|
||||||
if (content == fileOnDB.data && !force) {
|
if (content == fileOnDB.data && !force) {
|
||||||
@@ -3120,10 +3143,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
await this.app.vault.adapter.writeBinary(filename, base64ToArrayBuffer(fileOnDB.data), { mtime: fileOnDB.mtime, ctime: fileOnDB.ctime });
|
||||||
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""})`);
|
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""})`);
|
||||||
return true;
|
return true;
|
||||||
} catch (ex) {
|
|
||||||
Logger(ex);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`STORAGE <-- DB:${filename}: written (hidden, overwrite${force ? ", force" : ""}) Failed`);
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3154,54 +3179,58 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async resolveConflictOnInternalFile(id: string): Promise<boolean> {
|
async resolveConflictOnInternalFile(id: string): Promise<boolean> {
|
||||||
// Retrieve data
|
try {// Retrieve data
|
||||||
const doc = await this.localDatabase.localDatabase.get(id, { conflicts: true });
|
const doc = await this.localDatabase.localDatabase.get(id, { conflicts: true });
|
||||||
// If there is no conflict, return with false.
|
// If there is no conflict, return with false.
|
||||||
if (!("_conflicts" in doc)) return false;
|
if (!("_conflicts" in doc)) return false;
|
||||||
if (doc._conflicts.length == 0) return false;
|
if (doc._conflicts.length == 0) return false;
|
||||||
Logger(`Hidden file conflicted:${id2filenameInternalMetadata(id)}`);
|
Logger(`Hidden file conflicted:${id2filenameInternalMetadata(id)}`);
|
||||||
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
||||||
const revA = doc._rev;
|
const revA = doc._rev;
|
||||||
const revB = conflicts[0];
|
const revB = conflicts[0];
|
||||||
|
|
||||||
if (doc._id.endsWith(".json")) {
|
if (doc._id.endsWith(".json")) {
|
||||||
const conflictedRev = conflicts[0];
|
const conflictedRev = conflicts[0];
|
||||||
const conflictedRevNo = Number(conflictedRev.split("-")[0]);
|
const conflictedRevNo = Number(conflictedRev.split("-")[0]);
|
||||||
//Search
|
//Search
|
||||||
const revFrom = (await this.localDatabase.localDatabase.get(id, { revs_info: true })) as unknown as LoadedEntry & PouchDB.Core.GetMeta;
|
const revFrom = (await this.localDatabase.localDatabase.get(id, { revs_info: true })) as unknown as LoadedEntry & PouchDB.Core.GetMeta;
|
||||||
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
|
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
|
||||||
const result = await this.mergeObject(id, commonBase, doc._rev, conflictedRev);
|
const result = await this.mergeObject(id, commonBase, doc._rev, conflictedRev);
|
||||||
if (result) {
|
if (result) {
|
||||||
Logger(`Object merge:${id}`, LOG_LEVEL.INFO);
|
Logger(`Object merge:${id}`, LOG_LEVEL.INFO);
|
||||||
const filename = id2filenameInternalMetadata(id);
|
const filename = id2filenameInternalMetadata(id);
|
||||||
const isExists = await this.app.vault.adapter.exists(filename);
|
const isExists = await this.app.vault.adapter.exists(filename);
|
||||||
if (!isExists) {
|
if (!isExists) {
|
||||||
await this.ensureDirectoryEx(filename);
|
await this.ensureDirectoryEx(filename);
|
||||||
|
}
|
||||||
|
await this.app.vault.adapter.write(filename, result);
|
||||||
|
const stat = await this.app.vault.adapter.stat(filename);
|
||||||
|
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
||||||
|
await this.extractInternalFileFromDatabase(filename);
|
||||||
|
await this.localDatabase.localDatabase.remove(id, revB);
|
||||||
|
return this.resolveConflictOnInternalFile(id);
|
||||||
|
} else {
|
||||||
|
Logger(`Object merge is not applicable.`, LOG_LEVEL.VERBOSE);
|
||||||
}
|
}
|
||||||
await this.app.vault.adapter.write(filename, result);
|
|
||||||
const stat = await this.app.vault.adapter.stat(filename);
|
|
||||||
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
|
||||||
await this.extractInternalFileFromDatabase(filename);
|
|
||||||
await this.localDatabase.localDatabase.remove(id, revB);
|
|
||||||
return this.resolveConflictOnInternalFile(id);
|
|
||||||
} else {
|
|
||||||
Logger(`Object merge is not applicable.`, LOG_LEVEL.VERBOSE);
|
|
||||||
}
|
}
|
||||||
|
const revBDoc = await this.localDatabase.localDatabase.get(id, { rev: revB });
|
||||||
|
// determine which revision should been deleted.
|
||||||
|
// simply check modified time
|
||||||
|
const mtimeA = ("mtime" in doc && doc.mtime) || 0;
|
||||||
|
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
|
||||||
|
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
|
||||||
|
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
|
||||||
|
const delRev = mtimeA < mtimeB ? revA : revB;
|
||||||
|
// delete older one.
|
||||||
|
await this.localDatabase.localDatabase.remove(id, delRev);
|
||||||
|
Logger(`Older one has been deleted:${id2filenameInternalMetadata(id)}`);
|
||||||
|
// check the file again
|
||||||
|
return this.resolveConflictOnInternalFile(id);
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("Failed to resolve conflict (Hidden)")
|
||||||
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const revBDoc = await this.localDatabase.localDatabase.get(id, { rev: revB });
|
|
||||||
// determine which revision should been deleted.
|
|
||||||
// simply check modified time
|
|
||||||
const mtimeA = ("mtime" in doc && doc.mtime) || 0;
|
|
||||||
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
|
|
||||||
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
|
|
||||||
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
|
|
||||||
const delRev = mtimeA < mtimeB ? revA : revB;
|
|
||||||
// delete older one.
|
|
||||||
await this.localDatabase.localDatabase.remove(id, delRev);
|
|
||||||
Logger(`Older one has been deleted:${id2filenameInternalMetadata(id)}`);
|
|
||||||
// check the file again
|
|
||||||
return this.resolveConflictOnInternalFile(id);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||||
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
async syncInternalFilesAndDatabase(direction: "push" | "pull" | "safe", showMessage: boolean, files: InternalFileInfo[] | false = false, targetFiles: string[] | false = false) {
|
||||||
|
|||||||
@@ -80,8 +80,12 @@
|
|||||||
- The default batch size is smaller again.
|
- The default batch size is smaller again.
|
||||||
- Plugins and their setting can be synchronised again.
|
- Plugins and their setting can be synchronised again.
|
||||||
- Hidden files and plugins are correctly scanned while rebuilding.
|
- Hidden files and plugins are correctly scanned while rebuilding.
|
||||||
- Files with the name started `_` are also being performed conflict-checking.
|
- Files with the name started `_` are also being performed conflict-checking.
|
||||||
- 0.17.17
|
- 0.17.17
|
||||||
- Fixed: Now we can merge JSON files even if we failed to compare items like null.
|
- Fixed: Now we can merge JSON files even if we failed to compare items like null.
|
||||||
|
- 0.17.18
|
||||||
|
- Fixed: Fixed lack of error handling.
|
||||||
|
- 0.17.19
|
||||||
|
- Fixed: Error reporting has been ensured.
|
||||||
|
|
||||||
... To continue on to `updates_old.md`.
|
... To continue on to `updates_old.md`.
|
||||||
Reference in New Issue
Block a user