Compare commits

...

97 Commits

Author SHA1 Message Date
vorotamoroz
ca36e1b663 Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2022-08-09 17:10:19 +09:00
vorotamoroz
8f583e3680 Fixed:
- Now, we can synchronise hidden files that conflicted on each devices.
Enhanced:
- 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.
2022-08-09 17:10:08 +09:00
vorotamoroz
98407cf72f Merge pull request #101 from Miigon/chinese-doc
Add Chinese documentations
2022-08-09 17:06:39 +09:00
Miigon
1f377cdf67 add setup_own_server_cn.md 2022-08-09 13:06:35 +08:00
Miigon
3a965e74da add chinese README_cn.md 2022-08-09 12:38:43 +08:00
vorotamoroz
640c2a91f7 bumped 2022-08-08 09:57:53 +09:00
vorotamoroz
df9f9d9bc0 fixed the ambiguous wording and some minor changes 2022-08-08 09:53:17 +09:00
vorotamoroz
787bf37906 Merge pull request #98 from Miigon/docfix
improved grammar & fixed typos in docs and plugin texts
2022-08-08 09:30:18 +09:00
Miigon
63abeb7f6b improved grammar & fixed typos in docs and plugin texts 2022-08-04 14:34:45 +08:00
vorotamoroz
f0ffb0620e Fixed file deletion failures. 2022-07-29 13:33:33 +09:00
vorotamoroz
c88c939cd9 bumped 2022-07-29 09:42:22 +09:00
vorotamoroz
05b53eb2cf Fixes:
- Now conflict resolution back well
2022-07-29 09:38:23 +09:00
vorotamoroz
61b65b0461 Bumped 2022-07-28 19:20:59 +09:00
vorotamoroz
ac9be937b4 New feature:
- The metadata of the deleted files will be kept on the database by default.
- We can see the history of deleted files.
- Update information became to be shown on the major upgrade.

