Compare commits

...

11 Commits

Author SHA1 Message Date
vorotamoroz
5f96cc6b82 bump 2022-09-28 17:57:23 +09:00
vorotamoroz
8c8f5d045f Fixed:
- Fixed bug about renaming file
2022-09-28 17:56:34 +09:00
vorotamoroz
40cf8be890 Bump 2022-09-28 16:17:11 +09:00
vorotamoroz
6b03dbbe75 Fixed:
- File tracking logic has been refined.
2022-09-28 16:17:04 +09:00
vorotamoroz
74425f75d2 bump 2022-09-27 17:59:05 +09:00
vorotamoroz
ac7c622466 Fixed docs. 2022-09-27 17:58:31 +09:00
vorotamoroz
4b32365694 Implemented:
- Add new features for setting Self-hosted LiveSync up more easier.
2022-09-27 17:58:13 +09:00
vorotamoroz
728edac283 Merge pull request #114 from JEndler/main
Fixed Docker command in docs.
2022-09-15 17:45:38 +09:00
Jakob Endler
ab9c0190bb Fixed Docker command in docs. 2022-09-12 18:36:50 +02:00
vorotamoroz
5a7610d411 bump 2022-09-12 11:16:41 +09:00
vorotamoroz
4691ae1463 Fixed:
- Now we can detect hidden files changes and morethings again.
2022-09-12 11:03:28 +09:00
12 changed files with 341 additions and 228 deletions

View File

@@ -92,9 +92,6 @@ After installing Self-hosted LiveSync on the device, select `Open setup URI` fro
Answer the following. Answer the following.
- `Yes` to `Importing LiveSync's conf, OK?` - `Yes` to `Importing LiveSync's conf, OK?`
- `No` to `Keep local DB?` - `Set it up as secondary or subsequent device` to `How would you like to set it up?`.
- `Yes` to `Keep remote DB?`
- `No` to `Rebuild the database?`
- `Yes` to `Replicate once?`
Then, The configuration will now take effect and replication will start. Your files will be synchronised soon! Then, The configuration will now take effect and replication will start. Your files will be synchronised soon!

View File

@@ -85,13 +85,10 @@ All done! と表示されれば完了です。自動的に、`Copy setup URI`が
クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。 クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
# 2台目以降の設定方法 # 2台目以降の設定方法
台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Open setup URI`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。 2台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Open setup URI`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。
下記のように答えてください。 下記のように答えてください。
- `Importing LiveSync's conf, OK?``Yes` - `Importing LiveSync's conf, OK?``Yes`
- `Keep local DB?``No` - `How would you like to set it up?``Set it up as secondary or subsequent device`
- `Keep remote DB?``Yes`
- `Rebuild the database?``No`
- `Replicate once?``Yes`
これで設定が反映され、レプリケーションが開始されます。 これで設定が反映され、レプリケーションが開始されます。

View File

@@ -32,16 +32,18 @@ max_age = 3600
Make `local.ini` and run with docker run like this, you can launch the CouchDB. Make `local.ini` and run with docker run like this, you can launch the CouchDB.
``` ```
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb $ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
``` ```
*Remember to replace the path with the path to your local.ini*
Note: At this time, the file owner of local.ini became 5984:5984. It's the limitation docker image. please change the owner before editing local.ini again. Note: At this time, the file owner of local.ini became 5984:5984. It's the limitation docker image. please change the owner before editing local.ini again.
If you could confirm that Self-hosted LiveSync can sync with the server, launch docker image as background as you like. If you could confirm that Self-hosted LiveSync can sync with the server, launch docker image as background as you like.
example) Example to run docker in detached mode:
``` ```
$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb $ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
``` ```
*Remember to replace the path with the path to your local.ini*
## Access from mobile device ## Access from mobile device
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate. If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate.

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.15.3", "version": "0.15.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",

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.15.3", "version": "0.15.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",

View File

