Compare commits

...
51 Commits
Author SHA1 Message Date
fyears 6d1b451f4c bump to 0.3.40
Release A New Version / build (16.x) (push) Failing after 46s
2024-02-12 17:25:07 +08:00
fyears 69537f7002 better sync status in zh 2024-02-12 17:23:26 +08:00
Vijayasantham 17de78240f Feat: Add #481 (#492) 2024-02-12 17:13:43 +08:00
fyears 8cbcdf5323 fix doc format 2024-02-08 16:45:24 +08:00
fyears 6ba5abc0a6 format 2024-02-08 16:45:15 +08:00
Vijayasantham d820285d1e Fix: Save on sync issue (#486)
* Fix: Save on sync issue

* Fix: Add delay before run sync on save
2024-02-08 11:58:57 +08:00
fyears a6002ba340 storj 2024-01-21 15:00:01 +08:00
fyears 1f31303906 bump to 0.3.38
Release A New Version / build (16.x) (push) Failing after 49s
2024-01-21 14:40:09 +08:00
fyears 0fcedf397f concurrency should not influence the sync 2024-01-21 14:31:39 +08:00
fyears 085d8448d2 digest ok 2024-01-21 14:19:27 +08:00
fyears 0d9da32b00 update synology 2024-01-20 22:23:46 +08:00
fyears 22880d6cf5 split the zh doc 2024-01-20 13:13:31 +08:00
fyears a084ec7728 synology webdav server 2024-01-20 13:03:46 +08:00
fyears eeeb4893c7 draft 2024-01-17 01:24:39 +08:00
vpsone ed1ac66ac0 Fix status-bar tooltip position (#442) 2024-01-16 01:30:34 +08:00
fyears c33a9ecfa4 no auto for webdav depth 2024-01-16 01:22:54 +08:00
fyears cabca54d80 universally change webdav header 2024-01-16 00:54:11 +08:00
fyears 8dde6c59d2 strict anywhere 2024-01-14 22:13:10 +08:00
fyears 480cf224c6 bump to 0.3.37
Release A New Version / build (16.x) (push) Failing after 47s
2024-01-14 16:19:25 +08:00
fyears 37a801332f vault might be undefined 2024-01-14 16:19:00 +08:00
fyears 1e33f8b7d2 0.3.36
Release A New Version / build (16.x) (push) Failing after 43s
2024-01-14 11:47:49 +08:00
fyears 071b10387c add more compability to onedrive 2024-01-14 11:46:35 +08:00
fyears b99053ef10 add alist (and cloudreve) 2024-01-14 11:20:53 +08:00
fyears ff237ad8cb fix typo 2024-01-14 11:18:27 +08:00
fyears b89a163a66 update service 2024-01-14 00:49:07 +08:00
fyears 991376a7a3 fix typo 2024-01-14 00:13:23 +08:00
fyears 2da39bdfec bump 0.3.35
Release A New Version / build (16.x) (push) Failing after 48s
2024-01-14 00:09:23 +08:00
fyears f38c3d36be tencent cloud cos 2024-01-13 23:57:16 +08:00
fyears da0f183fad missing saving 2024-01-13 23:03:30 +08:00
fyears eb2ecca825 onedrive client update time 2024-01-13 22:47:34 +08:00
fyears 2494a2d96e ok no way to skip the region 2024-01-13 21:27:42 +08:00
fyears 8bb095c4aa round the time 2024-01-13 21:20:56 +08:00
fyears c5e3147805 dropbox supports client mtime 2024-01-13 20:14:22 +08:00
fyears 942e54961e unset region per user request 2024-01-13 19:37:58 +08:00
fyears 29a3c76b4e s3 mtime 2024-01-13 19:27:51 +08:00
fyears 897e4fc4e5 more clear about http debug server 2024-01-13 17:10:31 +08:00
fyears 0124a49d7e unload if reset 2024-01-13 17:03:42 +08:00
fyears 3e245af50c no need to read log records 2024-01-13 17:00:46 +08:00
fyears 695c74588f clear sync plans 2024-01-13 16:59:51 +08:00
fyears e3bd41a05a always bypass cors s3 2024-01-13 16:54:50 +08:00
fyears a6b73bde6a fix callback for dropbox 2024-01-13 16:47:55 +08:00
fyears 87d6e1f75f remove logtodb since we have remote debugging nowgit add .! 2024-01-13 16:44:56 +08:00
fyears 347c051d0c support json syncplans only 2024-01-13 12:03:21 +08:00
fyears 7422956621 fix minor bug of onedrive 2024-01-10 02:49:14 +08:00
fyears 7b67ba5b26 fix dropbox path 2024-01-10 02:28:42 +08:00
fyears 43cf95c417 fix more bugs on webdav 2024-01-10 02:16:18 +08:00
fyears 53a1c32b43 log everything 2024-01-10 01:40:17 +08:00
fyears 71e25be28e fix minor bug of webdav 2024-01-10 01:34:45 +08:00
fyears afc5670313 add b2 2024-01-10 01:19:57 +08:00
vardecab 81eb45d32c add Backblaze B2 (#418)
add Backblaze B2
2024-01-10 00:57:38 +08:00
fyears d7a59b82be add funding url
Release A New Version / build (16.x) (push) Failing after 42s
2024-01-07 11:38:56 +08:00
60 changed files with 1419 additions and 1090 deletions
+18 -1
View File
@@ -31,13 +31,30 @@ body:
- type: dropdown
id: service
attributes:
label: What remote cloud services are you using?
label: What remote cloud services are you using? (Please choose the specified one if it's in the list)
multiple: true
options:
- S3
- S3 (Cloudflare R2)
- S3 (BackBlaze B2)
- S3 (腾讯云 COS Tencent Cloud COS)
- S3 (阿里云 OSS Alibaba Cloud OSS)
- S3 (MinIO)
- S3 (Wasabi)
- S3 (Storj)
- OneDrive for personal
- OneDrive for business
- Dropbox
- webdav
- webdav (ownCloud)
- webdav (InfiniCloud (formally TeraCLOUD))
- webdav (AList)
- webdav (Cloudreve)
- webdav (坚果云 JianGuoYun/NutStore)
- webdav (NextCloud)
- webdav (FastMail)
- webdav (rclone webdav)
- webdav (nginx)
- others
validations:
required: true
@@ -21,9 +21,26 @@ body:
multiple: true
options:
- S3
- S3 (Cloudflare R2)
- S3 (BackBlaze B2)
- S3 (腾讯云 COS Tencent Cloud COS)
- S3 (阿里云 OSS Alibaba Cloud OSS)
- S3 (MinIO)
- S3 (Wasabi)
- S3 (Storj)
- OneDrive for personal
- OneDrive for business
- Dropbox
- webdav
- webdav (ownCloud)
- webdav (InfiniCloud (formally TeraCLOUD))
- webdav (AList)
- webdav (Cloudreve)
- webdav (坚果云 JianGuoYun/NutStore)
- webdav (NextCloud)
- webdav (FastMail)
- webdav (rclone webdav)
- webdav (nginx)
- others
validations:
required: false
+8 -3
View File
@@ -17,7 +17,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
## Features
- Supports:
- Amazon S3 or S3-compatible
- Amazon S3 or S3-compatible (Cloudflare R2 / BackBlaze B2 / MinIO / ...)
- Dropbox
- OneDrive for personal
- Webdav
@@ -26,7 +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!
- **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.**
@@ -63,6 +63,9 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
- Tutorials / Examples:
- [Cloudflare R2](./docs/remote_services/s3_cloudflare_r2/README.md)
- [BackBlaze B2](./docs/remote_services/s3_backblaze_b2/README.md)
- [Storj](./docs/remote_services/s3_storj_io/README.md)
- [腾讯云 COS](./docs/remote_services/s3_tencent_cloud_cos/README.zh-cn.md) | [Tencent Cloud COS](./docs/remote_services/s3_tencent_cloud_cos/README.md)
- [MinIO](./docs/remote_services/s3_minio/README.md)
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
- If you are using AWS S3, create [policy and user](./docs/remote_services/s3_general/s3_user_policy.md).
@@ -93,7 +96,9 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
- 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)
- [Synology webdav server](./docs/remote_services/webdav_synology_webdav_server/README.md) | [群晖 webdav server](./docs/remote_services/webdav_synology_webdav_server/README.zh-cn.md)
- [AList(中文)](./docs/remote_services/webdav_alist/README.zh-cn.md) | [AList (English)](./docs/remote_services/webdav_alist/README.md)
- [坚果云](./docs/remote_services/webdav_jianguoyun/README.zh-cn.md) | [JianGuoYun/NutStore](./docs/remote_services/webdav_jianguoyun/README.md)
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
@@ -0,0 +1,53 @@
# Backblaze B2
## Links
https://www.backblaze.com/cloud-storage
## Steps
1. Create a Backblaze account [on this page](https://www.backblaze.com/cloud-storage). Credit card info _is not_ required. Backblaze B2 offers 10 GB of free storage.
2. Please be aware that, though B2 provides some free quota, **it may still cost you money if the usage of storage or api requests exceed a certain value!!!** Especially pay attention to the api requests!!!
3. Create a **bucket**, you can leave the default settings, or you can enable the encryption (which is different from what you can set in Remotely Save):
![](./s3_backblaze_b2-1-bucket.png)
![](./s3_backblaze_b2-2-create_bucket.png)
4. Copy `Endpoint`, eg. `s3.us-east-005.backblazeb2.com` — it'll be used later.
5. Copy `bucketname` near the 🪣 icon (the "bucket icon") — it'll be used later.
![](./s3_backblaze_b2-3-copy.png)
6. Go to **Application Keys**:
![](./s3_backblaze_b2-4-app_keys.png)
7. **Add a new key**:
![](./s3_backblaze_b2-5-add_new_app_keys.png)
![](./s3_backblaze_b2-6-app_keys_copy.png)
8. Save `keyID` and `applicationKey` — they will be used later.
9. Go to Remotely Save settings in Obsidian and:
- Choose `S3 or compatibile` in **Remote Service**:
- Copy `Endpoint` from Backblaze (see 3. above) to `Endpoint` in Remotely Save
- From `endpoint` take `region` (eg. `us-east-005`) and paste it in `endpoint` in Remotely Save
- Copy `keyID` (see 7. above) to `Access Key ID` in Remotely Save
- Copy `applicationKey` (see 7. above) to `Secret Access Key` in Remotely Save
- Copy `bucketname` (see 4. above) to `Bucket Name` in Remotely Save
![](./s3_backblaze_b2-7-copy_paste.png)
10. **Enable Bypass CORS**:
![](./s3_backblaze_b2-8-cors.png)
11. Click **Check** in _Check Connectivity_ to see if you can connect to B2 bucket:
![](./s3_backblaze_b2-9-check_connectionpng.png)
12. Sync!
![](./s3_backblaze_b2-10-sync.png)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fae18a84bb218461aca01c7de47ddadfd2b451d865ec889c098cd6918f6e8773
size 59679
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9389f9c4462740371edb548f7e05191ede5277a5d470e3254c60fbcc4a75e552
size 4558
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46f35968938ebf3bbafae64ee49a4927844fd0abf79e4190522be91776d9fed0
size 107623
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fb639c7b4e595516594ee9d07c864e7cd15b608a97c81386f4cf72d499d0e5f
size 80121
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:299bfca82b77de86f60d8f481b5f74069dc977dece19fa025ee629d1e6f783d4
size 6499
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5985e918b521c91cc185f60c58cdd334ee76695b897a02e02eaa17c1bad71609
size 10214
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5796791bcf89c99c1e1e844c0083739fd46b1a458dc274f3c3fa3f65b87b71b3
size 70728
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3633379fcb12f1a5e0a4d1d68c193ddc7bf0be7edd92f2f6006351817035a94f
size 27164
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e130be182ae7d3270e5de3fec442ac5e81efd61822e844cd61ed24c7e485d39
size 16802
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29a8971fea065fd86223850198640f4072ee3fda2112820829cda1348f122c59
size 11103
@@ -0,0 +1,17 @@
# Storj
## Links
<https://www.storj.io/>
## Steps
1. Register an account. Login.
2. Create a bucket.
3. Create S3 Credentials in Access Management. Allow all permissions for the bucket. Remember the access key and secret key and the end point. The end point is likely to be [`https://gateway.storjshare.io`](https://docs.storj.io/dcs/api/s3/s3-compatible-gateway).
![](./storj_create_s3_cred_1.png)
![](./storj_create_s3_cred_2.png)
4. Input your credentials into remotely-save settings. Region [should be `global`](https://docs.storj.io/dcs/api/s3/s3-compatibility).
![](storj_remotely_save_settings.png)
5. Check connectivity.
6. Sync!
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3484f74f45fbdae6110de285edeff10c196c13160cb97f079da77843ae92e80
size 386188
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42593d98e1b154f4174a932088e72c25db9a976b5613f4d6ae21df1445b4559c
size 278219
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7843c31153054eb4762dbaf3027c1042b0d7cb4dd774bf584788cd7a97ba4044
size 571188
@@ -0,0 +1,23 @@
# Tencent Cloud COS
English | [中文](./README.zh-cn.md)
## Link
- international <https://console.tencentcloud.com/cos>
## 步骤 Steps
The example shows the steps of China version. International version should be similar.
1. Create a bucket with **private read-write permissions** and recommendly enable server-side-encryption.
2. In bucket list page, enter the bucket overview page of the bucket you just created. You should see the bucket name (your texts with the number of your account id), region, and access address.
![](./cos_bucket_info.png)
3. In CAM page, create api key, and note down the SecretID and SecretKey.
![](./cos_create_secret.png)
4. **Remove the bucket name from your access address to obtain your endpoint address! If your access address on the website is `https://<bucket-name-with-number>.cos.<region>.myqcloud.com`, then the endpoint address you are going to use is `https://cos.<region>.myqcloud.com`.**
5. In remotely-save settings page, enter your endpoint adress, SecretID, SecretKey,and bucket name.
![](./cos_setting.png)
6. Check Connectivity.
![](./cos_connection.png)
7. Sync!
@@ -0,0 +1,23 @@
# 腾讯云 COS
[English](./README.md) | 中文
## 链接
- 中国区 <https://console.cloud.tencent.com/cos>
## 步骤
注意这里用中国区示例,国际区配置应该类似。
1. 在“存储桶列表”页,[“创建存储桶”](https://console.cloud.tencent.com/cos/bucket?action=create)。注意创建**私有读写**,建议打开服务端加密。
2. 在桶列表页,点击刚刚存储的桶,进入概览页。可以见到桶名称(一般来说是之前指定的英文加账号数字),地域,访问域名。记录下来。
![](./cos_bucket_info.png)
3. 在[“访问管理页”](https://console.cloud.tencent.com/cam/capi) ,“API 密钥管理”,“创建密钥”,要记录 SecretID 和 SecretKey。
![](./cos_create_secret.png)
4. **把桶名称从访问域名移除,才是你即将输入的服务地址!假如你在腾讯云网站看到访问域名是 `https://<bucket-name-with-number>.cos.<region>.myqcloud.com`,那么“服务地址”是 `https://cos.<region>.myqcloud.com`.**
5. 在 remotely-save 设置,输入服务地址,SecretIDSecretKey,和 桶名称。
![](./cos_setting.png)
6. 检查连接。
![](./cos_connection.png)
7. 可以同步了!
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08b47288686c8ef8c6426776f7df52411efe431966e8f7f2f9a3a8836ee4d6c6
size 323127
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94c0ec5db7a75c54c36224f067659682e271aea2e61b1ee50d046cd493ba72ac
size 58410
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f51f542756def80a77404e47f9793d1cd6917df5ab6e09d5a0ee1f7191855dc6
size 425021
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55064233031b44066fb7b77e17ba1eb1d0ae8398235a343cc7e137b07aaf7a39
size 668954
+10 -9
View File
@@ -1,22 +1,23 @@
# AList
## 链接 Links
English | [中文](./README.zh-cn.md)
## 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
## 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`.
1. Install and run AList. Get the account and password. Login using the web page.
2. Add new storage. Pay attention to the mount path. The screenshot shows the mount path as `/alisttest davpath`.
![](./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`:
3. Construct the webdav address as: **http(s)://domain** + **port** + **`/dav`** + **mount path**, and the space inside the mount path should be replaced with `%20`:
```
http[s]://domain:port/dav/[mountpath url encoded]
http://127.0.0.1:5244/dav/alisttest%20davpath
```
4. 在 remotely-save 设置,输入**带域名端口`/dav`和挂载路径的网址**、账号、密码。In remotely-save setting page, select webdav type, then input the **full address with mount path**/account/password.
4. 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!
5. In remotely-save setting page, click "Check Connectivity".
6. Sync!
@@ -0,0 +1,23 @@
# AList
[English](./README.md) | 中文
## 链接
- 中文官网:<https://alist.nn.ci/zh/> 和 <https://alist.nn.ci/zh/guide/webdav.html>
## 步骤
1. 安装和使用 AList。获取账号名和密码。在网页上登录。
2. 新建挂载,检查挂载路径。如图所示是 `/alisttest davpath`
![](./alist_mount_path.zh.png)
![](./alist_mount_path.en.png)
3. 从而构建 webdav 网址如下,**http(s)://域名** + **端口** + **`/dav`** + **挂载路径**,其中挂载路径中假如有空格,换成 `%20`
```
http[s]://domain:port/dav/[mountpath url encoded]
http://127.0.0.1:5244/dav/alisttest%20davpath
```
4. 在 remotely-save 设置,输入**带域名端口`/dav`和挂载路径的网址**、账号、密码。
![](./alist_rs_settings.en.png)
5. 在 remotely-save 设置,检查连接。
6. 同步文件!
@@ -1,22 +1,22 @@
# 坚果云 JianGuoYun/NutStore
# JianGuoYun/NutStore
## 链接 Link
English | [中文](./README.zh-cn.md)
## Link
<https://www.jianguoyun.com/>
## 注意!!!Attentions!!!
坚果云有限制 api 数量等设定。本插件会产生若干查询,如果文件较多很容易触发 api 上限,从而工作不正常。这不是插件 bug,也没有办法解决。
## Attentions!!!
JianGuoYun/NutStore has api limits. The plugin may generate many queries, and it's possible to reach the api limits if there are many files, then do not work properly. It's not a bug and there's no way to fix this situation.
## 步骤 Steps
## 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).
1. **Be aware that JianGuoYun/NutStore has api limits, and the plugin may not work properly because of this.**
2. Register an account.
3. Go to "settings"->"Security", click "Add Application", then obtain the WebDAV account (email), and WebDAV password (a string different from web site password).
![](./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.
4. 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!
5. In remotely-save setting page, click "Check Connectivity".
6. Sync!
@@ -0,0 +1,22 @@
# 坚果云
[English](./README.md) | 中文
## 链接
<https://www.jianguoyun.com/>
## 注意!!!
坚果云有限制 api 数量等设定。本插件会产生若干查询,如果文件较多很容易触发 api 上限,从而工作不正常。这不是插件 bug,也没有办法解决。
## 步骤
1. **知悉坚果云有 api 限制,本插件可能因此工作不正常。**
2. 注册账号,登录。
3. 去“个人信息”->“安全”,“添加应用”,从而获取了 webDAV 账号(应该是 email)和 WebDAV 密码(一串特殊的字符,不等于网站密码)。
![](./webdav_jianguoyun.cn.png)
4. 在 remotely-save 设置,输入网址、账号、密码、**“发送到服务器的 Depth Header”设置为“只支持 depth='1'”**。
![](./webdav_jianguoyun_rs_settting.cn.png)
5. 在 remotely-save 设置,检查连接。
6. 同步文件!
@@ -0,0 +1,55 @@
# Synology Webdav Server
English | [中文](./README.zh-cn.md)
## Link
<https://kb.synology.com/en-global/DSM/tutorial/How_to_access_files_on_Synology_NAS_with_WebDAV>
## Attention
The tutorial author (the author of Remotely Save) is NOT an expert of NAS / Synology. Please read the doc carefully and change it to accommodate your needs by yourself.
**It's dangerous to expose your NAS into public Internet if you don't know how to set up firewalls and other protections.**
## Steps
Synology DSM 7 is used in this tutorial.
1. Create a new shared folder if you don't have one. For this tutorial, a new shared folder `share2` is created. You should assing a proper user to read / write the shared folder.
![](./synology_create_shared_folder.png)
2. Assuming you want to sync your vault into sub folder `哈哈哈/sub folder`, please create the sub folder(s) correspondingly inide the shared folder `share2`.
3. Install webdav server from package center.
![](./synology_install_webdav_server.png)
4. Enter the webdav server settings.
5. If you know how to configure https certificates correctly, you are strongly recommend to enable https.
For the demonstration purpose, this tutorial enable http server for the later steps.
Also "Enable DavDepthInfinity", which could speed up the plugin runnings greatly.
"Apply".
![](./synology_webdav_server_settings.png)
6. In Remotely Save settings, you should input your address as:
`http(s)://<your synology ip or domain>:<port>/<shared folder>/<sub folders>`
For example, in the tutorial, the proper url should be:
`http://<ip>:5000/share2/哈哈哈/sub folder`
Username and password should be the user you configured before with read / write permissions to `share2`.
Depth header should be "supports depth="infinity"".
Check connectivity!
![](./synology_remotely_save_settings.png)
7. Sync!
@@ -0,0 +1,56 @@
# 群晖 Webdav Server
[English](./README.md) | 中文
## 链接
<https://kb.synology.cn/zh-cn/DSM/tutorial/How_to_access_files_on_Synology_NAS_with_WebDAV>
## 注意
教程作者(Remotely Save 作者)**不是** NAS、群晖专家。请仔细阅读文档,并自行改动以适应您自身需求。
**没有设置防火墙和其他保护措施的话,将 NAS 暴露到公网上非常危险。**
## 步骤
本教程有用到群晖 DSM 7。
1. 创建共享文件夹。本教程示例创建了 `share2`。你需要允许某个账号对此的读写权限。
![](./synology_create_shared_folder.png)
2. 假设之后你想同步你的库到子文件夹,`哈哈哈/sub folder`,请先在共享文件夹 `share2` 底下创建好。
3. 从套件中心安装 webdav server 。
![](./synology_install_webdav_server.png)
4. 进入 webdav server 设置。
5. 如果你知道如何正确配置 https 证书的话,强烈建议开启 https。
本教程简化示例,开启了 http。
也设置“Enable DavDepthInfinity”,这可以加速插件连接速度。
“Apply”。
![](./synology_webdav_server_settings.png)
6. 在 Remotely Save 设置页,你的地址应如下格式输入:
`http(s)://<your synology ip or domain>:<port>/<shared folder>/<sub folders>`
比如说,本教程里,正确的地址类似于:
`http://<ip>:5000/share2/哈哈哈/sub folder`
用户名和密码是你之前配置了允许读写 `share2` 的那个账号。
Depth 设置应为“supports depth="infinity"”。
检查连接!
![](./synology_remotely_save_settings.png)
7. 同步!
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71e61c25db793c00d4406ea0f46745941e16e268a41b7328b03a183ab0706ec8
size 59700
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f0725099989b5496048c0e3cb284b4f6537d66866b4618c8e73ee758cb2b642
size 174986
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bc6d03a9deb0e8babd2ed102e7b85ee1062797abf1146ae71f80237a852ff10
size 522084
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba3781f24b5d65ee6915d496f7542c569116141cdced24ca505f4373d9efa9df
size 536163
+38
View File
@@ -0,0 +1,38 @@
# Sync Algorithm V3
Drafted on 20240117.
An absolutely better sync algorithm. Better for tracking deletions and better for subbranching.
## Huge Thanks
Basically a combination of algorithm v2 + [synclone](https://github.com/Jwink3101/syncrclone) + [rsinc](https://github.com/ConorWilliams/rsinc) + (some of rclone [bisync](https://rclone.org/bisync/)). All of the later three are released under MIT License so no worries about the licenses.
## Features
Must have
1. true deletion detection
2. deletion protection (blocking) with a setting
3. transaction from the old algorithm
4. user warning show up, **new algorithm needs all clients to be updated!** (deliberately corrput the metadata file??)
5. filters
6. conflict warning
7. partial sync
Nice to have
1. true time and hash
2. conflict rename
## Description
We have _five_ input sources: local all files, remote all files, _local previous succeeded sync history_, local deletions, remote deletions.
Init run, consuming local deletions and remote deletions :
TBD
Later runs, use the first, second, third sources **only**.
TBD
+3 -2
View File
@@ -1,10 +1,11 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.33",
"version": "0.3.40",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
"authorUrl": "https://github.com/fyears",
"isDesktopOnly": false
"isDesktopOnly": false,
"fundingUrl": "https://github.com/remotely-save/donation"
}
+3 -2
View File
@@ -1,10 +1,11 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.33",
"version": "0.3.40",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
"authorUrl": "https://github.com/fyears",
"isDesktopOnly": false
"isDesktopOnly": false,
"fundingUrl": "https://github.com/remotely-save/donation"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "remotely-save",
"version": "0.3.25",
"version": "0.3.40",
"description": "This is yet another sync plugin for Obsidian app.",
"scripts": {
"dev2": "node esbuild.config.mjs --watch",
+26 -7
View File
@@ -21,10 +21,17 @@ export interface S3Config {
s3AccessKeyID: string;
s3SecretAccessKey: string;
s3BucketName: string;
bypassCorsLocally?: boolean;
partsConcurrency?: number;
forcePathStyle?: boolean;
remotePrefix?: string;
useAccurateMTime?: boolean;
/**
* @deprecated
*/
bypassCorsLocally?: boolean;
}
export interface DropboxConfig {
@@ -41,9 +48,10 @@ export interface DropboxConfig {
export type WebdavAuthType = "digest" | "basic";
export type WebdavDepthType =
| "auto_unknown"
| "auto_1"
| "auto_infinity"
| "auto" // deprecated on 20240116
| "auto_unknown" // deprecated on 20240116
| "auto_1" // deprecated on 20240116
| "auto_infinity" // deprecated on 20240116
| "manual_1"
| "manual_infinity";
@@ -52,9 +60,14 @@ export interface WebdavConfig {
username: string;
password: string;
authType: WebdavAuthType;
manualRecursive: boolean; // deprecated in 0.3.6, use depth
depth?: WebdavDepthType;
remoteBaseDir?: string;
/**
* @deprecated
*/
manualRecursive: boolean; // deprecated in 0.3.6, use depth
}
export interface OnedriveConfig {
@@ -86,7 +99,7 @@ export interface RemotelySavePluginSettings {
syncConfigDir?: boolean;
syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto;
logToDB?: boolean;
skipSizeLargerThan?: number;
ignorePaths?: string[];
enableStatusBarInfo?: boolean;
@@ -96,11 +109,16 @@ export interface RemotelySavePluginSettings {
* @deprecated
*/
vaultRandomID?: string;
/**
* @deprecated
*/
logToDB?: boolean;
}
export interface RemoteItem {
key: string;
lastModified: number;
lastModified?: number;
size: number;
remoteType: SUPPORTED_SERVICES_TYPE;
etag?: string;
@@ -175,6 +193,7 @@ export interface FileOrFolderMixedState {
export const API_VER_STAT_FOLDER = "0.13.27";
export const API_VER_REQURL = "0.13.26"; // desktop ver 0.13.26, iOS ver 1.1.1
export const API_VER_REQURL_ANDROID = "0.14.6"; // Android ver 1.2.1
export const API_VER_ENSURE_REQURL_OK = "1.0.0"; // always bypass CORS here
export const VALID_REQURL =
(!Platform.isAndroidApp && requireApiVersion(API_VER_REQURL)) ||
+5 -40
View File
@@ -1,10 +1,7 @@
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
import type { SyncPlanType } from "./sync";
import {
readAllSyncPlanRecordTextsByVault,
readAllLogRecordTextsByVault,
} from "./localdb";
import { readAllSyncPlanRecordTextsByVault } from "./localdb";
import type { InternalDBs } from "./localdb";
import { mkdirpInVault } from "./misc";
import {
@@ -78,8 +75,7 @@ const turnSyncPlanToTable = (record: string) => {
export const exportVaultSyncPlansToFiles = async (
db: InternalDBs,
vault: Vault,
vaultRandomID: string,
toFormat: "table" | "json" = "json"
vaultRandomID: string
) => {
log.info("exporting");
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
@@ -88,16 +84,9 @@ export const exportVaultSyncPlansToFiles = async (
if (records.length === 0) {
md = "No sync plans history found";
} else {
if (toFormat === "json") {
md =
"Sync plans found:\n\n" +
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
} else if (toFormat === "table") {
md =
"Sync plans found:\n\n" + records.map(turnSyncPlanToTable).join("\n\n");
} else {
const _: never = toFormat;
}
md =
"Sync plans found:\n\n" +
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
}
const ts = Date.now();
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
@@ -106,27 +95,3 @@ export const exportVaultSyncPlansToFiles = async (
});
log.info("finish exporting");
};
export const exportVaultLoggerOutputToFiles = async (
db: InternalDBs,
vault: Vault,
vaultRandomID: string
) => {
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
const records = await readAllLogRecordTextsByVault(db, vaultRandomID);
let md = "";
if (records.length === 0) {
md = "No logger history found.";
} else {
md =
"Logger history found:\n\n" +
"```text\n" +
records.join("\n") +
"\n```\n";
}
const ts = Date.now();
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_LOG_HISTORY_FILE_PREFIX}${ts}.md`;
await vault.create(filePath, md, {
mtime: ts,
});
};
+1 -1
View File
@@ -14,7 +14,7 @@ export const exportQrCodeUri = async (
currentVaultName: string,
pluginVersion: string
) => {
const settings2 = cloneDeep(settings);
const settings2: Partial<RemotelySavePluginSettings> = cloneDeep(settings);
delete settings2.dropbox;
delete settings2.onedrive;
delete settings2.vaultRandomID;
+20 -28
View File
@@ -28,8 +28,9 @@
"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_fail": "Something went wrong from response from Dropbox. Maybe the network connection is not good. Maybe you rejected the auth?",
"protocol_dropbox_connect_unknown": "Do not know how to deal with the callback: {{params}}",
"protocol_dropbox_no_modal": "You are not startting Dropbox connection from the settings page. Abort.",
"protocol_onedrive_connecting": "Connecting to OneDrive...\nPlease DO NOT close this modal.",
"protocol_onedrive_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
"protocol_onedrive_connect_fail": "Something went wrong from response from OneDrive. Maybe you rejected the auth?",
@@ -37,17 +38,18 @@
"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_time_years": "Synced {{time}} years ago",
"statusbar_time_months": "Synced {{time}} months ago",
"statusbar_time_weeks": "Synced {{time}} weeks ago",
"statusbar_time_days": "Synced {{time}} days ago",
"statusbar_time_hours": "Synced {{time}} hours ago",
"statusbar_time_minutes": "Synced {{time}} minutes ago",
"statusbar_time_lessminute": "Synced less than a minute ago",
"statusbar_lastsync": "Synced {{time}} ago",
"statusbar_syncing": "Syncing...",
"statusbar_now": "Synced just now",
"statusbar_lastsync_label": "Last successful Sync on {{date}}",
"statusbar_lastsync_never": "Never Synced",
"statusbar_lastsync_never_label": "Never Synced before",
@@ -163,7 +165,7 @@
"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_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",
@@ -173,6 +175,8 @@
"settings_s3_bypasscorslocally_desc": "The plugin allows skipping server CORS config in new version of Obsidian ( desktop>=0.13.25 or iOS>=1.1.1 or Android>=1.2.1). If you encounter any issues, please disable this setting and config CORS on servers (allowing requests from app://obsidian.md and capacitor://localhost and http://localhost and add ETag into exposed headers).",
"settings_s3_parts": "Parts Concurrency",
"settings_s3_parts_desc": "Large files are split into small parts to upload in S3. How many parts do you want to upload in parallel at most?",
"settings_s3_accuratemtime": "Use Accurate MTime",
"settings_s3_accuratemtime_desc": "Read the uploaded accurate last modified time for better sync algorithm. But it causes extra api requests / time / money to the S3 endpoint.",
"settings_s3_urlstyle": "S3 URL style",
"settings_s3_urlstyle_desc": "Whether to force path-style URLs for S3 objects (e.g., https://s3.amazonaws.com/*/ instead of https://*.s3.amazonaws.com/).",
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
@@ -222,8 +226,7 @@
"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_desc": "Webdav servers should be configured to allow requests with header Depth being '1' or 'Infinity'. If you are not sure what's this, choose \"depth='1'\". If you are sure your server supports depth='infinity', please choose that to get way better performance.",
"settings_webdav_depth_1": "only supports depth='1'",
"settings_webdav_depth_inf": "supports depth='infinity'",
"settings_webdav_connect_succ": "Great! The webdav server can be accessed.",
@@ -260,27 +263,16 @@
"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_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
"settings_syncplans_button_json": "Export",
"settings_syncplans_notice": "Sync plans history exported.",
"settings_delsyncplans": "Delete Sync Plans History In DB",
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
"settings_delsyncplans_button": "Delete Sync Plans History",
"settings_delsyncplans_notice": "Sync plans history (in DB) deleted.",
"settings_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_logtohttpserver": "Log To HTTP(S) Server Temporarily",
"settings_logtohttpserver_desc": "It's very dangerous and please use the function with greate cautions!!!!! It will temporarily allow sending console loggings to HTTP(S) server.",
"settings_logtohttpserver_reset_notice": "Your input doesn't starts with \"http(s)\". Already removed the setting of logging to HTTP(S) 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",
+20 -28
View File
@@ -28,8 +28,9 @@
"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_fail": "Dropbox 的回调请求有点异常。您是否网络不顺畅?您是否拒绝了鉴权?",
"protocol_dropbox_connect_unknown": "不知道如何处理此 callback{{params}}",
"protocol_dropbox_no_modal": "您不是在设置页的弹窗里触发 Dropbox 的。中止操作。",
"protocol_onedrive_connecting": "正在连接 OneDrive……\n请不要关闭此弹窗。",
"protocol_onedrive_connect_succ_revoke": "您已作为用户 {{username}} 连接上了。如果您想取消连接,请点击此按钮。",
"protocol_onedrive_connect_fail": "OneDrive 的回调请求有点异常。您是否拒绝了鉴权?",
@@ -37,17 +38,18 @@
"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_time_years": "{{time}} 年前同步",
"statusbar_time_months": "{{time}} 月前同步",
"statusbar_time_weeks": "{{time}} 周前同步",
"statusbar_time_days": "{{time}} 天前同步",
"statusbar_time_hours": "{{time}} 小时前同步",
"statusbar_time_minutes": "{{time}} 分钟前同步",
"statusbar_time_lessminute": "一分钟之内同步",
"statusbar_lastsync": "上一次同步于:{{time}}",
"statusbar_syncing": "正在同步",
"statusbar_now": "刚同步完",
"statusbar_lastsync_label": "上一次同步于:{{date}}",
"statusbar_lastsync_never": "没触发过同步",
"statusbar_lastsync_never_label": "没触发过同步",
@@ -163,7 +165,7 @@
"settings_s3_prod3": "设置 CORS",
"settings_s3_endpoint": "服务地址(Endpoint",
"settings_s3_region": "区域(Region",
"settings_s3_region_desc": "如果您不知道这个值填什么,可以尝试填写此默认值us-east-1",
"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",
@@ -173,6 +175,8 @@
"settings_s3_bypasscorslocally_desc": "对于 Obsidian 新版本(桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.2.1),本插件可以跳过服务器设置 CORS 的步骤。如果您遇到任意问题,可以关闭此设定,并在服务端设置 CORS(允许来自 app://obsidian.md 和 capacitor://localhost 和 http://localhost 的请求且增加 ETag 到暴露 headers 里)。",
"settings_s3_parts": "分块并行度",
"settings_s3_parts_desc": "在 S3 里,大文件会被分块上传。您希望同一时间最多有多少个分块被上传?",
"settings_s3_accuratemtime": "使用准确的文件修改时间",
"settings_s3_accuratemtime_desc": "读取(已上传的)准确的文件修改时间,有助于同步算法更加准确和稳定。但是它也会导致额外的 api 请求、时间、金钱花费。",
"settings_s3_urlstyle": "S3 URL style",
"settings_s3_urlstyle_desc": "是否对 S3 对象强制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
"settings_s3_connect_succ": "很好!可以访问到对应存储桶。",
@@ -222,8 +226,7 @@
"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_desc": "Webdav 服务器会被设为接收 Depth header 为“1”或“Infinity”请求。如果您不清楚如何设置,可以选择 depth='1'。如果那你确认服务器支持 depth='infinity',请选择它,会获得更加好的性能。",
"settings_webdav_depth_1": "只支持 depth='1'",
"settings_webdav_depth_inf": "支持 depth='infinity'",
"settings_webdav_connect_succ": "很好!可以连接上 Webdav 服务器。",
@@ -260,27 +263,16 @@
"settings_outputsettingsconsole_button": "输出",
"settings_outputsettingsconsole_notice": "已输出到终端",
"settings_syncplans": "导出同步计划",
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。并且,您可以选择导出为内嵌在 Markdown 里的 json 格式或表格格式,它们是等价的。",
"settings_syncplans_button_json": "导出json",
"settings_syncplans_button_table": "导出(表格)",
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
"settings_syncplans_button_json": "导出",
"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_logtohttpserver": "临时设定终端日志实时转发到 HTTP(S) 服务器。",
"settings_logtohttpserver_desc": "非常危险,谨慎行动!!!!!临时设定终端日志实时转发到 HTTP(S) 服务器。",
"settings_logtohttpserver_reset_notice": "您的输入不是“http(s)”开头的。已移除了终端日志转发到 HTTP(S) 服务器的设定。",
"settings_delsyncmap": "删除数据库里的同步映射历史",
"settings_delsyncmap_desc": "同步映射历史存储了本地真正的最后修改时间和远程文件时间的映射。删除之可能会导致下一次同步时发生不必要的数据交换。点击按钮删除数据库里的同步映射历史。",
"settings_delsyncmap_button": "删除同步映射历史",
+20 -28
View File
@@ -28,8 +28,9 @@
"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_fail": "Dropbox 的回撥請求有點異常。您是否網路不順暢?您是否拒絕了鑑權?",
"protocol_dropbox_connect_unknown": "不知道如何處理此 callback{{params}}",
"protocol_dropbox_no_modal": "您不是在設定頁的彈窗裡觸發 Dropbox 的。中止操作。",
"protocol_onedrive_connecting": "正在連線 OneDrive……\n請不要關閉此彈窗。",
"protocol_onedrive_connect_succ_revoke": "您已作為使用者 {{username}} 連線上了。如果您想取消連線,請點選此按鈕。",
"protocol_onedrive_connect_fail": "OneDrive 的回撥請求有點異常。您是否拒絕了鑑權?",
@@ -37,17 +38,18 @@
"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_time_years": "{{time}} 年前同步",
"statusbar_time_months": "{{time}} 月前同步",
"statusbar_time_weeks": "{{time}} 周前同步",
"statusbar_time_days": "{{time}} 天前同步",
"statusbar_time_hours": "{{time}} 小時前同步",
"statusbar_time_minutes": "{{time}} 分鐘前同步",
"statusbar_time_lessminute": "一分鐘之內同步",
"statusbar_lastsync": "上一次同步於:{{time}}",
"statusbar_syncing": "正在同步",
"statusbar_now": "剛同步完",
"statusbar_lastsync_label": "上一次同步於:{{date}}",
"statusbar_lastsync_never": "沒觸發過同步",
"statusbar_lastsync_never_label": "沒觸發過同步",
@@ -163,7 +165,7 @@
"settings_s3_prod3": "設定 CORS",
"settings_s3_endpoint": "服務地址(Endpoint",
"settings_s3_region": "區域(Region",
"settings_s3_region_desc": "如果您不知道這個值填什麼,可以嘗試填寫此預設值us-east-1",
"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",
@@ -173,6 +175,8 @@
"settings_s3_bypasscorslocally_desc": "對於 Obsidian 新版本(桌面版>=0.13.25 或 iOS>=1.1.1 或 Android>=1.2.1),本外掛可以跳過伺服器設定 CORS 的步驟。如果您遇到任意問題,可以關閉此設定,並在服務端設定 CORS(允許來自 app://obsidian.md 和 capacitor://localhost 和 http://localhost 的請求且增加 ETag 到暴露 headers 裡)。",
"settings_s3_parts": "分塊並行度",
"settings_s3_parts_desc": "在 S3 裡,大檔案會被分塊上傳。您希望同一時間最多有多少個分塊被上傳?",
"settings_s3_accuratemtime": "使用準確的檔案修改時間",
"settings_s3_accuratemtime_desc": "讀取(已上傳的)準確的檔案修改時間,有助於同步演算法更加準確和穩定。但是它也會導致額外的 api 請求、時間、金錢花費。",
"settings_s3_urlstyle": "S3 URL style",
"settings_s3_urlstyle_desc": "是否對 S3 物件強制使用 path style URL(例如使用 https://s3.amazonaws.com/*/ 而不是 https://*.s3.amazonaws.com/)。",
"settings_s3_connect_succ": "很好!可以訪問到對應儲存桶。",
@@ -222,8 +226,7 @@
"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_desc": "Webdav 伺服器會被設為接收 Depth header 為“1”或“Infinity”請求。如果您不清楚如何設定,可以選擇 depth='1'。如果那你確認伺服器支援 depth='infinity',請選擇它,會獲得更加好的效能。",
"settings_webdav_depth_1": "只支援 depth='1'",
"settings_webdav_depth_inf": "支援 depth='infinity'",
"settings_webdav_connect_succ": "很好!可以連線上 Webdav 伺服器。",
@@ -260,27 +263,16 @@
"settings_outputsettingsconsole_button": "輸出",
"settings_outputsettingsconsole_notice": "已輸出到終端",
"settings_syncplans": "匯出同步計劃",
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。並且,您可以選擇匯出為內嵌在 Markdown 裡的 json 格式或表格格式,它們是等價的。",
"settings_syncplans_button_json": "匯出json",
"settings_syncplans_button_table": "匯出(表格)",
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
"settings_syncplans_button_json": "匯出",
"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_logtohttpserver": "臨時設定終端日誌實時轉發到 HTTP(S) 伺服器。",
"settings_logtohttpserver_desc": "非常危險,謹慎行動!!!!!臨時設定終端日誌實時轉發到 HTTP(S) 伺服器。",
"settings_logtohttpserver_reset_notice": "您的輸入不是“http(s)”開頭的。已移除了終端日誌轉發到 HTTP(S) 伺服器的設定。",
"settings_delsyncmap": "刪除資料庫裡的同步對映歷史",
"settings_delsyncmap_desc": "同步對映歷史儲存了本地真正的最後修改時間和遠端檔案時間的對映。刪除之可能會導致下一次同步時發生不必要的資料交換。點選按鈕刪除資料庫裡的同步對映歷史。",
"settings_delsyncmap_button": "刪除同步對映歷史",
+13 -94
View File
@@ -436,8 +436,8 @@ export const insertRenameRecordByVault = async (
vaultRandomID: string
) => {
// log.info(fileOrFolder);
let k1: FileFolderHistoryRecord;
let k2: FileFolderHistoryRecord;
let k1: FileFolderHistoryRecord | undefined;
let k2: FileFolderHistoryRecord | undefined;
const actionWhen = Date.now();
if (fileOrFolder instanceof TFile) {
k1 = {
@@ -473,8 +473,10 @@ export const insertRenameRecordByVault = async (
// TAbstractFile does not contain these info
// but from API_VER_STAT_FOLDER we can manually stat them by path.
const s = await statFix(fileOrFolder.vault, fileOrFolder.path);
ctime = s.ctime;
mtime = s.mtime;
if (s !== undefined && s !== null) {
ctime = s.ctime;
mtime = s.mtime;
}
}
k1 = {
key: key,
@@ -500,8 +502,8 @@ export const insertRenameRecordByVault = async (
};
}
await Promise.all([
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1.key}`, k1),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2.key}`, k2),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1!.key}`, k1),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2!.key}`, k2),
]);
};
@@ -608,13 +610,13 @@ export const readAllSyncPlanRecordTextsByVault = async (
};
/**
* We remove records that are older than 7 days or 10000 records.
* We remove records that are older than 3 days or 100 records.
* It's a heavy operation, so we shall not place it in the start up.
* @param db
*/
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 7; // 7 days
const COUNT_TO_MANY = 10000;
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 3; // 3 days
const COUNT_TO_MANY = 100;
const currTs = Date.now();
const expiredTs = currTs - MILLISECONDS_OLD;
@@ -634,7 +636,7 @@ export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
);
if (records.length - keysToRemove.size > COUNT_TO_MANY) {
// we need to find out records beyond 10000 records
// we need to find out records beyond 100 records
records = records.filter((x) => !x.expired); // shrink the array
records.sort((a, b) => -(a.ts - b.ts)); // descending
records.slice(COUNT_TO_MANY).forEach((element) => {
@@ -649,92 +651,9 @@ export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
await Promise.all(ps);
};
export const readAllLogRecordTextsByVault = async (
db: InternalDBs,
vaultRandomID: string
) => {
const records = [] as { ts: number; r: string }[];
await db.loggerOutputTbl.iterate((value, key, iterationNumber) => {
if (key.startsWith(`${vaultRandomID}\t`)) {
const item = {
ts: parseInt(key.split("\t")[1]),
r: value as string,
};
records.push(item);
}
});
// while reading the logs, we want it to be ascending
records.sort((a, b) => a.ts - b.ts);
if (records === undefined) {
return [] as string[];
} else {
return records.map((x) => x.r);
}
};
export const insertLoggerOutputByVault = async (
db: InternalDBs,
vaultRandomID: string,
...msg: any[]
) => {
const ts = Date.now();
const tsFmt = unixTimeToStr(ts);
const key = `${vaultRandomID}\t${ts}`;
try {
const val = [`[${tsFmt}]`, ...msg.map((x) => toText(x))].join(" ");
db.loggerOutputTbl.setItem(key, val);
} catch (err) {
// give up, and let it pass
}
};
export const clearAllLoggerOutputRecords = async (db: InternalDBs) => {
await db.loggerOutputTbl.clear();
};
/**
* We remove records that are older than 7 days or 10000 records.
* It's a heavy operation, so we shall not place it in the start up.
* @param db
*/
export const clearExpiredLoggerOutputRecords = async (db: InternalDBs) => {
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 7; // 7 days
const COUNT_TO_MANY = 10000;
const currTs = Date.now();
const expiredTs = currTs - MILLISECONDS_OLD;
let records = (await db.loggerOutputTbl.keys()).map((key) => {
const ts = parseInt(key.split("\t")[1]);
const expired = ts <= expiredTs;
return {
ts: ts,
key: key,
expired: expired,
};
});
const keysToRemove = new Set(
records.filter((x) => x.expired).map((x) => x.key)
);
if (records.length - keysToRemove.size > COUNT_TO_MANY) {
// we need to find out records beyond 10000 records
records = records.filter((x) => !x.expired); // shrink the array
records.sort((a, b) => -(a.ts - b.ts)); // descending
records.slice(COUNT_TO_MANY).forEach((element) => {
keysToRemove.add(element.key);
});
}
const ps = [] as Promise<void>[];
keysToRemove.forEach((element) => {
ps.push(db.loggerOutputTbl.removeItem(element));
});
await Promise.all(ps);
log.debug(`successfully clearAllLoggerOutputRecords`);
};
export const upsertLastSuccessSyncByVault = async (
+137 -116
View File
@@ -10,6 +10,7 @@ import {
TFile,
TFolder,
requestUrl,
requireApiVersion,
} from "obsidian";
import cloneDeep from "lodash/cloneDeep";
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
@@ -24,6 +25,7 @@ import {
COMMAND_CALLBACK_DROPBOX,
COMMAND_URI,
REMOTELY_SAVE_VERSION_2024PREPARE,
API_VER_ENSURE_REQURL_OK,
} from "./baseTypes";
import { importQrCodeUri } from "./importExport";
import {
@@ -33,12 +35,11 @@ import {
loadFileHistoryTableByVault,
prepareDBs,
InternalDBs,
insertLoggerOutputByVault,
clearExpiredLoggerOutputRecords,
clearExpiredSyncPlanRecords,
upsertLastSuccessSyncByVault,
getLastSuccessSyncByVault,
upsertPluginVersionByVault,
clearAllLoggerOutputRecords,
} from "./localdb";
import { RemoteClient } from "./remote";
import {
@@ -65,14 +66,10 @@ import type { LangType, LangTypeAndAuto, TransItemType } from "./i18n";
import { DeletionOnRemote, MetadataOnRemote } from "./metadataOnRemote";
import { SyncAlgoV2Modal } from "./syncAlgoV2Notice";
import { applyPresetRulesInplace } from "./presetRules";
import { applyLogWriterInplace, log } from "./moreOnLog";
import AggregateError from "aggregate-error";
import {
exportVaultLoggerOutputToFiles,
exportVaultSyncPlansToFiles,
} from "./debugMode";
import { exportVaultSyncPlansToFiles } from "./debugMode";
import { SizesConflictModal } from "./syncSizesConflictNotice";
import { compareVersion } from "./misc";
@@ -135,18 +132,18 @@ const getIconSvg = () => {
};
export default class RemotelySavePlugin extends Plugin {
settings: RemotelySavePluginSettings;
db: InternalDBs;
syncStatus: SyncStatusType;
statusBarElement: HTMLSpanElement;
oauth2Info: OAuth2Info;
currLogLevel: string;
settings!: RemotelySavePluginSettings;
db!: InternalDBs;
syncStatus!: SyncStatusType;
statusBarElement!: HTMLSpanElement;
oauth2Info!: OAuth2Info;
currLogLevel!: string;
currSyncMsg?: string;
syncRibbon?: HTMLElement;
autoRunIntervalID?: number;
syncOnSaveIntervalID?: number;
i18n: I18n;
vaultRandomID: string;
i18n!: I18n;
vaultRandomID!: string;
debugServerTemp?: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
@@ -177,7 +174,7 @@ export default class RemotelySavePlugin extends Plugin {
let originLabel = `${this.manifest.name}`;
if (this.syncRibbon !== undefined) {
originLabel = this.syncRibbon.getAttribute("aria-label");
originLabel = this.syncRibbon.getAttribute("aria-label") as string;
}
try {
@@ -206,6 +203,10 @@ export default class RemotelySavePlugin extends Plugin {
}
}
// change status to "syncing..." on statusbar
if (this.statusBarElement !== undefined) {
this.updateLastSuccessSyncMsg(-1);
}
//log.info(`huh ${this.settings.password}`)
if (this.settings.currLogLevel === "info") {
getNotice(
@@ -288,7 +289,8 @@ export default class RemotelySavePlugin extends Plugin {
this.db,
this.vaultRandomID
);
let localConfigDirContents: ObsConfigDirFileType[] = undefined;
let localConfigDirContents: ObsConfigDirFileType[] | undefined =
undefined;
if (this.settings.syncConfigDir) {
localConfigDirContents = await listFilesInObsFolder(
this.app.vault.configDir,
@@ -314,11 +316,11 @@ export default class RemotelySavePlugin extends Plugin {
client.serviceType,
triggerSource,
this.app.vault,
this.settings.syncConfigDir,
this.settings.syncConfigDir ?? false,
this.app.vault.configDir,
this.settings.syncUnderscoreItems,
this.settings.skipSizeLargerThan,
this.settings.ignorePaths,
this.settings.syncUnderscoreItems ?? false,
this.settings.skipSizeLargerThan ?? -1,
this.settings.ignorePaths ?? [],
this.settings.password
);
log.info(plan.mixedStates); // for debugging
@@ -334,6 +336,7 @@ export default class RemotelySavePlugin extends Plugin {
getNotice(t("syncrun_step7"));
}
this.syncStatus = "syncing";
await doActualSync(
client,
this.db,
@@ -352,7 +355,7 @@ export default class RemotelySavePlugin extends Plugin {
new SizesConflictModal(
self.app,
self,
this.settings.skipSizeLargerThan,
this.settings.skipSizeLargerThan ?? -1,
ss,
this.settings.password !== ""
).open();
@@ -399,7 +402,7 @@ export default class RemotelySavePlugin extends Plugin {
this.manifest.id
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
);
} catch (error) {
} catch (error: any) {
const msg = t("syncrun_abort", {
manifestID: this.manifest.id,
theDate: `${Date.now()}`,
@@ -414,7 +417,7 @@ export default class RemotelySavePlugin extends Plugin {
getNotice(e.message, 10 * 1000);
}
} else {
getNotice(error.message, 10 * 1000);
getNotice(error?.message ?? "error while sync", 10 * 1000);
}
this.syncStatus = "idle";
if (this.syncRibbon !== undefined) {
@@ -444,10 +447,9 @@ export default class RemotelySavePlugin extends Plugin {
this.currSyncMsg = "";
await this.loadSettings();
await this.checkIfPresetRulesFollowed();
// lang should be load early, but after settings
this.i18n = new I18n(this.settings.lang, async (lang: LangTypeAndAuto) => {
this.i18n = new I18n(this.settings.lang!, async (lang: LangTypeAndAuto) => {
this.settings.lang = lang;
await this.saveSettings();
});
@@ -477,8 +479,11 @@ export default class RemotelySavePlugin extends Plugin {
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
} catch (err) {
new Notice(err.message, 10 * 1000);
} catch (err: any) {
new Notice(
err?.message ?? "error of prepareDBAndVaultRandomID",
10 * 1000
);
throw err;
}
@@ -568,29 +573,41 @@ export default class RemotelySavePlugin extends Plugin {
this.registerObsidianProtocolHandler(
COMMAND_CALLBACK_DROPBOX,
async (inputParams) => {
if (inputParams.code !== undefined) {
if (
inputParams.code !== undefined &&
this.oauth2Info?.verifier !== undefined
) {
if (this.oauth2Info.helperModal !== undefined) {
this.oauth2Info.helperModal.contentEl.empty();
const k = this.oauth2Info.helperModal.contentEl;
k.empty();
t("protocol_dropbox_connecting")
.split("\n")
.forEach((val) => {
this.oauth2Info.helperModal.contentEl.createEl("p", {
k.createEl("p", {
text: val,
});
});
} else {
new Notice(t("protocol_dropbox_no_modal"));
return;
}
let authRes = await sendAuthReqDropbox(
this.settings.dropbox.clientID,
this.oauth2Info.verifier,
inputParams.code
inputParams.code,
async (e: any) => {
new Notice(t("protocol_dropbox_connect_fail"));
new Notice(`${e}`);
throw e;
}
);
const self = this;
setConfigBySuccessfullAuthInplaceDropbox(
this.settings.dropbox,
authRes,
authRes!,
() => self.saveSettings()
);
@@ -649,14 +666,18 @@ export default class RemotelySavePlugin extends Plugin {
this.registerObsidianProtocolHandler(
COMMAND_CALLBACK_ONEDRIVE,
async (inputParams) => {
if (inputParams.code !== undefined) {
if (
inputParams.code !== undefined &&
this.oauth2Info?.verifier !== undefined
) {
if (this.oauth2Info.helperModal !== undefined) {
this.oauth2Info.helperModal.contentEl.empty();
const k = this.oauth2Info.helperModal.contentEl;
k.empty();
t("protocol_onedrive_connecting")
.split("\n")
.forEach((val) => {
this.oauth2Info.helperModal.contentEl.createEl("p", {
k.createEl("p", {
text: val,
});
});
@@ -666,10 +687,16 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.onedrive.clientID,
this.settings.onedrive.authority,
inputParams.code,
this.oauth2Info.verifier
this.oauth2Info.verifier,
async (e: any) => {
new Notice(t("protocol_onedrive_connect_fail"));
new Notice(`${e}`);
return; // throw?
}
);
if ((rsp as any).error !== undefined) {
new Notice(`${JSON.stringify(rsp)}`);
throw Error(`${JSON.stringify(rsp)}`);
}
@@ -734,7 +761,7 @@ export default class RemotelySavePlugin extends Plugin {
if (!Platform.isMobileApp && this.settings.enableStatusBarInfo === true) {
const statusBarItem = this.addStatusBarItem();
this.statusBarElement = statusBarItem.createEl("span");
this.statusBarElement.setAttribute("aria-label-position", "top");
this.statusBarElement.setAttribute("data-tooltip-position", "top");
this.updateLastSuccessSyncMsg(
await getLastSuccessSyncByVault(this.db, this.vaultRandomID)
@@ -773,41 +800,11 @@ export default class RemotelySavePlugin extends Plugin {
icon: iconNameLogs,
callback: async () => {
await exportVaultSyncPlansToFiles(
this.db,
this.app.vault,
this.vaultRandomID,
"json"
);
new Notice(t("settings_syncplans_notice"));
},
});
this.addCommand({
id: "export-sync-plans-table",
name: t("command_exportsyncplans_table"),
icon: iconNameLogs,
callback: async () => {
await exportVaultSyncPlansToFiles(
this.db,
this.app.vault,
this.vaultRandomID,
"table"
);
new Notice(t("settings_syncplans_notice"));
},
});
this.addCommand({
id: "export-logs-in-db",
name: t("command_exportlogsindb"),
icon: iconNameLogs,
callback: async () => {
await exportVaultLoggerOutputToFiles(
this.db,
this.app.vault,
this.vaultRandomID
);
new Notice(t("settings_logtodbexport_notice"));
new Notice(t("settings_syncplans_notice"));
},
});
@@ -842,7 +839,13 @@ export default class RemotelySavePlugin extends Plugin {
this.syncRibbon = undefined;
if (this.oauth2Info !== undefined) {
this.oauth2Info.helperModal = undefined;
this.oauth2Info = undefined;
this.oauth2Info = {
verifier: "",
helperModal: undefined,
authDiv: undefined,
revokeDiv: undefined,
revokeAuthSetting: undefined,
};
}
}
@@ -868,10 +871,18 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.onedrive.remoteBaseDir = "";
}
if (this.settings.webdav.manualRecursive === undefined) {
this.settings.webdav.manualRecursive = false;
this.settings.webdav.manualRecursive = true;
}
if (this.settings.webdav.depth === undefined) {
this.settings.webdav.depth = "auto_unknown";
if (
this.settings.webdav.depth === undefined ||
this.settings.webdav.depth === "auto" ||
this.settings.webdav.depth === "auto_1" ||
this.settings.webdav.depth === "auto_infinity" ||
this.settings.webdav.depth === "auto_unknown"
) {
// auto is deprecated as of 20240116
this.settings.webdav.depth = "manual_1";
this.settings.webdav.manualRecursive = true;
}
if (this.settings.webdav.remoteBaseDir === undefined) {
this.settings.webdav.remoteBaseDir = "";
@@ -885,6 +896,10 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.s3.remotePrefix === undefined) {
this.settings.s3.remotePrefix = "";
}
if (this.settings.s3.useAccurateMTime === undefined) {
// it causes money, so disable it by default
this.settings.s3.useAccurateMTime = false;
}
if (this.settings.ignorePaths === undefined) {
this.settings.ignorePaths = [];
}
@@ -897,13 +912,13 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.deleteToWhere === undefined) {
this.settings.deleteToWhere = "system";
}
}
this.settings.logToDB = false; // deprecated as of 20240113
async checkIfPresetRulesFollowed() {
const res = applyPresetRulesInplace(this.settings);
if (res.changed) {
await this.saveSettings();
if (requireApiVersion(API_VER_ENSURE_REQURL_OK)) {
this.settings.s3.bypassCorsLocally = true; // deprecated as of 20240113
}
await this.saveSettings();
}
async saveSettings() {
@@ -939,7 +954,7 @@ export default class RemotelySavePlugin extends Plugin {
let dropboxExpired = false;
if (
this.settings.dropbox.refreshToken !== "" &&
current >= this.settings.dropbox.credentialsShouldBeDeletedAtTime
current >= this.settings!.dropbox!.credentialsShouldBeDeletedAtTime!
) {
dropboxExpired = true;
this.settings.dropbox = cloneDeep(DEFAULT_DROPBOX_CONFIG);
@@ -949,7 +964,7 @@ export default class RemotelySavePlugin extends Plugin {
let onedriveExpired = false;
if (
this.settings.onedrive.refreshToken !== "" &&
current >= this.settings.onedrive.credentialsShouldBeDeletedAtTime
current >= this.settings!.onedrive!.credentialsShouldBeDeletedAtTime!
) {
onedriveExpired = true;
this.settings.onedrive = cloneDeep(DEFAULT_ONEDRIVE_CONFIG);
@@ -1066,6 +1081,19 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.syncOnSaveAfterMilliseconds > 0
) {
let runScheduled = false;
let needToRunAgain = false;
const scheduleSyncOnSave = (scheduleTimeFromNow: number) => {
log.info(
`schedule a run for ${scheduleTimeFromNow} milliseconds later`
);
runScheduled = true;
setTimeout(() => {
this.syncRun("auto_sync_on_save");
runScheduled = false;
}, scheduleTimeFromNow);
};
this.app.workspace.onLayoutReady(() => {
const intervalID = window.setInterval(() => {
const currentFile = this.app.workspace.getActiveFile();
@@ -1076,29 +1104,28 @@ export default class RemotelySavePlugin extends Plugin {
// 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
this.settings!.syncOnSaveAfterMilliseconds!
) {
if (!runScheduled) {
if (
!needToRunAgain &&
!runScheduled &&
this.syncStatus === "idle"
) {
const scheduleTimeFromNow =
this.settings.syncOnSaveAfterMilliseconds -
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);
scheduleSyncOnSave(scheduleTimeFromNow);
} else if (
needToRunAgain &&
!runScheduled &&
this.syncStatus === "idle"
) {
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
needToRunAgain = false;
} else {
needToRunAgain = true;
}
}
}
@@ -1134,6 +1161,10 @@ export default class RemotelySavePlugin extends Plugin {
let lastSyncMsg = t("statusbar_lastsync_never");
let lastSyncLabelMsg = t("statusbar_lastsync_never_label");
if (lastSuccessSyncMillis !== undefined && lastSuccessSyncMillis === -1) {
lastSyncMsg = t("statusbar_syncing");
}
if (lastSuccessSyncMillis !== undefined && lastSuccessSyncMillis > 0) {
const deltaTime = Date.now() - lastSuccessSyncMillis;
@@ -1144,6 +1175,8 @@ export default class RemotelySavePlugin extends Plugin {
const days = Math.floor(deltaTime / 86400000);
const hours = Math.floor(deltaTime / 3600000);
const minutes = Math.floor(deltaTime / 60000);
const seconds = Math.floor(deltaTime / 1000);
let timeText = "";
if (years > 0) {
@@ -1158,8 +1191,10 @@ export default class RemotelySavePlugin extends Plugin {
timeText = t("statusbar_time_hours", { time: hours });
} else if (minutes > 0) {
timeText = t("statusbar_time_minutes", { time: minutes });
} else {
} else if (seconds > 30) {
timeText = t("statusbar_time_lessminute");
} else {
timeText = t("statusbar_now");
}
let dateText = new Date(lastSuccessSyncMillis).toLocaleTimeString(
@@ -1172,7 +1207,7 @@ export default class RemotelySavePlugin extends Plugin {
}
);
lastSyncMsg = t("statusbar_lastsync", { time: timeText });
lastSyncMsg = timeText;
lastSyncLabelMsg = t("statusbar_lastsync_label", { date: dateText });
}
@@ -1215,9 +1250,6 @@ export default class RemotelySavePlugin extends Plugin {
redirectLoggingOuputBasedOnSetting() {
applyLogWriterInplace((...msg: any[]) => {
if (this.settings.logToDB) {
insertLoggerOutputByVault(this.db, this.vaultRandomID, ...msg);
}
if (
this.debugServerTemp !== undefined &&
this.debugServerTemp.trim().startsWith("http")
@@ -1242,24 +1274,13 @@ export default class RemotelySavePlugin extends Plugin {
}
enableAutoClearOutputToDBHistIfSet() {
const initClearOutputToDBHistAfterMilliseconds = 1000 * 45;
const autoClearOutputToDBHistAfterMilliseconds = 1000 * 60 * 5;
const initClearOutputToDBHistAfterMilliseconds = 1000 * 30;
this.app.workspace.onLayoutReady(() => {
// init run
window.setTimeout(() => {
if (this.settings.logToDB) {
clearExpiredLoggerOutputRecords(this.db);
}
clearAllLoggerOutputRecords(this.db);
}, initClearOutputToDBHistAfterMilliseconds);
// scheduled run
const intervalID = window.setInterval(() => {
if (this.settings.logToDB) {
clearExpiredLoggerOutputRecords(this.db);
}
}, autoClearOutputToDBHistAfterMilliseconds);
this.registerInterval(intervalID);
});
}
+2 -2
View File
@@ -26,8 +26,8 @@ export interface MetadataOnRemote {
}
export const isEqualMetadataOnRemote = (
a: MetadataOnRemote,
b: MetadataOnRemote
a: MetadataOnRemote | undefined,
b: MetadataOnRemote | undefined
) => {
const m1 = a === undefined ? { deletions: [] } : a;
const m2 = b === undefined ? { deletions: [] } : b;
+12 -5
View File
@@ -126,8 +126,12 @@ export const base64ToArrayBuffer = (b64text: string) => {
* @returns
*/
export const hexStringToTypedArray = (hex: string) => {
const f = hex.match(/[\da-f]{2}/gi);
if (f === null) {
throw Error(`input ${hex} is not hex, no way to transform`);
}
return new Uint8Array(
hex.match(/[\da-f]{2}/gi).map(function (h) {
f.map(function (h) {
return parseInt(h, 16);
})
);
@@ -236,7 +240,7 @@ export const setToString = (a: Set<string>, delimiter: string = ",") => {
export const extractSvgSub = (x: string, subEl: string = "rect") => {
const parser = new window.DOMParser();
const dom = parser.parseFromString(x, "image/svg+xml");
const svg = dom.querySelector("svg");
const svg = dom.querySelector("svg")!;
svg.setAttribute("viewbox", "0 0 10 10");
return svg.innerHTML;
};
@@ -317,7 +321,7 @@ export const getTypeName = (obj: any) => {
* @param x
* @returns
*/
export const atWhichLevel = (x: string) => {
export const atWhichLevel = (x: string | undefined) => {
if (
x === undefined ||
x === "" ||
@@ -413,11 +417,14 @@ export const toText = (x: any) => {
*/
export const statFix = async (vault: Vault, path: string) => {
const s = await vault.adapter.stat(path);
if (s === undefined || s === null) {
return s;
}
if (s.ctime === undefined || s.ctime === null || Number.isNaN(s.ctime)) {
s.ctime = undefined;
s.ctime = undefined as any; // force assignment
}
if (s.mtime === undefined || s.mtime === null || Number.isNaN(s.mtime)) {
s.mtime = undefined;
s.mtime = undefined as any; // force assignment
}
if (
(s.size === undefined || s.size === null || Number.isNaN(s.size)) &&
+6 -3
View File
@@ -53,9 +53,9 @@ export const listFilesInObsFolder = async (
const CHUNK_SIZE = 10;
const contents: ObsConfigDirFileType[] = [];
while (q.length > 0) {
const itemsToFetch = [];
const itemsToFetch: string[] = [];
while (q.length > 0) {
itemsToFetch.push(q.pop());
itemsToFetch.push(q.pop()!);
}
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
@@ -63,8 +63,11 @@ export const listFilesInObsFolder = async (
const r = singleChunk.map(async (x) => {
const statRes = await statFix(vault, x);
if (statRes === undefined || statRes === null) {
throw Error("something goes wrong while listing hidden folder");
}
const isFolder = statRes.type === "folder";
let children: ListedFiles = undefined;
let children: ListedFiles | undefined = undefined;
if (isFolder) {
children = await vault.adapter.list(x);
}
-69
View File
@@ -1,69 +0,0 @@
import type {
RemotelySavePluginSettings,
WebdavConfig,
WebdavDepthType,
} from "./baseTypes";
const RULES = {
webdav: {
depth: [
{
url: "^https://(.+).teracloud.jp/.+",
depth: "auto_1",
manualRecursive: true,
},
{
url: "^https://dav.jianguoyun.com/dav/",
depth: "auto_1",
manualRecursive: true,
},
],
},
};
export const applyWebdavPresetRulesInplace = (
webdav: Partial<WebdavConfig> | undefined
) => {
if (webdav === undefined) {
return {
changed: false,
webdav: webdav,
};
}
for (const { url, depth, manualRecursive } of RULES.webdav.depth) {
if (
webdav.address !== undefined &&
new RegExp(url).test(webdav.address) &&
webdav.depth !== undefined &&
webdav.depth.startsWith("auto_") &&
webdav.depth !== depth
) {
webdav.depth = depth as WebdavDepthType;
webdav.manualRecursive = manualRecursive;
return {
changed: true,
webdav: webdav,
};
}
}
return {
changed: false,
webdav: webdav,
};
};
export const applyPresetRulesInplace = (
settings: RemotelySavePluginSettings | undefined
) => {
if (settings === undefined) {
return {
changed: false,
settings: settings,
};
}
const webdavRes = applyWebdavPresetRulesInplace(settings.webdav);
return {
changed: webdavRes.changed,
settings: settings,
};
};
+40 -40
View File
@@ -43,10 +43,10 @@ export class RemoteClient {
"remember to provide vault name and callback while init webdav client"
);
}
const remoteBaseDir = webdavConfig.remoteBaseDir || vaultName;
const remoteBaseDir = webdavConfig!.remoteBaseDir || vaultName;
this.webdavConfig = webdavConfig;
this.webdavClient = webdav.getWebdavClient(
this.webdavConfig,
this.webdavConfig!,
remoteBaseDir,
saveUpdatedConfigFunc
);
@@ -56,10 +56,10 @@ export class RemoteClient {
"remember to provide vault name and callback while init dropbox client"
);
}
const remoteBaseDir = dropboxConfig.remoteBaseDir || vaultName;
const remoteBaseDir = dropboxConfig!.remoteBaseDir || vaultName;
this.dropboxConfig = dropboxConfig;
this.dropboxClient = dropbox.getDropboxClient(
this.dropboxConfig,
this.dropboxConfig!,
remoteBaseDir,
saveUpdatedConfigFunc
);
@@ -69,10 +69,10 @@ export class RemoteClient {
"remember to provide vault name and callback while init onedrive client"
);
}
const remoteBaseDir = onedriveConfig.remoteBaseDir || vaultName;
const remoteBaseDir = onedriveConfig!.remoteBaseDir || vaultName;
this.onedriveConfig = onedriveConfig;
this.onedriveClient = onedrive.getOnedriveClient(
this.onedriveConfig,
this.onedriveConfig!,
remoteBaseDir,
saveUpdatedConfigFunc
);
@@ -84,17 +84,17 @@ export class RemoteClient {
getRemoteMeta = async (fileOrFolderPath: string) => {
if (this.serviceType === "s3") {
return await s3.getRemoteMeta(
s3.getS3Client(this.s3Config),
this.s3Config,
s3.getS3Client(this.s3Config!),
this.s3Config!,
fileOrFolderPath
);
} else if (this.serviceType === "webdav") {
return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath);
return await webdav.getRemoteMeta(this.webdavClient!, fileOrFolderPath);
} else if (this.serviceType === "dropbox") {
return await dropbox.getRemoteMeta(this.dropboxClient, fileOrFolderPath);
return await dropbox.getRemoteMeta(this.dropboxClient!, fileOrFolderPath);
} else if (this.serviceType === "onedrive") {
return await onedrive.getRemoteMeta(
this.onedriveClient,
this.onedriveClient!,
fileOrFolderPath
);
} else {
@@ -104,7 +104,7 @@ export class RemoteClient {
uploadToRemote = async (
fileOrFolderPath: string,
vault: Vault,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
@@ -114,8 +114,8 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.uploadToRemote(
s3.getS3Client(this.s3Config),
this.s3Config,
s3.getS3Client(this.s3Config!),
this.s3Config!,
fileOrFolderPath,
vault,
isRecursively,
@@ -126,7 +126,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "webdav") {
return await webdav.uploadToRemote(
this.webdavClient,
this.webdavClient!,
fileOrFolderPath,
vault,
isRecursively,
@@ -137,7 +137,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "dropbox") {
return await dropbox.uploadToRemote(
this.dropboxClient,
this.dropboxClient!,
fileOrFolderPath,
vault,
isRecursively,
@@ -149,7 +149,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "onedrive") {
return await onedrive.uploadToRemote(
this.onedriveClient,
this.onedriveClient!,
fileOrFolderPath,
vault,
isRecursively,
@@ -167,15 +167,15 @@ export class RemoteClient {
listAllFromRemote = async () => {
if (this.serviceType === "s3") {
return await s3.listAllFromRemote(
s3.getS3Client(this.s3Config),
this.s3Config
s3.getS3Client(this.s3Config!),
this.s3Config!
);
} else if (this.serviceType === "webdav") {
return await webdav.listAllFromRemote(this.webdavClient);
return await webdav.listAllFromRemote(this.webdavClient!);
} else if (this.serviceType === "dropbox") {
return await dropbox.listAllFromRemote(this.dropboxClient);
return await dropbox.listAllFromRemote(this.dropboxClient!);
} else if (this.serviceType === "onedrive") {
return await onedrive.listAllFromRemote(this.onedriveClient);
return await onedrive.listAllFromRemote(this.onedriveClient!);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
@@ -191,8 +191,8 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.downloadFromRemote(
s3.getS3Client(this.s3Config),
this.s3Config,
s3.getS3Client(this.s3Config!),
this.s3Config!,
fileOrFolderPath,
vault,
mtime,
@@ -202,7 +202,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "webdav") {
return await webdav.downloadFromRemote(
this.webdavClient,
this.webdavClient!,
fileOrFolderPath,
vault,
mtime,
@@ -212,7 +212,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "dropbox") {
return await dropbox.downloadFromRemote(
this.dropboxClient,
this.dropboxClient!,
fileOrFolderPath,
vault,
mtime,
@@ -222,7 +222,7 @@ export class RemoteClient {
);
} else if (this.serviceType === "onedrive") {
return await onedrive.downloadFromRemote(
this.onedriveClient,
this.onedriveClient!,
fileOrFolderPath,
vault,
mtime,
@@ -242,29 +242,29 @@ export class RemoteClient {
) => {
if (this.serviceType === "s3") {
return await s3.deleteFromRemote(
s3.getS3Client(this.s3Config),
this.s3Config,
s3.getS3Client(this.s3Config!),
this.s3Config!,
fileOrFolderPath,
password,
remoteEncryptedKey
);
} else if (this.serviceType === "webdav") {
return await webdav.deleteFromRemote(
this.webdavClient,
this.webdavClient!,
fileOrFolderPath,
password,
remoteEncryptedKey
);
} else if (this.serviceType === "dropbox") {
return await dropbox.deleteFromRemote(
this.dropboxClient,
this.dropboxClient!,
fileOrFolderPath,
password,
remoteEncryptedKey
);
} else if (this.serviceType === "onedrive") {
return await onedrive.deleteFromRemote(
this.onedriveClient,
this.onedriveClient!,
fileOrFolderPath,
password,
remoteEncryptedKey
@@ -277,17 +277,17 @@ export class RemoteClient {
checkConnectivity = async (callbackFunc?: any) => {
if (this.serviceType === "s3") {
return await s3.checkConnectivity(
s3.getS3Client(this.s3Config),
this.s3Config,
s3.getS3Client(this.s3Config!),
this.s3Config!,
callbackFunc
);
} else if (this.serviceType === "webdav") {
return await webdav.checkConnectivity(this.webdavClient, callbackFunc);
return await webdav.checkConnectivity(this.webdavClient!, callbackFunc);
} else if (this.serviceType === "dropbox") {
return await dropbox.checkConnectivity(this.dropboxClient, callbackFunc);
return await dropbox.checkConnectivity(this.dropboxClient!, callbackFunc);
} else if (this.serviceType === "onedrive") {
return await onedrive.checkConnectivity(
this.onedriveClient,
this.onedriveClient!,
callbackFunc
);
} else {
@@ -297,9 +297,9 @@ export class RemoteClient {
getUser = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.getUserDisplayName(this.dropboxClient);
return await dropbox.getUserDisplayName(this.dropboxClient!);
} else if (this.serviceType === "onedrive") {
return await onedrive.getUserDisplayName(this.onedriveClient);
return await onedrive.getUserDisplayName(this.onedriveClient!);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
@@ -307,7 +307,7 @@ export class RemoteClient {
revokeAuth = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.revokeAuth(this.dropboxClient);
return await dropbox.revokeAuth(this.dropboxClient!);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
+113 -72
View File
@@ -24,7 +24,7 @@ import { log } from "./moreOnLog";
export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
accessToken: "",
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
clientID: process.env.DEFAULT_DROPBOX_APP_KEY ?? "",
refreshToken: "",
accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0,
@@ -41,9 +41,12 @@ export const getDropboxPath = (
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `/${remoteBaseDir}`;
}
if (!fileOrFolderPath.startsWith("/")) {
// then this is original path in Obsidian
} else if (fileOrFolderPath.startsWith("/")) {
log.warn(
`why the path ${fileOrFolderPath} starts with '/'? but we just go on.`
);
key = `/${remoteBaseDir}${fileOrFolderPath}`;
} else {
key = `/${remoteBaseDir}/${fileOrFolderPath}`;
}
if (key.endsWith("/")) {
@@ -73,7 +76,7 @@ const fromDropboxItemToRemoteItem = (
| files.DeletedMetadataReference,
remoteBaseDir: string
): RemoteItem => {
let key = getNormPath(x.path_display, remoteBaseDir);
let key = getNormPath(x.path_display!, remoteBaseDir);
if (x[".tag"] === "folder" && !key.endsWith("/")) {
key = `${key}/`;
}
@@ -87,14 +90,19 @@ const fromDropboxItemToRemoteItem = (
etag: `${x.id}\t`,
} as RemoteItem;
} else if (x[".tag"] === "file") {
let mtime = Date.parse(x.client_modified).valueOf();
if (mtime === 0) {
mtime = Date.parse(x.server_modified).valueOf();
}
return {
key: key,
lastModified: Date.parse(x.server_modified).valueOf(),
lastModified: mtime,
size: x.size,
remoteType: "dropbox",
etag: `${x.id}\t${x.content_hash}`,
} as RemoteItem;
} else if (x[".tag"] === "deleted") {
} else {
// x[".tag"] === "deleted"
throw Error("do not support deleted tag");
}
};
@@ -181,7 +189,7 @@ export const getAuthUrlAndVerifier = async (
: `obsidian://${COMMAND_CALLBACK_DROPBOX}`;
const authUrl = (
await auth.getAuthenticationUrl(
callback,
callback as any,
undefined,
"code",
"offline",
@@ -210,38 +218,51 @@ export interface DropboxSuccessAuthRes {
export const sendAuthReq = async (
appKey: string,
verifier: string,
authCode: string
authCode: string,
errorCallBack: any
) => {
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
code: authCode,
grant_type: "authorization_code",
code_verifier: verifier,
client_id: appKey,
redirect_uri: `obsidian://${COMMAND_CALLBACK_DROPBOX}`,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
return resp2;
try {
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
code: authCode,
grant_type: "authorization_code",
code_verifier: verifier,
client_id: appKey,
redirect_uri: `obsidian://${COMMAND_CALLBACK_DROPBOX}`,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
return resp2;
} catch (e) {
log.error(e);
if (errorCallBack !== undefined) {
await errorCallBack(e);
}
}
};
export const sendRefreshTokenReq = async (
appKey: string,
refreshToken: string
) => {
log.info("start auto getting refreshed Dropbox access token.");
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appKey,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
log.info("finish auto getting refreshed Dropbox access token.");
return resp2;
try {
log.info("start auto getting refreshed Dropbox access token.");
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appKey,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
log.info("finish auto getting refreshed Dropbox access token.");
return resp2;
} catch (e) {
log.error(e);
throw e;
}
};
export const setConfigBySuccessfullAuthInplace = async (
@@ -262,9 +283,7 @@ export const setConfigBySuccessfullAuthInplace = async (
if (authRes.refresh_token !== undefined) {
config.refreshToken = authRes.refresh_token;
}
if (authRes.refresh_token !== undefined) {
config.accountID = authRes.account_id;
config.accountID = authRes.account_id!;
}
if (saveUpdatedConfigFunc !== undefined) {
@@ -287,7 +306,7 @@ interface ErrSubType {
async function retryReq<T>(
reqFunc: () => Promise<DropboxResponse<T>>,
extraHint: string = ""
): Promise<DropboxResponse<T>> {
): Promise<DropboxResponse<T> | undefined> {
const waitSeconds = [1, 2, 4, 8]; // hard code exponential backoff
for (let idx = 0; idx < waitSeconds.length; ++idx) {
try {
@@ -349,7 +368,7 @@ export class WrappedDropboxClient {
dropboxConfig: DropboxConfig;
remoteBaseDir: string;
saveUpdatedConfigFunc: () => Promise<any>;
dropbox: Dropbox;
dropbox!: Dropbox;
vaultFolderExists: boolean;
constructor(
dropboxConfig: DropboxConfig,
@@ -457,38 +476,39 @@ export const getDropboxClient = (
export const getRemoteMeta = async (
client: WrappedDropboxClient,
fileOrFolderPath: string
remotePath: string
) => {
await client.init();
if (fileOrFolderPath === "" || fileOrFolderPath === "/") {
// filesGetMetadata doesn't support root folder
// we instead try to list files
// if no error occurs, we ensemble a fake result.
const rsp = await retryReq(() =>
client.dropbox.filesListFolder({
path: `/${client.remoteBaseDir}`,
recursive: false, // don't need to recursive here
})
);
if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp));
}
return {
key: fileOrFolderPath,
lastModified: undefined,
size: 0,
remoteType: "dropbox",
etag: undefined,
} as RemoteItem;
}
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
// if (remotePath === "" || remotePath === "/") {
// // filesGetMetadata doesn't support root folder
// // we instead try to list files
// // if no error occurs, we ensemble a fake result.
// const rsp = await retryReq(() =>
// client.dropbox.filesListFolder({
// path: `/${client.remoteBaseDir}`,
// recursive: false, // don't need to recursive here
// })
// );
// if (rsp.status !== 200) {
// throw Error(JSON.stringify(rsp));
// }
// return {
// key: remotePath,
// lastModified: undefined,
// size: 0,
// remoteType: "dropbox",
// etag: undefined,
// } as RemoteItem;
// }
const rsp = await retryReq(() =>
client.dropbox.filesGetMetadata({
path: key,
path: remotePath,
})
);
if (rsp === undefined) {
throw Error("dropbox.filesGetMetadata undefinded");
}
if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp));
}
@@ -498,13 +518,15 @@ export const getRemoteMeta = async (
export const uploadToRemote = async (
client: WrappedDropboxClient,
fileOrFolderPath: string,
vault: Vault,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined,
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = ""
rawContent: string | ArrayBuffer = "",
rawContentMTime: number = 0,
rawContentCTime: number = 0
) => {
await client.init();
@@ -520,6 +542,15 @@ export const uploadToRemote = async (
);
}
let mtime = 0;
let ctime = 0;
const s = await vault?.adapter?.stat(fileOrFolderPath);
if (s !== undefined && s !== null) {
mtime = Math.round(s.mtime / 1000.0) * 1000;
ctime = Math.round(s.ctime / 1000.0) * 1000;
}
const mtimeStr = new Date(mtime).toISOString().replace(/\.\d{3}Z$/, "Z");
const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) {
@@ -565,6 +596,7 @@ export const uploadToRemote = async (
client.dropbox.filesUpload({
path: uploadFile,
contents: "",
client_modified: mtimeStr,
}),
fileOrFolderPath
);
@@ -581,6 +613,11 @@ export const uploadToRemote = async (
localContent = rawContent;
}
} else {
if (vault === undefined) {
throw new Error(
`the vault variable is not passed but we want to read ${fileOrFolderPath} for Dropbox`
);
}
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
@@ -598,6 +635,7 @@ export const uploadToRemote = async (
mode: {
".tag": "overwrite",
},
client_modified: mtimeStr,
}),
fileOrFolderPath
);
@@ -659,17 +697,19 @@ export const listAllFromRemote = async (client: WrappedDropboxClient) => {
const downloadFromRemoteRaw = async (
client: WrappedDropboxClient,
fileOrFolderPath: string
remotePath: string
) => {
await client.init();
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await retryReq(
() =>
client.dropbox.filesDownload({
path: key,
path: remotePath,
}),
fileOrFolderPath
`downloadFromRemoteRaw=${remotePath}`
);
if (rsp === undefined) {
throw Error(`unknown rsp from dropbox download: ${rsp}`);
}
if ((rsp.result as any).fileBlob !== undefined) {
// we get a Blob
const content = (rsp.result as any).fileBlob as Blob;
@@ -752,8 +792,8 @@ export const deleteFromRemote = async (
fileOrFolderPath
);
} catch (err) {
console.error("some error while deleting");
console.error(err);
log.error("some error while deleting");
log.error(err);
}
};
@@ -762,7 +802,8 @@ export const checkConnectivity = async (
callbackFunc?: any
) => {
try {
const results = await getRemoteMeta(client, "/");
await client.init();
const results = await getRemoteMeta(client, `/${client.remoteBaseDir}`);
if (results === undefined) {
return false;
}
+161 -67
View File
@@ -2,6 +2,7 @@ import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
import { AuthenticationProvider } from "@microsoft/microsoft-graph-client";
import type {
DriveItem,
FileSystemInfo,
UploadSession,
User,
} from "@microsoft/microsoft-graph-types";
@@ -30,8 +31,8 @@ const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = {
accessToken: "",
clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID,
authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY,
clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID ?? "",
authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY ?? "",
refreshToken: "",
accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0,
@@ -105,7 +106,8 @@ export const sendAuthReq = async (
clientID: string,
authority: string,
authCode: string,
verifier: string
verifier: string,
errorCallBack: any
) => {
// // original code snippets for references
// const authResponse = await pca.acquireTokenByCode({
@@ -123,28 +125,33 @@ export const sendAuthReq = async (
// instead of using msal
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow
const rsp1 = await request({
url: `${authority}/oauth2/v2.0/token`,
method: "POST",
contentType: "application/x-www-form-urlencoded",
body: new URLSearchParams({
tenant: "consumers",
client_id: clientID,
scope: SCOPES.join(" "),
code: authCode,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
code_verifier: verifier,
}).toString(),
});
try {
const rsp1 = await request({
url: `${authority}/oauth2/v2.0/token`,
method: "POST",
contentType: "application/x-www-form-urlencoded",
body: new URLSearchParams({
tenant: "consumers",
client_id: clientID,
scope: SCOPES.join(" "),
code: authCode,
redirect_uri: REDIRECT_URI,
grant_type: "authorization_code",
code_verifier: verifier,
}).toString(),
});
const rsp2 = JSON.parse(rsp1);
// log.info(rsp2);
const rsp2 = JSON.parse(rsp1);
// log.info(rsp2);
if (rsp2.error !== undefined) {
return rsp2 as AccessCodeResponseFailedType;
} else {
return rsp2 as AccessCodeResponseSuccessfulType;
if (rsp2.error !== undefined) {
return rsp2 as AccessCodeResponseFailedType;
} else {
return rsp2 as AccessCodeResponseSuccessfulType;
}
} catch (e) {
log.error(e);
await errorCallBack(e);
}
};
@@ -154,26 +161,31 @@ export const sendRefreshTokenReq = async (
refreshToken: string
) => {
// also use Obsidian request to bypass CORS issue.
const rsp1 = await request({
url: `${authority}/oauth2/v2.0/token`,
method: "POST",
contentType: "application/x-www-form-urlencoded",
body: new URLSearchParams({
tenant: "consumers",
client_id: clientID,
scope: SCOPES.join(" "),
refresh_token: refreshToken,
grant_type: "refresh_token",
}).toString(),
});
try {
const rsp1 = await request({
url: `${authority}/oauth2/v2.0/token`,
method: "POST",
contentType: "application/x-www-form-urlencoded",
body: new URLSearchParams({
tenant: "consumers",
client_id: clientID,
scope: SCOPES.join(" "),
refresh_token: refreshToken,
grant_type: "refresh_token",
}).toString(),
});
const rsp2 = JSON.parse(rsp1);
// log.info(rsp2);
const rsp2 = JSON.parse(rsp1);
// log.info(rsp2);
if (rsp2.error !== undefined) {
return rsp2 as AccessCodeResponseFailedType;
} else {
return rsp2 as AccessCodeResponseSuccessfulType;
if (rsp2.error !== undefined) {
return rsp2 as AccessCodeResponseFailedType;
} else {
return rsp2 as AccessCodeResponseSuccessfulType;
}
} catch (e) {
log.error(e);
throw e;
}
};
@@ -187,7 +199,7 @@ export const setConfigBySuccessfullAuthInplace = async (
config.accessTokenExpiresAtTime =
Date.now() + authRes.expires_in - 5 * 60 * 1000;
config.accessTokenExpiresInSeconds = authRes.expires_in;
config.refreshToken = authRes.refresh_token;
config.refreshToken = authRes.refresh_token!;
// manually set it expired after 80 days;
config.credentialsShouldBeDeletedAtTime =
@@ -207,10 +219,6 @@ export const setConfigBySuccessfullAuthInplace = async (
const getOnedrivePath = (fileOrFolderPath: string, remoteBaseDir: string) => {
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/special-folders-appfolder?view=odsp-graph-online
const prefix = `/drive/special/approot:/${remoteBaseDir}`;
if (fileOrFolderPath.startsWith(prefix)) {
// already transformed, return as is
return fileOrFolderPath;
}
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
@@ -221,7 +229,12 @@ const getOnedrivePath = (fileOrFolderPath: string, remoteBaseDir: string) => {
key = key.slice(0, key.length - 1);
}
key = `${prefix}/${key}`;
if (key.startsWith("/")) {
log.warn(`why the path ${key} starts with '/'? but we just go on.`);
key = `${prefix}${key}`;
} else {
key = `${prefix}/${key}`;
}
return key;
};
@@ -243,7 +256,9 @@ const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
};
const constructFromDriveItemToRemoteItemError = (x: DriveItem) => {
return `parentPath="${x.parentReference.path}", selfName="${x.name}"`;
return `parentPath="${
x.parentReference?.path ?? "(no parentReference or path)"
}", selfName="${x.name}"`;
};
const fromDriveItemToRemoteItem = (
@@ -260,14 +275,28 @@ const fromDriveItemToRemoteItem = (
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
// another possibile prefix
const THIRD_COMMON_PREFIX_RAW = `/drive/items/`;
// another report, why???
// /drive/root:/something/app/remotely-save/${remoteBaseDir}
const THIRD_COMMON_PREFIX_REGEX =
/^\/drive\/root:\/[^\/]+\/app\/remotely-save\//g;
// another possibile prefix
const FOURTH_COMMON_PREFIX_RAW = `/drive/items/`;
if (
x.parentReference === undefined ||
x.parentReference === null ||
x.parentReference.path === undefined ||
x.parentReference.path === null
) {
throw Error("x.parentReference.path is undefinded or null");
}
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
const matchSecondPrefixRes = fullPathOriginal.match(
SECOND_COMMON_PREFIX_REGEX
);
const matchThirdPrefixRes = fullPathOriginal.match(THIRD_COMMON_PREFIX_REGEX);
if (
matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`)
@@ -280,10 +309,21 @@ const fromDriveItemToRemoteItem = (
) {
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) {
} else if (
matchThirdPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchThirdPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(FOURTH_COMMON_PREFIX_RAW)) {
// it's something like
// /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
// with uri encoded!
if (x.name === undefined || x.name === null) {
throw Error(
`OneDrive item no name variable while matching ${FOURTH_COMMON_PREFIX_RAW}`
);
}
const parPath = decodeURIComponent(x.parentReference.path);
key = parPath.substring(parPath.indexOf(":") + 1);
if (key.startsWith(`/${remoteBaseDir}/`)) {
@@ -312,8 +352,8 @@ const fromDriveItemToRemoteItem = (
}
return {
key: key,
lastModified: Date.parse(x.fileSystemInfo.lastModifiedDateTime),
size: isFolder ? 0 : x.size,
lastModified: Date.parse(x!.fileSystemInfo!.lastModifiedDateTime!),
size: isFolder ? 0 : x.size!,
remoteType: "onedrive",
etag: x.cTag || "", // do NOT use x.eTag because it changes if meta changes
};
@@ -356,7 +396,7 @@ class MyAuthProvider implements AuthenticationProvider {
}
const r2 = r as AccessCodeResponseSuccessfulType;
this.onedriveConfig.accessToken = r2.access_token;
this.onedriveConfig.refreshToken = r2.refresh_token;
this.onedriveConfig.refreshToken = r2.refresh_token!;
this.onedriveConfig.accessTokenExpiresInSeconds = r2.expires_in;
this.onedriveConfig.accessTokenExpiresAtTime =
currentTs + r2.expires_in * 1000 - 60 * 2 * 1000;
@@ -638,10 +678,9 @@ export const listAllFromRemote = async (client: WrappedOnedriveClient) => {
export const getRemoteMeta = async (
client: WrappedOnedriveClient,
fileOrFolderPath: string
remotePath: string
) => {
await client.init();
const remotePath = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath=${remotePath}`);
const rsp = await client.getJson(
`${remotePath}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size`
@@ -656,7 +695,7 @@ export const getRemoteMeta = async (
export const uploadToRemote = async (
client: WrappedOnedriveClient,
fileOrFolderPath: string,
vault: Vault,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
@@ -673,6 +712,16 @@ export const uploadToRemote = async (
uploadFile = getOnedrivePath(uploadFile, client.remoteBaseDir);
log.debug(`uploadFile=${uploadFile}`);
let mtime = 0;
let ctime = 0;
const s = await vault?.adapter?.stat(fileOrFolderPath);
if (s !== undefined && s !== null) {
mtime = s.mtime;
ctime = s.ctime;
}
const ctimeStr = new Date(ctime).toISOString();
const mtimeStr = new Date(mtime).toISOString();
const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) {
@@ -689,10 +738,21 @@ export const uploadToRemote = async (
} else {
// https://stackoverflow.com/questions/56479865/creating-nested-folders-in-one-go-onedrive-api
// use PATCH to create folder recursively!!!
await client.patchJson(uploadFile, {
let k: any = {
folder: {},
"@microsoft.graph.conflictBehavior": "replace",
});
};
if (mtime !== 0 && ctime !== 0) {
k = {
folder: {},
"@microsoft.graph.conflictBehavior": "replace",
fileSystemInfo: {
lastModifiedDateTime: mtimeStr,
createdDateTime: ctimeStr,
} as FileSystemInfo,
};
}
await client.patchJson(uploadFile, k);
}
const res = await getRemoteMeta(client, uploadFile);
return res;
@@ -716,6 +776,14 @@ export const uploadToRemote = async (
})}`,
arrBufRandom
);
if (mtime !== 0 && ctime !== 0) {
await client.patchJson(`${uploadFile}`, {
fileSystemInfo: {
lastModifiedDateTime: mtimeStr,
createdDateTime: ctimeStr,
} as FileSystemInfo,
});
}
// log.info(uploadResult)
const res = await getRemoteMeta(client, uploadFile);
return res;
@@ -731,6 +799,11 @@ export const uploadToRemote = async (
localContent = rawContent;
}
} else {
if (vault === undefined) {
throw new Error(
`the vault variable is not passed but we want to read ${fileOrFolderPath} for OneDrive`
);
}
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
@@ -753,21 +826,43 @@ export const uploadToRemote = async (
})}`,
remoteContent
);
if (mtime !== 0 && ctime !== 0) {
await client.patchJson(`${uploadFile}`, {
fileSystemInfo: {
lastModifiedDateTime: mtimeStr,
createdDateTime: ctimeStr,
} as FileSystemInfo,
});
}
} else {
// upload large files!
// ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
// 1. create uploadSession
// uploadFile already starts with /drive/special/approot:/${remoteBaseDir}
const s: UploadSession = await client.postJson(
`${uploadFile}:/createUploadSession`,
{
let k: any = {
item: {
"@microsoft.graph.conflictBehavior": "replace",
},
};
if (mtime !== 0 && ctime !== 0) {
k = {
item: {
"@microsoft.graph.conflictBehavior": "replace",
// this is only possible using uploadSession
fileSystemInfo: {
lastModifiedDateTime: mtimeStr,
createdDateTime: ctimeStr,
} as FileSystemInfo,
},
}
};
}
const s: UploadSession = await client.postJson(
`${uploadFile}:/createUploadSession`,
k
);
const uploadUrl = s.uploadUrl;
const uploadUrl = s.uploadUrl!;
log.debug("uploadSession = ");
log.debug(s);
@@ -796,12 +891,11 @@ export const uploadToRemote = async (
const downloadFromRemoteRaw = async (
client: WrappedOnedriveClient,
fileOrFolderPath: string
remotePath: string
): Promise<ArrayBuffer> => {
await client.init();
const key = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.getJson(
`${key}?$select=@microsoft.graph.downloadUrl`
`${remotePath}?$select=@microsoft.graph.downloadUrl`
);
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
if (VALID_REQURL) {
+137 -21
View File
@@ -42,6 +42,7 @@ import {
export { S3Client } from "@aws-sdk/client-s3";
import { log } from "./moreOnLog";
import PQueue from "p-queue";
////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl
@@ -54,7 +55,7 @@ import { log } from "./moreOnLog";
* But this uses Obsidian requestUrl instead.
*/
class ObsHttpHandler extends FetchHttpHandler {
requestTimeoutInMs: number;
requestTimeoutInMs: number | undefined;
constructor(options?: FetchHttpHandlerOptions) {
super(options);
this.requestTimeoutInMs =
@@ -94,7 +95,7 @@ class ObsHttpHandler extends FetchHttpHandler {
transformedHeaders[keyLower] = request.headers[key];
}
let contentType: string = undefined;
let contentType: string | undefined = undefined;
if (transformedHeaders["content-type"] !== undefined) {
contentType = transformedHeaders["content-type"];
}
@@ -155,7 +156,7 @@ class ObsHttpHandler extends FetchHttpHandler {
// other stuffs
////////////////////////////////////////////////////////////////////////////////
export const DEFAULT_S3_CONFIG = {
export const DEFAULT_S3_CONFIG: S3Config = {
s3Endpoint: "",
s3Region: "",
s3AccessKeyID: "",
@@ -165,6 +166,7 @@ export const DEFAULT_S3_CONFIG = {
partsConcurrency: 20,
forcePathStyle: false,
remotePrefix: "",
useAccurateMTime: false, // it causes money, disable by default
};
export type S3ObjectType = _Object;
@@ -218,24 +220,47 @@ const getLocalNoPrefixPath = (
return fileOrFolderPathWithRemotePrefix.slice(`${remotePrefix}`.length);
};
const fromS3ObjectToRemoteItem = (x: S3ObjectType, remotePrefix: string) => {
return {
key: getLocalNoPrefixPath(x.Key, remotePrefix),
lastModified: x.LastModified.valueOf(),
size: x.Size,
const fromS3ObjectToRemoteItem = (
x: S3ObjectType,
remotePrefix: string,
mtimeRecords: Record<string, number>,
ctimeRecords: Record<string, number>
) => {
let mtime = x.LastModified!.valueOf();
if (x.Key! in mtimeRecords) {
const m2 = mtimeRecords[x.Key!];
if (m2 !== 0) {
mtime = m2;
}
}
const r: RemoteItem = {
key: getLocalNoPrefixPath(x.Key!, remotePrefix),
lastModified: mtime,
size: x.Size!,
remoteType: "s3",
etag: x.ETag,
} as RemoteItem;
};
return r;
};
const fromS3HeadObjectToRemoteItem = (
fileOrFolderPathWithRemotePrefix: string,
x: HeadObjectCommandOutput,
remotePrefix: string
remotePrefix: string,
useAccurateMTime: boolean
) => {
let mtime = x.LastModified!.valueOf();
if (useAccurateMTime && x.Metadata !== undefined) {
const m2 = Math.round(
parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0")
);
if (m2 !== 0) {
mtime = m2;
}
}
return {
key: getLocalNoPrefixPath(fileOrFolderPathWithRemotePrefix, remotePrefix),
lastModified: x.LastModified.valueOf(),
lastModified: mtime,
size: x.ContentLength,
remoteType: "s3",
etag: x.ETag,
@@ -292,6 +317,7 @@ export const getRemoteMeta = async (
fileOrFolderPathWithRemotePrefix: string
) => {
if (
s3Config.remotePrefix !== undefined &&
s3Config.remotePrefix !== "" &&
!fileOrFolderPathWithRemotePrefix.startsWith(s3Config.remotePrefix)
) {
@@ -307,7 +333,8 @@ export const getRemoteMeta = async (
return fromS3HeadObjectToRemoteItem(
fileOrFolderPathWithRemotePrefix,
res,
s3Config.remotePrefix
s3Config.remotePrefix ?? "",
s3Config.useAccurateMTime ?? false
);
};
@@ -315,18 +342,20 @@ export const uploadToRemote = async (
s3Client: S3Client,
s3Config: S3Config,
fileOrFolderPath: string,
vault: Vault,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
uploadRaw: boolean = false,
rawContent: string | ArrayBuffer = ""
rawContent: string | ArrayBuffer = "",
rawContentMTime: number = 0,
rawContentCTime: number = 0
) => {
let uploadFile = fileOrFolderPath;
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getRemoteWithPrefixPath(uploadFile, s3Config.remotePrefix);
uploadFile = getRemoteWithPrefixPath(uploadFile, s3Config.remotePrefix ?? "");
const isFolder = fileOrFolderPath.endsWith("/");
if (isFolder && isRecursively) {
@@ -336,6 +365,13 @@ export const uploadToRemote = async (
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
}
// folder
let mtime = 0;
let ctime = 0;
const s = await vault?.adapter?.stat(fileOrFolderPath);
if (s !== undefined && s !== null) {
mtime = s.mtime;
ctime = s.ctime;
}
const contentType = DEFAULT_CONTENT_TYPE;
await s3Client.send(
new PutObjectCommand({
@@ -343,6 +379,10 @@ export const uploadToRemote = async (
Key: uploadFile,
Body: "",
ContentType: contentType,
Metadata: {
MTime: `${mtime}`,
CTime: `${ctime}`,
},
})
);
return await getRemoteMeta(s3Client, s3Config, uploadFile);
@@ -357,14 +397,28 @@ export const uploadToRemote = async (
) || DEFAULT_CONTENT_TYPE;
}
let localContent = undefined;
let mtime = 0;
let ctime = 0;
if (uploadRaw) {
if (typeof rawContent === "string") {
localContent = new TextEncoder().encode(rawContent).buffer;
} else {
localContent = rawContent;
}
mtime = rawContentMTime;
ctime = rawContentCTime;
} else {
if (vault === undefined) {
throw new Error(
`the vault variable is not passed but we want to read ${fileOrFolderPath} for S3`
);
}
localContent = await vault.adapter.readBinary(fileOrFolderPath);
const s = await vault.adapter.stat(fileOrFolderPath);
if (s !== undefined && s !== null) {
mtime = s.mtime;
ctime = s.ctime;
}
}
let remoteContent = localContent;
if (password !== "") {
@@ -373,6 +427,7 @@ export const uploadToRemote = async (
const bytesIn5MB = 5242880;
const body = new Uint8Array(remoteContent);
const upload = new Upload({
client: s3Client,
queueSize: s3Config.partsConcurrency, // concurrency
@@ -383,6 +438,10 @@ export const uploadToRemote = async (
Key: uploadFile,
Body: body,
ContentType: contentType,
Metadata: {
MTime: `${mtime}`,
CTime: `${ctime}`,
},
},
});
upload.on("httpUploadProgress", (progress) => {
@@ -407,6 +466,17 @@ const listFromRemoteRaw = async (
}
const contents = [] as _Object[];
const mtimeRecords: Record<string, number> = {};
const ctimeRecords: Record<string, number> = {};
const queueHead = new PQueue({
concurrency: s3Config.partsConcurrency,
autoStart: true,
});
queueHead.on("error", (error) => {
queueHead.pause();
queueHead.clear();
throw error;
});
let isTruncated = true;
do {
@@ -420,7 +490,38 @@ const listFromRemoteRaw = async (
}
contents.push(...rsp.Contents);
isTruncated = rsp.IsTruncated;
if (s3Config.useAccurateMTime) {
// head requests of all objects, love it
for (const content of rsp.Contents) {
queueHead.add(async () => {
const rspHead = await s3Client.send(
new HeadObjectCommand({
Bucket: s3Config.s3BucketName,
Key: content.Key,
})
);
if (rspHead.$metadata.httpStatusCode !== 200) {
throw Error("some thing bad while heading single object!");
}
if (rspHead.Metadata === undefined) {
// pass
} else {
mtimeRecords[content.Key!] = Math.round(
parseFloat(
rspHead.Metadata.mtime || rspHead.Metadata.MTime || "0"
)
);
ctimeRecords[content.Key!] = Math.round(
parseFloat(
rspHead.Metadata.ctime || rspHead.Metadata.CTime || "0"
)
);
}
});
}
}
isTruncated = rsp.IsTruncated ?? false;
confCmd.ContinuationToken = rsp.NextContinuationToken;
if (
isTruncated &&
@@ -431,12 +532,20 @@ const listFromRemoteRaw = async (
}
} while (isTruncated);
// wait for any head requests
await queueHead.onIdle();
// 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, s3Config.remotePrefix)
fromS3ObjectToRemoteItem(
x,
s3Config.remotePrefix ?? "",
mtimeRecords,
ctimeRecords
)
),
};
};
@@ -456,8 +565,11 @@ export const listAllFromRemote = async (
* @returns Promise<ArrayBuffer>
*/
const getObjectBodyToArrayBuffer = async (
b: Readable | ReadableStream | Blob
b: Readable | ReadableStream | Blob | undefined
) => {
if (b === undefined) {
throw Error(`ObjectBody is undefined and don't know how to deal with it`);
}
if (b instanceof Readable) {
return (await new Promise((resolve, reject) => {
const chunks: Uint8Array[] = [];
@@ -480,6 +592,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPathWithRemotePrefix: string
) => {
if (
s3Config.remotePrefix !== undefined &&
s3Config.remotePrefix !== "" &&
!fileOrFolderPathWithRemotePrefix.startsWith(s3Config.remotePrefix)
) {
@@ -523,7 +636,10 @@ export const downloadFromRemote = async (
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getRemoteWithPrefixPath(downloadFile, s3Config.remotePrefix);
downloadFile = getRemoteWithPrefixPath(
downloadFile,
s3Config.remotePrefix ?? ""
);
const remoteContent = await downloadFromRemoteRaw(
s3Client,
s3Config,
@@ -565,7 +681,7 @@ export const deleteFromRemote = async (
}
remoteFileName = getRemoteWithPrefixPath(
remoteFileName,
s3Config.remotePrefix
s3Config.remotePrefix ?? ""
);
await s3Client.send(
new DeleteObjectCommand({
@@ -631,7 +747,7 @@ export const checkConnectivity = async (
return false;
}
return results.$metadata.httpStatusCode === 200;
} catch (err) {
} catch (err: any) {
log.debug(err);
if (callbackFunc !== undefined) {
if (s3Config.s3Endpoint.contains(s3Config.s3BucketName)) {
+115 -105
View File
@@ -19,6 +19,15 @@ import type {
// ResponseDataDetailed,
} from "webdav";
/**
* https://stackoverflow.com/questions/32850898/how-to-check-if-a-string-has-any-non-iso-8859-1-characters-with-javascript
* @param str
* @returns true if all are iso 8859 1 chars
*/
function onlyAscii(str: string) {
return !/[^\u0000-\u00ff]/g.test(str);
}
// @ts-ignore
import { getPatcher } from "webdav/dist/web/index.js";
if (VALID_REQURL) {
@@ -36,29 +45,49 @@ if (VALID_REQURL) {
method: options.method,
body: options.data as string | ArrayBuffer,
headers: transformedHeaders,
throw: false,
});
let contentType: string | undefined =
r.headers["Content-Type"] ||
r.headers["content-type"] ||
options.headers["Content-Type"] ||
options.headers["content-type"] ||
options.headers["Accept"] ||
options.headers["accept"];
r.headers["Content-Type"] || r.headers["content-type"];
if (options.headers !== undefined) {
contentType =
contentType ||
options.headers["Content-Type"] ||
options.headers["content-type"] ||
options.headers["Accept"] ||
options.headers["accept"];
}
if (contentType !== undefined) {
contentType = contentType.toLowerCase();
}
const rspHeaders = { ...r.headers };
console.log("rspHeaders");
console.log(rspHeaders);
for (let key in rspHeaders) {
if (rspHeaders.hasOwnProperty(key)) {
if (key === "content-disposition" || key === "Content-Disposition") {
// avoid the error:
// Failed to read the 'headers' property from 'ResponseInit': String contains non ISO-8859-1 code point.
// const possibleNonAscii = [
// "Content-Disposition",
// "X-Accel-Redirect",
// "X-Outfilename",
// "X-Sendfile"
// ];
// for (const p of possibleNonAscii) {
// if (key === p || key === p.toLowerCase()) {
// rspHeaders[key] = encodeURIComponent(rspHeaders[key]);
// }
// }
if (!onlyAscii(rspHeaders[key])) {
rspHeaders[key] = encodeURIComponent(rspHeaders[key]);
}
}
}
// console.log(`requesting url=${options.url}`);
// console.log(`contentType=${contentType}`);
// console.log(`rspHeaders=${JSON.stringify(rspHeaders)}`)
// log.info(`requesting url=${options.url}`);
// log.info(`contentType=${contentType}`);
// log.info(`rspHeaders=${JSON.stringify(rspHeaders)}`)
// let r2: Response = undefined;
// if (contentType.includes("xml")) {
@@ -71,9 +100,9 @@ if (VALID_REQURL) {
// contentType.includes("json") ||
// contentType.includes("javascript")
// ) {
// console.log('inside json branch');
// log.info('inside json branch');
// // const j = r.json;
// // console.log(j);
// // log.info(j);
// r2 = new Response(
// r.text, // yea, here is the text because Response constructor expects a text
// {
@@ -106,7 +135,7 @@ if (VALID_REQURL) {
// );
// }
let r2: Response = undefined;
let r2: Response | undefined = 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
@@ -138,8 +167,8 @@ export const DEFAULT_WEBDAV_CONFIG = {
username: "",
password: "",
authType: "basic",
manualRecursive: false,
depth: "auto_unknown",
manualRecursive: true,
depth: "manual_1",
remoteBaseDir: "",
} as WebdavConfig;
@@ -148,8 +177,12 @@ const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `/${remoteBaseDir}/`;
}
if (!fileOrFolderPath.startsWith("/")) {
} else if (fileOrFolderPath.startsWith("/")) {
log.warn(
`why the path ${fileOrFolderPath} starts with '/'? but we just go on.`
);
key = `/${remoteBaseDir}${fileOrFolderPath}`;
} else {
key = `/${remoteBaseDir}/${fileOrFolderPath}`;
}
return key;
@@ -189,7 +222,7 @@ const fromWebdavItemToRemoteItem = (x: FileStat, remoteBaseDir: string) => {
export class WrappedWebdavClient {
webdavConfig: WebdavConfig;
remoteBaseDir: string;
client: WebDAVClient;
client!: WebDAVClient;
vaultFolderExists: boolean;
saveUpdatedConfigFunc: () => Promise<any>;
constructor(
@@ -205,29 +238,30 @@ export class WrappedWebdavClient {
init = async () => {
// init client if not inited
if (this.client !== undefined) {
return;
}
const headers = {
"Cache-Control": "no-cache",
};
if (this.client === undefined) {
if (
this.webdavConfig.username !== "" &&
this.webdavConfig.password !== ""
) {
this.client = createClient(this.webdavConfig.address, {
username: this.webdavConfig.username,
password: this.webdavConfig.password,
headers: headers,
authType:
this.webdavConfig.authType === "digest"
? AuthType.Digest
: AuthType.Password,
});
} else {
log.info("no password");
this.client = createClient(this.webdavConfig.address, {
headers: headers,
});
}
if (
this.webdavConfig.username !== "" &&
this.webdavConfig.password !== ""
) {
this.client = createClient(this.webdavConfig.address, {
username: this.webdavConfig.username,
password: this.webdavConfig.password,
headers: headers,
authType:
this.webdavConfig.authType === "digest"
? AuthType.Digest
: AuthType.Password,
});
} else {
log.info("no password");
this.client = createClient(this.webdavConfig.address, {
headers: headers,
});
}
// check vault folder
@@ -247,56 +281,19 @@ export class WrappedWebdavClient {
}
// adjust depth parameter
if (this.webdavConfig.depth === "auto_unknown") {
let testPassed = false;
try {
const res = await this.client.customRequest(`/${this.remoteBaseDir}/`, {
method: "PROPFIND",
headers: {
Depth: "infinity",
Accept: "text/plain,application/xml",
},
// responseType: "text",
} as any);
if (res.status === 403) {
throw Error("not support Infinity, get 403");
} else {
testPassed = true;
this.webdavConfig.depth = "auto_infinity";
this.webdavConfig.manualRecursive = false;
}
} catch (error) {
testPassed = false;
}
if (!testPassed) {
try {
const res = await this.client.customRequest(
`/${this.remoteBaseDir}/`,
{
method: "PROPFIND",
headers: {
Depth: "1",
Accept: "text/plain,application/xml",
},
// responseType: "text",
} as any
);
testPassed = true;
this.webdavConfig.depth = "auto_1";
this.webdavConfig.manualRecursive = true;
} catch (error) {
testPassed = false;
}
}
if (testPassed) {
// the depth option has been changed
// save the setting
if (this.saveUpdatedConfigFunc !== undefined) {
await this.saveUpdatedConfigFunc();
log.info(
`webdav depth="auto_unknown" is changed to ${this.webdavConfig.depth}`
);
}
if (
this.webdavConfig.depth === "auto" ||
this.webdavConfig.depth === "auto_1" ||
this.webdavConfig.depth === "auto_infinity" ||
this.webdavConfig.depth === "auto_unknown"
) {
this.webdavConfig.depth = "manual_1";
this.webdavConfig.manualRecursive = true;
if (this.saveUpdatedConfigFunc !== undefined) {
await this.saveUpdatedConfigFunc();
log.info(
`webdav depth="auto_???" is changed to ${this.webdavConfig.depth}`
);
}
}
};
@@ -314,23 +311,29 @@ export const getWebdavClient = (
);
};
/**
*
* @param client
* @param remotePath It should be prefix-ed already
* @returns
*/
export const getRemoteMeta = async (
client: WrappedWebdavClient,
fileOrFolderPath: string
remotePath: string
) => {
await client.init();
const remotePath = getWebdavPath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath = ${remotePath}`);
log.debug(`getRemoteMeta remotePath = ${remotePath}`);
const res = (await client.client.stat(remotePath, {
details: false,
})) as FileStat;
log.debug(`getRemoteMeta res=${JSON.stringify(res)}`);
return fromWebdavItemToRemoteItem(res, client.remoteBaseDir);
};
export const uploadToRemote = async (
client: WrappedWebdavClient,
fileOrFolderPath: string,
vault: Vault,
vault: Vault | undefined,
isRecursively: boolean = false,
password: string = "",
remoteEncryptedKey: string = "",
@@ -382,6 +385,11 @@ export const uploadToRemote = async (
localContent = rawContent;
}
} else {
if (vault == undefined) {
throw new Error(
`the vault variable is not passed but we want to read ${fileOrFolderPath} for webdav`
);
}
localContent = await vault.adapter.readBinary(fileOrFolderPath);
}
let remoteContent = localContent;
@@ -410,7 +418,10 @@ export const listAllFromRemote = async (client: WrappedWebdavClient) => {
let contents = [] as FileStat[];
if (
client.webdavConfig.depth === "auto" ||
client.webdavConfig.depth === "auto_unknown" ||
client.webdavConfig.depth === "auto_1" ||
client.webdavConfig.depth === "auto_infinity" /* don't trust auto now */ ||
client.webdavConfig.depth === "manual_1"
) {
// the remote doesn't support infinity propfind,
@@ -418,9 +429,9 @@ export const listAllFromRemote = async (client: WrappedWebdavClient) => {
const q = new Queue([`/${client.remoteBaseDir}`]);
const CHUNK_SIZE = 10;
while (q.length > 0) {
const itemsToFetch = [];
const itemsToFetch: string[] = [];
while (q.length > 0) {
itemsToFetch.push(q.pop());
itemsToFetch.push(q.pop()!);
}
const itemsToFetchChunks = chunk(itemsToFetch, CHUNK_SIZE);
// log.debug(itemsToFetchChunks);
@@ -470,12 +481,11 @@ export const listAllFromRemote = async (client: WrappedWebdavClient) => {
const downloadFromRemoteRaw = async (
client: WrappedWebdavClient,
fileOrFolderPath: string
remotePath: string
) => {
await client.init();
const p = getWebdavPath(fileOrFolderPath, client.remoteBaseDir);
// console.log(`getWebdavPath=${p}`);
const buff = (await client.client.getFileContents(p)) as BufferLike;
// log.info(`getWebdavPath=${remotePath}`);
const buff = (await client.client.getFileContents(remotePath)) as BufferLike;
if (buff instanceof ArrayBuffer) {
return buff;
} else if (buff instanceof Buffer) {
@@ -514,7 +524,7 @@ export const downloadFromRemote = async (
downloadFile = remoteEncryptedKey;
}
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
// console.log(`downloadFile=${downloadFile}`);
// log.info(`downloadFile=${downloadFile}`);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
@@ -549,8 +559,8 @@ export const deleteFromRemote = async (
await client.client.deleteFile(remoteFileName);
// log.info(`delete ${remoteFileName} succeeded`);
} catch (err) {
console.error("some error while deleting");
log.info(err);
log.error("some error while deleting");
log.error(err);
}
};
@@ -565,7 +575,7 @@ export const checkConnectivity = async (
)
) {
const err = "Error: the url should start with http(s):// but it does not!";
log.debug(err);
log.error(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
@@ -573,10 +583,10 @@ export const checkConnectivity = async (
}
try {
await client.init();
const results = await getRemoteMeta(client, "/");
const results = await getRemoteMeta(client, `/${client.remoteBaseDir}/`);
if (results === undefined) {
const err = "results is undefined";
log.debug(err);
log.error(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
@@ -584,7 +594,7 @@ export const checkConnectivity = async (
}
return true;
} catch (err) {
log.debug(err);
log.error(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
}
+79 -138
View File
@@ -11,6 +11,7 @@ import {
import type { TextComponent } from "obsidian";
import { createElement, Eye, EyeOff } from "lucide";
import {
API_VER_ENSURE_REQURL_OK,
API_VER_REQURL,
DEFAULT_DEBUG_FOLDER,
SUPPORTED_SERVICES_TYPE,
@@ -19,18 +20,12 @@ import {
WebdavAuthType,
WebdavDepthType,
} from "./baseTypes";
import {
exportVaultSyncPlansToFiles,
exportVaultLoggerOutputToFiles,
} from "./debugMode";
import { exportVaultSyncPlansToFiles } from "./debugMode";
import { exportQrCodeUri } from "./importExport";
import {
clearAllSyncMetaMapping,
clearAllSyncPlanRecords,
destroyDBs,
clearAllLoggerOutputRecords,
insertLoggerOutputByVault,
clearExpiredLoggerOutputRecords,
upsertLastSuccessSyncByVault,
} from "./localdb";
import type RemotelySavePlugin from "./main"; // unavoidable
@@ -48,7 +43,6 @@ import {
import { messyConfigToNormal } from "./configPersist";
import type { TransItemType } from "./i18n";
import { checkHasSpecialCharForDir } from "./misc";
import { applyWebdavPresetRulesInplace } from "./presetRules";
import {
applyLogWriterInplace,
@@ -411,12 +405,17 @@ class DropboxAuthModal extends Modal {
const authRes = await sendAuthReqDropbox(
this.plugin.settings.dropbox.clientID,
verifier,
authCode
authCode,
async (e: any) => {
new Notice(t("protocol_dropbox_connect_fail"));
new Notice(`${e}`);
throw e;
}
);
const self = this;
setConfigBySuccessfullAuthInplace(
this.plugin.settings.dropbox,
authRes,
authRes!,
() => self.plugin.saveSettings()
);
const client = new RemoteClient(
@@ -451,7 +450,7 @@ class DropboxAuthModal extends Modal {
);
this.close();
} catch (err) {
console.error(err);
log.error(err);
new Notice(t("modal_dropboxauth_maualinput_conn_fail"));
}
});
@@ -587,7 +586,7 @@ export class OnedriveRevokeAuthModal extends Modal {
new Notice(t("modal_onedriverevokeauth_clean_notice"));
this.close();
} catch (err) {
console.error(err);
log.error(err);
new Notice(t("modal_onedriverevokeauth_clean_fail"));
}
});
@@ -784,7 +783,7 @@ const getEyesElements = () => {
const wrapTextWithPasswordHide = (text: TextComponent) => {
const { eye, eyeOff } = getEyesElements();
const hider = text.inputEl.insertAdjacentElement("afterend", createSpan());
const hider = text.inputEl.insertAdjacentElement("afterend", createSpan())!;
// the init type of hider is "hidden" === eyeOff === password
hider.innerHTML = eyeOff;
hider.addEventListener("click", (e) => {
@@ -963,7 +962,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
if (VALID_REQURL) {
if (VALID_REQURL && !requireApiVersion(API_VER_ENSURE_REQURL_OK)) {
new Setting(s3Div)
.setName(t("settings_s3_bypasscorslocally"))
.setDesc(t("settings_s3_bypasscorslocally_desc"))
@@ -1010,6 +1009,28 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(s3Div)
.setName(t("settings_s3_accuratemtime"))
.setDesc(t("settings_s3_accuratemtime_desc"))
.addDropdown((dropdown) => {
dropdown
.addOption("disable", t("disable"))
.addOption("enable", t("enable"));
dropdown
.setValue(
`${this.plugin.settings.s3.useAccurateMTime ? "enable" : "disable"}`
)
.onChange(async (val) => {
if (val === "enable") {
this.plugin.settings.s3.useAccurateMTime = true;
} else {
this.plugin.settings.s3.useAccurateMTime = false;
}
await this.plugin.saveSettings();
});
});
let newS3RemotePrefix = this.plugin.settings.s3.remotePrefix || "";
new Setting(s3Div)
.setName(t("settings_remoteprefix"))
@@ -1130,7 +1151,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
);
new Notice(t("settings_dropbox_revoke_notice"));
} catch (err) {
console.error(err);
log.error(err);
new Notice(t("settings_dropbox_revoke_noticeerr"));
}
});
@@ -1440,16 +1461,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.address)
.onChange(async (value) => {
this.plugin.settings.webdav.address = value.trim();
// deprecate auto on 20240116, force to manual_1
if (
this.plugin.settings.webdav.depth === "auto" ||
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
this.plugin.settings.webdav.depth === "auto_infinity" ||
this.plugin.settings.webdav.depth === "auto_unknown"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
this.plugin.settings.webdav.depth = "manual_1";
}
// TODO: any more elegant way?
applyWebdavPresetRulesInplace(this.plugin.settings.webdav);
// normally saved
await this.plugin.saveSettings();
})
@@ -1465,11 +1486,14 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.username)
.onChange(async (value) => {
this.plugin.settings.webdav.username = value.trim();
// deprecate auto on 20240116, force to manual_1
if (
this.plugin.settings.webdav.depth === "auto" ||
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
this.plugin.settings.webdav.depth === "auto_infinity" ||
this.plugin.settings.webdav.depth === "auto_unknown"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
this.plugin.settings.webdav.depth = "manual_1";
}
await this.plugin.saveSettings();
});
@@ -1485,11 +1509,14 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.password)
.onChange(async (value) => {
this.plugin.settings.webdav.password = value.trim();
// deprecate auto on 20240116, force to manual_1
if (
this.plugin.settings.webdav.depth === "auto" ||
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
this.plugin.settings.webdav.depth === "auto_infinity" ||
this.plugin.settings.webdav.depth === "auto_unknown"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
this.plugin.settings.webdav.depth = "manual_1";
}
await this.plugin.saveSettings();
});
@@ -1512,8 +1539,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropdown
.setValue(this.plugin.settings.webdav.authType)
.onChange(async (val: WebdavAuthType) => {
this.plugin.settings.webdav.authType = val;
.onChange(async (val) => {
this.plugin.settings.webdav.authType = val as WebdavAuthType;
await this.plugin.saveSettings();
});
});
@@ -1522,41 +1549,23 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setName(t("settings_webdav_depth"))
.setDesc(t("settings_webdav_depth_desc"))
.addDropdown((dropdown) => {
dropdown.addOption("auto", t("settings_webdav_depth_auto"));
dropdown.addOption("manual_1", t("settings_webdav_depth_1"));
dropdown.addOption("manual_infinity", t("settings_webdav_depth_inf"));
let initVal = "auto";
const autoOptions: Set<WebdavDepthType> = new Set([
"auto_unknown",
"auto_1",
"auto_infinity",
]);
if (autoOptions.has(this.plugin.settings.webdav.depth)) {
initVal = "auto";
} else {
initVal = this.plugin.settings.webdav.depth || "auto";
}
dropdown
.setValue(this.plugin.settings.webdav.depth || "manual_1")
.onChange(async (val) => {
if (val === "manual_1") {
this.plugin.settings.webdav.depth = "manual_1";
this.plugin.settings.webdav.manualRecursive = true;
} else if (val === "manual_infinity") {
this.plugin.settings.webdav.depth = "manual_infinity";
this.plugin.settings.webdav.manualRecursive = false;
}
type DepthOption = "auto" | "manual_1" | "manual_infinity";
dropdown.setValue(initVal).onChange(async (val: DepthOption) => {
if (val === "auto") {
this.plugin.settings.webdav.depth = "auto_unknown";
this.plugin.settings.webdav.manualRecursive = false;
} else if (val === "manual_1") {
this.plugin.settings.webdav.depth = "manual_1";
this.plugin.settings.webdav.manualRecursive = true;
} else if (val === "manual_infinity") {
this.plugin.settings.webdav.depth = "manual_infinity";
this.plugin.settings.webdav.manualRecursive = false;
}
// TODO: any more elegant way?
applyWebdavPresetRulesInplace(this.plugin.settings.webdav);
// normally save
await this.plugin.saveSettings();
});
// normally save
await this.plugin.saveSettings();
});
});
let newWebdavRemoteBaseDir =
@@ -1634,8 +1643,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropdown.addOption("onedrive", t("settings_chooseservice_onedrive"));
dropdown
.setValue(this.plugin.settings.serviceType)
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
this.plugin.settings.serviceType = val;
.onChange(async (val) => {
this.plugin.settings.serviceType = val as SUPPORTED_SERVICES_TYPE;
s3Div.toggleClass(
"s3-hide",
this.plugin.settings.serviceType !== "s3"
@@ -1793,11 +1802,11 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
// );
if (
currentTime - lastModified <
this.plugin.settings.syncOnSaveAfterMilliseconds
this.plugin.settings.syncOnSaveAfterMilliseconds!
) {
if (!runScheduled) {
const scheduleTimeFromNow =
this.plugin.settings.syncOnSaveAfterMilliseconds -
this.plugin.settings.syncOnSaveAfterMilliseconds! -
(currentTime - lastModified);
log.info(
`schedule a run for ${scheduleTimeFromNow} milliseconds later`
@@ -1842,7 +1851,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setDesc(t("settings_enablestatusbar_info_desc"))
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.enableStatusBarInfo)
.setValue(this.plugin.settings.enableStatusBarInfo ?? false)
.onChange(async (val) => {
this.plugin.settings.enableStatusBarInfo = val;
await this.plugin.saveSettings();
@@ -1875,7 +1884,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.addTextArea((textArea) => {
textArea
.setValue(`${this.plugin.settings.ignorePaths.join("\n")}`)
.setValue(`${(this.plugin.settings.ignorePaths ?? []).join("\n")}`)
.onChange(async (value) => {
this.plugin.settings.ignorePaths = value
.trim()
@@ -1977,9 +1986,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
t("settings_deletetowhere_obsidian_trash")
);
dropdown
.setValue(this.plugin.settings.deleteToWhere)
.onChange(async (val: "system" | "obsidian") => {
this.plugin.settings.deleteToWhere = val;
.setValue(this.plugin.settings.deleteToWhere ?? "system")
.onChange(async (val) => {
this.plugin.settings.deleteToWhere = val as "system" | "obsidian";
await this.plugin.saveSettings();
});
});
@@ -2022,7 +2031,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropdown.addOption("info", "info");
dropdown.addOption("debug", "debug");
dropdown
.setValue(this.plugin.settings.currLogLevel)
.setValue(this.plugin.settings.currLogLevel ?? "info")
.onChange(async (val: string) => {
this.plugin.settings.currLogLevel = val;
log.setLevel(val as any);
@@ -2052,24 +2061,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await exportVaultSyncPlansToFiles(
this.plugin.db,
this.app.vault,
this.plugin.vaultRandomID,
"json"
);
new Notice(t("settings_syncplans_notice"));
});
})
.addButton(async (button) => {
button.setButtonText(t("settings_syncplans_button_table"));
button.onClick(async () => {
await exportVaultSyncPlansToFiles(
this.plugin.db,
this.app.vault,
this.plugin.vaultRandomID,
"table"
this.plugin.vaultRandomID
);
new Notice(t("settings_syncplans_notice"));
});
});
new Setting(debugDiv)
.setName(t("settings_delsyncplans"))
.setDesc(t("settings_delsyncplans_desc"))
@@ -2081,63 +2078,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(debugDiv)
.setName(t("settings_logtodb"))
.setDesc(t("settings_logtodb_desc"))
.addDropdown(async (dropdown) => {
dropdown.addOption("enable", t("enable"));
dropdown.addOption("disable", t("disable"));
dropdown
.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();
// }
clearExpiredLoggerOutputRecords(this.plugin.db);
this.plugin.settings.logToDB = logToDB;
await this.plugin.saveSettings();
});
});
new Setting(debugDiv)
.setName(t("settings_logtodbexport"))
.setDesc(
t("settings_logtodbexport_desc", {
debugFolder: DEFAULT_DEBUG_FOLDER,
})
)
.addButton(async (button) => {
button.setButtonText(t("settings_logtodbexport_button"));
button.onClick(async () => {
await exportVaultLoggerOutputToFiles(
this.plugin.db,
this.app.vault,
this.plugin.vaultRandomID
);
new Notice(t("settings_logtodbexport_notice"));
});
});
new Setting(debugDiv)
.setName(t("settings_logtodbclear"))
.setDesc(t("settings_logtodbclear_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_logtodbclear_button"));
button.onClick(async () => {
await clearAllLoggerOutputRecords(this.plugin.db);
new Notice(t("settings_logtodbclear_notice"));
});
});
let logToHttpServer = this.plugin.debugServerTemp || "";
new Setting(debugDiv)
.setName(t("settings_logtohttpserver"))
@@ -2215,6 +2155,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
button.onClick(async () => {
await destroyDBs();
new Notice(t("settings_resetcache_notice"));
this.plugin.unload();
});
});
}
+62 -81
View File
@@ -186,7 +186,7 @@ export const parseRemoteItems = async (
password: string = ""
) => {
const remoteStates = [] as FileOrFolderMixedState[];
let metadataFile: FileOrFolderMixedState = undefined;
let metadataFile: FileOrFolderMixedState | undefined = undefined;
if (remote === undefined) {
return {
remoteStates: remoteStates,
@@ -212,8 +212,8 @@ export const parseRemoteItems = async (
remoteType,
db,
key,
entry.lastModified,
entry.etag,
entry.lastModified ?? Date.now(),
entry.etag ?? "",
vaultRandomID
);
@@ -268,7 +268,7 @@ export const parseRemoteItems = async (
};
export const fetchMetadataFile = async (
metadataFile: FileOrFolderMixedState,
metadataFile: FileOrFolderMixedState | undefined,
client: RemoteClient,
vault: Vault,
password: string = ""
@@ -283,7 +283,7 @@ export const fetchMetadataFile = async (
const buf = await client.downloadFromRemote(
metadataFile.key,
vault,
metadataFile.mtimeRemote,
metadataFile.mtimeRemote ?? Date.now(),
password,
metadataFile.remoteEncryptedKey,
true
@@ -325,8 +325,8 @@ const ensembleMixedStates = async (
remoteStates: FileOrFolderMixedState[],
local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[],
localFileHistory: FileFolderHistoryRecord[],
remoteDeleteHistory: DeletionOnRemote[] | undefined,
localFileHistory: FileFolderHistoryRecord[] | undefined,
syncConfigDir: boolean,
configDir: string,
syncUnderscoreItems: boolean,
@@ -413,7 +413,10 @@ const ensembleMixedStates = async (
if (syncConfigDir && localConfigDirContents !== undefined) {
for (const entry of localConfigDirContents) {
const key = entry.key;
let mtimeLocal = Math.max(entry.mtime ?? 0, entry.ctime ?? 0);
let mtimeLocal: number | undefined = Math.max(
entry.mtime ?? 0,
entry.ctime ?? 0
);
if (Number.isNaN(mtimeLocal) || mtimeLocal === 0) {
mtimeLocal = undefined;
}
@@ -453,7 +456,7 @@ const ensembleMixedStates = async (
}
}
for (const entry of remoteDeleteHistory) {
for (const entry of remoteDeleteHistory ?? []) {
const key = entry.key;
const r = {
key: key,
@@ -485,7 +488,7 @@ const ensembleMixedStates = async (
}
}
for (const entry of localFileHistory) {
for (const entry of localFileHistory ?? []) {
let key = entry.key;
if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) {
@@ -532,7 +535,7 @@ const ensembleMixedStates = async (
changeLocalMtimeUsingMapping: true,
};
if (results.hasOwnProperty(key)) {
let mtimeLocal = Math.max(
let mtimeLocal: number | undefined = Math.max(
r.mtimeLocal ?? 0,
results[key].mtimeLocal ?? 0
);
@@ -624,13 +627,13 @@ const assignOperationToFileInplace = (
// 1. mtimeLocal
if (r.existLocal) {
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote! : -1;
const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if (
r.mtimeLocal >= mtimeRemote &&
r.mtimeLocal >= deltimeLocal &&
r.mtimeLocal >= deltimeRemote
r.mtimeLocal! >= mtimeRemote &&
r.mtimeLocal! >= deltimeLocal &&
r.mtimeLocal! >= deltimeRemote
) {
if (sizeLocalComp === undefined) {
throw new Error(
@@ -654,7 +657,7 @@ const assignOperationToFileInplace = (
} else {
// limit the sizes
if (sizeLocalComp <= skipSizeLargerThan) {
if (sizeRemoteComp <= skipSizeLargerThan) {
if (sizeRemoteComp! <= skipSizeLargerThan) {
r.decision = "uploadLocalToRemote";
r.decisionBranch = 18;
} else {
@@ -662,7 +665,7 @@ const assignOperationToFileInplace = (
r.decisionBranch = 19;
}
} else {
if (sizeRemoteComp <= skipSizeLargerThan) {
if (sizeRemoteComp! <= skipSizeLargerThan) {
r.decision = "errorLocalTooLargeConflictRemote";
r.decisionBranch = 20;
} else {
@@ -713,13 +716,13 @@ const assignOperationToFileInplace = (
// 2. mtimeRemote
if (r.existRemote) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeLocal = r.existLocal ? r.mtimeLocal! : -1;
const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if (
r.mtimeRemote > mtimeLocal &&
r.mtimeRemote >= deltimeLocal &&
r.mtimeRemote >= deltimeRemote
r.mtimeRemote! > mtimeLocal &&
r.mtimeRemote! >= deltimeLocal &&
r.mtimeRemote! >= deltimeRemote
) {
// we have remote laregest mtime,
// and the local not existing or smaller mtime
@@ -771,8 +774,8 @@ const assignOperationToFileInplace = (
// 3. deltimeLocal
if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const mtimeLocal = r.existLocal ? r.mtimeLocal! : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote! : -1;
const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
if (
r.deltimeLocal >= mtimeLocal &&
@@ -787,9 +790,9 @@ const assignOperationToFileInplace = (
}
} else {
const localTooLargeToDelete =
r.existLocal && sizeLocalComp > skipSizeLargerThan;
r.existLocal && sizeLocalComp! > skipSizeLargerThan;
const remoteTooLargeToDelete =
r.existRemote && sizeRemoteComp > skipSizeLargerThan;
r.existRemote && sizeRemoteComp! > skipSizeLargerThan;
if (localTooLargeToDelete) {
if (remoteTooLargeToDelete) {
r.decision = "skipUsingLocalDelTooLarge";
@@ -824,8 +827,8 @@ const assignOperationToFileInplace = (
// 4. deltimeRemote
if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const mtimeLocal = r.existLocal ? r.mtimeLocal! : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote! : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if (
r.deltimeRemote >= mtimeLocal &&
@@ -840,9 +843,9 @@ const assignOperationToFileInplace = (
}
} else {
const localTooLargeToDelete =
r.existLocal && sizeLocalComp > skipSizeLargerThan;
r.existLocal && sizeLocalComp! > skipSizeLargerThan;
const remoteTooLargeToDelete =
r.existRemote && sizeRemoteComp > skipSizeLargerThan;
r.existRemote && sizeRemoteComp! > skipSizeLargerThan;
if (localTooLargeToDelete) {
if (remoteTooLargeToDelete) {
r.decision = "skipUsingRemoteDelTooLarge";
@@ -905,7 +908,13 @@ const assignOperationToFolderInplace = async (
// if it was created after deletion, we should keep it as is
if (requireApiVersion(API_VER_STAT_FOLDER)) {
if (r.existLocal) {
const { ctime, mtime } = await statFix(vault, r.key);
let ctime = 0;
let mtime = 0;
const s = await statFix(vault, r.key);
if (s !== undefined && s !== null) {
ctime = s.ctime;
mtime = s.mtime;
}
const cmtime = Math.max(ctime ?? 0, mtime ?? 0);
if (
!Number.isNaN(cmtime) &&
@@ -936,9 +945,9 @@ const assignOperationToFolderInplace = async (
if (
r.existLocal &&
r.changeLocalMtimeUsingMapping &&
r.mtimeLocal > 0 &&
r.mtimeLocal > deltimeLocal &&
r.mtimeLocal > deltimeRemote
r.mtimeLocal! > 0 &&
r.mtimeLocal! > deltimeLocal &&
r.mtimeLocal! > deltimeRemote
) {
keptFolder.add(getParentFolder(r.key));
if (r.existLocal && r.existRemote) {
@@ -1018,8 +1027,8 @@ export const getSyncPlan = async (
remoteStates: FileOrFolderMixedState[],
local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[],
localFileHistory: FileFolderHistoryRecord[],
remoteDeleteHistory: DeletionOnRemote[] | undefined,
localFileHistory: FileFolderHistoryRecord[] | undefined,
remoteType: SUPPORTED_SERVICES_TYPE,
triggerSource: SyncTriggerSourceType,
vault: Vault,
@@ -1071,30 +1080,30 @@ export const getSyncPlan = async (
);
}
if (SIZES_GO_WRONG_DECISIONS.has(val.decision)) {
if (SIZES_GO_WRONG_DECISIONS.has(val.decision!)) {
sizesGoWrong.push(val);
}
if (DELETION_DECISIONS.has(val.decision)) {
if (DELETION_DECISIONS.has(val.decision!)) {
if (val.decision === "uploadLocalDelHistToRemote") {
deletions.push({
key: key,
actionWhen: val.deltimeLocal,
actionWhen: val.deltimeLocal!,
});
} else if (val.decision === "keepRemoteDelHist") {
deletions.push({
key: key,
actionWhen: val.deltimeRemote,
actionWhen: val.deltimeRemote!,
});
} else if (val.decision === "uploadLocalDelHistToRemoteFolder") {
deletions.push({
key: key,
actionWhen: val.deltimeLocal,
actionWhen: val.deltimeLocal!,
});
} else if (val.decision === "keepRemoteDelHistFolder") {
deletions.push({
key: key,
actionWhen: val.deltimeRemote,
actionWhen: val.deltimeRemote!,
});
} else {
throw Error(`do not know how to delete for decision ${val.decision}`);
@@ -1131,7 +1140,7 @@ const uploadExtraMeta = async (
}
const key = DEFAULT_FILE_NAME_FOR_METADATAONREMOTE;
let remoteEncryptedKey = key;
let remoteEncryptedKey: string | undefined = key;
if (password !== "") {
if (metadataFile === undefined) {
@@ -1180,7 +1189,7 @@ const dispatchOperationToActual = async (
localDeleteFunc: any,
password: string = ""
) => {
let remoteEncryptedKey = key;
let remoteEncryptedKey: string | undefined = key;
if (password !== "") {
remoteEncryptedKey = r.remoteEncryptedKey;
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
@@ -1233,12 +1242,12 @@ const dispatchOperationToActual = async (
client.serviceType,
db,
r.key,
r.mtimeLocal,
r.sizeLocal,
r.mtimeLocal!,
r.sizeLocal!,
r.key,
remoteObjMeta.lastModified,
remoteObjMeta.lastModified ?? Date.now(),
remoteObjMeta.size,
remoteObjMeta.etag,
remoteObjMeta.etag ?? "",
vaultRandomID
);
}
@@ -1248,7 +1257,7 @@ const dispatchOperationToActual = async (
await client.downloadFromRemote(
r.key,
vault,
r.mtimeRemote,
r.mtimeRemote!,
password,
remoteEncryptedKey
);
@@ -1269,12 +1278,12 @@ const dispatchOperationToActual = async (
client.serviceType,
db,
r.key,
r.mtimeLocal,
r.sizeLocal,
r.mtimeLocal!,
r.sizeLocal!,
r.key,
remoteObjMeta.lastModified,
remoteObjMeta.lastModified ?? Date.now(),
remoteObjMeta.size,
remoteObjMeta.etag,
remoteObjMeta.etag ?? "",
vaultRandomID
);
}
@@ -1388,7 +1397,7 @@ export const doActualSync = async (
vault: Vault,
syncPlan: SyncPlanType,
sortedKeys: string[],
metadataFile: FileOrFolderMixedState,
metadataFile: FileOrFolderMixedState | undefined,
origMetadata: MetadataOnRemote,
sizesGoWrong: FileOrFolderMixedState[],
deletions: DeletionOnRemote[],
@@ -1418,34 +1427,6 @@ export const doActualSync = async (
log.debug(`finish syncing extra data firstly`);
log.debug(`concurrency === ${concurrency}`);
if (concurrency === 1) {
// run everything in sequence
// good old way
for (let i = 0; i < sortedKeys.length; ++i) {
const key = sortedKeys[i];
const val = mixedStates[key];
log.debug(`start syncing "${key}" with plan ${JSON.stringify(val)}`);
if (callbackSyncProcess !== undefined) {
await callbackSyncProcess(i, totalCount, key, val.decision);
}
await dispatchOperationToActual(
key,
vaultRandomID,
val,
client,
db,
vault,
localDeleteFunc,
password
);
log.debug(`finished ${key}`);
}
return; // shortcut return, avoid too many nests below
}
const { folderCreationOps, deletionOps, uploadDownloads, realTotalCount } =
splitThreeSteps(syncPlan, sortedKeys);
+2 -2
View File
@@ -45,8 +45,8 @@ describe("Metadata operations tests", () => {
});
it("should treat undefined correctly", async () => {
const a: MetadataOnRemote = undefined;
let b: MetadataOnRemote = {
const a: MetadataOnRemote | undefined = undefined;
let b: MetadataOnRemote | undefined = {
deletions: [
{ key: "xxx", actionWhen: 1 },
{ key: "yyy", actionWhen: 2 },
-111
View File
@@ -1,111 +0,0 @@
import { expect } from "chai";
import type { WebdavConfig } from "../src/baseTypes";
import { applyWebdavPresetRulesInplace } from "../src/presetRules";
describe("Preset rules tests", () => {
it("should check undefined correctly", () => {
let x: Partial<WebdavConfig> | undefined = undefined;
const y = applyWebdavPresetRulesInplace(x);
expect(y.webdav === undefined);
expect(!y.changed);
});
it("should check empty object", () => {
let x: Partial<WebdavConfig> | undefined = {};
const y = applyWebdavPresetRulesInplace(x);
expect(y.webdav).deep.equals({});
expect(!y.changed);
});
it("should modify depths correctly", () => {
let x: Partial<WebdavConfig> = {
address: "https://example.teracloud.jp/dav/",
depth: "auto_unknown",
};
let y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_1");
expect(y.changed);
x = {
address: "https://example.teracloud.jp/dav/example",
depth: "auto_unknown",
};
y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_1");
expect(y.changed);
x = {
address: "https://dav.jianguoyun.com/dav/",
depth: "auto_unknown",
};
y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_1");
expect(y.changed);
x = {
address: "https://dav.jianguoyun.com/dav/",
depth: "auto_infinity",
};
y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_1");
expect(y.changed);
});
it("should not modify depths if depths is set automatically correctly", () => {
let x: Partial<WebdavConfig> = {
address: "https://dav.jianguoyun.com/dav/",
depth: "auto_1",
};
let y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_1");
expect(!y.changed);
});
it("should not modify depths if depths have been set manually", () => {
let x: Partial<WebdavConfig> = {
address: "https://example.teracloud.jp/dav/",
depth: "manual_infinity",
};
let y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "manual_infinity");
expect(!y.changed);
x = {
address: "https://example.teracloud.jp/dav/example",
depth: "manual_1",
};
y = applyWebdavPresetRulesInplace(x);
expect(x.depth === "manual_1");
expect(!y.changed);
});
it("should not modify depths when urls are not in preset rules", () => {
let x: Partial<WebdavConfig> = {
address: "https://teracloud.jp/dav/",
depth: "auto_unknown",
};
applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_unknown");
x = {
address: "https://dav.jianguoyun.com/dav_example",
depth: "auto_unknown",
};
applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_unknown");
x = {
address: "",
depth: "auto_unknown",
};
applyWebdavPresetRulesInplace(x);
expect(x.depth === "auto_unknown");
x = {
address: "https://dav.jianguoyun.com/dav/",
depth: "what" as any,
};
applyWebdavPresetRulesInplace(x);
expect(x.depth === ("what" as any));
});
});
+1
View File
@@ -5,6 +5,7 @@
"inlineSources": true,
"module": "ESNext",
"target": "es6",
"strict": true,
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",