mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-23 12:38:47 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486f1aa4a0 | ||
|
|
075c6beb68 | ||
|
|
d6121b0c1e | ||
|
|
3292a48054 | ||
|
|
ee37764040 | ||
|
|
b6f7fced22 | ||
|
|
13456c0854 | ||
|
|
2663a52fd7 | ||
|
|
d4bbf79514 | ||
|
|
5f96cc6b82 | ||
|
|
8c8f5d045f | ||
|
|
40cf8be890 | ||
|
|
6b03dbbe75 | ||
|
|
74425f75d2 | ||
|
|
ac7c622466 | ||
|
|
4b32365694 | ||
|
|
728edac283 | ||
|
|
ab9c0190bb | ||
|
|
5a7610d411 | ||
|
|
4691ae1463 | ||
|
|
0923ac3d85 | ||
|
|
ca100d6d9d |
@@ -92,9 +92,6 @@ After installing Self-hosted LiveSync on the device, select `Open setup URI` fro
|
||||
Answer the following.
|
||||
|
||||
- `Yes` to `Importing LiveSync's conf, OK?`
|
||||
- `No` to `Keep local DB?`
|
||||
- `Yes` to `Keep remote DB?`
|
||||
- `No` to `Rebuild the database?`
|
||||
- `Yes` to `Replicate once?`
|
||||
- `Set it up as secondary or subsequent device` to `How would you like to set it up?`.
|
||||
|
||||
Then, The configuration will now take effect and replication will start. Your files will be synchronised soon!
|
||||
@@ -85,13 +85,10 @@ All done! と表示されれば完了です。自動的に、`Copy setup URI`が
|
||||
クリップボードに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`
|
||||
- `Keep local DB?` に `No`
|
||||
- `Keep remote DB?` に `Yes`
|
||||
- `Rebuild the database?` に `No`
|
||||
- `Replicate once?` に `Yes`
|
||||
- `How would you like to set it up?` に `Set it up as secondary or subsequent device`
|
||||
|
||||
これで設定が反映され、レプリケーションが開始されます。
|
||||
@@ -32,16 +32,18 @@ max_age = 3600
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.15.2",
|
||||
"version": "0.16.0",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.15.2",
|
||||
"version": "0.16.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.15.2",
|
||||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
@@ -30,7 +30,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.15.4",
|
||||
"obsidian": "^0.16.3",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"rollup": "^2.32.1",
|
||||
@@ -2571,9 +2571,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
|
||||
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -2671,13 +2671,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "0.15.4",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.15.4.tgz",
|
||||
"integrity": "sha512-FE11CxxpVD6t/DBvjLvlT7q7YYW91ubTqPKIIp286LdnyLipS8Xi3Tif8i8ALPv87Vg9obKM43aWcPsYLxLllQ==",
|
||||
"version": "0.16.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.16.3.tgz",
|
||||
"integrity": "sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.3"
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -5490,9 +5490,9 @@
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
|
||||
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
@@ -5560,13 +5560,13 @@
|
||||
}
|
||||
},
|
||||
"obsidian": {
|
||||
"version": "0.15.4",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.15.4.tgz",
|
||||
"integrity": "sha512-FE11CxxpVD6t/DBvjLvlT7q7YYW91ubTqPKIIp286LdnyLipS8Xi3Tif8i8ALPv87Vg9obKM43aWcPsYLxLllQ==",
|
||||
"version": "0.16.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.16.3.tgz",
|
||||
"integrity": "sha512-hal9qk1A0GMhHSeLr2/+o3OpLmImiP+Y+sx2ewP13ds76KXsziG96n+IPFT0mSkup1zSwhEu+DeRhmbcyCCXWw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/codemirror": "0.0.108",
|
||||
"moment": "2.29.3"
|
||||
"moment": "2.29.4"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.15.2",
|
||||
"version": "0.16.0",
|
||||
"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",
|
||||
"type": "module",
|
||||
@@ -27,7 +27,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"obsidian": "^0.15.4",
|
||||
"obsidian": "^0.16.3",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-load-config": "^3.1.4",
|
||||
"rollup": "^2.32.1",
|
||||
|
||||
@@ -67,9 +67,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
element.removeClass("selected");
|
||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = false;
|
||||
});
|
||||
console.log(`.sls-setting-label.c-${screen}`)
|
||||
w.querySelectorAll(`.sls-setting-label.c-${screen}`).forEach((element) => {
|
||||
console.log(element)
|
||||
element.addClass("selected");
|
||||
(element.querySelector("input[type=radio]") as HTMLInputElement).checked = true;
|
||||
});
|
||||
@@ -164,6 +162,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);
|
||||
|
||||
@@ -361,6 +377,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
.onClick(async () => {
|
||||
await applyEncryption(true);
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Apply w/o rebuilding")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.setClass("sls-btn-right")
|
||||
.onClick(async () => {
|
||||
await applyEncryption(false);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -635,6 +661,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.markRemoteLocked();
|
||||
})
|
||||
);
|
||||
let rebuildRemote = false;
|
||||
|
||||
new Setting(containerRemoteDatabaseEl)
|
||||
.setName("")
|
||||
@@ -653,7 +680,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
} else {
|
||||
this.plugin.settings.customChunkSize = 100;
|
||||
}
|
||||
|
||||
rebuildRemote = false;
|
||||
changeDisplay("10")
|
||||
})
|
||||
);
|
||||
@@ -674,8 +701,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
} else {
|
||||
this.plugin.settings.customChunkSize = 100;
|
||||
}
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
rebuildRemote = true;
|
||||
changeDisplay("10")
|
||||
})
|
||||
);
|
||||
@@ -936,8 +962,17 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Monitor changes to internal files")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.watchInternalFileChanges).onChange(async (value) => {
|
||||
this.plugin.settings.watchInternalFileChanges = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Scan for hidden files before replication")
|
||||
.setDesc("This configuration will be ignored if monitoring changes is enabled.")
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.syncInternalFilesBeforeReplication).onChange(async (value) => {
|
||||
this.plugin.settings.syncInternalFilesBeforeReplication = value;
|
||||
@@ -946,7 +981,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Scan hidden files periodically")
|
||||
.setDesc("Seconds, 0 to disable.")
|
||||
.setDesc("Seconds, 0 to disable. This configuration will be ignored if monitoring changes is enabled.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.syncInternalFilesInterval + "")
|
||||
@@ -962,7 +997,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
});
|
||||
let skipPatternTextArea: TextAreaComponent = null;
|
||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$";
|
||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Skip patterns")
|
||||
.setDesc(
|
||||
@@ -981,7 +1016,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Skip patterns defaults")
|
||||
.setName("Restore the skip pattern to default")
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Default")
|
||||
.onClick(async () => {
|
||||
@@ -1200,8 +1235,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
// @ts-ignore
|
||||
this.plugin.app.setting.close()
|
||||
await this.plugin.resetLocalDatabase();
|
||||
await this.plugin.initializeDatabase(true)
|
||||
await this.plugin.initializeDatabase(true);
|
||||
if (rebuildRemote) {
|
||||
await this.plugin.markRemoteLocked();
|
||||
await this.plugin.tryResetRemoteDatabase();
|
||||
await this.plugin.markRemoteLocked();
|
||||
await this.plugin.markRemoteResolved();
|
||||
}
|
||||
await this.plugin.replicate(true);
|
||||
|
||||
Logger("All done! Please set up subsequent devices with 'Copy setup URI' and 'Open setup URI'.", LOG_LEVEL.NOTICE);
|
||||
// @ts-ignore
|
||||
this.plugin.app.commands.executeCommandById("obsidian-livesync:livesync-copysetupuri")
|
||||
|
||||
@@ -98,7 +98,7 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
|
||||
constructor(app: App, note: string, placeholder: string | null, getItemsFun: () => string[], callback: (e: string) => void) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.setPlaceholder(placeholder ?? "y/n) " + note);
|
||||
this.setPlaceholder((placeholder ?? "y/n) ") + note);
|
||||
if (getItemsFun) this.getItemsFun = getItemsFun;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2c39c15177...564da310e2
531
src/main.ts
531
src/main.ts
@@ -41,7 +41,7 @@ setNoticeClass(Notice);
|
||||
const ICHeader = "i:";
|
||||
const ICHeaderEnd = "i;";
|
||||
const ICHeaderLength = ICHeader.length;
|
||||
|
||||
const FileWatchEventQueueMax = 10;
|
||||
|
||||
/**
|
||||
* 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 getItemsFun = () => items;
|
||||
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();
|
||||
});
|
||||
};
|
||||
@@ -98,7 +98,7 @@ let touchedFiles: string[] = [];
|
||||
function touch(file: TFile | string) {
|
||||
const f = file instanceof TFile ? file : app.vault.getAbstractFileByPath(file) as TFile;
|
||||
const key = `${f.path}-${f.stat.mtime}-${f.stat.size}`;
|
||||
touchedFiles.push(key);
|
||||
touchedFiles.unshift(key);
|
||||
touchedFiles = touchedFiles.slice(0, 100);
|
||||
}
|
||||
function recentlyTouched(file: TFile) {
|
||||
@@ -109,6 +109,19 @@ function recentlyTouched(file: TFile) {
|
||||
function clearTouched() {
|
||||
touchedFiles = [];
|
||||
}
|
||||
|
||||
type CacheData = string | ArrayBuffer;
|
||||
type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL";
|
||||
type FileEventArgs = {
|
||||
file: TAbstractFile | InternalFileInfo;
|
||||
cache?: CacheData;
|
||||
oldPath?: string;
|
||||
ctx?: any;
|
||||
}
|
||||
type FileEventItem = {
|
||||
type: FileEventType,
|
||||
args: FileEventArgs
|
||||
}
|
||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
settings: ObsidianLiveSyncSettings;
|
||||
localDatabase: LocalPouchDB;
|
||||
@@ -118,6 +131,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
suspended: boolean;
|
||||
deviceAndVaultName: string;
|
||||
isMobile = false;
|
||||
isReady = false;
|
||||
|
||||
watchedFileEventQueue = [] as FileEventItem[];
|
||||
|
||||
getVaultName(): string {
|
||||
return this.app.vault.getName() + (this.settings?.additionalSuffixOfDatabaseName ? ("-" + this.settings.additionalSuffixOfDatabaseName) : "");
|
||||
@@ -277,28 +293,27 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.refreshStatusText = this.refreshStatusText.bind(this);
|
||||
|
||||
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.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||
this.watchVaultRename = this.watchVaultRename.bind(this);
|
||||
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
|
||||
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 1000, false);
|
||||
this.watchWindowVisibility = debounce(this.watchWindowVisibility.bind(this), 1000, false);
|
||||
this.watchOnline = debounce(this.watchOnline.bind(this), 500, false);
|
||||
|
||||
this.parseReplicationResult = this.parseReplicationResult.bind(this);
|
||||
|
||||
this.periodicSync = this.periodicSync.bind(this);
|
||||
this.setPeriodicSync = this.setPeriodicSync.bind(this);
|
||||
this.periodicSync = this.periodicSync.bind(this);
|
||||
this.loadQueuedFiles = this.loadQueuedFiles.bind(this);
|
||||
|
||||
this.getPluginList = this.getPluginList.bind(this);
|
||||
// this.registerWatchEvents();
|
||||
this.addSettingTab(new ObsidianLiveSyncSettingTab(this.app, this));
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
this.registerFileWatchEvents();
|
||||
if (this.localDatabase.isReady)
|
||||
try {
|
||||
if (this.isRedFlagRaised()) {
|
||||
@@ -340,11 +355,31 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||
this.addCommand({
|
||||
id: "livesync-copysetupuri",
|
||||
name: "Copy setup URI (beta)",
|
||||
name: "Copy setup URI",
|
||||
callback: async () => {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", "");
|
||||
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}`;
|
||||
await navigator.clipboard.writeText(uri);
|
||||
Logger("Setup URI copied to clipboard", LOG_LEVEL.NOTICE);
|
||||
@@ -352,9 +387,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
this.addCommand({
|
||||
id: "livesync-opensetupuri",
|
||||
name: "Open setup URI (beta)",
|
||||
name: "Open setup URI",
|
||||
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.startsWith(`${configURIBase}`)) {
|
||||
Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE);
|
||||
@@ -374,58 +409,91 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (newConf) {
|
||||
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
|
||||
if (result == "yes") {
|
||||
const newSettingW = Object.assign({}, this.settings, newConf);
|
||||
// stopping once.
|
||||
const newSettingW = Object.assign({}, DEFAULT_SETTINGS, newConf);
|
||||
this.localDatabase.closeReplication();
|
||||
this.settings.suspendFileWatching = true;
|
||||
console.dir(newSettingW);
|
||||
const keepLocalDB = await askYesNo(this.app, "Keep local DB?");
|
||||
const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?");
|
||||
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
|
||||
// nothing to do. so peaceful.
|
||||
const setupJustImport = "Just import setting";
|
||||
const setupAsNew = "Set it up as secondary or subsequent device";
|
||||
const setupAgain = "Reconfigure and reconstitute the data";
|
||||
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;
|
||||
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;
|
||||
} else if (setupType == setupAsNew) {
|
||||
this.settings = newSettingW;
|
||||
await this.saveSettings();
|
||||
await this.resetLocalOldDatabase();
|
||||
await this.resetLocalDatabase();
|
||||
await this.localDatabase.initializeDatabase();
|
||||
await this.markRemoteResolved();
|
||||
await this.replicate(true);
|
||||
} else if (setupType == setupAgain) {
|
||||
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 (await askSelectString(this.app, "Do you really want to do this?", ["Cancel", confirm]) != confirm) {
|
||||
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.saveSettings();
|
||||
await this.resetLocalOldDatabase();
|
||||
await this.resetLocalDatabase();
|
||||
await this.localDatabase.initializeDatabase();
|
||||
await this.initializeDatabase(true);
|
||||
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.markRemoteResolved();
|
||||
await this.replicate(true);
|
||||
|
||||
} else if (setupType == setupManually) {
|
||||
const keepLocalDB = await askYesNo(this.app, "Keep local DB?");
|
||||
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 +715,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
gcTimerHandler: any = null;
|
||||
|
||||
|
||||
registerWatchEvents() {
|
||||
registerFileWatchEvents() {
|
||||
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
|
||||
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
|
||||
this.registerEvent(this.app.vault.on("rename", this.watchVaultRename));
|
||||
this.registerEvent(this.app.vault.on("create", this.watchVaultCreate));
|
||||
//@ts-ignore : Internal API
|
||||
this.registerEvent(this.app.vault.on("raw", this.watchVaultRawEvents));
|
||||
}
|
||||
|
||||
registerWatchEvents() {
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
window.addEventListener("visibilitychange", this.watchWindowVisibility);
|
||||
window.addEventListener("online", this.watchOnline);
|
||||
@@ -675,6 +747,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
|
||||
async watchWindowVisibilityAsync() {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
// if (this.suspended) return;
|
||||
const isHidden = document.hidden;
|
||||
await this.applyBatchChange();
|
||||
@@ -699,12 +772,140 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// Cache file and waiting to can be proceed.
|
||||
async appendWatchEvent(type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string, ctx?: any) {
|
||||
// check really we can process.
|
||||
if (file instanceof TFile && !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", 30000, () => {
|
||||
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 key = `file-last-proc-${queue.type}-${file.path}`;
|
||||
const last = Number(await this.localDatabase.kvDB.get(key) || 0);
|
||||
if (file instanceof TFile && file.stat.mtime == last) {
|
||||
Logger(`File has been already scanned on ${queue.type}, skip: ${file.path}`, LOG_LEVEL.VERBOSE);
|
||||
continue;
|
||||
}
|
||||
|
||||
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") {
|
||||
if (file instanceof TFile) {
|
||||
await this.watchVaultRenameAsync(file, queue.args.oldPath);
|
||||
}
|
||||
}
|
||||
if (queue.type == "INTERNAL") {
|
||||
await this.watchVaultRawEventsAsync(file.path);
|
||||
}
|
||||
if (file instanceof TFile) {
|
||||
await this.localDatabase.kvDB.set(key, file.stat.mtime);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
this.watchWorkspaceOpenAsync(file);
|
||||
}
|
||||
|
||||
async watchWorkspaceOpenAsync(file: TFile) {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.isReady) return;
|
||||
await this.applyBatchChange();
|
||||
if (file == null) {
|
||||
return;
|
||||
@@ -715,102 +916,54 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
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() {
|
||||
if (!this.settings.batchSave || this.batchFileChange.length == 0) {
|
||||
return await this.procFileEvent(true);
|
||||
}
|
||||
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: string) {
|
||||
if (!this.settings.syncInternalFiles) return;
|
||||
if (!this.settings.watchInternalFileChanges) return;
|
||||
if (!path.startsWith(this.app.vault.configDir)) return;
|
||||
const ignorePatterns = this.settings.syncInternalFilesIgnorePatterns.toLocaleLowerCase()
|
||||
.replace(/\n| /g, "")
|
||||
.split(",").filter(e => e).map(e => new RegExp(e));
|
||||
if (ignorePatterns.some(e => path.match(e))) return;
|
||||
this.appendWatchEvent("INTERNAL", { path, mtime: 0, ctime: 0, size: 0 }, "", null);
|
||||
}
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
async watchVaultRawEventsAsync(path: string) {
|
||||
|
||||
const stat = await this.app.vault.adapter.stat(path);
|
||||
// sometimes folder is coming.
|
||||
if (stat && stat.type != "file") return;
|
||||
const storageMTime = ~~((stat && stat.mtime || 0) / 1000);
|
||||
const key = `${path}-${storageMTime}`;
|
||||
if (this.recentProcessedInternalFiles.contains(key)) {
|
||||
//If recently processed, it may caused by self.
|
||||
return;
|
||||
}
|
||||
return await runWithLock("batchSave", false, async () => {
|
||||
const batchItems = JSON.parse(JSON.stringify(this.batchFileChange)) as string[];
|
||||
this.batchFileChange = [];
|
||||
const semaphore = Semaphore(3);
|
||||
this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100);
|
||||
const id = filename2idInternalChunk(path);
|
||||
const filesOnDB = await this.localDatabase.getDBEntryMeta(id);
|
||||
const dbMTime = ~~((filesOnDB && filesOnDB.mtime || 0) / 1000);
|
||||
|
||||
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();
|
||||
// Skip unchanged file.
|
||||
if (dbMTime == storageMTime) {
|
||||
// Logger(`STORAGE --> DB:${path}: (hidden) Nothing changed`);
|
||||
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);
|
||||
// Do not compare timestamp. Always local data should be preferred except this plugin wrote one.
|
||||
if (storageMTime == 0) {
|
||||
await this.deleteInternalFileOnDatabase(path);
|
||||
} else {
|
||||
await this.storeInternalFileToDatabase({ path: path, ...stat });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
GetAllFilesRecursively(file: TAbstractFile): TFile[] {
|
||||
if (file instanceof TFile) {
|
||||
return [file];
|
||||
@@ -826,12 +979,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 {
|
||||
if (file instanceof TFolder) {
|
||||
if (file.isRoot()) return "";
|
||||
@@ -844,13 +991,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
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);
|
||||
try {
|
||||
await this.applyBatchChange();
|
||||
} catch (ex) {
|
||||
Logger(ex);
|
||||
}
|
||||
if (file instanceof TFolder) {
|
||||
const newFiles = this.GetAllFilesRecursively(file);
|
||||
// for guard edge cases. this won't happen and each file's event will be raise.
|
||||
@@ -871,7 +1013,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
} else if (file instanceof TFile) {
|
||||
try {
|
||||
Logger(`file save ${file.path} into db`);
|
||||
await this.updateIntoDB(file);
|
||||
await this.updateIntoDB(file, false, cache);
|
||||
Logger(`deleted ${oldFile} from db`);
|
||||
await this.deleteFromDBbyPath(oldFile);
|
||||
} catch (ex) {
|
||||
@@ -993,7 +1135,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1013,7 +1155,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
ctime: doc.ctime,
|
||||
mtime: doc.mtime,
|
||||
});
|
||||
this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
// this.batchFileChange = this.batchFileChange.filter((e) => e != newFile.path);
|
||||
Logger(msg + path);
|
||||
touch(newFile);
|
||||
this.app.vault.trigger("create", newFile);
|
||||
@@ -1027,7 +1169,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
|
||||
async deleteVaultItem(file: TFile | TFolder) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
if (file instanceof TFile) {
|
||||
if (!this.isTargetFile(file)) return;
|
||||
}
|
||||
const dir = file.parent;
|
||||
if (this.settings.trashInsteadDelete) {
|
||||
await this.app.vault.trash(file, false);
|
||||
@@ -1081,7 +1225,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.ensureDirectory(path);
|
||||
try {
|
||||
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);
|
||||
const xf = this.app.vault.getAbstractFileByPath(file.path) as TFile;
|
||||
touch(xf);
|
||||
@@ -1099,7 +1243,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
try {
|
||||
await this.app.vault.modify(file, doc.data, { ctime: doc.ctime, mtime: doc.mtime });
|
||||
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;
|
||||
touch(xf);
|
||||
this.app.vault.trigger("modify", xf);
|
||||
@@ -1146,6 +1290,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
);
|
||||
this.refreshStatusText();
|
||||
}
|
||||
async handleDBChangedAsync(change: EntryBody) {
|
||||
|
||||
@@ -1206,6 +1351,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.syncInternalFilesAndDatabase("pull", false, false, w);
|
||||
Logger(`Applying hidden ${w.length} files changed`);
|
||||
});
|
||||
this.refreshStatusText();
|
||||
}
|
||||
procInternalFile(filename: string) {
|
||||
this.procInternalFiles.push(filename);
|
||||
@@ -1455,7 +1601,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
this.statusBar.title = this.localDatabase.syncStatus;
|
||||
let waiting = "";
|
||||
if (this.settings.batchSave) {
|
||||
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
|
||||
waiting = " " + this.watchedFileEventQueue.map((e) => "🛫").join("");
|
||||
waiting = waiting.replace(/(🛫){10}/g, "🚀");
|
||||
}
|
||||
let queued = "";
|
||||
@@ -1512,6 +1658,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
updateStatusBarText() { }
|
||||
|
||||
async replicate(showMessage?: boolean) {
|
||||
if (!this.isReady) return;
|
||||
if (this.settings.versionUpFlash != "") {
|
||||
Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE);
|
||||
return;
|
||||
@@ -1521,24 +1668,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
await this.sweepPlugin(false);
|
||||
}
|
||||
await this.loadQueuedFiles();
|
||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication) {
|
||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesBeforeReplication && !this.settings.watchInternalFileChanges) {
|
||||
await this.syncInternalFilesAndDatabase("push", showMessage);
|
||||
}
|
||||
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
|
||||
}
|
||||
|
||||
async initializeDatabase(showingNotice?: boolean) {
|
||||
this.isReady = false;
|
||||
if (await this.openDatabase()) {
|
||||
if (this.localDatabase.isReady) {
|
||||
await this.syncAllFiles(showingNotice);
|
||||
}
|
||||
this.isReady = true;
|
||||
// run queued event once.
|
||||
await this.procFileEvent(true);
|
||||
return true;
|
||||
} else {
|
||||
this.isReady = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async replicateAllToServer(showingNotice?: boolean) {
|
||||
if (!this.isReady) return false;
|
||||
if (this.settings.autoSweepPlugins) {
|
||||
await this.sweepPlugin(showingNotice);
|
||||
}
|
||||
@@ -1627,9 +1780,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
});
|
||||
if (!initialScan) {
|
||||
await runAll("UPDATE STORAGE", onlyInDatabase, async (e) => {
|
||||
Logger(`Check or pull from db:${e}`);
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
Logger(`Check or pull from db:${e} OK`);
|
||||
const w = await this.localDatabase.getDBEntryMeta(e);
|
||||
if (w) {
|
||||
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 (it is normal behavior):${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!initialScan) {
|
||||
@@ -1701,23 +1859,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
|
||||
async getConflictedDoc(path: string, rev: string): Promise<false | diff_result_leaf> {
|
||||
try {
|
||||
@@ -1974,20 +2115,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 (shouldBeIgnored(file.path)) {
|
||||
return;
|
||||
}
|
||||
let content = "";
|
||||
let datatype: "plain" | "newnote" = "newnote";
|
||||
if (!isPlainText(file.name)) {
|
||||
const contentBin = await this.app.vault.readBinary(file);
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
datatype = "newnote";
|
||||
if (!cache) {
|
||||
if (!isPlainText(file.name)) {
|
||||
const contentBin = await this.app.vault.readBinary(file);
|
||||
content = await arrayBufferToBase64(contentBin);
|
||||
datatype = "newnote";
|
||||
} else {
|
||||
content = await this.app.vault.read(file);
|
||||
datatype = "plain";
|
||||
}
|
||||
} else {
|
||||
content = await this.app.vault.read(file);
|
||||
datatype = "plain";
|
||||
if (cache instanceof ArrayBuffer) {
|
||||
content = await arrayBufferToBase64(cache);
|
||||
datatype = "newnote"
|
||||
} else {
|
||||
content = cache;
|
||||
datatype = "plain";
|
||||
}
|
||||
}
|
||||
const fullPath = path2id(file.path);
|
||||
const d: LoadedEntry = {
|
||||
@@ -2239,7 +2390,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||
if (this.periodicInternalFileScanHandler != null) {
|
||||
this.clearInternalFileScan();
|
||||
}
|
||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0) {
|
||||
if (this.settings.syncInternalFiles && this.settings.syncInternalFilesInterval > 0 && !this.settings.watchInternalFileChanges) {
|
||||
this.periodicPluginSweepHandler = this.setInterval(async () => await this.periodicInternalFileScan(), this.settings.syncInternalFilesInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
59
updates.md
59
updates.md
@@ -1,3 +1,8 @@
|
||||
### 0.16.0
|
||||
- Now hidden files need not be scanned. Changes will be detected automatically.
|
||||
- If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`.
|
||||
- Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once.
|
||||
|
||||
### 0.15.0
|
||||
- Outdated configuration items have been removed.
|
||||
- Setup wizard has been implemented!
|
||||
@@ -6,46 +11,16 @@ I appreciate for reviewing and giving me advice @Pouhon158!
|
||||
|
||||
#### Minors
|
||||
- 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.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.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices.
|
||||
- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests.
|
||||
- 0.15.10 Fixed:
|
||||
- The boot sequence has been corrected and now boots smoothly.
|
||||
- Auto applying of batch save will be processed earlier than before.
|
||||
|
||||
### 0.14.1
|
||||
- The target selecting filter was implemented.
|
||||
Now we can set what files are synchronised by regular expression.
|
||||
- We can configure the size of chunks.
|
||||
We can use larger chunks to improve performance.
|
||||
(This feature can not be used with IBM Cloudant)
|
||||
- Read chunks online.
|
||||
Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication.
|
||||
- Added this note.
|
||||
- Use local chunks in preference to remote them if present,
|
||||
|
||||
#### Recommended configuration for Self-hosted CouchDB
|
||||
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
||||
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
||||
- Be sure to `Read chunks online` checked.
|
||||
|
||||
#### Minors
|
||||
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
||||
- 0.14.3 New test items have been added to `Check database configuration`.
|
||||
- 0.14.4 Fixed issue of importing configurations.
|
||||
- 0.14.5 Auto chunk size adjusting implemented.
|
||||
- 0.14.6 Change Target to ES2018
|
||||
- 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.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.
|
||||
... To continue on to `updates_old.md`.
|
||||
42
updates_old.md
Normal file
42
updates_old.md
Normal file
@@ -0,0 +1,42 @@
|
||||
### 0.14.1
|
||||
- The target selecting filter was implemented.
|
||||
Now we can set what files are synchronised by regular expression.
|
||||
- We can configure the size of chunks.
|
||||
We can use larger chunks to improve performance.
|
||||
(This feature can not be used with IBM Cloudant)
|
||||
- Read chunks online.
|
||||
Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication.
|
||||
- Added this note.
|
||||
- Use local chunks in preference to remote them if present,
|
||||
|
||||
#### Recommended configuration for Self-hosted CouchDB
|
||||
- Set chunk size to around 100 to 250 (10MB - 25MB per chunk)
|
||||
- *Set batch size to 100 and batch limit to 20 (0.14.2)*
|
||||
- Be sure to `Read chunks online` checked.
|
||||
|
||||
#### Minors
|
||||
- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed
|
||||
- 0.14.3 New test items have been added to `Check database configuration`.
|
||||
- 0.14.4 Fixed issue of importing configurations.
|
||||
- 0.14.5 Auto chunk size adjusting implemented.
|
||||
- 0.14.6 Change Target to ES2018
|
||||
- 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.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.
|
||||
Reference in New Issue
Block a user