Compare commits

..
91 Commits
Author SHA1 Message Date
fyears d7a59b82be add funding url
Release A New Version / build (16.x) (push) Failing after 42s
2024-01-07 11:38:56 +08:00
fyears acbf0318b4 bump to 0.3.33
Release A New Version / build (16.x) (push) Failing after 43s
2024-01-07 11:34:49 +08:00
fyears f3f26f7c0b back to the party 2024-01-07 11:20:57 +08:00
fyears 9fba9241dc fix logginng error 2024-01-07 10:51:42 +08:00
fyears d4c6ec1ecf bump to 0.3.32
Release A New Version / build (16.x) (push) Failing after 38s
2024-01-07 01:13:43 +08:00
fyears a6146f98cd debug server 2024-01-07 01:09:47 +08:00
fyears 4d6e660f90 not allow empty in ignorepaths 2024-01-06 22:36:33 +08:00
fyears d0af937fd1 agree to upload deletions 2024-01-06 22:34:00 +08:00
fyears b9ddd97052 select where to delete 2024-01-06 22:17:58 +08:00
fyears 1466fdd3aa skip large files is good enough 2024-01-06 21:58:46 +08:00
fyears b070f51f5d s3 adds support for remote prefix 2024-01-06 21:53:52 +08:00
fyears 586e587d96 partial of s3 remoteprefix 2024-01-06 17:21:53 +08:00
fyears cbf656c7b7 reduce hints 2024-01-06 14:56:07 +08:00
fyears 2d9ba514fa steps with var is meaningless 2024-01-06 14:45:41 +08:00
fyears a13cc8d5c1 tidy up the codes of sync on save 2024-01-06 14:40:32 +08:00
fyears 30f7c0058c merge langs back to main repo 2024-01-06 13:49:49 +08:00
fyears c3f7b584c9 don't change compiler 2024-01-06 12:04:40 +08:00
fyears bd1f8455b0 ignore pnpm-lock.yaml 2024-01-06 12:02:37 +08:00
fyears 635db41d0d Merge branch 'master' of https://github.com/fyears/remotely-save 2024-01-06 12:01:41 +08:00
19d5da3e45 Issue 199 sync on save (#340)
* begin adding a sync on save option

* get the main sync on save working

* get settings sync on save working

---------

Co-authored-by: Sam Purkis <sam.purkis@zimmerbiomet.com>
Co-authored-by: fyears <1142836+fyears@users.noreply.github.com>
2024-01-06 12:00:55 +08:00
fyears 2991007435 merge lang of sync on save 2024-01-06 12:00:43 +08:00
23f1b7990c update lang files inline with issue-199-sync-on-save (#2)
Co-authored-by: Sam Purkis <sam.purkis@zimmerbiomet.com>
Co-authored-by: fyears <1142836+fyears@users.noreply.github.com>
2024-01-06 11:58:56 +08:00
fyears 0558d77074 update hints for .obsidian 2024-01-06 11:55:24 +08:00
fyears 4a5a398017 update hints for .obsidian 2024-01-06 11:55:00 +08:00
fyears 8d286a79a1 bump 0.3.31
Release A New Version / build (16.x) (push) Failing after 40s
2024-01-06 11:47:38 +08:00
fyears e69674e34e globally skip 2024-01-06 11:46:19 +08:00
fyears c3ed69af66 track deletions of .obsidian 2024-01-06 11:44:34 +08:00
fyears 832828e6f6 0.3.30
Release A New Version / build (16.x) (push) Failing after 38s
2024-01-06 00:04:58 +08:00
fyears b3c9ac8cbb refactor statusbar 2024-01-06 00:03:53 +08:00
fyears cdbb220a6b refactor status bar 2024-01-06 00:03:30 +08:00
fyears 8a631ee809 Merge branch 'Drumber-statusbar' 2024-01-05 23:08:43 +08:00
fyears b49e545759 merge statusbar 2024-01-05 23:07:15 +08:00
fyears a8f6e1d05d resolve 2024-01-05 22:59:35 +08:00
fyears 6588952227 webdav doc refactor 2024-01-05 22:44:19 +08:00
fyears 0f8b1951a6 refactor s3 docs 2024-01-05 22:38:53 +08:00
fyears 8b838656ce Merge branch 'master' of https://github.com/fyears/remotely-save 2024-01-05 22:26:47 +08:00
fyears 37d36fe5b3 Merge pull request #410 from williamgurzoni/s3-docs
Add guide to configure Policy and User for AWS S3 Bucket
2024-01-05 22:26:20 +08:00
fyears d1cfe0c978 list all the actions i could think of 2024-01-05 22:24:37 +08:00
fyears e3e8db04e2 Merge branch 'master' into s3-docs 2024-01-05 22:13:38 +08:00
fyears 084a04f4e9 wider 2024-01-05 22:11:34 +08:00
fyears d38ac34362 0.3.29
Release A New Version / build (16.x) (push) Failing after 41s
2024-01-04 00:26:37 +08:00
fyears 6a651b844f ignore paths by regexgit add . 2024-01-04 00:26:02 +08:00
fyears 9e4d3826fd add langs for ignorepath 2024-01-03 23:37:08 +08:00
ras0q d7e034f070 remove debug console.table 2024-01-03 23:36:12 +08:00
ras0q 79a0b9f44d add ignorePaths setting 2024-01-03 23:36:12 +08:00
fyears c75336a2a5 more desc of ignorepaths 2024-01-03 23:34:57 +08:00
fyears b5532dd834 Merge pull request #4 from ras0q/feat/ignorepaths
add settings_ignorepaths, settings_ignorepaths_desc
2024-01-03 23:25:32 +08:00
fyears 81c79ab848 minio custom port 2024-01-03 23:25:08 +08:00
fyears af71758ef7 add cloudflare r2 2024-01-03 00:08:51 +08:00
fyears ee8a3df0be change method of s3 check connect 2024-01-02 23:52:36 +08:00
fyears f55fad0e29 fix compability issue 2024-01-02 00:26:03 +08:00
fyears 15c163e73d alist 2024-01-02 00:08:09 +08:00
fyears d8f613bbb6 minio 2024-01-01 11:57:55 +08:00
fyears c8016b5f30 finally jianguoyun doc 2024-01-01 11:12:08 +08:00
fyears f66a7031b2 format 2024-01-01 11:06:04 +08:00
fyears bbc69715e8 add webdav links 2024-01-01 11:05:49 +08:00
fyears d6c39615d7 owncloud 2024-01-01 10:40:38 +08:00
fyears bbb909cda4 add link for null body 2024-01-01 01:32:22 +08:00
ras0q 0b961f4865 add settings_ignorepaths, settings_ignorepaths_desc 2023-12-29 21:45:08 +09:00
William Gurzoni 7b2343e879 Link s3 user and policy to main readme 2023-12-26 16:06:38 +13:00
William Gurzoni 6969c555da Create s3 user and policy guide 2023-12-26 16:06:12 +13:00
fyears 753937a932 Merge pull request #1 from rvben/fix-typos
Fixed several typo's/inconsistencies in en.json
2023-12-17 15:36:09 +08:00
Ruben J. Jongejan 81290728c2 Fixed several typo's/inconsistencies in en.json 2023-01-13 13:36:15 +01:00
Lars 229f74b5e8 Added option to settings to toggle status bar item 2022-08-13 11:55:19 +02:00
Lars a0c4a6ca6c Added option to settings to toggle status bar item 2022-08-13 11:55:09 +02:00
Lars 30d94986c6 Added last sync time to statusbar
Adds an item to the statusbar that displays the time of the last successful sync.
2022-08-12 18:05:45 +02:00
Lars 58c0bc5057 Added last sync time to statusbar 2022-08-12 18:02:40 +02:00
Lars 4f03024200 Fixed webpack build
Build failed due to missing polyfills needed by @azure/msal-node.
2022-08-12 18:02:06 +02:00
fyears 42eab5d544 normalize words 2022-05-03 22:58:39 +08:00
fyears 4726d51d60 split chooser and basic settings 2022-05-03 22:44:56 +08:00
fyears ad48de9227 langs for bypassing large files 2022-05-01 18:12:42 +08:00
fyears b227202f0e fix typo 2022-04-30 16:04:13 +08:00
fyears eda77d12e2 update sync 1 sec 2022-04-30 15:55:03 +08:00
fyears 82d0619761 add password hide support 2022-04-16 18:33:49 +08:00
fyears 9fdb379251 fix cors andr 2022-04-13 22:24:34 +08:00
fyears 470fe141ef add export format 2022-04-10 17:37:49 +08:00
fyears 6bfbaa3c1b add commands 2022-04-10 11:17:41 +08:00
fyears c46d302441 add more specific desc to s3 cors 2022-04-07 22:00:26 +08:00
fyears 2afc10b080 update texts for logtodb 2022-04-06 00:24:14 +08:00
fyears ea076d310b add tmpl 2022-04-05 23:44:56 +08:00
fyears 75a3b2a830 fix typo 2022-04-03 14:53:17 +08:00
fyears 9a4c2d3c1c add dbx clearlocal 2022-04-02 00:00:49 +08:00
fyears c729c117e8 add remoteBaseDir 2022-03-29 00:12:30 +08:00
fyears 230fee440e add vault id setting wording 2022-03-27 00:48:26 +08:00
fyears 5e740813d5 update text 2022-03-26 15:11:08 +08:00
fyears 475d524994 add more hints for webdav 2022-03-23 22:54:20 +08:00
fyears 28730585f8 more s3 urlstyle 2022-03-20 23:36:54 +08:00
fyears 4b69d60d89 fix wording 2022-03-20 20:52:07 +08:00
fyears d60fe622ed add zh_tw 2022-03-20 16:18:23 +08:00
fyears f2202029fc fix word 2022-03-20 16:09:16 +08:00
fyears 340ac09961 en and zh_cn 2022-03-20 15:33:08 +08:00
54 changed files with 2486 additions and 179 deletions
+1
View File
@@ -5,6 +5,7 @@
# npm
node_modules
package-lock.json
pnpm-lock.yaml
# build
main.js
-3
View File
@@ -1,3 +0,0 @@
[submodule "src/langs"]
path = src/langs
url = https://github.com/remotely-save/langs.git
+11 -11
View File
@@ -26,6 +26,7 @@ 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.**
@@ -60,10 +61,12 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### S3
- Tutorials / Examples:
- [Cloudflare R2](./docs/remote_services/s3_cloudflare_r2/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.
@@ -87,14 +90,11 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### 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)
- [坚果云 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**.
+25
View File
@@ -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}`);
});
@@ -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.
![](./s3_cloudflare_r2_create_bucket.png)
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.
![](./s3_cloudflare_r2_create_api_token.png)
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.)
![](./s3_cloudflare_r2_rs_settings.png)
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,83 @@
# 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": "ObsidianBucket",
"Effect": "Allow",
"Action": [
"s3:HeadBucket"
],
"Resource": "arn:aws:s3:::my-bucket"
},
{
"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.
+27
View File
@@ -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).
![](./minio_access_key.png)
3. Check or set the region.
![](./minio_region.png)
4. Create a bucket.
![](./minio_create_bucket.png)
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.
![](./minio_rs_settings.png)
6. Sync!
![](./minio_sync_success.png)
## 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.
![](./minio_custom_port.png)
@@ -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,22 @@
# AList
## 链接 Links
- English official website: <https://alist.nn.ci/> and <https://alist.nn.ci/guide/webdav.html>
- 中文官网:<https://alist.nn.ci/zh/> 和 <https://alist.nn.ci/zh/guide/webdav.html>
## 步骤 Steps
1. 安装和使用 AList。获取账号名和密码。在网页上登录。Install and run AList. Get the account and password. Login using the web page.
2. 新建挂载,检查挂载路径。如图所示是 `/alisttest davpath`。Add new storage. Pay attention to the mount path. The screenshot shows the mount path as `/alisttest davpath`.
![](./alist_mount_path.zh.png)
![](./alist_mount_path.en.png)
3. 从而构建 webdav 网址如下,**http(s)://域名** + **端口** + **`/dav`** + **挂载路径**,其中挂载路径中假如有空格,换成 `%20`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. 在 remotely-save 设置,输入**带域名端口`/dav`和挂载路径的网址**、账号、密码。In remotely-save setting page, select webdav type, then input the **full address with mount path**/account/password.
![](./alist_rs_settings.en.png)
5. 在 remotely-save 设置,检查连接。In remotely-save setting page, click "Check Connectivity".
6. 同步文件!Sync!
@@ -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,22 @@
# 坚果云 JianGuoYun/NutStore
## 链接 Link
<https://www.jianguoyun.com/>
## 注意!!!Attentions!!!
坚果云有限制 api 数量等设定。本插件会产生若干查询,如果文件较多很容易触发 api 上限,从而工作不正常。这不是插件 bug,也没有办法解决。
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. **知悉坚果云有 api 限制,本插件可能因此工作不正常。Be aware that JianGuoYun/NutStore has api limits, and the plugin may not work properly because of this.**
2. 注册账号,登录。Register an account.
3. 去“个人信息”->“安全”,“添加应用”,从而获取了 webDAV 账号(应该是 email)和 WebDAV 密码(一串特殊的字符,不等于网站密码)。Go to "settings"->"Security", click "Add Application", then obtain the WebDAV account (email), and WebDAV password (a string different from web site password).
![](./webdav_jianguoyun.cn.png)
4. 在 remotely-save 设置,输入网址、账号、密码、**“发送到服务器的 Depth Header”设置为“只支持 depth='1'”**。Input the WebDAV address, account, password, **Depth Header Sent To Servers="only supports depth='1'"** in remotely-save settings.
![](./webdav_jianguoyun_rs_settting.cn.png)
5. 在 remotely-save 设置,检查连接。In remotely-save setting page, click "Check Connectivity".
6. 同步文件!Sync!
@@ -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.
![](./owncloud_address.png)
4. Input the WebDAV address, account, password, **Depth Header Sent To Servers="only supports depth='1'"** in remotely-save settings.
![](./owncloud_rs_settings.png)
5. In remotely-save setting page, click "Check Connectivity".
6. Sync!
![](./owncloud_files.png)
@@ -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
+3 -2
View File
@@ -1,10 +1,11 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.28",
"version": "0.3.34",
"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"
}
+3 -2
View File
@@ -1,10 +1,11 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.25",
"version": "0.3.34",
"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"
}
+14 -1
View File
@@ -24,6 +24,7 @@ export interface S3Config {
bypassCorsLocally?: boolean;
partsConcurrency?: number;
forcePathStyle?: boolean;
remotePrefix?: string;
}
export interface DropboxConfig {
@@ -79,6 +80,7 @@ export interface RemotelySavePluginSettings {
currLogLevel?: string;
autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number;
syncOnSaveAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean;
concurrency?: number;
syncConfigDir?: boolean;
@@ -86,6 +88,9 @@ export interface RemotelySavePluginSettings {
lang?: LangTypeAndAuto;
logToDB?: boolean;
skipSizeLargerThan?: number;
ignorePaths?: string[];
enableStatusBarInfo?: boolean;
deleteToWhere?: "system" | "obsidian";
/**
* @deprecated
@@ -180,4 +185,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";
Submodule src/langs deleted from 42eab5d544
+202
View File
@@ -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.
+14
View File
@@ -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.
+301
View File
@@ -0,0 +1,301 @@
{
"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 running in dry mode, not actual file changes would happen.",
"syncrun_step1": "1/8 Remotely Save Sync 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 Trying to fetch extra meta data from remote.",
"syncrun_step5": "5/8 Starting to fetch local meta data.",
"syncrun_step6": "6/8 Starting to generate sync plan.",
"syncrun_step7": "7/8 Remotely Save Sync data exchanging!",
"syncrun_step7skip": "7/8 Remotely Save real sync is skipped in dry run mode.",
"syncrun_step8": "8/8 Remotely Save finish!",
"syncrun_shortstep0": "0/2 Remotely Save running in dry mode, not actual file changes would happen.",
"syncrun_shortstep1": "1/2 Remotely Save Sync Start Running ({{serviceType}})",
"syncrun_shortstep2skip": "2/2 Remotely Save real sync is skipped in dry run mode.",
"syncrun_shortstep2": "2/2 Remotely Save finish!",
"syncrun_abort": "{{manifestID}}-{{theDate}}: abort sync, triggerSource={{triggerSource}}, error while {{syncStatus}}",
"protocol_saveqr": "New not-oauth2 settings for {{manifestName}} saved. Reopen the plugin Settings to the effect.",
"protocol_callbacknotsupported": "Your uri call 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 you rejected the auth?",
"protocol_dropbox_connect_unknown": "Do not know how to deal with the callback: {{params}}",
"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_exportsyncplans_table": "export sync plans in table format",
"command_exportlogsindb": "export logs saved in db",
"statusbar_time_years": "{{time}} years",
"statusbar_time_months": "{{time}} months",
"statusbar_time_weeks": "{{time}} weeks",
"statusbar_time_days": "{{time}} days",
"statusbar_time_hours": "{{time}} hours",
"statusbar_time_minutes": "{{time}} minutes",
"statusbar_time_lessminute": "less than a minute",
"statusbar_lastsync": "Synced {{time}} ago",
"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!",
"modal_logtohttpserver_title": "Log To HTTP(S) Server Is DANGEROUS!",
"modal_logtohttpserver_desc": "All your sensitive logging information will be posted to the HTTP(S) server without any authentications!!!!!\nPlease make sure you trust the HTTP(S) server, and it's better to setup a HTTPS one instead of HTTP one.\nIt's for debugging purposes only, especially on mobile.",
"modal_logtohttpserver_secondconfirm": "I know it's dangerous, and insist, and am willing to bear all possible losses.",
"modal_logtohttpserver_notice": "OK.",
"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_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'. The plugin needs to know this info. If you are not sure what's this, choose \"auto\".",
"settings_webdav_depth_auto": "auto detect",
"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_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 Console Log Level",
"settings_debuglevel_desc": "By default the log level is \"info\". You can change to \"debug\" to get verbose information in console.",
"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_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. You can choose to export to json or table format inside Markdown, which are equivalent.",
"settings_syncplans_button_json": "Export (json)",
"settings_syncplans_button_table": "Export (table)",
"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_logtodb": "Save Console Logs Into DB",
"settings_logtodb_desc": "Enable this to save all console logs into DB, so that we can export and read them later. Especially useful for mobile debugging. Has performance cost and may impact battery. The history is saved for at most 10000 records and 7 days old.",
"settings_logtodbexport": "Export Console Logs From DB",
"settings_logtodbexport_desc": "Export the saved console logs of this vault from DB into a new file in the folder {{&debugFolder}} .",
"settings_logtodbexport_button": "Export",
"settings_logtodbexport_notice": "Console logs of this vault from DB are exported!",
"settings_logtodbclear": "Delete All Console Logs In DB",
"settings_logtodbclear_desc": "Delete all the console logs saved inside DB. This setting applies to all saved logs for all vaults in this device.",
"settings_logtodbclear_button": "Delete",
"settings_logtodbclear_notice": "All console logs saved in DB are deleted!",
"settings_logtohttpserver": "Log To HTTP Server Temporarily",
"settings_logtohttpserver_desc": "It's very dangerous and please use the function with greate cautions!!!!! It will temporarily allow sending console loggins to HTTP server.",
"settings_logtohttpserver_reset_notice": "Already removed the setting of logging to HTTP server.",
"settings_delsyncmap": "Delete Sync Mappings History In DB",
"settings_delsyncmap_desc": "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync. Click the button to delete sync mappings history in DB.",
"settings_delsyncmap_button": "Delete Sync Mappings",
"settings_delsyncmap_notice": "Sync mappings 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.",
"syncalgov2_title": "Remotely Save has a better sync algorithm",
"syncalgov2_texts": "Welcome to use Remotely Save!\nFrom version 0.3.0, a new algorithm has been developed, but it needs uploading extra meta data files _remotely-save-metadata-on-remote.{json,bin} to YOUR configured cloud destinations, besides your notes.\nSo that, for example, the second device can know that what files/folders have been deleted on the first device by reading those files.\nIf you agree, plase click the button \"Agree\", and enjoy the plugin! AND PLEASE REMEMBER TO BACKUP YOUR VAULT FIRSTLY!\nIf you do not agree, you should stop using the current and later versions of Remotely Save. You could consider manually install the old version 0.2.14 which uses old algorithm and does not upload any extra meta data files. By clicking the \"Do Not Agree\" button, the plugin will unload itself, and you need to manually disable it in Obsidian settings.",
"syncalgov2_button_agree": "Agree",
"syncalgov2_button_disagree": "Do Not Agree",
"official_notice_2024_first_party": "Plugin Remotely-Save is back to the party and get a HUGE update!🎉🎉🎉 Try it yourself or see the release note on https://github.com/remotely-save/remotely-save/releases."
}
+9
View File
@@ -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,
};
+301
View File
@@ -0,0 +1,301 @@
{
"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}}",
"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_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_exportsyncplans_table": "导出同步计划为表格格式(export sync plans in table 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_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": "所有的文件大小冲突信息,已被复制到剪贴板!",
"modal_logtohttpserver_title": "转发终端日志到 HTTP 服务器,此操作很危险!",
"modal_logtohttpserver_desc": "所有您的带敏感信息的终端日志,都会被转发到 HTTP(S) 服务器,没有任何鉴权!!!!!\n请确保您信任对应的服务器,最好设置为 HTTPS 而不是 HTTP。\n仅仅用于 debug 用途,例如手机上的 debug。",
"modal_logtohttpserver_secondconfirm": "我知道很危险,坚持要设置,愿意承担所有可能损失。",
"modal_logtohttpserver_notice": "已设置。",
"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_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”请求。本插件需要得知此信息。如果您不清楚如何设置,可以选择“自动检测”",
"settings_webdav_depth_auto": "自动检测",
"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_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": "修改终端输出的 level",
"settings_debuglevel_desc": "默认值为 \"info\"。您可以改为 \"debug\" 从而在终端里获取更多信息。",
"settings_outputsettingsconsole": "读取硬盘上的设置文件输出到终端",
"settings_outputsettingsconsole_desc": "硬盘上的设置文件是编码过的,点击这里从而解码并输出到终端。",
"settings_outputsettingsconsole_button": "输出",
"settings_outputsettingsconsole_notice": "已输出到终端",
"settings_syncplans": "导出同步计划",
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。并且,您可以选择导出为内嵌在 Markdown 里的 json 格式或表格格式,它们是等价的。",
"settings_syncplans_button_json": "导出(json",
"settings_syncplans_button_table": "导出(表格)",
"settings_syncplans_notice": "同步计划已导出",
"settings_delsyncplans": "删除数据库里的同步计划历史",
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
"settings_delsyncplans_button": "删除同步计划历史",
"settings_delsyncplans_notice": "(数据库里的)同步计划已被删除。",
"settings_logtodb": "保存终端日志到数据库",
"settings_logtodb_desc": "如果开启此设置,所有终端日志都会保存到数据,从而我们可以在之后导出和阅读它们。对于移动设备的调试较为有用。有性能成本,可能会影响电池用量。历史会保存最多 10000 条和 7 天时间。",
"settings_logtodbexport": "从数据库导出终端日志",
"settings_logtodbexport_desc": "从数据库导出本资料库的已保存的终端日志,输出到文件夹 {{&debugFolder}} 下的一个新文件内。",
"settings_logtodbexport_button": "导出",
"settings_logtodbexport_notice": "数据库内的本资料库的终端日志已导出!",
"settings_logtodbclear": "删除数据库的所有终端日志",
"settings_logtodbclear_desc": "删除数据库内的所有已保存的终端日志。本设置会影响本设备下所有资料库的所有已保存日志。",
"settings_logtodbclear_button": "删除",
"settings_logtodbclear_notice": "数据库内的所有终端日志已删除!",
"settings_logtohttpserver": "临时设定终端日志实时转发到 HTTP 服务器。",
"settings_logtohttpserver_desc": "非常危险,谨慎行动!!!!!临时设定终端日志实时转发到 HTTP 服务器。",
"settings_logtohttpserver_reset_notice": "移除了终端日志转发到 HTTP 服务器的设定。",
"settings_delsyncmap": "删除数据库里的同步映射历史",
"settings_delsyncmap_desc": "同步映射历史存储了本地真正的最后修改时间和远程文件时间的映射。删除之可能会导致下一次同步时发生不必要的数据交换。点击按钮删除数据库里的同步映射历史。",
"settings_delsyncmap_button": "删除同步映射历史",
"settings_delsyncmap_notice": "(本地数据库里的)同步映射历史已被删除。",
"settings_outputbasepathvaultid": "输出资料库对应的位置和随机分配的 ID",
"settings_outputbasepathvaultid_desc": "用于调试。",
"settings_outputbasepathvaultid_button": "输出",
"settings_resetcache": "重设本地缓存和数据库",
"settings_resetcache_desc": "(出于调试原因)重设本地缓存和数据库。您需要在重设之后重新载入此插件。本重设不会删除 s3,密码……等设定。",
"settings_resetcache_button": "重设",
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
"syncalgov2_title": "Remotely Save 的同步算法得到优化",
"syncalgov2_texts": "欢迎使用 Remotely Save!\n从版本 0.3.0 开始,它带来了新的同步算法,但是,除了您的笔记之外,它还需要上传额外的带有元信息的文件 _remotely-save-metadata-on-remote.{json,bin} 到您的云服务目的地上。\n从而,比如说,通过读取这些信息,另一台设备可以知道什么文件或文件夹在第一台设备上被删除了。\n如果您同意此策略,请点击按钮 \"同意\",然后开始享用此插件!且特别要注意:使用插件之前,请首先备份好您的库(Vault)!\n如果您不同意此策略,您应该停止使用此版本和之后版本的 Remotely Save。您可以考虑手动安装旧版 0.2.14,它使用旧的同步算法,并不上传额外元信息文件。点击 \"不同意\" 之后,插件会自动停止运行(unload),然后您需要 Obsidian 设置里手动停用(disable)此插件。",
"syncalgov2_button_agree": "同意",
"syncalgov2_button_disagree": "不同意",
"official_notice_2024_first_party": "插件 Remotely-Save 回来了,更新了一大堆功能!🎉🎉🎉请自行使用,或参阅更新文档: https://github.com/remotely-save/remotely-save/releases 。"
}
+300
View File
@@ -0,0 +1,300 @@
{
"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}}",
"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_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_exportsyncplans_table": "匯出同步計劃為表格格式(export sync plans in table 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_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": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
"modal_logtohttpserver_title": "轉發終端日誌到 HTTP 伺服器,此操作很危險!",
"modal_logtohttpserver_desc": "所有您的帶敏感資訊的終端日誌,都會被轉發到 HTTP(S) 伺服器,沒有任何鑑權!!!!!\n請確保您信任對應的伺服器,最好設定為 HTTPS 而不是 HTTP。\n僅僅用於 debug 用途,例如手機上的 debug。",
"modal_logtohttpserver_secondconfirm": "我知道很危險,堅持要設定,願意承擔所有可能損失。",
"modal_logtohttpserver_notice": "已設定。",
"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_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”請求。本外掛需要得知此資訊。如果您不清楚如何設定,可以選擇“自動檢測”",
"settings_webdav_depth_auto": "自動檢測",
"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_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": "修改終端輸出的 level",
"settings_debuglevel_desc": "預設值為 \"info\"。您可以改為 \"debug\" 從而在終端裡獲取更多資訊。",
"settings_outputsettingsconsole": "讀取硬碟上的設定檔案輸出到終端",
"settings_outputsettingsconsole_desc": "硬碟上的設定檔案是編碼過的,點選這裡從而解碼並輸出到終端。",
"settings_outputsettingsconsole_button": "輸出",
"settings_outputsettingsconsole_notice": "已輸出到終端",
"settings_syncplans": "匯出同步計劃",
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。並且,您可以選擇匯出為內嵌在 Markdown 裡的 json 格式或表格格式,它們是等價的。",
"settings_syncplans_button_json": "匯出(json",
"settings_syncplans_button_table": "匯出(表格)",
"settings_syncplans_notice": "同步計劃已匯出",
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
"settings_delsyncplans_button": "刪除同步計劃歷史",
"settings_delsyncplans_notice": "(資料庫裡的)同步計劃已被刪除。",
"settings_logtodb": "儲存終端日誌到資料庫",
"settings_logtodb_desc": "如果開啟此設定,所有終端日誌都會儲存到資料,從而我們可以在之後匯出和閱讀它們。對於移動裝置的除錯較為有用。有效能成本,可能會影響電池用量。歷史會儲存最多 10000 條和 7 天時間。",
"settings_logtodbexport": "從資料庫匯出終端日誌",
"settings_logtodbexport_desc": "從資料庫匯出本資料庫的已儲存的終端日誌,輸出到資料夾 {{&debugFolder}} 下的一個新檔案內。",
"settings_logtodbexport_button": "匯出",
"settings_logtodbexport_notice": "資料庫內的本資料庫的終端日誌已匯出!",
"settings_logtodbclear": "刪除資料庫的所有終端日誌",
"settings_logtodbclear_desc": "刪除資料庫內的所有已儲存的終端日誌。本設定會影響本裝置下所有資料庫的所有已儲存日誌。",
"settings_logtodbclear_button": "刪除",
"settings_logtodbclear_notice": "資料庫內的所有終端日誌已刪除!",
"settings_logtohttpserver": "臨時設定終端日誌實時轉發到 HTTP 伺服器。",
"settings_logtohttpserver_desc": "非常危險,謹慎行動!!!!!臨時設定終端日誌實時轉發到 HTTP 伺服器。",
"settings_logtohttpserver_reset_notice": "移除了終端日誌轉發到 HTTP 伺服器的設定。",
"settings_delsyncmap": "刪除資料庫裡的同步對映歷史",
"settings_delsyncmap_desc": "同步對映歷史儲存了本地真正的最後修改時間和遠端檔案時間的對映。刪除之可能會導致下一次同步時發生不必要的資料交換。點選按鈕刪除資料庫裡的同步對映歷史。",
"settings_delsyncmap_button": "刪除同步對映歷史",
"settings_delsyncmap_notice": "(本地資料庫裡的)同步對映歷史已被刪除。",
"settings_outputbasepathvaultid": "輸出資料庫對應的位置和隨機分配的 ID",
"settings_outputbasepathvaultid_desc": "用於除錯。",
"settings_outputbasepathvaultid_button": "輸出",
"settings_resetcache": "重設本地快取和資料庫",
"settings_resetcache_desc": "(出於除錯原因)重設本地快取和資料庫。您需要在重設之後重新載入此外掛。本重設不會刪除 s3,密碼……等設定。",
"settings_resetcache_button": "重設",
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
"syncalgov2_title": "Remotely Save 的同步演算法得到最佳化",
"syncalgov2_texts": "歡迎使用 Remotely Save!\n從版本 0.3.0 開始,它帶來了新的同步演算法,但是,除了您的筆記之外,它還需要上傳額外的帶有元資訊的檔案 _remotely-save-metadata-on-remote.{json,bin} 到您的雲服務目的地上。\n從而,比如說,透過讀取這些資訊,另一臺裝置可以知道什麼檔案或資料夾在第一臺裝置上被刪除了。\n如果您同意此策略,請點選按鈕 \"同意\",然後開始享用此外掛!且特別要注意:使用外掛之前,請首先備份好您的儲存庫(Vault)!\n如果您不同意此策略,您應該停止使用此版本和之後版本的 Remotely Save。您可以考慮手動安裝舊版 0.2.14,它使用舊的同步演算法,並不上傳額外元資訊檔案。點選 \"不同意\" 之後,外掛會自動停止執行(unload),然後您需要 Obsidian 設定裡手動停用(disable)此外掛。",
"syncalgov2_button_agree": "同意",
"syncalgov2_button_disagree": "不同意",
"official_notice_2024_first_party": "外掛 Remotely-Save 回來了,更新了一大堆功能!🎉🎉🎉請自行使用,或參閱更新文件: https://github.com/remotely-save/remotely-save/releases 。"
}
+100 -2
View File
@@ -18,6 +18,7 @@ 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 interface FileFolderHistoryRecord {
key: string;
@@ -58,6 +59,7 @@ export interface InternalDBs {
syncPlansTbl: LocalForage;
vaultRandomIDMappingTbl: LocalForage;
loggerOutputTbl: LocalForage;
simpleKVForMiscTbl: LocalForage;
}
/**
@@ -216,6 +218,10 @@ 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,
}),
} as InternalDBs;
// try to get vaultRandomID firstly
@@ -324,7 +330,7 @@ export const clearDeleteRenameHistoryOfKeyAndVault = async (
export const insertDeleteRecordByVault = async (
db: InternalDBs,
fileOrFolder: TAbstractFile,
fileOrFolder: TAbstractFile | string,
vaultRandomID: string
) => {
// log.info(fileOrFolder);
@@ -341,6 +347,7 @@ export const insertDeleteRecordByVault = async (
renameTo: "",
vaultRandomID: vaultRandomID,
};
await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
} else if (fileOrFolder instanceof TFolder) {
// key should endswith "/"
const key = fileOrFolder.path.endsWith("/")
@@ -359,8 +366,57 @@ export const insertDeleteRecordByVault = async (
renameTo: "",
vaultRandomID: vaultRandomID,
};
}
await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
} else if (typeof fileOrFolder === "string") {
// always the deletions in .obsidian folder
// so annoying that the path doesn't exists
// and we have to guess whether the path is folder or file
k = {
key: fileOrFolder,
ctime: 0,
mtime: 0,
size: 0,
actionWhen: Date.now(),
actionType: "delete",
keyType: "file",
renameTo: "",
vaultRandomID: vaultRandomID,
};
await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
for (const ext of [
"json",
"js",
"mjs",
"ts",
"md",
"txt",
"css",
"png",
"gif",
"jpg",
"jpeg",
"gitignore",
"gitkeep",
]) {
if (fileOrFolder.endsWith(`.${ext}`)) {
// stop here, no more need to insert the folder record later
return;
}
}
// also add a deletion record as folder if not ending with special exts
k = {
key: `${fileOrFolder}/`,
ctime: 0,
mtime: 0,
size: 0,
actionWhen: Date.now(),
actionType: "delete",
keyType: "folder",
renameTo: "",
vaultRandomID: vaultRandomID,
};
await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
}
};
/**
@@ -680,3 +736,45 @@ export const clearExpiredLoggerOutputRecords = async (db: InternalDBs) => {
});
await Promise.all(ps);
};
export const upsertLastSuccessSyncByVault = async (
db: InternalDBs,
vaultRandomID: string,
millis: number
) => {
await db.simpleKVForMiscTbl.setItem(
`${vaultRandomID}-lastSuccessSyncMillis`,
millis
);
};
export const getLastSuccessSyncByVault = 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
);
return {
oldVersion: oldVersion,
newVersion: newVersion,
};
};
+293 -58
View File
@@ -6,6 +6,10 @@ import {
addIcon,
setIcon,
FileSystemAdapter,
Platform,
TFile,
TFolder,
requestUrl,
} from "obsidian";
import cloneDeep from "lodash/cloneDeep";
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
@@ -19,6 +23,7 @@ import {
COMMAND_CALLBACK_ONEDRIVE,
COMMAND_CALLBACK_DROPBOX,
COMMAND_URI,
REMOTELY_SAVE_VERSION_2024PREPARE,
} from "./baseTypes";
import { importQrCodeUri } from "./importExport";
import {
@@ -31,6 +36,9 @@ import {
insertLoggerOutputByVault,
clearExpiredLoggerOutputRecords,
clearExpiredSyncPlanRecords,
upsertLastSuccessSyncByVault,
getLastSuccessSyncByVault,
upsertPluginVersionByVault,
} from "./localdb";
import { RemoteClient } from "./remote";
import {
@@ -66,6 +74,7 @@ import {
exportVaultSyncPlansToFiles,
} from "./debugMode";
import { SizesConflictModal } from "./syncSizesConflictNotice";
import { compareVersion } from "./misc";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG,
@@ -78,13 +87,17 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
// vaultRandomID: "", // deprecated
autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false,
syncOnSaveAfterMilliseconds: -1,
agreeToUploadExtraMetadata: true, // as of 20240106, it's safe to assume every new user agrees with this
concurrency: 5,
syncConfigDir: false,
syncUnderscoreItems: false,
lang: "auto",
logToDB: false,
skipSizeLargerThan: -1,
ignorePaths: [],
enableStatusBarInfo: true,
deleteToWhere: "system",
};
interface OAuth2Info {
@@ -125,13 +138,16 @@ export default class RemotelySavePlugin extends Plugin {
settings: RemotelySavePluginSettings;
db: InternalDBs;
syncStatus: SyncStatusType;
statusBarElement: HTMLSpanElement;
oauth2Info: OAuth2Info;
currLogLevel: string;
currSyncMsg?: string;
syncRibbon?: HTMLElement;
autoRunIntervalID?: number;
syncOnSaveIntervalID?: number;
i18n: I18n;
vaultRandomID: string;
debugServerTemp?: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
const t = (x: TransItemType, vars?: any) => {
@@ -182,30 +198,36 @@ export default class RemotelySavePlugin extends Plugin {
);
}
const MAX_STEPS = 8;
if (triggerSource === "dry") {
if (this.settings.currLogLevel === "info") {
getNotice(t("syncrun_shortstep0"));
} else {
getNotice(t("syncrun_step0"));
}
}
//log.info(`huh ${this.settings.password}`)
if (this.settings.currLogLevel === "info") {
getNotice(
t("syncrun_step0", {
maxSteps: `${MAX_STEPS}`,
t("syncrun_shortstep1", {
serviceType: this.settings.serviceType,
})
);
} else {
getNotice(
t("syncrun_step1", {
serviceType: this.settings.serviceType,
})
);
}
//log.info(`huh ${this.settings.password}`)
getNotice(
t("syncrun_step1", {
maxSteps: `${MAX_STEPS}`,
serviceType: this.settings.serviceType,
})
);
this.syncStatus = "preparing";
getNotice(
t("syncrun_step2", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step2"));
}
this.syncStatus = "getting_remote_files_list";
const self = this;
const client = new RemoteClient(
@@ -217,14 +239,14 @@ export default class RemotelySavePlugin extends Plugin {
this.app.vault.getName(),
() => self.saveSettings()
);
const remoteRsp = await client.listFromRemote();
const remoteRsp = await client.listAllFromRemote();
// log.debug(remoteRsp);
getNotice(
t("syncrun_step3", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step3"));
}
this.syncStatus = "checking_password";
const passwordCheckResult = await isPasswordOk(
remoteRsp.Contents,
@@ -235,11 +257,11 @@ export default class RemotelySavePlugin extends Plugin {
throw Error(passwordCheckResult.reason);
}
getNotice(
t("syncrun_step4", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step4"));
}
this.syncStatus = "getting_remote_extra_meta";
const { remoteStates, metadataFile } = await parseRemoteItems(
remoteRsp.Contents,
@@ -255,11 +277,11 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.password
);
getNotice(
t("syncrun_step5", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step5"));
}
this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadFileHistoryTableByVault(
@@ -277,11 +299,11 @@ export default class RemotelySavePlugin extends Plugin {
// log.info(local);
// log.info(localHistory);
getNotice(
t("syncrun_step6", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step6"));
}
this.syncStatus = "generating_plan";
const { plan, sortedKeys, deletions, sizesGoWrong } = await getSyncPlan(
remoteStates,
@@ -296,6 +318,7 @@ export default class RemotelySavePlugin extends Plugin {
this.app.vault.configDir,
this.settings.syncUnderscoreItems,
this.settings.skipSizeLargerThan,
this.settings.ignorePaths,
this.settings.password
);
log.info(plan.mixedStates); // for debugging
@@ -305,12 +328,11 @@ export default class RemotelySavePlugin extends Plugin {
// The operations below begins to write or delete (!!!) something.
if (triggerSource !== "dry") {
getNotice(
t("syncrun_step7", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
// pass
} else {
getNotice(t("syncrun_step7"));
}
this.syncStatus = "syncing";
await doActualSync(
client,
@@ -340,26 +362,38 @@ export default class RemotelySavePlugin extends Plugin {
);
} else {
this.syncStatus = "syncing";
getNotice(
t("syncrun_step7skip", {
maxSteps: `${MAX_STEPS}`,
})
);
if (this.settings.currLogLevel === "info") {
getNotice(t("syncrun_shortstep2skip"));
} else {
getNotice(t("syncrun_step7skip"));
}
}
if (this.settings.currLogLevel === "info") {
getNotice(t("syncrun_shortstep2"));
} else {
getNotice(t("syncrun_step8"));
}
getNotice(
t("syncrun_step8", {
maxSteps: `${MAX_STEPS}`,
})
);
this.syncStatus = "finish";
this.syncStatus = "idle";
const lastSuccessSyncMillis = Date.now();
await upsertLastSuccessSyncByVault(
this.db,
this.vaultRandomID,
lastSuccessSyncMillis
);
if (this.syncRibbon !== undefined) {
setIcon(this.syncRibbon, iconNameSyncWait);
this.syncRibbon.setAttribute("aria-label", originLabel);
}
if (this.statusBarElement !== undefined) {
this.updateLastSuccessSyncMsg(lastSuccessSyncMillis);
}
log.info(
`${
this.manifest.id
@@ -449,7 +483,7 @@ export default class RemotelySavePlugin extends Plugin {
}
// must AFTER preparing DB
this.addOutputToDBIfSet();
this.redirectLoggingOuputBasedOnSetting();
this.enableAutoClearOutputToDBHistIfSet();
// must AFTER preparing DB
@@ -478,6 +512,31 @@ export default class RemotelySavePlugin extends Plugin {
})
);
function getMethods(obj: any) {
var result = [];
for (var id in obj) {
try {
if (typeof obj[id] == "function") {
result.push(id + ": " + obj[id].toString());
}
} catch (err) {
result.push(id + ": inaccessible");
}
}
return result.join("\n");
}
this.registerEvent(
this.app.vault.on("raw" as any, async (fileOrFolder) => {
// special track on .obsidian folder
const name = `${fileOrFolder}`;
if (name.startsWith(this.app.vault.configDir)) {
if (!(await this.app.vault.adapter.exists(name))) {
await insertDeleteRecordByVault(this.db, name, this.vaultRandomID);
}
}
})
);
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
if (parsed.status === "error") {
@@ -671,6 +730,25 @@ export default class RemotelySavePlugin extends Plugin {
async () => this.syncRun("manual")
);
// Create Status Bar Item (not supported on mobile)
if (!Platform.isMobileApp && this.settings.enableStatusBarInfo === true) {
const statusBarItem = this.addStatusBarItem();
this.statusBarElement = statusBarItem.createEl("span");
this.statusBarElement.setAttribute("aria-label-position", "top");
this.updateLastSuccessSyncMsg(
await getLastSuccessSyncByVault(this.db, this.vaultRandomID)
);
// update statusbar text every 30 seconds
this.registerInterval(
window.setInterval(async () => {
this.updateLastSuccessSyncMsg(
await getLastSuccessSyncByVault(this.db, this.vaultRandomID)
);
}, 1000 * 30)
);
}
this.addCommand({
id: "start-sync",
name: t("command_startsync"),
@@ -745,6 +823,17 @@ export default class RemotelySavePlugin extends Plugin {
} else {
this.enableAutoSyncIfSet();
this.enableInitSyncIfSet();
this.enableSyncOnSaveIfSet();
}
// compare versions and read new versions
const { oldVersion } = await upsertPluginVersionByVault(
this.db,
this.vaultRandomID,
this.manifest.version
);
if (compareVersion(REMOTELY_SAVE_VERSION_2024PREPARE, oldVersion) >= 0) {
new Notice(t("official_notice_2024_first_party"), 10 * 1000);
}
}
@@ -793,6 +882,21 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.s3.forcePathStyle === undefined) {
this.settings.s3.forcePathStyle = false;
}
if (this.settings.s3.remotePrefix === undefined) {
this.settings.s3.remotePrefix = "";
}
if (this.settings.ignorePaths === undefined) {
this.settings.ignorePaths = [];
}
if (this.settings.enableStatusBarInfo === undefined) {
this.settings.enableStatusBarInfo = true;
}
if (this.settings.syncOnSaveAfterMilliseconds === undefined) {
this.settings.syncOnSaveAfterMilliseconds = -1;
}
if (this.settings.deleteToWhere === undefined) {
this.settings.deleteToWhere = "system";
}
}
async checkIfPresetRulesFollowed() {
@@ -893,10 +997,15 @@ export default class RemotelySavePlugin extends Plugin {
}
async trash(x: string) {
if (this.settings.deleteToWhere === "obsidian") {
await this.app.vault.adapter.trashLocal(x);
} else {
// "system"
if (!(await this.app.vault.adapter.trashSystem(x))) {
await this.app.vault.adapter.trashLocal(x);
}
}
}
getVaultBasePath() {
if (this.app.vault.adapter instanceof FileSystemAdapter) {
@@ -950,6 +1059,56 @@ export default class RemotelySavePlugin extends Plugin {
}
}
enableSyncOnSaveIfSet() {
if (
this.settings.syncOnSaveAfterMilliseconds !== undefined &&
this.settings.syncOnSaveAfterMilliseconds !== null &&
this.settings.syncOnSaveAfterMilliseconds > 0
) {
let runScheduled = false;
this.app.workspace.onLayoutReady(() => {
const intervalID = window.setInterval(() => {
const currentFile = this.app.workspace.getActiveFile();
if (currentFile) {
// get the last modified time of the current file
// if it has been modified within the last syncOnSaveAfterMilliseconds
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
const lastModified = currentFile.stat.mtime;
const currentTime = Date.now();
// log.debug(
// `Checking if file was modified within last ${
// this.settings.syncOnSaveAfterMilliseconds / 1000
// } seconds, last modified: ${
// (currentTime - lastModified) / 1000
// } seconds ago`
// );
if (
currentTime - lastModified <
this.settings.syncOnSaveAfterMilliseconds
) {
if (!runScheduled) {
const scheduleTimeFromNow =
this.settings.syncOnSaveAfterMilliseconds -
(currentTime - lastModified);
log.info(
`schedule a run for ${scheduleTimeFromNow} milliseconds later`
);
runScheduled = true;
setTimeout(() => {
this.syncRun("auto_sync_on_save");
runScheduled = false;
}, scheduleTimeFromNow);
}
}
}
}, this.settings.syncOnSaveAfterMilliseconds);
this.syncOnSaveIntervalID = intervalID;
this.registerInterval(intervalID);
});
}
}
async saveAgreeToUseNewSyncAlgorithm() {
this.settings.agreeToUploadExtraMetadata = true;
await this.saveSettings();
@@ -965,6 +1124,62 @@ export default class RemotelySavePlugin extends Plugin {
this.currSyncMsg = msg;
}
updateLastSuccessSyncMsg(lastSuccessSyncMillis?: number) {
if (this.statusBarElement === undefined) return;
const t = (x: TransItemType, vars?: any) => {
return this.i18n.t(x, vars);
};
let lastSyncMsg = t("statusbar_lastsync_never");
let lastSyncLabelMsg = t("statusbar_lastsync_never_label");
if (lastSuccessSyncMillis !== undefined && lastSuccessSyncMillis > 0) {
const deltaTime = Date.now() - lastSuccessSyncMillis;
// create human readable time
const years = Math.floor(deltaTime / 31556952000);
const months = Math.floor(deltaTime / 2629746000);
const weeks = Math.floor(deltaTime / 604800000);
const days = Math.floor(deltaTime / 86400000);
const hours = Math.floor(deltaTime / 3600000);
const minutes = Math.floor(deltaTime / 60000);
let timeText = "";
if (years > 0) {
timeText = t("statusbar_time_years", { time: years });
} else if (months > 0) {
timeText = t("statusbar_time_months", { time: months });
} else if (weeks > 0) {
timeText = t("statusbar_time_weeks", { time: weeks });
} else if (days > 0) {
timeText = t("statusbar_time_days", { time: days });
} else if (hours > 0) {
timeText = t("statusbar_time_hours", { time: hours });
} else if (minutes > 0) {
timeText = t("statusbar_time_minutes", { time: minutes });
} else {
timeText = t("statusbar_time_lessminute");
}
let dateText = new Date(lastSuccessSyncMillis).toLocaleTimeString(
navigator.language,
{
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}
);
lastSyncMsg = t("statusbar_lastsync", { time: timeText });
lastSyncLabelMsg = t("statusbar_lastsync_label", { date: dateText });
}
this.statusBarElement.setText(lastSyncMsg);
this.statusBarElement.setAttribute("aria-label", lastSyncLabelMsg);
}
/**
* Because data.json contains sensitive information,
* We usually want to ignore it in the version control.
@@ -998,12 +1213,32 @@ export default class RemotelySavePlugin extends Plugin {
}
}
addOutputToDBIfSet() {
if (this.settings.logToDB) {
redirectLoggingOuputBasedOnSetting() {
applyLogWriterInplace((...msg: any[]) => {
if (this.settings.logToDB) {
insertLoggerOutputByVault(this.db, this.vaultRandomID, ...msg);
});
}
if (
this.debugServerTemp !== undefined &&
this.debugServerTemp.trim().startsWith("http")
) {
try {
requestUrl({
url: this.debugServerTemp,
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
send_time: Date.now(),
log_text: msg,
}),
});
} catch (e) {
// pass
}
}
});
}
enableAutoClearOutputToDBHistIfSet() {
+56
View File
@@ -427,3 +427,59 @@ export const statFix = async (vault: Vault, path: string) => {
}
return s;
};
export const isFolderToSkip = (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;
};
+3 -25
View File
@@ -2,7 +2,7 @@ import { Vault, Stat, ListedFiles } from "obsidian";
import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk";
import flatten from "lodash/flatten";
import { statFix } from "./misc";
import { statFix, isFolderToSkip } from "./misc";
export interface ObsConfigDirFileType {
key: string;
@@ -12,28 +12,6 @@ export interface ObsConfigDirFileType {
type: "folder" | "file";
}
const isFolderToSkip = (x: string) => {
let specialFolders = [
".git",
".github",
".gitlab",
".svn",
"node_modules",
".DS_Store",
];
for (const iterator of specialFolders) {
if (
x === iterator ||
x === `${iterator}/` ||
x.endsWith(`/${iterator}`) ||
x.endsWith(`/${iterator}/`)
) {
return true;
}
}
return false;
};
const isPluginDirItself = (x: string, pluginId: string) => {
return (
x === pluginId ||
@@ -106,7 +84,7 @@ 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 (isFolderToSkip(iter2, ["workspace", "workspace.json"])) {
continue;
}
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
@@ -116,7 +94,7 @@ export const listFilesInObsFolder = async (
q.push(iter2);
}
for (const iter2 of iter.children.files) {
if (isFolderToSkip(iter2)) {
if (isFolderToSkip(iter2, ["workspace", "workspace.json"])) {
continue;
}
if (isInsideSelfPlugin && !isLikelyPluginSubFiles(iter2)) {
+6 -7
View File
@@ -164,19 +164,18 @@ export class RemoteClient {
}
};
listFromRemote = async (prefix?: string) => {
listAllFromRemote = async () => {
if (this.serviceType === "s3") {
return await s3.listFromRemote(
return await s3.listAllFromRemote(
s3.getS3Client(this.s3Config),
this.s3Config,
prefix
this.s3Config
);
} else if (this.serviceType === "webdav") {
return await webdav.listFromRemote(this.webdavClient, prefix);
return await webdav.listAllFromRemote(this.webdavClient);
} else if (this.serviceType === "dropbox") {
return await dropbox.listFromRemote(this.dropboxClient, prefix);
return await dropbox.listAllFromRemote(this.dropboxClient);
} else if (this.serviceType === "onedrive") {
return await onedrive.listFromRemote(this.onedriveClient, prefix);
return await onedrive.listAllFromRemote(this.onedriveClient);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
+1 -7
View File
@@ -615,13 +615,7 @@ export const uploadToRemote = async (
}
};
export const listFromRemote = async (
client: WrappedDropboxClient,
prefix?: string
) => {
if (prefix !== undefined) {
throw Error("prefix not supported (yet)");
}
export const listAllFromRemote = async (client: WrappedDropboxClient) => {
await client.init();
let res = await client.dropbox.filesListFolder({
path: `/${client.remoteBaseDir}`,
+1 -8
View File
@@ -602,15 +602,8 @@ export const getOnedriveClient = (
* Use delta api to list all files and folders
* https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta?view=odsp-graph-online
* @param client
* @param prefix
*/
export const listFromRemote = async (
client: WrappedOnedriveClient,
prefix?: string
) => {
if (prefix !== undefined) {
throw Error("prefix not supported (yet)");
}
export const listAllFromRemote = async (client: WrappedOnedriveClient) => {
await client.init();
const NEXT_LINK_KEY = "@odata.nextLink";
+115 -19
View File
@@ -24,6 +24,7 @@ import { Buffer } from "buffer";
import * as mime from "mime-types";
import { Vault, requestUrl, RequestUrlParam } from "obsidian";
import { Readable } from "stream";
import * as path from "path";
import AggregateError from "aggregate-error";
import {
DEFAULT_CONTENT_TYPE,
@@ -163,13 +164,63 @@ export const DEFAULT_S3_CONFIG = {
bypassCorsLocally: true,
partsConcurrency: 20,
forcePathStyle: false,
remotePrefix: "",
};
export type S3ObjectType = _Object;
const fromS3ObjectToRemoteItem = (x: S3ObjectType) => {
export const simpleTransRemotePrefix = (x: string) => {
if (x === undefined) {
return "";
}
let y = path.posix.normalize(x.trim());
if (y === undefined || y === "" || y === "/" || y === ".") {
return "";
}
if (y.startsWith("/")) {
y = y.slice(1);
}
if (!y.endsWith("/")) {
y = `${y}/`;
}
return y;
};
const getRemoteWithPrefixPath = (
fileOrFolderPath: string,
remotePrefix: string
) => {
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = remotePrefix;
}
if (!fileOrFolderPath.startsWith("/")) {
key = `${remotePrefix}${fileOrFolderPath}`;
}
return key;
};
const getLocalNoPrefixPath = (
fileOrFolderPathWithRemotePrefix: string,
remotePrefix: string
) => {
if (
!(
fileOrFolderPathWithRemotePrefix === `${remotePrefix}` ||
fileOrFolderPathWithRemotePrefix.startsWith(`${remotePrefix}`)
)
) {
throw Error(
`"${fileOrFolderPathWithRemotePrefix}" doesn't starts with "${remotePrefix}"`
);
}
return fileOrFolderPathWithRemotePrefix.slice(`${remotePrefix}`.length);
};
const fromS3ObjectToRemoteItem = (x: S3ObjectType, remotePrefix: string) => {
return {
key: x.Key,
key: getLocalNoPrefixPath(x.Key, remotePrefix),
lastModified: x.LastModified.valueOf(),
size: x.Size,
remoteType: "s3",
@@ -178,11 +229,12 @@ const fromS3ObjectToRemoteItem = (x: S3ObjectType) => {
};
const fromS3HeadObjectToRemoteItem = (
key: string,
x: HeadObjectCommandOutput
fileOrFolderPathWithRemotePrefix: string,
x: HeadObjectCommandOutput,
remotePrefix: string
) => {
return {
key: key,
key: getLocalNoPrefixPath(fileOrFolderPathWithRemotePrefix, remotePrefix),
lastModified: x.LastModified.valueOf(),
size: x.ContentLength,
remoteType: "s3",
@@ -237,16 +289,26 @@ export const getS3Client = (s3Config: S3Config) => {
export const getRemoteMeta = async (
s3Client: S3Client,
s3Config: S3Config,
fileOrFolderPath: string
fileOrFolderPathWithRemotePrefix: string
) => {
if (
s3Config.remotePrefix !== "" &&
!fileOrFolderPathWithRemotePrefix.startsWith(s3Config.remotePrefix)
) {
throw Error(`s3 getRemoteMeta should only accept prefix-ed path`);
}
const res = await s3Client.send(
new HeadObjectCommand({
Bucket: s3Config.s3BucketName,
Key: fileOrFolderPath,
Key: fileOrFolderPathWithRemotePrefix,
})
);
return fromS3HeadObjectToRemoteItem(fileOrFolderPath, res);
return fromS3HeadObjectToRemoteItem(
fileOrFolderPathWithRemotePrefix,
res,
s3Config.remotePrefix
);
};
export const uploadToRemote = async (
@@ -264,6 +326,7 @@ export const uploadToRemote = async (
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getRemoteWithPrefixPath(uploadFile, s3Config.remotePrefix);
const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) {
@@ -331,16 +394,16 @@ export const uploadToRemote = async (
}
};
export const listFromRemote = async (
const listFromRemoteRaw = async (
s3Client: S3Client,
s3Config: S3Config,
prefix?: string
prefixOfRawKeys?: string
) => {
const confCmd = {
Bucket: s3Config.s3BucketName,
} as ListObjectsV2CommandInput;
if (prefix !== undefined) {
confCmd.Prefix = prefix;
if (prefixOfRawKeys !== undefined && prefixOfRawKeys !== "") {
confCmd.Prefix = prefixOfRawKeys;
}
const contents = [] as _Object[];
@@ -369,11 +432,22 @@ export const listFromRemote = async (
} while (isTruncated);
// ensemble fake rsp
// in the end, we need to transform the response list
// back to the local contents-alike list
return {
Contents: contents.map((x) => fromS3ObjectToRemoteItem(x)),
Contents: contents.map((x) =>
fromS3ObjectToRemoteItem(x, s3Config.remotePrefix)
),
};
};
export const listAllFromRemote = async (
s3Client: S3Client,
s3Config: S3Config
) => {
return await listFromRemoteRaw(s3Client, s3Config, s3Config.remotePrefix);
};
/**
* The Body of resp of aws GetObject has mix types
* and we want to get ArrayBuffer here.
@@ -403,12 +477,18 @@ const getObjectBodyToArrayBuffer = async (
const downloadFromRemoteRaw = async (
s3Client: S3Client,
s3Config: S3Config,
fileOrFolderPath: string
fileOrFolderPathWithRemotePrefix: string
) => {
if (
s3Config.remotePrefix !== "" &&
!fileOrFolderPathWithRemotePrefix.startsWith(s3Config.remotePrefix)
) {
throw Error(`downloadFromRemoteRaw should only accept prefix-ed path`);
}
const data = await s3Client.send(
new GetObjectCommand({
Bucket: s3Config.s3BucketName,
Key: fileOrFolderPath,
Key: fileOrFolderPathWithRemotePrefix,
})
);
const bodyContents = await getObjectBodyToArrayBuffer(data.Body);
@@ -443,6 +523,7 @@ export const downloadFromRemote = async (
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getRemoteWithPrefixPath(downloadFile, s3Config.remotePrefix);
const remoteContent = await downloadFromRemoteRaw(
s3Client,
s3Config,
@@ -482,6 +563,10 @@ export const deleteFromRemote = async (
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getRemoteWithPrefixPath(
remoteFileName,
s3Config.remotePrefix
);
await s3Client.send(
new DeleteObjectCommand({
Bucket: s3Config.s3BucketName,
@@ -490,7 +575,7 @@ export const deleteFromRemote = async (
);
if (fileOrFolderPath.endsWith("/") && password === "") {
const x = await listFromRemote(s3Client, s3Config, fileOrFolderPath);
const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName);
x.Contents.forEach(async (element) => {
await s3Client.send(
new DeleteObjectCommand({
@@ -509,6 +594,11 @@ export const deleteFromRemote = async (
/**
* Check the config of S3 by heading bucket
* https://stackoverflow.com/questions/50842835
*
* Updated on 20240102:
* Users are not always have permission of heading bucket,
* so we need to use listing objects instead...
*
* @param s3Client
* @param s3Config
* @returns
@@ -519,9 +609,15 @@ export const checkConnectivity = async (
callbackFunc?: any
) => {
try {
const results = await s3Client.send(
new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
);
// const results = await s3Client.send(
// new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
// );
// very simplified version of listing objects
const confCmd = {
Bucket: s3Config.s3BucketName,
} as ListObjectsV2CommandInput;
const results = await s3Client.send(new ListObjectsV2Command(confCmd));
if (
results === undefined ||
results.$metadata === undefined ||
+17 -13
View File
@@ -48,15 +48,24 @@ if (VALID_REQURL) {
if (contentType !== undefined) {
contentType = contentType.toLowerCase();
}
const rspHeaders = { ...r.headers };
for (let key in rspHeaders) {
if (rspHeaders.hasOwnProperty(key)) {
if (key === "content-disposition" || key === "Content-Disposition") {
rspHeaders[key] = encodeURIComponent(rspHeaders[key]);
}
}
}
// console.log(`requesting url=${options.url}`);
// console.log(`contentType=${contentType}`);
// console.log(`rspHeaders=${JSON.stringify(rspHeaders)}`)
// let r2: Response = undefined;
// if (contentType.includes("xml")) {
// r2 = new Response(r.text, {
// status: r.status,
// statusText: getReasonPhrase(r.status),
// headers: r.headers,
// headers: rspHeaders,
// });
// } else if (
// contentType.includes("json") ||
@@ -70,7 +79,7 @@ if (VALID_REQURL) {
// {
// status: r.status,
// statusText: getReasonPhrase(r.status),
// headers: r.headers,
// headers: rspHeaders,
// });
// } else if (contentType.includes("text")) {
// // avoid text/json,
@@ -78,7 +87,7 @@ if (VALID_REQURL) {
// r2 = new Response(r.text, {
// status: r.status,
// statusText: getReasonPhrase(r.status),
// headers: r.headers,
// headers: rspHeaders,
// });
// } else if (
// contentType.includes("octet-stream") ||
@@ -89,7 +98,7 @@ if (VALID_REQURL) {
// r2 = new Response(r.arrayBuffer, {
// status: r.status,
// statusText: getReasonPhrase(r.status),
// headers: r.headers,
// headers: rspHeaders,
// });
// } else {
// throw Error(
@@ -100,17 +109,18 @@ if (VALID_REQURL) {
let r2: Response = undefined;
if ([101, 103, 204, 205, 304].includes(r.status)) {
// A null body status is a status that is 101, 103, 204, 205, or 304.
// https://fetch.spec.whatwg.org/#statuses
// fix this: Failed to construct 'Response': Response with null body status cannot have body
r2 = new Response(null, {
status: r.status,
statusText: getReasonPhrase(r.status),
headers: r.headers,
headers: rspHeaders,
});
} else {
r2 = new Response(r.arrayBuffer, {
status: r.status,
statusText: getReasonPhrase(r.status),
headers: r.headers,
headers: rspHeaders,
});
}
@@ -395,13 +405,7 @@ export const uploadToRemote = async (
}
};
export const listFromRemote = async (
client: WrappedWebdavClient,
prefix?: string
) => {
if (prefix !== undefined) {
throw Error("prefix not supported");
}
export const listAllFromRemote = async (client: WrappedWebdavClient) => {
await client.init();
let contents = [] as FileStat[];
+368 -11
View File
@@ -6,6 +6,7 @@ import {
Setting,
Platform,
requireApiVersion,
requestUrl,
} from "obsidian";
import type { TextComponent } from "obsidian";
import { createElement, Eye, EyeOff } from "lucide";
@@ -30,6 +31,7 @@ import {
clearAllLoggerOutputRecords,
insertLoggerOutputByVault,
clearExpiredLoggerOutputRecords,
upsertLastSuccessSyncByVault,
} from "./localdb";
import type RemotelySavePlugin from "./main"; // unavoidable
import { RemoteClient } from "./remote";
@@ -53,6 +55,7 @@ import {
log,
restoreLogWritterInplace,
} from "./moreOnLog";
import { simpleTransRemotePrefix } from "./remoteForS3";
class PasswordModal extends Modal {
plugin: RemotelySavePlugin;
@@ -218,6 +221,88 @@ class ChangeRemoteBaseDirModal extends Modal {
}
}
/**
* s3 is special and do not necessarily the same as others
* thus a new Modal here
*/
class ChangeRemotePrefixModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly newRemotePrefix: string;
constructor(app: App, plugin: RemotelySavePlugin, newRemotePrefix: string) {
super(app);
this.plugin = plugin;
this.newRemotePrefix = newRemotePrefix;
}
onOpen() {
let { contentEl } = this;
const t = (x: TransItemType, vars?: any) => {
return this.plugin.i18n.t(x, vars);
};
contentEl.createEl("h2", { text: t("modal_remoteprefix_title") });
t("modal_remoteprefix_shortdesc")
.split("\n")
.forEach((val, idx) => {
contentEl.createEl("p", {
text: val,
});
});
contentEl.createEl("p", {
text: t("modal_remoteprefix_tosave", { prefix: this.newRemotePrefix }),
});
if (
this.newRemotePrefix === "" ||
this.newRemotePrefix === this.app.vault.getName()
) {
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(t("modal_remoteprefix_secondconfirm_empty"));
button.onClick(async () => {
// in the settings, the value is reset to the special case ""
this.plugin.settings.s3.remotePrefix = "";
await this.plugin.saveSettings();
new Notice(t("modal_remoteprefix_notice"));
this.close();
});
button.setClass("remoteprefix-second-confirm");
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
} else {
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(t("modal_remoteprefix_secondconfirm_change"));
button.onClick(async () => {
this.plugin.settings.s3.remotePrefix = this.newRemotePrefix;
await this.plugin.saveSettings();
new Notice(t("modal_remoteprefix_notice"));
this.close();
});
button.setClass("remoteprefix-second-confirm");
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
}
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
@@ -629,6 +714,65 @@ class ExportSettingsQrCodeModal extends Modal {
}
}
class SetLogToHttpServerModal extends Modal {
plugin: RemotelySavePlugin;
serverAddr: string;
callBack: any;
constructor(
app: App,
plugin: RemotelySavePlugin,
serverAddr: string,
callBack: any
) {
super(app);
this.plugin = plugin;
this.serverAddr = serverAddr;
this.callBack = callBack;
}
onOpen() {
let { contentEl } = this;
const t = (x: TransItemType, vars?: any) => {
return this.plugin.i18n.t(x, vars);
};
contentEl.createEl("h2", { text: t("modal_logtohttpserver_title") });
const div1 = contentEl.createDiv();
div1.addClass("logtohttpserver-warning");
t("modal_logtohttpserver_desc")
.split("\n")
.forEach((val) => {
div1.createEl("p", {
text: val,
});
});
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(t("modal_logtohttpserver_secondconfirm"));
button.setClass("logtohttpserver-warning");
button.onClick(async () => {
this.callBack();
new Notice(t("modal_logtohttpserver_notice"));
this.close();
});
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
const getEyesElements = () => {
const eyeEl = createElement(Eye);
const eyeOffEl = createElement(EyeOff);
@@ -866,6 +1010,29 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
let newS3RemotePrefix = this.plugin.settings.s3.remotePrefix || "";
new Setting(s3Div)
.setName(t("settings_remoteprefix"))
.setDesc(t("settings_remoteprefix_desc"))
.addText((text) =>
text
.setPlaceholder("")
.setValue(newS3RemotePrefix)
.onChange((value) => {
newS3RemotePrefix = simpleTransRemotePrefix(value.trim());
})
)
.addButton((button) => {
button.setButtonText(t("confirm"));
button.onClick(() => {
new ChangeRemotePrefixModal(
this.app,
this.plugin,
simpleTransRemotePrefix(newS3RemotePrefix.trim())
).open();
});
});
new Setting(s3Div)
.setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc"))
@@ -1545,6 +1712,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
realVal > 0
) {
const intervalID = window.setInterval(() => {
log.info("auto run from settings.ts");
this.plugin.syncRun("auto");
}, realVal);
this.plugin.autoRunIntervalID = intervalID;
@@ -1578,6 +1746,77 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
});
});
new Setting(basicDiv)
.setName(t("settings_saverun"))
.setDesc(t("settings_saverun_desc"))
.addDropdown((dropdown) => {
dropdown.addOption("-1", t("settings_saverun_notset"));
dropdown.addOption(`${1000 * 1}`, t("settings_saverun_1sec"));
dropdown.addOption(`${1000 * 5}`, t("settings_saverun_5sec"));
dropdown.addOption(`${1000 * 10}`, t("settings_saverun_10sec"));
dropdown.addOption(`${1000 * 60}`, t("settings_saverun_1min"));
let runScheduled = false;
dropdown
.setValue(`${this.plugin.settings.syncOnSaveAfterMilliseconds}`)
.onChange(async (val: string) => {
const realVal = parseInt(val);
this.plugin.settings.syncOnSaveAfterMilliseconds = realVal;
await this.plugin.saveSettings();
if (
(realVal === undefined || realVal === null || realVal <= 0) &&
this.plugin.syncOnSaveIntervalID !== undefined
) {
// clear
window.clearInterval(this.plugin.syncOnSaveIntervalID);
this.plugin.syncOnSaveIntervalID = undefined;
} else if (
realVal !== undefined &&
realVal !== null &&
realVal > 0
) {
const intervalID = window.setInterval(() => {
const currentFile = this.app.workspace.getActiveFile();
if (currentFile) {
// get the last modified time of the current file
// if it has been modified within the last syncOnSaveAfterMilliseconds
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
const lastModified = currentFile.stat.mtime;
const currentTime = Date.now();
// log.debug(
// `Checking if file was modified within last ${
// this.plugin.settings.syncOnSaveAfterMilliseconds / 1000
// } seconds, last modified: ${
// (currentTime - lastModified) / 1000
// } seconds ago`
// );
if (
currentTime - lastModified <
this.plugin.settings.syncOnSaveAfterMilliseconds
) {
if (!runScheduled) {
const scheduleTimeFromNow =
this.plugin.settings.syncOnSaveAfterMilliseconds -
(currentTime - lastModified);
log.info(
`schedule a run for ${scheduleTimeFromNow} milliseconds later`
);
runScheduled = true;
setTimeout(() => {
this.plugin.syncRun("auto_sync_on_save");
runScheduled = false;
}, scheduleTimeFromNow);
}
}
}
}, realVal);
this.plugin.syncOnSaveIntervalID = intervalID;
this.plugin.registerInterval(intervalID);
}
});
});
new Setting(basicDiv)
.setName(t("settings_skiplargefiles"))
.setDesc(t("settings_skiplargefiles_desc"))
@@ -1596,6 +1835,60 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
// custom status bar items is not supported on mobile
if (!Platform.isMobileApp) {
new Setting(basicDiv)
.setName(t("settings_enablestatusbar_info"))
.setDesc(t("settings_enablestatusbar_info_desc"))
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.enableStatusBarInfo)
.onChange(async (val) => {
this.plugin.settings.enableStatusBarInfo = val;
await this.plugin.saveSettings();
new Notice(t("settings_enablestatusbar_reloadrequired_notice"));
});
});
new Setting(basicDiv)
.setName(t("settings_resetstatusbar_time"))
.setDesc(t("settings_resetstatusbar_time_desc"))
.addButton((button) => {
button.setButtonText(t("settings_resetstatusbar_button"));
button.onClick(async () => {
// reset last sync time
await upsertLastSuccessSyncByVault(
this.plugin.db,
this.plugin.vaultRandomID,
-1
);
this.plugin.updateLastSuccessSyncMsg(-1);
new Notice(t("settings_resetstatusbar_notice"));
});
});
}
new Setting(basicDiv)
.setName(t("settings_ignorepaths"))
.setDesc(t("settings_ignorepaths_desc"))
.setClass("ignorepaths-settings")
.addTextArea((textArea) => {
textArea
.setValue(`${this.plugin.settings.ignorePaths.join("\n")}`)
.onChange(async (value) => {
this.plugin.settings.ignorePaths = value
.trim()
.split("\n")
.filter((x) => x.trim() !== "");
await this.plugin.saveSettings();
});
textArea.inputEl.rows = 10;
textArea.inputEl.cols = 30;
textArea.inputEl.addClass("ignorepaths-textarea");
});
//////////////////////////////////////////////////
// below for advanced settings
//////////////////////////////////////////////////
@@ -1674,6 +1967,23 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(advDiv)
.setName(t("settings_deletetowhere"))
.setDesc(t("settings_deletetowhere_desc"))
.addDropdown((dropdown) => {
dropdown.addOption("system", t("settings_deletetowhere_system_trash"));
dropdown.addOption(
"obsidian",
t("settings_deletetowhere_obsidian_trash")
);
dropdown
.setValue(this.plugin.settings.deleteToWhere)
.onChange(async (val: "system" | "obsidian") => {
this.plugin.settings.deleteToWhere = val;
await this.plugin.saveSettings();
});
});
//////////////////////////////////////////////////
// below for import and export functions
//////////////////////////////////////////////////
@@ -1781,17 +2091,17 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.logToDB ? "enable" : "disable")
.onChange(async (val: string) => {
const logToDB = val === "enable";
if (logToDB) {
applyLogWriterInplace((...msg: any[]) => {
insertLoggerOutputByVault(
this.plugin.db,
this.plugin.vaultRandomID,
...msg
);
});
} else {
restoreLogWritterInplace();
}
// if (logToDB) {
// applyLogWriterInplace((...msg: any[]) => {
// insertLoggerOutputByVault(
// this.plugin.db,
// this.plugin.vaultRandomID,
// ...msg
// );
// });
// } else {
// restoreLogWritterInplace();
// }
clearExpiredLoggerOutputRecords(this.plugin.db);
this.plugin.settings.logToDB = logToDB;
await this.plugin.saveSettings();
@@ -1828,6 +2138,53 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
let logToHttpServer = this.plugin.debugServerTemp || "";
new Setting(debugDiv)
.setName(t("settings_logtohttpserver"))
.setDesc(t("settings_logtohttpserver_desc"))
.addText(async (text) => {
text.setValue(logToHttpServer).onChange(async (value) => {
logToHttpServer = value.trim();
});
})
.addButton(async (button) => {
button.setButtonText(t("confirm"));
button.onClick(async () => {
if (logToHttpServer === "" || !logToHttpServer.startsWith("http")) {
this.plugin.debugServerTemp = "";
logToHttpServer = "";
// restoreLogWritterInplace();
new Notice(t("settings_logtohttpserver_reset_notice"));
} else {
new SetLogToHttpServerModal(
this.app,
this.plugin,
logToHttpServer,
() => {
this.plugin.debugServerTemp = logToHttpServer;
// applyLogWriterInplace((...msg: any[]) => {
// try {
// requestUrl({
// url: logToHttpServer,
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// send_time: Date.now(),
// log_text: msg,
// }),
// });
// } catch (e) {
// // pass
// }
// });
}
).open();
}
});
});
new Setting(debugDiv)
.setName(t("settings_delsyncmap"))
.setDesc(t("settings_delsyncmap_desc"))
+68 -6
View File
@@ -7,6 +7,7 @@ import {
} from "obsidian";
import AggregateError from "aggregate-error";
import PQueue from "p-queue";
import XRegExp from "xregexp";
import type {
RemoteItem,
SyncTriggerSourceType,
@@ -38,6 +39,7 @@ import {
atWhichLevel,
unixTimeToStr,
statFix,
isFolderToSkip,
} from "./misc";
import { RemoteClient } from "./remote";
import {
@@ -217,6 +219,7 @@ export const parseRemoteItems = async (
let r = {} as FileOrFolderMixedState;
if (backwardMapping !== undefined) {
// log.debug(`backwardMapping=${backwardMapping}`);
key = backwardMapping.localKey;
const mtimeRemote = backwardMapping.localMtime || entry.lastModified;
@@ -234,7 +237,7 @@ export const parseRemoteItems = async (
changeRemoteMtimeUsingMapping: true,
};
} else {
// do not have backwardMapping
// log.debug(`do not have backwardMapping`);
r = {
key: key,
existRemote: true,
@@ -293,11 +296,23 @@ const isSkipItem = (
key: string,
syncConfigDir: boolean,
syncUnderscoreItems: boolean,
configDir: string
configDir: string,
ignorePaths: string[]
) => {
if (ignorePaths !== undefined && ignorePaths.length > 0) {
for (const r of ignorePaths) {
if (XRegExp(r, "A").test(key)) {
return true;
}
}
}
if (syncConfigDir && isInsideObsFolder(key, configDir)) {
return false;
}
if (isFolderToSkip(key, [])) {
// some special dirs and files are always skipped
return true;
}
return (
isHiddenPath(key, true, false) ||
(!syncUnderscoreItems && isHiddenPath(key, false, true)) ||
@@ -315,6 +330,7 @@ const ensembleMixedStates = async (
syncConfigDir: boolean,
configDir: string,
syncUnderscoreItems: boolean,
ignorePaths: string[],
password: string
) => {
const results = {} as Record<string, FileOrFolderMixedState>;
@@ -322,7 +338,15 @@ const ensembleMixedStates = async (
for (const r of remoteStates) {
const key = r.key;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
if (
isSkipItem(
key,
syncConfigDir,
syncUnderscoreItems,
configDir,
ignorePaths
)
) {
continue;
}
results[key] = r;
@@ -361,7 +385,15 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`);
}
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
if (
isSkipItem(
key,
syncConfigDir,
syncUnderscoreItems,
configDir,
ignorePaths
)
) {
continue;
}
@@ -395,6 +427,18 @@ const ensembleMixedStates = async (
password === "" ? undefined : getSizeFromOrigToEnc(entry.size),
};
if (
isSkipItem(
key,
syncConfigDir,
syncUnderscoreItems,
configDir,
ignorePaths
)
) {
continue;
}
if (results.hasOwnProperty(key)) {
results[key].key = r.key;
results[key].existLocal = r.existLocal;
@@ -417,7 +461,15 @@ const ensembleMixedStates = async (
deltimeRemoteFmt: unixTimeToStr(entry.actionWhen),
} as FileOrFolderMixedState;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
if (
isSkipItem(
key,
syncConfigDir,
syncUnderscoreItems,
configDir,
ignorePaths
)
) {
continue;
}
@@ -445,7 +497,15 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`);
}
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
if (
isSkipItem(
key,
syncConfigDir,
syncUnderscoreItems,
configDir,
ignorePaths
)
) {
continue;
}
@@ -967,6 +1027,7 @@ export const getSyncPlan = async (
configDir: string,
syncUnderscoreItems: boolean,
skipSizeLargerThan: number,
ignorePaths: string[],
password: string = ""
) => {
const mixedStates = await ensembleMixedStates(
@@ -978,6 +1039,7 @@ export const getSyncPlan = async (
syncConfigDir,
configDir,
syncUnderscoreItems,
ignorePaths,
password
);
+1
View File
@@ -55,6 +55,7 @@ export class SyncAlgoV2Modal extends Modal {
this.plugin.saveAgreeToUseNewSyncAlgorithm();
this.plugin.enableAutoSyncIfSet();
this.plugin.enableInitSyncIfSet();
this.plugin.enableSyncOnSaveIfSet();
} else {
log.info("do not agree to use the new algorithm");
this.plugin.unload();
+9
View File
@@ -61,3 +61,12 @@
width: 350px;
height: 350px;
}
.ignorepaths-textarea {
font-family: monospace;
}
.logtohttpserver-warning {
color: red;
font-weight: bolder;
}
+2
View File
@@ -23,6 +23,8 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
password: "password",
serviceType: "s3",
currLogLevel: "info",
ignorePaths: ["somefoldertoignore"],
enableStatusBarInfo: true,
};
describe("Config Persist tests", () => {