@@ -164,6 +164,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
}) })
const infoWarnForSubsequent = setupWizardEl.createEl("div", { text: `To set up second or subsequent device, please use 'Copy setup URI' and 'Open setup URI'` });
infoWarnForSubsequent.addClass("op-warn-info");
new Setting(setupWizardEl)
.setName("Copy setup URI")
.addButton((text) => {
text.setButtonText("Copy setup URI").onClick(() => {
// @ts-ignore
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
})
})
.addButton((text) => {
text.setButtonText("Open setup URI").onClick(() => {
// @ts-ignore
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-opensetupuri")
})
})
addScreenElement("110", setupWizardEl); addScreenElement("110", setupWizardEl);
@@ -361,7 +379,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.onClick(async () => { .onClick(async () => {
await applyEncryption(true); await applyEncryption(true);
}) })
); )
.addButton((button) =>
button
.setButtonText("Apply w/o rebuilding")
.setWarning()
.setDisabled(false)
.setClass("sls-btn-right")
.onClick(async () => {
await applyEncryption(false);
})
);
const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => { const rebuildDB = async (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice") => {

View File

@@ -98,7 +98,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) { constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
super(app); super(app);
this.app = app; this.app = app;
this.setPlaceholder(placeholder ?? "y/n) " + note); this.setPlaceholder((placeholder ?? "y/n) ") + note);
if (getItemsFun) this.getItemsFun = getItemsFun; if (getItemsFun) this.getItemsFun = getItemsFun;
this.callback = callback; this.callback = callback;
} }

Submodule src/lib updated: 2c39c15177...d8d83b7f46

View File

