Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62b176d709 | ||
|
|
9553bff44a | ||
|
|
ede1f4e1bc | ||
|
|
97f4c0c427 | ||
|
|
94a56dabdd | ||
|
|
4947eb91a2 | ||
|
|
2d12c9977d | ||
|
|
f2794ac539 | ||
|
|
519fd27dd5 | ||
|
|
dcbd36e33a | ||
|
|
e40a6adf63 | ||
|
|
06bf59bf2a | ||
|
|
d51659d3d1 | ||
|
|
7ec0102c7a | ||
|
|
c828a341ba | ||
|
|
d7ff793715 | ||
|
|
996533328e | ||
|
|
aff5a8b088 | ||
|
|
9fc46553e1 | ||
|
|
447e0c0aa4 | ||
|
|
768d6636d0 | ||
|
|
5f5b5de505 | ||
|
|
5df06bbed5 | ||
|
|
1f4737bfb8 | ||
|
|
40020c3e44 | ||
|
|
c512330d84 | ||
|
|
e34b120a63 | ||
|
|
725b12832c | ||
|
|
e341c4f780 | ||
|
|
10d22cb912 | ||
|
|
06b15b2146 | ||
|
|
2fbd87eed4 | ||
|
|
f6ea9938d1 | ||
|
|
38b189e2a7 | ||
|
|
6d1b451f4c | ||
|
|
69537f7002 | ||
|
|
17de78240f | ||
|
|
8cbcdf5323 | ||
|
|
6ba5abc0a6 | ||
|
|
d820285d1e | ||
|
|
a6002ba340 | ||
|
|
1f31303906 | ||
|
|
0fcedf397f | ||
|
|
085d8448d2 | ||
|
|
0d9da32b00 | ||
|
|
22880d6cf5 | ||
|
|
a084ec7728 | ||
|
|
eeeb4893c7 | ||
|
|
ed1ac66ac0 | ||
|
|
c33a9ecfa4 | ||
|
|
cabca54d80 | ||
|
|
8dde6c59d2 | ||
|
|
480cf224c6 | ||
|
|
37a801332f | ||
|
|
1e33f8b7d2 | ||
|
|
071b10387c | ||
|
|
b99053ef10 | ||
|
|
ff237ad8cb | ||
|
|
b89a163a66 | ||
|
|
991376a7a3 | ||
|
|
2da39bdfec | ||
|
|
f38c3d36be | ||
|
|
da0f183fad | ||
|
|
eb2ecca825 | ||
|
|
2494a2d96e | ||
|
|
8bb095c4aa | ||
|
|
c5e3147805 | ||
|
|
942e54961e | ||
|
|
29a3c76b4e | ||
|
|
897e4fc4e5 | ||
|
|
0124a49d7e | ||
|
|
3e245af50c | ||
|
|
695c74588f | ||
|
|
e3bd41a05a | ||
|
|
a6b73bde6a | ||
|
|
87d6e1f75f | ||
|
|
347c051d0c | ||
|
|
7422956621 | ||
|
|
7b67ba5b26 | ||
|
|
43cf95c417 | ||
|
|
53a1c32b43 | ||
|
|
71e25be28e | ||
|
|
afc5670313 | ||
|
|
81eb45d32c | ||
|
|
d7a59b82be | ||
|
|
acbf0318b4 | ||
|
|
f3f26f7c0b | ||
|
|
9fba9241dc | ||
|
|
d4c6ec1ecf | ||
|
|
a6146f98cd | ||
|
|
4d6e660f90 | ||
|
|
d0af937fd1 | ||
|
|
b9ddd97052 | ||
|
|
1466fdd3aa | ||
|
|
b070f51f5d | ||
|
|
586e587d96 | ||
|
|
cbf656c7b7 | ||
|
|
2d9ba514fa | ||
|
|
a13cc8d5c1 | ||
|
|
30f7c0058c | ||
|
|
c3f7b584c9 | ||
|
|
bd1f8455b0 | ||
|
|
635db41d0d | ||
|
|
19d5da3e45 | ||
|
|
2991007435 | ||
|
|
23f1b7990c | ||
|
|
0558d77074 | ||
|
|
4a5a398017 | ||
|
|
8d286a79a1 | ||
|
|
e69674e34e | ||
|
|
c3ed69af66 | ||
|
|
832828e6f6 | ||
|
|
b3c9ac8cbb | ||
|
|
cdbb220a6b | ||
|
|
8a631ee809 | ||
|
|
b49e545759 | ||
|
|
a8f6e1d05d | ||
|
|
6588952227 | ||
|
|
0f8b1951a6 | ||
|
|
8b838656ce | ||
|
|
37d36fe5b3 | ||
|
|
d1cfe0c978 | ||
|
|
e3e8db04e2 | ||
|
|
084a04f4e9 | ||
|
|
d38ac34362 | ||
|
|
6a651b844f | ||
|
|
9e4d3826fd | ||
|
|
d7e034f070 | ||
|
|
79a0b9f44d | ||
|
|
c75336a2a5 | ||
|
|
b5532dd834 | ||
|
|
81c79ab848 | ||
|
|
af71758ef7 | ||
|
|
ee8a3df0be | ||
|
|
f55fad0e29 | ||
|
|
15c163e73d | ||
|
|
d8f613bbb6 | ||
|
|
c8016b5f30 | ||
|
|
f66a7031b2 | ||
|
|
bbc69715e8 | ||
|
|
d6c39615d7 | ||
|
|
bbb909cda4 | ||
|
|
200d25de70 | ||
|
|
cd0a9a395f | ||
|
|
2deb3be8eb | ||
|
|
9519793860 | ||
|
|
2d7c33fb1a | ||
|
|
6a009b93cb | ||
|
|
ce8d8d1c62 | ||
|
|
0b961f4865 | ||
|
|
7b2343e879 | ||
|
|
6969c555da | ||
|
|
881147e91a | ||
|
|
6e282c86af | ||
|
|
c689235ff0 | ||
|
|
753937a932 | ||
|
|
b238988012 | ||
|
|
24fb9e1675 | ||
|
|
f6a1a7c032 | ||
|
|
cfe7ae9826 | ||
|
|
186cff9c36 | ||
|
|
44dbec175b | ||
|
|
7ecddf29fa | ||
|
|
81290728c2 | ||
|
|
d23183395c | ||
|
|
8dd7879c5d | ||
|
|
f0d2766f48 | ||
|
|
229f74b5e8 | ||
|
|
a0c4a6ca6c | ||
|
|
30d94986c6 | ||
|
|
58c0bc5057 | ||
|
|
4f03024200 | ||
|
|
92183e0fb6 | ||
|
|
28bd861c9e | ||
|
|
4e8bec9511 | ||
|
|
c98d91ab60 | ||
|
|
8bd456b08e | ||
|
|
a5c25aecd0 | ||
|
|
42eab5d544 | ||
|
|
806438964c | ||
|
|
069e993fb0 | ||
|
|
4726d51d60 | ||
|
|
7c2a17c404 | ||
|
|
ad48de9227 | ||
|
|
b227202f0e | ||
|
|
eda77d12e2 | ||
|
|
82d0619761 | ||
|
|
9fdb379251 | ||
|
|
470fe141ef | ||
|
|
6bfbaa3c1b | ||
|
|
c46d302441 | ||
|
|
2afc10b080 | ||
|
|
ea076d310b | ||
|
|
75a3b2a830 | ||
|
|
9a4c2d3c1c | ||
|
|
c729c117e8 | ||
|
|
230fee440e | ||
|
|
5e740813d5 | ||
|
|
475d524994 | ||
|
|
28730585f8 | ||
|
|
4b69d60d89 | ||
|
|
d60fe622ed | ||
|
|
f2202029fc | ||
|
|
340ac09961 |
@@ -31,13 +31,30 @@ body:
|
||||
- type: dropdown
|
||||
id: service
|
||||
attributes:
|
||||
label: What remote cloud services are you using?
|
||||
label: What remote cloud services are you using? (Please choose the specified one if it's in the list)
|
||||
multiple: true
|
||||
options:
|
||||
- S3
|
||||
- S3 (Cloudflare R2)
|
||||
- S3 (BackBlaze B2)
|
||||
- S3 (腾讯云 COS Tencent Cloud COS)
|
||||
- S3 (阿里云 OSS Alibaba Cloud OSS)
|
||||
- S3 (MinIO)
|
||||
- S3 (Wasabi)
|
||||
- S3 (Storj)
|
||||
- OneDrive for personal
|
||||
- OneDrive for business
|
||||
- Dropbox
|
||||
- webdav
|
||||
- webdav (ownCloud)
|
||||
- webdav (InfiniCloud (formally TeraCLOUD))
|
||||
- webdav (AList)
|
||||
- webdav (Cloudreve)
|
||||
- webdav (坚果云 JianGuoYun/NutStore)
|
||||
- webdav (NextCloud)
|
||||
- webdav (FastMail)
|
||||
- webdav (rclone webdav)
|
||||
- webdav (nginx)
|
||||
- others
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -21,9 +21,26 @@ body:
|
||||
multiple: true
|
||||
options:
|
||||
- S3
|
||||
- S3 (Cloudflare R2)
|
||||
- S3 (BackBlaze B2)
|
||||
- S3 (腾讯云 COS Tencent Cloud COS)
|
||||
- S3 (阿里云 OSS Alibaba Cloud OSS)
|
||||
- S3 (MinIO)
|
||||
- S3 (Wasabi)
|
||||
- S3 (Storj)
|
||||
- OneDrive for personal
|
||||
- OneDrive for business
|
||||
- Dropbox
|
||||
- webdav
|
||||
- webdav (ownCloud)
|
||||
- webdav (InfiniCloud (formally TeraCLOUD))
|
||||
- webdav (AList)
|
||||
- webdav (Cloudreve)
|
||||
- webdav (坚果云 JianGuoYun/NutStore)
|
||||
- webdav (NextCloud)
|
||||
- webdav (FastMail)
|
||||
- webdav (rclone webdav)
|
||||
- webdav (nginx)
|
||||
- others
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
# build
|
||||
main.js
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "src/langs"]
|
||||
path = src/langs
|
||||
url = https://github.com/remotely-save/langs.git
|
||||
@@ -17,7 +17,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
|
||||
## Features
|
||||
|
||||
- Supports:
|
||||
- Amazon S3 or S3-compatible
|
||||
- Amazon S3 or S3-compatible (Cloudflare R2 / BackBlaze B2 / MinIO / ...)
|
||||
- Dropbox
|
||||
- OneDrive for personal
|
||||
- Webdav
|
||||
@@ -26,13 +26,13 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
|
||||
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
||||
- **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination).
|
||||
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
||||
- **Skip Large files** and **skip paths** by custom regex conditions!
|
||||
- **Fully open source under [Apache-2.0 License](./LICENSE).**
|
||||
- **[Sync Algorithm open](./docs/sync_algorithm_v2.md) for discussion.**
|
||||
- **[Sync Algorithm open](./docs/sync_algorithm/v3/intro.md) for discussion.**
|
||||
- **[Basic Conflict Detection And Handling](./docs/sync_algorithm/v3/intro.md)** now, more to come!
|
||||
|
||||
## Limitations
|
||||
|
||||
- **To support deltions sync, extra metadata will also be uploaded.** See [Minimal Intrusive](./docs/minimal_intrusive_design.md).
|
||||
- **No Conflict resolution. No content-diff-and-patch algorithm.** All files and folders are compared using their local and remote "last modified time" and those with later "last modified time" wins.
|
||||
- **Cloud services cost you money.** Always be aware of the costs and pricing. Specifically, all the operations, including but not limited to downloading, uploading, listing all files, calling any api, storage sizes, may or may not cost you money.
|
||||
- **Some limitations from the browser environment.** More technical details are [in the doc](./docs/browser_env.md).
|
||||
- **You should protect your `data.json` file.** The file contains sensitive information.
|
||||
@@ -60,15 +60,21 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
|
||||
### S3
|
||||
|
||||
- Tutorials / Examples:
|
||||
- [Cloudflare R2](./docs/remote_services/s3_cloudflare_r2/README.md)
|
||||
- [BackBlaze B2](./docs/remote_services/s3_backblaze_b2/README.md)
|
||||
- [Storj](./docs/remote_services/s3_storj_io/README.md)
|
||||
- [腾讯云 COS](./docs/remote_services/s3_tencent_cloud_cos/README.zh-cn.md) | [Tencent Cloud COS](./docs/remote_services/s3_tencent_cloud_cos/README.md)
|
||||
- [MinIO](./docs/remote_services/s3_minio/README.md)
|
||||
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
||||
- About CORS:
|
||||
- If you are using Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, you can skip this CORS part.
|
||||
- If you are using Obsidian desktop < 0.13.25 or mobile < 1.1.1, you need to configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, and add at least `ETag` into exposed headers. Full example is [here](./docs/s3_cors_configure.md). It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
||||
- If you are using AWS S3, create [policy and user](./docs/remote_services/s3_general/s3_user_policy.md).
|
||||
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/s3_general/s3_cors_configure.md).
|
||||
- Download and enable this plugin.
|
||||
- Enter your information to the settings of this plugin.
|
||||
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
||||
- Click the new "circle arrow" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (Or, you could configure auto sync in the settings panel (See next chapter).) While syncing, the icon becomes "two half-circle arrows". Besides clicking the icon on the sidebar ribbon, you can also activate the corresponding command in the command palette.
|
||||
- **Be patient while syncing.** Especially in the first-time sync.
|
||||
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||
|
||||
### Dropbox
|
||||
|
||||
@@ -76,6 +82,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/remotely-save` folder.
|
||||
- If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions. [More with screenshot is here](./docs/dropbox_review_material/README.md).
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||
|
||||
### OneDrive for personal
|
||||
|
||||
@@ -84,19 +91,20 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- After the authorization, the plugin can read your name and email, and read and write files in your OneDrive's `/Apps/remotely-save` folder.
|
||||
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||
|
||||
### webdav
|
||||
|
||||
- About CORS:
|
||||
- If you are using Obsidian desktop >= 0.13.25 or iOS >= 1.1.1, you can skip this CORS part.
|
||||
- If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
|
||||
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
|
||||
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
|
||||
- **Unofficial** workaround: NextCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can install [WebAppPassword](https://apps.nextcloud.com/apps/webapppassword) app, and add `app://obsidian.md`, `capacitor://localhost`, `http://localhost` to `Allowed origins`
|
||||
- **Unofficial** workaround: OwnCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can download `.tar.gz` of `WebAppPassword` above and manually install and configure it on their instances.
|
||||
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
|
||||
- Tutorials / Examples:
|
||||
- [ownCloud](./docs/remote_services/webdav_owncloud/README.md)
|
||||
- [InfiniCloud](./docs/remote_services/webdav_infinicloud_teracloud/README.md)
|
||||
- [Synology webdav server](./docs/remote_services/webdav_synology_webdav_server/README.md) | [群晖 webdav server](./docs/remote_services/webdav_synology_webdav_server/README.zh-cn.md)
|
||||
- [AList(中文)](./docs/remote_services/webdav_alist/README.zh-cn.md) | [AList (English)](./docs/remote_services/webdav_alist/README.md)
|
||||
- [坚果云](./docs/remote_services/webdav_jianguoyun/README.zh-cn.md) | [JianGuoYun/NutStore](./docs/remote_services/webdav_jianguoyun/README.md)
|
||||
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
- If you want to sync the files across multiple devices, **your vault name should be the same** while using default settings.
|
||||
|
||||
## Scheduled Auto Sync
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Importing the http module
|
||||
const http = require("http");
|
||||
|
||||
const requestHandler = (req, res) => {
|
||||
let body = [];
|
||||
req
|
||||
.on("data", (chunk) => {
|
||||
body.push(chunk);
|
||||
})
|
||||
.on("end", () => {
|
||||
const parsed = JSON.parse(Buffer.concat(body).toString());
|
||||
const prettyParsed = JSON.stringify(parsed, null, 2);
|
||||
console.log(prettyParsed);
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(prettyParsed);
|
||||
});
|
||||
};
|
||||
|
||||
const server = http.createServer(requestHandler);
|
||||
|
||||
const addr = "0.0.0.0";
|
||||
const port = 3000;
|
||||
server.listen(port, addr, undefined, () => {
|
||||
console.log(`Server is Running on ${addr}:${port}`);
|
||||
});
|
||||
@@ -12,8 +12,8 @@ See [here](./export_sync_plans.md).
|
||||
|
||||
See [here](./check_console_output.md).
|
||||
|
||||
## Advanced: Save Console Output Then Read Them Later
|
||||
## Advanced: Use `Logstravaganza` to export logs
|
||||
|
||||
This method works for desktop and mobile devices (iOS, Android).
|
||||
This method works for desktop and mobile devices (iOS, Android), especially useful for iOS.
|
||||
|
||||
See [here](./save_console_output_and_export.md).
|
||||
See [here](./use_logstravaganza.md).
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Save Console Output And Read Them Later
|
||||
|
||||
## Disable Auto Sync Firstly
|
||||
|
||||
You should disable auto sync to avoid any unexpected running.
|
||||
|
||||
## Set The Output Level To Debug
|
||||
|
||||
Go to the plugin settings, scroll down to the section "Debug" -> "alter console log level", and change it from "info" to "debug".
|
||||
|
||||
## Enable Saving The Output To DB
|
||||
|
||||
Go to the plugin settings, scroll down to the section "Debug" -> "Save Console Logs Into DB", and change it from "disable" to "enable". **This setting has some performance cost, so do NOT always turn this on when not necessary!**
|
||||
|
||||
## Run The Sync
|
||||
|
||||
Trigger the sync manually (by clicking the icon on the ribbon sidebar). Something (hopefully) helpful should show up in the console. The the console logs are also saved into DB now.
|
||||
|
||||
## Export The Output And Read The Logs
|
||||
|
||||
Go to the plugin settings, scroll down to the section "Debug" -> "Export Console Logs From DB", and click the button. A new file `log_hist_exported_on_....md` should be created inside the special folder `_debug_remotely_save/`. You could read it and hopefully find something useful.
|
||||
|
||||
## Disable Saving The Output To DB
|
||||
|
||||
After debugging, go to the plugin settings, scroll down to the section "Debug" -> "Save Console Logs Into DB", and change it from "enable" to "disable".
|
||||
@@ -0,0 +1,14 @@
|
||||
# Use `Logstravaganza`
|
||||
|
||||
On iOS, it's quite hard to directly check the console logs.
|
||||
|
||||
Luckily, there is a third-party plugin: [`Logstravaganza`](https://obsidian.md/plugins?search=Logstravaganza#), by Carlo Zottmann, that can redirect the output to a note.
|
||||
|
||||
You can just:
|
||||
|
||||
1. Install it.
|
||||
2. Enable it.
|
||||
3. Do something, to trigger some console logs.
|
||||
4. Checkout `LOGGING-NOTE (device name).md` in the root of your vault.
|
||||
|
||||
See more on its site: <https://github.com/czottmann/obsidian-logstravaganza>.
|
||||
@@ -1,8 +1,10 @@
|
||||
# Minimal Intrusive Design
|
||||
|
||||
Before version 0.3.0, the plugin did not upload additional meta data to the remote.
|
||||
~~Before version 0.3.0, the plugin did not upload additional meta data to the remote.~~
|
||||
|
||||
From and after version 0.3.0, the plugin just upload minimal extra necessary meta data to the remote.
|
||||
~~From version 0.3.0 ~ 0.3.40, the plugin just upload minimal extra necessary meta data to the remote.~~
|
||||
|
||||
From version 0.4.1 and above, the plugin doesn't need uploading meta data due to the sync algorithm upgrade.
|
||||
|
||||
## Benefits
|
||||
|
||||
@@ -12,10 +14,14 @@ For example, it's possbile for a uses to manually upload a file to s3, and next
|
||||
|
||||
And it's also possible to combine another "sync-to-s3" solution (like, another software) on desktops, and this plugin on mobile devices, together.
|
||||
|
||||
## Necessarity Of Uploading Extra Metadata
|
||||
## ~~Necessarity Of Uploading Extra Metadata from 0.3.0 ~ 0.3.40~~
|
||||
|
||||
The main issue comes from deletions (and renamings which is actually interpreted as "deletion-then-creation").
|
||||
~~The main issue comes from deletions (and renamings which is actually interpreted as "deletion-then-creation").~~
|
||||
|
||||
If we don't upload any extra info to the remote, there's usually no way for the second device to know what files / folders have been deleted on the first device.
|
||||
~~If we don't upload any extra info to the remote, there's usually no way for the second device to know what files / folders have been deleted on the first device.~~
|
||||
|
||||
To overcome this issue, from and after version 0.3.0, the plugin uploads extra metadata files `_remotely-save-metadata-on-remote.{json,bin}` to users' configured cloud services. Those files contain some info about what has been deleted on the first device, so that the second device can read the list to apply the deletions to itself. Some other necessary meta info would also be written into the extra files.
|
||||
~~To overcome this issue, from and after version 0.3.0, the plugin uploads extra metadata files `_remotely-save-metadata-on-remote.{json,bin}` to users' configured cloud services. Those files contain some info about what has been deleted on the first device, so that the second device can read the list to apply the deletions to itself. Some other necessary meta info would also be written into the extra files.~~
|
||||
|
||||
## No uploading extra metadata from 0.4.1
|
||||
|
||||
Some information, including previous successful sync status of each file, is kept locally.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Backblaze B2
|
||||
|
||||
## Links
|
||||
|
||||
https://www.backblaze.com/cloud-storage
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create a Backblaze account [on this page](https://www.backblaze.com/cloud-storage). Credit card info _is not_ required. Backblaze B2 offers 10 GB of free storage.
|
||||
|
||||
2. Please be aware that, though B2 provides some free quota, **it may still cost you money if the usage of storage or api requests exceed a certain value!!!** Especially pay attention to the api requests!!!
|
||||
|
||||
3. Create a **bucket**, you can leave the default settings, or you can enable the encryption (which is different from what you can set in Remotely Save):
|
||||
|
||||

|
||||

|
||||
|
||||
4. Copy `Endpoint`, eg. `s3.us-east-005.backblazeb2.com` — it'll be used later.
|
||||
|
||||
5. Copy `bucketname` near the 🪣 icon (the "bucket icon") — it'll be used later.
|
||||
|
||||

|
||||
|
||||
6. Go to **Application Keys**:
|
||||
|
||||

|
||||
|
||||
7. **Add a new key**:
|
||||
|
||||

|
||||

|
||||
|
||||
8. Save `keyID` and `applicationKey` — they will be used later.
|
||||
|
||||
9. Go to Remotely Save settings in Obsidian and:
|
||||
|
||||
- Choose `S3 or compatibile` in **Remote Service**:
|
||||
- Copy `Endpoint` from Backblaze (see 3. above) to `Endpoint` in Remotely Save
|
||||
- From `endpoint` take `region` (eg. `us-east-005`) and paste it in `endpoint` in Remotely Save
|
||||
- Copy `keyID` (see 7. above) to `Access Key ID` in Remotely Save
|
||||
- Copy `applicationKey` (see 7. above) to `Secret Access Key` in Remotely Save
|
||||
- Copy `bucketname` (see 4. above) to `Bucket Name` in Remotely Save
|
||||

|
||||
|
||||
10. **Enable Bypass CORS**:
|
||||

|
||||
|
||||
11. Click **Check** in _Check Connectivity_ to see if you can connect to B2 bucket:
|
||||

|
||||
|
||||
12. Sync!
|
||||
|
||||

|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fae18a84bb218461aca01c7de47ddadfd2b451d865ec889c098cd6918f6e8773
|
||||
size 59679
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9389f9c4462740371edb548f7e05191ede5277a5d470e3254c60fbcc4a75e552
|
||||
size 4558
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:46f35968938ebf3bbafae64ee49a4927844fd0abf79e4190522be91776d9fed0
|
||||
size 107623
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7fb639c7b4e595516594ee9d07c864e7cd15b608a97c81386f4cf72d499d0e5f
|
||||
size 80121
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:299bfca82b77de86f60d8f481b5f74069dc977dece19fa025ee629d1e6f783d4
|
||||
size 6499
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5985e918b521c91cc185f60c58cdd334ee76695b897a02e02eaa17c1bad71609
|
||||
size 10214
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5796791bcf89c99c1e1e844c0083739fd46b1a458dc274f3c3fa3f65b87b71b3
|
||||
size 70728
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3633379fcb12f1a5e0a4d1d68c193ddc7bf0be7edd92f2f6006351817035a94f
|
||||
size 27164
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e130be182ae7d3270e5de3fec442ac5e81efd61822e844cd61ed24c7e485d39
|
||||
size 16802
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:29a8971fea065fd86223850198640f4072ee3fda2112820829cda1348f122c59
|
||||
size 11103
|
||||
@@ -0,0 +1,27 @@
|
||||
# Cloudflare R2
|
||||
|
||||
## Links
|
||||
|
||||
<https://www.cloudflare.com/developer-platform/r2/>
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Be aware that it may cost you money.**
|
||||
2. Create a Cloudflare account and enable R2 feature. **Credit card info might be required by Cloudflare**, though Cloudflare provides generous free tier and zero egress fee.
|
||||
3. Create a bucket.
|
||||

|
||||
4. Create an Access Key with "Object Read & Write" permission, and add specify to your created bucket. During the creation, you will also get the auto-generated secret key, and the endpoint address.
|
||||

|
||||
5. In remotely-save setting page, input the address / bucket / access key / secret key. **Region being set to `us-east-1` is sufficient.** Enable "Bypass CORS", because usually that's what you want.
|
||||
|
||||
Click "check connectivity". (If you encounter an issue and sure the info are correct, please upgrade remotely-save to **version >= 0.3.29** and try again.)
|
||||
|
||||

|
||||
|
||||
6. Sync!
|
||||
|
||||
## And Issue Related To "Check Connectivity"
|
||||
|
||||
If you encounter an issue and sure the info are correct, please upgrade remotely-save to **version >= 0.3.29** and try again.
|
||||
|
||||
Cloudflare doesn't allow `HeadBucket` for access keys with "Object Read & Write". So it may be possible that checking connectivity is not ok but actual syncing is ok. New version >= 0.3.29 of the plugin fix this problem by using `ListObjects` instead of `HeadBucket`.
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:49a5cd07e538205ba733dc3ccb30e4e7da1290a4a80aefe08afac19fd2db2232
|
||||
size 467448
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:61fc0812fe96871eed507d2f6b7bbad790ab70e95ff5fe764f8f8b2ab8ce82c2
|
||||
size 153015
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:02f9c0fea36679e54c8c3fac979bc41d15ae4f7992b269bc2b67255059af1d2f
|
||||
size 579889
|
||||
@@ -0,0 +1,75 @@
|
||||
# AWS S3 Bucket: How to configure user's policy
|
||||
|
||||
## Attention
|
||||
|
||||
Please read the doc carefully and adjust the optional fields accordingly. The doc is not fully tested and contributions are welcome.
|
||||
|
||||
## AWS Official Docs
|
||||
|
||||
- <https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html>
|
||||
- <https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-actions.html>
|
||||
- <https://docs.aws.amazon.com/AmazonS3/latest/API/API_Operations.html>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Using the principle of least privilege is crucial for security when allowing a third party system to access your AWS resources.
|
||||
|
||||
**Prerequisites**: Ensure you have an AWS account and administrative access to manage IAM policies.
|
||||
|
||||
## Step 1: Create a new IAM Policy
|
||||
|
||||
1. Log in to your AWS Management Console.
|
||||
1. Navigate to the IAM Policies section.
|
||||
1. Create a new policy with the following configuration.
|
||||
|
||||
**Note**: `my-bucket` is a placeholder. For example, if your bucket's name is `obsidian-data`, the resource line should read `arn:aws:s3:::obsidian-data`.
|
||||
|
||||
```JSON
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "ObsidianObjects",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:HeadObject",
|
||||
"s3:PutObject",
|
||||
"s3:CopyObject",
|
||||
"s3:UploadPart",
|
||||
"s3:UploadPartCopy",
|
||||
"s3:ListMultipartUploads",
|
||||
"s3:AbortMultipartUpload",
|
||||
"s3:CompleteMultipartUpload",
|
||||
"s3:ListObjects",
|
||||
"s3:ListObjectsV2",
|
||||
"s3:ListParts",
|
||||
"s3:GetObject",
|
||||
"s3:GetObjectAttributes",
|
||||
"s3:DeleteObject",
|
||||
"s3:DeleteObjects"
|
||||
],
|
||||
"Resource": "arn:aws:s3:::my-bucket/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> The policy allows the Obsidian plugin to list, add, retrieve, and delete objects in the specified S3 bucket.
|
||||
|
||||
## Step 2: Attach the Policy to Obsidian user
|
||||
|
||||
1. Create a new user in the IAM console. (Never use your own root user, as it would have full access to your AWS account).
|
||||
1. When creating the user, select "Attach policy directly" and select the policy created.
|
||||
1. Edit the recent created user and go to "Security Credentials" tab to create your access key.
|
||||
1. Create an Access Key. If asked for a "use case", select "other"
|
||||
1. Use the credentials in the plugin settings. (NEVER share these credentials)
|
||||
|
||||
> PS. The bucket doesn't need to have a policy, only the user.
|
||||
|
||||
## Verifying the Policy
|
||||
|
||||
After attaching the policy, test it by trying to access the S3 bucket through the Obsidian plugin. Ensure that all intended actions can be performed without errors.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter permission errors, check the policy for typos in the bucket name or actions. Ensure the policy is attached to the correct user.
|
||||
@@ -0,0 +1,27 @@
|
||||
# MinIO
|
||||
|
||||
## Links
|
||||
|
||||
<https://min.io/>
|
||||
|
||||
## Steps
|
||||
|
||||
1. Configure your minio instance and get an account.
|
||||
2. Create an Access Key (during the creation, you will also get the auto-generated secret key).
|
||||

|
||||
3. Check or set the region.
|
||||

|
||||
4. Create a bucket.
|
||||

|
||||
5. In remotely-save setting page, input the address / bucket / access key / secret key. **Usually minio instances may need "S3 URL style"="Path Style".** Enable "Bypass CORS", because usually that's what you want.
|
||||

|
||||
6. Sync!
|
||||

|
||||
|
||||
## Ports In Address
|
||||
|
||||
Just type in the full address with `http(s)://` and `:port` in remotely-save settings, for example `http://192.168.31.198:9000`.
|
||||
|
||||
It's verified that everything is ok.
|
||||
|
||||

|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c384ec32a43fcac1e63dbc81d4fbb7fbfaffcd67d0f5a66104482a39475ad639
|
||||
size 323050
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:894a6a10b7f41754936d77a935b4a5a4ae722796fa5680c9c6d0dd53c3cdc1a2
|
||||
size 269705
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b25c1209875ad781a29d3ac1cde5bd9c2137622e3919987c4d4e5f77dc8e3ec7
|
||||
size 68997
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bfcc526281d2c6a9f1261f489603f3dd768f8b94f099a1b26649fa37c9318575
|
||||
size 315030
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83615a6c2fbf7ff74679b480d29d222db8835bc067aee33a646e4b8d500c3aca
|
||||
size 467575
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd1bb9a6e285a1e63b545d714ca269eb714e980a5177392b967556f1f1754afe
|
||||
size 147218
|
||||
@@ -0,0 +1,17 @@
|
||||
# Storj
|
||||
|
||||
## Links
|
||||
|
||||
<https://www.storj.io/>
|
||||
|
||||
## Steps
|
||||
|
||||
1. Register an account. Login.
|
||||
2. Create a bucket.
|
||||
3. Create S3 Credentials in Access Management. Allow all permissions for the bucket. Remember the access key and secret key and the end point. The end point is likely to be [`https://gateway.storjshare.io`](https://docs.storj.io/dcs/api/s3/s3-compatible-gateway).
|
||||

|
||||

|
||||
4. Input your credentials into remotely-save settings. Region [should be `global`](https://docs.storj.io/dcs/api/s3/s3-compatibility).
|
||||

|
||||
5. Check connectivity.
|
||||
6. Sync!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a3484f74f45fbdae6110de285edeff10c196c13160cb97f079da77843ae92e80
|
||||
size 386188
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:42593d98e1b154f4174a932088e72c25db9a976b5613f4d6ae21df1445b4559c
|
||||
size 278219
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7843c31153054eb4762dbaf3027c1042b0d7cb4dd774bf584788cd7a97ba4044
|
||||
size 571188
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tencent Cloud COS
|
||||
|
||||
English | [中文](./README.zh-cn.md)
|
||||
|
||||
## Link
|
||||
|
||||
- international <https://console.tencentcloud.com/cos>
|
||||
|
||||
## 步骤 Steps
|
||||
|
||||
The example shows the steps of China version. International version should be similar.
|
||||
|
||||
1. Create a bucket with **private read-write permissions** and recommendly enable server-side-encryption.
|
||||
2. In bucket list page, enter the bucket overview page of the bucket you just created. You should see the bucket name (your texts with the number of your account id), region, and access address.
|
||||

|
||||
3. In CAM page, create api key, and note down the SecretID and SecretKey.
|
||||

|
||||
4. **Remove the bucket name from your access address to obtain your endpoint address! If your access address on the website is `https://<bucket-name-with-number>.cos.<region>.myqcloud.com`, then the endpoint address you are going to use is `https://cos.<region>.myqcloud.com`.**
|
||||
5. In remotely-save settings page, enter your endpoint adress, SecretID, SecretKey,and bucket name.
|
||||

|
||||
6. Check Connectivity.
|
||||

|
||||
7. Sync!
|
||||
@@ -0,0 +1,23 @@
|
||||
# 腾讯云 COS
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 链接
|
||||
|
||||
- 中国区 <https://console.cloud.tencent.com/cos>
|
||||
|
||||
## 步骤
|
||||
|
||||
注意这里用中国区示例,国际区配置应该类似。
|
||||
|
||||
1. 在“存储桶列表”页,[“创建存储桶”](https://console.cloud.tencent.com/cos/bucket?action=create)。注意创建**私有读写**,建议打开服务端加密。
|
||||
2. 在桶列表页,点击刚刚存储的桶,进入概览页。可以见到桶名称(一般来说是之前指定的英文加账号数字),地域,访问域名。记录下来。
|
||||

|
||||
3. 在[“访问管理页”](https://console.cloud.tencent.com/cam/capi) ,“API 密钥管理”,“创建密钥”,要记录 SecretID 和 SecretKey。
|
||||

|
||||
4. **把桶名称从访问域名移除,才是你即将输入的服务地址!假如你在腾讯云网站看到访问域名是 `https://<bucket-name-with-number>.cos.<region>.myqcloud.com`,那么“服务地址”是 `https://cos.<region>.myqcloud.com`.**
|
||||
5. 在 remotely-save 设置,输入服务地址,SecretID,SecretKey,和 桶名称。
|
||||

|
||||
6. 检查连接。
|
||||

|
||||
7. 可以同步了!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:08b47288686c8ef8c6426776f7df52411efe431966e8f7f2f9a3a8836ee4d6c6
|
||||
size 323127
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94c0ec5db7a75c54c36224f067659682e271aea2e61b1ee50d046cd493ba72ac
|
||||
size 58410
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f51f542756def80a77404e47f9793d1cd6917df5ab6e09d5a0ee1f7191855dc6
|
||||
size 425021
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:55064233031b44066fb7b77e17ba1eb1d0ae8398235a343cc7e137b07aaf7a39
|
||||
size 668954
|
||||
@@ -0,0 +1,23 @@
|
||||
# AList
|
||||
|
||||
English | [中文](./README.zh-cn.md)
|
||||
|
||||
## Links
|
||||
|
||||
- English official website: <https://alist.nn.ci/> and <https://alist.nn.ci/guide/webdav.html>
|
||||
|
||||
## Steps
|
||||
|
||||
1. Install and run AList. Get the account and password. Login using the web page.
|
||||
2. Add new storage. Pay attention to the mount path. The screenshot shows the mount path as `/alisttest davpath`.
|
||||

|
||||

|
||||
3. Construct the webdav address as: **http(s)://domain** + **port** + **`/dav`** + **mount path**, and the space inside the mount path should be replaced with `%20`:
|
||||
```
|
||||
http[s]://domain:port/dav/[mountpath url encoded]
|
||||
http://127.0.0.1:5244/dav/alisttest%20davpath
|
||||
```
|
||||
4. In remotely-save setting page, select webdav type, then input the **full address with mount path**/account/password.
|
||||

|
||||
5. In remotely-save setting page, click "Check Connectivity".
|
||||
6. Sync!
|
||||
@@ -0,0 +1,23 @@
|
||||
# AList
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 链接
|
||||
|
||||
- 中文官网:<https://alist.nn.ci/zh/> 和 <https://alist.nn.ci/zh/guide/webdav.html>
|
||||
|
||||
## 步骤
|
||||
|
||||
1. 安装和使用 AList。获取账号名和密码。在网页上登录。
|
||||
2. 新建挂载,检查挂载路径。如图所示是 `/alisttest davpath`。
|
||||

|
||||

|
||||
3. 从而构建 webdav 网址如下,**http(s)://域名** + **端口** + **`/dav`** + **挂载路径**,其中挂载路径中假如有空格,换成 `%20`:
|
||||
```
|
||||
http[s]://domain:port/dav/[mountpath url encoded]
|
||||
http://127.0.0.1:5244/dav/alisttest%20davpath
|
||||
```
|
||||
4. 在 remotely-save 设置,输入**带域名端口`/dav`和挂载路径的网址**、账号、密码。
|
||||

|
||||
5. 在 remotely-save 设置,检查连接。
|
||||
6. 同步文件!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea05f67782b8f84518f13cf5587df719bdaa15f9e35b79f2801f91e695de6480
|
||||
size 46798
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dcaaa5e64e894f0e9c8b0242b64bf4dd364c16d7314b09bc004f7ed0a0f1e0e5
|
||||
size 60853
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b792320e2a4aaa16a17d82e80225ab35da05fd1f407fe067de72651beca088e
|
||||
size 426289
|
||||
@@ -0,0 +1,10 @@
|
||||
If you are using Obsidian desktop >= 0.13.25 or iOS >= 1.1.1, you can skip this CORS part.
|
||||
|
||||
If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
|
||||
|
||||
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
|
||||
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
|
||||
- **Unofficial** workaround: NextCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can install [WebAppPassword](https://apps.nextcloud.com/apps/webapppassword) app, and add `app://obsidian.md`, `capacitor://localhost`, `http://localhost` to `Allowed origins`
|
||||
- **Unofficial** workaround: OwnCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can download `.tar.gz` of `WebAppPassword` above and manually install and configure it on their instances.
|
||||
- [Apache is also possible](./webdav_apache_cors.md).
|
||||
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
|
||||
@@ -0,0 +1,15 @@
|
||||
# InfiniCLOUD (formally TeraCLOUD) Webdav
|
||||
|
||||
## Link
|
||||
|
||||
<https://infini-cloud.net/en/>
|
||||
|
||||
## Steps
|
||||
|
||||
1. Register an acount.
|
||||
2. Go to <https://infini-cloud.net/en/modules/mypage/usage/>, in section "Apps Connection", enable "Turn on Apps Connection". Here you get the address and account and webdav password (different from your account password):
|
||||

|
||||
3. In remotely-save setting page, select webdav type, then input the address/account/**webdav password**(not your account password).
|
||||

|
||||
4. In remotely-save setting page, click "Check Connectivity".
|
||||
5. Sync!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0e16bc9d86b30ab907fc176087701474f4ca2b8d887f649302f2e184f69d0cbc
|
||||
size 373845
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8d707f6997df57b9c6b74d72bcfd567995d15062269761ee404e6596a266a241
|
||||
size 91012
|
||||
@@ -0,0 +1,22 @@
|
||||
# JianGuoYun/NutStore
|
||||
|
||||
English | [中文](./README.zh-cn.md)
|
||||
|
||||
## Link
|
||||
|
||||
<https://www.jianguoyun.com/>
|
||||
|
||||
## Attentions!!!
|
||||
|
||||
JianGuoYun/NutStore has api limits. The plugin may generate many queries, and it's possible to reach the api limits if there are many files, then do not work properly. It's not a bug and there's no way to fix this situation.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Be aware that JianGuoYun/NutStore has api limits, and the plugin may not work properly because of this.**
|
||||
2. Register an account.
|
||||
3. Go to "settings"->"Security", click "Add Application", then obtain the WebDAV account (email), and WebDAV password (a string different from web site password).
|
||||

|
||||
4. Input the WebDAV address, account, password, **Depth Header Sent To Servers="only supports depth='1'"** in remotely-save settings.
|
||||

|
||||
5. In remotely-save setting page, click "Check Connectivity".
|
||||
6. Sync!
|
||||
@@ -0,0 +1,22 @@
|
||||
# 坚果云
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 链接
|
||||
|
||||
<https://www.jianguoyun.com/>
|
||||
|
||||
## 注意!!!
|
||||
|
||||
坚果云有限制 api 数量等设定。本插件会产生若干查询,如果文件较多很容易触发 api 上限,从而工作不正常。这不是插件 bug,也没有办法解决。
|
||||
|
||||
## 步骤
|
||||
|
||||
1. **知悉坚果云有 api 限制,本插件可能因此工作不正常。**
|
||||
2. 注册账号,登录。
|
||||
3. 去“个人信息”->“安全”,“添加应用”,从而获取了 webDAV 账号(应该是 email)和 WebDAV 密码(一串特殊的字符,不等于网站密码)。
|
||||

|
||||
4. 在 remotely-save 设置,输入网址、账号、密码、**“发送到服务器的 Depth Header”设置为“只支持 depth='1'”**。
|
||||

|
||||
5. 在 remotely-save 设置,检查连接。
|
||||
6. 同步文件!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fd5aaab1e1ac0ea8996517c819bf5ac0d2a828483037419ba289b538957f4752
|
||||
size 515913
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:112778176af954e16c93c582d968e7c14e5b950d64facce8593855f5fb75fbcc
|
||||
size 431527
|
||||
@@ -0,0 +1,17 @@
|
||||
# ownCloud Webdav
|
||||
|
||||
## Link
|
||||
|
||||
<https://owncloud.com/>
|
||||
|
||||
# Steps
|
||||
|
||||
1. Create an account.
|
||||
2. Login.
|
||||
3. In the Settings position, enable the "Show hidden files" and find out the WebDAV address.
|
||||

|
||||
4. Input the WebDAV address, account, password, **Depth Header Sent To Servers="only supports depth='1'"** in remotely-save settings.
|
||||

|
||||
5. In remotely-save setting page, click "Check Connectivity".
|
||||
6. Sync!
|
||||

|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e0d025b4ccf1be7fa2cc74e914e9a1a89a8d8f12a1b917bd4b26b975bf208294
|
||||
size 27796
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b3d24d416016b2676a86166fcc35fa89f8befb7a872503b8da2be4374ef360ce
|
||||
size 53503
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2e8bf9a51e5d5bed66fb45fe085f37943bd90a51b33ef752d898c5b2caa71c2d
|
||||
size 448797
|
||||
@@ -0,0 +1,55 @@
|
||||
# Synology Webdav Server
|
||||
|
||||
English | [中文](./README.zh-cn.md)
|
||||
|
||||
## Link
|
||||
|
||||
<https://kb.synology.com/en-global/DSM/tutorial/How_to_access_files_on_Synology_NAS_with_WebDAV>
|
||||
|
||||
## Attention
|
||||
|
||||
The tutorial author (the author of Remotely Save) is NOT an expert of NAS / Synology. Please read the doc carefully and change it to accommodate your needs by yourself.
|
||||
|
||||
**It's dangerous to expose your NAS into public Internet if you don't know how to set up firewalls and other protections.**
|
||||
|
||||
## Steps
|
||||
|
||||
Synology DSM 7 is used in this tutorial.
|
||||
|
||||
1. Create a new shared folder if you don't have one. For this tutorial, a new shared folder `share2` is created. You should assing a proper user to read / write the shared folder.
|
||||

|
||||
|
||||
2. Assuming you want to sync your vault into sub folder `哈哈哈/sub folder`, please create the sub folder(s) correspondingly inide the shared folder `share2`.
|
||||
|
||||
3. Install webdav server from package center.
|
||||

|
||||
|
||||
4. Enter the webdav server settings.
|
||||
|
||||
5. If you know how to configure https certificates correctly, you are strongly recommend to enable https.
|
||||
|
||||
For the demonstration purpose, this tutorial enable http server for the later steps.
|
||||
|
||||
Also "Enable DavDepthInfinity", which could speed up the plugin runnings greatly.
|
||||
|
||||
"Apply".
|
||||
|
||||

|
||||
|
||||
6. In Remotely Save settings, you should input your address as:
|
||||
|
||||
`http(s)://<your synology ip or domain>:<port>/<shared folder>/<sub folders>`
|
||||
|
||||
For example, in the tutorial, the proper url should be:
|
||||
|
||||
`http://<ip>:5000/share2/哈哈哈/sub folder`
|
||||
|
||||
Username and password should be the user you configured before with read / write permissions to `share2`.
|
||||
|
||||
Depth header should be "supports depth="infinity"".
|
||||
|
||||
Check connectivity!
|
||||
|
||||

|
||||
|
||||
7. Sync!
|
||||
@@ -0,0 +1,56 @@
|
||||
# 群晖 Webdav Server
|
||||
|
||||
[English](./README.md) | 中文
|
||||
|
||||
## 链接
|
||||
|
||||
<https://kb.synology.cn/zh-cn/DSM/tutorial/How_to_access_files_on_Synology_NAS_with_WebDAV>
|
||||
|
||||
## 注意
|
||||
|
||||
教程作者(Remotely Save 作者)**不是** NAS、群晖专家。请仔细阅读文档,并自行改动以适应您自身需求。
|
||||
|
||||
**没有设置防火墙和其他保护措施的话,将 NAS 暴露到公网上非常危险。**
|
||||
|
||||
## 步骤
|
||||
|
||||
本教程有用到群晖 DSM 7。
|
||||
|
||||
1. 创建共享文件夹。本教程示例创建了 `share2`。你需要允许某个账号对此的读写权限。
|
||||
|
||||

|
||||
|
||||
2. 假设之后你想同步你的库到子文件夹,`哈哈哈/sub folder`,请先在共享文件夹 `share2` 底下创建好。
|
||||
|
||||
3. 从套件中心安装 webdav server 。
|
||||

|
||||
|
||||
4. 进入 webdav server 设置。
|
||||
|
||||
5. 如果你知道如何正确配置 https 证书的话,强烈建议开启 https。
|
||||
|
||||
本教程简化示例,开启了 http。
|
||||
|
||||
也设置“Enable DavDepthInfinity”,这可以加速插件连接速度。
|
||||
|
||||
“Apply”。
|
||||
|
||||

|
||||
|
||||
6. 在 Remotely Save 设置页,你的地址应如下格式输入:
|
||||
|
||||
`http(s)://<your synology ip or domain>:<port>/<shared folder>/<sub folders>`
|
||||
|
||||
比如说,本教程里,正确的地址类似于:
|
||||
|
||||
`http://<ip>:5000/share2/哈哈哈/sub folder`
|
||||
|
||||
用户名和密码是你之前配置了允许读写 `share2` 的那个账号。
|
||||
|
||||
Depth 设置应为“supports depth="infinity"”。
|
||||
|
||||
检查连接!
|
||||
|
||||

|
||||
|
||||
7. 同步!
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:71e61c25db793c00d4406ea0f46745941e16e268a41b7328b03a183ab0706ec8
|
||||
size 59700
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f0725099989b5496048c0e3cb284b4f6537d66866b4618c8e73ee758cb2b642
|
||||
size 174986
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7bc6d03a9deb0e8babd2ed102e7b85ee1062797abf1146ae71f80237a852ff10
|
||||
size 522084
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ba3781f24b5d65ee6915d496f7542c569116141cdced24ca505f4373d9efa9df
|
||||
size 536163
|
||||
@@ -18,7 +18,7 @@ The list is for information purposes only.
|
||||
| [MinIO](https://min.io/) | ? | ? | | | | |
|
||||
| [WsgiDAV](https://github.com/mar10/wsgidav) | Yes | | Yes | | Yes | CORS rules can be set. |
|
||||
| [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? |
|
||||
| NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| NextCloud | Yes | | Yes | | Yes? | No CORS config by default. |
|
||||
| OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| Seafile | Yes | | Yes | | Yes? | No CORS config by default. |
|
||||
| `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. |
|
||||
@@ -26,7 +26,7 @@ The list is for information purposes only.
|
||||
| [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. |
|
||||
| Dropbox | Yes | | | Yes | | |
|
||||
| OneDrive for personal | Yes | | | Yes | | |
|
||||
| OneDrive for Business | In the plan | | | ? | | |
|
||||
| OneDrive for Business | Yes | | | ? | | |
|
||||
| Google Drive | In the plan | | | ? | | |
|
||||
| [Box](https://www.box.com/) | ? | | | May be possible but needs further development. | | |
|
||||
| Google Cloud Storage | ? | | | May be possible but needs further development. | | |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Sync Algorithm
|
||||
|
||||
- [v1](./v1/README.md)
|
||||
- [v2](./v2/README.md)
|
||||
- v3
|
||||
- [intro doc for end users](./v3/intro.md)
|
||||
- [design doc](./v3/design.md)
|
||||
@@ -0,0 +1,4 @@
|
||||
# Sync Algorithm V3
|
||||
|
||||
- [intro doc for end users](./intro.md)
|
||||
- [design doc](./design.md)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Sync Algorithm V3
|
||||
|
||||
Drafted on 20240117.
|
||||
|
||||
An absolutely better sync algorithm. Better for tracking deletions and better for subbranching.
|
||||
|
||||
## Huge Thanks
|
||||
|
||||
Basically a combination of algorithm v2 + [synclone](https://github.com/Jwink3101/syncrclone/blob/master/docs/algorithm.md) + [rsinc](https://github.com/ConorWilliams/rsinc) + (some of rclone [bisync](https://rclone.org/bisync/)). All of the later three are released under MIT License so no worries about the licenses.
|
||||
|
||||
## Features
|
||||
|
||||
Must have
|
||||
|
||||
1. true deletion detection
|
||||
2. deletion protection (blocking) with a setting
|
||||
3. transaction from the old algorithm
|
||||
4. user warning show up, **new algorithm needs all clients to be updated!** (deliberately corrput the metadata file??)
|
||||
5. filters
|
||||
6. conflict warning
|
||||
7. partial sync
|
||||
|
||||
Nice to have
|
||||
|
||||
1. true time and hash
|
||||
2. conflict rename
|
||||
|
||||
## Description
|
||||
|
||||
We have _five_ input sources:
|
||||
|
||||
1. local all files
|
||||
2. remote all files
|
||||
3. _local previous succeeded sync history_
|
||||
4. local deletions
|
||||
5. remote deletions.
|
||||
|
||||
Init run, consuming remote deletions :
|
||||
|
||||
Change history data into _local previous succeeded sync history_.
|
||||
|
||||
Later runs, use the first, second, third sources **only**.
|
||||
|
||||
Bidirectional table is modified based on synclone and rsinc. Incremental push / pull only tables is further modified based on the bidirectional table. The number inside the table cell is the decision branch in the code.
|
||||
|
||||
Bidirectional:
|
||||
|
||||
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
|
||||
| --------------- | ------------------ | ------------------------- | ------------------ | ------------------------- |
|
||||
| local unchanged | (02/21) do nothing | (09) pull | (07) delete local | (??) conflict |
|
||||
| local modified | (10) push | (16/17/18/19/20) conflict | (08) push | (??) conflict |
|
||||
| local deleted | (04) delete remote | (05) pull | (01) clean history | (03) pull |
|
||||
| local created | (??) conflict | (??) conflict | (06) push | (11/12/13/14/15) conflict |
|
||||
|
||||
Incremental push only:
|
||||
|
||||
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
|
||||
| --------------- | ---------------------------- | ---------------------------- | ---------------------- | ---------------------------- |
|
||||
| local unchanged | (02/21) do nothing | **(26) conflict push** | **(32) conflict push** | (??) conflict |
|
||||
| local modified | (10) push | **(25) conflict push** | (08) push | (??) conflict |
|
||||
| local deleted | **(29) conflict do nothing** | **(30) conflict do nothing** | (01) clean history | **(28) conflict do nothing** |
|
||||
| local created | (??) conflict | (??) conflict | (06) push | **(23) conflict push** |
|
||||
|
||||
Incremental pull only:
|
||||
|
||||
| local\remote | remote unchanged | remote modified | remote deleted | remote created |
|
||||
| --------------- | ---------------------- | ---------------------- | ---------------------------- | ---------------------- |
|
||||
| local unchanged | (02/21) do nothing | (09) pull | **(33) conflict do nothing** | (??) conflict |
|
||||
| local modified | **(27) conflict pull** | **(24) conflict pull** | **(34) conflict do nothing** | (??) conflict |
|
||||
| local deleted | **(35) conflict pull** | (05) pull | (01) clean history | (03) pull |
|
||||
| local created | (??) conflict | (??) conflict | **(31) conflict do nothing** | **(22) conflict pull** |
|
||||
@@ -0,0 +1,13 @@
|
||||
# Introduction To Sync Algorithm V3
|
||||
|
||||
- [x] sync conflict: keep newer
|
||||
- [x] sync conflict: keep larger
|
||||
- [ ] sync conflict: keep both and rename
|
||||
- [ ] sync conflict: show warning
|
||||
- [x] deletion: true deletion status computation
|
||||
- [x] meta data: no remote meta data any more
|
||||
- [x] migration: old data auto transfer to new db (hopefully)
|
||||
- [x] sync direction: incremental push only
|
||||
- [x] sync direction: incremental pull only
|
||||
- [x] sync protection: warning based on the threshold
|
||||
- [ ] partial sync: better sync on save
|
||||
+15
-2
@@ -18,7 +18,7 @@ const DEFAULT_ONEDRIVE_CLIENT_ID = process.env.ONEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_ONEDRIVE_AUTHORITY = process.env.ONEDRIVE_AUTHORITY || "";
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
@@ -33,11 +33,13 @@ esbuild
|
||||
"fs",
|
||||
"tls",
|
||||
"net",
|
||||
"http",
|
||||
"https",
|
||||
// ...builtins
|
||||
],
|
||||
inject: ["./esbuild.injecthelper.mjs"],
|
||||
format: "cjs",
|
||||
watch: !prod,
|
||||
// watch: !prod, // no longer valid in esbuild 0.17
|
||||
target: "es2016",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
@@ -53,4 +55,15 @@ esbuild
|
||||
"process.env.DEBUG": `undefined`, // ugly fix
|
||||
},
|
||||
})
|
||||
.then((context) => {
|
||||
if (process.argv.includes("--watch")) {
|
||||
// Enable watch mode
|
||||
context.watch();
|
||||
} else {
|
||||
// Build once and exit if not in watch mode
|
||||
context.rebuild().then((result) => {
|
||||
context.dispose();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.4.4",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
"authorUrl": "https://github.com/fyears",
|
||||
"isDesktopOnly": false,
|
||||
"fundingUrl": "https://github.com/remotely-save/donation"
|
||||
}
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.3.23",
|
||||
"version": "0.3.40",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
"authorUrl": "https://github.com/fyears",
|
||||
"isDesktopOnly": false
|
||||
"isDesktopOnly": false,
|
||||
"fundingUrl": "https://github.com/remotely-save/donation"
|
||||
}
|
||||
|
||||
+56
-55
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.3.23",
|
||||
"version": "0.4.4",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
"dev2": "node esbuild.config.mjs --watch",
|
||||
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"build": "webpack --mode production",
|
||||
"dev": "webpack --mode development --watch",
|
||||
"format": "npx prettier --write .",
|
||||
"format": "npx prettier --trailing-comma es5 --write .",
|
||||
"clean": "npx rimraf main.js",
|
||||
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'"
|
||||
},
|
||||
@@ -23,73 +23,74 @@
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.19.0",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^17.0.30",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"builtin-modules": "^3.2.0",
|
||||
"chai": "^4.3.6",
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@types/chai": "^4.3.11",
|
||||
"@types/chai-as-promised": "^7.1.8",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"chai": "^4.3.10",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"esbuild": "^0.14.38",
|
||||
"jsdom": "^19.0.0",
|
||||
"mocha": "^9.2.2",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-loader": "^9.2.9",
|
||||
"ts-node": "^10.7.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"esbuild": "^0.19.9",
|
||||
"jsdom": "^23.0.1",
|
||||
"mocha": "^10.2.0",
|
||||
"npm-check-updates": "^16.14.12",
|
||||
"obsidian": "^1.4.11",
|
||||
"prettier": "^3.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"webdav-server": "^2.6.2",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.81.0",
|
||||
"@aws-sdk/fetch-http-handler": "^3.78.0",
|
||||
"@aws-sdk/lib-storage": "^3.81.0",
|
||||
"@aws-sdk/protocol-http": "^3.78.0",
|
||||
"@aws-sdk/querystring-builder": "^3.78.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.78.0",
|
||||
"@aws-sdk/types": "^3.78.0",
|
||||
"@azure/msal-node": "^1.8.0",
|
||||
"@aws-sdk/client-s3": "^3.474.0",
|
||||
"@aws-sdk/lib-storage": "^3.474.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.474.0",
|
||||
"@aws-sdk/types": "^3.468.0",
|
||||
"@azure/msal-node": "^2.6.0",
|
||||
"@fyears/tsqueue": "^1.0.1",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.2",
|
||||
"acorn": "^8.7.1",
|
||||
"aggregate-error": "^4.0.0",
|
||||
"assert": "^2.0.0",
|
||||
"aws-crt": "^1.12.1",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@smithy/fetch-http-handler": "^2.3.1",
|
||||
"@smithy/protocol-http": "^3.0.11",
|
||||
"@smithy/querystring-builder": "^2.0.15",
|
||||
"acorn": "^8.11.2",
|
||||
"aggregate-error": "^5.0.0",
|
||||
"assert": "^2.1.0",
|
||||
"aws-crt": "^1.20.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"delay": "^5.0.0",
|
||||
"dropbox": "^10.28.0",
|
||||
"emoji-regex": "^10.1.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"dropbox": "^10.34.0",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"lucide": "^0.35.0",
|
||||
"loglevel": "^1.8.1",
|
||||
"lucide": "^0.298.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^3.3.3",
|
||||
"obsidian": "^0.14.6",
|
||||
"p-queue": "^7.2.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"p-queue": "^8.0.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"qrcode": "^1.5.0",
|
||||
"rfc4648": "^1.5.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
"rfc4648": "^1.5.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webdav": "^4.9.0",
|
||||
"url": "^0.11.3",
|
||||
"util": "^0.12.5",
|
||||
"webdav": "^5.3.1",
|
||||
"webdav-fs": "^4.0.1",
|
||||
"xregexp": "^5.1.0"
|
||||
"xregexp": "^5.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
+125
-38
@@ -21,9 +21,17 @@ export interface S3Config {
|
||||
s3AccessKeyID: string;
|
||||
s3SecretAccessKey: string;
|
||||
s3BucketName: string;
|
||||
bypassCorsLocally?: boolean;
|
||||
|
||||
partsConcurrency?: number;
|
||||
forcePathStyle?: boolean;
|
||||
remotePrefix?: string;
|
||||
|
||||
useAccurateMTime?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
bypassCorsLocally?: boolean;
|
||||
}
|
||||
|
||||
export interface DropboxConfig {
|
||||
@@ -40,9 +48,10 @@ export interface DropboxConfig {
|
||||
|
||||
export type WebdavAuthType = "digest" | "basic";
|
||||
export type WebdavDepthType =
|
||||
| "auto_unknown"
|
||||
| "auto_1"
|
||||
| "auto_infinity"
|
||||
| "auto" // deprecated on 20240116
|
||||
| "auto_unknown" // deprecated on 20240116
|
||||
| "auto_1" // deprecated on 20240116
|
||||
| "auto_infinity" // deprecated on 20240116
|
||||
| "manual_1"
|
||||
| "manual_infinity";
|
||||
|
||||
@@ -51,9 +60,14 @@ export interface WebdavConfig {
|
||||
username: string;
|
||||
password: string;
|
||||
authType: WebdavAuthType;
|
||||
manualRecursive: boolean; // deprecated in 0.3.6, use depth
|
||||
|
||||
depth?: WebdavDepthType;
|
||||
remoteBaseDir?: string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
manualRecursive: boolean; // deprecated in 0.3.6, use depth
|
||||
}
|
||||
|
||||
export interface OnedriveConfig {
|
||||
@@ -69,6 +83,11 @@ export interface OnedriveConfig {
|
||||
remoteBaseDir?: string;
|
||||
}
|
||||
|
||||
export type SyncDirectionType =
|
||||
| "bidirectional"
|
||||
| "incremental_pull_only"
|
||||
| "incremental_push_only";
|
||||
|
||||
export interface RemotelySavePluginSettings {
|
||||
s3: S3Config;
|
||||
webdav: WebdavConfig;
|
||||
@@ -79,26 +98,41 @@ export interface RemotelySavePluginSettings {
|
||||
currLogLevel?: string;
|
||||
autoRunEveryMilliseconds?: number;
|
||||
initRunAfterMilliseconds?: number;
|
||||
agreeToUploadExtraMetadata?: boolean;
|
||||
syncOnSaveAfterMilliseconds?: number;
|
||||
|
||||
concurrency?: number;
|
||||
syncConfigDir?: boolean;
|
||||
syncUnderscoreItems?: boolean;
|
||||
lang?: LangTypeAndAuto;
|
||||
logToDB?: boolean;
|
||||
agreeToUseSyncV3?: boolean;
|
||||
skipSizeLargerThan?: number;
|
||||
ignorePaths?: string[];
|
||||
enableStatusBarInfo?: boolean;
|
||||
deleteToWhere?: "system" | "obsidian";
|
||||
conflictAction?: ConflictActionType;
|
||||
howToCleanEmptyFolder?: EmptyFolderCleanType;
|
||||
|
||||
protectModifyPercentage?: number;
|
||||
syncDirection?: SyncDirectionType;
|
||||
|
||||
obfuscateSettingFile?: boolean;
|
||||
|
||||
enableMobileStatusBar?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
agreeToUploadExtraMetadata?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
vaultRandomID?: string;
|
||||
}
|
||||
|
||||
export interface RemoteItem {
|
||||
key: string;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||
etag?: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
logToDB?: boolean;
|
||||
}
|
||||
|
||||
export const COMMAND_URI = "remotely-save";
|
||||
@@ -116,32 +150,76 @@ export interface UriParams {
|
||||
// 80 days
|
||||
export const OAUTH2_FORCE_EXPIRE_MILLISECONDS = 1000 * 60 * 60 * 24 * 80;
|
||||
|
||||
type DecisionTypeForFile =
|
||||
| "skipUploading" // special, mtimeLocal === mtimeRemote
|
||||
| "uploadLocalDelHistToRemote" // "delLocalIfExists && delRemoteIfExists && cleanLocalDelHist && uploadLocalDelHistToRemote"
|
||||
| "keepRemoteDelHist" // "delLocalIfExists && delRemoteIfExists && cleanLocalDelHist && keepRemoteDelHist"
|
||||
| "uploadLocalToRemote" // "skipLocal && uploadLocalToRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
||||
| "downloadRemoteToLocal"; // "downloadRemoteToLocal && skipRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
||||
export type EmptyFolderCleanType = "skip" | "clean_both";
|
||||
|
||||
type DecisionTypeForFileSize =
|
||||
| "skipUploadingTooLarge"
|
||||
| "skipDownloadingTooLarge"
|
||||
| "skipUsingLocalDelTooLarge"
|
||||
| "skipUsingRemoteDelTooLarge"
|
||||
| "errorLocalTooLargeConflictRemote"
|
||||
| "errorRemoteTooLargeConflictLocal";
|
||||
export type ConflictActionType = "keep_newer" | "keep_larger" | "rename_both";
|
||||
|
||||
type DecisionTypeForFolder =
|
||||
| "createFolder"
|
||||
| "uploadLocalDelHistToRemoteFolder"
|
||||
| "keepRemoteDelHistFolder"
|
||||
| "skipFolder";
|
||||
export type DecisionTypeForMixedEntity =
|
||||
| "only_history"
|
||||
| "equal"
|
||||
| "local_is_modified_then_push"
|
||||
| "remote_is_modified_then_pull"
|
||||
| "local_is_created_then_push"
|
||||
| "remote_is_created_then_pull"
|
||||
| "local_is_deleted_thus_also_delete_remote"
|
||||
| "remote_is_deleted_thus_also_delete_local"
|
||||
| "conflict_created_then_keep_local"
|
||||
| "conflict_created_then_keep_remote"
|
||||
| "conflict_created_then_keep_both"
|
||||
| "conflict_created_then_do_nothing"
|
||||
| "conflict_modified_then_keep_local"
|
||||
| "conflict_modified_then_keep_remote"
|
||||
| "conflict_modified_then_keep_both"
|
||||
| "folder_existed_both_then_do_nothing"
|
||||
| "folder_existed_local_then_also_create_remote"
|
||||
| "folder_existed_remote_then_also_create_local"
|
||||
| "folder_to_be_created"
|
||||
| "folder_to_skip"
|
||||
| "folder_to_be_deleted";
|
||||
|
||||
export type DecisionType =
|
||||
| DecisionTypeForFile
|
||||
| DecisionTypeForFileSize
|
||||
| DecisionTypeForFolder;
|
||||
/**
|
||||
* uniform representation
|
||||
* everything should be flat and primitive, so that we can copy.
|
||||
*/
|
||||
export interface Entity {
|
||||
key?: string;
|
||||
keyEnc?: string;
|
||||
keyRaw: string;
|
||||
mtimeCli?: number;
|
||||
mtimeCliFmt?: string;
|
||||
mtimeSvr?: number;
|
||||
mtimeSvrFmt?: string;
|
||||
prevSyncTime?: number;
|
||||
prevSyncTimeFmt?: string;
|
||||
size?: number; // might be unknown or to be filled
|
||||
sizeEnc?: number;
|
||||
sizeRaw: number;
|
||||
hash?: string;
|
||||
etag?: string;
|
||||
}
|
||||
|
||||
export interface UploadedType {
|
||||
entity: Entity;
|
||||
mtimeCli?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacement of FileOrFolderMixedState
|
||||
*/
|
||||
export interface MixedEntity {
|
||||
key: string;
|
||||
local?: Entity;
|
||||
prevSync?: Entity;
|
||||
remote?: Entity;
|
||||
|
||||
decisionBranch?: number;
|
||||
decision?: DecisionTypeForMixedEntity;
|
||||
conflictAction?: ConflictActionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export interface FileOrFolderMixedState {
|
||||
key: string;
|
||||
existLocal?: boolean;
|
||||
@@ -156,7 +234,7 @@ export interface FileOrFolderMixedState {
|
||||
sizeRemoteEnc?: number;
|
||||
changeRemoteMtimeUsingMapping?: boolean;
|
||||
changeLocalMtimeUsingMapping?: boolean;
|
||||
decision?: DecisionType;
|
||||
decision?: string; // old DecisionType is deleted, fallback to string
|
||||
decisionBranch?: number;
|
||||
syncDone?: "done";
|
||||
remoteEncryptedKey?: string;
|
||||
@@ -170,6 +248,7 @@ export interface FileOrFolderMixedState {
|
||||
export const API_VER_STAT_FOLDER = "0.13.27";
|
||||
export const API_VER_REQURL = "0.13.26"; // desktop ver 0.13.26, iOS ver 1.1.1
|
||||
export const API_VER_REQURL_ANDROID = "0.14.6"; // Android ver 1.2.1
|
||||
export const API_VER_ENSURE_REQURL_OK = "1.0.0"; // always bypass CORS here
|
||||
|
||||
export const VALID_REQURL =
|
||||
(!Platform.isAndroidApp && requireApiVersion(API_VER_REQURL)) ||
|
||||
@@ -180,4 +259,12 @@ export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
|
||||
"sync_plans_hist_exported_on_";
|
||||
export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
|
||||
|
||||
export type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
||||
export type SyncTriggerSourceType =
|
||||
| "manual"
|
||||
| "auto"
|
||||
| "dry"
|
||||
| "autoOnceInit"
|
||||
| "auto_sync_on_save";
|
||||
|
||||
export const REMOTELY_SAVE_VERSION_2022 = "0.3.25";
|
||||
export const REMOTELY_SAVE_VERSION_2024PREPARE = "0.3.32";
|
||||
|
||||
+9
-11
@@ -3,8 +3,6 @@ import { reverseString } from "./misc";
|
||||
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const DEFAULT_README: string =
|
||||
"The file contains sensitive info, so DO NOT take screenshot of, copy, or share it to anyone! It's also generated automatically, so do not edit it manually.";
|
||||
|
||||
@@ -19,10 +17,10 @@ interface MessyConfigType {
|
||||
export const messyConfigToNormal = (
|
||||
x: MessyConfigType | RemotelySavePluginSettings | null | undefined
|
||||
): RemotelySavePluginSettings | null | undefined => {
|
||||
// log.debug("loading, original config on disk:");
|
||||
// log.debug(x);
|
||||
// console.debug("loading, original config on disk:");
|
||||
// console.debug(x);
|
||||
if (x === null || x === undefined) {
|
||||
log.debug("the messy config is null or undefined, skip");
|
||||
console.debug("the messy config is null or undefined, skip");
|
||||
return x as any;
|
||||
}
|
||||
if ("readme" in x && "d" in x) {
|
||||
@@ -35,12 +33,12 @@ export const messyConfigToNormal = (
|
||||
}) as Buffer
|
||||
).toString("utf-8")
|
||||
);
|
||||
// log.debug("loading, parsed config is:");
|
||||
// log.debug(y);
|
||||
// console.debug("loading, parsed config is:");
|
||||
// console.debug(y);
|
||||
return y;
|
||||
} else {
|
||||
// return as is
|
||||
// log.debug("loading, parsed config is the same");
|
||||
// console.debug("loading, parsed config is the same");
|
||||
return x;
|
||||
}
|
||||
};
|
||||
@@ -52,7 +50,7 @@ export const normalConfigToMessy = (
|
||||
x: RemotelySavePluginSettings | null | undefined
|
||||
) => {
|
||||
if (x === null || x === undefined) {
|
||||
log.debug("the normal config is null or undefined, skip");
|
||||
console.debug("the normal config is null or undefined, skip");
|
||||
return x;
|
||||
}
|
||||
const y = {
|
||||
@@ -63,7 +61,7 @@ export const normalConfigToMessy = (
|
||||
})
|
||||
),
|
||||
};
|
||||
// log.debug("encoding, encoded config is:");
|
||||
// log.debug(y);
|
||||
// console.debug("encoding, encoded config is:");
|
||||
// console.debug(y);
|
||||
return y;
|
||||
};
|
||||
|
||||
+4
-41
@@ -1,10 +1,7 @@
|
||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||
|
||||
import type { SyncPlanType } from "./sync";
|
||||
import {
|
||||
readAllSyncPlanRecordTextsByVault,
|
||||
readAllLogRecordTextsByVault,
|
||||
} from "./localdb";
|
||||
import { readAllSyncPlanRecordTextsByVault } from "./localdb";
|
||||
import type { InternalDBs } from "./localdb";
|
||||
import { mkdirpInVault } from "./misc";
|
||||
import {
|
||||
@@ -14,8 +11,6 @@ import {
|
||||
FileOrFolderMixedState,
|
||||
} from "./baseTypes";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const turnSyncPlanToTable = (record: string) => {
|
||||
const syncPlan: SyncPlanType = JSON.parse(record);
|
||||
const { ts, tsFmt, remoteType, mixedStates } = syncPlan;
|
||||
@@ -78,55 +73,23 @@ const turnSyncPlanToTable = (record: string) => {
|
||||
export const exportVaultSyncPlansToFiles = async (
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
vaultRandomID: string,
|
||||
toFormat: "table" | "json" = "json"
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
log.info("exporting");
|
||||
console.info("exporting");
|
||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||
const records = await readAllSyncPlanRecordTextsByVault(db, vaultRandomID);
|
||||
let md = "";
|
||||
if (records.length === 0) {
|
||||
md = "No sync plans history found";
|
||||
} else {
|
||||
if (toFormat === "json") {
|
||||
md =
|
||||
"Sync plans found:\n\n" +
|
||||
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
||||
} else if (toFormat === "table") {
|
||||
md =
|
||||
"Sync plans found:\n\n" + records.map(turnSyncPlanToTable).join("\n\n");
|
||||
} else {
|
||||
const _: never = toFormat;
|
||||
}
|
||||
}
|
||||
const ts = Date.now();
|
||||
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
||||
await vault.create(filePath, md, {
|
||||
mtime: ts,
|
||||
});
|
||||
log.info("finish exporting");
|
||||
};
|
||||
|
||||
export const exportVaultLoggerOutputToFiles = async (
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||
const records = await readAllLogRecordTextsByVault(db, vaultRandomID);
|
||||
let md = "";
|
||||
if (records.length === 0) {
|
||||
md = "No logger history found.";
|
||||
} else {
|
||||
md =
|
||||
"Logger history found:\n\n" +
|
||||
"```text\n" +
|
||||
records.join("\n") +
|
||||
"\n```\n";
|
||||
}
|
||||
const ts = Date.now();
|
||||
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_LOG_HISTORY_FILE_PREFIX}${ts}.md`;
|
||||
await vault.create(filePath, md, {
|
||||
mtime: ts,
|
||||
});
|
||||
console.info("finish exporting");
|
||||
};
|
||||
|
||||
+7
-7
@@ -1,8 +1,6 @@
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import { bufferToArrayBuffer, hexStringToTypedArray } from "./misc";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const DEFAULT_ITER = 20000;
|
||||
|
||||
// base32.stringify(Buffer.from('Salted__'))
|
||||
@@ -166,18 +164,20 @@ export const decryptBase64urlToString = async (
|
||||
};
|
||||
|
||||
export const getSizeFromOrigToEnc = (x: number) => {
|
||||
if (x < 0 || !Number.isInteger(x)) {
|
||||
throw Error(`x=${x} is not a valid size`);
|
||||
if (x < 0 || Number.isNaN(x) || !Number.isInteger(x)) {
|
||||
throw Error(`getSizeFromOrigToEnc: x=${x} is not a valid size`);
|
||||
}
|
||||
return (Math.floor(x / 16) + 1) * 16 + 16;
|
||||
};
|
||||
|
||||
export const getSizeFromEncToOrig = (x: number) => {
|
||||
if (x < 32 || !Number.isInteger(x)) {
|
||||
throw Error(`${x} is not a valid size`);
|
||||
if (x < 32 || Number.isNaN(x) || !Number.isInteger(x)) {
|
||||
throw Error(`getSizeFromEncToOrig: ${x} is not a valid size`);
|
||||
}
|
||||
if (x % 16 !== 0) {
|
||||
throw Error(`${x} is not a valid encrypted file size`);
|
||||
throw Error(
|
||||
`getSizeFromEncToOrig: ${x} is not a valid encrypted file size`
|
||||
);
|
||||
}
|
||||
return {
|
||||
minSize: ((x - 16) / 16 - 1) * 16,
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { LANGS } from "./langs";
|
||||
|
||||
export type LangType = keyof typeof LANGS;
|
||||
export type LangTypeAndAuto = LangType | "auto";
|
||||
export type TransItemType = keyof typeof LANGS["en"];
|
||||
export type TransItemType = keyof (typeof LANGS)["en"];
|
||||
|
||||
export class I18n {
|
||||
lang: LangTypeAndAuto;
|
||||
@@ -31,7 +31,7 @@ export class I18n {
|
||||
}
|
||||
|
||||
const res: string =
|
||||
(LANGS[realLang] as typeof LANGS["en"])[key] || LANGS["en"][key] || key;
|
||||
(LANGS[realLang] as (typeof LANGS)["en"])[key] || LANGS["en"][key] || key;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -7,14 +7,12 @@ import {
|
||||
RemotelySavePluginSettings,
|
||||
} from "./baseTypes";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
export const exportQrCodeUri = async (
|
||||
settings: RemotelySavePluginSettings,
|
||||
currentVaultName: string,
|
||||
pluginVersion: string
|
||||
) => {
|
||||
const settings2 = cloneDeep(settings);
|
||||
const settings2: Partial<RemotelySavePluginSettings> = cloneDeep(settings);
|
||||
delete settings2.dropbox;
|
||||
delete settings2.onedrive;
|
||||
delete settings2.vaultRandomID;
|
||||
@@ -22,7 +20,7 @@ export const exportQrCodeUri = async (
|
||||
const vault = encodeURIComponent(currentVaultName);
|
||||
const version = encodeURIComponent(pluginVersion);
|
||||
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
|
||||
// log.info(uri)
|
||||
// console.info(uri)
|
||||
const imgUri = await QRCode.toDataURL(rawUri);
|
||||
return {
|
||||
rawUri,
|
||||
|
||||
-1
Submodule src/langs deleted from ad48de9227
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Translations for Remotely Save
|
||||
|
||||
## How To Add A Language?
|
||||
|
||||
1. Copy `en.json` to a new json file named `<lang>.json`, and translate all the items inside. The language should match what is available in Obsidian app.
|
||||
2. Modify the `index.ts` file to include the new language file.
|
||||
|
||||
~~## Why Seperated Repo?~~
|
||||
|
||||
~~For better pull requests management.~~
|
||||
|
||||
## No more `lang` submodule from 20240106
|
||||
|
||||
It's actually harder to manage pull requests of submodule for me. The submodule is merged back to the main repo on 20240106. Any further improved translations (and/or pull requestes) should be dealt with in the main repo.
|
||||
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"confirm": "Confirm",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"goback": "Go Back",
|
||||
"submit": "Submit",
|
||||
"sometext": "Here are some texts.",
|
||||
"syncrun_alreadyrunning": "{{pluginName}} already running in stage {{syncStatus}}!",
|
||||
"syncrun_syncingribbon": "{{pluginName}}: syncing from {{triggerSource}}",
|
||||
"syncrun_step0": "0/8 Remotely Save is running in dry mode, thus not actual file changes would happen.",
|
||||
"syncrun_step1": "1/8 Remotely Save is preparing ({{serviceType}})",
|
||||
"syncrun_step2": "2/8 Starting to fetch remote meta data.",
|
||||
"syncrun_step3": "3/8 Checking password correct or not.",
|
||||
"syncrun_passworderr": "Something goes wrong while checking password.",
|
||||
"syncrun_step4": "4/8 Starting to fetch local meta data.",
|
||||
"syncrun_step5": "5/8 Starting to fetch local prev sync data.",
|
||||
"syncrun_step6": "6/8 Starting to generate sync plan.",
|
||||
"syncrun_step7": "7/8 Remotely Save Sync data is exchanging!",
|
||||
"syncrun_step7skip": "7/8 Remotely Save real sync is skipped in dry run mode.",
|
||||
"syncrun_step8": "8/8 Remotely Save finished!",
|
||||
"syncrun_shortstep0": "0/2 Remotely Save is running in dry mode, not actual file changes would happen.",
|
||||
"syncrun_shortstep1": "1/2 Remotely Save starts running ({{serviceType}})",
|
||||
"syncrun_shortstep2skip": "2/2 Remotely Save real sync is skipped in dry run mode.",
|
||||
"syncrun_shortstep2": "2/2 Remotely Save finished!",
|
||||
"syncrun_abort": "{{manifestID}}-{{theDate}}: abort sync, triggerSource={{triggerSource}}, error while {{syncStatus}}",
|
||||
"syncrun_abort_protectmodifypercentage": "Abort! you set changing files >= {{protectModifyPercentage}}% is not allowed but {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% is going to be modified or deleted! If you are sure you want this sync, please adjust the allowed ratio in the settings.",
|
||||
"protocol_saveqr": "New not-oauth2 settings for {{manifestName}} is saved. Reopen the plugin settings to make it effective.",
|
||||
"protocol_callbacknotsupported": "Your uri calls a callback that's not supported yet: {{params}}",
|
||||
"protocol_dropbox_connecting": "Connecting to Dropbox...\nPlease DO NOT close this modal.",
|
||||
"protocol_dropbox_connect_succ": "Good! We've connected to Dropbox as user {{username}}!",
|
||||
"protocol_dropbox_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_dropbox_connect_fail": "Something went wrong from response from Dropbox. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
"protocol_dropbox_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||
"protocol_dropbox_no_modal": "You are not startting Dropbox connection from the settings page. Abort.",
|
||||
"protocol_onedrive_connecting": "Connecting to OneDrive...\nPlease DO NOT close this modal.",
|
||||
"protocol_onedrive_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_onedrive_connect_fail": "Something went wrong from response from OneDrive. Maybe you rejected the auth?",
|
||||
"protocol_onedrive_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||
"command_startsync": "start sync",
|
||||
"command_drynrun": "start sync (dry run only)",
|
||||
"command_exportsyncplans_json": "export sync plans in json format",
|
||||
"command_exportlogsindb": "export logs saved in db",
|
||||
|
||||
"statusbar_time_years": "Synced {{time}} years ago",
|
||||
"statusbar_time_months": "Synced {{time}} months ago",
|
||||
"statusbar_time_weeks": "Synced {{time}} weeks ago",
|
||||
"statusbar_time_days": "Synced {{time}} days ago",
|
||||
"statusbar_time_hours": "Synced {{time}} hours ago",
|
||||
"statusbar_time_minutes": "Synced {{time}} minutes ago",
|
||||
"statusbar_time_lessminute": "Synced less than a minute ago",
|
||||
"statusbar_lastsync": "Synced {{time}} ago",
|
||||
"statusbar_syncing": "Syncing...",
|
||||
"statusbar_now": "Synced just now",
|
||||
"statusbar_lastsync_label": "Last successful Sync on {{date}}",
|
||||
"statusbar_lastsync_never": "Never Synced",
|
||||
"statusbar_lastsync_never_label": "Never Synced before",
|
||||
|
||||
"modal_password_title": "Hold on and PLEASE READ ON...",
|
||||
"modal_password_shortdesc": "If the field is not empty, files would be encrypted locally before being uploaded.\nIf the field is empty, then files would be uploaded without encryption.",
|
||||
"modal_password_attn1": "Attention 1/5: The vault name is NOT encrypted. The plugin creates a folder with the vault name on some remote services.",
|
||||
"modal_password_attn2": "Attention 2/5: The password itself is stored in PLAIN TEXT LOCALLY.",
|
||||
"modal_password_attn3": "Attention 3/5: Some metadata are not encrypted or can be easily guessed. (File sizes are closed to their unencrypted ones, and directory path may be stored as 0-byte-size object.)",
|
||||
"modal_password_attn4": "Attention 4/5: You should make sure the remote store IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD, to avoid conflicts.",
|
||||
"modal_password_attn5": "Attention 5/5: The longer the password, the better.",
|
||||
"modal_password_secondconfirm": "The Second Confirm to change password.",
|
||||
"modal_password_notice": "New password saved!",
|
||||
"modal_remotebasedir_title": "You are changing the remote base directory config",
|
||||
"modal_remotebasedir_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the config would be reset to use the vault folder name (the default config).\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.",
|
||||
"modal_remotebasedir_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
|
||||
"modal_remotebasedir_secondconfirm_vaultname": "Reset To The Default Vault Folder Name",
|
||||
"modal_remotebasedir_secondconfirm_change": "Confirm To Change",
|
||||
"modal_remotebasedir_notice": "New remote base directory config saved!",
|
||||
"modal_remoteprefix_title": "You are changing the remote prefix config",
|
||||
"modal_remoteprefix_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the prefix will be empty and the files will be saved at the root of the bucket.\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.",
|
||||
"modal_remoteprefix_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
|
||||
"modal_remoteprefix_tosave": "The prefix to save is \"{{{prefix}}}\"",
|
||||
"modal_remoteprefix_secondconfirm_empty": "The prefix is empty and the files will be saved at the root of the bucket.",
|
||||
"modal_remoteprefix_secondconfirm_change": "Confirm To Change",
|
||||
"modal_remoteprefix_notice": "New remote prefix config saved!",
|
||||
"modal_dropboxauth_manualsteps": "Step 1: Visit the address in a browser, and follow the steps.\nStep 2: In the end of the web flow, you obtain a long code. Paste it here then click \"Submit\".",
|
||||
"modal_dropboxauth_autosteps": "Visit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.",
|
||||
"modal_dropboxauth_copybutton": "Click to copy the auth url",
|
||||
"modal_dropboxauth_copynotice": "The auth url is copied to the clipboard!",
|
||||
"modal_dropboxauth_maualinput": "Auth Code from web page",
|
||||
"modal_dropboxauth_maualinput_desc": "You need to click \"Confirm\".",
|
||||
"modal_dropboxauth_maualinput_notice": "Trying to connect to Dropbox",
|
||||
"modal_dropboxauth_maualinput_conn_succ": "Good! We've connected to Dropbox as user {{username}}!",
|
||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"modal_dropboxauth_maualinput_conn_fail": "Something goes wrong while connecting to Dropbox.",
|
||||
"modal_onedriveauth_shortdesc": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).\nVisit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.",
|
||||
"modal_onedriveauth_copybutton": "Click to copy the auth url",
|
||||
"modal_onedriveauth_copynotice": "The auth url is copied to the clipboard!",
|
||||
"modal_onedriverevokeauth_step1": "Step 1: Go to the following address, click the \"Edit\" button for the plugin, then click \"Remove these permissions\" button on the page.",
|
||||
"modal_onedriverevokeauth_step2": "Step 2: Click the button below, to clean the locally-saved login credentials.",
|
||||
"modal_onedriverevokeauth_clean": "Clean Locally-Saved Login Credentials",
|
||||
"modal_onedriverevokeauth_clean_desc": "You need to click the button.",
|
||||
"modal_onedriverevokeauth_clean_button": "Clean",
|
||||
"modal_onedriverevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_onedriverevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other startting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||
"modal_syncconfig_secondconfirm": "The Second Confirm To Enable.",
|
||||
"modal_syncconfig_notice": "You've enabled syncing config folder!",
|
||||
"modal_qr_shortdesc": "This exports not-oauth2 settings. (It means that Dropbox, OneDrive info are NOT exported.)\nYou can use another device to scan this qrcode.\nOr, you can click the button to copy the special url.",
|
||||
"modal_qr_button": "Click to copy the special URI",
|
||||
"modal_qr_button_notice": "The special uri is copied to the clipboard!",
|
||||
"modal_sizesconflict_title": "Remotely Save: Some conflict were found while skipping large files",
|
||||
"modal_sizesconflict_desc": "You've set skipping files larger than {{thresholdMB}} MB ({{thresholdBytes}} bytes).\nBut the following files have sizes larger than the threshold on one side, and sizes smaller than the threshold on the other side.\nTo avoid unexpected overwriting or deleting, the plugin stops, and you have to manually deal with at least one side of the files.",
|
||||
"modal_sizesconflict_copybutton": "Click to copy all the below sizes conflicts info",
|
||||
"modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!",
|
||||
"settings_basic": "Basic Settings",
|
||||
"settings_password": "Encryption Password",
|
||||
"settings_password_desc": "Password for E2E encryption. Empty for no password. You need to click \"Confirm\". Attention: the password and other info are saved locally.",
|
||||
"settings_autorun": "Schedule For Auto Run",
|
||||
"settings_autorun_desc": "The plugin tries to schedule the running after every interval. Battery may be impacted.",
|
||||
"settings_autorun_notset": "(not set)",
|
||||
"settings_autorun_1min": "every 1 minute",
|
||||
"settings_autorun_5min": "every 5 minutes",
|
||||
"settings_autorun_10min": "every 10 minutes",
|
||||
"settings_autorun_30min": "every 30 minutes",
|
||||
"settings_runoncestartup": "Run Once On Start Up Automatically",
|
||||
"settings_runoncestartup_desc": "This settings allows setting running ONCE on start up automatically. This will take effect on NEXT start up after changing. This setting, is different from \"schedule for auto run\" which starts syncing after EVERY interval.",
|
||||
"settings_runoncestartup_notset": "(not set)",
|
||||
"settings_runoncestartup_1sec": "sync once after 1 second of start up",
|
||||
"settings_runoncestartup_10sec": "sync once after 10 seconds of start up",
|
||||
"settings_runoncestartup_30sec": "sync once after 30 seconds of start up",
|
||||
"settings_saverun": "Sync On Save (experimental)",
|
||||
"settings_saverun_desc": "A sync will be triggered if a file save action happened within a few seconds. Please pay attention that syncing is potentially a heavy action and battery may be impacted. (May need to reload the plugin or restart Obsidian after changing)",
|
||||
"settings_saverun_notset": "(not set)",
|
||||
"settings_saverun_1sec": "check every 1 second",
|
||||
"settings_saverun_5sec": "check every 5 seconds",
|
||||
"settings_saverun_10sec": "check every 10 seconds (recommended)",
|
||||
"settings_saverun_1min": "check every 1 minute",
|
||||
"settings_skiplargefiles": "Skip Large Files",
|
||||
"settings_skiplargefiles_desc": "Skip files with sizes larger than the threshold. Here 1 MB = 10^6 bytes.",
|
||||
"settings_skiplargefiles_notset": "(not set)",
|
||||
|
||||
"settings_ignorepaths": "Regex Of Paths To Ignore",
|
||||
"settings_ignorepaths_desc": "Regex of paths of folders or files to ignore. One regex per line. The path is relative to the vault root without leading slash.",
|
||||
"settings_enablestatusbar_info": "Show Last Successful Sync In Status Bar",
|
||||
"settings_enablestatusbar_info_desc": "Show the time of the last successful sync in the status bar.",
|
||||
"settings_enablestatusbar_reloadrequired_notice": "Reload the plugin for the changes to take effect.",
|
||||
"settings_resetstatusbar_time": "Reset Last Successful Sync Time",
|
||||
"settings_resetstatusbar_time_desc": "Reset last successful sync time.",
|
||||
"settings_resetstatusbar_button": "Reset",
|
||||
"settings_resetstatusbar_notice": "Reset done!",
|
||||
|
||||
"settings_checkonnectivity": "Check Connectivity",
|
||||
"settings_checkonnectivity_desc": "Check connectivity.",
|
||||
"settings_checkonnectivity_button": "Check",
|
||||
"settings_checkonnectivity_checking": "Checking...",
|
||||
"settings_remotebasedir": "Change The Remote Base Directory (experimental)",
|
||||
"settings_remotebasedir_desc": "By default the content is synced to a remote directory with the same name as the vault name. You can change the remote folder name here, or keep the input field empty to reset to the default. You need to click \"Confirm\".",
|
||||
"settings_remoteprefix": "Change The Remote Prefix (experimental)",
|
||||
"settings_remoteprefix_desc": "By default in s3 the files are saved at the root of the bucket. You can change the remote prefix here, or keep the input field empty to reset to the default. You need to click \"Confirm\".",
|
||||
"settings_s3": "Remote For S3 or compatible",
|
||||
"settings_s3_disclaimer1": "Disclaimer: This plugin is NOT an official Amazon product.",
|
||||
"settings_s3_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.",
|
||||
"settings_s3_cors": "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost, and add ETag into exposed headers.",
|
||||
"settings_s3_prod": "Some Amazon S3 official docs for references:",
|
||||
"settings_s3_prod1": "Endpoint and region info",
|
||||
"settings_s3_prod2": "Access Key ID and Secret Access Key info",
|
||||
"settings_s3_prod3": "Configuring CORS",
|
||||
"settings_s3_endpoint": "Endpoint",
|
||||
"settings_s3_region": "Region",
|
||||
"settings_s3_region_desc": "If you are not sure what to enter, you could try the value: us-east-1 .",
|
||||
"settings_s3_accesskeyid": "Access Key ID",
|
||||
"settings_s3_accesskeyid_desc": "Access key ID. Attention: Access key ID and other info are saved locally.",
|
||||
"settings_s3_secretaccesskey": "Secret Access Key",
|
||||
"settings_s3_secretaccesskey_desc": "Secret access key. Attention: Secret access key and other info are saved locally.",
|
||||
"settings_s3_bucketname": "Bucket Name",
|
||||
"settings_s3_bypasscorslocally": "Bypass CORS Issue Locally",
|
||||
"settings_s3_bypasscorslocally_desc": "The plugin allows skipping server CORS config in new version of Obsidian ( desktop>=0.13.25 or iOS>=1.1.1 or Android>=1.2.1). If you encounter any issues, please disable this setting and config CORS on servers (allowing requests from app://obsidian.md and capacitor://localhost and http://localhost and add ETag into exposed headers).",
|
||||
"settings_s3_parts": "Parts Concurrency",
|
||||
"settings_s3_parts_desc": "Large files are split into small parts to upload in S3. How many parts do you want to upload in parallel at most?",
|
||||
"settings_s3_accuratemtime": "Use Accurate MTime",
|
||||
"settings_s3_accuratemtime_desc": "Read the uploaded accurate last modified time for better sync algorithm. But it causes extra api requests / time / money to the S3 endpoint.",
|
||||
"settings_s3_urlstyle": "S3 URL style",
|
||||
"settings_s3_urlstyle_desc": "Whether to force path-style URLs for S3 objects (e.g., https://s3.amazonaws.com/*/ instead of https://*.s3.amazonaws.com/).",
|
||||
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
|
||||
"settings_s3_connect_fail": "The S3 bucket cannot be reached.",
|
||||
"settings_dropbox": "Remote For Dropbox",
|
||||
"settings_dropbox_disclaimer1": "Disclaimer: This app is NOT an official Dropbox product.",
|
||||
"settings_dropbox_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .",
|
||||
"settings_dropbox_folder": "We will create and sync inside the folder /Apps/{{pluginID}}/{{remoteBaseDir}} on your Dropbox.",
|
||||
"settings_dropbox_revoke": "Revoke Auth",
|
||||
"settings_dropbox_revoke_desc": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"settings_dropbox_revoke_button": "Revoke Auth",
|
||||
"settings_dropbox_revoke_notice": "Revoked!",
|
||||
"settings_dropbox_revoke_noticeerr": "Something goes wrong while revoking.",
|
||||
"settings_dropbox_clearlocal": "Clear Locally Saved Credentials",
|
||||
"settings_dropbox_clearlocal_desc": "You can forcefully clear the locally saved Dropbox login credentials, but not send revoke auth request to the server.",
|
||||
"settings_dropbox_clearlocal_button": "Clear",
|
||||
"settings_dropbox_clearlocal_notice": "Cleared!",
|
||||
"settings_dropbox_auth": "Auth",
|
||||
"settings_dropbox_auth_desc": "Auth.",
|
||||
"settings_dropbox_auth_button": "Auth",
|
||||
"settings_dropbox_connect_succ": "Great! We can connect to Dropbox!",
|
||||
"settings_dropbox_connect_fail": "We cannot connect to Dropbox.",
|
||||
"settings_onedrive": "Remote For Onedrive (for personal)",
|
||||
"settings_onedrive_disclaimer1": "Disclaimer: This app is NOT an official Microsoft / OneDrive product.",
|
||||
"settings_onedrive_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .",
|
||||
"settings_onedrive_folder": "We will create and sync inside the folder /Apps/{{pluginID}}/{{remoteBaseDir}} on your OneDrive.",
|
||||
"settings_onedrive_nobiz": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).",
|
||||
"settings_onedrive_revoke": "Revoke Auth",
|
||||
"settings_onedrive_revoke_desc": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"settings_onedrive_revoke_button": "Revoke Auth",
|
||||
"settings_onedrive_auth": "Auth",
|
||||
"settings_onedrive_auth_desc": "Auth.",
|
||||
"settings_onedrive_auth_button": "Auth",
|
||||
"settings_onedrive_connect_succ": "Great! We can connect to Onedrive!",
|
||||
"settings_onedrive_connect_fail": "We cannot connect to Onedrive.",
|
||||
"settings_webdav": "Remote For Webdav",
|
||||
"settings_webdav_disclaimer1": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.",
|
||||
"settings_webdav_cors_os": "Obsidian desktop>=0.13.25 or iOS>=1.1.1 or Android>=1.2.1 supports bypassing CORS locally. But you are using an old version, and you're suggested to upgrade Obsidian.",
|
||||
"settings_webdav_cors": "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost",
|
||||
"settings_webdav_folder": "We will create and sync inside the folder /{{remoteBaseDir}} on your server.",
|
||||
"settings_webdav_addr": "Server Address",
|
||||
"settings_webdav_addr_desc": "Server address.",
|
||||
"settings_webdav_user": "Username",
|
||||
"settings_webdav_user_desc": "Username. Attention: the username and other info are saved locally.",
|
||||
"settings_webdav_password": "Password",
|
||||
"settings_webdav_password_desc": "Password. Attention: the password and other info are saved locally.",
|
||||
"settings_webdav_auth": "Auth Type",
|
||||
"settings_webdav_auth_desc": "If no password, this option would be ignored.",
|
||||
"settings_webdav_depth": "Depth Header Sent To Servers",
|
||||
"settings_webdav_depth_desc": "Webdav servers should be configured to allow requests with header Depth being '1' or 'Infinity'. If you are not sure what's this, choose \"depth='1'\". If you are sure your server supports depth='infinity', please choose that to get way better performance.",
|
||||
"settings_webdav_depth_1": "only supports depth='1'",
|
||||
"settings_webdav_depth_inf": "supports depth='infinity'",
|
||||
"settings_webdav_connect_succ": "Great! The webdav server can be accessed.",
|
||||
"settings_webdav_connect_fail": "The webdav server cannot be reached (possible to be any of address/username/password/authtype errors).",
|
||||
"settings_webdav_connect_fail_withcors": "The webdav server cannot be reached (possible to be any of address/username/password/authtype/CORS errors).",
|
||||
"settings_chooseservice": "Choose A Remote Service",
|
||||
"settings_chooseservice_desc": "Start here. What service are you connecting to? S3, Dropbox, Webdav, or OneDrive for personal?",
|
||||
"settings_chooseservice_s3": "S3 or compatible",
|
||||
"settings_chooseservice_dropbox": "Dropbox",
|
||||
"settings_chooseservice_webdav": "Webdav",
|
||||
"settings_chooseservice_onedrive": "OneDrive for personal",
|
||||
"settings_adv": "Advanced Settings",
|
||||
"settings_concurrency": "Concurrency",
|
||||
"settings_concurrency_desc": "How many files do you want to download or upload in parallel at most? By default it's set to 5. If you meet any problems such as rate limit, you can reduce the concurrency to a lower value.",
|
||||
"settings_syncunderscore": "Sync _ Files Or Folders",
|
||||
"settings_syncunderscore_desc": "Sync files or folders starting with _ (\"underscore\") or not",
|
||||
"settings_configdir": "Sync Config Dir (experimental)",
|
||||
"settings_configdir_desc": "Sync config dir {{configDir}} or not (inner folder .git and node_modules would be ignored). Please be aware that this may impact all your plugins' or Obsidian's settings, and may require you restart Obsidian after sync. Enable this at your own risk.",
|
||||
"settings_deletetowhere": "Deletion Destination",
|
||||
"settings_deletetowhere_desc": "Which trash should the plugin put the files into while deleting?",
|
||||
"settings_deletetowhere_system_trash": "system trash (default)",
|
||||
"settings_deletetowhere_obsidian_trash": "Obsidian .trash folder",
|
||||
"settings_conflictaction": "Action For Conflict",
|
||||
"settings_conflictaction_desc": "If a file is created or modified on both side since last update, it's a conflict event. How to deal with it? This only works for bidirectional sync.",
|
||||
"settings_conflictaction_keep_newer": "newer version survives (default)",
|
||||
"settings_conflictaction_keep_larger": "larger size version survives",
|
||||
"settings_cleanemptyfolder": "Action For Empty Folders",
|
||||
"settings_cleanemptyfolder_desc": "The sync algorithm majorly deals with files, so you need to specify how to deal with empty folders.",
|
||||
"settings_cleanemptyfolder_skip": "leave them as is (default)",
|
||||
"settings_cleanemptyfolder_clean_both": "delete local and remote",
|
||||
"settings_protectmodifypercentage": "Abort Sync If Modification Above Percentage",
|
||||
"settings_protectmodifypercentage_desc": "Abort the sync if more than n% of the files are going to be deleted / modified. Useful to protect users' files from unexpected modifications. You can set to 100 to disable the protection, or set to 0 to always block the sync.",
|
||||
"settings_protectmodifypercentage_000_desc": "0 (always block)",
|
||||
"settings_protectmodifypercentage_050_desc": "50 (default)",
|
||||
"settings_protectmodifypercentage_100_desc": "100 (disable the protection)",
|
||||
"setting_syncdirection": "Sync Direction",
|
||||
"setting_syncdirection_desc": "Which direction should the plugin sync to? Please be aware that only CHANGED files (based on time and size) are synced regardless any option.",
|
||||
"setting_syncdirection_bidirectional_desc": "Bidirectional (default)",
|
||||
"setting_syncdirection_incremental_push_only_desc": "Incremental Push Only (aka backup mode)",
|
||||
"setting_syncdirection_incremental_pull_only_desc": "Incremental Pull Only",
|
||||
"settings_enablemobilestatusbar": "Enable Mobile Status Bar Or Not",
|
||||
"settings_enablemobilestatusbar_desc": "By default Obsidian mobile hides status bar. But some users want to show it up. So here is a hack.",
|
||||
"settings_importexport": "Import and Export Partial Settings",
|
||||
"settings_export": "Export",
|
||||
"settings_export_desc": "Export not-oauth2 settings by generating a qrcode.",
|
||||
"settings_export_desc_button": "Get QR Code",
|
||||
"settings_import": "Import",
|
||||
"settings_import_desc": "You should open a camera or scan-qrcode app, to manually scan the QR code.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_debuglevel": "Alter Notice Level",
|
||||
"settings_debuglevel_desc": "By default the notice level is \"info\". You can change to \"debug\" to get verbose information while syncing.",
|
||||
"settings_outputsettingsconsole": "Output Current Settings From Disk To Console",
|
||||
"settings_outputsettingsconsole_desc": "The settings save on disk in encoded. Click this to see the decoded settings in console.",
|
||||
"settings_outputsettingsconsole_button": "Output",
|
||||
"settings_outputsettingsconsole_notice": "Finished outputing in console.",
|
||||
"settings_obfuscatesettingfile": "Obfuscate The Setting File Or Not",
|
||||
"settings_obfuscatesettingfile_desc": "The setting file (data.json) has some sensitive information. It's strongly recommended to obfuscate it to avoid unexpected read and modification. If you are sure to view and edit it manually, you can disable the obfuscation.",
|
||||
"settings_viewconsolelog": "View Console Log",
|
||||
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
||||
"settings_syncplans": "Export Sync Plans",
|
||||
"settings_syncplans_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
|
||||
"settings_syncplans_button_json": "Export",
|
||||
"settings_syncplans_notice": "Sync plans history exported.",
|
||||
"settings_delsyncplans": "Delete Sync Plans History In DB",
|
||||
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
|
||||
"settings_delsyncplans_button": "Delete Sync Plans History",
|
||||
"settings_delsyncplans_notice": "Sync plans history (in DB) deleted.",
|
||||
"settings_delprevsync": "Delete Prev Sync Details In DB",
|
||||
"settings_delprevsync_desc": "The sync algorithm keeps the previous successful sync information in DB to determine the file changes. If you want to ignore them so that all files are treated newly created, you can delete the prev sync info here.",
|
||||
"settings_delprevsync_button": "Delete Prev Sync Details",
|
||||
"settings_delprevsync_notice": "Previous sync history (in local DB) deleted",
|
||||
"settings_outputbasepathvaultid": "Output Vault Base Path And Randomly Assigned ID",
|
||||
"settings_outputbasepathvaultid_desc": "For debugging purposes.",
|
||||
"settings_outputbasepathvaultid_button": "Output",
|
||||
"settings_resetcache": "Reset Local Internal Cache/Databases",
|
||||
"settings_resetcache_desc": "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings.",
|
||||
"settings_resetcache_button": "Reset",
|
||||
"settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.",
|
||||
"syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm",
|
||||
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
|
||||
"syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.",
|
||||
"syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.",
|
||||
"syncalgov3_button_agree": "Agree",
|
||||
"syncalgov3_button_disagree": "Do Not Agree"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import en from "./en.json";
|
||||
import zh_cn from "./zh_cn.json";
|
||||
import zh_tw from "./zh_tw.json";
|
||||
|
||||
export const LANGS = {
|
||||
en: en,
|
||||
zh_cn: zh_cn,
|
||||
zh_tw: zh_tw,
|
||||
};
|
||||
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"confirm": "确认",
|
||||
"disable": "关闭",
|
||||
"enable": "开启",
|
||||
"goback": "返回",
|
||||
"submit": "提交",
|
||||
"sometext": "这里有一段文字。",
|
||||
"syncrun_alreadyrunning": "{{pluginName}} 正处于此阶段:{{syncStatus}}!",
|
||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 触发运行",
|
||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
||||
"syncrun_step1": "1/8 Remotely Save 准备同步({{serviceType}})",
|
||||
"syncrun_step2": "2/8 正在获取远端的元数据。",
|
||||
"syncrun_step3": "3/8 正在检查密码正确与否。",
|
||||
"syncrun_passworderr": "检查密码时候出错。",
|
||||
"syncrun_step4": "4/8 正在获取本地的元数据。",
|
||||
"syncrun_step5": "5/8 正在获取本地上一次同步的元数据。",
|
||||
"syncrun_step6": "6/8 正在生成同步计划。",
|
||||
"syncrun_step7": "7/8 Remotely Save 开始发生数据交换!",
|
||||
"syncrun_step7skip": "7/8 Remotely Save 在空跑模式,跳过实际数据交换步骤。",
|
||||
"syncrun_step8": "8/8 Remotely Save 已完成同步!",
|
||||
"syncrun_shortstep0": "0/2 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
||||
"syncrun_shortstep1": "1/2 Remotely Save 开始同步({{serviceType}})",
|
||||
"syncrun_shortstep2skip": "2/2 Remotely Save 在空跑模式,跳过实际数据交换步骤。",
|
||||
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
||||
"syncrun_abort": "{{manifestID}}-{{theDate}}:中断同步,同步来源={{triggerSource}},出错阶段={{syncStatus}}",
|
||||
"syncrun_abort_protectmodifypercentage": "中断同步!您设置了不允许 >= {{protectModifyPercentage}}% 的变更,但是现在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的文件会被修改或删除!如果您确认这次同步是您想要的,那么请在设置里修改允许比例。",
|
||||
"protocol_saveqr": " {{manifestName}} 新的非 oauth2 设置保存完成。请重启插件设置页使之生效。",
|
||||
"protocol_callbacknotsupported": "您的 uri callback 暂不支持: {{params}}",
|
||||
"protocol_dropbox_connecting": "正在连接 Dropbox……\n请不要关闭此弹窗。",
|
||||
"protocol_dropbox_connect_succ": "好!我们作为用户 {{username}} 连接上了 Dropbox!",
|
||||
"protocol_dropbox_connect_succ_revoke": "您已作为用户 {{username}} 连接上了。如果您想取消连接,请点击此按钮。",
|
||||
"protocol_dropbox_connect_fail": "Dropbox 的回调请求有点异常。您是否网络不顺畅?您是否拒绝了鉴权?",
|
||||
"protocol_dropbox_connect_unknown": "不知道如何处理此 callback:{{params}}",
|
||||
"protocol_dropbox_no_modal": "您不是在设置页的弹窗里触发 Dropbox 的。中止操作。",
|
||||
"protocol_onedrive_connecting": "正在连接 OneDrive……\n请不要关闭此弹窗。",
|
||||
"protocol_onedrive_connect_succ_revoke": "您已作为用户 {{username}} 连接上了。如果您想取消连接,请点击此按钮。",
|
||||
"protocol_onedrive_connect_fail": "OneDrive 的回调请求有点异常。您是否拒绝了鉴权?",
|
||||
"protocol_onedrive_connect_unknown": "不知道如何处理此 callback:{{params}}",
|
||||
"command_startsync": "开始同步(start sync)",
|
||||
"command_drynrun": "开始同步(空跑模式)(start sync (dry run only))",
|
||||
"command_exportsyncplans_json": "导出同步计划为 json 格式(export sync plans in json format)",
|
||||
"command_exportlogsindb": "从数据库导出终端日志(export logs saved in db)",
|
||||
|
||||
"statusbar_time_years": "{{time}} 年前同步",
|
||||
"statusbar_time_months": "{{time}} 月前同步",
|
||||
"statusbar_time_weeks": "{{time}} 周前同步",
|
||||
"statusbar_time_days": "{{time}} 天前同步",
|
||||
"statusbar_time_hours": "{{time}} 小时前同步",
|
||||
"statusbar_time_minutes": "{{time}} 分钟前同步",
|
||||
"statusbar_time_lessminute": "一分钟之内同步",
|
||||
"statusbar_lastsync": "上一次同步于:{{time}}",
|
||||
"statusbar_syncing": "正在同步",
|
||||
"statusbar_now": "刚同步完",
|
||||
"statusbar_lastsync_label": "上一次同步于:{{date}}",
|
||||
"statusbar_lastsync_never": "没触发过同步",
|
||||
"statusbar_lastsync_never_label": "没触发过同步",
|
||||
|
||||
"modal_password_title": "稍等一下,请阅读下文:",
|
||||
"modal_password_shortdesc": "如果密码不是空的,那么文件会在上传之前,在本地先用此密码加密。\n如果密码是空的,那么文件会被非加密地上传。",
|
||||
"modal_password_attn1": "注意 1/5:库(Vault)名字是不会加密的!本插件会在一些远程存储里创建一个和库名字有着同名的文件夹。",
|
||||
"modal_password_attn2": "注意 2/5:密码在本地不加密地保存!",
|
||||
"modal_password_attn3": "注意 3/5:一些元信息是没有被加密的,或者可以被较容易地猜测出来。(文件大小很接近于没加密的时候,且文件夹可能会被存储为大小为 0 byte 的对象。)",
|
||||
"modal_password_attn4": "注意 4/5:您需要确保远程存储是“空”的,或者已有文件也是被同一个新密码加密的;从而避免冲突。",
|
||||
"modal_password_attn5": "注意 5/5:密码越长越好。",
|
||||
"modal_password_secondconfirm": "再次确认保存新密码",
|
||||
"modal_password_notice": "新密码已保存!",
|
||||
"modal_remotebasedir_title": "您正在修改远端基文件夹设置",
|
||||
"modal_remotebasedir_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置会被重设回库的文件夹名(默认设置)。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
|
||||
"modal_remotebasedir_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
|
||||
"modal_remotebasedir_secondconfirm_vaultname": "重设回默认的库文件夹名",
|
||||
"modal_remotebasedir_secondconfirm_change": "确认修改",
|
||||
"modal_remotebasedir_notice": "新的远端基文件夹设置已保存!",
|
||||
"modal_remoteprefix_title": "您正在修改远端路径前缀设置",
|
||||
"modal_remoteprefix_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置为保存为空,文件将会被存储在桶(Bucket)的根目录。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
|
||||
"modal_remoteprefix_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
|
||||
"modal_remoteprefix_tosave": "您设定的新前缀为:“{{{prefix}}}”",
|
||||
"modal_remoteprefix_secondconfirm_empty": "前缀为空,文件会保存在根目录",
|
||||
"modal_remoteprefix_secondconfirm_change": "确认修改",
|
||||
"modal_remoteprefix_notice": "新的远端路径前缀设置已保存!",
|
||||
"modal_dropboxauth_manualsteps": "第 1 步:在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会获得一串很长的代码文本,请复制粘贴到下方,并点击“提交”",
|
||||
"modal_dropboxauth_autosteps": "在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。",
|
||||
"modal_dropboxauth_copybutton": "点击此按钮从而复制鉴权 url",
|
||||
"modal_dropboxauth_copynotice": "鉴权 url 已复制到剪贴板!",
|
||||
"modal_dropboxauth_maualinput": "来自网页的授权码",
|
||||
"modal_dropboxauth_maualinput_desc": "您需要点击“确认”",
|
||||
"modal_dropboxauth_maualinput_notice": "正在尝试连接 Dropbox",
|
||||
"modal_dropboxauth_maualinput_conn_succ": "很好!我们作为用户 {{username}} 连接到 Dropbox 了!",
|
||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作为用户 {{username}} 连接到 Dropbox。如果您想断开连接,点击此按钮。",
|
||||
"modal_dropboxauth_maualinput_conn_fail": "连接 Dropbox 途中出错了。",
|
||||
"modal_onedriveauth_shortdesc": "现在只支持个人版 OneDrive,(暂)不支持企业版。\n在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。",
|
||||
"modal_onedriveauth_copybutton": "点击此按钮从而复制鉴权 url",
|
||||
"modal_onedriveauth_copynotice": "鉴权 url 已复制到剪贴板!",
|
||||
"modal_onedriverevokeauth_step1": "第 1 步:用浏览器打开以下地址,点击本插件对应的“Edit”按钮,点击“Remove these permissions”按钮。",
|
||||
"modal_onedriverevokeauth_step2": "第 2 步:点击以下按钮,清理本地保存的登录信息。",
|
||||
"modal_onedriverevokeauth_clean": "清理本地保存的登录信息",
|
||||
"modal_onedriverevokeauth_clean_desc": "您需要点击“清理”按钮。",
|
||||
"modal_onedriverevokeauth_clean_button": "清理",
|
||||
"modal_onedriverevokeauth_clean_notice": "已清理!",
|
||||
"modal_onedriverevokeauth_clean_fail": "鉴权途中出错了。",
|
||||
"modal_syncconfig_attn": "注意 1/2:此设置只同步(复制)整个 Obsidian 的配置文件夹,但是不会同步其它 . 开头的文件夹或文件。除了会忽略 .git 和 node_modules 文件夹之外,它也并不理解配置文件夹的里各个子文件或子文件夹的含义。\n注意 2/2:配置文件夹被同步之后,各插件的设置或许会出错,且 Obsidian 或许需要重启来重载各插件的新配置。\n如果您同意自行承受以上风险,您可以点击以下再次确认按钮。",
|
||||
"modal_syncconfig_secondconfirm": "再次确认开启",
|
||||
"modal_syncconfig_notice": "您已开启配置文件夹的同步!",
|
||||
"modal_qr_shortdesc": "这里可导出非 oauth2 设置。(意味着:Dropbox 和 OneDrive 信息不会被导出。)\n您可以使用另一个设备来扫描此 QR 码。\n又或者,您可以点击以下按钮复制此特殊 URI。",
|
||||
"modal_qr_button": "点击此按钮复制特殊 URI",
|
||||
"modal_qr_button_notice": "特殊 URI 已被复制到剪贴板!",
|
||||
"modal_sizesconflict_title": "Remotely Save:跳过大文件的时候出现了一些冲突",
|
||||
"modal_sizesconflict_desc": "您设置了跳过同步大于 {{thresholdMB}} MB({{thresholdBytes}} bytes)的文件。\n但是以下文件的大小,在一端大于阈值,在另一端则小于阈值。\n为了避免意外的覆盖或删除,插件停止了运作,您需要手动处理至少一端的文件。",
|
||||
"modal_sizesconflict_copybutton": "点击以复制以下所有文件大小冲突信息",
|
||||
"modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!",
|
||||
"settings_basic": "基本设置",
|
||||
"settings_password": "密码",
|
||||
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。",
|
||||
"settings_autorun": "自动运行",
|
||||
"settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。",
|
||||
"settings_autorun_notset": "(不设置)",
|
||||
"settings_autorun_1min": "每 1 分钟",
|
||||
"settings_autorun_5min": "每 5 分钟",
|
||||
"settings_autorun_10min": "每 10 分钟",
|
||||
"settings_autorun_30min": "每 30 分钟",
|
||||
"settings_runoncestartup": "启动后自动运行一次",
|
||||
"settings_runoncestartup_desc": "本设置使得插件在程序启动后自动触发一次同步。修改后,会在下一次启动生效。本设置和“自动运行”是不同的,那个设置是指“每隔一段时间”都自动同步一次。",
|
||||
"settings_runoncestartup_notset": "(不设置)",
|
||||
"settings_runoncestartup_1sec": "启动后第 1 秒运行一次",
|
||||
"settings_runoncestartup_10sec": "启动后第 10 秒运行一次",
|
||||
"settings_runoncestartup_30sec": "启动后第 30 秒运行一次",
|
||||
"settings_saverun": "保存时同步(实验性质)",
|
||||
"settings_saverun_desc": "插件如果检查到当前文件在最近一段时间有修改保存过,则尝试同步。请注意,同步是一个很重的操作,因此会影响到耗电量。(修改设置后可能需要重载插件或重启。)",
|
||||
"settings_saverun_notset": "(不设置)",
|
||||
"settings_saverun_1sec": "隔 1 秒检查一次",
|
||||
"settings_saverun_5sec": "隔 5 秒检查一次",
|
||||
"settings_saverun_10sec": "隔 10 秒检查一次(推荐)",
|
||||
"settings_saverun_1min": "隔 1 分钟检查一次",
|
||||
"settings_skiplargefiles": "跳过大文件",
|
||||
"settings_skiplargefiles_desc": "跳过大于某一个阈值的文件。这里 1 MB = 10^6 bytes。",
|
||||
"settings_skiplargefiles_notset": "(不设置)",
|
||||
|
||||
"settings_ignorepaths": "忽略的文件或文件夹的正则表达式",
|
||||
"settings_ignorepaths_desc": "忽略的文件或文件夹的正则表达式。每行一条。路径是相对于库(Vault)根目录的,没有前置 / 符号。",
|
||||
"settings_enablestatusbar_info": "在状态栏显示上一次成功的同步",
|
||||
"settings_enablestatusbar_info_desc": "在状态栏显示上一次成功的同步的时间。",
|
||||
"settings_enablestatusbar_reloadrequired_notice": "需要重新载入此插件才生效。",
|
||||
"settings_resetstatusbar_time": "重设上一次成功同步的时间记录",
|
||||
"settings_resetstatusbar_time_desc": "重设上一次成功同步的时间记录。",
|
||||
"settings_resetstatusbar_button": "重设",
|
||||
"settings_resetstatusbar_notice": "重设完毕!",
|
||||
|
||||
"settings_checkonnectivity": "检查可否连接",
|
||||
"settings_checkonnectivity_desc": "检查可否连接。",
|
||||
"settings_checkonnectivity_button": "检查",
|
||||
"settings_checkonnectivity_checking": "正在检查……",
|
||||
"settings_remotebasedir": "修改远端基文件夹(实验性质)",
|
||||
"settings_remotebasedir_desc": "默认设定,内容会被同步到远端的和资料库同名的文件夹下。您可以在此修改远端文件夹名,或删除输入框文本从而重设到默认值。您需要点击“确认”。",
|
||||
"settings_remoteprefix": "修改远端前缀路径(实验性质)",
|
||||
"settings_remoteprefix_desc": "默认设定 s3 保存在存储桶(Bucket)的根目录。您可以在这里修改路径前缀,或者保持为空保持默认设置。您需要点击“确认”。",
|
||||
"settings_s3": "S3 或兼容 S3 的服务的设置",
|
||||
"settings_s3_disclaimer1": "声明:本插件不是 Amazon 的官方产品。",
|
||||
"settings_s3_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了存储桶有不符合预期的访问,请立刻从 AWS(或其它 S3 服务商)删除记录于此的 access key。",
|
||||
"settings_s3_cors": "您需要设置 CORS,从而允许这些源的请求访问:app://obsidian.md 和 capacitor://localhost 和 http://localhost 且增加 ETag 到暴露 headers 里。",
|
||||
"settings_s3_prod": "一些可参考的 Amazon S3 官方文档:",
|
||||
"settings_s3_prod1": "服务地址(Endpoint)和区域(Region)信息",
|
||||
"settings_s3_prod2": "Access Key ID 和 Secret Access Key 信息",
|
||||
"settings_s3_prod3": "设置 CORS",
|
||||
"settings_s3_endpoint": "服务地址(Endpoint)",
|
||||
"settings_s3_region": "区域(Region)",
|
||||
"settings_s3_region_desc": "如果您不知道这个值填什么,可以尝试填写:us-east-1。",
|
||||
"settings_s3_accesskeyid": "Access Key ID",
|
||||
"settings_s3_accesskeyid_desc": "Access key ID。注意:access key ID 和其它信息都会在本地保存。",
|
||||
"settings_s3_secretaccesskey": "Secret Access Key",
|
||||
"settings_s3_secretaccesskey_desc": "Secret access key。注意:secret access key 和其它信息都会在本地保存。",
|
||||
"settings_s3_bucketname": "存储桶(Bucket)的名字",
|
||||
"settings_s3_bypasscorslocally": "本地绕过 CORS 问题",
|
||||
"settings_s3_bypasscorslocally_desc": "对于 Obsidian 新版本(桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.2.1),本插件可以跳过服务器设置 CORS 的步骤。如果您遇到任意问题,可以关闭此设定,并在服务端设置 CORS(允许来自 app://obsidian.md 和 capacitor://localhost 和 http://localhost 的请求且增加 ETag 到暴露 headers 里)。",
|
||||
"settings_s3_parts": "分块并行度",
|
||||
"settings_s3_parts_desc": "在 S3 里,大文件会被分块上传。您希望同一时间最多有多少个分块被上传?",
|
||||
"settings_s3_accuratemtime": "使用准确的文件修改时间",
|
||||
"settings_s3_accuratemtime_desc": "读取(已上传的)准确的文件修改时间,有助于同步算法更加准确和稳定。但是它也会导致额外的 api 请求、时间、金钱花费。",
|
||||
"settings_s3_urlstyle": "S3 URL style",
|
||||
"settings_s3_urlstyle_desc": "是否对 S3 对象强制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
||||
"settings_s3_connect_succ": "很好!可以访问到对应存储桶。",
|
||||
"settings_s3_connect_fail": "无法访问到对应存储桶。",
|
||||
"settings_dropbox": "Dropbox 设置",
|
||||
"settings_dropbox_disclaimer1": "声明:此插件不是 Dropbox 官方产品。",
|
||||
"settings_dropbox_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 Dropbox 有不符合预期的访问,请立刻从 https://www.dropbox.com/account/connected_apps 删除记录于此插件的连接鉴权。",
|
||||
"settings_dropbox_folder": "我们会在您的 Dropbox 上创建此文件夹并在里面同步:/Apps/{{pluginID}}/{{remoteBaseDir}}。",
|
||||
"settings_dropbox_revoke": "撤回鉴权",
|
||||
"settings_dropbox_revoke_desc": "您现在使用了用户名 {{username}} 来连接。如果想取消连接,请点击此按钮。",
|
||||
"settings_dropbox_revoke_button": "撤回鉴权",
|
||||
"settings_dropbox_revoke_notice": "鉴权已被撤回!",
|
||||
"settings_dropbox_revoke_noticeerr": "撤回鉴权出错。",
|
||||
"settings_dropbox_clearlocal": "清除本地保存的鉴权信息",
|
||||
"settings_dropbox_clearlocal_desc": "您可以强制清除本地已保存的 Dropbox 的登录鉴权信息,但不向服务器发送撤回鉴权的请求。",
|
||||
"settings_dropbox_clearlocal_button": "清除",
|
||||
"settings_dropbox_clearlocal_notice": "已清除!",
|
||||
"settings_dropbox_auth": "鉴权",
|
||||
"settings_dropbox_auth_desc": "鉴权。",
|
||||
"settings_dropbox_auth_button": "鉴权",
|
||||
"settings_dropbox_connect_succ": "很好!我们可连接上 Dropbox!",
|
||||
"settings_dropbox_connect_fail": "我们未能连接上 Dropbox。",
|
||||
"settings_onedrive": "Onedrive(个人版)设置",
|
||||
"settings_onedrive_disclaimer1": "声明:此插件不是微软或 OneDrive 的官方产品。",
|
||||
"settings_onedrive_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 OneDrive 有不符合预期的访问,请立刻从 https://microsoft.com/consent 删除记录于此插件的连接鉴权。",
|
||||
"settings_onedrive_folder": "我们会在您的 OneDrive 上创建此文件夹并在里面同步:/Apps/{{pluginID}}/{{remoteBaseDir}}。",
|
||||
"settings_onedrive_nobiz": "现在只支持个人版 OneDrive。(暂时)并不支持企业版 OneDrive。",
|
||||
"settings_onedrive_revoke": "撤回鉴权",
|
||||
"settings_onedrive_revoke_desc": "您现在使用了用户名 {{username}} 来连接。如果想取消连接,请点击此按钮。",
|
||||
"settings_onedrive_revoke_button": "撤回鉴权",
|
||||
"settings_onedrive_auth": "鉴权",
|
||||
"settings_onedrive_auth_desc": "鉴权。",
|
||||
"settings_onedrive_auth_button": "鉴权",
|
||||
"settings_onedrive_connect_succ": "很好!我们可连接上 OneDrive!",
|
||||
"settings_onedrive_connect_fail": "我们未能连接上 OneDrive。",
|
||||
"settings_webdav": "Webdav 设置",
|
||||
"settings_webdav_disclaimer1": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 Webdav 服务器有不符合预期的访问,请立刻修改用户名和密码。",
|
||||
"settings_webdav_cors_os": "Obsidian 桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.2.1 支持跳过 CORS 设置。但您正在使用旧版,建议升级。",
|
||||
"settings_webdav_cors": "您需要设置 CORS,允许来自这些源的请求:app://obsidian.md 和 capacitor://localhost 和 http://localhost。",
|
||||
"settings_webdav_folder": "我们会在您的服务器上创建此文件夹并在里面同步:/{{remoteBaseDir}}",
|
||||
"settings_webdav_addr": "服务器地址",
|
||||
"settings_webdav_addr_desc": "服务器地址。",
|
||||
"settings_webdav_user": "用户名",
|
||||
"settings_webdav_user_desc": "用户名。注意:用户名和其它信息都会保存在本地。",
|
||||
"settings_webdav_password": "密码",
|
||||
"settings_webdav_password_desc": "密码。注意:密码和其它信息都会保存在本地。",
|
||||
"settings_webdav_auth": "鉴权类型",
|
||||
"settings_webdav_auth_desc": "如果不填写密码,本设置会被忽略。",
|
||||
"settings_webdav_depth": "发送到服务器的 Depth header",
|
||||
"settings_webdav_depth_desc": "Webdav 服务器会被设为接收 Depth header 为“1”或“Infinity”请求。如果您不清楚如何设置,可以选择 depth='1'。如果那你确认服务器支持 depth='infinity',请选择它,会获得更加好的性能。",
|
||||
"settings_webdav_depth_1": "只支持 depth='1'",
|
||||
"settings_webdav_depth_inf": "支持 depth='infinity'",
|
||||
"settings_webdav_connect_succ": "很好!可以连接上 Webdav 服务器。",
|
||||
"settings_webdav_connect_fail": "无法连接上 Webdav 服务器。(可能是地址/账号/密码/鉴权类型等错误。)",
|
||||
"settings_webdav_connect_fail_withcors": "无法连接上 Webdav 服务器。(可能是地址/账号/密码/鉴权类型/CORS 等错误。)",
|
||||
"settings_chooseservice": "选择远程服务",
|
||||
"settings_chooseservice_desc": "从这里开始设置。您想连接到哪一个服务?S3、Dropbox、Webdav、OneDrive(个人版)?",
|
||||
"settings_chooseservice_s3": "S3 或兼容 S3 的服务",
|
||||
"settings_chooseservice_dropbox": "Dropbox",
|
||||
"settings_chooseservice_webdav": "Webdav",
|
||||
"settings_chooseservice_onedrive": "OneDrive(个人版)",
|
||||
"settings_adv": "进阶设置",
|
||||
"settings_concurrency": "并行度",
|
||||
"settings_concurrency_desc": "您希望同时最多有多少个文件被上传和下载?默认值是 5。如果您遇到了一些问题(如访问频率限制),您可以减少并行度。",
|
||||
"settings_syncunderscore": "同步 _ 文件或文件夹",
|
||||
"settings_syncunderscore_desc": "是否同步“_”(下划线)开始的文件或文件夹。",
|
||||
"settings_configdir": "同步配置文件夹(实验性质)",
|
||||
"settings_configdir_desc": "是否同步 Obsidian 的配置文件夹 {{configDir}}(其中的 .git 和 node_modules 依然会被忽略)。请注意,此设置可能会影响到您 Obsidian 或者所有的插件设置,也可能需要您同步完毕后重启 Obsidian。如果您同意自行承受其风险,则可以开启本设置。",
|
||||
"settings_deletetowhere": "删除文件到哪里",
|
||||
"settings_deletetowhere_desc": "插件触发删除操作时候,删除到哪里?",
|
||||
"settings_deletetowhere_system_trash": "系统回收站(默认)",
|
||||
"settings_deletetowhere_obsidian_trash": "Obsidian .trash 文件夹",
|
||||
"settings_conflictaction": "处理冲突",
|
||||
"settings_conflictaction_desc": "如果一个文件,在本地和服务器都被创建或者修改了,那么这就是一个“冲突”情况。如何处理?这个设置只在双向同步时候生效。",
|
||||
"settings_conflictaction_keep_newer": "保留最后修改的版本(默认)",
|
||||
"settings_conflictaction_keep_larger": "保留文件体积较大的版本",
|
||||
"settings_cleanemptyfolder": "处理空文件夹",
|
||||
"settings_cleanemptyfolder_desc": "同步算法主要是针对文件处理的,您要要手动指定空文件夹如何处理。",
|
||||
"settings_cleanemptyfolder_skip": "跳过处理空文件夹(默认)",
|
||||
"settings_cleanemptyfolder_clean_both": "删除本地和服务器的空文件夹",
|
||||
"settings_protectmodifypercentage": "如果修改超过百分比则中止同步",
|
||||
"settings_protectmodifypercentage_desc": "如果算法检测到超过 n% 的文件会被修改或删除,则中止同步。从而可以保护用户的文件免受预料之外的修改。您可以设置为 100 而去除此保护,也可以设置为 0 总是强制中止所有同步。",
|
||||
"settings_protectmodifypercentage_000_desc": "0(总是强制中止)",
|
||||
"settings_protectmodifypercentage_050_desc": "50(默认值)",
|
||||
"settings_protectmodifypercentage_100_desc": "100(去除此保护)",
|
||||
"setting_syncdirection": "同步方向",
|
||||
"setting_syncdirection_desc": "插件应该向哪里同步?注意每个选项都是只有修改了的文件(基于修改时间和大小判断)才会触发同步动作。",
|
||||
"setting_syncdirection_bidirectional_desc": "双向同步(默认)",
|
||||
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:备份模式)",
|
||||
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
||||
"settings_enablemobilestatusbar": "是否显示手机的状态栏",
|
||||
"settings_enablemobilestatusbar_desc": "Obsidian 手机版默认隐藏了状态栏。有些用户希望展示它。这里提供了设置选项。",
|
||||
"settings_importexport": "导入导出部分设置",
|
||||
"settings_export": "导出",
|
||||
"settings_export_desc": "用 QR 码导出非 oauth2 的设置信息。",
|
||||
"settings_export_desc_button": "生成 QR 码",
|
||||
"settings_import": "导入",
|
||||
"settings_import_desc": "您需要使用系统拍摄 app 或者扫描 QR 码的app,来扫描对应的 QR 码。",
|
||||
"settings_debug": "调试",
|
||||
"settings_debuglevel": "修改同步提示信息",
|
||||
"settings_debuglevel_desc": "默认值为 \"info\"。您可以改为 \"debug\" 从而在同步时候里获取更多信息。",
|
||||
"settings_outputsettingsconsole": "读取硬盘上的设置文件输出到终端",
|
||||
"settings_outputsettingsconsole_desc": "硬盘上的设置文件是编码过的,点击这里从而解码并输出到终端。",
|
||||
"settings_outputsettingsconsole_button": "输出",
|
||||
"settings_outputsettingsconsole_notice": "已输出到终端",
|
||||
"settings_obfuscatesettingfile": "是否混淆保存设置文件",
|
||||
"settings_obfuscatesettingfile_desc": "设置文件(data.json)含有敏感信息。强烈建议混淆后保存它,从而避免出乎意料的读取和修改。如果您确认要手动查看和修改它,可以关闭混淆保存。",
|
||||
"settings_viewconsolelog": "查看终端输出",
|
||||
"settings_viewconsolelog_desc": "电脑上,输入“ctrl+shift+i”或“cmd+shift+i”来查看终端输出。手机上,安装第三方插件 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 来导出终端输出到一篇笔记上。",
|
||||
"settings_syncplans": "导出同步计划",
|
||||
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
|
||||
"settings_syncplans_button_json": "导出",
|
||||
"settings_syncplans_notice": "同步计划已导出",
|
||||
"settings_delsyncplans": "删除数据库里的同步计划历史",
|
||||
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
|
||||
"settings_delsyncplans_button": "删除同步计划历史",
|
||||
"settings_delsyncplans_notice": "(数据库里的)同步计划已被删除。",
|
||||
"settings_delprevsync": "删除数据库里的上次同步明细",
|
||||
"settings_delprevsync_desc": "同步算法需要上次成功同步的信息来决定文件变更,这个信息保存在本地的数据库里。如果您想忽略这些信息从而所有文件都被视为新创建的话,可以在此删除之前的信息。",
|
||||
"settings_delprevsync_button": "删除上次同步明细",
|
||||
"settings_delprevsync_notice": "(本地数据库里的)上次同步明细已被删除。",
|
||||
"settings_outputbasepathvaultid": "输出资料库对应的位置和随机分配的 ID",
|
||||
"settings_outputbasepathvaultid_desc": "用于调试。",
|
||||
"settings_outputbasepathvaultid_button": "输出",
|
||||
"settings_resetcache": "重设本地缓存和数据库",
|
||||
"settings_resetcache_desc": "(出于调试原因)重设本地缓存和数据库。您需要在重设之后重新载入此插件。本重设不会删除 s3,密码……等设定。",
|
||||
"settings_resetcache_button": "重设",
|
||||
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
|
||||
"syncalgov3_title": "Remotely Save 的同步算法有重大更新",
|
||||
"syncalgov3_texts": "欢迎使用 Remotely Save!\n从这个版本开始,插件更新了同步算法:\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本,请阅读和勾选两个勾选框,然后点击“同意”按钮,开始使用插件吧!\n如果您不同意,请点击“不同意”按钮,插件将自动停止运行(unload)。\n此外,请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
|
||||
"syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库(Vault)。",
|
||||
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。",
|
||||
"syncalgov3_button_agree": "同意",
|
||||
"syncalgov3_button_disagree": "不同意"
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"confirm": "確認",
|
||||
"disable": "關閉",
|
||||
"enable": "開啟",
|
||||
"goback": "返回",
|
||||
"submit": "提交",
|
||||
"sometext": "這裡有一段文字。",
|
||||
"syncrun_alreadyrunning": "{{pluginName}} 正處於此階段:{{syncStatus}}!",
|
||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 觸發執行",
|
||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
||||
"syncrun_step1": "1/8 Remotely Save 準備同步({{serviceType}})",
|
||||
"syncrun_step2": "2/8 正在獲取遠端的元資料。",
|
||||
"syncrun_step3": "3/8 正在檢查密碼正確與否。",
|
||||
"syncrun_passworderr": "檢查密碼時候出錯。",
|
||||
"syncrun_step4": "4/8 正在獲取本地的元資料。",
|
||||
"syncrun_step5": "5/8 正在獲取本地上一次同步的元資料。",
|
||||
"syncrun_step6": "6/8 正在生成同步計劃。",
|
||||
"syncrun_step7": "7/8 Remotely Save 開始發生資料交換!",
|
||||
"syncrun_step7skip": "7/8 Remotely Save 在空跑模式,跳過實際資料交換步驟。",
|
||||
"syncrun_step8": "8/8 Remotely Save 已完成同步!",
|
||||
"syncrun_shortstep0": "0/2 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
||||
"syncrun_shortstep1": "1/2 Remotely Save 開始同步({{serviceType}})",
|
||||
"syncrun_shortstep2skip": "2/2 Remotely Save 在空跑模式,跳過實際資料交換步驟。",
|
||||
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
||||
"syncrun_abort": "{{manifestID}}-{{theDate}}:中斷同步,同步來源={{triggerSource}},出錯階段={{syncStatus}}",
|
||||
"syncrun_abort_protectmodifypercentage": "中斷同步!您設定了不允許 >= {{protectModifyPercentage}}% 的變更,但是現在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的檔案會被修改或刪除!如果您確認這次同步是您想要的,那麼請在設定裡修改允許比例。",
|
||||
"protocol_saveqr": " {{manifestName}} 新的非 oauth2 設定儲存完成。請重啟外掛設定頁使之生效。",
|
||||
"protocol_callbacknotsupported": "您的 uri callback 暫不支援: {{params}}",
|
||||
"protocol_dropbox_connecting": "正在連線 Dropbox……\n請不要關閉此彈窗。",
|
||||
"protocol_dropbox_connect_succ": "好!我們作為使用者 {{username}} 連線上了 Dropbox!",
|
||||
"protocol_dropbox_connect_succ_revoke": "您已作為使用者 {{username}} 連線上了。如果您想取消連線,請點選此按鈕。",
|
||||
"protocol_dropbox_connect_fail": "Dropbox 的回撥請求有點異常。您是否網路不順暢?您是否拒絕了鑑權?",
|
||||
"protocol_dropbox_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
||||
"protocol_dropbox_no_modal": "您不是在設定頁的彈窗裡觸發 Dropbox 的。中止操作。",
|
||||
"protocol_onedrive_connecting": "正在連線 OneDrive……\n請不要關閉此彈窗。",
|
||||
"protocol_onedrive_connect_succ_revoke": "您已作為使用者 {{username}} 連線上了。如果您想取消連線,請點選此按鈕。",
|
||||
"protocol_onedrive_connect_fail": "OneDrive 的回撥請求有點異常。您是否拒絕了鑑權?",
|
||||
"protocol_onedrive_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
||||
"command_startsync": "開始同步(start sync)",
|
||||
"command_drynrun": "開始同步(空跑模式)(start sync (dry run only))",
|
||||
"command_exportsyncplans_json": "匯出同步計劃為 json 格式(export sync plans in json format)",
|
||||
"command_exportlogsindb": "從資料庫匯出終端日誌(export logs saved in db)",
|
||||
|
||||
"statusbar_time_years": "{{time}} 年前同步",
|
||||
"statusbar_time_months": "{{time}} 月前同步",
|
||||
"statusbar_time_weeks": "{{time}} 周前同步",
|
||||
"statusbar_time_days": "{{time}} 天前同步",
|
||||
"statusbar_time_hours": "{{time}} 小時前同步",
|
||||
"statusbar_time_minutes": "{{time}} 分鐘前同步",
|
||||
"statusbar_time_lessminute": "一分鐘之內同步",
|
||||
"statusbar_lastsync": "上一次同步於:{{time}}",
|
||||
"statusbar_syncing": "正在同步",
|
||||
"statusbar_now": "剛同步完",
|
||||
"statusbar_lastsync_label": "上一次同步於:{{date}}",
|
||||
"statusbar_lastsync_never": "沒觸發過同步",
|
||||
"statusbar_lastsync_never_label": "沒觸發過同步",
|
||||
|
||||
"modal_password_title": "稍等一下,請閱讀下文:",
|
||||
"modal_password_shortdesc": "如果密碼不是空的,那麼檔案會在上傳之前,在本地先用此密碼加密。\n如果密碼是空的,那麼檔案會被非加密地上傳。",
|
||||
"modal_password_attn1": "注意 1/5:儲存庫(Vault)名字是不會加密的!本外掛會在一些遠端儲存裡建立一個和庫名字有著同名的資料夾。",
|
||||
"modal_password_attn2": "注意 2/5:密碼在本地不加密地儲存!",
|
||||
"modal_password_attn3": "注意 3/5:一些元資訊是沒有被加密的,或者可以被較容易地猜測出來。(檔案大小很接近於沒加密的時候,且資料夾可能會被儲存為大小為 0 byte 的物件。)",
|
||||
"modal_password_attn4": "注意 4/5:您需要確保遠端儲存是“空”的,或者已有檔案也是被同一個新密碼加密的;從而避免衝突。",
|
||||
"modal_password_attn5": "注意 5/5:密碼越長越好。",
|
||||
"modal_password_secondconfirm": "再次確認儲存新密碼",
|
||||
"modal_password_notice": "新密碼已儲存!",
|
||||
"modal_remotebasedir_title": "您正在修改遠端基資料夾設定",
|
||||
"modal_remotebasedir_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定會被重設回庫的資料夾名(預設設定)。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
|
||||
"modal_remotebasedir_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
|
||||
"modal_remotebasedir_secondconfirm_vaultname": "重設回預設的庫資料夾名",
|
||||
"modal_remotebasedir_secondconfirm_change": "確認修改",
|
||||
"modal_remotebasedir_notice": "新的遠端基資料夾設定已儲存!",
|
||||
"modal_remoteprefix_title": "您正在修改遠端路徑字首設定",
|
||||
"modal_remoteprefix_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定為儲存為空,檔案將會被儲存在桶(Bucket)的根目錄。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
|
||||
"modal_remoteprefix_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
|
||||
"modal_remoteprefix_tosave": "您設定的新字首為:“{{{prefix}}}”",
|
||||
"modal_remoteprefix_secondconfirm_empty": "字首為空,檔案會儲存在根目錄",
|
||||
"modal_remoteprefix_secondconfirm_change": "確認修改",
|
||||
"modal_remoteprefix_notice": "新的遠端路徑字首設定已儲存!",
|
||||
"modal_dropboxauth_manualsteps": "第 1 步:在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會獲得一串很長的程式碼文字,請複製貼上到下方,並點選“提交”",
|
||||
"modal_dropboxauth_autosteps": "在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。",
|
||||
"modal_dropboxauth_copybutton": "點選此按鈕從而複製鑑權 url",
|
||||
"modal_dropboxauth_copynotice": "鑑權 url 已複製到剪貼簿!",
|
||||
"modal_dropboxauth_maualinput": "來自網頁的授權碼",
|
||||
"modal_dropboxauth_maualinput_desc": "您需要點選“確認”",
|
||||
"modal_dropboxauth_maualinput_notice": "正在嘗試連線 Dropbox",
|
||||
"modal_dropboxauth_maualinput_conn_succ": "很好!我們作為使用者 {{username}} 連線到 Dropbox 了!",
|
||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作為使用者 {{username}} 連線到 Dropbox。如果您想斷開連線,點選此按鈕。",
|
||||
"modal_dropboxauth_maualinput_conn_fail": "連線 Dropbox 途中出錯了。",
|
||||
"modal_onedriveauth_shortdesc": "現在只支援個人版 OneDrive,(暫)不支援企業版。\n在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。",
|
||||
"modal_onedriveauth_copybutton": "點選此按鈕從而複製鑑權 url",
|
||||
"modal_onedriveauth_copynotice": "鑑權 url 已複製到剪貼簿!",
|
||||
"modal_onedriverevokeauth_step1": "第 1 步:用瀏覽器開啟以下地址,點選本外掛對應的“Edit”按鈕,點選“Remove these permissions”按鈕。",
|
||||
"modal_onedriverevokeauth_step2": "第 2 步:點選以下按鈕,清理本地儲存的登入資訊。",
|
||||
"modal_onedriverevokeauth_clean": "清理本地儲存的登入資訊",
|
||||
"modal_onedriverevokeauth_clean_desc": "您需要點選“清理”按鈕。",
|
||||
"modal_onedriverevokeauth_clean_button": "清理",
|
||||
"modal_onedriverevokeauth_clean_notice": "已清理!",
|
||||
"modal_onedriverevokeauth_clean_fail": "鑑權途中出錯了。",
|
||||
"modal_syncconfig_attn": "注意 1/2:此設定只同步(複製)整個 Obsidian 的配置資料夾,但是不會同步其它 . 開頭的資料夾或檔案。除了會忽略 .git 和 node_modules 資料夾之外,它也並不理解配置資料夾的裡各個子檔案或子資料夾的含義。\n注意 2/2:配置資料夾被同步之後,各外掛的設定或許會出錯,且 Obsidian 或許需要重啟來過載各外掛的新配置。\n如果您同意自行承受以上風險,您可以點選以下再次確認按鈕。",
|
||||
"modal_syncconfig_secondconfirm": "再次確認開啟",
|
||||
"modal_syncconfig_notice": "您已開啟配置資料夾的同步!",
|
||||
"modal_qr_shortdesc": "這裡可匯出非 oauth2 設定。(意味著:Dropbox 和 OneDrive 資訊不會被匯出。)\n您可以使用另一個裝置來掃描此 QR 碼。\n又或者,您可以點選以下按鈕複製此特殊 URI。",
|
||||
"modal_qr_button": "點選此按鈕複製特殊 URI",
|
||||
"modal_qr_button_notice": "特殊 URI 已被複制到剪貼簿!",
|
||||
"modal_sizesconflict_title": "Remotely Save:跳過大檔案的時候出現了一些衝突",
|
||||
"modal_sizesconflict_desc": "您設定了跳過同步大於 {{thresholdMB}} MB({{thresholdBytes}} bytes)的檔案。\n但是以下檔案的大小,在一端大於閾值,在另一端則小於閾值。\n為了避免意外的覆蓋或刪除,外掛停止了運作,您需要手動處理至少一端的檔案。",
|
||||
"modal_sizesconflict_copybutton": "點選以複製以下所有檔案大小衝突資訊",
|
||||
"modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
|
||||
"settings_basic": "基本設定",
|
||||
"settings_password": "密碼",
|
||||
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。",
|
||||
"settings_autorun": "自動執行",
|
||||
"settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。",
|
||||
"settings_autorun_notset": "(不設定)",
|
||||
"settings_autorun_1min": "每 1 分鐘",
|
||||
"settings_autorun_5min": "每 5 分鐘",
|
||||
"settings_autorun_10min": "每 10 分鐘",
|
||||
"settings_autorun_30min": "每 30 分鐘",
|
||||
"settings_runoncestartup": "啟動後自動執行一次",
|
||||
"settings_runoncestartup_desc": "本設定使得外掛在程式啟動後自動觸發一次同步。修改後,會在下一次啟動生效。本設定和“自動執行”是不同的,那個設定是指“每隔一段時間”都自動同步一次。",
|
||||
"settings_runoncestartup_notset": "(不設定)",
|
||||
"settings_runoncestartup_1sec": "啟動後第 1 秒執行一次",
|
||||
"settings_runoncestartup_10sec": "啟動後第 10 秒執行一次",
|
||||
"settings_runoncestartup_30sec": "啟動後第 30 秒執行一次",
|
||||
"settings_saverun": "儲存時同步(實驗性質)",
|
||||
"settings_saverun_desc": "外掛如果檢查到當前檔案在最近一段時間有修改儲存過,則嘗試同步。請注意,同步是一個很重的操作,因此會影響到耗電量。(修改設定後可能需要過載外掛或重啟。)",
|
||||
"settings_saverun_notset": "(不設定)",
|
||||
"settings_saverun_1sec": "隔 1 秒檢查一次",
|
||||
"settings_saverun_5sec": "隔 5 秒檢查一次",
|
||||
"settings_saverun_10sec": "隔 10 秒檢查一次(推薦)",
|
||||
"settings_saverun_1min": "隔 1 分鐘檢查一次",
|
||||
"settings_skiplargefiles": "跳過大檔案",
|
||||
"settings_skiplargefiles_desc": "跳過大於某一個閾值的檔案。這裡 1 MB = 10^6 bytes。",
|
||||
"settings_skiplargefiles_notset": "(不設定)",
|
||||
|
||||
"settings_ignorepaths": "忽略的檔案或資料夾的正則表示式",
|
||||
"settings_ignorepaths_desc": "忽略的檔案或資料夾的正則表示式。每行一條。路徑是相對於庫(Vault)根目錄的,沒有前置 / 符號。",
|
||||
"settings_enablestatusbar_info": "在狀態列顯示上一次成功的同步",
|
||||
"settings_enablestatusbar_info_desc": "在狀態列顯示上一次成功的同步的時間。",
|
||||
"settings_enablestatusbar_reloadrequired_notice": "需要重新載入此外掛才生效。",
|
||||
"settings_resetstatusbar_time": "重設上一次成功同步的時間記錄",
|
||||
"settings_resetstatusbar_time_desc": "重設上一次成功同步的時間記錄。",
|
||||
"settings_resetstatusbar_button": "重設",
|
||||
"settings_resetstatusbar_notice": "重設完畢!",
|
||||
|
||||
"settings_checkonnectivity": "檢查可否連線",
|
||||
"settings_checkonnectivity_desc": "檢查可否連線。",
|
||||
"settings_checkonnectivity_button": "檢查",
|
||||
"settings_checkonnectivity_checking": "正在檢查……",
|
||||
"settings_remotebasedir": "修改遠端基資料夾(實驗性質)",
|
||||
"settings_remotebasedir_desc": "預設設定,內容會被同步到遠端的和資料庫同名的資料夾下。您可以在此修改遠端資料夾名,或刪除輸入框文字從而重設到預設值。您需要點選“確認”。",
|
||||
"settings_remoteprefix": "修改遠端字首路徑(實驗性質)",
|
||||
"settings_remoteprefix_desc": "預設設定 s3 儲存在儲存桶(Bucket)的根目錄。您可以在這裡修改路徑字首,或者保持為空保持預設設定。您需要點選“確認”。",
|
||||
"settings_s3": "S3 或相容 S3 的服務的設定",
|
||||
"settings_s3_disclaimer1": "宣告:本外掛不是 Amazon 的官方產品。",
|
||||
"settings_s3_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了儲存桶有不符合預期的訪問,請立刻從 AWS(或其它 S3 服務商)刪除記錄於此的 access key。",
|
||||
"settings_s3_cors": "您需要設定 CORS,從而允許這些源的請求訪問:app://obsidian.md 和 capacitor://localhost 和 http://localhost 且增加 ETag 到暴露 headers 裡。",
|
||||
"settings_s3_prod": "一些可參考的 Amazon S3 官方文件:",
|
||||
"settings_s3_prod1": "服務地址(Endpoint)和區域(Region)資訊",
|
||||
"settings_s3_prod2": "Access Key ID 和 Secret Access Key 資訊",
|
||||
"settings_s3_prod3": "設定 CORS",
|
||||
"settings_s3_endpoint": "服務地址(Endpoint)",
|
||||
"settings_s3_region": "區域(Region)",
|
||||
"settings_s3_region_desc": "如果您不知道這個值填什麼,可以嘗試填寫:us-east-1。",
|
||||
"settings_s3_accesskeyid": "Access Key ID",
|
||||
"settings_s3_accesskeyid_desc": "Access key ID。注意:access key ID 和其它資訊都會在本地儲存。",
|
||||
"settings_s3_secretaccesskey": "Secret Access Key",
|
||||
"settings_s3_secretaccesskey_desc": "Secret access key。注意:secret access key 和其它資訊都會在本地儲存。",
|
||||
"settings_s3_bucketname": "儲存桶(Bucket)的名字",
|
||||
"settings_s3_bypasscorslocally": "本地繞過 CORS 問題",
|
||||
"settings_s3_bypasscorslocally_desc": "對於 Obsidian 新版本(桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.2.1),本外掛可以跳過伺服器設定 CORS 的步驟。如果您遇到任意問題,可以關閉此設定,並在服務端設定 CORS(允許來自 app://obsidian.md 和 capacitor://localhost 和 http://localhost 的請求且增加 ETag 到暴露 headers 裡)。",
|
||||
"settings_s3_parts": "分塊並行度",
|
||||
"settings_s3_parts_desc": "在 S3 裡,大檔案會被分塊上傳。您希望同一時間最多有多少個分塊被上傳?",
|
||||
"settings_s3_accuratemtime": "使用準確的檔案修改時間",
|
||||
"settings_s3_accuratemtime_desc": "讀取(已上傳的)準確的檔案修改時間,有助於同步演算法更加準確和穩定。但是它也會導致額外的 api 請求、時間、金錢花費。",
|
||||
"settings_s3_urlstyle": "S3 URL style",
|
||||
"settings_s3_urlstyle_desc": "是否對 S3 物件強制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
|
||||
"settings_s3_connect_succ": "很好!可以訪問到對應儲存桶。",
|
||||
"settings_s3_connect_fail": "無法訪問到對應儲存桶。",
|
||||
"settings_dropbox": "Dropbox 設定",
|
||||
"settings_dropbox_disclaimer1": "宣告:此外掛不是 Dropbox 官方產品。",
|
||||
"settings_dropbox_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 Dropbox 有不符合預期的訪問,請立刻從 https://www.dropbox.com/account/connected_apps 刪除記錄於此外掛的連線鑑權。",
|
||||
"settings_dropbox_folder": "我們會在您的 Dropbox 上建立此資料夾並在裡面同步:/Apps/{{pluginID}}/{{remoteBaseDir}}。",
|
||||
"settings_dropbox_revoke": "撤回鑑權",
|
||||
"settings_dropbox_revoke_desc": "您現在使用了使用者名稱 {{username}} 來連線。如果想取消連線,請點選此按鈕。",
|
||||
"settings_dropbox_revoke_button": "撤回鑑權",
|
||||
"settings_dropbox_revoke_notice": "鑑權已被撤回!",
|
||||
"settings_dropbox_revoke_noticeerr": "撤回鑑權出錯。",
|
||||
"settings_dropbox_clearlocal": "清除本地儲存的鑑權資訊",
|
||||
"settings_dropbox_clearlocal_desc": "您可以強制清除本地已儲存的 Dropbox 的登入鑑權資訊,但不向伺服器傳送撤回鑑權的請求。",
|
||||
"settings_dropbox_clearlocal_button": "清除",
|
||||
"settings_dropbox_clearlocal_notice": "已清除!",
|
||||
"settings_dropbox_auth": "鑑權",
|
||||
"settings_dropbox_auth_desc": "鑑權。",
|
||||
"settings_dropbox_auth_button": "鑑權",
|
||||
"settings_dropbox_connect_succ": "很好!我們可連線上 Dropbox!",
|
||||
"settings_dropbox_connect_fail": "我們未能連線上 Dropbox。",
|
||||
"settings_onedrive": "Onedrive(個人版)設定",
|
||||
"settings_onedrive_disclaimer1": "宣告:此外掛不是微軟或 OneDrive 的官方產品。",
|
||||
"settings_onedrive_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 OneDrive 有不符合預期的訪問,請立刻從 https://microsoft.com/consent 刪除記錄於此外掛的連線鑑權。",
|
||||
"settings_onedrive_folder": "我們會在您的 OneDrive 上建立此資料夾並在裡面同步:/Apps/{{pluginID}}/{{remoteBaseDir}}。",
|
||||
"settings_onedrive_nobiz": "現在只支援個人版 OneDrive。(暫時)並不支援企業版 OneDrive。",
|
||||
"settings_onedrive_revoke": "撤回鑑權",
|
||||
"settings_onedrive_revoke_desc": "您現在使用了使用者名稱 {{username}} 來連線。如果想取消連線,請點選此按鈕。",
|
||||
"settings_onedrive_revoke_button": "撤回鑑權",
|
||||
"settings_onedrive_auth": "鑑權",
|
||||
"settings_onedrive_auth_desc": "鑑權。",
|
||||
"settings_onedrive_auth_button": "鑑權",
|
||||
"settings_onedrive_connect_succ": "很好!我們可連線上 OneDrive!",
|
||||
"settings_onedrive_connect_fail": "我們未能連線上 OneDrive。",
|
||||
"settings_webdav": "Webdav 設定",
|
||||
"settings_webdav_disclaimer1": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 Webdav 伺服器有不符合預期的訪問,請立刻修改使用者名稱和密碼。",
|
||||
"settings_webdav_cors_os": "Obsidian 桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.1.1 支援跳過 CORS 設定。但您正在使用舊版,建議升級。",
|
||||
"settings_webdav_cors": "您需要設定 CORS,允許來自這些源的請求:app://obsidian.md 和 capacitor://localhost 和 http://localhost。",
|
||||
"settings_webdav_folder": "我們會在您的伺服器上建立此資料夾並在裡面同步:/{{remoteBaseDir}}",
|
||||
"settings_webdav_addr": "伺服器地址",
|
||||
"settings_webdav_addr_desc": "伺服器地址。",
|
||||
"settings_webdav_user": "使用者名稱",
|
||||
"settings_webdav_user_desc": "使用者名稱。注意:使用者名稱和其它資訊都會儲存在本地。",
|
||||
"settings_webdav_password": "密碼",
|
||||
"settings_webdav_password_desc": "密碼。注意:密碼和其它資訊都會儲存在本地。",
|
||||
"settings_webdav_auth": "鑑權型別",
|
||||
"settings_webdav_auth_desc": "如果不填寫密碼,本設定會被忽略。",
|
||||
"settings_webdav_depth": "傳送到伺服器的 Depth header",
|
||||
"settings_webdav_depth_desc": "Webdav 伺服器會被設為接收 Depth header 為“1”或“Infinity”請求。如果您不清楚如何設定,可以選擇 depth='1'。如果那你確認伺服器支援 depth='infinity',請選擇它,會獲得更加好的效能。",
|
||||
"settings_webdav_depth_1": "只支援 depth='1'",
|
||||
"settings_webdav_depth_inf": "支援 depth='infinity'",
|
||||
"settings_webdav_connect_succ": "很好!可以連線上 Webdav 伺服器。",
|
||||
"settings_webdav_connect_fail": "無法連線上 Webdav 伺服器。(可能是地址/賬號/密碼/鑑權型別等錯誤。)",
|
||||
"settings_webdav_connect_fail_withcors": "無法連線上 Webdav 伺服器。(可能是地址/賬號/密碼/鑑權型別/CORS 等錯誤。)",
|
||||
"settings_chooseservice": "選擇遠端服務",
|
||||
"settings_chooseservice_desc": "從這裡開始設定。您想連線到哪一個服務?S3、Dropbox、Webdav、OneDrive(個人版)?",
|
||||
"settings_chooseservice_s3": "S3 或相容 S3 的服務",
|
||||
"settings_chooseservice_dropbox": "Dropbox",
|
||||
"settings_chooseservice_webdav": "Webdav",
|
||||
"settings_chooseservice_onedrive": "OneDrive(個人版)",
|
||||
"settings_adv": "進階設定",
|
||||
"settings_concurrency": "並行度",
|
||||
"settings_concurrency_desc": "您希望同時最多有多少個檔案被上傳和下載?預設值是 5。如果您遇到了一些問題(如訪問頻率限制),您可以減少並行度。",
|
||||
"settings_syncunderscore": "同步 _ 檔案或資料夾",
|
||||
"settings_syncunderscore_desc": "是否同步“_”(下劃線)開始的檔案或資料夾。",
|
||||
"settings_configdir": "同步配置資料夾(實驗性質)",
|
||||
"settings_configdir_desc": "是否同步 Obsidian 的配置資料夾 {{configDir}}(其中的 .git 和 node_modules 依然會被忽略)。請注意,此設定可能會影響到您 Obsidian 或者所有的外掛設定,也可能需要您同步完畢後重啟 Obsidian。如果您同意自行承受其風險,則可以開啟本設定。",
|
||||
"settings_deletetowhere": "刪除檔案到哪裡",
|
||||
"settings_deletetowhere_desc": "外掛觸發刪除操作時候,刪除到哪裡?",
|
||||
"settings_deletetowhere_system_trash": "系統回收站(預設)",
|
||||
"settings_deletetowhere_obsidian_trash": "Obsidian .trash 資料夾",
|
||||
"settings_conflictaction": "處理衝突",
|
||||
"settings_conflictaction_desc": "如果一個檔案,在本地和伺服器都被建立或者修改了,那麼這就是一個“衝突”情況。如何處理?這個設定只在雙向同步時候生效。",
|
||||
"settings_conflictaction_keep_newer": "保留最後修改的版本(預設)",
|
||||
"settings_conflictaction_keep_larger": "保留檔案體積較大的版本",
|
||||
"settings_cleanemptyfolder": "處理空資料夾",
|
||||
"settings_cleanemptyfolder_desc": "同步演算法主要是針對檔案處理的,您需要手動指定空資料夾如何處理。",
|
||||
"settings_cleanemptyfolder_skip": "跳過處理空資料夾(預設)",
|
||||
"settings_cleanemptyfolder_clean_both": "刪除本地和伺服器的空資料夾",
|
||||
"settings_protectmodifypercentage": "如果修改超過百分比則中止同步",
|
||||
"settings_protectmodifypercentage_desc": "如果演算法檢測到超過 n% 的檔案會被修改或刪除,則中止同步。從而可以保護使用者的檔案免受預料之外的修改。您可以設定為 100 而去除此保護,也可以設定為 0 總是強制中止所有同步。",
|
||||
"settings_protectmodifypercentage_000_desc": "0(總是強制中止)",
|
||||
"settings_protectmodifypercentage_050_desc": "50(預設值)",
|
||||
"settings_protectmodifypercentage_100_desc": "100(去除此保護)",
|
||||
"setting_syncdirection": "同步方向",
|
||||
"setting_syncdirection_desc": "外掛應該向哪裡同步?注意每個選項都是隻有修改了的檔案(基於修改時間和大小判斷)才會觸發同步動作。",
|
||||
"setting_syncdirection_bidirectional_desc": "雙向同步(預設)",
|
||||
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:備份模式)",
|
||||
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
||||
"settings_enablemobilestatusbar": "是否顯示手機的狀態列",
|
||||
"settings_enablemobilestatusbar_desc": "Obsidian 手機版預設隱藏了狀態列。有些使用者希望展示它。這裡提供了設定選項。",
|
||||
"settings_importexport": "匯入匯出部分設定",
|
||||
"settings_export": "匯出",
|
||||
"settings_export_desc": "用 QR 碼匯出非 oauth2 的設定資訊。",
|
||||
"settings_export_desc_button": "生成 QR 碼",
|
||||
"settings_import": "匯入",
|
||||
"settings_import_desc": "您需要使用系統拍攝 app 或者掃描 QR 碼的app,來掃描對應的 QR 碼。",
|
||||
"settings_debug": "除錯",
|
||||
"settings_debuglevel": "修改同步提示資訊",
|
||||
"settings_debuglevel_desc": "預設值為 \"info\"。您可以改為 \"debug\" 從而在同步時候裡獲取更多資訊。",
|
||||
"settings_outputsettingsconsole": "讀取硬碟上的設定檔案輸出到終端",
|
||||
"settings_outputsettingsconsole_desc": "硬碟上的設定檔案是編碼過的,點選這裡從而解碼並輸出到終端。",
|
||||
"settings_outputsettingsconsole_button": "輸出",
|
||||
"settings_outputsettingsconsole_notice": "已輸出到終端",
|
||||
"settings_obfuscatesettingfile": "是否混淆儲存設定檔案",
|
||||
"settings_obfuscatesettingfile_desc": "設定檔案(data.json)含有敏感資訊。強烈建議混淆後儲存它,從而避免出乎意料的讀取和修改。如果您確認要手動檢視和修改它,可以關閉混淆儲存。",
|
||||
"settings_viewconsolelog": "檢視終端輸出",
|
||||
"settings_viewconsolelog_desc": "電腦上,輸入“ctrl+shift+i”或“cmd+shift+i”來檢視終端輸出。手機上,安裝第三方外掛 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 來匯出終端輸出到一篇筆記上。",
|
||||
"settings_syncplans": "匯出同步計劃",
|
||||
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
|
||||
"settings_syncplans_button_json": "匯出",
|
||||
"settings_syncplans_notice": "同步計劃已匯出",
|
||||
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
|
||||
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
|
||||
"settings_delsyncplans_button": "刪除同步計劃歷史",
|
||||
"settings_delsyncplans_notice": "(資料庫裡的)同步計劃已被刪除。",
|
||||
"settings_delprevsync": "刪除資料庫裡的上次同步明細",
|
||||
"settings_delprevsync_desc": "同步演算法需要上次成功同步的資訊來決定檔案變更,這個資訊儲存在本地的資料庫裡。如果您想忽略這些資訊從而所有檔案都被視為新建立的話,可以在此刪除之前的資訊。",
|
||||
"settings_delprevsync_button": "刪除上次同步明細",
|
||||
"settings_delprevsync_notice": "(本地資料庫裡的)上次同步明細已被刪除。",
|
||||
"settings_outputbasepathvaultid": "輸出資料庫對應的位置和隨機分配的 ID",
|
||||
"settings_outputbasepathvaultid_desc": "用於除錯。",
|
||||
"settings_outputbasepathvaultid_button": "輸出",
|
||||
"settings_resetcache": "重設本地快取和資料庫",
|
||||
"settings_resetcache_desc": "(出於除錯原因)重設本地快取和資料庫。您需要在重設之後重新載入此外掛。本重設不會刪除 s3,密碼……等設定。",
|
||||
"settings_resetcache_button": "重設",
|
||||
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
|
||||
"syncalgov3_title": "Remotely Save 的同步演算法有重大更新",
|
||||
"syncalgov3_texts": "歡迎使用 Remotely Save!\n從這個版本開始,外掛更新了同步演算法:\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本,請閱讀和勾選兩個勾選框,然後點選“同意”按鈕,開始使用外掛吧!\n如果您不同意,請點選“不同意”按鈕,外掛將自動停止執行(unload)。\n此外,請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
|
||||
"syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫(Vault)。",
|
||||
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。",
|
||||
"syncalgov3_button_agree": "同意",
|
||||
"syncalgov3_button_disagree": "不同意"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { TFile, TFolder, type Vault } from "obsidian";
|
||||
import type { Entity, MixedEntity } from "./baseTypes";
|
||||
import { listFilesInObsFolder } from "./obsFolderLister";
|
||||
|
||||
export const getLocalEntityList = async (
|
||||
vault: Vault,
|
||||
syncConfigDir: boolean,
|
||||
configDir: string,
|
||||
pluginID: string
|
||||
) => {
|
||||
const local: Entity[] = [];
|
||||
|
||||
const localTAbstractFiles = vault.getAllLoadedFiles();
|
||||
for (const entry of localTAbstractFiles) {
|
||||
let r = {} as Entity;
|
||||
let key = entry.path;
|
||||
|
||||
if (entry.path === "/") {
|
||||
// ignore
|
||||
continue;
|
||||
} else if (entry instanceof TFile) {
|
||||
let mtimeLocal: number | undefined = entry.stat.mtime;
|
||||
if (mtimeLocal <= 0) {
|
||||
mtimeLocal = entry.stat.ctime;
|
||||
}
|
||||
if (mtimeLocal === 0) {
|
||||
mtimeLocal = undefined;
|
||||
}
|
||||
if (mtimeLocal === undefined) {
|
||||
throw Error(
|
||||
`Your file has last modified time 0: ${key}, don't know how to deal with it`
|
||||
);
|
||||
}
|
||||
r = {
|
||||
key: entry.path, // local always unencrypted
|
||||
keyRaw: entry.path,
|
||||
mtimeCli: mtimeLocal,
|
||||
mtimeSvr: mtimeLocal,
|
||||
size: entry.stat.size, // local always unencrypted
|
||||
sizeRaw: entry.stat.size,
|
||||
};
|
||||
} else if (entry instanceof TFolder) {
|
||||
key = `${entry.path}/`;
|
||||
r = {
|
||||
key: key,
|
||||
keyRaw: key,
|
||||
size: 0,
|
||||
sizeRaw: 0,
|
||||
};
|
||||
} else {
|
||||
throw Error(`unexpected ${entry}`);
|
||||
}
|
||||
|
||||
local.push(r);
|
||||
}
|
||||
|
||||
if (syncConfigDir) {
|
||||
const syncFiles = await listFilesInObsFolder(configDir, vault, pluginID);
|
||||
for (const f of syncFiles) {
|
||||
local.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
return local;
|
||||
};
|
||||
+254
-411
@@ -3,34 +3,34 @@ export type LocalForage = typeof localforage;
|
||||
import { nanoid } from "nanoid";
|
||||
import { requireApiVersion, TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
|
||||
import { API_VER_STAT_FOLDER, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
import { API_VER_STAT_FOLDER } from "./baseTypes";
|
||||
import type { Entity, MixedEntity, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
import type { SyncPlanType } from "./sync";
|
||||
import { toText, unixTimeToStr } from "./misc";
|
||||
import { statFix, toText, unixTimeToStr } from "./misc";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326];
|
||||
export const DEFAULT_DB_VERSION_NUMBER: number = 20220326;
|
||||
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326, 20240220];
|
||||
export const DEFAULT_DB_VERSION_NUMBER: number = 20240220;
|
||||
export const DEFAULT_DB_NAME = "remotelysavedb";
|
||||
export const DEFAULT_TBL_VERSION = "schemaversion";
|
||||
export const DEFAULT_TBL_FILE_HISTORY = "filefolderoperationhistory";
|
||||
export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory";
|
||||
export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory";
|
||||
export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
|
||||
export const DEFAULT_TBL_LOGGER_OUTPUT = "loggeroutput";
|
||||
export const DEFAULT_TBL_SIMPLE_KV_FOR_MISC = "simplekvformisc";
|
||||
export const DEFAULT_TBL_PREV_SYNC_RECORDS = "prevsyncrecords";
|
||||
|
||||
export interface FileFolderHistoryRecord {
|
||||
key: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
actionWhen: number;
|
||||
actionType: "delete" | "rename" | "renameDestination";
|
||||
keyType: "folder" | "file";
|
||||
renameTo: string;
|
||||
vaultRandomID: string;
|
||||
}
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const DEFAULT_TBL_FILE_HISTORY = "filefolderoperationhistory";
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* But we cannot remove it. Because we want to migrate the old data.
|
||||
*/
|
||||
interface SyncMetaMappingRecord {
|
||||
localKey: string;
|
||||
remoteKey: string;
|
||||
@@ -53,131 +53,118 @@ interface SyncPlanRecord {
|
||||
|
||||
export interface InternalDBs {
|
||||
versionTbl: LocalForage;
|
||||
fileHistoryTbl: LocalForage;
|
||||
syncMappingTbl: LocalForage;
|
||||
syncPlansTbl: LocalForage;
|
||||
vaultRandomIDMappingTbl: LocalForage;
|
||||
loggerOutputTbl: LocalForage;
|
||||
simpleKVForMiscTbl: LocalForage;
|
||||
prevSyncRecordsTbl: LocalForage;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* But we cannot remove it. Because we want to migrate the old data.
|
||||
*/
|
||||
fileHistoryTbl: LocalForage;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* But we cannot remove it. Because we want to migrate the old data.
|
||||
*/
|
||||
syncMappingTbl: LocalForage;
|
||||
}
|
||||
|
||||
/**
|
||||
* This migration mainly aims to assign vault name or vault id into all tables.
|
||||
* @param db
|
||||
* @param vaultRandomID
|
||||
* TODO
|
||||
* @param syncMappings
|
||||
* @returns
|
||||
*/
|
||||
const migrateDBsFrom20211114To20220108 = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const oldVer = 20211114;
|
||||
const newVer = 20220108;
|
||||
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
const fromSyncMappingsToPrevSyncRecords = (
|
||||
oldSyncMappings: SyncMetaMappingRecord[]
|
||||
): Entity[] => {
|
||||
const res: Entity[] = [];
|
||||
for (const oldMapping of oldSyncMappings) {
|
||||
const newEntity: Entity = {
|
||||
key: oldMapping.localKey,
|
||||
keyEnc: oldMapping.remoteKey,
|
||||
keyRaw:
|
||||
oldMapping.remoteKey !== undefined && oldMapping.remoteKey !== ""
|
||||
? oldMapping.remoteKey
|
||||
: oldMapping.localKey,
|
||||
mtimeCli: oldMapping.localMtime,
|
||||
mtimeSvr: oldMapping.remoteMtime,
|
||||
size: oldMapping.localSize,
|
||||
sizeEnc: oldMapping.remoteSize,
|
||||
sizeRaw:
|
||||
oldMapping.remoteKey !== undefined && oldMapping.remoteKey !== ""
|
||||
? oldMapping.remoteSize
|
||||
: oldMapping.localSize,
|
||||
etag: oldMapping.remoteExtraKey,
|
||||
};
|
||||
|
||||
const allPromisesToWait: Promise<any>[] = [];
|
||||
|
||||
log.debug("assign vault id to any delete history");
|
||||
const keysInDeleteHistoryTbl = await db.fileHistoryTbl.keys();
|
||||
for (const key of keysInDeleteHistoryTbl) {
|
||||
if (key.startsWith(vaultRandomID)) {
|
||||
continue;
|
||||
res.push(newEntity);
|
||||
}
|
||||
const value = (await db.fileHistoryTbl.getItem(
|
||||
key
|
||||
)) as FileFolderHistoryRecord;
|
||||
if (value === null || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||
value.vaultRandomID = vaultRandomID;
|
||||
}
|
||||
const newKey = `${vaultRandomID}\t${key}`;
|
||||
allPromisesToWait.push(db.fileHistoryTbl.setItem(newKey, value));
|
||||
allPromisesToWait.push(db.fileHistoryTbl.removeItem(key));
|
||||
}
|
||||
|
||||
log.debug("assign vault id to any sync mapping");
|
||||
const keysInSyncMappingTbl = await db.syncMappingTbl.keys();
|
||||
for (const key of keysInSyncMappingTbl) {
|
||||
if (key.startsWith(vaultRandomID)) {
|
||||
continue;
|
||||
}
|
||||
const value = (await db.syncMappingTbl.getItem(
|
||||
key
|
||||
)) as SyncMetaMappingRecord;
|
||||
if (value === null || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||
value.vaultRandomID = vaultRandomID;
|
||||
}
|
||||
const newKey = `${vaultRandomID}\t${key}`;
|
||||
allPromisesToWait.push(db.syncMappingTbl.setItem(newKey, value));
|
||||
allPromisesToWait.push(db.syncMappingTbl.removeItem(key));
|
||||
}
|
||||
|
||||
log.debug("assign vault id to any sync plan records");
|
||||
const keysInSyncPlansTbl = await db.syncPlansTbl.keys();
|
||||
for (const key of keysInSyncPlansTbl) {
|
||||
if (key.startsWith(vaultRandomID)) {
|
||||
continue;
|
||||
}
|
||||
const value = (await db.syncPlansTbl.getItem(key)) as SyncPlanRecord;
|
||||
if (value === null || value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (value.vaultRandomID === undefined || value.vaultRandomID === "") {
|
||||
value.vaultRandomID = vaultRandomID;
|
||||
}
|
||||
const newKey = `${vaultRandomID}\t${key}`;
|
||||
allPromisesToWait.push(db.syncPlansTbl.setItem(newKey, value));
|
||||
allPromisesToWait.push(db.syncPlansTbl.removeItem(key));
|
||||
}
|
||||
|
||||
log.debug("finally update version if everything is ok");
|
||||
await Promise.all(allPromisesToWait);
|
||||
await db.versionTbl.setItem("version", newVer);
|
||||
|
||||
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* no need to do anything except changing version
|
||||
* we just add more file operations in db, and no schema is changed.
|
||||
*
|
||||
* @param db
|
||||
* @param vaultRandomID
|
||||
* Migrate the sync mapping record to sync Entity.
|
||||
*/
|
||||
const migrateDBsFrom20220108To20220326 = async (
|
||||
const migrateDBsFrom20220326To20240220 = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
vaultRandomID: string,
|
||||
profileID: string
|
||||
) => {
|
||||
const oldVer = 20220108;
|
||||
const newVer = 20220326;
|
||||
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
await db.versionTbl.setItem("version", newVer);
|
||||
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
const oldVer = 20220326;
|
||||
const newVer = 20240220;
|
||||
console.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
|
||||
// from sync mapping to prev sync
|
||||
const syncMappings = await getAllSyncMetaMappingByVault(db, vaultRandomID);
|
||||
const prevSyncRecords = fromSyncMappingsToPrevSyncRecords(syncMappings);
|
||||
for (const prevSyncRecord of prevSyncRecords) {
|
||||
await upsertPrevSyncRecordByVaultAndProfile(
|
||||
db,
|
||||
vaultRandomID,
|
||||
profileID,
|
||||
prevSyncRecord
|
||||
);
|
||||
}
|
||||
|
||||
// // clear not used data
|
||||
// // as of 20240220, we don't call them,
|
||||
// // for the opportunity for users to downgrade
|
||||
// await clearFileHistoryOfEverythingByVault(db, vaultRandomID);
|
||||
// await clearAllSyncMetaMappingByVault(db, vaultRandomID);
|
||||
|
||||
await db.versionTbl.setItem(`${vaultRandomID}\tversion`, newVer);
|
||||
console.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
|
||||
};
|
||||
|
||||
const migrateDBs = async (
|
||||
db: InternalDBs,
|
||||
oldVer: number,
|
||||
newVer: number,
|
||||
vaultRandomID: string
|
||||
vaultRandomID: string,
|
||||
profileID: string
|
||||
) => {
|
||||
if (oldVer === newVer) {
|
||||
return;
|
||||
}
|
||||
if (oldVer === 20211114 && newVer === 20220108) {
|
||||
return await migrateDBsFrom20211114To20220108(db, vaultRandomID);
|
||||
|
||||
// as of 20240220, we assume everyone is using 20220326 already
|
||||
// drop any old code to reduce the verbose
|
||||
if (oldVer < 20220326) {
|
||||
throw Error(
|
||||
"You are using a very old version of Remotely Save. No way to auto update internal DB. Please install and enable 0.3.40 firstly, then install a later version."
|
||||
);
|
||||
}
|
||||
if (oldVer === 20220108 && newVer === 20220326) {
|
||||
return await migrateDBsFrom20220108To20220326(db, vaultRandomID);
|
||||
}
|
||||
if (oldVer === 20211114 && newVer === 20220326) {
|
||||
// TODO: more steps with more versions in the future
|
||||
await migrateDBsFrom20211114To20220108(db, vaultRandomID);
|
||||
await migrateDBsFrom20220108To20220326(db, vaultRandomID);
|
||||
return;
|
||||
|
||||
if (oldVer === 20220326 && newVer === 20240220) {
|
||||
return await migrateDBsFrom20220326To20240220(db, vaultRandomID, profileID);
|
||||
}
|
||||
|
||||
if (newVer < oldVer) {
|
||||
throw Error(
|
||||
"You've installed a new version, but then downgrade to an old version. Stop working!"
|
||||
@@ -189,21 +176,14 @@ const migrateDBs = async (
|
||||
|
||||
export const prepareDBs = async (
|
||||
vaultBasePath: string,
|
||||
vaultRandomIDFromOldConfigFile: string
|
||||
vaultRandomIDFromOldConfigFile: string,
|
||||
profileID: string
|
||||
) => {
|
||||
const db = {
|
||||
versionTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_VERSION,
|
||||
}),
|
||||
fileHistoryTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_FILE_HISTORY,
|
||||
}),
|
||||
syncMappingTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_SYNC_MAPPING,
|
||||
}),
|
||||
syncPlansTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_SYNC_PLANS_HISTORY,
|
||||
@@ -216,6 +196,23 @@ export const prepareDBs = async (
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_LOGGER_OUTPUT,
|
||||
}),
|
||||
simpleKVForMiscTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_SIMPLE_KV_FOR_MISC,
|
||||
}),
|
||||
prevSyncRecordsTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_PREV_SYNC_RECORDS,
|
||||
}),
|
||||
|
||||
fileHistoryTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_FILE_HISTORY,
|
||||
}),
|
||||
syncMappingTbl: localforage.createInstance({
|
||||
name: DEFAULT_DB_NAME,
|
||||
storeName: DEFAULT_TBL_SYNC_MAPPING,
|
||||
}),
|
||||
} as InternalDBs;
|
||||
|
||||
// try to get vaultRandomID firstly
|
||||
@@ -247,27 +244,35 @@ export const prepareDBs = async (
|
||||
throw Error("no vaultRandomID found or generated");
|
||||
}
|
||||
|
||||
const originalVersion: number | null = await db.versionTbl.getItem("version");
|
||||
// as of 20240220, we set the version per vault, instead of global "version"
|
||||
const originalVersion: number | null =
|
||||
(await db.versionTbl.getItem(`${vaultRandomID}\tversion`)) ??
|
||||
(await db.versionTbl.getItem("version"));
|
||||
if (originalVersion === null) {
|
||||
log.debug(
|
||||
console.debug(
|
||||
`no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}`
|
||||
);
|
||||
await db.versionTbl.setItem("version", DEFAULT_DB_VERSION_NUMBER);
|
||||
// as of 20240220, we set the version per vault, instead of global "version"
|
||||
await db.versionTbl.setItem(
|
||||
`${vaultRandomID}\tversion`,
|
||||
DEFAULT_DB_VERSION_NUMBER
|
||||
);
|
||||
} else if (originalVersion === DEFAULT_DB_VERSION_NUMBER) {
|
||||
// do nothing
|
||||
} else {
|
||||
log.debug(
|
||||
console.debug(
|
||||
`trying to upgrade db version from ${originalVersion} to ${DEFAULT_DB_VERSION_NUMBER}`
|
||||
);
|
||||
await migrateDBs(
|
||||
db,
|
||||
originalVersion,
|
||||
DEFAULT_DB_VERSION_NUMBER,
|
||||
vaultRandomID
|
||||
vaultRandomID,
|
||||
profileID
|
||||
);
|
||||
}
|
||||
|
||||
log.info("db connected");
|
||||
console.info("db connected");
|
||||
return {
|
||||
db: db,
|
||||
vaultRandomID: vaultRandomID,
|
||||
@@ -278,255 +283,79 @@ export const destroyDBs = async () => {
|
||||
// await localforage.dropInstance({
|
||||
// name: DEFAULT_DB_NAME,
|
||||
// });
|
||||
// log.info("db deleted");
|
||||
// console.info("db deleted");
|
||||
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME);
|
||||
req.onsuccess = (event) => {
|
||||
log.info("db deleted");
|
||||
console.info("db deleted");
|
||||
};
|
||||
req.onblocked = (event) => {
|
||||
log.warn("trying to delete db but it was blocked");
|
||||
console.warn("trying to delete db but it was blocked");
|
||||
};
|
||||
req.onerror = (event) => {
|
||||
log.error("tried to delete db but something goes wrong!");
|
||||
log.error(event);
|
||||
console.error("tried to delete db but something goes wrong!");
|
||||
console.error(event);
|
||||
};
|
||||
};
|
||||
|
||||
export const loadFileHistoryTableByVault = async (
|
||||
export const clearFileHistoryOfEverythingByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const records = [] as FileFolderHistoryRecord[];
|
||||
await db.fileHistoryTbl.iterate((value, key, iterationNumber) => {
|
||||
const keys = await db.fileHistoryTbl.keys();
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||
records.push(value as FileFolderHistoryRecord);
|
||||
await db.fileHistoryTbl.removeItem(key);
|
||||
}
|
||||
});
|
||||
records.sort((a, b) => a.actionWhen - b.actionWhen); // ascending
|
||||
return records;
|
||||
};
|
||||
|
||||
export const clearDeleteRenameHistoryOfKeyAndVault = async (
|
||||
db: InternalDBs,
|
||||
key: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const fullKey = `${vaultRandomID}\t${key}`;
|
||||
const item: FileFolderHistoryRecord | null = await db.fileHistoryTbl.getItem(
|
||||
fullKey
|
||||
);
|
||||
if (
|
||||
item !== null &&
|
||||
(item.actionType === "delete" || item.actionType === "rename")
|
||||
) {
|
||||
await db.fileHistoryTbl.removeItem(fullKey);
|
||||
}
|
||||
};
|
||||
|
||||
export const insertDeleteRecordByVault = async (
|
||||
db: InternalDBs,
|
||||
fileOrFolder: TAbstractFile,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
// log.info(fileOrFolder);
|
||||
let k: FileFolderHistoryRecord;
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k = {
|
||||
key: fileOrFolder.path,
|
||||
ctime: fileOrFolder.stat.ctime,
|
||||
mtime: fileOrFolder.stat.mtime,
|
||||
size: fileOrFolder.stat.size,
|
||||
actionWhen: Date.now(),
|
||||
actionType: "delete",
|
||||
keyType: "file",
|
||||
renameTo: "",
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
} else if (fileOrFolder instanceof TFolder) {
|
||||
// key should endswith "/"
|
||||
const key = fileOrFolder.path.endsWith("/")
|
||||
? fileOrFolder.path
|
||||
: `${fileOrFolder.path}/`;
|
||||
const ctime = 0; // they are deleted, so no way to get ctime, mtime
|
||||
const mtime = 0; // they are deleted, so no way to get ctime, mtime
|
||||
k = {
|
||||
key: key,
|
||||
ctime: ctime,
|
||||
mtime: mtime,
|
||||
size: 0,
|
||||
actionWhen: Date.now(),
|
||||
actionType: "delete",
|
||||
keyType: "folder",
|
||||
renameTo: "",
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
}
|
||||
await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
|
||||
};
|
||||
|
||||
/**
|
||||
* A file/folder is renamed from A to B
|
||||
* We insert two records:
|
||||
* A with actionType="rename"
|
||||
* B with actionType="renameDestination"
|
||||
* @deprecated But we cannot remove it. Because we want to migrate the old data.
|
||||
* @param db
|
||||
* @param fileOrFolder
|
||||
* @param oldPath
|
||||
* @param vaultRandomID
|
||||
* @returns
|
||||
*/
|
||||
export const insertRenameRecordByVault = async (
|
||||
export const getAllSyncMetaMappingByVault = async (
|
||||
db: InternalDBs,
|
||||
fileOrFolder: TAbstractFile,
|
||||
oldPath: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
// log.info(fileOrFolder);
|
||||
let k1: FileFolderHistoryRecord;
|
||||
let k2: FileFolderHistoryRecord;
|
||||
const actionWhen = Date.now();
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k1 = {
|
||||
key: oldPath,
|
||||
ctime: fileOrFolder.stat.ctime,
|
||||
mtime: fileOrFolder.stat.mtime,
|
||||
size: fileOrFolder.stat.size,
|
||||
actionWhen: actionWhen,
|
||||
actionType: "rename",
|
||||
keyType: "file",
|
||||
renameTo: fileOrFolder.path,
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
k2 = {
|
||||
key: fileOrFolder.path,
|
||||
ctime: fileOrFolder.stat.ctime,
|
||||
mtime: fileOrFolder.stat.mtime,
|
||||
size: fileOrFolder.stat.size,
|
||||
actionWhen: actionWhen,
|
||||
actionType: "renameDestination",
|
||||
keyType: "file",
|
||||
renameTo: "", // itself is the destination, so no need to set this field
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
} else if (fileOrFolder instanceof TFolder) {
|
||||
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
|
||||
const renameTo = fileOrFolder.path.endsWith("/")
|
||||
? fileOrFolder.path
|
||||
: `${fileOrFolder.path}/`;
|
||||
let ctime = 0;
|
||||
let mtime = 0;
|
||||
if (requireApiVersion(API_VER_STAT_FOLDER)) {
|
||||
// TAbstractFile does not contain these info
|
||||
// but from API_VER_STAT_FOLDER we can manually stat them by path.
|
||||
const s = await fileOrFolder.vault.adapter.stat(fileOrFolder.path);
|
||||
ctime = s.ctime;
|
||||
mtime = s.mtime;
|
||||
}
|
||||
k1 = {
|
||||
key: key,
|
||||
ctime: ctime,
|
||||
mtime: mtime,
|
||||
size: 0,
|
||||
actionWhen: actionWhen,
|
||||
actionType: "rename",
|
||||
keyType: "folder",
|
||||
renameTo: renameTo,
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
k2 = {
|
||||
key: renameTo,
|
||||
ctime: ctime,
|
||||
mtime: mtime,
|
||||
size: 0,
|
||||
actionWhen: actionWhen,
|
||||
actionType: "renameDestination",
|
||||
keyType: "folder",
|
||||
renameTo: "", // itself is the destination, so no need to set this field
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
}
|
||||
await Promise.all([
|
||||
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1.key}`, k1),
|
||||
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2.key}`, k2),
|
||||
]);
|
||||
};
|
||||
|
||||
export const upsertSyncMetaMappingDataByVault = async (
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
db: InternalDBs,
|
||||
localKey: string,
|
||||
localMTime: number,
|
||||
localSize: number,
|
||||
remoteKey: string,
|
||||
remoteMTime: number,
|
||||
remoteSize: number,
|
||||
remoteExtraKey: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const aggregratedInfo: SyncMetaMappingRecord = {
|
||||
localKey: localKey,
|
||||
localMtime: localMTime,
|
||||
localSize: localSize,
|
||||
remoteKey: remoteKey,
|
||||
remoteMtime: remoteMTime,
|
||||
remoteSize: remoteSize,
|
||||
remoteExtraKey: remoteExtraKey,
|
||||
remoteType: serviceType,
|
||||
keyType: localKey.endsWith("/") ? "folder" : "file",
|
||||
vaultRandomID: vaultRandomID,
|
||||
};
|
||||
await db.syncMappingTbl.setItem(
|
||||
`${vaultRandomID}\t${remoteKey}`,
|
||||
aggregratedInfo
|
||||
return await Promise.all(
|
||||
((await db.syncMappingTbl.keys()) ?? [])
|
||||
.filter((key) => key.startsWith(`${vaultRandomID}\t`))
|
||||
.map(
|
||||
async (key) =>
|
||||
(await db.syncMappingTbl.getItem(key)) as SyncMetaMappingRecord
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const getSyncMetaMappingByRemoteKeyAndVault = async (
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
export const clearAllSyncMetaMappingByVault = async (
|
||||
db: InternalDBs,
|
||||
remoteKey: string,
|
||||
remoteMTime: number,
|
||||
remoteExtraKey: string,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const potentialItem = (await db.syncMappingTbl.getItem(
|
||||
`${vaultRandomID}\t${remoteKey}`
|
||||
)) as SyncMetaMappingRecord;
|
||||
|
||||
if (potentialItem === null) {
|
||||
// no result was found
|
||||
return undefined;
|
||||
const keys = await db.syncMappingTbl.keys();
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||
await db.syncMappingTbl.removeItem(key);
|
||||
}
|
||||
|
||||
if (
|
||||
potentialItem.remoteKey === remoteKey &&
|
||||
potentialItem.remoteMtime === remoteMTime &&
|
||||
potentialItem.remoteExtraKey === remoteExtraKey &&
|
||||
potentialItem.remoteType === serviceType
|
||||
) {
|
||||
// the result was found
|
||||
return potentialItem;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllSyncMetaMapping = async (db: InternalDBs) => {
|
||||
await db.syncMappingTbl.clear();
|
||||
};
|
||||
|
||||
export const insertSyncPlanRecordByVault = async (
|
||||
db: InternalDBs,
|
||||
syncPlan: SyncPlanType,
|
||||
vaultRandomID: string
|
||||
vaultRandomID: string,
|
||||
remoteType: SUPPORTED_SERVICES_TYPE
|
||||
) => {
|
||||
const now = Date.now();
|
||||
const record = {
|
||||
ts: syncPlan.ts,
|
||||
tsFmt: syncPlan.tsFmt,
|
||||
ts: now,
|
||||
tsFmt: unixTimeToStr(now),
|
||||
vaultRandomID: vaultRandomID,
|
||||
remoteType: syncPlan.remoteType,
|
||||
remoteType: remoteType,
|
||||
syncPlan: JSON.stringify(syncPlan /* directly stringify */, null, 2),
|
||||
} as SyncPlanRecord;
|
||||
await db.syncPlansTbl.setItem(`${vaultRandomID}\t${syncPlan.ts}`, record);
|
||||
await db.syncPlansTbl.setItem(`${vaultRandomID}\t${now}`, record);
|
||||
};
|
||||
|
||||
export const clearAllSyncPlanRecords = async (db: InternalDBs) => {
|
||||
@@ -553,13 +382,13 @@ export const readAllSyncPlanRecordTextsByVault = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* We remove records that are older than 7 days or 10000 records.
|
||||
* We remove records that are older than 3 days or 100 records.
|
||||
* It's a heavy operation, so we shall not place it in the start up.
|
||||
* @param db
|
||||
*/
|
||||
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
||||
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 7; // 7 days
|
||||
const COUNT_TO_MANY = 10000;
|
||||
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 3; // 3 days
|
||||
const COUNT_TO_MANY = 100;
|
||||
|
||||
const currTs = Date.now();
|
||||
const expiredTs = currTs - MILLISECONDS_OLD;
|
||||
@@ -579,7 +408,7 @@ export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
||||
);
|
||||
|
||||
if (records.length - keysToRemove.size > COUNT_TO_MANY) {
|
||||
// we need to find out records beyond 10000 records
|
||||
// we need to find out records beyond 100 records
|
||||
records = records.filter((x) => !x.expired); // shrink the array
|
||||
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
||||
records.slice(COUNT_TO_MANY).forEach((element) => {
|
||||
@@ -594,90 +423,104 @@ export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
||||
await Promise.all(ps);
|
||||
};
|
||||
|
||||
export const readAllLogRecordTextsByVault = async (
|
||||
export const getAllPrevSyncRecordsByVaultAndProfile = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
profileID: string
|
||||
) => {
|
||||
// console.debug('inside getAllPrevSyncRecordsByVaultAndProfile')
|
||||
const keys = await db.prevSyncRecordsTbl.keys();
|
||||
// console.debug(`inside getAllPrevSyncRecordsByVaultAndProfile, keys=${keys}`)
|
||||
const res: Entity[] = [];
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(`${vaultRandomID}\t${profileID}\t`)) {
|
||||
const val: Entity | null = await db.prevSyncRecordsTbl.getItem(key);
|
||||
if (val !== null) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const upsertPrevSyncRecordByVaultAndProfile = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
profileID: string,
|
||||
prevSync: Entity
|
||||
) => {
|
||||
await db.prevSyncRecordsTbl.setItem(
|
||||
`${vaultRandomID}\t${profileID}\t${prevSync.key}`,
|
||||
prevSync
|
||||
);
|
||||
};
|
||||
|
||||
export const clearPrevSyncRecordByVaultAndProfile = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
profileID: string,
|
||||
key: string
|
||||
) => {
|
||||
await db.prevSyncRecordsTbl.removeItem(
|
||||
`${vaultRandomID}\t${profileID}\t${key}`
|
||||
);
|
||||
};
|
||||
|
||||
export const clearAllPrevSyncRecordByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
const records = [] as { ts: number; r: string }[];
|
||||
await db.loggerOutputTbl.iterate((value, key, iterationNumber) => {
|
||||
const keys = await db.prevSyncRecordsTbl.keys();
|
||||
for (const key of keys) {
|
||||
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||
const item = {
|
||||
ts: parseInt(key.split("\t")[1]),
|
||||
r: value as string,
|
||||
};
|
||||
records.push(item);
|
||||
await db.prevSyncRecordsTbl.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
// while reading the logs, we want it to be ascending
|
||||
records.sort((a, b) => a.ts - b.ts);
|
||||
|
||||
if (records === undefined) {
|
||||
return [] as string[];
|
||||
} else {
|
||||
return records.map((x) => x.r);
|
||||
}
|
||||
};
|
||||
|
||||
export const insertLoggerOutputByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
...msg: any[]
|
||||
) => {
|
||||
const ts = Date.now();
|
||||
const tsFmt = unixTimeToStr(ts);
|
||||
const key = `${vaultRandomID}\t${ts}`;
|
||||
|
||||
try {
|
||||
const val = [`[${tsFmt}]`, ...msg.map((x) => toText(x))].join(" ");
|
||||
db.loggerOutputTbl.setItem(key, val);
|
||||
} catch (err) {
|
||||
// give up, and let it pass
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllLoggerOutputRecords = async (db: InternalDBs) => {
|
||||
await db.loggerOutputTbl.clear();
|
||||
console.debug(`successfully clearAllLoggerOutputRecords`);
|
||||
};
|
||||
|
||||
/**
|
||||
* We remove records that are older than 7 days or 10000 records.
|
||||
* It's a heavy operation, so we shall not place it in the start up.
|
||||
* @param db
|
||||
*/
|
||||
export const clearExpiredLoggerOutputRecords = async (db: InternalDBs) => {
|
||||
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 7; // 7 days
|
||||
const COUNT_TO_MANY = 10000;
|
||||
|
||||
const currTs = Date.now();
|
||||
const expiredTs = currTs - MILLISECONDS_OLD;
|
||||
|
||||
let records = (await db.loggerOutputTbl.keys()).map((key) => {
|
||||
const ts = parseInt(key.split("\t")[1]);
|
||||
const expired = ts <= expiredTs;
|
||||
return {
|
||||
ts: ts,
|
||||
key: key,
|
||||
expired: expired,
|
||||
export const upsertLastSuccessSyncTimeByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
millis: number
|
||||
) => {
|
||||
await db.simpleKVForMiscTbl.setItem(
|
||||
`${vaultRandomID}-lastSuccessSyncMillis`,
|
||||
millis
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const keysToRemove = new Set(
|
||||
records.filter((x) => x.expired).map((x) => x.key)
|
||||
export const getLastSuccessSyncTimeByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string
|
||||
) => {
|
||||
return (await db.simpleKVForMiscTbl.getItem(
|
||||
`${vaultRandomID}-lastSuccessSyncMillis`
|
||||
)) as number;
|
||||
};
|
||||
|
||||
export const upsertPluginVersionByVault = async (
|
||||
db: InternalDBs,
|
||||
vaultRandomID: string,
|
||||
newVersion: string
|
||||
) => {
|
||||
let oldVersion: string | null = await db.simpleKVForMiscTbl.getItem(
|
||||
`${vaultRandomID}-pluginversion`
|
||||
);
|
||||
if (oldVersion === null) {
|
||||
oldVersion = "0.0.0";
|
||||
}
|
||||
await db.simpleKVForMiscTbl.setItem(
|
||||
`${vaultRandomID}-pluginversion`,
|
||||
newVersion
|
||||
);
|
||||
|
||||
if (records.length - keysToRemove.size > COUNT_TO_MANY) {
|
||||
// we need to find out records beyond 10000 records
|
||||
records = records.filter((x) => !x.expired); // shrink the array
|
||||
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
||||
records.slice(COUNT_TO_MANY).forEach((element) => {
|
||||
keysToRemove.add(element.key);
|
||||
});
|
||||
}
|
||||
|
||||
const ps = [] as Promise<void>[];
|
||||
keysToRemove.forEach((element) => {
|
||||
ps.push(db.loggerOutputTbl.removeItem(element));
|
||||
});
|
||||
await Promise.all(ps);
|
||||
return {
|
||||
oldVersion: oldVersion,
|
||||
newVersion: newVersion,
|
||||
};
|
||||
};
|
||||
|
||||
+510
-264
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { base64url } from "rfc4648";
|
||||
import { reverseString } from "./misc";
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const DEFAULT_README_FOR_METADATAONREMOTE =
|
||||
"Do NOT edit or delete the file manually. This file is for the plugin remotely-save to store some necessary meta data on the remote services. Its content is slightly obfuscated.";
|
||||
@@ -26,8 +25,8 @@ export interface MetadataOnRemote {
|
||||
}
|
||||
|
||||
export const isEqualMetadataOnRemote = (
|
||||
a: MetadataOnRemote,
|
||||
b: MetadataOnRemote
|
||||
a: MetadataOnRemote | undefined,
|
||||
b: MetadataOnRemote | undefined
|
||||
) => {
|
||||
const m1 = a === undefined ? { deletions: [] } : a;
|
||||
const m2 = b === undefined ? { deletions: [] } : b;
|
||||
|
||||
+129
-14
@@ -5,8 +5,6 @@ import { base32, base64url } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
import emojiRegex from "emoji-regex";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
moment: (...data: any) => any;
|
||||
@@ -30,7 +28,7 @@ export const isHiddenPath = (
|
||||
}
|
||||
const k = path.posix.normalize(item); // TODO: only unix path now
|
||||
const k2 = k.split("/"); // TODO: only unix path now
|
||||
// log.info(k2)
|
||||
// console.info(k2)
|
||||
for (const singlePart of k2) {
|
||||
if (singlePart === "." || singlePart === ".." || singlePart === "") {
|
||||
continue;
|
||||
@@ -75,14 +73,14 @@ export const getFolderLevels = (x: string, addEndingSlash: boolean = false) => {
|
||||
};
|
||||
|
||||
export const mkdirpInVault = async (thePath: string, vault: Vault) => {
|
||||
// log.info(thePath);
|
||||
// console.info(thePath);
|
||||
const foldersToBuild = getFolderLevels(thePath);
|
||||
// log.info(foldersToBuild);
|
||||
// console.info(foldersToBuild);
|
||||
for (const folder of foldersToBuild) {
|
||||
const r = await vault.adapter.exists(folder);
|
||||
// log.info(r);
|
||||
// console.info(r);
|
||||
if (!r) {
|
||||
log.info(`mkdir ${folder}`);
|
||||
console.info(`mkdir ${folder}`);
|
||||
await vault.adapter.mkdir(folder);
|
||||
}
|
||||
}
|
||||
@@ -126,8 +124,12 @@ export const base64ToArrayBuffer = (b64text: string) => {
|
||||
* @returns
|
||||
*/
|
||||
export const hexStringToTypedArray = (hex: string) => {
|
||||
const f = hex.match(/[\da-f]{2}/gi);
|
||||
if (f === null) {
|
||||
throw Error(`input ${hex} is not hex, no way to transform`);
|
||||
}
|
||||
return new Uint8Array(
|
||||
hex.match(/[\da-f]{2}/gi).map(function (h) {
|
||||
f.map(function (h) {
|
||||
return parseInt(h, 16);
|
||||
})
|
||||
);
|
||||
@@ -236,7 +238,7 @@ export const setToString = (a: Set<string>, delimiter: string = ",") => {
|
||||
export const extractSvgSub = (x: string, subEl: string = "rect") => {
|
||||
const parser = new window.DOMParser();
|
||||
const dom = parser.parseFromString(x, "image/svg+xml");
|
||||
const svg = dom.querySelector("svg");
|
||||
const svg = dom.querySelector("svg")!;
|
||||
svg.setAttribute("viewbox", "0 0 10 10");
|
||||
return svg.innerHTML;
|
||||
};
|
||||
@@ -317,7 +319,7 @@ export const getTypeName = (obj: any) => {
|
||||
* @param x
|
||||
* @returns
|
||||
*/
|
||||
export const atWhichLevel = (x: string) => {
|
||||
export const atWhichLevel = (x: string | undefined) => {
|
||||
if (
|
||||
x === undefined ||
|
||||
x === "" ||
|
||||
@@ -339,12 +341,9 @@ export const checkHasSpecialCharForDir = (x: string) => {
|
||||
};
|
||||
|
||||
export const unixTimeToStr = (x: number | undefined | null) => {
|
||||
if (x === undefined) {
|
||||
if (x === undefined || x === null || Number.isNaN(x)) {
|
||||
return undefined;
|
||||
}
|
||||
if (x === null) {
|
||||
return null;
|
||||
}
|
||||
return window.moment(x).format() as string;
|
||||
};
|
||||
|
||||
@@ -408,3 +407,119 @@ export const toText = (x: any) => {
|
||||
return `${x}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* On Android the stat has bugs for folders. So we need a fixed version.
|
||||
* @param vault
|
||||
* @param path
|
||||
*/
|
||||
export const statFix = async (vault: Vault, path: string) => {
|
||||
const s = await vault.adapter.stat(path);
|
||||
if (s === undefined || s === null) {
|
||||
return s;
|
||||
}
|
||||
if (s.ctime === undefined || s.ctime === null || Number.isNaN(s.ctime)) {
|
||||
s.ctime = undefined as any; // force assignment
|
||||
}
|
||||
if (s.mtime === undefined || s.mtime === null || Number.isNaN(s.mtime)) {
|
||||
s.mtime = undefined as any; // force assignment
|
||||
}
|
||||
if (
|
||||
(s.size === undefined || s.size === null || Number.isNaN(s.size)) &&
|
||||
s.type === "folder"
|
||||
) {
|
||||
s.size = 0;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
export const isSpecialFolderNameToSkip = (
|
||||
x: string,
|
||||
more: string[] | undefined
|
||||
) => {
|
||||
let specialFolders = [
|
||||
".git",
|
||||
".github",
|
||||
".gitlab",
|
||||
".svn",
|
||||
"node_modules",
|
||||
".DS_Store",
|
||||
"__MACOSX ",
|
||||
"Icon\r", // https://superuser.com/questions/298785/icon-file-on-os-x-desktop
|
||||
"desktop.ini",
|
||||
"Desktop.ini",
|
||||
"thumbs.db",
|
||||
"Thumbs.db",
|
||||
].concat(more !== undefined ? more : []);
|
||||
for (const iterator of specialFolders) {
|
||||
if (
|
||||
x === iterator ||
|
||||
x === `${iterator}/` ||
|
||||
x.endsWith(`/${iterator}`) ||
|
||||
x.endsWith(`/${iterator}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param x versionX
|
||||
* @param y versionY
|
||||
* @returns 1(x>y), 0(x==y), -1(x<y)
|
||||
*/
|
||||
export const compareVersion = (x: string | null, y: string | null) => {
|
||||
if (x === undefined || x === null) {
|
||||
return -1;
|
||||
}
|
||||
if (y === undefined || y === null) {
|
||||
return 1;
|
||||
}
|
||||
if (x === y) {
|
||||
return 0;
|
||||
}
|
||||
const [x1, x2, x3] = x.split(".").map((k) => Number(k));
|
||||
const [y1, y2, y3] = y.split(".").map((k) => Number(k));
|
||||
if (
|
||||
x1 > y1 ||
|
||||
(x1 === y1 && x2 > y2) ||
|
||||
(x1 === y1 && x2 === y2 && x3 > y3)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/19929641/how-to-append-an-html-string-to-a-documentfragment
|
||||
* To introduce some advanced html fragments.
|
||||
* @param string
|
||||
* @returns
|
||||
*/
|
||||
export const stringToFragment = (string: string) => {
|
||||
const wrapper = document.createElement("template");
|
||||
wrapper.innerHTML = string;
|
||||
return wrapper.content;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://forum.obsidian.md/t/css-to-show-status-bar-on-mobile-devices/77185
|
||||
* @param op
|
||||
*/
|
||||
export const changeMobileStatusBar = (op: "enable" | "disable") => {
|
||||
if (op === "enable") {
|
||||
(
|
||||
document.querySelector(
|
||||
".is-mobile .app-container .status-bar"
|
||||
) as HTMLElement
|
||||
).style.setProperty("display", "flex");
|
||||
} else {
|
||||
(
|
||||
document.querySelector(
|
||||
".is-mobile .app-container .status-bar"
|
||||
) as HTMLElement
|
||||
).style.setProperty("display", "none");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// It's very dangerous for this file to depend on other files in the same project.
|
||||
// We should avoid this situation as much as possible.
|
||||
|
||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
import type { LogLevelNumbers, Logger, LogLevel, LogLevelDesc } from "loglevel";
|
||||
const log2 = origLog.getLogger("rs-default");
|
||||
|
||||
const originalFactory = log2.methodFactory;
|
||||
|
||||
export const applyLogWriterInplace = function (writer: (...msg: any[]) => any) {
|
||||
log2.methodFactory = function (
|
||||
methodName: string,
|
||||
logLevel: LogLevelNumbers,
|
||||
loggerName: string | symbol
|
||||
) {
|
||||
const rawMethod = originalFactory(methodName, logLevel, loggerName);
|
||||
|
||||
return function (...msg: any[]) {
|
||||
rawMethod.apply(undefined, msg);
|
||||
writer(...msg);
|
||||
};
|
||||
};
|
||||
|
||||
log2.setLevel(log2.getLevel());
|
||||
};
|
||||
|
||||
export const restoreLogWritterInplace = () => {
|
||||
log2.methodFactory = originalFactory;
|
||||
log2.setLevel(log2.getLevel());
|
||||
};
|
||||
|
||||
export const log = log2;
|
||||
+38
-35
@@ -1,30 +1,10 @@
|
||||
import { Vault, Stat, ListedFiles } from "obsidian";
|
||||
import type { Vault, Stat, ListedFiles } from "obsidian";
|
||||
import type { Entity, MixedEntity } from "./baseTypes";
|
||||
|
||||
import { Queue } from "@fyears/tsqueue";
|
||||
import chunk from "lodash/chunk";
|
||||
import flatten from "lodash/flatten";
|
||||
|
||||
export interface ObsConfigDirFileType {
|
||||
key: string;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
type: "folder" | "file";
|
||||
}
|
||||
|
||||
const isFolderToSkip = (x: string) => {
|
||||
let specialFolders = [".git", ".svn", "node_modules"];
|
||||
for (const iterator of specialFolders) {
|
||||
if (
|
||||
x === iterator ||
|
||||
x === `${iterator}/` ||
|
||||
x.endsWith(`/${iterator}`) ||
|
||||
x.endsWith(`/${iterator}/`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
import { statFix, isSpecialFolderNameToSkip } from "./misc";
|
||||
|
||||
const isPluginDirItself = (x: string, pluginId: string) => {
|
||||
return (
|
||||
@@ -62,31 +42,50 @@ export const listFilesInObsFolder = async (
|
||||
configDir: string,
|
||||
vault: Vault,
|
||||
pluginId: string
|
||||
) => {
|
||||
): Promise<Entity[]> => {
|
||||
const q = new Queue([configDir]);
|
||||
const CHUNK_SIZE = 10;
|
||||
const contents: ObsConfigDirFileType[] = [];
|
||||
const contents: Entity[] = [];
|
||||
while (q.length > 0) {
|
||||
const itemsToFetch = [];
|
||||
const itemsToFetch: string[] = [];
|
||||
while (q.length > 0) {
|
||||
itemsToFetch.push(q.pop());
|
||||
itemsToFetch.push(q.pop()!);
|
||||
}
|
||||
|
||||
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
|
||||
for (const singleChunk of itemsToFetchChunks) {
|
||||
const r = singleChunk.map(async (x) => {
|
||||
const statRes = await vault.adapter.stat(x);
|
||||
const statRes = await statFix(vault, x);
|
||||
|
||||
if (statRes === undefined || statRes === null) {
|
||||
throw Error("something goes wrong while listing hidden folder");
|
||||
}
|
||||
const isFolder = statRes.type === "folder";
|
||||
let children: ListedFiles = undefined;
|
||||
let children: ListedFiles | undefined = undefined;
|
||||
if (isFolder) {
|
||||
children = await vault.adapter.list(x);
|
||||
}
|
||||
|
||||
if (
|
||||
!isFolder &&
|
||||
(statRes.mtime === undefined ||
|
||||
statRes.mtime === null ||
|
||||
statRes.mtime === 0)
|
||||
) {
|
||||
throw Error(
|
||||
`File in Obsidian ${configDir} has last modified time 0: ${x}, don't know how to deal with it.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
itself: {
|
||||
key: isFolder ? `${x}/` : x,
|
||||
...statRes,
|
||||
} as ObsConfigDirFileType,
|
||||
key: isFolder ? `${x}/` : x, // local always unencrypted
|
||||
keyRaw: isFolder ? `${x}/` : x,
|
||||
mtimeCli: statRes.mtime,
|
||||
mtimeSvr: statRes.mtime,
|
||||
size: statRes.size, // local always unencrypted
|
||||
sizeRaw: statRes.size,
|
||||
},
|
||||
children: children,
|
||||
};
|
||||
});
|
||||
@@ -97,7 +96,9 @@ export const listFilesInObsFolder = async (
|
||||
const isInsideSelfPlugin = isPluginDirItself(iter.itself.key, pluginId);
|
||||
if (iter.children !== undefined) {
|
||||
for (const iter2 of iter.children.folders) {
|
||||
if (isFolderToSkip(iter2)) {
|
||||
if (
|
||||
isSpecialFolderNameToSkip(iter2, ["workspace", "workspace.json"])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
|
||||
@@ -107,7 +108,9 @@ export const listFilesInObsFolder = async (
|
||||
q.push(iter2);
|
||||
}
|
||||
for (const iter2 of iter.children.files) {
|
||||
if (isFolderToSkip(iter2)) {
|
||||
if (
|
||||
isSpecialFolderNameToSkip(iter2, ["workspace", "workspace.json"])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user