Fixed:
- `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.
2022-07-28 19:19:37 +09:00
vorotamoroz
c610284cab bumped again 2022-07-25 13:50:42 +09:00
vorotamoroz
2e6ed4777c Added postcss for ci 2022-07-25 13:22:37 +09:00
vorotamoroz
ab6ff01f1a Bumped 2022-07-25 13:16:49 +09:00
vorotamoroz
c836953fa9 Fixed:
- Leaked prototyping code.

    New feature:
    - `Pick file to show history` shows a file list and you can choose one for showing history.
    - `Back to this revision` implemented on the Document history dialog.
2022-07-25 13:14:59 +09:00
vorotamoroz
e69371ff24 Degraded! 2022-07-25 13:05:00 +09:00
vorotamoroz
d324add086 Fixed:
- Leaked prototyping code.

New feature:
- `Pick file to show history` shows a file list and you can choose one for showing history.
- `Back to this revision` implemented on the Document history dialog.
2022-07-25 12:48:48 +09:00
vorotamoroz
0caf330f39 Merge remote-tracking branch 'origin/snyk-upgrade-52349e72cdcadcd0ba41dd967c715aa7' 2022-07-21 16:40:18 +09:00
vorotamoroz
3a147ca427 Merge branch 'snyk-upgrade-6a3786fc4cbe394542e9d1b6058c46e7' 2022-07-21 16:26:54 +09:00
vorotamoroz
8266cfba40 Merge remote-tracking branch 'origin/snyk-upgrade-d6d88a887d6bc2d451a6e9c7a7759727' 2022-07-21 16:15:35 +09:00
vorotamoroz
e2f06181fa bumped 2022-07-21 13:06:57 +09:00
vorotamoroz
bb6d787607 Imprinting version numbers to boot log. 2022-07-21 13:06:42 +09:00
vorotamoroz
cb406e2db6 New feature.
- Local database name can now be customized.
- Buttons to back skip-patterns of Hidden file sync to default.
2022-07-21 13:05:35 +09:00
vorotamoroz
0a1248c5fc Fixed:
- Now Notification is less noisy.
- Some synchronization won't be missed.
- Scanning speed improved.

Implemented:

- Implemented notifications to reload the plugin.
- Rescue button to updating all hidden files for overwriting them on other vaults.
2022-07-20 16:57:21 +09:00
vorotamoroz
7b9b934c61 Tidied up. 2022-07-19 18:41:39 +09:00
vorotamoroz
27505f3024 Add the doc 2022-07-19 18:41:27 +09:00
vorotamoroz
1cddcf8b95 bumped 2022-07-19 18:00:25 +09:00
vorotamoroz
fddc466b0f New Feature
- Hidden file sync.
2022-07-19 17:57:29 +09:00
vorotamoroz
0e6a6dcd2a bumped 2022-07-14 18:35:04 +09:00
vorotamoroz
f3a47b904f - fix for the pop-out window.
- fix file handling in boot sequence.
2022-07-14 18:34:55 +09:00
vorotamoroz
6563481501 add type annotation 2022-07-14 18:32:59 +09:00
vorotamoroz
b5e8ee691a Merge pull request #89 from MohamedBassem/patch-1
Removing the step to enable history from the README
2022-07-13 10:03:40 +09:00
Mohamed Bassem
22a428f216 Removing the step to enable history from the README
While following the README, it told me to enable "Use History" though I couldn't find this setting anywhere. Turned out, it became the default as of 89de551fd7 with no way to be configured. So this PR, removes this step from the readme.
2022-07-12 12:20:24 +01:00
snyk-bot
d5a95d43dd fix: upgrade idb from 7.0.1 to 7.0.2
Snyk has created this PR to upgrade idb from 7.0.1 to 7.0.2.

See this package in npm:
https://www.npmjs.com/package/idb

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-07-09 05:02:39 +00:00
vorotamoroz
7d6b83a1cb Fixed
- Saving notes with wrong type.
2022-07-07 17:21:23 +09:00
vorotamoroz
41034d7d92 Create release.yml 2022-06-30 18:18:31 +09:00
vorotamoroz
2455ff6ee1 bump 2022-06-30 18:17:09 +09:00
vorotamoroz
89de551fd7 Fixed:
- Unexpected massive palallel running of file checking in boot sequence is solved.
- Batch file change is not  missing changes now.
- Ignore changes caused by the plug-ins themselves.
- Garbage collection is completely disabled.
- Fixed sometimes fails initial replication after dropping local DB.
Improved:
- a bit more understandable messages
- Save the file into the big chunk on initial scan.
- Use history is always enabled.
- Boot sequence got faster.
2022-06-30 17:46:42 +09:00
vorotamoroz
124a49b80f Fixed and implemented
- Readme tidied.
- More faster e2ee.
- URI and databasename check improved.
2022-06-23 18:26:43 +09:00
vorotamoroz
3e76292aa7 Implemented:
- Encrypting setup URI by passphrse.
(Note: You have to make the setup URI again)

Fixed:
- Setup procedure fixed.
- Status text fixed.
- Documentation fixed.
2022-06-19 15:36:36 +09:00
vorotamoroz
4634ab73b1 - Automatic garbage collection disabled
- Fixed database unloading problem
2022-06-19 14:09:11 +09:00
vorotamoroz
359c10f1d7 Correction of wording 2022-06-15 17:59:10 +09:00
vorotamoroz
59ebac3efc Correction of wording 2022-06-15 17:54:35 +09:00
vorotamoroz
b4edca3a99 Fixed format. 2022-06-15 17:51:07 +09:00
vorotamoroz
4b76b10a6f Add some note. 2022-06-15 17:45:46 +09:00
vorotamoroz
d4b53280e3 Fixed status message and lag on boot time scan. 2022-06-15 17:45:37 +09:00
vorotamoroz
dbd9b17b20 Fixed:
- Fixed ignoring changes on replicating.
- Disabled `Skip old files on sync` temporary (so fragile between multiple devices)
2022-06-14 19:49:21 +09:00
vorotamoroz
dcfb9867f2 Fixed:
- Rewritten lock acquiring logic.
- Fixed plugin dialog's message.
- Fixed some error messages.
- Fixed action on replicating non-note entries.
2022-06-14 19:01:31 +09:00
vorotamoroz
46ff17fdf3 New Feature:
- Skip conflicted check while replication

Fixed:
- Rewrited replication reflection algorithm.
2022-06-13 17:36:26 +09:00
vorotamoroz
728dabce60 Fixed repo issue. 2022-06-10 19:04:11 +09:00
vorotamoroz
3783fc6926 Implemented:
- Exporting settings and setup from uri.

Fixed:
- Change "Leaf" into "Chunk"
- Reduced meaninglessly verbose logging
- Trimmed deadcode.
2022-06-10 18:48:04 +09:00
vorotamoroz
236f2293ce Remove notice. 2022-06-10 01:28:41 +09:00
vorotamoroz
4cb908cf62 Fixed migration problem. 2022-06-10 01:26:55 +09:00
vorotamoroz
fab2327937 fix a typo. 2022-06-09 18:22:38 +09:00
vorotamoroz
0837648aa6 Add a note 2022-06-09 18:20:51 +09:00
vorotamoroz
58dcc13b50 Bumped 2022-06-09 17:45:32 +09:00
vorotamoroz
e2da4ec454 # Fixed
- Illegible coloring of the Diff dialog.

# Implemented
- On-the-fly encryption and decryption in replication.
- Text splitting algorithms updated
(use a bit more memory (which is saved by On-the-fly enc-dec), but faster than old algorithms.)
- Garbage collector is now decent and memory saving.

# Internal things
- Refactored so much.
2022-06-09 17:44:08 +09:00
vorotamoroz
f613f1b887 New feature:
- Add database configuration check & fixing tool
2022-05-10 13:43:50 +09:00
vorotamoroz
88ef7c316a Fixed:
- Do not show error message when synchronization run automatically .
2022-05-09 11:08:10 +09:00
vorotamoroz
3fbecdf567 Fixed:
- Newly created files could not be synchronized.
2022-05-08 00:02:34 +09:00
vorotamoroz
5db3a374a9 Fixed:
- Freezing LiveSync on mobile devices.
2022-05-06 18:14:45 +09:00
vorotamoroz
6f76f90075 - Reverted PouchDB direct importing.
(I completely forgot why I webpacked.)
- Submodule re-init
2022-04-30 01:11:17 +09:00
vorotamoroz
9acf9fe093 remove wrong submodule 2022-04-30 00:46:14 +09:00
snyk-bot
7da930a8bb fix: upgrade svelte-preprocess from 4.10.3 to 4.10.5
Snyk has created this PR to upgrade svelte-preprocess from 4.10.3 to 4.10.5.

See this package in npm:
https://www.npmjs.com/package/svelte-preprocess

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-29 03:54:29 +00:00
snyk-bot
a632b79726 fix: upgrade esbuild-svelte from 0.6.2 to 0.7.0
Snyk has created this PR to upgrade esbuild-svelte from 0.6.2 to 0.7.0.

See this package in npm:
https://www.npmjs.com/package/esbuild-svelte

See this project in Snyk:
https://app.snyk.io/org/vrtmrz/project/d2c9b72d-6e38-433f-bbad-725719c0fa4d?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-29 03:54:25 +00:00
vorotamoroz
1e3de47d92 Update manifest.json 2022-04-28 18:43:46 +09:00
vorotamoroz
a50f0965f6 Refactored and touched up some.
Not available on iOS yet, be careful!
2022-04-28 18:24:48 +09:00
vorotamoroz
9d3aa35b0b Fixed:
- Problems around new request API's
2022-04-20 15:02:06 +09:00
vorotamoroz
b4b9684a55 Fixed:
- Failure on the first sync
2022-04-07 16:17:20 +09:00
vorotamoroz
221cccb845 bumped 2022-04-04 20:01:50 +09:00
vorotamoroz
801500f924 Fixed:
- Fixed merging issue (Concat both)
- Overdetection of file change after the replication
2022-04-04 19:58:44 +09:00
vorotamoroz
3545ae9690 Implemented:
- using Obsidian API to synchronize.
- Copy button on history dialog.

Documented:
- Document improved.
2022-04-01 17:57:14 +09:00
vorotamoroz
255e7bf828 bumped 2022-03-08 10:40:11 +09:00
vorotamoroz
6f9e7bbcf4 Merge pull request #49 from banool/main
Print exception on failure in certain cases
2022-03-08 10:31:25 +09:00
Daniel Porteous
ce1c94a814 Print exception on failure in certain cases 2022-03-06 16:13:57 -08:00
vorotamoroz
caf7934f28 Create FUNDING.yml 2022-02-25 13:14:24 +09:00
vorotamoroz
31ab0e90f6 Fixed:
- Device and vault name is now not stored in the data.json.
You can synchronize LiveSync's configuration!
2022-02-18 20:10:43 +09:00
vorotamoroz
43fba807c3 Implemented: New "plugins and their settings"
Fixed: some plugin synchronization bugs.
2022-02-16 18:26:13 +09:00
vorotamoroz
3a8e52425e Fixed:
- Some extensions are encoded incorrectly.
2022-01-27 12:15:23 +09:00
vorotamoroz
15b580aa9a Implemented:
- History dialog

Improved:
- Speed up Garbage Collection.
2022-01-13 17:41:45 +09:00
vorotamoroz
ebcb059d99 Modified:
- Plugins and settings is now in beta.

Implemented:
- Show the count of the pending processes into the status.
2022-01-11 13:17:35 +09:00
vorotamoroz
5bb8b2567b Modified:
- Implement automatic temporary reduction of batch sizes.
- Disable remote checkpointing.
2022-01-05 17:20:33 +09:00
vorotamoroz
c3464a4e9c New feature:
- Bootup sequence prevention implemented.

Touched the docs up:
2021-12-28 11:30:19 +09:00
vorotamoroz
55545da45f Fixed:
- Fixed problems about saving or deleting files to the local database.
- Disable version up warning.
- Fixed error on folder renaming.
- Merge dialog is now shown one by one.
- Fixed icons of queued files.
- Handled sync issue of Folder to File
- Fixed the messages in the setting dialog.
- Fixed deadlock.
2021-12-24 17:05:57 +09:00
vorotamoroz
96165b4f9b Fixed and implemented:
- New configuration to solve synchronization failure on large vault.
- Preset misconfigurations
- Sometimes hanged on replication.
- Wrote documents
2021-12-23 13:22:46 +09:00
vorotamoroz
abe613539b Fixes:
- Allow "'" and ban "#" in filenames. #27
- Fixed misspelling (one of #28)
2021-12-22 19:38:00 +09:00
vorotamoroz
fc210de58b bumped 2021-12-16 19:08:15 +09:00
vorotamoroz
1b2f9dd171 Refactored and fixed:
- Refactored, linted, fixed potential problems, enabled 'use strict'

Fixed:
- Added "Enable plugin synchronization" option
(Plugins and settings had been run always)

Implemented:
- Sync preset implemented.
- "Check integrity on saving" implemented.
- "Sanity check" implemented
It's mainly for debugging.
2021-12-16 19:06:42 +09:00
vorotamoroz
eef2281ae3 Mini fix:
fixed the problem that plugin sync timing and notification.
2021-12-15 09:22:43 +09:00
vorotamoroz
40ed2bbdcf Improved:
- Tidied up the Setting dialog.
- Implemented Automatic plugin saving.
- implemented notifying the new plugin or its settings.

Fixed:

- Reduced reconnection when editing configuration.
- Fixed the problem about syncing the stylesheet of the plugin.
2021-12-14 19:14:17 +09:00
vorotamoroz
92fd814c89 update readme 2021-12-07 17:29:36 +09:00
vorotamoroz
3118276603 Wrote the docs. 2021-12-07 17:28:18 +09:00
vorotamoroz
2b11be05ec Add new feature:
- Reread all files
2021-12-06 12:19:05 +09:00
vorotamoroz
0ee73860d1 Fixed:
- Make less file corruption.
- Some notice was not hidden automatically
2021-12-06 11:43:42 +09:00
43 changed files with 12845 additions and 13767 deletions

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
npm node_modules
build
.eslintrc.js.bak

19
.eslintrc Normal file
View File

@@ -0,0 +1,19 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"require-await": "warn",
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: vrtmrz

94
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,94 @@
name: Release Obsidian Plugin
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
submodules: recursive
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '14.x' # You might need to adjust this value to your own version
# Get the version number and put it in a variable
- name: Get Version
id: version
run: |
echo "::set-output name=tag::$(git describe --abbrev=0)"
# Build the plugin
- name: Build
id: build
run: |
npm ci
npm run build --if-present
# Package the required files into a zip
- name: Package
run: |
mkdir ${{ github.event.repository.name }}
cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }}
zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }}
# Create the release on github
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.ref }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
prerelease: false
# Upload the packaged release file
- name: Upload zip file
id: upload-zip
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./${{ github.event.repository.name }}.zip
asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip
asset_content_type: application/zip
# Upload the main.js
- name: Upload main.js
id: upload-main
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./main.js
asset_name: main.js
asset_content_type: text/javascript
# Upload the manifest.json
- name: Upload manifest.json
id: upload-manifest
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./manifest.json
asset_name: manifest.json
asset_content_type: application/json
# Upload the style.css
- name: Upload styles.css
id: upload-css
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./styles.css
asset_name: styles.css
asset_content_type: text/css
# TODO: release notes???

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/lib"]
path = src/lib
url = https://github.com/vrtmrz/livesync-commonlib

153
README.md
View File

@@ -1,90 +1,129 @@
# Self-hosted LiveSync
Sorry for late! [Japanese docs](./README_ja.md) is also coming up.
[Japanese docs](./README_ja.md) [Chinese docs](./README_cn.md).
**Renamed from: obsidian-livesync**
Self-hosted LiveSync is a community implemented synchronization plugin.
A self-hosted or purchased CouchDB acts as the intermediate server. Available on every obsidian-compatible platform.
Using a self-hosted database, live-sync to multi-devices bidirectionally.
Runs in Mac, Android, Windows, and iOS. Perhaps available on Linux too.
Community implementation, not compatible with official "Sync".
Note: It has no compatibility with the official "Obsidian Sync".
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
**It's getting almost stable now, But Please make sure to back your vault up!**
Before installing or upgrading LiveSync, please back your vault up.
Limitations: ~~Folder deletion handling is not completed.~~ **It would work now.**
## Features
## This plugin enables...
- Runs in Windows, Mac, iPad, iPhone, Android, Chromebook
- Synchronize to Self-hosted Database
- Replicate to/from other devices bidirectionally near-real-time
- 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).
- Visual conflict resolver included.
- Bidirectional synchronization between devices nearly in real-time
- You can use CouchDB or its compatibles like IBM Cloudant.
- End-to-End encryption supported.
- Plugin synchronization(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.)
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.
Useful for researchers, engineers and developers with a need to keep their notes fully self-hosted for security reasons. Or just anyone who would like the peace of mind knowing that their notes are fully private.
## IMPORTANT NOTICE
- Do not use with other synchronize solutions. Before enabling this plugin, make sure to disable other synchronize solutions, to avoid content corruption or duplication. If you want to synchronize to both backend, sync one by one, please.
This includes making your vault on the cloud-controlled folder(e.g., Inside the iCloud folder).
- This is the synchronization plugin. Not backup solutions. Do not rely on this for backup.
- 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.**)
## 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.
- Do not use in conjunction with another synchronization solution (including iCloud, Obsidian Sync). Before enabling this plugin, make sure to disable all the other synchronization methods to avoid content corruption or duplication. If you want to synchronize to two or more services, do them one by one and never enable two synchronization methods at the same time.
This includes not putting your vault inside a cloud-synchronized folder (eg. an iCloud folder or Dropbox folder)
- This is a synchronization plugin. Not a backup solutions. Do not rely on this for backup.
- If the device's storage runs out, database corruption may happen.
- Hidden files or any other invisible files wouldn't be kept in the database, thus won't be synchronized. (**and may also get deleted**)
## 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/`
2. Get your database. 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)
2. [Setup your CouchDB](docs/setup_own_server.md) (Now writing)
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.
### Get your database ready.
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)
2. [Setup your CouchDB](docs/setup_own_server.md)
Note: More information about alternative hosting methods needed! Currently, [using fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85) is being discussed.
### First device
1. Install the plugin on your device.
2. Configure remote database infomation.
1. Fill your server's information into the `Remote Database configuration` pane.
2. Enabling `End to End Encryption` is recommended. After entering a passphrase, click `Apply`.
3. Click `Test Database Connection` and make sure that the plugin says `Connected to (your-database-name)`.
4. Click `Check database configuration` and make sure all tests have passed.
3. Configure when should the synchronization happen in `Sync Settings` tab. (You can also leave them for later)
1. If you want to synchronize in real-time, enable `LiveSync`.
2. Or, set up the synchronization as you like. By default, none of the settings are enabled, meaning you would need to manually trigger the synchronization process.
3. Additional configurations are also here. I recommend enabling `Use Trash for deleted files`, but you can also leave all configurations as-is.
4. Configure miscellaneous features.
1. Enabling `Show staus inside editor` shows status at the top-right corner of the editor while in editing mode. (Recommended)
5. Go back to the editor. Wait for the initial scan to complete.
6. When the status no longer changes and shows a ⏹️ for COMPLETED (No ⏳ and 🧩 icons), you are ready to synchronize with the server.
7. Press the replicate icon on the Ribbon or run `Replicate now` from the command palette. This will send all your data to the server.
8. Open command palette, run `Copy setup URI`, and set a passphrase. This will export your configuration to clipboard as a link for you to import into your other devices.
**IMPORTANT: BE CAREFUL NOT TO SHARE THIS LINK. THE URI CONTAINS ALL YOUR CREDENTIALS.** (even though nobody could read them without the passphrase)
### Subsequent Devices
Note: If we are going to synchronize with a non-empty vault, the modification dates and times of the files must match between them. Otherwise, extra transfers may occur or files may become corrupted.
For simplicity, we strongly recommend that we sync to an empty vault.
1. Install the plug-in.
2. Open the link that you have exported from the first device.
3. The plug-in will ask you whether you are sure to apply the configurations. Answer `Yes`, then follow these instructions:
1. Answer `Yes` to `Keep local DB?`.
*Note: If you start with an existing vault, you have to answer `No` to this question and also answer `No` to `Rebuild the database?`.*
2. Answer `Yes` to `Keep remote DB?`.
3. Answer `Yes` to `Replicate once?`.
Then, all your settings should be successfully imported from the first device.
4. Your notes should get synchronized soon.
## Something looks corrupted...
Please open the configuration 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 with a timestamp. So you can use a existed vault).
## 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 set up a [Tasting server for self-hosted-livesync](https://olstaste.vrtmrz.net/). Try it out for free!
Note: Please read "Limitations" carefully. Do not send your private vault.
## 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.)
# Information in StatusBar
## Information in StatusBar
Synchronization status is shown in statusbar.
- Status
- ⏹️ Stopped
- 💤 LiveSync is enabled. Waiting for changes.
- ⚡️ Synchronize is now in progress.
-Error occurred.
- ↑ Uploaded pieces
- ↓ Downloaded pieces
- 💤 LiveSync enabled. Waiting for changes.
- ⚡️ Synchronization in progress.
-An error occurred.
- ↑ Uploaded chunks and metadata
- ↓ Downloaded chunks and metadata
- ⏳ Number of pending processes
- 🧩 Number of files waiting for their chunks.
If you have deleted or renamed files, please wait until ⏳ icon disappeared.
# More supplements
- 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.
## Hints
- If a folder becomes empty after a replication, it will be deleted by default. But you can toggle this behaviour. Check the [Settings](docs/settings.md).
- LiveSync mode drains more batteries in mobile devices. Periodic sync with some automatic sync is recommended.
- Mobile Obsidian can not connect to a non-secure (HTTP) or a locally-signed servers, even if the root certificate is installed on the device.
- There are no 'exclude_folders' like configurations.
- While synchronizing, files are compared by their modification time and the older ones will be overwritten by the newer ones. Then plugin checks for conflicts and if a merge is needed, a dialog will open.
- Rarely, a file in the database could be corrupted. The plugin will not write to local storage when a file looks corrupted. If a local version of the file is on your device, the corruption could be fixed by editing the local file and synchronizing it. But if the file does not exist on any of your devices, then it can not be rescued. In this case you can delete these items from the settings dialog.
- If your database looks corrupted, try "Drop History". Usually, It is the easiest way.
- 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.
- To stop the bootup sequence (eg. for fixing problems on databases), you can put a `redflag.md` file at the root of your vault.
- Q: Database is growing, how can I shrink it down?
A: each of the docs is saved with their past 100 revisions for detecting and resolving conflicts. Picturing that one device has been offline for a while, and comes online again. The device has to compare its notes with the remotely saved ones. If there exists a historic revision in which the note used to be identical, it could be updated safely (like git fast-forward). Even if that is not in revision histories, we only have to check the differences after the revision that both devices commonly have. This is like 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)
- If you want to synchronize files without obsidian, you can use [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync).
- WebClipper is also available on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
# License
Repo is here: [obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). (Docs are work in progress.)
The source code is licensed MIT.
## License
The source code is licensed under the MIT License.

130
README_cn.md Normal file
View File