@@ -41,7 +41,7 @@ setNoticeClass(Notice);
const ICHeader = "i:"; const ICHeader = "i:";
const ICHeaderEnd = "i;"; const ICHeaderEnd = "i;";
const ICHeaderLength = ICHeader.length; const ICHeaderLength = ICHeader.length;
const FileWatchEventQueueMax = 10;
/** /**
* returns is internal chunk of file * returns is internal chunk of file
@@ -82,7 +82,7 @@ const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
const askSelectString = (app: App, message: string, items: string[]): Promise<string> => { const askSelectString = (app: App, message: string, items: string[]): Promise<string> => {
const getItemsFun = () => items; const getItemsFun = () => items;
return new Promise((res) => { return new Promise((res) => {
const popover = new PopoverSelectString(app, message, "Select file)", getItemsFun, (result) => res(result)); const popover = new PopoverSelectString(app, message, "", getItemsFun, (result) => res(result));
popover.open(); popover.open();
}); });
}; };
@@ -109,6 +109,19 @@ function recentlyTouched(file: TFile) {
function clearTouched() { function clearTouched() {
touchedFiles = []; touchedFiles = [];
} }
type CacheData = string | ArrayBuffer;
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME";
type FileEventArgs = {
file: TAbstractFile;
cache?: CacheData;
oldPath?: string;
ctx?: any;
}
type FileEventItem = {
type: FileEventType,
args: FileEventArgs
}
export default class ObsidianLiveSyncPlugin extends Plugin { export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings; settings: ObsidianLiveSyncSettings;
localDatabase: LocalPouchDB; localDatabase: LocalPouchDB;
@@ -118,6 +131,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
suspended: boolean; suspended: boolean;
deviceAndVaultName: string; deviceAndVaultName: string;
isMobile = false; isMobile = false;
isReady = false;
watchedFileEventQueue = [] as FileEventItem[];
getVaultName(): string { getVaultName(): string {
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : ""); return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
@@ -277,10 +293,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.refreshStatusText = this.refreshStatusText.bind(this); this.refreshStatusText = this.refreshStatusText.bind(this);
this.statusBar2 = this.addStatusBarItem(); this.statusBar2 = this.addStatusBarItem();
// this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
// this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
// this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
this.watchVaultChange = this.watchVaultChange.bind(this); this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this); this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this); this.watchVaultDelete = this.watchVaultDelete.bind(this);
@@ -298,6 +310,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
// this.registerWatchEvents(); // this.registerWatchEvents();
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this)); this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
this.registerFileWatchEvents();
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
if (this.localDatabase.isReady) if (this.localDatabase.isReady)
try { try {
@@ -340,11 +353,31 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const configURIBase = "obsidian://setuplivesync?settings="; const configURIBase = "obsidian://setuplivesync?settings=";
this.addCommand({ this.addCommand({
id: "livesync-copysetupuri", id: "livesync-copysetupuri",
name: "Copy setup URI (beta)", name: "Copy setup URI",
callback: async () => { callback: async () => {
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", ""); const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", "");
if (encryptingPassphrase === false) return; if (encryptingPassphrase === false) return;
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), encryptingPassphrase)); const setting = { ...this.settings };
const keys = Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[];
for (const k of keys) {
if (JSON.stringify(k in setting ? setting[k] : "") == JSON.stringify(k in DEFAULT_SETTINGS ? DEFAULT_SETTINGS[k] : "*")) {
delete setting[k];
}
}
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase));
const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri);
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
},
});
this.addCommand({
id: "livesync-copysetupurifull",
name: "Copy setup URI (Full)",
callback: async () => {
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", "");
if (encryptingPassphrase === false) return;
const setting = { ...this.settings };
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(setting), encryptingPassphrase));
const uri = `${configURIBase}${encryptedSetting}`; const uri = `${configURIBase}${encryptedSetting}`;
await navigator.clipboard.writeText(uri); await navigator.clipboard.writeText(uri);
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE); Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
@@ -352,9 +385,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
this.addCommand({ this.addCommand({
id: "livesync-opensetupuri", id: "livesync-opensetupuri",
name: "Open setup URI (beta)", name: "Open setup URI",
callback: async () => { callback: async () => {
const setupURI = await askString(this.app, "Set up manually", "Set up URI", `${configURIBase}aaaaa`); const setupURI = await askString(this.app, "Easy setup", "Set up URI", `${configURIBase}aaaaa`);
if (setupURI === false) return; if (setupURI === false) return;
if (!setupURI.startsWith(`${configURIBase}`)) { if (!setupURI.startsWith(`${configURIBase}`)) {
Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE); Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE);
@@ -374,58 +407,91 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (newConf) { if (newConf) {
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?"); const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
if (result == "yes") { if (result == "yes") {
const newSettingW = Object.assign({}, this.settings, newConf); const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf);
// stopping once.
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
this.settings.suspendFileWatching = true; this.settings.suspendFileWatching = true;
console.dir(newSettingW); console.dir(newSettingW);
const keepLocalDB = await askYesNo(this.app, "Keep local DB?"); const setupJustImport = "Just import setting";
const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?"); const setupAsNew = "Set it up as secondary or subsequent device";
if (keepLocalDB == "yes" && keepRemoteDB == "yes") { const setupAgain = "Reconfigure and reconstitute the data";
// nothing to do. so peaceful. const setupManually = "Leave everything to me";
const setupType = await askSelectString(this.app, "How would you like to set it up?", [setupAsNew, setupAgain, setupJustImport, setupManually]);
if (setupType == setupJustImport) {
this.settings = newSettingW; this.settings = newSettingW;
await this.saveSettings(); await this.saveSettings();
const replicate = await askYesNo(this.app, "Unlock and replicate?"); } else if (setupType == setupAsNew) {
if (replicate == "yes") { this.settings = newSettingW;
await this.replicate(true); await this.saveSettings();
await this.markRemoteUnlocked(); await this.resetLocalOldDatabase();
} await this.resetLocalDatabase();
Logger("Configuration loaded.", LOG_LEVEL.NOTICE); await this.localDatabase.initializeDatabase();
return; await this.markRemoteResolved();
} await this.replicate(true);
if (keepLocalDB == "no" && keepRemoteDB == "no") { } else if (setupType == setupAgain) {
const reset = await askYesNo(this.app, "Drop everything?"); const confirm = "I know this operation will rebuild all my databases with files on this device, and files that are on the remote database and I didn't synchronize to any other devices will be lost and want to proceed indeed.";
if (reset != "yes") { if (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) {
Logger("Cancelled", LOG_LEVEL.NOTICE);
this.settings = oldConf;
return; return;
} }
} await this.saveSettings();
let initDB; await this.resetLocalOldDatabase();
this.settings = newSettingW; await this.resetLocalDatabase();
await this.saveSettings(); await this.localDatabase.initializeDatabase();
if (keepLocalDB == "no") { await this.initializeDatabase(true);
this.resetLocalOldDatabase();
this.resetLocalDatabase();
this.localDatabase.initializeDatabase();
const rebuild = await askYesNo(this.app, "Rebuild the database?");
if (rebuild == "yes") {
initDB = this.initializeDatabase(true);
} else {
this.markRemoteResolved();
}
}
if (keepRemoteDB == "no") {
await this.tryResetRemoteDatabase(); await this.tryResetRemoteDatabase();
await this.markRemoteLocked(); await this.markRemoteLocked();
} await this.markRemoteResolved();
if (keepLocalDB == "no" || keepRemoteDB == "no") { await this.replicate(true);
const replicate = await askYesNo(this.app, "Replicate once?");
if (replicate == "yes") { } else if (setupType == setupManually) {
if (initDB != null) { const keepLocalDB = await askYesNo(this.app, "Keep local DB?");
await initDB; const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?");
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
// nothing to do. so peaceful.
this.settings = newSettingW;
await this.saveSettings();
const replicate = await askYesNo(this.app, "Unlock and replicate?");
if (replicate == "yes") {
await this.replicate(true);
await this.markRemoteUnlocked();
}
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
return;
}
if (keepLocalDB == "no" && keepRemoteDB == "no") {
const reset = await askYesNo(this.app, "Drop everything?");
if (reset != "yes") {
Logger("Cancelled", LOG_LEVEL.NOTICE);
this.settings = oldConf;
return;
}
}
let initDB;
this.settings = newSettingW;
await this.saveSettings();
if (keepLocalDB == "no") {
this.resetLocalOldDatabase();
this.resetLocalDatabase();
this.localDatabase.initializeDatabase();
const rebuild = await askYesNo(this.app, "Rebuild the database?");
if (rebuild == "yes") {
initDB = this.initializeDatabase(true);
} else {
this.markRemoteResolved();
}
}
if (keepRemoteDB == "no") {
await this.tryResetRemoteDatabase();
await this.markRemoteLocked();
}
if (keepLocalDB == "no" || keepRemoteDB == "no") {
const replicate = await askYesNo(this.app, "Replicate once?");
if (replicate == "yes") {
if (initDB != null) {
await initDB;
}
await this.replicate(true);
} }
await this.replicate(true);
} }
} }
} }
@@ -647,12 +713,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
gcTimerHandler: any = null; gcTimerHandler: any = null;
registerFileWatchEvents() {
registerWatchEvents() {
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange)); this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete)); this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename)); this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate)); this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
}
registerWatchEvents() {
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
window.addEventListener("visibilitychange", this.watchWindowVisibility); window.addEventListener("visibilitychange", this.watchWindowVisibility);
window.addEventListener("online", this.watchOnline); window.addEventListener("online", this.watchOnline);
@@ -675,6 +743,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async watchWindowVisibilityAsync() { async watchWindowVisibilityAsync() {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
// if (this.suspended) return; // if (this.suspended) return;
const isHidden = document.hidden; const isHidden = document.hidden;
await this.applyBatchChange(); await this.applyBatchChange();
@@ -699,12 +768,125 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
// Cache file and waiting to can be proceed.
async appendWatchEvent(type: FileEventType, file: TAbstractFile, oldPath?: string, ctx?: any) {
// check really we can process.
if (!this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
let cache: null | string | ArrayBuffer;
// new file or something changed, cache the changes.
if (file instanceof TFile && (type == "CREATE" || type == "CHANGED")) {
if (recentlyTouched(file)) {
return;
}
if (!isPlainText(file.name)) {
cache = await this.app.vault.readBinary(file);
} else {
// cache = await this.app.vault.read(file);
cache = await this.app.vault.cachedRead(file);
if (!cache) cache = await this.app.vault.read(file);
}
}
if (this.settings.batchSave) {
// if the latest event is the same type, omit that
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
// a.md MODIFY
// a.md CREATE
// :
let i = this.watchedFileEventQueue.length;
while (i >= 0) {
i--;
if (i < 0) break;
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
continue;
}
if (this.watchedFileEventQueue[i].type != type) break;
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
}
}
this.watchedFileEventQueue.push({
type,
args: {
file,
oldPath,
cache,
ctx
}
})
this.refreshStatusText();
if (this.isReady) {
await this.procFileEvent();
}
}
async procFileEvent(applyBatch?: boolean) {
if (!this.isReady) return;
if (this.settings.batchSave) {
if (!applyBatch && this.watchedFileEventQueue.length < FileWatchEventQueueMax) {
// Defer till applying batch save or queue has been grown enough.
// or 120 seconds after.
setTrigger("applyBatchAuto", 120000, () => {
this.procFileEvent(true);
})
return;
}
}
clearTrigger("applyBatchAuto");
const ret = await runWithLock("procFiles", false, async () => {
const procs = [...this.watchedFileEventQueue];
this.watchedFileEventQueue = [];
for (const queue of procs) {
const file = queue.args.file;
const cache = queue.args.cache;
if ((queue.type == "CREATE" || queue.type == "CHANGED") && file instanceof TFile) {
await this.updateIntoDB(file, false, cache);
}
if (queue.type == "DELETE") {
if (file instanceof TFile) {
await this.deleteFromDB(file);
} else if (file instanceof TFolder) {
await this.deleteFolderOnDB(file);
}
}
if (queue.type == "RENAME") {
await this.watchVaultRenameAsync(file, queue.args.oldPath);
}
}
this.refreshStatusText();
})
this.refreshStatusText();
return ret;
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CREATE", file, null, ctx);
}
watchVaultChange(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("CHANGED", file, null, ctx);
}
watchVaultDelete(file: TAbstractFile, ctx?: any) {
this.appendWatchEvent("DELETE", file, null, ctx);
}
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
this.appendWatchEvent("RENAME", file, oldFile, ctx);
}
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
if (this.settings.suspendFileWatching) return; if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
this.watchWorkspaceOpenAsync(file); this.watchWorkspaceOpenAsync(file);
} }
async watchWorkspaceOpenAsync(file: TFile) { async watchWorkspaceOpenAsync(file: TFile) {
if (this.settings.suspendFileWatching) return;
if (!this.isReady) return;
await this.applyBatchChange(); await this.applyBatchChange();
if (file == null) { if (file == null) {
return; return;
@@ -715,100 +897,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.showIfConflicted(file); await this.showIfConflicted(file);
} }
watchVaultCreate(file: TFile, ...args: any[]) {
if (!this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
if (recentlyTouched(file)) {
return;
}
this.watchVaultChangeAsync(file, ...args);
}
watchVaultChange(file: TAbstractFile, ...args: any[]) {
if (!this.isTargetFile(file)) return;
if (!(file instanceof TFile)) {
return;
}
if (recentlyTouched(file)) {
return;
}
if (this.settings.suspendFileWatching) return;
// If batchSave is enabled, queue all changes and do nothing.
if (this.settings.batchSave) {
~(async () => {
const meta = await this.localDatabase.getDBEntryMeta(file.path);
if (meta != false) {
const localMtime = ~~(file.stat.mtime / 1000);
const docMtime = ~~(meta.mtime / 1000);
if (localMtime !== docMtime) {
// Perhaps we have to modify (to using newer doc), but we don't be sure to every device's clock is adjusted.
this.batchFileChange = Array.from(new Set([...this.batchFileChange, file.path]));
this.refreshStatusText();
}
}
})();
return;
}
this.watchVaultChangeAsync(file, ...args);
}
async applyBatchChange() { async applyBatchChange() {
if (!this.settings.batchSave || this.batchFileChange.length == 0) { return await this.procFileEvent(true);
return;
}
return await runWithLock("batchSave", false, async () => {
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
this.batchFileChange = [];
const semaphore = Semaphore(3);
const batchProcesses = batchItems.map(e => (async (e) => {
const releaser = await semaphore.acquire(1, "batch");
try {
const f = this.app.vault.getAbstractFileByPath(normalizePath(e));
if (f && f instanceof TFile) {
await this.updateIntoDB(f);
Logger(`Batch save:${e}`);
}
} catch (ex) {
Logger(`Batch save error:${e}`, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE);
} finally {
releaser();
}
})(e))
await Promise.all(batchProcesses);
this.refreshStatusText();
return;
});
}
batchFileChange: string[] = [];
async watchVaultChangeAsync(file: TFile, ...args: any[]) {
if (file instanceof TFile) {
if (recentlyTouched(file)) {
return;
}
await this.updateIntoDB(file);
}
}
watchVaultDelete(file: TAbstractFile) {
if (!this.isTargetFile(file)) return;
// When save is delayed, it should be cancelled.
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
if (this.settings.suspendFileWatching) return;
this.watchVaultDeleteAsync(file).then(() => { });
}
async watchVaultDeleteAsync(file: TAbstractFile) {
if (file instanceof TFile) {
await this.deleteFromDB(file);
} else if (file instanceof TFolder) {
await this.deleteFolderOnDB(file);
}
} }
GetAllFilesRecursively(file: TAbstractFile): TFile[] { GetAllFilesRecursively(file: TAbstractFile): TFile[] {
@@ -826,12 +916,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
watchVaultRename(file: TAbstractFile, oldFile: any) {
if (!this.isTargetFile(file)) return;
if (this.settings.suspendFileWatching) return;
this.watchVaultRenameAsync(file, oldFile).then(() => { });
}
getFilePath(file: TAbstractFile): string { getFilePath(file: TAbstractFile): string {
if (file instanceof TFolder) { if (file instanceof TFolder) {
if (file.isRoot()) return ""; if (file.isRoot()) return "";
@@ -844,13 +928,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return this.getFilePath(file.parent) + "/" + file.name; return this.getFilePath(file.parent) + "/" + file.name;
} }
async watchVaultRenameAsync(file: TAbstractFile, oldFile: any) { async watchVaultRenameAsync(file: TAbstractFile, oldFile: any, cache?: CacheData) {
Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE); Logger(`${oldFile} renamed to ${file.path}`, LOG_LEVEL.VERBOSE);
try {
await this.applyBatchChange();
} catch (ex) {
Logger(ex);
}
if (file instanceof TFolder) { if (file instanceof TFolder) {
const newFiles = this.GetAllFilesRecursively(file); const newFiles = this.GetAllFilesRecursively(file);
// for guard edge cases. this won't happen and each file's event will be raise. // for guard edge cases. this won't happen and each file's event will be raise.
@@ -871,7 +950,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else if (file instanceof TFile) { } else if (file instanceof TFile) {
try { try {
Logger(`file save ${file.path} into db`); Logger(`file save ${file.path} into db`);
await this.updateIntoDB(file); await this.updateIntoDB(file, false, cache);
Logger(`deleted ${oldFile} from db`); Logger(`deleted ${oldFile} from db`);
await this.deleteFromDBbyPath(oldFile); await this.deleteFromDBbyPath(oldFile);
} catch (ex) { } catch (ex) {
@@ -993,7 +1072,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
ctime: doc.ctime, ctime: doc.ctime,
mtime: doc.mtime, mtime: doc.mtime,
}); });
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1013,7 +1092,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
ctime: doc.ctime, ctime: doc.ctime,
mtime: doc.mtime, mtime: doc.mtime,
}); });
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
Logger(msg + path); Logger(msg + path);
touch(newFile); touch(newFile);
this.app.vault.trigger("create", newFile); this.app.vault.trigger("create", newFile);
@@ -1081,7 +1160,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.ensureDirectory(path); await this.ensureDirectory(path);
try { try {
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 });
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
Logger(msg + path); Logger(msg + path);
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile; const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
touch(xf); touch(xf);
@@ -1099,7 +1178,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
try { try {
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 + path); Logger(msg + path);
this.batchFileChange = this.batchFileChange.filter((e) => e != file.path); // this.batchFileChange = this.batchFileChange.filter((e) => e != file.path);
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile; const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
touch(xf); touch(xf);
this.app.vault.trigger("modify", xf); this.app.vault.trigger("modify", xf);
@@ -1455,7 +1534,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.statusBar.title = this.localDatabase.syncStatus; this.statusBar.title = this.localDatabase.syncStatus;
let waiting = ""; let waiting = "";
if (this.settings.batchSave) { if (this.settings.batchSave) {
waiting = " " + this.batchFileChange.map((e) => "🛫").join(""); waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
waiting = waiting.replace(/(🛫){10}/g, "🚀"); waiting = waiting.replace(/(🛫){10}/g, "🚀");
} }
let queued = ""; let queued = "";
@@ -1528,17 +1607,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async initializeDatabase(showingNotice?: boolean) { async initializeDatabase(showingNotice?: boolean) {
this.isReady = false;
if (await this.openDatabase()) { if (await this.openDatabase()) {
if (this.localDatabase.isReady) { if (this.localDatabase.isReady) {
await this.syncAllFiles(showingNotice); await this.syncAllFiles(showingNotice);
} }
this.isReady = true;
// run queued event once.
await this.procFileEvent(true);
return true; return true;
} else { } else {
this.isReady = false;
return false; return false;
} }
} }
async replicateAllToServer(showingNotice?: boolean) { async replicateAllToServer(showingNotice?: boolean) {
if (!this.isReady) return false;
if (this.settings.autoSweepPlugins) { if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(showingNotice); await this.sweepPlugin(showingNotice);
} }
@@ -1627,9 +1712,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
}); });
if (!initialScan) { if (!initialScan) {
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => { await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
Logger(`Check or pull from db:${e}`); const w = await this.localDatabase.getDBEntryMeta(e);
await this.pullFile(e, filesStorage, false, null, false); if (w) {
Logger(`Check or pull from db:${e} OK`); Logger(`Check or pull from db:${e}`);
await this.pullFile(e, filesStorage, false, null, false);
Logger(`Check or pull from db:${e} OK`);
} else {
Logger(`entry not found, maybe deleted:${e}`);
}
}); });
} }
if (!initialScan) { if (!initialScan) {
@@ -1701,23 +1791,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
} }
} }
async renameFolder(folder: TFolder, oldFile: any) {
for (const v of folder.children) {
const entry = v as TFile & TFolder;
if (entry.children) {
await this.deleteFolderOnDB(entry);
if (this.settings.trashInsteadDelete) {
await this.app.vault.trash(entry, false);
} else {
await this.app.vault.delete(entry);
}
} else {
await this.deleteFromDB(entry);
}
}
}
// --> conflict resolving // --> conflict resolving
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> { async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
try { try {
@@ -1974,20 +2047,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
async updateIntoDB(file: TFile, initialScan?: boolean) { async updateIntoDB(file: TFile, initialScan?: boolean, cache?: CacheData) {
if (!this.isTargetFile(file)) return; if (!this.isTargetFile(file)) return;
if (shouldBeIgnored(file.path)) { if (shouldBeIgnored(file.path)) {
return; return;
} }
let content = ""; let content = "";
let datatype: "plain" | "newnote" = "newnote"; let datatype: "plain" | "newnote" = "newnote";
if (!isPlainText(file.name)) { if (!cache) {
const contentBin = await this.app.vault.readBinary(file); if (!isPlainText(file.name)) {
content = await arrayBufferToBase64(contentBin); const contentBin = await this.app.vault.readBinary(file);
datatype = "newnote"; content = await arrayBufferToBase64(contentBin);
datatype = "newnote";
} else {
content = await this.app.vault.read(file);
datatype = "plain";
}
} else { } else {
content = await this.app.vault.read(file); if (cache instanceof ArrayBuffer) {
datatype = "plain"; content = await arrayBufferToBase64(cache);
datatype = "newnote"
} else {
content = cache;
datatype = "plain";
}
} }
const fullPath = path2id(file.path); const fullPath = path2id(file.path);
const d: LoadedEntry = { const d: LoadedEntry = {

View File

@@ -6,8 +6,12 @@ I appreciate for reviewing and giving me advice @Pouhon158!
#### Minors #### Minors
- 0.15.1 Missed the stylesheet. - 0.15.1 Missed the stylesheet.
- 0.15.2 The wizard has been improved and documentated! - 0.15.2 The wizard has been improved and documented!
- 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard. - 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard.
- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection)
- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier.
- 0.15.6 File tracking logic has been refined.
- 0.15.7 Fixed bug about renaming file.
### 0.14.1 ### 0.14.1
- The target selecting filter was implemented. - The target selecting filter was implemented.
@@ -33,20 +37,5 @@ I appreciate for reviewing and giving me advice @Pouhon158!
- 0.14.6 Change Target to ES2018 - 0.14.6 Change Target to ES2018
- 0.14.7 Refactor and fix typos. - 0.14.7 Refactor and fix typos.
- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any. - 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any.
### 0.13.0
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded. ... To continue on to `updates_old.md`.
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
- We can see the history of deleted files.
- `Pick file to show` was renamed to `Pick a file to show.
- Files in the `Pick a file to show` are now ordered by their modified date descent.
- Update information became to be shown on the major upgrade.
#### Minors
- 0.13.1 Fixed on conflict resolution.
- 0.13.2 Fixed file deletion failures.
- 0.13.4
- Now, we can synchronise hidden files that conflicted on each devices.
- We can search for conflicting docs.
- Pending processes can now be run at any time.
- Performance improved on synchronising large numbers of files at once.

17
updates_old.md Normal file
View File

@@ -0,0 +1,17 @@
### 0.13.0
- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded.
- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync.
- We can see the history of deleted files.
- `Pick file to show` was renamed to `Pick a file to show.
- Files in the `Pick a file to show` are now ordered by their modified date descent.
- Update information became to be shown on the major upgrade.
#### Minors
- 0.13.1 Fixed on conflict resolution.
- 0.13.2 Fixed file deletion failures.
- 0.13.4
- Now, we can synchronise hidden files that conflicted on each devices.
- We can search for conflicting docs.
- Pending processes can now be run at any time.
- Performance improved on synchronising large numbers of files at once.