mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-10 01:31:54 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee9364310d | ||
|
|
86b9695bc2 |
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.22.6",
|
"version": "0.22.7",
|
||||||
"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",
|
||||||
"authorUrl": "https://github.com/vrtmrz",
|
"authorUrl": "https://github.com/vrtmrz",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.22.6",
|
"version": "0.22.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.22.6",
|
"version": "0.22.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.22.6",
|
"version": "0.22.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",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { normalizePath, type PluginManifest } from "./deps";
|
import { normalizePath, type PluginManifest } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry, type DocumentID } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||||
import { createBinaryBlob, isDocContentSame, sendSignal } from "./lib/src/utils";
|
import { createBinaryBlob, isDocContentSame, sendSignal } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
@@ -102,11 +102,13 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
const stat = await this.vaultAccess.adapterStat(path);
|
const stat = await this.vaultAccess.adapterStat(path);
|
||||||
// sometimes folder is coming.
|
// sometimes folder is coming.
|
||||||
if (stat && stat.type != "file")
|
if (stat != null && stat.type != "file") {
|
||||||
return;
|
return;
|
||||||
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
}
|
||||||
|
const mtime = stat == null ? 0 : stat?.mtime ?? 0;
|
||||||
|
const storageMTime = ~~((mtime) / 1000);
|
||||||
const key = `${path}-${storageMTime}`;
|
const key = `${path}-${storageMTime}`;
|
||||||
if (this.recentProcessedInternalFiles.contains(key)) {
|
if (mtime != 0 && this.recentProcessedInternalFiles.contains(key)) {
|
||||||
//If recently processed, it may caused by self.
|
//If recently processed, it may caused by self.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,6 +152,27 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
|
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: EntryDoc, currentRev: string, conflictedRev: string) {
|
||||||
|
const conflictedDoc = await this.localDatabase.getRaw(id, { rev: conflictedRev });
|
||||||
|
// determine which revision should been deleted.
|
||||||
|
// simply check modified time
|
||||||
|
const mtimeCurrent = ("mtime" in currentDoc && currentDoc.mtime) || 0;
|
||||||
|
const mtimeConflicted = ("mtime" in conflictedDoc && conflictedDoc.mtime) || 0;
|
||||||
|
// Logger(`Revisions:${new Date(mtimeA).toLocaleString} and ${new Date(mtimeB).toLocaleString}`);
|
||||||
|
// console.log(`mtime:${mtimeA} - ${mtimeB}`);
|
||||||
|
const delRev = mtimeCurrent < mtimeConflicted ? currentRev : conflictedRev;
|
||||||
|
// delete older one.
|
||||||
|
await this.localDatabase.removeRevision(id, delRev);
|
||||||
|
Logger(`Older one has been deleted:${path}`);
|
||||||
|
const cc = await this.localDatabase.getRaw(id, { conflicts: true });
|
||||||
|
if (cc._conflicts.length == 0) {
|
||||||
|
await this.extractInternalFileFromDatabase(stripAllPrefixes(path))
|
||||||
|
} else {
|
||||||
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
}
|
||||||
|
// check the file again
|
||||||
|
|
||||||
|
}
|
||||||
conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => {
|
conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => {
|
||||||
const path = paths[0];
|
const path = paths[0];
|
||||||
sendSignal(`cancel-internal-conflict:${path}`);
|
sendSignal(`cancel-internal-conflict:${path}`);
|
||||||
@@ -185,27 +208,16 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
const stat = await this.vaultAccess.adapterStat(filename);
|
const stat = await this.vaultAccess.adapterStat(filename);
|
||||||
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
||||||
await this.extractInternalFileFromDatabase(filename);
|
await this.extractInternalFileFromDatabase(filename);
|
||||||
await this.localDatabase.removeRaw(id, revB);
|
await this.localDatabase.removeRevision(id, revB);
|
||||||
this.conflictResolutionProcessor.enqueue(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
return [{ path, revA, revB }];
|
return [{ path, revA, revB, id, doc }];
|
||||||
}
|
}
|
||||||
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB });
|
// When not JSON file, resolve conflicts by choosing a newer one.
|
||||||
// determine which revision should been deleted.
|
await this.resolveByNewerEntry(id, path, doc, revA, revB);
|
||||||
// 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.removeRaw(id, delRev);
|
|
||||||
Logger(`Older one has been deleted:${path}`);
|
|
||||||
// check the file again
|
|
||||||
this.conflictResolutionProcessor.enqueue(path);
|
|
||||||
return;
|
return;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
||||||
@@ -215,7 +227,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}, {
|
}, {
|
||||||
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10,
|
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10,
|
||||||
pipeTo: new QueueProcessor(async (results) => {
|
pipeTo: new QueueProcessor(async (results) => {
|
||||||
const { path, revA, revB } = results[0]
|
const { id, doc, path, revA, revB } = results[0];
|
||||||
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
||||||
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
||||||
if (docAMerge != false && docBMerge != false) {
|
if (docAMerge != false && docBMerge != false) {
|
||||||
@@ -224,6 +236,9 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
this.conflictResolutionProcessor.enqueue(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
// If either revision could not read, force resolving by the newer one.
|
||||||
|
await this.resolveByNewerEntry(id, path, doc, revA, revB);
|
||||||
}
|
}
|
||||||
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 })
|
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 })
|
||||||
})
|
})
|
||||||
@@ -361,7 +376,11 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (xFileOnStorage && !xFileOnDatabase) {
|
} else if (xFileOnStorage && !xFileOnDatabase) {
|
||||||
await this.storeInternalFileToDatabase(xFileOnStorage);
|
if (direction == "push" || direction == "pushForce" || direction == "safe") {
|
||||||
|
await this.storeInternalFileToDatabase(xFileOnStorage);
|
||||||
|
} else {
|
||||||
|
await this.extractInternalFileFromDatabase(xFileOnStorage.path);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid state on hidden file sync");
|
throw new Error("Invalid state on hidden file sync");
|
||||||
// Something corrupted?
|
// Something corrupted?
|
||||||
@@ -513,6 +532,14 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
type: "newnote",
|
type: "newnote",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
// Remove all conflicted before deleting.
|
||||||
|
const conflicts = await this.localDatabase.getRaw(old._id, { conflicts: true });
|
||||||
|
if ("_conflicts" in conflicts) {
|
||||||
|
for (const conflictRev of conflicts._conflicts) {
|
||||||
|
await this.localDatabase.removeRevision(old._id, conflictRev);
|
||||||
|
Logger(`STORAGE -x> DB:${filename}: (hidden) conflict removed ${old._rev} => ${conflictRev}`, LOG_LEVEL_VERBOSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (old.deleted) {
|
if (old.deleted) {
|
||||||
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
|
Logger(`STORAGE -x> DB:${filename}: (hidden) already deleted`);
|
||||||
return;
|
return;
|
||||||
@@ -546,8 +573,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
return await serialized("file-" + prefixedFileName, async () => {
|
return await serialized("file-" + prefixedFileName, async () => {
|
||||||
try {
|
try {
|
||||||
// Check conflicted status
|
// Check conflicted status
|
||||||
//TODO option
|
const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true, true);
|
||||||
const fileOnDB = await this.localDatabase.getDBEntry(prefixedFileName, { conflicts: true }, false, true);
|
|
||||||
if (fileOnDB === false)
|
if (fileOnDB === false)
|
||||||
throw new Error(`File not found on database.:${filename}`);
|
throw new Error(`File not found on database.:${filename}`);
|
||||||
// Prevent overwrite for Prevent overwriting while some conflicted revision exists.
|
// Prevent overwrite for Prevent overwriting while some conflicted revision exists.
|
||||||
@@ -555,7 +581,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO);
|
Logger(`Hidden file ${filename} has conflicted revisions, to keep in safe, writing to storage has been prevented`, LOG_LEVEL_INFO);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deleted = "deleted" in fileOnDB ? fileOnDB.deleted : false;
|
const deleted = fileOnDB.deleted || fileOnDB._deleted || false;
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
if (!isExists) {
|
if (!isExists) {
|
||||||
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) Deleted on DB, but the file is already not found on storage.`);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function isComparableTextDecode(path: string) {
|
|||||||
return ["json"].includes(ext)
|
return ["json"].includes(ext)
|
||||||
}
|
}
|
||||||
function readDocument(w: LoadedEntry) {
|
function readDocument(w: LoadedEntry) {
|
||||||
|
if (w.data.length == 0) return "";
|
||||||
if (isImage(w.path)) {
|
if (isImage(w.path)) {
|
||||||
return new Uint8Array(decodeBinary(w.data));
|
return new Uint8Array(decodeBinary(w.data));
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
}
|
}
|
||||||
const db = this.plugin.localDatabase;
|
const db = this.plugin.localDatabase;
|
||||||
try {
|
try {
|
||||||
const w = await db.localDatabase.get(this.id, { revs_info: true });
|
const w = await db.getRaw(this.id, { revs_info: true });
|
||||||
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
|
||||||
this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
|
this.range.max = `${Math.max(this.revs_info.length - 1, 0)}`;
|
||||||
this.range.value = this.range.max;
|
this.range.value = this.range.max;
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 46256dba3a...1c8ed1d974
@@ -2864,7 +2864,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
|||||||
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
|
const mtimeB = ("mtime" in revBDoc && revBDoc.mtime) || 0;
|
||||||
const delRev = mtimeA < mtimeB ? revA : revB;
|
const delRev = mtimeA < mtimeB ? revA : revB;
|
||||||
// delete older one.
|
// delete older one.
|
||||||
await this.localDatabase.removeRaw(id, delRev);
|
await this.localDatabase.removeRevision(id, delRev);
|
||||||
Logger(`Older one has been deleted:${this.getPath(doc)}`);
|
Logger(`Older one has been deleted:${this.getPath(doc)}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ Note: we got a very performance improvement.
|
|||||||
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
|
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
|
||||||
|
|
||||||
#### Version history
|
#### Version history
|
||||||
|
- 0.22.7
|
||||||
|
- Fixed:
|
||||||
|
- No longer deleted hidden files were ignored.
|
||||||
|
- The document history dialogue is now able to process the deleted revisions.
|
||||||
|
- Deletion of a hidden file is now surely performed even if the file is already conflicted.
|
||||||
- 0.22.6
|
- 0.22.6
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- Fixed a problem with synchronisation taking a long time to start in some cases.
|
- Fixed a problem with synchronisation taking a long time to start in some cases.
|
||||||
|
|||||||
Reference in New Issue
Block a user