mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-11 18:21:50 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124a49b80f | ||
|
|
3e76292aa7 | ||
|
|
4634ab73b1 | ||
|
|
359c10f1d7 | ||
|
|
59ebac3efc | ||
|
|
b4edca3a99 | ||
|
|
4b76b10a6f | ||
|
|
d4b53280e3 | ||
|
|
dbd9b17b20 |
123
README.md
123
README.md
@@ -1,32 +1,25 @@
|
|||||||
# Self-hosted LiveSync
|
# Self-hosted LiveSync
|
||||||
|
|
||||||
Sorry for late! [Japanese docs](./README_ja.md) is also coming up.
|
[Japanese docs](./README_ja.md).
|
||||||
|
|
||||||
**Renamed from: obsidian-livesync**
|
Self-hosted LiveSync is a community implemented synchronization plugin.
|
||||||
|
It uses Self-hosted or you procured CouchDB as the server. Available on every obsidian installed devices.
|
||||||
Using a self-hosted database, live-sync to multi-devices bidirectionally.
|
Note: It has no compatibilities with official "Sync".
|
||||||
Runs in Mac, Android, Windows, and iOS. Perhaps available on Linux too.
|
|
||||||
Community implementation, not compatible with official "Sync".
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**It's getting almost stable now, But Please make sure to back your vault up!**
|
If you install or upgrade LiveSync, please back your vault up.
|
||||||
|
|
||||||
Limitations: ~~Folder deletion handling is not completed.~~ **It would work now.**
|
## Features
|
||||||
|
|
||||||
## This plugin enables...
|
- Visual conflict resolver included.
|
||||||
|
- Synchronize with other devices bidirectionally near-real-time
|
||||||
- Runs in Windows, Mac, iPad, iPhone, Android, Chromebook
|
- You can use CouchDB or its compatibles like IBM Cloudant.
|
||||||
- Synchronize to Self-hosted Database
|
- End-to-End encryption.
|
||||||
- Replicate to/from other devices bidirectionally near-real-time
|
- Plugin synchronization(Beta)
|
||||||
- Resolving synchronizing conflicts in the Obsidian.
|
|
||||||
- You can use CouchDB or its compatibles like IBM Cloudant. CouchDB is OSS, and IBM Cloudant has the terms and certificates about security. Your notes are yours.
|
|
||||||
- Off-line sync is also available.
|
|
||||||
- End-to-End encryption is available (beta).
|
|
||||||
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) (End-to-End encryption will not be applicable.)
|
- Receive WebClip from [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) (End-to-End encryption will not be applicable.)
|
||||||
|
|
||||||
It must be useful for the Researcher, Engineer, Developer who has to keep NDA or something like agreement.
|
It must be useful for the Researcher, Engineer, Developer who has to keep NDA or something like agreement. Especially, in some companies, people have to store all data to their fully controlled host, even End-to-End encryption applied.
|
||||||
Especially, in some companies, people have to store all data to their fully controlled host, even End-to-End encryption applied.
|
|
||||||
|
|
||||||
## IMPORTANT NOTICE
|
## IMPORTANT NOTICE
|
||||||
|
|
||||||
@@ -36,35 +29,68 @@ Especially, in some companies, people have to store all data to their fully cont
|
|||||||
- When the device's storage has been run out, Database corruption may happen.
|
- When the device's storage has been run out, Database corruption may happen.
|
||||||
- When editing hidden files or any other invisible files from obsidian, the file wouldn't be kept in the database. (**Or be deleted.**)
|
- When editing hidden files or any other invisible files from obsidian, the file wouldn't be kept in the database. (**Or be deleted.**)
|
||||||
|
|
||||||
## Supplements
|
|
||||||
|
|
||||||
- When the file has been deleted, the deletion of the file is replicated to other devices.
|
|
||||||
- When the folder became empty by replication, The folder will be deleted in the default setting. But you can change this behaivour. Check the [Settings](docs/settings.md).
|
|
||||||
- LiveSync drains many batteries in mobile devices.
|
|
||||||
- Mobile Obsidian can not connect to the non-secure(HTTP) or local CA-signed servers, even though the certificate is stored in the device store.
|
|
||||||
- There are no 'exclude_folders' like configurations.
|
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
1. Install from Obsidian, or download from this repo's releases, copy `main.js`, `styles.css` and `manifest.json` into `[your-vault]/.obsidian/plugins/`
|
### Get your database ready.
|
||||||
2. Get your database. IBM Cloudant is preferred for testing. Or you can use your own server with CouchDB.
|
|
||||||
For more information, refer below:
|
First, get your database ready. IBM Cloudant is preferred for testing. Or you can use your own server with CouchDB. For more information, refer below:
|
||||||
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
||||||
2. [Setup your CouchDB](docs/setup_own_server.md)
|
2. [Setup your CouchDB](docs/setup_own_server.md)
|
||||||
3. Enter connection information to Plugin's setting dialog. In details, refer [Settings of Self-hosted LiveSync](docs/settings.md)
|
|
||||||
4. Enable LiveSync or other Synchronize method as you like.
|
### First device
|
||||||
|
|
||||||
|
1. Install the plugin on your device.
|
||||||
|
2. Configure with the remote database.
|
||||||
|
1. Fill your server's information into the `Remote Database configuration` pane.
|
||||||
|
2. Enabling `End to End Encryption` is recommended. After inputting the passphrase, you have to press `Just apply`.
|
||||||
|
3. Hit `Test Database Connection` and make sure that the plugin says `Connected`.
|
||||||
|
4. Hit `Check database configuration` and make sure all tests have been passed.
|
||||||
|
3. Configure how to synchronize on `Sync setting`. (You can leave these configures later)
|
||||||
|
1. If you want to synchronize in real-time, enable `LiveSync`.
|
||||||
|
2. Or, set up the synchronization as you like.
|
||||||
|
3. Additional configuration is also here. I recommend enabling `Use Trash for deleted files, but you can leave all configurations disabled.
|
||||||
|
4. Configure miscellaneous features.
|
||||||
|
1. Enabling `Show staus inside editor` bring you information. While edit mode, you can see the status on the top-right of the editor. (Recommended)
|
||||||
|
2. Enabling `Use history` let you see the diffs between your edit and synchronization. (Recommended)
|
||||||
|
5. Back to the editor. I hope that initial scan is in the progress or done.
|
||||||
|
6. When status became stabilized (All ⏳ and 🧩 have been disappeared), you are ready to synchronize with the server.
|
||||||
|
7. Press the replicate icon on the Ribbon or run `Replicate now` from the Command pallet. You'll send all your data to the server.
|
||||||
|
8. Open the command palette, `Copy setup URI`, and set the passphrase to encrypt the information. Then your configuration will be copied to the clipboard. Please share copied URI with your other devices.
|
||||||
|
**IMPORTANT NOTICE: BE CAREFUL TO TREAT THIS URI. THE URI CONTAINS YOUR CREDENTIALS EVEN THOUGH NOBODY COULD READ WITHOUT THE PASSPHRASE.**
|
||||||
|
|
||||||
|
### Subsequent Devices
|
||||||
|
|
||||||
|
Strongly recommend using the vault in which all files are completely synchronized including timestamps. Otherwise, some files will be corrupted if failed to resolve conflicts. To simplify, I recommend using a new empty vault.
|
||||||
|
|
||||||
|
1. Install the plug-in.
|
||||||
|
2. Open the link that you had been copied to the other device.
|
||||||
|
3. The plug-in asks you that are you sure to apply the configurations. Please answer `Yes` and the following instruction below:
|
||||||
|
1. Answer `Yes` to `Keep local DB?`.
|
||||||
|
*Note: If you started with existed vault, you have to answer `No`. And `No` to `Rebuild the database?`.*
|
||||||
|
2. Answer `Yes` to `Keep remote DB?`.
|
||||||
|
3. Answer `Yes` to `Replicate once?`.
|
||||||
|
Yes, you have to answer `Yes` to everything.
|
||||||
|
Then, all your settings are copied from the first device.
|
||||||
|
4. Your notes will arrive soon.
|
||||||
|
|
||||||
|
## Something looks corrupted...
|
||||||
|
|
||||||
|
Please open the link again and Answer as below:
|
||||||
|
- If your local database looks corrupted
|
||||||
|
(in other words, when your Obsidian getting weird even standalone.)
|
||||||
|
- Answer `No` to `Keep local DB?`
|
||||||
|
- If your remote database looks corrupted
|
||||||
|
(in other words, when something happens while replicating)
|
||||||
|
- Answer `No` to `Keep remote DB?`
|
||||||
|
|
||||||
|
If you answered `No` to both, your databases will be rebuilt by the content on your device. And the remote database will lock out other devices. You have to synchronize all your devices again. (When this time, almost all your files should be synchronized including a timestamp. So you can use the existed vault).
|
||||||
|
|
||||||
## Test Server
|
## Test Server
|
||||||
|
|
||||||
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of self-hosted-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
Setting up an instance of Cloudant or local CouchDB is a little complicated, so I made the [Tasting server of self-hosted-livesync](https://olstaste.vrtmrz.net/) up. Try free!
|
||||||
Note: Please read "Limitations" carefully. Do not send your private vault.
|
Note: Please read "Limitations" carefully. Do not send your private vault.
|
||||||
|
|
||||||
## WebClipper is also available.
|
## Information in StatusBar
|
||||||
|
|
||||||
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
|
||||||
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
|
||||||
|
|
||||||
# Information in StatusBar
|
|
||||||
|
|
||||||
Synchronization status is shown in statusbar.
|
Synchronization status is shown in statusbar.
|
||||||
|
|
||||||
@@ -75,11 +101,16 @@ Synchronization status is shown in statusbar.
|
|||||||
- ⚠ Error occurred.
|
- ⚠ Error occurred.
|
||||||
- ↑ Uploaded pieces
|
- ↑ Uploaded pieces
|
||||||
- ↓ Downloaded pieces
|
- ↓ Downloaded pieces
|
||||||
- ⏳ Count of the pending process
|
- ⏳ Number of the pending processes
|
||||||
If you have deleted or renamed files, please wait until this disappears.
|
- 🧩 Number of the files that waiting for their chunks.
|
||||||
|
If you have deleted or renamed files, please wait until ⏳ disappeared.
|
||||||
|
|
||||||
# More supplements
|
|
||||||
|
|
||||||
|
## Hints
|
||||||
|
- When the folder became empty by replication, The folder will be deleted in the default setting. But you can change this behaivour. Check the [Settings](docs/settings.md).
|
||||||
|
- LiveSync mode drains many batteries in mobile devices. Periodic sync and some automatic sync is recommended.
|
||||||
|
- Mobile Obsidian can not connect to the non-secure(HTTP) or local CA-signed servers, even though the certificate is stored in the device store.
|
||||||
|
- There are no 'exclude_folders' like configurations.
|
||||||
- When synchronized, files are compared by their modified times and overwritten by the newer ones once. Then plugin checks the conflicts and if a merge is needed, the dialog will open.
|
- When synchronized, files are compared by their modified times and overwritten by the newer ones once. Then plugin checks the conflicts and if a merge is needed, the dialog will open.
|
||||||
- Rarely, the file in the database would be broken. The plugin will not write storage when it looks broken, so some old files must be on your device. If you edit the file, it will be cured. But if the file does not exist on any device, can not rescue it. So you can delete these items from the setting dialog.
|
- Rarely, the file in the database would be broken. The plugin will not write storage when it looks broken, so some old files must be on your device. If you edit the file, it will be cured. But if the file does not exist on any device, can not rescue it. So you can delete these items from the setting dialog.
|
||||||
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
|
||||||
@@ -87,7 +118,11 @@ If you have deleted or renamed files, please wait until this disappears.
|
|||||||
- Q: Database is growing, how can I shrink it up?
|
- Q: Database is growing, how can I shrink it up?
|
||||||
A: each of the docs is saved with their old 100 revisions to detect and resolve confliction. Picture yourself that one device has been off the line for a while, and joined again. The device has to check his note and remote saved note. If exists in revision histories of remote notes even though the device's note is a little different from the latest one, it could be merged safely. Even if that is not in revision histories, we only have to check differences after the revision that both devices commonly have. This is like The git's conflict resolving method. So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem.
|
A: each of the docs is saved with their old 100 revisions to detect and resolve confliction. Picture yourself that one device has been off the line for a while, and joined again. The device has to check his note and remote saved note. If exists in revision histories of remote notes even though the device's note is a little different from the latest one, it could be merged safely. Even if that is not in revision histories, we only have to check differences after the revision that both devices commonly have. This is like The git's conflict resolving method. So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem.
|
||||||
- And more technical Information are in the [Technical Information](docs/tech_info.md)
|
- And more technical Information are in the [Technical Information](docs/tech_info.md)
|
||||||
|
- If you want to synchronize files without obsidian, you can use [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync).
|
||||||
|
- WebClipper is also available.
|
||||||
|
Available from on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||||
|
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
|
||||||
|
|
||||||
# License
|
## License
|
||||||
|
|
||||||
The source code is licensed MIT.
|
The source code is licensed MIT.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.11.2",
|
"version": "0.11.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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.11.2",
|
"version": "0.11.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.11.2",
|
"version": "0.11.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.11.2",
|
"version": "0.11.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",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"esbuild": "0.13.12",
|
"esbuild": "0.13.12",
|
||||||
"esbuild-svelte": "^0.6.0",
|
"esbuild-svelte": "^0.6.0",
|
||||||
"idb": "^7.0.1",
|
"idb": "^7.0.1",
|
||||||
"svelte-preprocess": "^4.10.2",
|
"svelte-preprocess": "^4.10.2",
|
||||||
"xxhash-wasm": "^0.4.2"
|
"xxhash-wasm": "^0.4.2"
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/KeyValueDB.ts
Normal file
50
src/KeyValueDB.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { deleteDB, IDBPDatabase, openDB } from "idb";
|
||||||
|
export interface KeyValueDatabase {
|
||||||
|
get<T>(key: string): Promise<T>;
|
||||||
|
set<T>(key: string, value: T): Promise<IDBValidKey>;
|
||||||
|
del(key: string): Promise<void>;
|
||||||
|
clear(): Promise<void>;
|
||||||
|
keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise<IDBValidKey[]>;
|
||||||
|
close(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||||
|
export const OpenKeyValueDatabase = (dbKey: string): KeyValueDatabase => {
|
||||||
|
if (dbKey in databaseCache) {
|
||||||
|
databaseCache[dbKey].close();
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
}
|
||||||
|
const storeKey = dbKey;
|
||||||
|
const dbPromise = openDB(dbKey, 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
db.createObjectStore(storeKey);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
~(async () => (databaseCache[dbKey] = await dbPromise))();
|
||||||
|
return {
|
||||||
|
async get<T>(key: string): Promise<T> {
|
||||||
|
return (await dbPromise).get(storeKey, key);
|
||||||
|
},
|
||||||
|
async set<T>(key: string, value: T) {
|
||||||
|
return (await dbPromise).put(storeKey, value, key);
|
||||||
|
},
|
||||||
|
async del(key: string) {
|
||||||
|
return (await dbPromise).delete(storeKey, key);
|
||||||
|
},
|
||||||
|
async clear() {
|
||||||
|
return (await dbPromise).clear(storeKey);
|
||||||
|
},
|
||||||
|
async keys(query?: IDBValidKey | IDBKeyRange, count?: number) {
|
||||||
|
return (await dbPromise).getAllKeys(storeKey, query, count);
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
return (await dbPromise).close();
|
||||||
|
},
|
||||||
|
async destroy() {
|
||||||
|
delete databaseCache[dbKey];
|
||||||
|
(await dbPromise).close();
|
||||||
|
await deleteDB(dbKey);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
VER,
|
VER,
|
||||||
MILSTONE_DOCID,
|
MILSTONE_DOCID,
|
||||||
DatabaseConnectingStatus,
|
DatabaseConnectingStatus,
|
||||||
|
ObsidianLiveSyncSettings,
|
||||||
} from "./lib/src/types";
|
} from "./lib/src/types";
|
||||||
import { RemoteDBSettings } from "./lib/src/types";
|
import { RemoteDBSettings } from "./lib/src/types";
|
||||||
import { resolveWithIgnoreKnownError, delay, runWithLock, NewNotice, WrappedNotice, shouldSplitAsPlainText, splitPieces2, enableEncryption } from "./lib/src/utils";
|
import { resolveWithIgnoreKnownError, delay, runWithLock, NewNotice, WrappedNotice, shouldSplitAsPlainText, splitPieces2, enableEncryption } from "./lib/src/utils";
|
||||||
@@ -26,13 +27,14 @@ import { path2id } from "./utils";
|
|||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { checkRemoteVersion, connectRemoteCouchDBWithSetting, getLastPostFailedBySize } from "./utils_couchdb";
|
import { checkRemoteVersion, connectRemoteCouchDBWithSetting, getLastPostFailedBySize } from "./utils_couchdb";
|
||||||
import { openDB, deleteDB, IDBPDatabase } from "idb";
|
import { openDB, deleteDB, IDBPDatabase } from "idb";
|
||||||
|
import { KeyValueDatabase, OpenKeyValueDatabase } from "./KeyValueDB";
|
||||||
|
|
||||||
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
|
type ReplicationCallback = (e: PouchDB.Core.ExistingDocument<EntryDoc>[]) => Promise<void>;
|
||||||
class LRUCache {
|
class LRUCache {
|
||||||
cache = new Map<string, string>([]);
|
cache = new Map<string, string>([]);
|
||||||
revCache = new Map<string, string>([]);
|
revCache = new Map<string, string>([]);
|
||||||
maxCache = 100;
|
maxCache = 100;
|
||||||
constructor() {}
|
constructor() { }
|
||||||
get(key: string) {
|
get(key: string) {
|
||||||
// debugger
|
// debugger
|
||||||
const v = this.cache.get(key);
|
const v = this.cache.get(key);
|
||||||
@@ -75,6 +77,7 @@ export class LocalPouchDB {
|
|||||||
dbname: string;
|
dbname: string;
|
||||||
settings: RemoteDBSettings;
|
settings: RemoteDBSettings;
|
||||||
localDatabase: PouchDB.Database<EntryDoc>;
|
localDatabase: PouchDB.Database<EntryDoc>;
|
||||||
|
kvDB: KeyValueDatabase;
|
||||||
nodeid = "";
|
nodeid = "";
|
||||||
isReady = false;
|
isReady = false;
|
||||||
|
|
||||||
@@ -115,6 +118,7 @@ export class LocalPouchDB {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
onunload() {
|
onunload() {
|
||||||
|
this.kvDB.close();
|
||||||
this.recentModifiedDocs = [];
|
this.recentModifiedDocs = [];
|
||||||
this.leafArrivedCallbacks;
|
this.leafArrivedCallbacks;
|
||||||
this.changeHandler = this.cancelHandler(this.changeHandler);
|
this.changeHandler = this.cancelHandler(this.changeHandler);
|
||||||
@@ -139,6 +143,7 @@ export class LocalPouchDB {
|
|||||||
if (this.localDatabase != null) {
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.close();
|
this.localDatabase.close();
|
||||||
}
|
}
|
||||||
|
this.kvDB.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
async isOldDatabaseExists() {
|
async isOldDatabaseExists() {
|
||||||
@@ -167,6 +172,7 @@ export class LocalPouchDB {
|
|||||||
revs_limit: 100,
|
revs_limit: 100,
|
||||||
deterministic_revs: true,
|
deterministic_revs: true,
|
||||||
});
|
});
|
||||||
|
this.kvDB = OpenKeyValueDatabase(this.dbname + "-livesync-kv");
|
||||||
Logger("Database info", LOG_LEVEL.VERBOSE);
|
Logger("Database info", LOG_LEVEL.VERBOSE);
|
||||||
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
|
Logger(await this.localDatabase.info(), LOG_LEVEL.VERBOSE);
|
||||||
Logger("Open Database...");
|
Logger("Open Database...");
|
||||||
@@ -765,7 +771,7 @@ export class LocalPouchDB {
|
|||||||
this.openOneshotReplication(
|
this.openOneshotReplication(
|
||||||
setting,
|
setting,
|
||||||
showingNotice,
|
showingNotice,
|
||||||
async (e) => {},
|
async (e) => { },
|
||||||
false,
|
false,
|
||||||
(e) => {
|
(e) => {
|
||||||
if (e === true) res(e);
|
if (e === true) res(e);
|
||||||
@@ -1108,6 +1114,7 @@ export class LocalPouchDB {
|
|||||||
Logger("Database closed for reset Database.");
|
Logger("Database closed for reset Database.");
|
||||||
this.isReady = false;
|
this.isReady = false;
|
||||||
await this.localDatabase.destroy();
|
await this.localDatabase.destroy();
|
||||||
|
await this.kvDB.destroy();
|
||||||
this.localDatabase = null;
|
this.localDatabase = null;
|
||||||
await this.initializeDatabase();
|
await this.initializeDatabase();
|
||||||
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
|
Logger("Local Database Reset", LOG_LEVEL.NOTICE);
|
||||||
@@ -1216,10 +1223,14 @@ export class LocalPouchDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async garbageCollect() {
|
async garbageCollect() {
|
||||||
// if (this.settings.useHistory) {
|
if (this.settings.useHistory) {
|
||||||
// Logger("GC skipped for using history", LOG_LEVEL.VERBOSE);
|
Logger("GC skipped for using history", LOG_LEVEL.VERBOSE);
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
if ((this.settings as ObsidianLiveSyncSettings).liveSync) {
|
||||||
|
Logger("GC skipped while live sync.", LOG_LEVEL.VERBOSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// NOTE:Garbage collection could break old revisions.
|
// NOTE:Garbage collection could break old revisions.
|
||||||
await runWithLock("replicate", true, async () => {
|
await runWithLock("replicate", true, async () => {
|
||||||
if (this.gcRunning) return;
|
if (this.gcRunning) return;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { path2id, id2path } from "./utils";
|
|||||||
import { NewNotice, runWithLock } from "./lib/src/utils";
|
import { NewNotice, runWithLock } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
import { checkSyncInfo, connectRemoteCouchDBWithSetting } from "./utils_couchdb";
|
||||||
import { testCrypt } from "./lib/src/e2ee";
|
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
@@ -188,7 +188,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
new Setting(containerRemoteDatabaseEl)
|
new Setting(containerRemoteDatabaseEl)
|
||||||
.setName("End to End Encryption")
|
.setName("End to End Encryption")
|
||||||
.setDesc("Encrypting contents on the database.")
|
.setDesc("Encrypt contents on the remote database. If you use the plugins synchronizing feature, enabling this is recommend.")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.workingEncrypt).onChange(async (value) => {
|
||||||
this.plugin.settings.workingEncrypt = value;
|
this.plugin.settings.workingEncrypt = value;
|
||||||
@@ -710,15 +710,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
// new Setting(containerSyncSettingEl)
|
||||||
.setName("Skip old files on sync")
|
// .setName("Skip old files on sync")
|
||||||
.setDesc("Skip old incoming if incoming changes older than storage.")
|
// .setDesc("Skip old incoming if incoming changes older than storage.")
|
||||||
.addToggle((toggle) =>
|
// .addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.skipOlderFilesOnSync).onChange(async (value) => {
|
// toggle.setValue(this.plugin.settings.skipOlderFilesOnSync).onChange(async (value) => {
|
||||||
this.plugin.settings.skipOlderFilesOnSync = value;
|
// this.plugin.settings.skipOlderFilesOnSync = value;
|
||||||
await this.plugin.saveSettings();
|
// await this.plugin.saveSettings();
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Check conflict only on opening file.")
|
.setName("Check conflict only on opening file.")
|
||||||
.setDesc("Do not check conflict while replication")
|
.setDesc("Do not check conflict while replication")
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 6451afd112...4decf16d62
209
src/main.ts
209
src/main.ts
@@ -1,4 +1,4 @@
|
|||||||
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal } from "obsidian";
|
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal, Setting } from "obsidian";
|
||||||
import { diff_match_patch } from "diff-match-patch";
|
import { diff_match_patch } from "diff-match-patch";
|
||||||
|
|
||||||
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types";
|
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types";
|
||||||
@@ -26,9 +26,11 @@ import { ConflictResolveModal } from "./ConflictResolveModal";
|
|||||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
import PluginPane from "./PluginPane.svelte";
|
import PluginPane from "./PluginPane.svelte";
|
||||||
import { id2path, path2id } from "./utils";
|
import { id2path, path2id } from "./utils";
|
||||||
import { decrypt, encrypt } from "./lib/src/e2ee";
|
import { decrypt, encrypt } from "./lib/src/e2ee_v2";
|
||||||
|
|
||||||
const isDebug = false;
|
const isDebug = false;
|
||||||
setNoticeClass(Notice);
|
setNoticeClass(Notice);
|
||||||
class PluginDialogModal extends Modal {
|
class PluginDialogModal extends Modal {
|
||||||
@@ -58,9 +60,65 @@ class PluginDialogModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InputStringDialog extends Modal {
|
||||||
|
result: string | false = false;
|
||||||
|
onSubmit: (result: string | boolean) => void;
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
placeholder: string;
|
||||||
|
isManuallyClosed = false;
|
||||||
|
|
||||||
|
constructor(app: App, title: string, key: string, placeholder: string, onSubmit: (result: string | false) => void) {
|
||||||
|
super(app);
|
||||||
|
this.onSubmit = onSubmit;
|
||||||
|
this.title = title;
|
||||||
|
this.placeholder = placeholder;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
|
||||||
|
contentEl.createEl("h1", { text: this.title });
|
||||||
|
|
||||||
|
new Setting(contentEl).setName(this.key).addText((text) =>
|
||||||
|
text.onChange((value) => {
|
||||||
|
this.result = value;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(contentEl).addButton((btn) =>
|
||||||
|
btn
|
||||||
|
.setButtonText("Ok")
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => {
|
||||||
|
this.isManuallyClosed = true;
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
).addButton((btn) =>
|
||||||
|
btn
|
||||||
|
.setButtonText("Cancel")
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => {
|
||||||
|
this.close();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
if (this.isManuallyClosed) {
|
||||||
|
this.onSubmit(this.result);
|
||||||
|
} else {
|
||||||
|
this.onSubmit(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
class PopoverYesNo extends FuzzySuggestModal<string> {
|
class PopoverYesNo extends FuzzySuggestModal<string> {
|
||||||
app: App;
|
app: App;
|
||||||
callback: (e: string) => void = () => {};
|
callback: (e: string) => void = () => { };
|
||||||
|
|
||||||
constructor(app: App, note: string, callback: (e: string) => void) {
|
constructor(app: App, note: string, callback: (e: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
@@ -98,6 +156,13 @@ const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const askString = (app: App, title: string, key: string, placeholder: string): Promise<string | false> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
const dialog = new InputStringDialog(app, title, key, placeholder, (result) => res(result));
|
||||||
|
dialog.open();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default class ObsidianLiveSyncPlugin extends Plugin {
|
export default class ObsidianLiveSyncPlugin extends Plugin {
|
||||||
settings: ObsidianLiveSyncSettings;
|
settings: ObsidianLiveSyncSettings;
|
||||||
localDatabase: LocalPouchDB;
|
localDatabase: LocalPouchDB;
|
||||||
@@ -240,20 +305,40 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(ex, LOG_LEVEL.VERBOSE);
|
Logger(ex, LOG_LEVEL.VERBOSE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const configURIBase = "obsidian://setuplivesync?settings=";
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-exportconfig",
|
id: "livesync-copysetupuri",
|
||||||
name: "Copy setup uri (beta)",
|
name: "Copy setup URI (beta)",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), "---"));
|
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "Passphrase", "");
|
||||||
const uri = `obsidian://setuplivesync?settings=${encryptedSetting}`;
|
if (encryptingPassphrase === false) return;
|
||||||
|
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), encryptingPassphrase));
|
||||||
|
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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
this.addCommand({
|
||||||
|
id: "livesync-opensetupuri",
|
||||||
|
name: "Open setup URI (beta)",
|
||||||
|
callback: async () => {
|
||||||
|
const setupURI = await askString(this.app, "Set up manually", "Set up URI", `${configURIBase}aaaaa`);
|
||||||
|
if (setupURI === false) return;
|
||||||
|
if (!setupURI.startsWith(`${configURIBase}`)) {
|
||||||
|
Logger("Set up URI looks wrong.", LOG_LEVEL.NOTICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = decodeURIComponent(setupURI.substring(configURIBase.length));
|
||||||
|
console.dir(config)
|
||||||
|
await setupwizard(config);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const setupwizard = async (confString: string) => {
|
||||||
try {
|
try {
|
||||||
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
const oldConf = JSON.parse(JSON.stringify(this.settings));
|
||||||
const newconf = await JSON.parse(await decrypt(conf.settings, "---"));
|
const encryptingPassphrase = await askString(this.app, "Passphrase", "Passphrase for your settings", "");
|
||||||
|
if (encryptingPassphrase === false) return;
|
||||||
|
const newconf = await JSON.parse(await decrypt(confString, encryptingPassphrase));
|
||||||
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") {
|
||||||
@@ -268,6 +353,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// nothing to do. so peaceful.
|
// nothing to do. so peaceful.
|
||||||
this.settings = newSettingW;
|
this.settings = newSettingW;
|
||||||
await this.saveSettings();
|
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);
|
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -312,8 +402,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger("Cancelled.", LOG_LEVEL.NOTICE);
|
Logger("Cancelled.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger("Couldn't parse configuration uri.", LOG_LEVEL.NOTICE);
|
Logger("Couldn't parse or decrypt configuration uri.", LOG_LEVEL.NOTICE);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||||
|
await setupwizard(conf.settings);
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "livesync-replicate",
|
id: "livesync-replicate",
|
||||||
@@ -414,15 +507,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
this.hidePluginSyncModal();
|
this.hidePluginSyncModal();
|
||||||
this.localDatabase.onunload();
|
if (this.localDatabase != null) {
|
||||||
|
this.localDatabase.onunload();
|
||||||
|
}
|
||||||
if (this.gcTimerHandler != null) {
|
if (this.gcTimerHandler != null) {
|
||||||
clearTimeout(this.gcTimerHandler);
|
clearTimeout(this.gcTimerHandler);
|
||||||
this.gcTimerHandler = null;
|
this.gcTimerHandler = null;
|
||||||
}
|
}
|
||||||
this.clearPeriodicSync();
|
this.clearPeriodicSync();
|
||||||
this.clearPluginSweep();
|
this.clearPluginSweep();
|
||||||
this.localDatabase.closeReplication();
|
if (this.localDatabase != null) {
|
||||||
this.localDatabase.close();
|
this.localDatabase.closeReplication();
|
||||||
|
this.localDatabase.close();
|
||||||
|
}
|
||||||
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
|
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
|
||||||
Logger("unloading plugin");
|
Logger("unloading plugin");
|
||||||
}
|
}
|
||||||
@@ -452,6 +549,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.settings.workingPassphrase = this.settings.passphrase;
|
this.settings.workingPassphrase = this.settings.passphrase;
|
||||||
// Delete this feature to avoid problems on mobile.
|
// Delete this feature to avoid problems on mobile.
|
||||||
this.settings.disableRequestURI = true;
|
this.settings.disableRequestURI = true;
|
||||||
|
// Temporary disabled
|
||||||
|
// TODO: If a new GC is created, a new default value must be created.
|
||||||
|
this.settings.gcDelay = 0;
|
||||||
|
|
||||||
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
|
const lsname = "obsidian-live-sync-vaultanddevicename-" + this.app.vault.getName();
|
||||||
if (this.settings.deviceAndVaultName != "") {
|
if (this.settings.deviceAndVaultName != "") {
|
||||||
if (!localStorage.getItem(lsname)) {
|
if (!localStorage.getItem(lsname)) {
|
||||||
@@ -617,7 +718,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
// When save is delayed, it should be cancelled.
|
// When save is delayed, it should be cancelled.
|
||||||
this.batchFileChange = this.batchFileChange.filter((e) => e == file.path);
|
this.batchFileChange = this.batchFileChange.filter((e) => e == file.path);
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
this.watchVaultDeleteAsync(file).then(() => {});
|
this.watchVaultDeleteAsync(file).then(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
async watchVaultDeleteAsync(file: TAbstractFile) {
|
async watchVaultDeleteAsync(file: TAbstractFile) {
|
||||||
@@ -646,7 +747,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
watchVaultRename(file: TAbstractFile, oldFile: any) {
|
||||||
if (this.settings.suspendFileWatching) return;
|
if (this.settings.suspendFileWatching) return;
|
||||||
this.watchVaultRenameAsync(file, oldFile).then(() => {});
|
this.watchVaultRenameAsync(file, oldFile).then(() => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilePath(file: TAbstractFile): string {
|
getFilePath(file: TAbstractFile): string {
|
||||||
@@ -961,7 +1062,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async procQueuedFiles() {
|
async procQueuedFiles() {
|
||||||
await runWithLock("procQueue", true, async () => {
|
await runWithLock("procQueue", false, async () => {
|
||||||
this.saveQueuedFiles();
|
this.saveQueuedFiles();
|
||||||
for (const queue of this.queuedFiles) {
|
for (const queue of this.queuedFiles) {
|
||||||
if (queue.done) continue;
|
if (queue.done) continue;
|
||||||
@@ -972,8 +1073,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
|
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
|
||||||
await this.handleDBChanged(queue.entry);
|
await this.handleDBChanged(queue.entry);
|
||||||
}
|
}
|
||||||
}
|
} else if (now > queue.timeout) {
|
||||||
if (now > queue.timeout) {
|
|
||||||
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL.NOTICE);
|
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL.NOTICE);
|
||||||
queue.warned = true;
|
queue.warned = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -1005,13 +1105,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (isNewFileCompleted) this.procQueuedFiles();
|
if (isNewFileCompleted) this.procQueuedFiles();
|
||||||
}
|
}
|
||||||
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
|
||||||
const skipOldFile = this.settings.skipOlderFilesOnSync;
|
const skipOldFile = this.settings.skipOlderFilesOnSync && false; //patched temporary.
|
||||||
if (skipOldFile) {
|
if (skipOldFile) {
|
||||||
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
|
||||||
|
|
||||||
if (info && info instanceof TFile) {
|
if (info && info instanceof TFile) {
|
||||||
const localMtime = ~~((info as TFile).stat.mtime / 1000);
|
const localMtime = ~~((info as TFile).stat.mtime / 1000);
|
||||||
const docMtime = ~~(doc.mtime / 1000);
|
const docMtime = ~~(doc.mtime / 1000);
|
||||||
|
//TODO: some margin required.
|
||||||
if (localMtime >= docMtime) {
|
if (localMtime >= docMtime) {
|
||||||
Logger(`${doc._id} Skipped, older than storage.`, LOG_LEVEL.VERBOSE);
|
Logger(`${doc._id} Skipped, older than storage.`, LOG_LEVEL.VERBOSE);
|
||||||
return;
|
return;
|
||||||
@@ -1027,15 +1128,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if ("children" in doc) {
|
if ("children" in doc) {
|
||||||
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
|
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
|
||||||
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
|
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
|
||||||
if (missing.length) Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
|
Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
|
||||||
newQueue.missingChildren = missing;
|
newQueue.missingChildren = missing;
|
||||||
this.queuedFiles.push(newQueue);
|
this.queuedFiles.push(newQueue);
|
||||||
this.saveQueuedFiles();
|
|
||||||
} else {
|
} else {
|
||||||
this.queuedFiles.push(newQueue);
|
this.queuedFiles.push(newQueue);
|
||||||
this.saveQueuedFiles();
|
|
||||||
this.procQueuedFiles();
|
|
||||||
}
|
}
|
||||||
|
this.saveQueuedFiles();
|
||||||
|
this.procQueuedFiles();
|
||||||
}
|
}
|
||||||
periodicSyncHandler: number = null;
|
periodicSyncHandler: number = null;
|
||||||
|
|
||||||
@@ -1207,8 +1307,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
const procsDisp = procs == 0 ? "" : ` ⏳${procs}`;
|
const procsDisp = procs == 0 ? "" : ` ⏳${procs}`;
|
||||||
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}${procsDisp}${queued}`;
|
const message = `Sync:${w} ↑${sent} ↓${arrived}${waiting}${procsDisp}${queued}`;
|
||||||
const locks = getLocks();
|
const locks = getLocks();
|
||||||
const pendingTask = locks.pending.length ? `\nPending:${locks.pending.join(", ")}` : "";
|
const pendingTask = locks.pending.length
|
||||||
const runningTask = locks.running.length ? `\nRunning:${locks.running.join(", ")}` : "";
|
? "\nPending: " +
|
||||||
|
Object.entries(locks.pending.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
|
||||||
|
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
|
||||||
|
.join(", ")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const runningTask = locks.running.length
|
||||||
|
? "\nRunning: " +
|
||||||
|
Object.entries(locks.running.reduce((p, c) => ({ ...p, [c]: (p[c] ?? 0) + 1 }), {} as { [key: string]: number }))
|
||||||
|
.map((e) => `${e[0]}${e[1] == 1 ? "" : `(${e[1]})`}`)
|
||||||
|
.join(", ")
|
||||||
|
: "";
|
||||||
this.setStatusBarText(message + pendingTask + runningTask);
|
this.setStatusBarText(message + pendingTask + runningTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1235,7 +1346,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
this.lastLog = newLog;
|
this.lastLog = newLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateStatusBarText() {}
|
updateStatusBarText() { }
|
||||||
|
|
||||||
async replicate(showMessage?: boolean) {
|
async replicate(showMessage?: boolean) {
|
||||||
if (this.settings.versionUpFlash != "") {
|
if (this.settings.versionUpFlash != "") {
|
||||||
@@ -1328,6 +1439,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await allSettledWithConcurrencyLimit(procs, 10);
|
await allSettledWithConcurrencyLimit(procs, 10);
|
||||||
|
Logger(`${procedurename} done.`);
|
||||||
};
|
};
|
||||||
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
await runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||||
Logger(`Update into ${e.path}`);
|
Logger(`Update into ${e.path}`);
|
||||||
@@ -1613,23 +1725,30 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
|
|
||||||
const storageMtime = ~~(file.stat.mtime / 1000);
|
const storageMtime = ~~(file.stat.mtime / 1000);
|
||||||
const docMtime = ~~(doc.mtime / 1000);
|
const docMtime = ~~(doc.mtime / 1000);
|
||||||
if (storageMtime > docMtime) {
|
const dK = `${file.path}-diff`;
|
||||||
//newer local file.
|
const isLastDiff = (await this.localDatabase.kvDB.get<{ storageMtime: number; docMtime: number }>(dK)) || { storageMtime: 0, docMtime: 0 };
|
||||||
Logger("STORAGE -> DB :" + file.path);
|
if (isLastDiff.docMtime == docMtime && isLastDiff.storageMtime == storageMtime) {
|
||||||
Logger(`${storageMtime} > ${docMtime}`);
|
// Logger("CHECKED :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
await this.updateIntoDB(file);
|
|
||||||
} else if (storageMtime < docMtime) {
|
|
||||||
//newer database file.
|
|
||||||
Logger("STORAGE <- DB :" + file.path);
|
|
||||||
Logger(`${storageMtime} < ${docMtime}`);
|
|
||||||
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
|
||||||
if (docx != false) {
|
|
||||||
await this.doc2storage_modify(docx, file);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
if (storageMtime > docMtime) {
|
||||||
// Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE);
|
//newer local file.
|
||||||
//eq.case
|
Logger("STORAGE -> DB :" + file.path);
|
||||||
|
Logger(`${storageMtime} > ${docMtime}`);
|
||||||
|
await this.updateIntoDB(file);
|
||||||
|
} else if (storageMtime < docMtime) {
|
||||||
|
//newer database file.
|
||||||
|
Logger("STORAGE <- DB :" + file.path);
|
||||||
|
Logger(`${storageMtime} < ${docMtime}`);
|
||||||
|
const docx = await this.localDatabase.getDBEntry(file.path, null, false, false);
|
||||||
|
if (docx != false) {
|
||||||
|
await this.doc2storage_modify(docx, file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Logger("EVEN :" + file.path, LOG_LEVEL.VERBOSE);
|
||||||
|
// Logger(`${storageMtime} = ${docMtime}`, LOG_LEVEL.VERBOSE);
|
||||||
|
//eq.case
|
||||||
|
}
|
||||||
|
await this.localDatabase.kvDB.set(dK, { storageMtime, docMtime });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1738,10 +1857,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
|
|||||||
if (!this.localDatabase.isReady) return;
|
if (!this.localDatabase.isReady) return;
|
||||||
await runWithLock("sweepplugin", true, async () => {
|
await runWithLock("sweepplugin", true, async () => {
|
||||||
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
|
||||||
if (!this.settings.encrypt) {
|
|
||||||
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.deviceAndVaultName) {
|
if (!this.deviceAndVaultName) {
|
||||||
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
Logger("You have to set your device and vault name.", LOG_LEVEL.NOTICE);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export const connectRemoteCouchDBWithSetting = (settings: RemoteDBSettings, isMo
|
|||||||
|
|
||||||
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
const connectRemoteCouchDB = async (uri: string, auth: { username: string; password: string }, disableRequestURI: boolean, passphrase: string | boolean): Promise<string | { db: PouchDB.Database<EntryDoc>; info: PouchDB.Core.DatabaseInfo }> => {
|
||||||
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
if (!isValidRemoteCouchDBURI(uri)) return "Remote URI is not valid";
|
||||||
|
if (uri.toLowerCase() != uri) return "Remote URI and database name cound not contain capital letters.";
|
||||||
|
if (uri.indexOf(" ") !== -1) return "Remote URI and database name cound not contain spaces.";
|
||||||
let authHeader = "";
|
let authHeader = "";
|
||||||
if (auth.username && auth.password) {
|
if (auth.username && auth.password) {
|
||||||
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
|
||||||
|
|||||||
39
styles.css
39
styles.css
@@ -2,28 +2,33 @@
|
|||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-accent);
|
background-color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal {
|
.normal {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleted {
|
.deleted {
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-scrollable {
|
.op-scrollable {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
/* min-height: 280px; */
|
/* min-height: 280px; */
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-pre {
|
.op-pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-warn {
|
.op-warn {
|
||||||
border: 1px solid salmon;
|
border: 1px solid salmon;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-warn::before {
|
.op-warn::before {
|
||||||
content: "Warning";
|
content: "Warning";
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -31,11 +36,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-warn-info {
|
.op-warn-info {
|
||||||
border: 1px solid rgb(255, 209, 81);
|
border: 1px solid rgb(255, 209, 81);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-warn-info::before {
|
.op-warn-info::before {
|
||||||
content: "Notice";
|
content: "Notice";
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -43,27 +50,33 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.syncstatusbar {
|
.syncstatusbar {
|
||||||
-webkit-filter: grayscale(100%);
|
-webkit-filter: grayscale(100%);
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tcenter {
|
.tcenter {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-wrap {
|
.sls-plugins-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl {
|
.sls-plugins-tbl {
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 80%;
|
max-height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider th {
|
.divider th {
|
||||||
border-top: 1px solid var(--background-modifier-border);
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .sls-table-head{
|
/* .sls-table-head{
|
||||||
width:50%;
|
width:50%;
|
||||||
}
|
}
|
||||||
@@ -75,9 +88,11 @@
|
|||||||
.sls-btn-left {
|
.sls-btn-left {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-btn-right {
|
.sls-btn-right {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-hidden {
|
.sls-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -85,8 +100,9 @@
|
|||||||
:root {
|
:root {
|
||||||
--slsmessage: "";
|
--slsmessage: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-wrap::before,
|
.CodeMirror-wrap::before,
|
||||||
.cm-s-obsidian > .cm-editor::before {
|
.cm-s-obsidian>.cm-editor::before {
|
||||||
content: var(--slsmessage);
|
content: var(--slsmessage);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -105,12 +121,15 @@
|
|||||||
.CodeMirror-wrap::before {
|
.CodeMirror-wrap::before {
|
||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
.cm-s-obsidian > .cm-editor::before {
|
|
||||||
|
.cm-s-obsidian>.cm-editor::before {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-tab {
|
.sls-setting-tab {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sls-setting-menu-btn {
|
div.sls-setting-menu-btn {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
@@ -131,8 +150,9 @@ div.sls-setting-menu-btn {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
}
|
}
|
||||||
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
|
|
||||||
.sls-setting-tab:checked ~ div.sls-setting-menu-btn {
|
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
||||||
|
.sls-setting-tab:checked~div.sls-setting-menu-btn {
|
||||||
background-color: var(--interactive-accent);
|
background-color: var(--interactive-accent);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
@@ -143,14 +163,17 @@ div.sls-setting-menu-btn {
|
|||||||
/* flex-wrap: wrap; */
|
/* flex-wrap: wrap; */
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-label {
|
.sls-setting-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-collapsed {
|
.setting-collapsed {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl-buttons {
|
.sls-plugins-tbl-buttons {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -159,13 +182,16 @@ div.sls-setting-menu-btn {
|
|||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-plugins-tbl-device-head {
|
.sls-plugins-tbl-device-head {
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
color: var(--text-accent);
|
color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-flex {
|
.op-flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-flex input {
|
.op-flex input {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -185,9 +211,11 @@ div.sls-setting-menu-btn {
|
|||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-accent);
|
background-color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-normal {
|
.history-normal {
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-deleted {
|
.history-deleted {
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
@@ -197,6 +225,7 @@ div.sls-setting-menu-btn {
|
|||||||
.ob-btn-config-fix label {
|
.ob-btn-config-fix label {
|
||||||
margin-right: 40px;
|
margin-right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ob-btn-config-info {
|
.ob-btn-config-info {
|
||||||
border: 1px solid salmon;
|
border: 1px solid salmon;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user