@@ -0,0 +1,130 @@
# Self-hosted LiveSync
Self-hosted LiveSync (自搭建在线同步) 是一个社区实现的在线同步插件。
使用一个自搭建的或者购买的 CouchDB 作为中转服务器。兼容所有支持 Obsidian 的平台。
注意: 本插件与官方的 "Obsidian Sync" 服务不兼容。
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
安装或升级 LiveSync 之前,请备份你的 vault。
## 功能
- 可视化的冲突解决器
- 接近实时的多设备双向同步
- 可使用 CouchDB 以及兼容的服务,如 IBM Cloudant
- 支持端到端加密
- 插件同步 (Beta)
- 从 [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) 接收 WebClip (本功能不适用端到端加密)
适用于出于安全原因需要将笔记完全自托管的研究人员、工程师或开发人员,以及任何喜欢笔记完全私密所带来的安全感的人。
## 重要提醒
- 请勿与其他同步解决方案(包括 iCloud、Obsidian Sync一起使用。在启用此插件之前请确保禁用所有其他同步方法以避免内容损坏或重复。如果要同步到多个服务请一一进行切勿同时启用两种同步方法。
这包括不能将您的保管库放在云同步文件夹中(例如 iCloud 文件夹或 Dropbox 文件夹)
- 这是一个同步插件,不是备份解决方案。不要依赖它进行备份。
- 如果设备的存储空间耗尽,可能会发生数据库损坏。
- 隐藏文件或任何其他不可见文件不会保存在数据库中,因此不会被同步。(**并且可能会被删除**
## 如何使用
### 准备好你的数据库
首先准备好你的数据库。IBM Cloudant 是用于测试的首选。或者,您也可以在自己的服务器上安装 CouchDB。有关更多信息请参阅以下内容
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
2. [Setup your CouchDB](docs/setup_own_server_cn.md)
Note: 正在征集更多搭建方法!目前在讨论的有 [使用 fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85)。
### 第一个设备
1. 在您的设备上安装插件。
2. 配置远程数据库信息。
1. 将您的服务器信息填写到 `Remote Database configuration`(远程数据库配置)设置页中。
2. 建议启用 `End to End Encryption`(端到端加密)。输入密码后,单击“应用”。
3. 点击 `Test Database Connection` 并确保插件显示 `Connected to (你的数据库名称)`
4. 单击 `Check database configuration`(检查数据库配置)并确保所有测试均已通过。
3.`Sync Settings`(同步设置)选项卡中配置何时进行同步。(您也可以稍后再设置)
1. 如果要实时同步,请启用 `LiveSync`
2. 或者,根据您的需要设置同步方式。默认情况下,不会启用任何自动同步,这意味着您需要手动触发同步过程。
3. 其他配置也在这里。建议启用 `Use Trash for deleted files`(删除文件到回收站),但您也可以保持所有配置不变。
4. 配置杂项功能。
1. 启用 `Show staus inside editor` 会在编辑器右上角显示状态。(推荐开启)
5. 回到编辑器。等待初始扫描完成。
6. 当状态不再变化并显示 ⏹️ 图标表示 COMPLETED没有 ⏳ 和 🧩 图标)时,您就可以与服务器同步了。
7. 按功能区上的复制图标或从命令面板运行 `Replicate now`(立刻复制)。这会将您的所有数据发送到服务器。
8. 打开命令面板,运行 `Copy setup URI`(复制设置链接),并设置密码。这会将您的配置导出到剪贴板,作为您导入其他设备的链接。
**重要: 不要公开本链接,这个链接包含了你的所有认证信息!** (即使没有密码别人读不了)
### 后续设备
注意:如果要与非空的 vault 进行同步,文件的修改日期和时间必须互相匹配。否则,可能会发生额外的传输或文件可能会损坏。
为简单起见,我们强烈建议同步到一个全空的 vault。
1. 安装插件。
2. 打开您从第一台设备导出的链接。
3. 插件会询问您是否确定应用配置。 回答 `Yes`,然后按照以下说明进行操作:
1.`Keep local DB?` 回答 `Yes`
*注意:如果您希望保留本地现有 vault则必须对此问题回答 `No`,并对 `Rebuild the database?` 回答 `No`。*
2.`Keep remote DB?` 回答 `Yes`
3.`Replicate once?` 回答 `Yes`
完成后,您的所有设置将会从第一台设备成功导入。
4. 你的笔记应该很快就会同步。
## 文件看起来有损坏...
请再次打开配置链接并回答如下:
- 如果您的本地数据库看起来已损坏(当你的本地 Obsidian 文件看起来很奇怪)
-`Keep local DB?` 回答 `No`
- 如果您的远程数据库看起来已损坏(当复制时发生中断)
-`Keep remote DB?` 回答 `No`
如果您对两者都回答“否”,您的数据库将根据您设备上的内容重建。并且远程数据库将锁定其他设备,您必须再次同步所有设备。(此时,几乎所有文件都会与时间戳同步。因此您可以安全地使用现有的 vault
## 测试服务器
设置 Cloudant 或本地 CouchDB 实例有点复杂,所以我搭建了一个 [self-hosted-livesync 尝鲜服务器](https://olstaste.vrtmrz.net/)。欢迎免费尝试!
注意:请仔细阅读“限制”条目。不要发送您的私人 vault。
## 状态栏信息
同步状态将显示在状态栏。
- 状态
- ⏹️ 就绪
- 💤 LiveSync 已启用,正在等待更改。
- ⚡️ 同步中。
- ⚠ 一个错误出现了。
- ↑ 上传的 chunk 和元数据数量
- ↓ 下载的 chunk 和元数据数量
- ⏳ 等待的过程的数量
- 🧩 正在等待 chunk 的文件数量
如果你删除或更名了文件,请等待 ⏳ 图标消失。
## 提示
- 如果文件夹在复制后变为空,则默认情况下该文件夹会被删除。您可以关闭此行为。检查 [设置](docs/settings.md)。
- LiveSync 模式在移动设备上可能导致耗电量增加。建议使用定期同步 + 条件自动同步。
- 移动平台上的 Obsidian 无法连接到非安全 (HTTP) 或本地签名的服务器,即使设备上安装了根证书。
- 没有类似“exclude_folders”的配置。
- 同步时,文件按修改时间进行比较,较旧的将被较新的文件覆盖。然后插件检查冲突,如果需要合并,将打开一个对话框。
- 数据库中的文件在罕见情况下可能会损坏。当接收到的文件看起来已损坏时,插件不会将其写入本地存储。如果您的设备上有文件的本地版本,则可以通过编辑本地文件并进行同步来覆盖损坏的版本。但是,如果您的任何设备上都不存在该文件,则无法挽救该文件。在这种情况下,您可以从设置对话框中删除这些损坏的文件。
- 如果您的数据库看起来已损坏,请尝试 "Drop History"(“删除历史记录”)。通常,这是最简单的方法。
- 要阻止插件的启动流程(例如,为了修复数据库问题),您可以在 vault 的根目录创建一个 "redflag.md" 文件。
- 问:数据库在增长,我该如何缩小它?
答:每个文档都保存了过去 100 次修订,用于检测和解决冲突。想象一台设备已经离线一段时间,然后再次上线。设备必须将其笔记与远程保存的笔记进行比较。如果存在曾经相同的历史修订,则可以安全地直接更新这个文件(和 git 的快进原理一样)。即使文件不在修订历史中,我们也只需检查两个设备上该文件的公有修订版本之后的差异。这就像 git 的冲突解决方法。所以,如果想从根本上解决数据库太大的问题,我们像构建一个扩大版的 git repo 一样去重新设计数据库。
- 更多技术信息在 [技术信息](docs/tech_info.md)
- 如果你想在没有黑曜石的情况下同步文件,你可以使用[filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync)。
- WebClipper 也可在 Chrome Web Store 上使用:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
仓库地址:[obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip) (文档施工中)
## License
The source code is licensed under the MIT License.
本源代码使用 MIT 协议授权。

View File

@@ -47,7 +47,7 @@ NDAや類似の契約や義務、倫理を守る必要のある、研究者、
1. Community Pluginsから、Self-holsted LiveSyncと検索しインストールするか、このリポジトリのReleasesから`main.js`, `manifest.json`, `style.css` をダウンロードしvaultの中の`.obsidian/plugins/obsidian-livesync`に入れて、Obsidianを再起動してください。
2. サーバーを確保します。IBM Cloudantがお手軽かつ堅牢で便利です。完全にセルフホストする際にはお持ちのサーバーにCouchDBをインストールする必要があります。詳しくは下記を参照してください
1. [IBM Cloudantのセットアップ](docs/setup_cloudant_ja.md)
2. [独自のCouchDBのセットアップ](docs/setup_own_server_ja.md) (執筆中)
2. [独自のCouchDBのセットアップ](docs/setup_own_server_ja.md)
3. サーバー情報を入力します。初回のみ、Obsidianを再起動することをオススメします。
設定内容の詳細は[このプラグインの設定](docs/settings_ja.md)を参照してください。
@@ -78,11 +78,14 @@ Self-hosted LiveSync用にWebClipperも作りました。Chrome Web Storeから
- ⚠ エラーが発生しています
- ↑ 送信したデータ数
- ↓ 受信したデータ数
- ⏳ 保留している処理の数です
ファイルを削除したりリネームした場合、この表示が消えるまでお待ちください。
# さらなる補足
- ファイルは同期された後、タイムスタンプを比較して新しければいったん新しい方で上書きされます。その後、衝突が発生したかによって、マージが行われます。
- まれにファイルが破損することがあります。破損したファイルに関してはディスクへの反映を試みないため、実際には使用しているデバイスには少し古いファイルが残っていることが多いです。そのファイルを再度更新してもらうと、データベースが更新されて問題なくなるケースがあります。ファイルがどの端末にも存在しない場合は、設定画面から、削除できます。
- データベースが変。そういうときは、いったんデータベースをDrop Historyのapply and sendで再初期化してみてください。だいたい直ります。
- データベースの復旧中に再起動した場合など、うまくローカルデータベースを修正できない際には、Vaultのトップに`redflag.md`というファイルを置いてください。起動時のシーケンスがスキップされます。
- データベースが大きくなってきてるんだけど、小さくできる→各ートは、それぞれの古い100リビジョンとともに保存されています。例えば、しばらくオフラインだったあるデバイスが、久しぶりに同期したと想定してみてください。そのとき、そのデバイスは最新とは少し異なるリビジョンを持ってるはずです。その場合でも、リモートのリビジョン履歴にリモートのものが存在した場合、安全にマージできます。もしリビジョン履歴に存在しなかった場合、確認しなければいけない差分も、対象を存在して持っている共通のリビジョン以降のみに絞れます。ちょうどGitのような方法で、衝突を解決している形になるのです。そのため、肥大化したリポジトリの解消と同様に、本質的にデータベースを小さくしたい場合は、データベースの作り直しが必要です。
- その他の技術的なお話は、[技術的な内容](docs/tech_info_ja.md)に書いてあります。

View File

@@ -1,5 +1,21 @@
NOTE: This document surely became outdated. I'll improve this doc in a while. but your contributions are always welcome.
# Settings of this plugin
The settings dialog has been quite long, so I split each configuration into tabs.
If you feel something, please feel free to inform me.
| icon | description |
| :---: | ----------------------------------------------------------------- |
| 🛰️ | [Remote Database Configurations](#remote-database-configurations) |
| 📦 | [Local Database Configurations](#local-database-configurations) |
| ⚙️ | [General Settings](#general-settings) |
| 🔁 | [Sync Settings](#sync-settings) |
| 🔧 | [Miscellaneous](#miscellaneous) |
| 🧰 | [Hatch](#miscellaneous) |
| 🔌 | [Plugin and its settings](#plugin-and-its-settings) |
| 🚑 | [Corrupted data](#corrupted-data) |
## Remote Database Configurations
Configure settings of synchronize server. If any synchronization is enabled, you can't edit this section. Please disable all synchronization to change.
@@ -18,65 +34,78 @@ Note: This password is saved into your Obsidian's vault in plain text.
The Database name to synchronize.
If not exist, created automatically.
### Test Database connection
## Local Database Configurations
"Local Database" is created inside your obsidian.
### Batch database update (beta)
Delay database update until raise replication, open another file, window visibility changed, or file events except for file modification.
This option can not be used with LiveSync at the same time.
### Auto Garbage Collection delay
When the note has been modified, Self-hosted LiveSync splits the note into some chunks by parsing the markdown structure. And saving only file information and modified chunks into the Local Database. At this time, do not delete old chunks.
So, Self-hosted LiveSync has to delete old chunks somewhen.
However, the chunk is represented as the crc32 of their contents and shared between all notes. In this way, Self-hosted LiveSync dedupes the entries and keeps low bandwidth and low transfer amounts.
In addition to this, when we edit notes, sometimes back to the previous expression. So It cannot be said that it will be unnecessary immediately.
Therefore, the plugin deletes unused chunks at once when you leave Obsidian for a while (after this setting seconds).
This process is called "Garbage Collection"
As a result, Obsidian's behavior is temporarily slowed down.
Default is 300 seconds.
If you are an early adopter, maybe this value is left as 30 seconds. Please change this value to larger values.
### Manual Garbage Collect
Run "Garbage Collection" manually.
### End to End Encryption
Encrypt your database. It affects only the database, your files are left as plain.
The encryption algorithm is AES-GCM.
Note: If you want to use "Plugins and their settings", you have to enable this.
### Passphrase
The passphrase to used as the key of encryption. Please use the long text.
### Apply
To enable End-to-End encryption, there must be no items of the same content encrypted with different passphrases to avoid attackers guessing passphrases. Self-hosted LiveSync uses crc32 of the chunks, It is really a must.
Set the End to End encryption enabled and its passphrase for use in replication.
If you change the passphrase of a existing database, overwriting the remote database is strongly recommended.
So, this plugin completely deletes everything from both local and remote databases before enabling it and then synchronizing again.
To enable, "Apply and send" from the most powerful device, and "Apply and receive" from every other device.
### Overwrite remote database
Overwrite the remote database by the local database using the passphrase you applied.
- Apply and send
1. Initialize the Local Database and set (or clear) passphrase, put all files into the database again.
2. Initialize the Remote Database.
3. Lock the Remote Database.
4. Send it all.
This process is simply heavy. Using a PC or Mac is preferred.
- Apply and receive
1. Initialize the Local Database and set (or clear) the passphrase.
2. Unlock the Remote Database.
3. Retrieve all and decrypt to file.
### Rebuild
Rebuild remote and local databases with local files. It will delete all document history and retained chunks, and shrink the database.
When running these operations, every synchronization settings is disabled.
### Test Database connection
You can check the connection by clicking this button.
**And even your passphrase is wrong, It doesn't be checked before the plugin really decrypts. So If you set the wrong passphrase and run "Apply and Receive", you will get an amount of decryption error. But, this is the specification.**
### Check database configuration
You can check and modify your CouchDB's configuration from here directly.
### Lock remote database.
Other devices are banned from the database when you have locked the database.
If you have something troubled with other devices, you can protect the vault and remote database by your device.
## Local Database Configurations
"Local Database" is created inside your obsidian.
### Batch database update
Delay database update until raise replication, open another file, window visibility changed, or file events except for file modification.
This option can not be used with LiveSync at the same time.
### Garbage check
This plugin saves the file by splitting it into chunks to speed replication up and keep low bandwidth.
They share the chunk if you use the same paragraph in some notes. And if you change the file, only the paragraph you changed is transferred with metadata of the file. And I know that editing notes are not so straight. Sometimes paragraphs will be back into an old phrase. In these cases, we do not have to transfer the chunk again if the chunk will not be deleted. So all chunks will be reused.
As the side effect of this, you can see history the file.
The check will show the number of chunks used or retained. If there are so many retained chunks, you can rebuild the database.
### Fetch rebuilt DB.
If one device rebuilds or locks the remote database, every other device will be locked out from the remote database until it fetches rebuilt DB.
### minimum chunk size and LongLine threshold
The configuration of chunk splitting.
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than "Minimum chunk size".
Specifically, the length of the chunk is determined by the following orders.
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
2. If not, find nearest to these items.
1. Newline character
2. Empty line (Windows style)
3. Empty line (non-Windows style)
3. Compare the farther in these 3 positions and next "\[newline\]#" position, pick a shorter piece to as chunk.
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
You can dump saved note structure to `Dump informations of this doc`. Replace every character to x except newline and "#" when sending information to me.
Default values are 20 letters and 250 letters.
## General Settings
@@ -85,7 +114,7 @@ If you enable this option, log only the entries with the popup.
### Verbose log
## Sync setting
## Sync Settings
### LiveSync
Do LiveSync.
@@ -122,32 +151,77 @@ Self-hosted LiveSync will delete the folder when the folder becomes empty. If th
### Use newer file if conflicted (beta)
Always use the newer file to resolve and overwrite when conflict has occurred.
### minimum chunk size and LongLine threshold
The configuration of chunk splitting.
Self-hosted LiveSync splits the note into chunks for efficient synchronization. This chunk should be longer than "Minimum chunk size".
### Experimental.
### Sync hidden files
Specifically, the length of the chunk is determined by the following orders.
Synchronize hidden files.
1. Find the nearest newline character, and if it is farther than LongLineThreshold, this piece becomes an independent chunk.
- Scan hidden files before replication.
If you enable this option, all hidden files are scanned once before replication.
2. If not, find nearest to these items.
1. Newline character
2. Empty line (Windows style)
3. Empty line (non-Windows style)
3. Compare the farther in these 3 positions and next "\[newline\]#" position, pick a shorter piece to as chunk.
- Scan hidden files periodicaly.
If you enable this option, all hidden files will be scanned each [n] seconds.
This rule was made empirically from my dataset. If this rule acts as badly on your data. Please give me the information.
Hidden files are not actively detected, so we need scanning.
You can dump saved note structure to `Dump informations of this doc`. Replace every character to x except newline and "#" when sending information to me.
Each scan stores the file with their modification time. And if the file has been disappeared, the fact is also stored. Then, When the entry of the hidden file has been replicated, it will be reflected in the storage if the entry is newer than storage.
Default values are 20 letters and 250 letters.
Therefore, the clock must be adjusted. If the modification time is determined to be older, the changeset will be skipped or cancelled (It means, **deleted**), even if the file spawned in a hidden folder.
### Advanced settings
Self-hosted LiveSync using PouchDB and synchronizes with the remote by [this protocol](https://docs.couchdb.org/en/stable/replication/protocol.html).
So, it splits every entry into chunks to be acceptable by the database with limited payload size and document size.
However, it was not enough.
According to [2.4.2.5.2. Upload Batch of Changed Documents](https://docs.couchdb.org/en/stable/replication/protocol.html#upload-batch-of-changed-documents) in [Replicate Changes](https://docs.couchdb.org/en/stable/replication/protocol.html#replicate-changes), it might become a bigger request.
Unfortunately, there is no way to deal with this automatically by size for every request.
Therefore, I made it possible to configure this.
Note: If you set these values lower number, the number of requests will increase.
Therefore, if you are far from the server, the total throughput will be low, and the traffic will increase.
### Batch size
Number of change feed items to process at a time. Defaults to 250.
### Batch limit
Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.
## Miscellaneous
### Show status inside editor
Show information inside the editor pane.
It would be useful for mobile.
### Check integrity on saving
Check all chunks are correctly saved on saving.
### Presets
You can set synchronization method at once as these pattern:
- LiveSync
- LiveSync : enabled
- Batch database update : disabled
- Periodic Sync : disabled
- Sync on Save : disabled
- Sync on File Open : disabled
- Sync on Start : disabled
- Periodic w/ batch
- LiveSync : disabled
- Batch database update : enabled
- Periodic Sync : enabled
- Sync on Save : disabled
- Sync on File Open : enabled
- Sync on Start : enabled
- Disable all sync
- LiveSync : disabled
- Batch database update : disabled
- Periodic Sync : disabled
- Sync on Save : disabled
- Sync on File Open : disabled
- Sync on Start : disabled
## Hatch
From here, everything is under the hood. Please handle it with care.
@@ -165,6 +239,12 @@ The remote database indicates that has been unlocked Pattern 1.
When you mark all devices as resolved, you can unlock the database.
But, there's no problem even if you leave it as it is.
### Verify and repair all files
read all files in the vault, and update them into the database if there's diff or could not read from the database.
### Sanity check
Make sure that all the files on the local database have all chunks.
### Drop history
Drop all histories on the local database and the remote database, and initialize When synchronization time has been prolonged to the new device or new vault, or database size became to be much larger. Try this.
@@ -187,11 +267,6 @@ Same as a setting passphrase, database locking is also performed.
3. Retrieve all and decrypt to file.
### Lock remote database
Lock the remote database to ban out other devices for synchronization. It is the same as the database lock that happened in dropping databases or applying passphrases.
Use it as an emergency evacuation method to protect local or remote databases when synchronization has been broken.
### Suspend file watching
If enable this option, Self-hosted LiveSync dismisses every file change or deletes the event.
@@ -208,9 +283,27 @@ Discard the data stored in the local database.
### Initialize local database again
Discard the data stored in the local database and initialize and create the database from the files on storage.
### Corrupted data
## Plugins and settings (beta)
### Enable plugin synchronization
If you want to use this feature, you have to activate this feature by this switch.
### Sweep plugins automatically
Plugin sweep will run before replication automatically.
### Sweep plugins periodically
Plugin sweep will run each 1 minute.
### Notify updates
When replication is complete, a message will be notified if a newer version of the plugin applied to this device is configured on another device.
### Device and Vault name
To save the plugins, you have to set a unique name every each device.
### Open
Open the "Plugins and their settings" dialog.
### Corrupted or missing data
![CorruptedData](../images/corrupted_data.png)
When Self-hosted LiveSync could not write to the file on the storage, the files are shown here. If you have the old data in your vault, change it once, it will be cured. Or you can use the "File History" plugin.
But if you don't, sorry, we can't rescue the file, and error messages are shown frequently, and you have to delete the file from here.

View File

@@ -1,3 +1,5 @@
注意:少し内容が古くなっています。
# このプラグインの設定項目
## Remote Database Configurations
@@ -19,13 +21,48 @@ CouchDBのURIを入力します。Cloudantの場合は「External Endpoint(prefe
⚠️存在しない場合は、テストや接続を行った際、自動的に作成されます[^1]。
[^1]:権限がない場合は自動作成には失敗します。
### End to End Encryption
データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。
暗号化はAES-GCMを使用して行っています。
### Passphrase
暗号化を行う際に使用するパスフレーズです。充分に長いものを使用してください。
### Apply
End to End 暗号化を行うに当たって、異なるパスフレーズで暗号化された同一の内容を入手されることは避けるべきです。また、Self-hosted LiveSyncはコンテンツのcrc32を重複回避に使用しているため、その点でも攻撃が有効になってしまいます。
そのため、End to End 暗号化を有効にする際には、ローカル、リモートすべてのデータベースをいったん破棄し、新しいパスフレーズで暗号化された内容のみを、改めて同期し直します。
有効化するには、一番体力のある端末からApply and sendを行います。
既に存在するリモートと同期する場合は、設定してJust applyを行ってください。
- Apply and send
1. ローカルのデータベースを初期化しパスフレーズを設定(またはクリア)します。その後、すべてのファイルをもう一度データベースに登録します。
2. リモートのデータベースを初期化します。
3. リモートのデータベースをロックし、他の端末を締め出します。
4. すべて再送信します。
負荷と時間がかかるため、デスクトップから行う方が好ましいです。
- Apply and receive
1. ローカルのデータベースを初期化し、パスフレーズを設定(またはクリア)します。
2. リモートのデータベースにかかっているロックを解除します。
3. すべて受信して、復号します。
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
### Test Database connection
上記の設定でデータベースに接続できるか確認します。
### Check database configuration
ここから直接CouchDBの設定を確認・変更できます。
## Local Database Configurations
端末内に作成されるデータベースの設定です。
### Batch database update (beta)
### Batch database update
データベースの更新を以下の事象が発生するまで遅延させます。
- レプリケーションが発生する
- 他のファイルを開く
@@ -49,34 +86,23 @@ Obsidianでのファイル操作が終わってから指定秒数が経過した
### Manual Garbage Collect
上記のGarbage Collectionを手動で行います。
### End to End Encryption
データベースを暗号化します。この効果はデータベースに格納されるデータに限られ、ディスク上のファイルは平文のままです。
暗号化はAES-GCMを使用して行っています。
### minimum chunk size と LongLine threshold
チャンクの分割についての設定です。
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ートを分割してチャンクを作成します。
これは、同期を行う際に、一定の文字数で分割した場合、先頭の方を編集すると、その後の分割位置がすべてずれ、結果としてほぼまるごとのファイルのファイル送受信を行うことになっていた問題を避けるために実装されました。
具体的には、先頭から順に直近の下記の箇所を検索し、一番長く切れたものを一つのチャンクとします。
### Passphrase
暗号化を行う際に使用するパスフレーズです。充分に長いものを使用してください。
1. 次の改行を探し、それがLongLine Thresholdより先であれば、一つのチャンクとして確定します。
### Apply
End to End 暗号化を行うに当たって、異なるパスフレーズで暗号化された同一の内容を入手されることは避けるべきです。また、Self-hosted LiveSyncはコンテンツのcrc32を重複回避に使用しているため、その点でも攻撃が有効になってしまいます。
2. そうではない場合は、下記を順に探します。
1. 改行
2. windowsでの空行がある所
3. 非Windowsでの空行がある所
3. この三つのうち一番遠い場所と、 「改行後、#から始まる所」を比べ、短い方をチャンクとします。
そのため、End to End 暗号化を有効にする際には、ローカル、リモートすべてのデータベースをいったん破棄し、新しいパスフレーズで暗号化された内容のみを、改めて同期し直します。
有効化するには、一番体力のある端末からApply and sendを行い、他の端末でApply and receiveを行います。
- Apply and send
1. ローカルのデータベースを初期化しパスフレーズを設定(またはクリア)します。その後、すべてのファイルをもう一度データベースに登録します。
2. リモートのデータベースを初期化します。
3. リモートのデータベースをロックし、他の端末を締め出します。
4. すべて再送信します。
負荷と時間がかかるため、デスクトップから行う方が好ましいです。
- Apply and receive
1. ローカルのデータベースを初期化し、パスフレーズを設定(またはクリア)します。
2. リモートのデータベースにかかっているロックを解除します。
3. すべて受信して、復号します。
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
**また、パスフレーズのチェックは、実際に復号するまで行いません。そのため、パスフレーズを間違えて設定し、Apply and receiveで同期を行うと、大量のエラーが発生します。これは仕様です。**
このルールは経験則的に作りました。実データが偏っているため。もし思わぬ挙動をしている場合は、是非コマンドから`Dump informations of this doc`を選択し、情報をください。
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
デフォルトは20文字と、250文字です。
## General Settings
一般的な設定です。
@@ -124,30 +150,58 @@ Self-hosted LiveSyncは通常、フォルダ内のファイルがすべて削除
### Use newer file if conflicted (beta)
競合が発生したとき、常に新しいファイルを使用して競合を自動的に解決します。
### minimum chunk size と LongLine threshold
チャンクの分割についての設定です。
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ートを分割してチャンクを作成します。
これは、同期を行う際に、一定の文字数で分割した場合、先頭の方を編集すると、その後の分割位置がすべてずれ、結果としてほぼまるごとのファイルのファイル送受信を行うことになっていた問題を避けるために実装されました。
具体的には、先頭から順に直近の下記の箇所を検索し、一番長く切れたものを一つのチャンクとします。
1. 次の改行を探し、それがLongLine Thresholdより先であれば、一つのチャンクとして確定します。
### Experimental.
### Sync hidden files
2. そうではない場合は、下記を順に探します
1. 改行
2. windowsでの空行がある所
3. 非Windowsでの空行がある所
3. この三つのうち一番遠い場所と、 「改行後、#から始まる所」を比べ、短い方をチャンクとします。
隠しファイルを同期します
このルールは経験則的に作りました。実データが偏っているため。もし思わぬ挙動をしている場合は、是非コマンドから`Dump informations of this doc`を選択し、情報をください。
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
デフォルトは20文字と、250文字です。
- Scan hidden files before replication.
このオプション有効にすると、レプリケーションを実行する前に隠しファイルをスキャンします。
- Scan hidden files periodicaly.
このオプションを有効にすると、n秒おきに隠しファイルをスキャンします。
隠しファイルは能動的に検出されないため、スキャンが必要です。
スキャンでは、ファイルと共にファイルの変更時刻を保存します。もしファイルが消された場合は、その事実も保存します。このファイルを記録したエントリーがレプリケーションされた際、ストレージよりも新しい場合はストレージに反映されます。
そのため、端末のクロックは時刻合わせされている必要があります。ファイルが隠しフォルダに生成された場合でも、もし変更時刻が古いと判断された場合はスキップされるかキャンセル(つまり、削除)されます。
Each scan stores the file with their modification time. And if the file has been disappeared, the fact is also stored. Then, When the entry of the hidden file has been replicated, it will be reflected in the storage if the entry is newer than storage.
Therefore, the clock must be adjusted. If the modification time is old, the changeset will be skipped or cancelled (It means, **deleted**), even if the file spawned in a hidden folder.
### Advanced settings
Self-hosted LiveSyncはPouchDBを使用し、リモートと[このプロトコル](https://docs.couchdb.org/en/stable/replication/protocol.html)で同期しています。
そのため、全てのノートなどはデータベースが許容するペイロードサイズやドキュメントサイズに併せてチャンクに分割されています。
しかしながら、それだけでは不十分なケースがあり、[Replicate Changes](https://docs.couchdb.org/en/stable/replication/protocol.html#replicate-changes)の[2.4.2.5.2. Upload Batch of Changed Documents](https://docs.couchdb.org/en/stable/replication/protocol.html#upload-batch-of-changed-documents)を参照すると、このリクエストは巨大になる可能性がありました。
残念ながら、このサイズを呼び出しごとに自動的に調整する方法はありません。
そのため、設定を変更できるように機能追加いたしました。
備考:もし小さな値を設定した場合、リクエスト数は増えます。
もしサーバから遠い場合、トータルのスループットは遅くなり、転送量は増えます。
### Batch size
一度に処理するChange feedの数です。デフォルトは250です。
### Batch limit
一度に処理するBatchの数です。デフォルトは40です。
## Miscellaneous
その他の設定です
### Show status inside editor
同期の情報をエディター内に表示します。
同期の情報をエディター内に表示します。
モバイルで便利です。
### Check integrity on saving
保存時にデータが全て保存できたかチェックを行います。
## Hatch
ここから先は、困ったときに開ける蓋の中身です。注意して使用してください。
@@ -166,6 +220,12 @@ Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk s
ご使用のすべてのデバイスでロックを解除した場合は、データベースのロックを解除することができます。
ただし、このまま放置しても問題はありません。
### Verify and repair all files
Vault内のファイルを全て読み込み直し、もし差分があったり、データベースから正常に読み込めなかったものに関して、データベースに反映します。
### Sanity check
ローカルデータベースに保存されている全てのファイルが正しくチャンクを持っていることを確認します。
### Drop history
データベースに記録されている履歴を削除し、データベースを初期化します。
新しい端末や新しいVaultへの同期にやたらと時間がかかったり、データベースサイズが肥大化したりしてきた際に使用してください。

View File

@@ -71,7 +71,7 @@ In these instructions, create IBM Cloudant Instance for trial.
follow the figure, it's
"apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup> and "c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup>
## Self-hosted LiveSync setting
## Self-hosted LiveSync settings
![Setting](../images/remote_db_setting.png)

View File

@@ -1,3 +1,95 @@
# Setup CouchDB to your server
Coming soon!
## Install CouchDB and access from PC or Mac
The easiest way to set up the CouchDB is using the [docker image]((https://hub.docker.com/_/couchdb)).
But some additional configurations are required in `local.ini` to use from Self-hosted LiveSync, like below:
```
[couchdb]
single_node=true
[chttpd]
require_valid_user = true
[chttpd_auth]
require_valid_user = true
authentication_redirect = /_utils/session.html
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
[cors]
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
headers = accept, authorization, content-type, origin, referer
methods = GET, PUT, POST, HEAD, DELETE
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
```
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)
```
$ 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
```
## Access from mobile device
If you want to access Self-hosted LiveSync from mobile devices, you need a valid SSL certificate.
### Testing from mobile
In the testing phase, [localhost.run](http://localhost.run/) or something like services is very useful.
example on using localhost.run)
```
$ ssh -R 80:localhost:5984 nokey@localhost.run
Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts.
===============================================================================
Welcome to localhost.run!
Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
**You need a SSH key to access this service.**
If you get a permission denied follow Gitlab's most excellent howto:
https://docs.gitlab.com/ee/ssh/
*Only rsa and ed25519 keys are supported*
To set up and manage custom domains go to https://admin.localhost.run/
More details on custom domains (and how to enable subdomains of your custom
domain) at https://localhost.run/docs/custom-domains
To explore using localhost.run visit the documentation site:
https://localhost.run/docs/
===============================================================================
** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. **
xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run
Connection to localhost.run closed by remote host.
Connection to localhost.run closed.
```
https://xxxxxxxx.localhost.run is the temporary server address.
### Setting up your domain
Set the A record of your domain to point to your server, and host reverse proxy as you like.
Note: Mounting CouchDB on the top directory is not recommended.
Using Caddy is a handy way to serve the server with SSL automatically.
I have published [docker-compose.yml and ini files](https://github.com/vrtmrz/self-hosted-livesync-server) that launches Caddy and CouchDB at once. Please try it out.
And, be sure to check the server log and be careful of malicious access.

View File

@@ -0,0 +1,95 @@
# 在你自己的服务器上设置 CouchDB
> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档)
## 安装 CouchDB 并从 PC 或 Mac 上访问
设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)).
需要修改一些 `local.ini` 中的配置,以让它可以用于 Self-hosted LiveSync如下
```
[couchdb]
single_node=true
[chttpd]
require_valid_user = true
[chttpd_auth]
require_valid_user = true
authentication_redirect = /_utils/session.html
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
[cors]
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
headers = accept, authorization, content-type, origin, referer
methods = GET, PUT, POST, HEAD, DELETE
max_age = 3600
```
创建 `local.ini` 并用如下指令启动 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
```
Note: 此时 local.ini 的文件所有者会变成 5984:5984。这是 docker 镜像的限制,请修改文件所有者后再编辑 local.ini。
在确定 Self-hosted LiveSync 可以和服务器同步后,可以后台启动 docker 镜像:
```
$ 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
```
## 从移动设备访问
如果你想要从移动设备访问 Self-hosted LiveSync你需要一个合法的 SSL 证书。
### 移动设备测试
测试时,[localhost.run](http://localhost.run/) 这一类的反向隧道服务很实用。(非必须,只是用于终端设备不方便 ssh 的时候的备选方案)
```
$ ssh -R 80:localhost:5984 nokey@localhost.run
Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts.
===============================================================================
Welcome to localhost.run!
Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
**You need a SSH key to access this service.**
If you get a permission denied follow Gitlab's most excellent howto:
https://docs.gitlab.com/ee/ssh/
*Only rsa and ed25519 keys are supported*
To set up and manage custom domains go to https://admin.localhost.run/
More details on custom domains (and how to enable subdomains of your custom
domain) at https://localhost.run/docs/custom-domains
To explore using localhost.run visit the documentation site:
https://localhost.run/docs/
===============================================================================
** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. **
xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run
Connection to localhost.run closed by remote host.
Connection to localhost.run closed.
```
https://xxxxxxxx.localhost.run 即为临时服务器地址。
### 设置你的域名
设置一个指向你服务器的 A 记录,并根据需要设置反向代理。
Note: 不推荐将 CouchDB 挂载到根目录
可以使用 Caddy 很方便的给服务器加上 SSL 功能
提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。
注意检查服务器日志,当心恶意访问。

View File

@@ -1,3 +1,91 @@
# CouchDB のセットアップ方法
# CouchDBのセットアップ方法
早めに作成します!
## CouchDBのインストールとPCやMacでの使用
CouchDBを構築するには、[Dockerのイメージ](https://hub.docker.com/_/couchdb)を使用するのが一番簡単です。
ただし、インストールしたCouchDBをSelf-hosted LiveSyncから使用するためには、少々設定が必要となります。
具体的には、下記の設定が`local.ini`として必要になります。
```
[couchdb]
single_node=true
[chttpd]
require_valid_user = true
[chttpd_auth]
require_valid_user = true
authentication_redirect = /_utils/session.html
[httpd]
WWW-Authenticate = Basic realm="couchdb"
enable_cors = true
[cors]
origins = app://obsidian.md,capacitor://localhost,http://localhost
credentials = true
headers = accept, authorization, content-type, origin, referer
methods = GET, PUT, POST, HEAD, DELETE
max_age = 3600
```
このファイルを作成し、
```
$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v .local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb
```
とすると簡単にCouchDBを起動することができます。
備考このとき、local.iniのオーナーが5984:5984になります。これは、Dockerイメージの制限事項です。編集する場合はいったんオーナーを変更してください。
正常にSelf-hosted LiveSyncからアクセスすることができたら、お好みでバックグラウンドで起動するように編集して起動してください。
例)
```
$ 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
```
## モバイルからのアクセス
MacやPCからアクセスする場合は上記の方法で作ったサーバーで問題ありませんが、モバイル端末からアクセスする場合は有効なSSLの証明書が必要となります。
### モバイルからのアクセスのテスト
テストを行う場合は、[localhost.run](http://localhost.run/)などのサービスが便利です。
```
$ ssh -R 80:localhost:5984 nokey@localhost.run
Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts.
===============================================================================
Welcome to localhost.run!
Follow your favourite reverse tunnel at [https://twitter.com/localhost_run].
**You need a SSH key to access this service.**
If you get a permission denied follow Gitlab's most excellent howto:
https://docs.gitlab.com/ee/ssh/
*Only rsa and ed25519 keys are supported*
To set up and manage custom domains go to https://admin.localhost.run/
More details on custom domains (and how to enable subdomains of your custom
domain) at https://localhost.run/docs/custom-domains
To explore using localhost.run visit the documentation site:
https://localhost.run/docs/
===============================================================================
** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. **
xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run
Connection to localhost.run closed by remote host.
Connection to localhost.run closed.
```
このように表示された場合、`https://xxxxxxxx.localhost.run`が一時的なサーバアドレスとして使用できます。
### ドメインを設定してアクセスする。
DNSのAレコードを設定し、お好みの方法でリバースプロキシをホスティングしてください。
備考:トップディレクトリにCouchDBを露出させるのはおすすめしません。
Caddy等でLet's Encryptの証明書を自動取得すると運用が楽になります。
CaddyとCouchDBを同時に立てられる[docker-composeの設定とiniファイル](https://github.com/vrtmrz/self-hosted-livesync-server)を公開しています。
ぜひご利用下さい。
なお、サーバのログは必ず確認し、不正なアクセスに注意してください。

View File

@@ -9,8 +9,8 @@
3. Another device is watching remote CouchDB's changes, so retrieve new changes.
4. Self-hosted LiveSync reflects replicated changeset into Obsidian's vault.
Note: The figure is drawn as single-directional, between two devices. But everything occurs bi-directionally between many devices at once in real.
Note: The figure is drawn as single-directional, between two devices for demonstration purposes. Everything actually occurs bi-directionally between many devices at the same time.
## Techniques to keep bandwidth low.
## Techniques to keep bandwidth consumption low.
![dedupe](../images/2.png)

44
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,44 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import sveltePlugin from "esbuild-svelte";
import sveltePreprocess from "svelte-preprocess";
import fs from "node:fs";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
const manifestJson = JSON.parse(fs.readFileSync("./manifest.json"));
const packageJson = JSON.parse(fs.readFileSync("./package.json"));
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
esbuild
.build({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
define: {
"MANIFEST_VERSION": `"${manifestJson.version}"`,
"PACKAGE_VERSION": `"${packageJson.version}"`,
"UPDATE_INFO": `${updateInfo}`,
},
external: ["obsidian", "electron", ...builtins],
format: "cjs",
watch: !prod,
target: "es2015",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
plugins: [
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: { css: true },
}),
],
outfile: "main.js",
})
.catch(() => process.exit(1));

3705
main.ts

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.1.23",
"version": "0.13.4",
"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",
"authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false
}
}

5234
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
{
"name": "obsidian-livesync",
"version": "0.1.23",
"version": "0.13.4",
"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",
"scripts": {
"dev": "rollup --config rollup.config.js -w",
"build": "rollup --config rollup.config.js --environment BUILD:production"
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production",
"lint": "eslint src"
},
"keywords": [],
"author": "vorotamoroz",
@@ -15,14 +17,30 @@
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-typescript": "^8.2.1",
"@types/diff-match-patch": "^1.0.32",
"@types/pouchdb": "^6.4.0",
"@types/pouchdb-browser": "^6.1.3",
"obsidian": "^0.12.0",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.0.0",
"builtin-modules": "^3.2.0",
"esbuild": "0.13.12",
"esbuild-svelte": "^0.7.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.25.2",
"obsidian": "^0.15.4",
"postcss": "^8.4.14",
"postcss-load-config": "^3.1.4",
"rollup": "^2.32.1",
"svelte": "^3.49.0",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.2.0",
"typescript": "^4.2.4"
},
"dependencies": {
"diff-match-patch": "^1.0.5",
"esbuild": "0.13.12",
"esbuild-svelte": "^0.7.0",
"idb": "^7.0.2",
"xxhash-wasm": "^0.4.2"
}
}
}

View File

@@ -1,2 +1,4 @@
# PouchDB-browser
just webpacked.
Just webpacked.
(Rollup couldn't pack pouchdb-browser into browser bundle)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,25 @@
{
"name": "pouchdb-browser-webpack",
"version": "1.0.0",
"description": "pouchdb-browser webpack",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode=production --node-env=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"watch": "webpack --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"pouchdb-browser": "^7.2.2"
},
"devDependencies": {
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
}
"name": "pouchdb-browser-webpack",
"version": "1.0.0",
"description": "pouchdb-browser webpack",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode=production --node-env=production",
"build:dev": "webpack --mode=development",
"build:prod": "webpack --mode=production --node-env=production",
"watch": "webpack --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"pouchdb-browser": "^7.3.0",
"transform-pouch": "^2.0.0",
"pouchdb-find": "^7.3.0"
},
"devDependencies": {
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
}
}

View File

@@ -1,4 +1,10 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-var-requires */
// This module just webpacks pouchdb-browser
import * as PouchDB_src from "pouchdb-browser";
const PouchDB = PouchDB_src.default;
// import * as PouchDB_src from "pouchdb-browser";
const pouch = require("pouchdb-browser").default;
const find = require("pouchdb-find").default;
const transform = require("transform-pouch");
const PouchDB = pouch.plugin(find).plugin(transform);
export { PouchDB };

View File

@@ -11,7 +11,7 @@ if you want to view the source visit the plugins github repository
`;
export default {
input: "main.ts",
input: "./src/main.ts",
output: {
dir: ".",
sourcemap: "inline",

View File

@@ -0,0 +1,81 @@
import { App, Modal } from "obsidian";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
import { diff_result } from "./lib/src/types";
import { escapeStringToHTML } from "./lib/src/utils";
export class ConflictResolveModal extends Modal {
// result: Array<[number, string]>;
result: diff_result;
callback: (remove_rev: string) => Promise<void>;
constructor(app: App, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
super(app);
this.result = diff;
this.callback = callback;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "This document has conflicted changes." });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
let diff = "";
for (const v of this.result.diff) {
const x1 = v[0];
const x2 = v[1];
if (x1 == DIFF_DELETE) {
diff += "<span class='deleted'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_EQUAL) {
diff += "<span class='normal'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_INSERT) {
diff += "<span class='added'>" + escapeStringToHTML(x2) + "</span>";
}
}
diff = diff.replace(/\n/g, "<br>");
div.innerHTML = diff;
const div2 = contentEl.createDiv("");
const date1 = new Date(this.result.left.mtime).toLocaleString();
const date2 = new Date(this.result.right.mtime).toLocaleString();
div2.innerHTML = `
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
`;
contentEl.createEl("button", { text: "Keep A" }, (e) => {
e.addEventListener("click", async () => {
await this.callback(this.result.right.rev);
this.callback = null;
this.close();
});
});
contentEl.createEl("button", { text: "Keep B" }, (e) => {
e.addEventListener("click", async () => {
await this.callback(this.result.left.rev);
this.callback = null;
this.close();
});
});
contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", async () => {
await this.callback("");
this.callback = null;
this.close();
});
});
contentEl.createEl("button", { text: "Not now" }, (e) => {
e.addEventListener("click", () => {
this.close();
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
if (this.callback != null) {
this.callback(null);
}
}
}

186
src/DocumentHistoryModal.ts Normal file
View File

@@ -0,0 +1,186 @@
import { TFile, Modal, App } from "obsidian";
import { path2id } from "./utils";
import { base64ToArrayBuffer, base64ToString, escapeStringToHTML, isValidPath } from "./lib/src/utils";
import ObsidianLiveSyncPlugin from "./main";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import { LoadedEntry, LOG_LEVEL } from "./lib/src/types";
import { Logger } from "./lib/src/logger";
export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
range: HTMLInputElement;
contentView: HTMLDivElement;
info: HTMLDivElement;
fileInfo: HTMLDivElement;
showDiff = false;
file: string;
revs_info: PouchDB.Core.RevisionInfo[] = [];
currentDoc: LoadedEntry;
currentText = "";
currentDeleted = false;
constructor(app: App, plugin: ObsidianLiveSyncPlugin, file: TFile | string) {
super(app);
this.plugin = plugin;
this.file = (file instanceof TFile) ? file.path : file;
if (localStorage.getItem("ols-history-highlightdiff") == "1") {
this.showDiff = true;
}
}
async loadFile() {
const db = this.plugin.localDatabase;
const w = await db.localDatabase.get(path2id(this.file), { revs_info: true });
this.revs_info = w._revs_info.filter((e) => e.status == "available");
this.range.max = `${this.revs_info.length - 1}`;
this.range.value = this.range.max;
this.fileInfo.setText(`${this.file} / ${this.revs_info.length} revisions`);
await this.loadRevs();
}
async loadRevs() {
const db = this.plugin.localDatabase;
const index = this.revs_info.length - 1 - (this.range.value as any) / 1;
const rev = this.revs_info[index];
const w = await db.getDBEntry(path2id(this.file), { rev: rev.rev }, false, false, true);
this.currentText = "";
this.currentDeleted = false;
if (w === false) {
this.currentDeleted = true;
this.info.innerHTML = "";
this.contentView.innerHTML = `Could not read this revision<br>(${rev.rev})`;
} else {
this.currentDoc = w;
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
let result = "";
const w1data = w.datatype == "plain" ? w.data : base64ToString(w.data);
this.currentDeleted = w.deleted;
this.currentText = w1data;
if (this.showDiff) {
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
const oldRev = this.revs_info[prevRevIdx].rev;
const w2 = await db.getDBEntry(path2id(this.file), { rev: oldRev }, false, false, true);
if (w2 != false) {
const dmp = new diff_match_patch();
const w2data = w2.datatype == "plain" ? w2.data : base64ToString(w2.data);
const diff = dmp.diff_main(w2data, w1data);
dmp.diff_cleanupSemantic(diff);
for (const v of diff) {
const x1 = v[0];
const x2 = v[1];
if (x1 == DIFF_DELETE) {
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_EQUAL) {
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
} else if (x1 == DIFF_INSERT) {
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
}
}
result = result.replace(/\n/g, "<br>");
} else {
result = escapeStringToHTML(w1data);
}
} else {
result = escapeStringToHTML(w1data);
}
} else {
result = escapeStringToHTML(w1data);
}
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
}
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Document History" });
this.fileInfo = contentEl.createDiv("");
this.fileInfo.addClass("op-info");
const divView = contentEl.createDiv("");
divView.addClass("op-flex");
divView.createEl("input", { type: "range" }, (e) => {
this.range = e;
e.addEventListener("change", (e) => {
this.loadRevs();
});
e.addEventListener("input", (e) => {
this.loadRevs();
});
});
contentEl
.createDiv("", (e) => {
e.createEl("label", {}, (label) => {
label.appendChild(
createEl("input", { type: "checkbox" }, (checkbox) => {
if (this.showDiff) {
checkbox.checked = true;
}
checkbox.addEventListener("input", (evt: any) => {
this.showDiff = checkbox.checked;
localStorage.setItem("ols-history-highlightdiff", this.showDiff == true ? "1" : "");
this.loadRevs();
});
})
);
label.appendText("Highlight diff");
});
})
.addClass("op-info");
this.info = contentEl.createDiv("");
this.info.addClass("op-info");
this.loadFile();
const div = contentEl.createDiv({ text: "Loading old revisions..." });
this.contentView = div;
div.addClass("op-scrollable");
div.addClass("op-pre");
const buttons = contentEl.createDiv("");
buttons.createEl("button", { text: "Copy to clipboard" }, (e) => {
e.addClass("mod-cta");
e.addEventListener("click", async () => {
await navigator.clipboard.writeText(this.currentText);
Logger(`Old content copied to clipboard`, LOG_LEVEL.NOTICE);
});
});
async function focusFile(path: string) {
const targetFile = app.vault
.getFiles()
.find((f) => f.path === path);
if (targetFile) {
const leaf = app.workspace.getLeaf(false);
await leaf.openFile(targetFile);
} else {
Logger("The file cound not view on the editor", LOG_LEVEL.NOTICE)
}
}
buttons.createEl("button", { text: "Back to this revision" }, (e) => {
e.addClass("mod-cta");
e.addEventListener("click", async () => {
const pathToWrite = this.file.startsWith("i:") ? this.file.substring("i:".length) : this.file;
if (!isValidPath(pathToWrite)) {
Logger("Path is not vaild to write content.", LOG_LEVEL.INFO);
}
if (this.currentDoc?.datatype == "plain") {
await this.app.vault.adapter.write(pathToWrite, this.currentDoc.data);
await focusFile(pathToWrite);
this.close();
} else if (this.currentDoc?.datatype == "newnote") {
await this.app.vault.adapter.writeBinary(pathToWrite, base64ToArrayBuffer(this.currentDoc.data));
await focusFile(pathToWrite);
this.close();
} else {
Logger(`Could not parse entry`, LOG_LEVEL.NOTICE);
}
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

52
src/KeyValueDB.ts Normal file
View File

@@ -0,0 +1,52 @@
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 = async (dbKey: string): Promise<KeyValueDatabase> => {
if (dbKey in databaseCache) {
databaseCache[dbKey].close();
delete databaseCache[dbKey];
}
const storeKey = dbKey;
const dbPromise = openDB(dbKey, 1, {
upgrade(db) {
db.createObjectStore(storeKey);
},
});
let db: IDBPDatabase<any> = null;
db = await dbPromise;
databaseCache[dbKey] = db;
return {
get<T>(key: string): Promise<T> {
return db.get(storeKey, key);
},
set<T>(key: string, value: T) {
return db.put(storeKey, value, key);
},
del(key: string) {
return db.delete(storeKey, key);
},
clear() {
return db.clear(storeKey);
},
keys(query?: IDBValidKey | IDBKeyRange, count?: number) {
return db.getAllKeys(storeKey, query, count);
},
close() {
delete databaseCache[dbKey];
return db.close();
},
async destroy() {
delete databaseCache[dbKey];
db.close();
await deleteDB(dbKey);
},
};
};

1296
src/LocalPouchDB.ts Normal file

File diff suppressed because it is too large Load Diff

37
src/LogDisplayModal.ts Normal file
View File

@@ -0,0 +1,37 @@
import { App, Modal } from "obsidian";
import { escapeStringToHTML } from "./lib/src/utils";
import ObsidianLiveSyncPlugin from "./main";
export class LogDisplayModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
updateLog() {
let msg = "";
for (const v of this.plugin.logMessage) {
msg += escapeStringToHTML(v) + "<br>";
}
this.logEl.innerHTML = msg;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: "Sync Status" });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
div.addClass("op-pre");
this.logEl = div;
this.updateLog = this.updateLog.bind(this);
this.plugin.addLogHook = this.updateLog;
this.updateLog();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
this.plugin.addLogHook = null;
}
}

File diff suppressed because it is too large Load Diff

295
src/PluginPane.svelte Normal file
View File

@@ -0,0 +1,295 @@
<script lang="ts">
import ObsidianLiveSyncPlugin from "./main";
import { onMount } from "svelte";
import { DevicePluginList, PluginDataEntry } from "./types";
import { versionNumberString2Number } from "./lib/src/utils";
type JudgeResult = "" | "NEWER" | "EVEN" | "EVEN_BUT_DIFFERENT" | "OLDER" | "REMOTE_ONLY";
interface PluginDataEntryDisp extends PluginDataEntry {
versionInfo: string;
mtimeInfo: string;
mtimeFlag: JudgeResult;
versionFlag: JudgeResult;
}
export let plugin: ObsidianLiveSyncPlugin;
let plugins: PluginDataEntry[] = [];
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
let devicePluginList: [string, PluginDataEntryDisp[]][] = null;
let ownPlugins: DevicePluginList = null;
let showOwnPlugins = false;
let targetList: { [key: string]: boolean } = {};
function saveTargetList() {
window.localStorage.setItem("ols-plugin-targetlist", JSON.stringify(targetList));
}
function loadTargetList() {
let e = window.localStorage.getItem("ols-plugin-targetlist") || "{}";
try {
targetList = JSON.parse(e);
} catch (_) {
// NO OP.
}
}
function clearSelection() {
targetList = {};
}
async function updateList() {
let x = await plugin.getPluginList();
ownPlugins = x.thisDevicePlugins;
plugins = Object.values(x.allPlugins);
let targetListItems = Array.from(new Set(plugins.map((e) => e.deviceVaultName + "---" + e.manifest.id)));
let newTargetList: { [key: string]: boolean } = {};
for (const id of targetListItems) {
for (const tag of ["---plugin", "---setting"]) {
newTargetList[id + tag] = id + tag in targetList && targetList[id + tag];
}
}
targetList = newTargetList;
saveTargetList();
}
$: {
deviceAndPlugins = {};
for (const p of plugins) {
if (p.deviceVaultName == plugin.deviceAndVaultName && !showOwnPlugins) {
continue;
}
if (!(p.deviceVaultName in deviceAndPlugins)) {
deviceAndPlugins[p.deviceVaultName] = [];
}
let dispInfo: PluginDataEntryDisp = { ...p, versionInfo: "", mtimeInfo: "", versionFlag: "", mtimeFlag: "" };
dispInfo.versionInfo = p.manifest.version;
let x = new Date().getTime() / 1000;
let mtime = p.mtime / 1000;
let diff = (x - mtime) / 60;
if (p.mtime == 0) {
dispInfo.mtimeInfo = `-`;
} else if (diff < 60) {
dispInfo.mtimeInfo = `${diff | 0} Mins ago`;
} else if (diff < 60 * 24) {
dispInfo.mtimeInfo = `${(diff / 60) | 0} Hours ago`;
} else if (diff < 60 * 24 * 10) {
dispInfo.mtimeInfo = `${(diff / (60 * 24)) | 0} Days ago`;
} else {
dispInfo.mtimeInfo = new Date(dispInfo.mtime).toLocaleString();
}
// compare with own plugin
let id = p.manifest.id;
if (id in ownPlugins) {
// Which we have.
const ownPlugin = ownPlugins[id];
let localVer = versionNumberString2Number(ownPlugin.manifest.version);
let pluginVer = versionNumberString2Number(p.manifest.version);
if (localVer > pluginVer) {
dispInfo.versionFlag = "OLDER";
} else if (localVer == pluginVer) {
if (ownPlugin.manifestJson + (ownPlugin.styleCss ?? "") + ownPlugin.mainJs != p.manifestJson + (p.styleCss ?? "") + p.mainJs) {
dispInfo.versionFlag = "EVEN_BUT_DIFFERENT";
} else {
dispInfo.versionFlag = "EVEN";
}
} else if (localVer < pluginVer) {
dispInfo.versionFlag = "NEWER";
}
if ((ownPlugin.dataJson ?? "") == (p.dataJson ?? "")) {
if (ownPlugin.mtime == 0 && p.mtime == 0) {
dispInfo.mtimeFlag = "";
} else {
dispInfo.mtimeFlag = "EVEN";
}
} else {
if (((ownPlugin.mtime / 1000) | 0) > ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "OLDER";
} else if (((ownPlugin.mtime / 1000) | 0) == ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "EVEN_BUT_DIFFERENT";
} else if (((ownPlugin.mtime / 1000) | 0) < ((p.mtime / 1000) | 0)) {
dispInfo.mtimeFlag = "NEWER";
}
}
} else {
dispInfo.versionFlag = "REMOTE_ONLY";
dispInfo.mtimeFlag = "REMOTE_ONLY";
}
deviceAndPlugins[p.deviceVaultName].push(dispInfo);
}
devicePluginList = Object.entries(deviceAndPlugins);
}
function getDispString(stat: JudgeResult): string {
if (stat == "") return "";
if (stat == "NEWER") return " (Newer)";
if (stat == "OLDER") return " (Older)";
if (stat == "EVEN") return " (Even)";
if (stat == "EVEN_BUT_DIFFERENT") return " (Even but different)";
if (stat == "REMOTE_ONLY") return " (Remote Only)";
return "";
}
onMount(async () => {
loadTargetList();
await updateList();
});
function toggleShowOwnPlugins() {
showOwnPlugins = !showOwnPlugins;
}
function toggleTarget(key: string) {
targetList[key] = !targetList[key];
saveTargetList();
}
function toggleAll(devicename: string) {
for (const c in targetList) {
if (c.startsWith(devicename)) {
targetList[c] = true;
}
}
}
async function sweepPlugins() {
//@ts-ignore
await plugin.app.plugins.loadManifests();
await plugin.sweepPlugin(true);
updateList();
}
async function applyPlugins() {
for (const c in targetList) {
if (targetList[c] == true) {
const [deviceAndVault, id, opt] = c.split("---");
if (deviceAndVault in deviceAndPlugins) {
const entry = deviceAndPlugins[deviceAndVault].find((e) => e.manifest.id == id);
if (entry) {
if (opt == "plugin") {
if (entry.versionFlag != "EVEN") await plugin.applyPlugin(entry);
} else if (opt == "setting") {
if (entry.mtimeFlag != "EVEN") await plugin.applyPluginData(entry);
}
}
}
}
}
//@ts-ignore
await plugin.app.plugins.loadManifests();
await plugin.sweepPlugin(true);
updateList();
}
async function checkUpdates() {
await plugin.checkPluginUpdate();
}
async function replicateAndRefresh() {
await plugin.replicate(true);
updateList();
}
</script>
<div>
<h1>Plugins and their settings</h1>
<div class="ols-plugins-div-buttons">
Show own items
<div class="checkbox-container" class:is-enabled={showOwnPlugins} on:click={toggleShowOwnPlugins} />
</div>
<div class="sls-plugins-wrap">
<table class="sls-plugins-tbl">
<tr style="position:sticky">
<th class="sls-plugins-tbl-device-head">Name</th>
<th class="sls-plugins-tbl-device-head">Info</th>
<th class="sls-plugins-tbl-device-head">Target</th>
</tr>
{#if !devicePluginList}
<tr>
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
</tr>
{:else if devicePluginList.length == 0}
<tr>
<td colspan="3" class="sls-table-tail tcenter"> No plugins found. </td>
</tr>
{:else}
{#each devicePluginList as [deviceName, devicePlugins]}
<tr>
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
<th class="sls-plugins-tbl-device-head">
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
</th>
</tr>
{#each devicePlugins as plugin}
<tr>
<td class="sls-table-head">{plugin.manifest.name}</td>
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
/>
</div>
{/if}
</td>
</tr>
<tr>
<td class="sls-table-head">Settings</td>
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
/>
</div>
{/if}
</td>
</tr>
<tr class="divider">
<th colspan="3" />
</tr>
{/each}
{/each}
{/if}
</table>
</div>
<div class="ols-plugins-div-buttons">
<button class="" on:click={replicateAndRefresh}>Replicate and refresh</button>
<button class="" on:click={clearSelection}>Clear Selection</button>
</div>
<div class="ols-plugins-div-buttons">
<button class="mod-cta" on:click={checkUpdates}>Check Updates</button>
<button class="mod-cta" on:click={sweepPlugins}>Scan installed</button>
<button class="mod-cta" on:click={applyPlugins}>Apply all</button>
</div>
<!-- <div class="ols-plugins-div-buttons">-->
<!-- <button class="mod-warning" on:click={applyPlugins}>Delete all selected</button>-->
<!-- </div>-->
</div>
<style>
.ols-plugins-div-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 8px;
}
.wrapToggle {
display: flex;
justify-content: center;
align-content: center;
}
</style>

126
src/dialogs.ts Normal file
View File

@@ -0,0 +1,126 @@
import { App, FuzzySuggestModal, Modal, Setting } from "obsidian";
import ObsidianLiveSyncPlugin from "./main";
//@ts-ignore
import PluginPane from "./PluginPane.svelte";
export class PluginDialogModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
logEl: HTMLDivElement;
component: PluginPane = null;
constructor(app: App, plugin: ObsidianLiveSyncPlugin) {
super(app);
this.plugin = plugin;
}
onOpen() {
const { contentEl } = this;
if (this.component == null) {
this.component = new PluginPane({
target: contentEl,
props: { plugin: this.plugin },
});
}
}
onClose() {
if (this.component != null) {
this.component.$destroy();
this.component = null;
}
}
}
export 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);
}
}
}
export class PopoverSelectString extends FuzzySuggestModal<string> {
app: App;
callback: (e: string) => void = () => { };
getItemsFun: () => string[] = () => {
return ["yes", "no"];
}
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);
if (getItemsFun) this.getItemsFun = getItemsFun;
this.callback = callback;
}
getItems(): string[] {
return this.getItemsFun();
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
// debugger;
this.callback(item);
this.callback = null;
}
onClose(): void {
setTimeout(() => {
if (this.callback != null) {
this.callback("");
}
}, 100);
}
}

1
src/lib Submodule

Submodule src/lib added at a49a096a6a

2694
src/main.ts Normal file

File diff suppressed because it is too large Load Diff

4
src/pouchdb-browser.ts Normal file
View File

@@ -0,0 +1,4 @@
import { PouchDB as PouchDB_ } from "../pouchdb-browser-webpack/dist/pouchdb-browser.js";
const Pouch: PouchDB.Static = PouchDB_;
export { Pouch as PouchDB };

32
src/types.ts Normal file
View File

@@ -0,0 +1,32 @@
import { PluginManifest } from "obsidian";
import { DatabaseEntry } from "./lib/src/types";
export interface PluginDataEntry extends DatabaseEntry {
deviceVaultName: string;
mtime: number;
manifest: PluginManifest;
mainJs: string;
manifestJson: string;
styleCss?: string;
// it must be encrypted.
dataJson?: string;
_conflicts?: string[];
type: "plugin";
}
export interface PluginList {
[key: string]: PluginDataEntry[];
}
export interface DevicePluginList {
[key: string]: PluginDataEntry;
}
export const PERIODIC_PLUGIN_SWEEP = 60;
export interface InternalFileInfo {
path: string;
mtime: number;
ctime: number;
size: number;
deleted?: boolean;
}

75
src/utils.ts Normal file
View File

@@ -0,0 +1,75 @@
import { normalizePath } from "obsidian";
import { path2id_base, id2path_base } from "./lib/src/utils";
// For backward compatibility, using the path for determining id.
// Only CouchDB nonacceptable ID (that starts with an underscore) has been prefixed with "/".
// The first slash will be deleted when the path is normalized.
export function path2id(filename: string): string {
const x = normalizePath(filename);
return path2id_base(x);
}
export function id2path(filename: string): string {
return id2path_base(normalizePath(filename));
}
const triggers: { [key: string]: ReturnType<typeof setTimeout> } = {};
export function setTrigger(key: string, timeout: number, proc: (() => Promise<any> | void)) {
clearTrigger(key);
triggers[key] = setTimeout(async () => {
delete triggers[key];
await proc();
}, timeout);
}
export function clearTrigger(key: string) {
if (key in triggers) {
clearTimeout(triggers[key]);
}
}
export function clearAllTriggers() {
for (const v in triggers) {
clearTimeout(triggers[v]);
}
}
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
export function setPeriodic(key: string, timeout: number, proc: (() => Promise<any> | void)) {
clearPeriodic(key);
intervals[key] = setInterval(async () => {
delete intervals[key];
await proc();
}, timeout);
}
export function clearPeriodic(key: string) {
if (key in intervals) {
clearInterval(intervals[key]);
}
}
export function clearAllPeriodic() {
for (const v in intervals) {
clearInterval(intervals[v]);
}
}
const memos: { [key: string]: any } = {};
export function memoObject<T>(key: string, obj: T): T {
memos[key] = obj;
return memos[key] as T;
}
export async function memoIfNotExist<T>(key: string, func: () => T | Promise<T>): Promise<T> {
if (!(key in memos)) {
const w = func();
const v = w instanceof Promise ? (await w) : w;
memos[key] = v;
}
return memos[key] as T;
}
export function retriveMemoObject<T>(key: string): T | false {
if (key in memos) {
return memos[key];
} else {
return false;
}
}
export function disposeMemoObject(key: string) {
delete memos[key];
}

227
src/utils_couchdb.ts Normal file
View File

@@ -0,0 +1,227 @@
import { Logger } from "./lib/src/logger";
import { LOG_LEVEL, VER, VERSIONINFO_DOCID, EntryVersionInfo, EntryDoc, RemoteDBSettings, SYNCINFO_ID, SyncInfo } from "./lib/src/types";
import { enableEncryption, resolveWithIgnoreKnownError } from "./lib/src/utils";
import { PouchDB } from "./pouchdb-browser";
import { requestUrl, RequestUrlParam, RequestUrlResponse } from "obsidian";
export const isValidRemoteCouchDBURI = (uri: string): boolean => {
if (uri.startsWith("https://")) return true;
if (uri.startsWith("http://")) return true;
return false;
};
let last_post_successed = false;
export const getLastPostFailedBySize = () => {
return !last_post_successed;
};
const fetchByAPI = async (request: RequestUrlParam): Promise<RequestUrlResponse> => {
const ret = await requestUrl(request);
if (ret.status - (ret.status % 100) !== 200) {
const er: Error & { status?: number } = new Error(`Request Error:${ret.status}`);
if (ret.json) {
er.message = ret.json.reason;
er.name = `${ret.json.error ?? ""}:${ret.json.message ?? ""}`;
}
er.status = ret.status;
throw er;
}
return ret;
};
export const connectRemoteCouchDBWithSetting = (settings: RemoteDBSettings, isMobile: boolean) =>
connectRemoteCouchDB(
settings.couchDB_URI + (settings.couchDB_DBNAME == "" ? "" : "/" + settings.couchDB_DBNAME),
{
username: settings.couchDB_USER,
password: settings.couchDB_PASSWORD,
},
settings.disableRequestURI || isMobile,
settings.encrypt ? settings.passphrase : settings.encrypt
);
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 (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 = "";
if (auth.username && auth.password) {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${auth.username}:${auth.password}`));
const encoded = window.btoa(utf8str);
authHeader = "Basic " + encoded;
} else {
authHeader = "";
}
const conf: PouchDB.HttpAdapter.HttpAdapterConfiguration = {
adapter: "http",
auth,
fetch: async function (url: string | Request, opts: RequestInit) {
let size = "";
const localURL = url.toString().substring(uri.length);
const method = opts.method ?? "GET";
if (opts.body) {
const opts_length = opts.body.toString().length;
if (opts_length > 1024 * 1024 * 10) {
// over 10MB
if (uri.contains(".cloudantnosqldb.")) {
last_post_successed = false;
Logger("This request should fail on IBM Cloudant.", LOG_LEVEL.VERBOSE);
throw new Error("This request should fail on IBM Cloudant.");
}
}
size = ` (${opts_length})`;
}
if (!disableRequestURI && typeof url == "string" && typeof (opts.body ?? "") == "string") {
const body = opts.body as string;
const transformedHeaders = { ...(opts.headers as Record<string, string>) };
if (authHeader != "") transformedHeaders["authorization"] = authHeader;
delete transformedHeaders["host"];
delete transformedHeaders["Host"];
delete transformedHeaders["content-length"];
delete transformedHeaders["Content-Length"];
const requestParam: RequestUrlParam = {
url: url as string,
method: opts.method,
body: body,
headers: transformedHeaders,
contentType: "application/json",
// contentType: opts.headers,
};
try {
const r = await fetchByAPI(requestParam);
if (method == "POST" || method == "PUT") {
last_post_successed = r.status - (r.status % 100) == 200;
} else {
last_post_successed = true;
}
Logger(`HTTP:${method}${size} to:${localURL} -> ${r.status}`, LOG_LEVEL.DEBUG);
return new Response(r.arrayBuffer, {
headers: r.headers,
status: r.status,
statusText: `${r.status}`,
});
} catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
last_post_successed = false;
}
Logger(ex);
throw ex;
}
}
// -old implementation
try {
const responce: Response = await fetch(url, opts);
if (method == "POST" || method == "PUT") {
last_post_successed = responce.ok;
} else {
last_post_successed = true;
}
Logger(`HTTP:${method}${size} to:${localURL} -> ${responce.status}`, LOG_LEVEL.DEBUG);
return responce;
} catch (ex) {
Logger(`HTTP:${method}${size} to:${localURL} -> failed`, LOG_LEVEL.VERBOSE);
// limit only in bulk_docs.
if (url.toString().indexOf("_bulk_docs") !== -1) {
last_post_successed = false;
}
Logger(ex);
throw ex;
}
// return await fetch(url, opts);
},
};
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
if (passphrase && typeof passphrase === "string") {
enableEncryption(db, passphrase);
}
try {
const info = await db.info();
return { db: db, info: info };
} catch (ex) {
let msg = `${ex.name}:${ex.message}`;
if (ex.name == "TypeError" && ex.message == "Failed to fetch") {
msg += "\n**Note** This error caused by many reasons. The only sure thing is you didn't touch the server.\nTo check details, open inspector.";
}
Logger(ex, LOG_LEVEL.VERBOSE);
return msg;
}
};
// check the version of remote.
// if remote is higher than current(or specified) version, return false.
export const checkRemoteVersion = async (db: PouchDB.Database, migrate: (from: number, to: number) => Promise<boolean>, barrier: number = VER): Promise<boolean> => {
try {
const versionInfo = (await db.get(VERSIONINFO_DOCID)) as EntryVersionInfo;
if (versionInfo.type != "versioninfo") {
return false;
}
const version = versionInfo.version;
if (version < barrier) {
const versionUpResult = await migrate(version, barrier);
if (versionUpResult) {
await bumpRemoteVersion(db);
return true;
}
}
if (version == barrier) return true;
return false;
} catch (ex) {
if (ex.status && ex.status == 404) {
if (await bumpRemoteVersion(db)) {
return true;
}
return false;
}
throw ex;
}
};
export const bumpRemoteVersion = async (db: PouchDB.Database, barrier: number = VER): Promise<boolean> => {
const vi: EntryVersionInfo = {
_id: VERSIONINFO_DOCID,
version: barrier,
type: "versioninfo",
};
const versionInfo = (await resolveWithIgnoreKnownError(db.get(VERSIONINFO_DOCID), vi)) as EntryVersionInfo;
if (versionInfo.type != "versioninfo") {
return false;
}
vi._rev = versionInfo._rev;
await db.put(vi);
return true;
};
export const checkSyncInfo = async (db: PouchDB.Database): Promise<boolean> => {
try {
const syncinfo = (await db.get(SYNCINFO_ID)) as SyncInfo;
console.log(syncinfo);
// if we could decrypt the doc, it must be ok.
return true;
} catch (ex) {
if (ex.status && ex.status == 404) {
const randomStrSrc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const temp = [...Array(30)]
.map((e) => Math.floor(Math.random() * randomStrSrc.length))
.map((e) => randomStrSrc[e])
.join("");
const newSyncInfo: SyncInfo = {
_id: SYNCINFO_ID,
type: "syncinfo",
data: temp,
};
if (await db.put(newSyncInfo)) {
return true;
}
return false;
} else {
console.dir(ex);
return false;
}
}
};

View File

@@ -1,48 +1,82 @@
.added {
color: black;
background-color: white;
color: var(--text-on-accent);
background-color: var(--text-accent);
}
.normal {
color: lightgray;
color: var(--text-normal);
}
.deleted {
color: white;
background-color: black;
/* text-decoration: line-through; */
color: var(--text-on-accent);
background-color: var(--text-muted);
}
.op-scrollable {
overflow-y: scroll;
/* min-height: 280px; */
max-height: 280px;
user-select: text;
}
.op-pre {
white-space: pre-wrap;
}
.op-warn {
border: 1px solid salmon;
padding: 2px;
border-radius: 4px;
}
.op-warn::before {
content: "Warning";
font-weight: bold;
color: salmon;
position: relative;
display: block;
}
.op-warn-info {
border: 1px solid rgb(255, 209, 81);
padding: 2px;
border-radius: 4px;
}
.op-warn-info::before {
content: "Notice";
font-weight: bold;
color: rgb(255, 209, 81);
position: relative;
display: block;
}
.syncstatusbar {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.tcenter {
text-align: center;
}
.sls-plugins-wrap {
display: flex;
flex-grow: 1;
/* overflow: scroll; */
max-height: 50vh;
overflow-y: scroll;
}
.sls-plugins-tbl {
border: 1px solid var(--background-modifier-border);
width: 100%;
max-height: 80%;
}
.divider th {
border-top: 1px solid var(--background-modifier-border);
}
/* .sls-table-head{
width:50%;
}
@@ -54,9 +88,15 @@
.sls-btn-left {
padding-right: 4px;
}
.sls-btn-right {
padding-left: 4px;
}
.sls-header-button {
margin-left: 2em;
}
.sls-hidden {
display: none;
}
@@ -64,8 +104,12 @@
:root {
--slsmessage: "";
}
.CodeMirror-wrap::before , .cm-s-obsidian > .cm-editor::before {
.CodeMirror-wrap::before,
.cm-s-obsidian>.cm-editor::before {
content: var(--slsmessage);
text-align: right;
white-space: pre-wrap;
position: absolute;
border-radius: 4px;
/* border:1px solid --background-modifier-border; */
@@ -73,13 +117,128 @@
top: 8px;
color: --text-normal;
opacity: 0.5;
font-size:80%;
font-size: 80%;
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.CodeMirror-wrap::before {
right: 0px;
} .cm-s-obsidian > .cm-editor::before {
}
.cm-s-obsidian>.cm-editor::before {
right: 16px;
}
.sls-setting-tab {
display: none;
}
div.sls-setting-menu-btn {
color: var(--text-normal);
background-color: var(--background-secondary-alt);
border-radius: 4px 4px 0 0;
padding: 6px 10px;
cursor: pointer;
margin-right: 12px;
font-family: "Inter", sans-serif;
outline: none;
user-select: none;
flex-grow: 1;
text-align: center;
flex-shrink: 1;
}
.sls-setting-label.selected {
/* order: 1; */
flex-grow: 1;
/* width: 100%; */
}
.sls-setting-tab:hover~div.sls-setting-menu-btn,
.sls-setting-tab:checked~div.sls-setting-menu-btn {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
.sls-setting-menu {
display: flex;
flex-direction: row;
/* flex-wrap: wrap; */
overflow-x: auto;
}
.sls-setting-label {
flex-grow: 1;
display: inline-flex;
justify-content: center;
}
.setting-collapsed {
display: none;
}
.sls-plugins-tbl-buttons {
text-align: right;
}
.sls-plugins-tbl-buttons button {
flex-grow: 0;
padding: 6px 10px;
}
.sls-plugins-tbl-device-head {
background-color: var(--background-secondary-alt);
color: var(--text-accent);
}
.op-flex {
display: flex;
}
.op-flex input {
display: inline-flex;
flex-grow: 1;
margin-bottom: 8px;
}
.op-info {
display: inline-flex;
flex-grow: 1;
border-bottom: 1px solid var(--background-modifier-border);
width: 100%;
margin-bottom: 4px;
padding-bottom: 4px;
}
.history-added {
color: var(--text-on-accent);
background-color: var(--text-accent);
}
.history-normal {
color: var(--text-normal);
}
.history-deleted {
color: var(--text-on-accent);
background-color: var(--text-muted);
text-decoration: line-through;
}
.ob-btn-config-fix label {
margin-right: 40px;
}
.ob-btn-config-info {
border: 1px solid salmon;
padding: 2px;
margin: 1px;
border-radius: 4px;
}
.ob-btn-config-head {
padding: 2px;
margin: 1px;
border-radius: 4px;
}

View File

@@ -1,17 +1,16 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "es6",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
// "importsNotUsedAsValues": "error",
"importHelpers": true,
"lib": ["dom", "es5", "scripthost", "es2015"]
"alwaysStrict": true,
"lib": ["es2018", "DOM", "ES5", "ES6", "ES7"]
},
"include": ["**/*.ts"],
"files": ["./main.ts"],
"exclude": ["pouchdb-browser-webpack"]
}

17
updates.md Normal file
View File

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