Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb6f7e572c | ||
|
|
83e0073134 | ||
|
|
cfe316f690 | ||
|
|
c472654060 | ||
|
|
46cbfcc3aa | ||
|
|
ce94a6d79c | ||
|
|
0fc0dcad64 | ||
|
|
577cdde21f | ||
|
|
583b365e72 | ||
|
|
3d213b2be8 | ||
|
|
ee224bf4f2 | ||
|
|
f1bd0b1ce6 | ||
|
|
300ed213af | ||
|
|
4be67ce491 | ||
|
|
98107ba4ea | ||
|
|
b674dd0f10 | ||
|
|
7930509e2a | ||
|
|
1c918d82da | ||
|
|
593fd7471b | ||
|
|
222f386586 | ||
|
|
dcd02457cb | ||
|
|
791c0e8df6 | ||
|
|
c37bf6aedd | ||
|
|
bed28d9f0b | ||
|
|
02e03681f7 | ||
|
|
62452341a3 | ||
|
|
bff2f6a642 | ||
|
|
833fdee69e | ||
|
|
936fce76a1 | ||
|
|
e2e8265d43 | ||
|
|
e228250613 | ||
|
|
dde4327249 | ||
|
|
e283efc8f7 | ||
|
|
6825241071 | ||
|
|
98380b6c92 | ||
|
|
d3eb2166aa | ||
|
|
7ecce29940 | ||
|
|
581c6237cc | ||
|
|
257c995090 |
@@ -23,7 +23,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
|
|||||||
- Webdav
|
- Webdav
|
||||||
- [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details.
|
- [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details.
|
||||||
- **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
- **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
||||||
- **[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.
|
- **[End-to-end encryption](./docs/encryption/README.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).
|
- **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).**
|
- **[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!
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Encryption
|
||||||
|
|
||||||
|
Currently (March 2024), Remotely Save supports two end to end encryption format:
|
||||||
|
|
||||||
|
1. [RClone Crypt](./rclone.md) format, which is the recommend way now.
|
||||||
|
2. [OpenSSL enc](./openssl.md) format
|
||||||
|
|
||||||
|
Here is also the [comparation](./comparation.md).
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Comparation Between Encryption Formats
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
|
||||||
|
**ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
|
||||||
|
|
||||||
|
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
|
||||||
|
|
||||||
|
## The feature table
|
||||||
|
|
||||||
|
| | RClone Crypt | OpenSSL enc | comments |
|
||||||
|
| ------------------------ | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| key generation | scrypt with fixed salt | PBKDF2 with dynamic salt | scrypt is better than PBKDF2 from the algorithm aspect. But RClone uses fixed salt by default. Also the parameters might affect the result. |
|
||||||
|
| content encryption | XSalsa20Poly1305 on chunks | AES-256-CBC | XSalsa20Poly1305 is way better than AES-256-CBC. And encryption by chunks should require less resources. |
|
||||||
|
| file name encryption | EME on each segment of the path | AES-256-CBC on the whole path | RClone has the benefit as well as pitfall that the path structure is preserved. Maybe it's more of a design decision difference? No comment on EME and AES-256-CBC. |
|
||||||
|
| viewing decrypted result | RClone has command that can mount the encrypted vault as if the encryption is transparent. | No convenient way except writing some scripts we are aware of. | RClone is way more convenient. |
|
||||||
|
|
||||||
|
## Some notes
|
||||||
|
|
||||||
|
1. Anyway, security is a hard problem. The author of Remotely Save doesn't have sufficient knowledge to "judge" which one is the better format. **Use them at your own risk.**
|
||||||
|
2. Currently the RClone Crypt format is recommended by default in Remotely Save. Just because of the taste from the Remotely Save author, who likes RClone.
|
||||||
|
3. **Always use a long password.**
|
||||||
|
4. Both algorithms are selected deliberately to **be compatible with some well-known third-party tools** (instead of some home-made methods) and **have many tests to ensure the correctness**.
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
# Encryption
|
# OpenSSL enc format
|
||||||
|
|
||||||
If a password is set, the files are encrypted before being sent to the cloud.
|
If a password is set, the files are encrypted before being sent to the cloud.
|
||||||
|
|
||||||
The encryption algorithm is delibrately designed to be aligned with openssl format.
|
## Warning
|
||||||
|
|
||||||
1. The encryption algorithm is implemented using web-crypto.
|
**ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
|
||||||
|
|
||||||
|
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
|
||||||
|
|
||||||
|
## Comparation between encryption formats
|
||||||
|
|
||||||
|
See the doc [Comparation](./comparation.md).
|
||||||
|
|
||||||
|
## Interoperability with official OpenSSL
|
||||||
|
|
||||||
|
This encryption algorithm is delibrately designed to be aligned with openssl format.
|
||||||
|
|
||||||
|
1. The encryption algorithm is implemented using web-crypto. Using AES-256-CBC.
|
||||||
2. The file content is encrypted using openssl format. Assuming a file named `sometext.txt`, a password `somepassword`, then the encryption is equivalent to the following command:
|
2. The file content is encrypted using openssl format. Assuming a file named `sometext.txt`, a password `somepassword`, then the encryption is equivalent to the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# RClone Crypt format
|
||||||
|
|
||||||
|
The encryption is compatible with RClone Crypt with **base64** name encryption format.
|
||||||
|
|
||||||
|
It's developed based on another js project by the same author of Remotely Save: [`@fyears/rclone-crypt`](https://github.com/fyears/rclone-crypt), which is NOT an official library from RClone, and is NOT affiliated with RClone.
|
||||||
|
|
||||||
|
Reasonable tests are also ported from official RClone code, to ensure the compatibility and correctness of the encryption.
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
|
||||||
|
**ALWAYS BACKUP YOUR VAULT MANUALLY!!!**
|
||||||
|
|
||||||
|
If you switch between RClone Crypt format and OpenSSL enc format, you have to delete the cloud vault files **manually** and **fully**, so that the plugin can re-sync (i.e. re-upload) the newly encrypted versions to the cloud.
|
||||||
|
|
||||||
|
## Comparation between encryption formats
|
||||||
|
|
||||||
|
See the doc [Comparation](./comparation.md).
|
||||||
|
|
||||||
|
## Interoperability with official RClone
|
||||||
|
|
||||||
|
Please pay attention that the plugin uses **base64** of encrypted file names, while official RClone by default uses **base32** file names. The intention is purely for potentially support longer file names.
|
||||||
|
|
||||||
|
You could set up the RClone profile by calling `rclone config`. You need to create two profiles, one for your original connection and the other for RClone Crypt.
|
||||||
|
|
||||||
|
Finally, a working config file should like this:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[webdav1]
|
||||||
|
type = webdav
|
||||||
|
url = https://example.com/sharefolder1/subfolder1 # the same as the web address in Remotely Save settings.
|
||||||
|
vendor = other
|
||||||
|
user = <some webdav username>
|
||||||
|
pass = <some webdav password, obfuscated>
|
||||||
|
|
||||||
|
[webdav1crypt]
|
||||||
|
type = crypt
|
||||||
|
remote = nas1test:vaultname # the same as your "Remote Base Directory" (usually the vault name) in Remotely Save settings
|
||||||
|
password = <some encryption password, obfuscated>
|
||||||
|
filename_encoding = base64 # don't forget this!!!
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the `mount` command to view and see the files in file explorer! On Windows, the command should like this (the remote vault is mounted to drive `X:`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone mount webdav1crypt: X: --network-mode
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# How to receive `obsidian://` in Linux
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
For example, when we are authorizing OneDrive, we have to jump back to Obsidian automatically using `obsidian://`.
|
||||||
|
|
||||||
|
## Short Desc From Official Obsidian Doc
|
||||||
|
|
||||||
|
Official doc has some explanation:
|
||||||
|
|
||||||
|
<https://help.obsidian.md/Extending+Obsidian/Obsidian+URI#Register+Obsidian+URI>
|
||||||
|
|
||||||
|
# Long Desc
|
||||||
|
|
||||||
|
Assuming the username is `somebody`, and the `.AppImage` file is downloaded to `~/Desktop`.
|
||||||
|
|
||||||
|
1. Download and **extract** the app image file in terminal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/somebody/Desktop
|
||||||
|
chmod +x Obsidian-x.y.z.AppImage
|
||||||
|
./Obsidian-x.y.z.AppImage --appimage-extract
|
||||||
|
|
||||||
|
# you should have the folder squashfs-root
|
||||||
|
# we want to rename it
|
||||||
|
mv squashfs-root Obsidian
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a `.desktop` file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# copy and paste the follow MULTI LINE command
|
||||||
|
# you might need to input your password because it requires root privilege
|
||||||
|
# remember to adjust the path
|
||||||
|
cat > ~/Desktop/obsidian.desktop <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Obsidian
|
||||||
|
Comment=obsidian
|
||||||
|
Exec=/home/somebody/Desktop/Obsidian/obsidian %u
|
||||||
|
Keywords=obsidian
|
||||||
|
StartupNotify=true
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Icon=/home/somebody/Desktop/Obsidian/obsidian.png
|
||||||
|
MimeType=x-scheme-handler/obsidian;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# yeah we can check out the output
|
||||||
|
cat ~/Desktop/obsidian.desktop
|
||||||
|
## [Desktop Entry]
|
||||||
|
## ...
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Right click the `obsidian.desktop` file on the Desktop, and click "Allow launching"
|
||||||
|
|
||||||
|
4. Double click the `obsidian.desktop` file.
|
||||||
@@ -33,6 +33,7 @@ Using the principle of least privilege is crucial for security when allowing a t
|
|||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"s3:HeadObject",
|
"s3:HeadObject",
|
||||||
|
"s3:ListBucket",
|
||||||
"s3:PutObject",
|
"s3:PutObject",
|
||||||
"s3:CopyObject",
|
"s3:CopyObject",
|
||||||
"s3:UploadPart",
|
"s3:UploadPart",
|
||||||
@@ -48,7 +49,10 @@ Using the principle of least privilege is crucial for security when allowing a t
|
|||||||
"s3:DeleteObject",
|
"s3:DeleteObject",
|
||||||
"s3:DeleteObjects"
|
"s3:DeleteObjects"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:s3:::my-bucket/*"
|
"Resource": [
|
||||||
|
"arn:aws:s3:::my-bucket",
|
||||||
|
"arn:aws:s3:::my-bucket/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,4 @@
|
|||||||
- [x] sync direction: incremental pull only
|
- [x] sync direction: incremental pull only
|
||||||
- [x] sync protection: warning based on the threshold
|
- [x] sync protection: warning based on the threshold
|
||||||
- [ ] partial sync: better sync on save
|
- [ ] partial sync: better sync on save
|
||||||
|
- [x] encrpytion: new encryption method, see [this](../../encryption/)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import dotenv from "dotenv/config";
|
import dotenv from "dotenv/config";
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
|
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
|
||||||
// import builtins from 'builtin-modules'
|
// import builtins from 'builtin-modules'
|
||||||
|
|
||||||
const banner = `/*
|
const banner = `/*
|
||||||
@@ -54,6 +55,7 @@ esbuild
|
|||||||
"process.env.NODE_DEBUG": `undefined`, // ugly fix
|
"process.env.NODE_DEBUG": `undefined`, // ugly fix
|
||||||
"process.env.DEBUG": `undefined`, // ugly fix
|
"process.env.DEBUG": `undefined`, // ugly fix
|
||||||
},
|
},
|
||||||
|
plugins: [inlineWorkerPlugin()],
|
||||||
})
|
})
|
||||||
.then((context) => {
|
.then((context) => {
|
||||||
if (process.argv.includes("--watch")) {
|
if (process.argv.includes("--watch")) {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.4.6",
|
"version": "0.4.15",
|
||||||
"minAppVersion": "0.13.21",
|
"minAppVersion": "0.13.21",
|
||||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||||
"author": "fyears",
|
"author": "fyears",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.3.40",
|
"version": "0.4.15",
|
||||||
"minAppVersion": "0.13.21",
|
"minAppVersion": "0.13.21",
|
||||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||||
"author": "fyears",
|
"author": "fyears",
|
||||||
|
|||||||
+8
-8
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.4.6",
|
"version": "0.4.15",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev2": "node esbuild.config.mjs --watch",
|
"dev2": "node esbuild.config.mjs --watch",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.14",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@types/chai-as-promised": "^7.1.8",
|
||||||
"@types/jsdom": "^21.1.6",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
@@ -34,13 +34,14 @@
|
|||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"builtin-modules": "^3.3.0",
|
"builtin-modules": "^3.3.0",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.4.1",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"jsdom": "^23.0.1",
|
"jsdom": "^23.0.1",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.4.0",
|
||||||
"npm-check-updates": "^16.14.12",
|
"npm-check-updates": "^16.14.12",
|
||||||
"obsidian": "^1.4.11",
|
"obsidian": "^1.4.11",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
@@ -50,7 +51,8 @@
|
|||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"webdav-server": "^2.6.2",
|
"webdav-server": "^2.6.2",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4",
|
||||||
|
"worker-loader": "^3.0.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.474.0",
|
"@aws-sdk/client-s3": "^3.474.0",
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
"@aws-sdk/signature-v4-crt": "^3.474.0",
|
"@aws-sdk/signature-v4-crt": "^3.474.0",
|
||||||
"@aws-sdk/types": "^3.468.0",
|
"@aws-sdk/types": "^3.468.0",
|
||||||
"@azure/msal-node": "^2.6.0",
|
"@azure/msal-node": "^2.6.0",
|
||||||
|
"@fyears/rclone-crypt": "^0.0.7",
|
||||||
"@fyears/tsqueue": "^1.0.1",
|
"@fyears/tsqueue": "^1.0.1",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"@smithy/fetch-http-handler": "^2.3.1",
|
"@smithy/fetch-http-handler": "^2.3.1",
|
||||||
@@ -69,13 +72,11 @@
|
|||||||
"aws-crt": "^1.20.0",
|
"aws-crt": "^1.20.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"delay": "^6.0.0",
|
|
||||||
"dropbox": "^10.34.0",
|
"dropbox": "^10.34.0",
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.8.1",
|
|
||||||
"lucide": "^0.298.0",
|
"lucide": "^0.298.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
@@ -90,7 +91,6 @@
|
|||||||
"url": "^0.11.3",
|
"url": "^0.11.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"webdav": "^5.3.1",
|
"webdav": "^5.3.1",
|
||||||
"webdav-fs": "^4.0.1",
|
|
||||||
"xregexp": "^5.1.1"
|
"xregexp": "^5.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -88,6 +88,10 @@ export type SyncDirectionType =
|
|||||||
| "incremental_pull_only"
|
| "incremental_pull_only"
|
||||||
| "incremental_push_only";
|
| "incremental_push_only";
|
||||||
|
|
||||||
|
export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown";
|
||||||
|
|
||||||
|
export type QRExportType = "all_but_oauth2" | "dropbox" | "onedrive";
|
||||||
|
|
||||||
export interface RemotelySavePluginSettings {
|
export interface RemotelySavePluginSettings {
|
||||||
s3: S3Config;
|
s3: S3Config;
|
||||||
webdav: WebdavConfig;
|
webdav: WebdavConfig;
|
||||||
@@ -119,6 +123,8 @@ export interface RemotelySavePluginSettings {
|
|||||||
|
|
||||||
enableMobileStatusBar?: boolean;
|
enableMobileStatusBar?: boolean;
|
||||||
|
|
||||||
|
encryptionMethod?: CipherMethodType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
@@ -161,6 +167,8 @@ export type DecisionTypeForMixedEntity =
|
|||||||
| "remote_is_modified_then_pull"
|
| "remote_is_modified_then_pull"
|
||||||
| "local_is_created_then_push"
|
| "local_is_created_then_push"
|
||||||
| "remote_is_created_then_pull"
|
| "remote_is_created_then_pull"
|
||||||
|
| "local_is_created_too_large_then_do_nothing"
|
||||||
|
| "remote_is_created_too_large_then_do_nothing"
|
||||||
| "local_is_deleted_thus_also_delete_remote"
|
| "local_is_deleted_thus_also_delete_remote"
|
||||||
| "remote_is_deleted_thus_also_delete_local"
|
| "remote_is_deleted_thus_also_delete_local"
|
||||||
| "conflict_created_then_keep_local"
|
| "conflict_created_then_keep_local"
|
||||||
@@ -175,7 +183,9 @@ export type DecisionTypeForMixedEntity =
|
|||||||
| "folder_existed_remote_then_also_create_local"
|
| "folder_existed_remote_then_also_create_local"
|
||||||
| "folder_to_be_created"
|
| "folder_to_be_created"
|
||||||
| "folder_to_skip"
|
| "folder_to_skip"
|
||||||
| "folder_to_be_deleted";
|
| "folder_to_be_deleted_on_both"
|
||||||
|
| "folder_to_be_deleted_on_remote"
|
||||||
|
| "folder_to_be_deleted_on_local";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uniform representation
|
* uniform representation
|
||||||
@@ -196,6 +206,7 @@ export interface Entity {
|
|||||||
sizeRaw: number;
|
sizeRaw: number;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
etag?: string;
|
etag?: string;
|
||||||
|
synthesizedFolder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadedType {
|
export interface UploadedType {
|
||||||
@@ -215,6 +226,8 @@ export interface MixedEntity {
|
|||||||
decisionBranch?: number;
|
decisionBranch?: number;
|
||||||
decision?: DecisionTypeForMixedEntity;
|
decision?: DecisionTypeForMixedEntity;
|
||||||
conflictAction?: ConflictActionType;
|
conflictAction?: ConflictActionType;
|
||||||
|
|
||||||
|
sideNotes?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,12 +271,14 @@ export const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
|||||||
export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
|
export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
|
||||||
"sync_plans_hist_exported_on_";
|
"sync_plans_hist_exported_on_";
|
||||||
export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
|
export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
|
||||||
|
export const DEFAULT_PROFILER_RESULT_FILE_PREFIX =
|
||||||
|
"profiler_results_exported_on_";
|
||||||
|
|
||||||
export type SyncTriggerSourceType =
|
export type SyncTriggerSourceType =
|
||||||
| "manual"
|
| "manual"
|
||||||
| "auto"
|
|
||||||
| "dry"
|
| "dry"
|
||||||
| "autoOnceInit"
|
| "auto"
|
||||||
|
| "auto_once_init"
|
||||||
| "auto_sync_on_save";
|
| "auto_sync_on_save";
|
||||||
|
|
||||||
export const REMOTELY_SAVE_VERSION_2022 = "0.3.25";
|
export const REMOTELY_SAVE_VERSION_2022 = "0.3.25";
|
||||||
|
|||||||
+43
-67
@@ -1,95 +1,71 @@
|
|||||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||||
|
|
||||||
import type { SyncPlanType } from "./sync";
|
import {
|
||||||
import { readAllSyncPlanRecordTextsByVault } from "./localdb";
|
readAllProfilerResultsByVault,
|
||||||
|
readAllSyncPlanRecordTextsByVault,
|
||||||
|
} from "./localdb";
|
||||||
import type { InternalDBs } from "./localdb";
|
import type { InternalDBs } from "./localdb";
|
||||||
import { mkdirpInVault } from "./misc";
|
import { mkdirpInVault, unixTimeToStr } from "./misc";
|
||||||
import {
|
import {
|
||||||
DEFAULT_DEBUG_FOLDER,
|
DEFAULT_DEBUG_FOLDER,
|
||||||
DEFAULT_LOG_HISTORY_FILE_PREFIX,
|
DEFAULT_PROFILER_RESULT_FILE_PREFIX,
|
||||||
DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX,
|
DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX,
|
||||||
FileOrFolderMixedState,
|
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
|
|
||||||
const turnSyncPlanToTable = (record: string) => {
|
|
||||||
const syncPlan: SyncPlanType = JSON.parse(record);
|
|
||||||
const { ts, tsFmt, remoteType, mixedStates } = syncPlan;
|
|
||||||
|
|
||||||
type allowedHeadersType = keyof FileOrFolderMixedState;
|
|
||||||
const headers: allowedHeadersType[] = [
|
|
||||||
"key",
|
|
||||||
"remoteEncryptedKey",
|
|
||||||
"existLocal",
|
|
||||||
"sizeLocal",
|
|
||||||
"sizeLocalEnc",
|
|
||||||
"mtimeLocal",
|
|
||||||
"deltimeLocal",
|
|
||||||
"changeLocalMtimeUsingMapping",
|
|
||||||
"existRemote",
|
|
||||||
"sizeRemote",
|
|
||||||
"sizeRemoteEnc",
|
|
||||||
"mtimeRemote",
|
|
||||||
"deltimeRemote",
|
|
||||||
"changeRemoteMtimeUsingMapping",
|
|
||||||
"decision",
|
|
||||||
"decisionBranch",
|
|
||||||
];
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
`ts: ${ts}${tsFmt !== undefined ? " / " + tsFmt : ""}`,
|
|
||||||
`remoteType: ${remoteType}`,
|
|
||||||
`| ${headers.join(" | ")} |`,
|
|
||||||
`| ${headers.map((x) => "---").join(" | ")} |`,
|
|
||||||
];
|
|
||||||
for (const [k1, v1] of Object.entries(syncPlan.mixedStates)) {
|
|
||||||
const k = k1 as string;
|
|
||||||
const v = v1 as FileOrFolderMixedState;
|
|
||||||
const singleLine = [];
|
|
||||||
for (const h of headers) {
|
|
||||||
const field = v[h];
|
|
||||||
if (field === undefined) {
|
|
||||||
singleLine.push("");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
h === "mtimeLocal" ||
|
|
||||||
h === "deltimeLocal" ||
|
|
||||||
h === "mtimeRemote" ||
|
|
||||||
h === "deltimeRemote"
|
|
||||||
) {
|
|
||||||
const fmt = v[(h + "Fmt") as allowedHeadersType] as string;
|
|
||||||
const s = `${field}${fmt !== undefined ? " / " + fmt : ""}`;
|
|
||||||
singleLine.push(s);
|
|
||||||
} else {
|
|
||||||
singleLine.push(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines.push(`| ${singleLine.join(" | ")} |`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join("\n");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const exportVaultSyncPlansToFiles = async (
|
export const exportVaultSyncPlansToFiles = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
vaultRandomID: string
|
vaultRandomID: string,
|
||||||
|
howMany: number
|
||||||
) => {
|
) => {
|
||||||
console.info("exporting");
|
console.info("exporting sync plans");
|
||||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||||
const records = await readAllSyncPlanRecordTextsByVault(db, vaultRandomID);
|
const records = await readAllSyncPlanRecordTextsByVault(db, vaultRandomID);
|
||||||
let md = "";
|
let md = "";
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
md = "No sync plans history found";
|
md = "No sync plans history found";
|
||||||
} else {
|
} else {
|
||||||
|
if (howMany <= 0) {
|
||||||
md =
|
md =
|
||||||
"Sync plans found:\n\n" +
|
"Sync plans found:\n\n" +
|
||||||
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
||||||
|
} else {
|
||||||
|
md =
|
||||||
|
"Sync plans found:\n\n" +
|
||||||
|
records
|
||||||
|
.map((x) => "```json\n" + x + "\n```\n")
|
||||||
|
.slice(0, howMany)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
||||||
await vault.create(filePath, md, {
|
await vault.create(filePath, md, {
|
||||||
mtime: ts,
|
mtime: ts,
|
||||||
});
|
});
|
||||||
console.info("finish exporting");
|
console.info("finish exporting sync plans");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportVaultProfilerResultsToFiles = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
vault: Vault,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
|
console.info("exporting profiler results");
|
||||||
|
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||||
|
const records = await readAllProfilerResultsByVault(db, vaultRandomID);
|
||||||
|
let md = "";
|
||||||
|
if (records.length === 0) {
|
||||||
|
md = "No profiler results found";
|
||||||
|
} else {
|
||||||
|
md =
|
||||||
|
"Profiler results found:\n\n" +
|
||||||
|
records.map((x) => "```\n" + x + "\n```\n").join("\n");
|
||||||
|
}
|
||||||
|
const ts = Date.now();
|
||||||
|
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_PROFILER_RESULT_FILE_PREFIX}${ts}.md`;
|
||||||
|
await vault.create(filePath, md, {
|
||||||
|
mtime: ts,
|
||||||
|
});
|
||||||
|
console.info("finish exporting profiler results");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import {
|
||||||
|
Cipher as CipherRCloneCryptPack,
|
||||||
|
encryptedSize,
|
||||||
|
} from "@fyears/rclone-crypt";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import EncryptWorker from "./encryptRClone.worker";
|
||||||
|
|
||||||
|
interface RecvMsg {
|
||||||
|
status: "ok" | "error";
|
||||||
|
outputName?: string;
|
||||||
|
outputContent?: ArrayBuffer;
|
||||||
|
error?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSizeFromOrigToEnc = encryptedSize;
|
||||||
|
|
||||||
|
export class CipherRclone {
|
||||||
|
readonly password: string;
|
||||||
|
readonly cipher: CipherRCloneCryptPack;
|
||||||
|
readonly workers: Worker[];
|
||||||
|
init: boolean;
|
||||||
|
workerIdx: number;
|
||||||
|
constructor(password: string, workerNum: number) {
|
||||||
|
this.password = password;
|
||||||
|
this.init = false;
|
||||||
|
this.workerIdx = 0;
|
||||||
|
|
||||||
|
// console.debug("begin creating CipherRCloneCryptPack");
|
||||||
|
this.cipher = new CipherRCloneCryptPack("base64");
|
||||||
|
// console.debug("finish creating CipherRCloneCryptPack");
|
||||||
|
|
||||||
|
// console.debug("begin creating EncryptWorker");
|
||||||
|
this.workers = [];
|
||||||
|
for (let i = 0; i < workerNum; ++i) {
|
||||||
|
this.workers.push(new (EncryptWorker as any)() as Worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.debug("finish creating EncryptWorker");
|
||||||
|
}
|
||||||
|
|
||||||
|
closeResources() {
|
||||||
|
for (let i = 0; i < this.workers.length; ++i) {
|
||||||
|
this.workers[i].terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prepareByCallingWorker(): Promise<void> {
|
||||||
|
if (this.init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// console.debug("begin prepareByCallingWorker");
|
||||||
|
await this.cipher.key(this.password, "");
|
||||||
|
// console.debug("finish getting key");
|
||||||
|
|
||||||
|
const res: Promise<void>[] = [];
|
||||||
|
for (let i = 0; i < this.workers.length; ++i) {
|
||||||
|
res.push(
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port2.onmessage = (event) => {
|
||||||
|
// console.debug("main: receiving msg in prepare");
|
||||||
|
const { status } = event.data as RecvMsg;
|
||||||
|
if (status === "ok") {
|
||||||
|
// console.debug("main: receiving init ok in prepare");
|
||||||
|
this.init = true;
|
||||||
|
resolve(); // return the class object itself
|
||||||
|
} else {
|
||||||
|
reject("error after prepareByCallingWorker");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.port2.onmessageerror = (event) => {
|
||||||
|
// console.debug("main: receiving error in prepare");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug("main: before postMessage in prepare");
|
||||||
|
this.workers[i].postMessage(
|
||||||
|
{
|
||||||
|
action: "prepare",
|
||||||
|
dataKeyBuf: this.cipher.dataKey.buffer,
|
||||||
|
nameKeyBuf: this.cipher.nameKey.buffer,
|
||||||
|
nameTweakBuf: this.cipher.nameTweak.buffer,
|
||||||
|
},
|
||||||
|
[channel.port1 /* buffer no transfered because we need to copy */]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Promise.all(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptNameByCallingWorker(inputName: string): Promise<string> {
|
||||||
|
// console.debug("main: start encryptNameByCallingWorker");
|
||||||
|
await this.prepareByCallingWorker();
|
||||||
|
// console.debug(
|
||||||
|
// "main: really start generate promise in encryptNameByCallingWorker"
|
||||||
|
// );
|
||||||
|
++this.workerIdx;
|
||||||
|
const whichWorker = this.workerIdx % this.workers.length;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port2.onmessage = (event) => {
|
||||||
|
// console.debug("main: receiving msg in encryptNameByCallingWorker");
|
||||||
|
const { outputName } = event.data as RecvMsg;
|
||||||
|
if (outputName === undefined) {
|
||||||
|
reject("unknown outputName after encryptNameByCallingWorker");
|
||||||
|
} else {
|
||||||
|
resolve(outputName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.port2.onmessageerror = (event) => {
|
||||||
|
// console.debug("main: receiving error in encryptNameByCallingWorker");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug("main: before postMessage in encryptNameByCallingWorker");
|
||||||
|
this.workers[whichWorker].postMessage(
|
||||||
|
{
|
||||||
|
action: "encryptName",
|
||||||
|
inputName: inputName,
|
||||||
|
},
|
||||||
|
[channel.port1]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptNameByCallingWorker(inputName: string): Promise<string> {
|
||||||
|
await this.prepareByCallingWorker();
|
||||||
|
++this.workerIdx;
|
||||||
|
const whichWorker = this.workerIdx % this.workers.length;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port2.onmessage = (event) => {
|
||||||
|
// console.debug("main: receiving msg in decryptNameByCallingWorker");
|
||||||
|
const { outputName, status } = event.data as RecvMsg;
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
reject("error");
|
||||||
|
} else {
|
||||||
|
if (outputName === undefined) {
|
||||||
|
reject("unknown outputName after decryptNameByCallingWorker");
|
||||||
|
} else {
|
||||||
|
resolve(outputName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.port2.onmessageerror = (event) => {
|
||||||
|
// console.debug("main: receiving error in decryptNameByCallingWorker");
|
||||||
|
reject(event);
|
||||||
|
channel;
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug("main: before postMessage in decryptNameByCallingWorker");
|
||||||
|
this.workers[whichWorker].postMessage(
|
||||||
|
{
|
||||||
|
action: "decryptName",
|
||||||
|
inputName: inputName,
|
||||||
|
},
|
||||||
|
[channel.port1]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptContentByCallingWorker(
|
||||||
|
input: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
await this.prepareByCallingWorker();
|
||||||
|
++this.workerIdx;
|
||||||
|
const whichWorker = this.workerIdx % this.workers.length;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port2.onmessage = (event) => {
|
||||||
|
// console.debug("main: receiving msg in encryptContentByCallingWorker");
|
||||||
|
const { outputContent } = event.data as RecvMsg;
|
||||||
|
if (outputContent === undefined) {
|
||||||
|
reject("unknown outputContent after encryptContentByCallingWorker");
|
||||||
|
} else {
|
||||||
|
resolve(outputContent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.port2.onmessageerror = (event) => {
|
||||||
|
// console.debug("main: receiving error in encryptContentByCallingWorker");
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug(
|
||||||
|
// "main: before postMessage in encryptContentByCallingWorker"
|
||||||
|
// );
|
||||||
|
this.workers[whichWorker].postMessage(
|
||||||
|
{
|
||||||
|
action: "encryptContent",
|
||||||
|
inputContent: input,
|
||||||
|
},
|
||||||
|
[channel.port1, input]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptContentByCallingWorker(
|
||||||
|
input: ArrayBuffer
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
await this.prepareByCallingWorker();
|
||||||
|
++this.workerIdx;
|
||||||
|
const whichWorker = this.workerIdx % this.workers.length;
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
|
channel.port2.onmessage = (event) => {
|
||||||
|
// console.debug("main: receiving msg in decryptContentByCallingWorker");
|
||||||
|
const { outputContent, status } = event.data as RecvMsg;
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
reject("error");
|
||||||
|
} else {
|
||||||
|
if (outputContent === undefined) {
|
||||||
|
reject("unknown outputContent after decryptContentByCallingWorker");
|
||||||
|
} else {
|
||||||
|
resolve(outputContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.port2.onmessageerror = (event) => {
|
||||||
|
// console.debug(
|
||||||
|
// "main: receiving onmessageerror in decryptContentByCallingWorker"
|
||||||
|
// );
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug(
|
||||||
|
// "main: before postMessage in decryptContentByCallingWorker"
|
||||||
|
// );
|
||||||
|
this.workers[whichWorker].postMessage(
|
||||||
|
{
|
||||||
|
action: "decryptContent",
|
||||||
|
inputContent: input,
|
||||||
|
},
|
||||||
|
[channel.port1, input]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { Cipher as CipherRCloneCryptPack } from "@fyears/rclone-crypt";
|
||||||
|
|
||||||
|
const ctx: WorkerGlobalScope = self as any;
|
||||||
|
|
||||||
|
const workerNanoID = nanoid();
|
||||||
|
const cipher = new CipherRCloneCryptPack("base64");
|
||||||
|
|
||||||
|
// console.debug(`worker [${workerNanoID}]: cipher created`);
|
||||||
|
|
||||||
|
async function encryptNameStr(input: string) {
|
||||||
|
const res = await cipher.encryptFileName(input);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptNameStr(input: string) {
|
||||||
|
return await cipher.decryptFileName(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptContentBuf(input: ArrayBuffer) {
|
||||||
|
return (await cipher.encryptData(new Uint8Array(input), undefined)).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptContentBuf(input: ArrayBuffer) {
|
||||||
|
return (await cipher.decryptData(new Uint8Array(input))).buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addEventListener("message", async (event: any) => {
|
||||||
|
const port: MessagePort = event.ports[0];
|
||||||
|
const {
|
||||||
|
action,
|
||||||
|
dataKeyBuf,
|
||||||
|
nameKeyBuf,
|
||||||
|
nameTweakBuf,
|
||||||
|
inputName,
|
||||||
|
inputContent,
|
||||||
|
} = event.data as {
|
||||||
|
action:
|
||||||
|
| "prepare"
|
||||||
|
| "encryptContent"
|
||||||
|
| "decryptContent"
|
||||||
|
| "encryptName"
|
||||||
|
| "decryptName";
|
||||||
|
dataKeyBuf?: ArrayBuffer;
|
||||||
|
nameKeyBuf?: ArrayBuffer;
|
||||||
|
nameTweakBuf?: ArrayBuffer;
|
||||||
|
inputName?: string;
|
||||||
|
inputContent?: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.debug(`worker [${workerNanoID}]: receiving action=${action}`);
|
||||||
|
|
||||||
|
if (action === "prepare") {
|
||||||
|
// console.debug(`worker [${workerNanoID}]: prepare: start`);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
dataKeyBuf === undefined ||
|
||||||
|
nameKeyBuf === undefined ||
|
||||||
|
nameTweakBuf === undefined
|
||||||
|
) {
|
||||||
|
// console.debug(`worker [${workerNanoID}]: prepare: no buffer??`);
|
||||||
|
throw Error(
|
||||||
|
`worker [${workerNanoID}]: prepare: internal keys not transferred to worker properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// console.debug(`worker [${workerNanoID}]: prepare: so we update`);
|
||||||
|
cipher.updateInternalKey(
|
||||||
|
new Uint8Array(dataKeyBuf),
|
||||||
|
new Uint8Array(nameKeyBuf),
|
||||||
|
new Uint8Array(nameTweakBuf)
|
||||||
|
);
|
||||||
|
port.postMessage({
|
||||||
|
status: "ok",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === "encryptName") {
|
||||||
|
try {
|
||||||
|
if (inputName === undefined) {
|
||||||
|
throw Error(
|
||||||
|
`worker [${workerNanoID}]: encryptName: internal inputName not transferred to worker properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputName = await encryptNameStr(inputName);
|
||||||
|
// console.debug(
|
||||||
|
// `worker [${workerNanoID}]: after encryptNameStr, before postMessage`
|
||||||
|
// );
|
||||||
|
port.postMessage({
|
||||||
|
status: "ok",
|
||||||
|
outputName: outputName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`worker [${workerNanoID}]: encryptName=${inputName}`);
|
||||||
|
console.error(error);
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === "decryptName") {
|
||||||
|
try {
|
||||||
|
if (inputName === undefined) {
|
||||||
|
throw Error(
|
||||||
|
`worker [${workerNanoID}]: decryptName: internal inputName not transferred to worker properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputName = await decryptNameStr(inputName);
|
||||||
|
// console.debug(
|
||||||
|
// `worker [${workerNanoID}]: after decryptNameStr, before postMessage`
|
||||||
|
// );
|
||||||
|
port.postMessage({
|
||||||
|
status: "ok",
|
||||||
|
outputName: outputName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`worker [${workerNanoID}]: decryptName=${inputName}`);
|
||||||
|
console.error(error);
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === "encryptContent") {
|
||||||
|
try {
|
||||||
|
if (inputContent === undefined) {
|
||||||
|
throw Error(
|
||||||
|
`worker [${workerNanoID}]: encryptContent: internal inputContent not transferred to worker properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputContent = await encryptContentBuf(inputContent);
|
||||||
|
// console.debug(
|
||||||
|
// `worker [${workerNanoID}]: after encryptContentBuf, before postMessage`
|
||||||
|
// );
|
||||||
|
port.postMessage(
|
||||||
|
{
|
||||||
|
status: "ok",
|
||||||
|
outputContent: outputContent,
|
||||||
|
},
|
||||||
|
[outputContent]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === "decryptContent") {
|
||||||
|
try {
|
||||||
|
if (inputContent === undefined) {
|
||||||
|
throw Error(
|
||||||
|
`worker [${workerNanoID}]: decryptContent: internal inputContent not transferred to worker properly`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputContent = await decryptContentBuf(inputContent);
|
||||||
|
// console.debug(
|
||||||
|
// `worker [${workerNanoID}]: after decryptContentBuf, before postMessage`
|
||||||
|
// );
|
||||||
|
port.postMessage(
|
||||||
|
{
|
||||||
|
status: "ok",
|
||||||
|
outputContent: outputContent,
|
||||||
|
},
|
||||||
|
[outputContent]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port.postMessage({
|
||||||
|
status: "error",
|
||||||
|
error: `worker [${workerNanoID}]: unknown action=${action}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { CipherMethodType } from "./baseTypes";
|
||||||
|
import * as openssl from "./encryptOpenSSL";
|
||||||
|
import * as rclone from "./encryptRClone";
|
||||||
|
import { isVaildText } from "./misc";
|
||||||
|
|
||||||
|
export class Cipher {
|
||||||
|
readonly password: string;
|
||||||
|
readonly method: CipherMethodType;
|
||||||
|
cipherRClone?: rclone.CipherRclone;
|
||||||
|
constructor(password: string, method: CipherMethodType) {
|
||||||
|
this.password = password ?? "";
|
||||||
|
this.method = method;
|
||||||
|
|
||||||
|
if (method === "rclone-base64") {
|
||||||
|
this.cipherRClone = new rclone.CipherRclone(password, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeResources() {
|
||||||
|
if (this.method === "rclone-base64" && this.cipherRClone !== undefined) {
|
||||||
|
this.cipherRClone.closeResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPasswordEmpty() {
|
||||||
|
return this.password === "";
|
||||||
|
}
|
||||||
|
|
||||||
|
isFolderAware() {
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.method === "rclone-base64") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw Error(`no idea about isFolderAware for method=${this.method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptContent(content: ArrayBuffer) {
|
||||||
|
// console.debug("start encryptContent");
|
||||||
|
if (this.password === "") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
const res = await openssl.encryptArrayBuffer(content, this.password);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot encrypt content`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else if (this.method === "rclone-base64") {
|
||||||
|
const res =
|
||||||
|
await this.cipherRClone!.encryptContentByCallingWorker(content);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot encrypt content`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported encrypt method=${this.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptContent(content: ArrayBuffer) {
|
||||||
|
// console.debug("start decryptContent");
|
||||||
|
if (this.password === "") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
const res = await openssl.decryptArrayBuffer(content, this.password);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot decrypt content`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else if (this.method === "rclone-base64") {
|
||||||
|
const res =
|
||||||
|
await this.cipherRClone!.decryptContentByCallingWorker(content);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot decrypt content`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported decrypt method=${this.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptName(name: string) {
|
||||||
|
// console.debug("start encryptName");
|
||||||
|
if (this.password === "") {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
const res = await openssl.encryptStringToBase64url(name, this.password);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot encrypt name=${name}`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else if (this.method === "rclone-base64") {
|
||||||
|
const res = await this.cipherRClone!.encryptNameByCallingWorker(name);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot encrypt name=${name}`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported encrypt method=${this.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptName(name: string): Promise<string> {
|
||||||
|
// console.debug("start decryptName");
|
||||||
|
if (this.password === "") {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
if (name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
||||||
|
// backward compitable with the openssl-base32
|
||||||
|
try {
|
||||||
|
const res = await openssl.decryptBase32ToString(name, this.password);
|
||||||
|
if (res !== undefined && isVaildText(res)) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`cannot decrypt name=${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw Error(`cannot decrypt name=${name}`);
|
||||||
|
}
|
||||||
|
} else if (name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
|
||||||
|
try {
|
||||||
|
const res = await openssl.decryptBase64urlToString(
|
||||||
|
name,
|
||||||
|
this.password
|
||||||
|
);
|
||||||
|
if (res !== undefined && isVaildText(res)) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`cannot decrypt name=${name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw Error(`cannot decrypt name=${name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error(
|
||||||
|
`method=${this.method} but the name=${name}, likely mismatch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (this.method === "rclone-base64") {
|
||||||
|
const res = await this.cipherRClone!.decryptNameByCallingWorker(name);
|
||||||
|
if (res === undefined) {
|
||||||
|
throw Error(`cannot decrypt name=${name}`);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported decrypt method=${this.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSizeFromOrigToEnc(x: number) {
|
||||||
|
if (this.password === "") {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
if (this.method === "openssl-base64") {
|
||||||
|
return openssl.getSizeFromOrigToEnc(x);
|
||||||
|
} else if (this.method === "rclone-base64") {
|
||||||
|
return rclone.getSizeFromOrigToEnc(x);
|
||||||
|
} else {
|
||||||
|
throw Error(`not supported encrypt method=${this.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick guess, no actual decryption here
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static isLikelyOpenSSLEncryptedName(name: string): boolean {
|
||||||
|
if (
|
||||||
|
name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE32) ||
|
||||||
|
name.startsWith(openssl.MAGIC_ENCRYPTED_PREFIX_BASE64URL)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick guess, no actual decryption here
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static isLikelyEncryptedName(name: string): boolean {
|
||||||
|
return Cipher.isLikelyOpenSSLEncryptedName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick guess, no actual decryption here, only openssl can be guessed here
|
||||||
|
* @param name
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static isLikelyEncryptedNameNotMatchMethod(
|
||||||
|
name: string,
|
||||||
|
method: CipherMethodType
|
||||||
|
): boolean {
|
||||||
|
if (
|
||||||
|
Cipher.isLikelyOpenSSLEncryptedName(name) &&
|
||||||
|
method !== "openssl-base64"
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Cipher.isLikelyOpenSSLEncryptedName(name) &&
|
||||||
|
method === "openssl-base64"
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-2
@@ -5,16 +5,28 @@ import {
|
|||||||
COMMAND_URI,
|
COMMAND_URI,
|
||||||
UriParams,
|
UriParams,
|
||||||
RemotelySavePluginSettings,
|
RemotelySavePluginSettings,
|
||||||
|
QRExportType,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
|
import { getShrinkedSettings } from "./remoteForOnedrive";
|
||||||
|
|
||||||
export const exportQrCodeUri = async (
|
export const exportQrCodeUri = async (
|
||||||
settings: RemotelySavePluginSettings,
|
settings: RemotelySavePluginSettings,
|
||||||
currentVaultName: string,
|
currentVaultName: string,
|
||||||
pluginVersion: string
|
pluginVersion: string,
|
||||||
|
exportFields: QRExportType
|
||||||
) => {
|
) => {
|
||||||
const settings2: Partial<RemotelySavePluginSettings> = cloneDeep(settings);
|
let settings2: Partial<RemotelySavePluginSettings> = {};
|
||||||
|
|
||||||
|
if (exportFields === "all_but_oauth2") {
|
||||||
|
settings2 = cloneDeep(settings);
|
||||||
delete settings2.dropbox;
|
delete settings2.dropbox;
|
||||||
delete settings2.onedrive;
|
delete settings2.onedrive;
|
||||||
|
} else if (exportFields === "dropbox") {
|
||||||
|
settings2 = { dropbox: cloneDeep(settings.dropbox) };
|
||||||
|
} else if (exportFields === "onedrive") {
|
||||||
|
settings2 = { onedrive: getShrinkedSettings(settings.onedrive) };
|
||||||
|
}
|
||||||
|
|
||||||
delete settings2.vaultRandomID;
|
delete settings2.vaultRandomID;
|
||||||
const data = encodeURIComponent(JSON.stringify(settings2));
|
const data = encodeURIComponent(JSON.stringify(settings2));
|
||||||
const vault = encodeURIComponent(currentVaultName);
|
const vault = encodeURIComponent(currentVaultName);
|
||||||
@@ -34,6 +46,20 @@ export interface ProcessQrCodeResultType {
|
|||||||
result?: RemotelySavePluginSettings;
|
result?: RemotelySavePluginSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* we also support directly parse the uri, instead of relying on web browser
|
||||||
|
* @param input
|
||||||
|
*/
|
||||||
|
export const parseUriByHand = (input: string) => {
|
||||||
|
if (!input.startsWith("obsidian://remotely-save?func=settings&")) {
|
||||||
|
throw Error(`not valid string`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const k = new URL(input);
|
||||||
|
const output = Object.fromEntries(k.searchParams);
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
export const importQrCodeUri = (
|
export const importQrCodeUri = (
|
||||||
inputParams: any,
|
inputParams: any,
|
||||||
currentVaultName: string
|
currentVaultName: string
|
||||||
|
|||||||
+32
-11
@@ -5,7 +5,7 @@
|
|||||||
"goback": "Go Back",
|
"goback": "Go Back",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"sometext": "Here are some texts.",
|
"sometext": "Here are some texts.",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} already running in stage {{syncStatus}}!",
|
"syncrun_alreadyrunning": "New command {{newTriggerSource}} stops because {{pluginName}} is already running in stage {{syncStatus}}!",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}: syncing from {{triggerSource}}",
|
"syncrun_syncingribbon": "{{pluginName}}: syncing from {{triggerSource}}",
|
||||||
"syncrun_step0": "0/8 Remotely Save is running in dry mode, thus not actual file changes would happen.",
|
"syncrun_step0": "0/8 Remotely Save is running in dry mode, thus not actual file changes would happen.",
|
||||||
"syncrun_step1": "1/8 Remotely Save is preparing ({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save is preparing ({{serviceType}})",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"syncrun_shortstep2": "2/2 Remotely Save finished!",
|
"syncrun_shortstep2": "2/2 Remotely Save finished!",
|
||||||
"syncrun_abort": "{{manifestID}}-{{theDate}}: abort sync, triggerSource={{triggerSource}}, error while {{syncStatus}}",
|
"syncrun_abort": "{{manifestID}}-{{theDate}}: abort sync, triggerSource={{triggerSource}}, error while {{syncStatus}}",
|
||||||
"syncrun_abort_protectmodifypercentage": "Abort! you set changing files >= {{protectModifyPercentage}}% is not allowed but {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% is going to be modified or deleted! If you are sure you want this sync, please adjust the allowed ratio in the settings.",
|
"syncrun_abort_protectmodifypercentage": "Abort! you set changing files >= {{protectModifyPercentage}}% is not allowed but {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% is going to be modified or deleted! If you are sure you want this sync, please adjust the allowed ratio in the settings.",
|
||||||
"protocol_saveqr": "New not-oauth2 settings for {{manifestName}} is saved. Reopen the plugin settings to make it effective.",
|
"protocol_saveqr": "New settings for {{manifestName}} is imported and saved. Reopen the plugin settings to make it effective.",
|
||||||
"protocol_callbacknotsupported": "Your uri calls a callback that's not supported yet: {{params}}",
|
"protocol_callbacknotsupported": "Your uri calls a callback that's not supported yet: {{params}}",
|
||||||
"protocol_dropbox_connecting": "Connecting to Dropbox...\nPlease DO NOT close this modal.",
|
"protocol_dropbox_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": "Good! We've connected to Dropbox as user {{username}}!",
|
||||||
@@ -38,7 +38,10 @@
|
|||||||
"protocol_onedrive_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
"protocol_onedrive_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||||
"command_startsync": "start sync",
|
"command_startsync": "start sync",
|
||||||
"command_drynrun": "start sync (dry run only)",
|
"command_drynrun": "start sync (dry run only)",
|
||||||
"command_exportsyncplans_json": "export sync plans in json format",
|
"command_exportsyncplans_1": "export sync plans (latest 1)",
|
||||||
|
"command_exportsyncplans_5": "export sync plans (latest 5)",
|
||||||
|
"command_exportsyncplans_all": "export sync plans (all)",
|
||||||
|
|
||||||
"command_exportlogsindb": "export logs saved in db",
|
"command_exportlogsindb": "export logs saved in db",
|
||||||
|
|
||||||
"statusbar_time_years": "Synced {{time}} years ago",
|
"statusbar_time_years": "Synced {{time}} years ago",
|
||||||
@@ -64,6 +67,8 @@
|
|||||||
"modal_password_attn5": "Attention 5/5: The longer the password, the better.",
|
"modal_password_attn5": "Attention 5/5: The longer the password, the better.",
|
||||||
"modal_password_secondconfirm": "The Second Confirm to change password.",
|
"modal_password_secondconfirm": "The Second Confirm to change password.",
|
||||||
"modal_password_notice": "New password saved!",
|
"modal_password_notice": "New password saved!",
|
||||||
|
"modal_encryptionmethod_title": "Hold on and PLEASE READ ON...",
|
||||||
|
"modal_encryptionmethod_shortdesc": "You are changing the encrpytion method but you have set the password before.\nAfter switching the method, you need to <b>manually</b> and <b>fully</b> delete every encrypted vault files in the remote and re-sync (so that re-upload) the newly encrypted files again.",
|
||||||
"modal_remotebasedir_title": "You are changing the remote base directory config",
|
"modal_remotebasedir_title": "You are changing the remote base directory config",
|
||||||
"modal_remotebasedir_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the config would be reset to use the vault folder name (the default config).\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.",
|
"modal_remotebasedir_shortdesc": "1. The plugin would NOT automatically move the content from the old directory to the new one directly on the remote. Everything syncs from the beginning again.\n2. If you set the string to the empty, the config would be reset to use the vault folder name (the default config).\n3. The remote directory name itself would not be encrypted even you've set an E2E password.\n4. Some special char like '?', '/', '\\' are not allowed. Spaces in the beginning or in the end are also trimmed.",
|
||||||
"modal_remotebasedir_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
|
"modal_remotebasedir_invaliddirhint": "Your input contains special characters like '?', '/', '\\' which are not allowed.",
|
||||||
@@ -88,6 +93,7 @@
|
|||||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
"modal_dropboxauth_maualinput_conn_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||||
"modal_dropboxauth_maualinput_conn_fail": "Something goes wrong while connecting to Dropbox.",
|
"modal_dropboxauth_maualinput_conn_fail": "Something goes wrong while connecting to Dropbox.",
|
||||||
"modal_onedriveauth_shortdesc": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).\nVisit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.",
|
"modal_onedriveauth_shortdesc": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).\nVisit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.",
|
||||||
|
"modal_onedriveauth_shortdesc_linux": "It seems that you are using Obsidian on Linux, and you might not be able to jump back here properly. Please consider <a href=\"https://github.com/remotely-save/remotely-save/issues/415\">using</a> the flatpack version of Obsidian, or creating an <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> file</a>.",
|
||||||
"modal_onedriveauth_copybutton": "Click to copy the auth url",
|
"modal_onedriveauth_copybutton": "Click to copy the auth url",
|
||||||
"modal_onedriveauth_copynotice": "The auth url is copied to the clipboard!",
|
"modal_onedriveauth_copynotice": "The auth url is copied to the clipboard!",
|
||||||
"modal_onedriverevokeauth_step1": "Step 1: Go to the following address, click the \"Edit\" button for the plugin, then click \"Remove these permissions\" button on the page.",
|
"modal_onedriverevokeauth_step1": "Step 1: Go to the following address, click the \"Edit\" button for the plugin, then click \"Remove these permissions\" button on the page.",
|
||||||
@@ -100,7 +106,7 @@
|
|||||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other startting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other startting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||||
"modal_syncconfig_secondconfirm": "The Second Confirm To Enable.",
|
"modal_syncconfig_secondconfirm": "The Second Confirm To Enable.",
|
||||||
"modal_syncconfig_notice": "You've enabled syncing config folder!",
|
"modal_syncconfig_notice": "You've enabled syncing config folder!",
|
||||||
"modal_qr_shortdesc": "This exports not-oauth2 settings. (It means that Dropbox, OneDrive info are NOT exported.)\nYou can use another device to scan this qrcode.\nOr, you can click the button to copy the special url.",
|
"modal_qr_shortdesc": "This exports (partial) settings.\nYou can use another device to scan this qrcode.\nOr, you can click the button to copy the special uri and paste it into another device's web browser or Remotely Save Import Setting.",
|
||||||
"modal_qr_button": "Click to copy the special URI",
|
"modal_qr_button": "Click to copy the special URI",
|
||||||
"modal_qr_button_notice": "The special uri is copied to the clipboard!",
|
"modal_qr_button_notice": "The special uri is copied to the clipboard!",
|
||||||
"modal_sizesconflict_title": "Remotely Save: Some conflict were found while skipping large files",
|
"modal_sizesconflict_title": "Remotely Save: Some conflict were found while skipping large files",
|
||||||
@@ -109,7 +115,12 @@
|
|||||||
"modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!",
|
"modal_sizesconflict_copynotice": "All the sizes conflicts info have been copied to the clipboard!",
|
||||||
"settings_basic": "Basic Settings",
|
"settings_basic": "Basic Settings",
|
||||||
"settings_password": "Encryption Password",
|
"settings_password": "Encryption Password",
|
||||||
"settings_password_desc": "Password for E2E encryption. Empty for no password. You need to click \"Confirm\". Attention: the password and other info are saved locally.",
|
"settings_password_desc": "Password for E2E encryption. Empty for no password. You need to click \"Confirm\". Attention: The password and other info are saved locally. After changing the password, you need to manually delete every original files in the remote, and re-sync (so that upload) the encrypted files again.",
|
||||||
|
"settings_encryptionmethod": "Encryption Method",
|
||||||
|
"settings_encryptionmethod_desc": "Encryption method for E2E encryption. RClone Crypt format is recommended but it doesn't encrypt path structure. OpenSSL enc is the legacy format of this plugin. <b>Both are not affliated with official RClone and OpenSSL product or community.</b> Attention: After switching the method, you need to manually delete every original files in the remote and re-sync (so that upload) the encrypted files again. More info in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>online doc</a>.",
|
||||||
|
"settings_encryptionmethod_rclone": "RClone Crypt (recommended)",
|
||||||
|
"settings_encryptionmethod_openssl": "OpenSSL enc (legacy)",
|
||||||
|
|
||||||
"settings_autorun": "Schedule For Auto Run",
|
"settings_autorun": "Schedule For Auto Run",
|
||||||
"settings_autorun_desc": "The plugin tries to schedule the running after every interval. Battery may be impacted.",
|
"settings_autorun_desc": "The plugin tries to schedule the running after every interval. Battery may be impacted.",
|
||||||
"settings_autorun_notset": "(not set)",
|
"settings_autorun_notset": "(not set)",
|
||||||
@@ -264,14 +275,18 @@
|
|||||||
"setting_syncdirection_bidirectional_desc": "Bidirectional (default)",
|
"setting_syncdirection_bidirectional_desc": "Bidirectional (default)",
|
||||||
"setting_syncdirection_incremental_push_only_desc": "Incremental Push Only (aka backup mode)",
|
"setting_syncdirection_incremental_push_only_desc": "Incremental Push Only (aka backup mode)",
|
||||||
"setting_syncdirection_incremental_pull_only_desc": "Incremental Pull Only",
|
"setting_syncdirection_incremental_pull_only_desc": "Incremental Pull Only",
|
||||||
"settings_enablemobilestatusbar": "Enable Mobile Status Bar Or Not",
|
"settings_enablemobilestatusbar": "Mobile Status Bar (experimental)",
|
||||||
"settings_enablemobilestatusbar_desc": "By default Obsidian mobile hides status bar. But some users want to show it up. So here is a hack.",
|
"settings_enablemobilestatusbar_desc": "By default Obsidian mobile hides status bar. But some users want to show it up. So here is a hack.",
|
||||||
"settings_importexport": "Import and Export Partial Settings",
|
"settings_importexport": "Import and Export Partial Settings",
|
||||||
"settings_export": "Export",
|
"settings_export": "Export",
|
||||||
"settings_export_desc": "Export not-oauth2 settings by generating a qrcode.",
|
"settings_export_desc": "Export settings by generating a QR code or URI.",
|
||||||
"settings_export_desc_button": "Get QR Code",
|
"settings_export_all_but_oauth2_button": "Export Non-Oauth2 Part",
|
||||||
|
"settings_export_dropbox_button": "Export Dropbox Part",
|
||||||
|
"settings_export_onedrive_button": "Export OneDrive Part",
|
||||||
"settings_import": "Import",
|
"settings_import": "Import",
|
||||||
"settings_import_desc": "You should open a camera or scan-qrcode app, to manually scan the QR code.",
|
"settings_import_desc": "Paste the exported URI into here and click \"Import\". Or, you can open a camera or scan-qrcode app to scan the QR code.",
|
||||||
|
"settings_import_button": "Import",
|
||||||
|
"settings_import_error_notice": "Your URI string is empty or not correct!",
|
||||||
"settings_debug": "Debug",
|
"settings_debug": "Debug",
|
||||||
"settings_debuglevel": "Alter Notice Level",
|
"settings_debuglevel": "Alter Notice Level",
|
||||||
"settings_debuglevel_desc": "By default the notice level is \"info\". You can change to \"debug\" to get verbose information while syncing.",
|
"settings_debuglevel_desc": "By default the notice level is \"info\". You can change to \"debug\" to get verbose information while syncing.",
|
||||||
@@ -285,7 +300,9 @@
|
|||||||
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
||||||
"settings_syncplans": "Export Sync Plans",
|
"settings_syncplans": "Export Sync Plans",
|
||||||
"settings_syncplans_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
|
"settings_syncplans_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_button_1": "Export latest 1",
|
||||||
|
"settings_syncplans_button_5": "Export latest 5",
|
||||||
|
"settings_syncplans_button_all": "Export All",
|
||||||
"settings_syncplans_notice": "Sync plans history exported.",
|
"settings_syncplans_notice": "Sync plans history exported.",
|
||||||
"settings_delsyncplans": "Delete Sync Plans History In DB",
|
"settings_delsyncplans": "Delete Sync Plans History In DB",
|
||||||
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
|
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
|
||||||
@@ -295,6 +312,10 @@
|
|||||||
"settings_delprevsync_desc": "The sync algorithm keeps the previous successful sync information in DB to determine the file changes. If you want to ignore them so that all files are treated newly created, you can delete the prev sync info here.",
|
"settings_delprevsync_desc": "The sync algorithm keeps the previous successful sync information in DB to determine the file changes. If you want to ignore them so that all files are treated newly created, you can delete the prev sync info here.",
|
||||||
"settings_delprevsync_button": "Delete Prev Sync Details",
|
"settings_delprevsync_button": "Delete Prev Sync Details",
|
||||||
"settings_delprevsync_notice": "Previous sync history (in local DB) deleted",
|
"settings_delprevsync_notice": "Previous sync history (in local DB) deleted",
|
||||||
|
"settings_profiler_results": "Export Profiler Results",
|
||||||
|
"settings_profiler_results_desc": "The plugin records the time cost of each steps. Here you can export them to know which step is slow.",
|
||||||
|
"settings_profiler_results_notice": "Profiler results exported.",
|
||||||
|
"settings_profiler_results_button_all": "Export All",
|
||||||
"settings_outputbasepathvaultid": "Output Vault Base Path And Randomly Assigned ID",
|
"settings_outputbasepathvaultid": "Output Vault Base Path And Randomly Assigned ID",
|
||||||
"settings_outputbasepathvaultid_desc": "For debugging purposes.",
|
"settings_outputbasepathvaultid_desc": "For debugging purposes.",
|
||||||
"settings_outputbasepathvaultid_button": "Output",
|
"settings_outputbasepathvaultid_button": "Output",
|
||||||
@@ -303,7 +324,7 @@
|
|||||||
"settings_resetcache_button": "Reset",
|
"settings_resetcache_button": "Reset",
|
||||||
"settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.",
|
"settings_resetcache_notice": "Local internal cache/databases deleted. Please manually reload the plugin.",
|
||||||
"syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm",
|
"syncalgov3_title": "Remotely Save has HUGE updates on the sync algorithm",
|
||||||
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
|
"syncalgov3_texts": "Welcome to use Remotely Save!\nFrom this version, a new algorithm has been developed:\n<ul><li>More robust deletion sync,</li><li>minimal conflict handling,</li><li>no meta data uploaded any more,</li><li>deletion / modification protection,</li><li>backup mode</li><li>new encryption method</li><li>...</li></ul>\nStay tune for more! A full introduction is in the <a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>doc website</a>.\nIf you agree to use this, please read and check two checkboxes then click the \"Agree\" button, and enjoy the plugin!\nIf you do not agree, please click the \"Do Not Agree\" button, the plugin will unload itself.\nAlso, please consider <a href='https://github.com/remotely-save/remotely-save'>visit the GitHub repo and star ⭐ it</a>! Or even <a href='https://github.com/remotely-save/donation'>buy me a coffee</a>. Your support is very important to me! Thanks!",
|
||||||
"syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.",
|
"syncalgov3_checkbox_manual_backup": "I will backup my vault manually firstly.",
|
||||||
"syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.",
|
"syncalgov3_checkbox_requiremultidevupdate": "I understand I need to update the plugin ACROSS ALL DEVICES to make them work properly.",
|
||||||
"syncalgov3_button_agree": "Agree",
|
"syncalgov3_button_agree": "Agree",
|
||||||
|
|||||||
+30
-10
@@ -5,7 +5,7 @@
|
|||||||
"goback": "返回",
|
"goback": "返回",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"sometext": "这里有一段文字。",
|
"sometext": "这里有一段文字。",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} 正处于此阶段:{{syncStatus}}!",
|
"syncrun_alreadyrunning": "{{pluginName}} 正处于此阶段:{{syncStatus}}!中断触发 {{newTriggerSource}}。",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 触发运行",
|
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 触发运行",
|
||||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
||||||
"syncrun_step1": "1/8 Remotely Save 准备同步({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save 准备同步({{serviceType}})",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
||||||
"syncrun_abort": "{{manifestID}}-{{theDate}}:中断同步,同步来源={{triggerSource}},出错阶段={{syncStatus}}",
|
"syncrun_abort": "{{manifestID}}-{{theDate}}:中断同步,同步来源={{triggerSource}},出错阶段={{syncStatus}}",
|
||||||
"syncrun_abort_protectmodifypercentage": "中断同步!您设置了不允许 >= {{protectModifyPercentage}}% 的变更,但是现在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的文件会被修改或删除!如果您确认这次同步是您想要的,那么请在设置里修改允许比例。",
|
"syncrun_abort_protectmodifypercentage": "中断同步!您设置了不允许 >= {{protectModifyPercentage}}% 的变更,但是现在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的文件会被修改或删除!如果您确认这次同步是您想要的,那么请在设置里修改允许比例。",
|
||||||
"protocol_saveqr": " {{manifestName}} 新的非 oauth2 设置保存完成。请重启插件设置页使之生效。",
|
"protocol_saveqr": " {{manifestName}} 的新设置导入完成。请重启插件设置页使之生效。",
|
||||||
"protocol_callbacknotsupported": "您的 uri callback 暂不支持: {{params}}",
|
"protocol_callbacknotsupported": "您的 uri callback 暂不支持: {{params}}",
|
||||||
"protocol_dropbox_connecting": "正在连接 Dropbox……\n请不要关闭此弹窗。",
|
"protocol_dropbox_connecting": "正在连接 Dropbox……\n请不要关闭此弹窗。",
|
||||||
"protocol_dropbox_connect_succ": "好!我们作为用户 {{username}} 连接上了 Dropbox!",
|
"protocol_dropbox_connect_succ": "好!我们作为用户 {{username}} 连接上了 Dropbox!",
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
"command_startsync": "开始同步(start sync)",
|
"command_startsync": "开始同步(start sync)",
|
||||||
"command_drynrun": "开始同步(空跑模式)(start sync (dry run only))",
|
"command_drynrun": "开始同步(空跑模式)(start sync (dry run only))",
|
||||||
"command_exportsyncplans_json": "导出同步计划为 json 格式(export sync plans in json format)",
|
"command_exportsyncplans_json": "导出同步计划为 json 格式(export sync plans in json format)",
|
||||||
|
"command_exportsyncplans_1": "导出同步计划(最近 1 次)(export sync plans (latest 1))",
|
||||||
|
"command_exportsyncplans_5": "导出同步计划(最近 5 次)(export sync plans (latest 5))",
|
||||||
|
"command_exportsyncplans_all": "导出同步计划(所有)(export sync plans (all))",
|
||||||
"command_exportlogsindb": "从数据库导出终端日志(export logs saved in db)",
|
"command_exportlogsindb": "从数据库导出终端日志(export logs saved in db)",
|
||||||
|
|
||||||
"statusbar_time_years": "{{time}} 年前同步",
|
"statusbar_time_years": "{{time}} 年前同步",
|
||||||
@@ -64,6 +67,8 @@
|
|||||||
"modal_password_attn5": "注意 5/5:密码越长越好。",
|
"modal_password_attn5": "注意 5/5:密码越长越好。",
|
||||||
"modal_password_secondconfirm": "再次确认保存新密码",
|
"modal_password_secondconfirm": "再次确认保存新密码",
|
||||||
"modal_password_notice": "新密码已保存!",
|
"modal_password_notice": "新密码已保存!",
|
||||||
|
"modal_encryptionmethod_title": "稍等一下,请阅读下文:",
|
||||||
|
"modal_encryptionmethod_shortdesc": "您正在修改加密方式,但是您已经设置了密码。\n修改加密方式之后,您需要<b>手动</b>和<b>完全</b>删除在远端的之前加密过的库文件,然后重新同步(从而重新上传)新的加密文件。",
|
||||||
"modal_remotebasedir_title": "您正在修改远端基文件夹设置",
|
"modal_remotebasedir_title": "您正在修改远端基文件夹设置",
|
||||||
"modal_remotebasedir_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置会被重设回库的文件夹名(默认设置)。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
|
"modal_remotebasedir_shortdesc": "1. 本插件并不会自动在远端把内容从旧文件夹移动到新文件夹。所有内容都会重新同步。\n2. 如果你使得文本输入框为空,那么本设置会被重设回库的文件夹名(默认设置)。\n3. 即使您设置了端对端加密的密码,远端文件夹名称本身也不会被加密。\n4. 某些特殊字符,如“?”、“/”、“\\”是不允许的。文本前后的空格也会被自动删去。",
|
||||||
"modal_remotebasedir_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
|
"modal_remotebasedir_invaliddirhint": "您所输入的内容含有某些特殊字符,如“?”、“/”、“\\”,它们是不允许的。",
|
||||||
@@ -88,6 +93,7 @@
|
|||||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作为用户 {{username}} 连接到 Dropbox。如果您想断开连接,点击此按钮。",
|
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作为用户 {{username}} 连接到 Dropbox。如果您想断开连接,点击此按钮。",
|
||||||
"modal_dropboxauth_maualinput_conn_fail": "连接 Dropbox 途中出错了。",
|
"modal_dropboxauth_maualinput_conn_fail": "连接 Dropbox 途中出错了。",
|
||||||
"modal_onedriveauth_shortdesc": "现在只支持个人版 OneDrive,(暂)不支持企业版。\n在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。",
|
"modal_onedriveauth_shortdesc": "现在只支持个人版 OneDrive,(暂)不支持企业版。\n在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。",
|
||||||
|
"modal_onedriveauth_shortdesc_linux": "您正在用 Linux,有可能无法跳转回来。请考虑<a href=\"https://github.com/remotely-save/remotely-save/issues/415\">使用</a> flatpack 版本的 Obsidian,或创建 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> 文件</a>。",
|
||||||
"modal_onedriveauth_copybutton": "点击此按钮从而复制鉴权 url",
|
"modal_onedriveauth_copybutton": "点击此按钮从而复制鉴权 url",
|
||||||
"modal_onedriveauth_copynotice": "鉴权 url 已复制到剪贴板!",
|
"modal_onedriveauth_copynotice": "鉴权 url 已复制到剪贴板!",
|
||||||
"modal_onedriverevokeauth_step1": "第 1 步:用浏览器打开以下地址,点击本插件对应的“Edit”按钮,点击“Remove these permissions”按钮。",
|
"modal_onedriverevokeauth_step1": "第 1 步:用浏览器打开以下地址,点击本插件对应的“Edit”按钮,点击“Remove these permissions”按钮。",
|
||||||
@@ -100,7 +106,7 @@
|
|||||||
"modal_syncconfig_attn": "注意 1/2:此设置只同步(复制)整个 Obsidian 的配置文件夹,但是不会同步其它 . 开头的文件夹或文件。除了会忽略 .git 和 node_modules 文件夹之外,它也并不理解配置文件夹的里各个子文件或子文件夹的含义。\n注意 2/2:配置文件夹被同步之后,各插件的设置或许会出错,且 Obsidian 或许需要重启来重载各插件的新配置。\n如果您同意自行承受以上风险,您可以点击以下再次确认按钮。",
|
"modal_syncconfig_attn": "注意 1/2:此设置只同步(复制)整个 Obsidian 的配置文件夹,但是不会同步其它 . 开头的文件夹或文件。除了会忽略 .git 和 node_modules 文件夹之外,它也并不理解配置文件夹的里各个子文件或子文件夹的含义。\n注意 2/2:配置文件夹被同步之后,各插件的设置或许会出错,且 Obsidian 或许需要重启来重载各插件的新配置。\n如果您同意自行承受以上风险,您可以点击以下再次确认按钮。",
|
||||||
"modal_syncconfig_secondconfirm": "再次确认开启",
|
"modal_syncconfig_secondconfirm": "再次确认开启",
|
||||||
"modal_syncconfig_notice": "您已开启配置文件夹的同步!",
|
"modal_syncconfig_notice": "您已开启配置文件夹的同步!",
|
||||||
"modal_qr_shortdesc": "这里可导出非 oauth2 设置。(意味着:Dropbox 和 OneDrive 信息不会被导出。)\n您可以使用另一个设备来扫描此 QR 码。\n又或者,您可以点击以下按钮复制此特殊 URI。",
|
"modal_qr_shortdesc": "这里可导出(部分)设置。\n您可以使用另一个设备来扫描此 QR 码。\n又或者,您可以点击以下按钮复制此特殊 URI,然后粘贴到另一台设备的网络浏览器或 Remotely Save 设置里的导入部分。",
|
||||||
"modal_qr_button": "点击此按钮复制特殊 URI",
|
"modal_qr_button": "点击此按钮复制特殊 URI",
|
||||||
"modal_qr_button_notice": "特殊 URI 已被复制到剪贴板!",
|
"modal_qr_button_notice": "特殊 URI 已被复制到剪贴板!",
|
||||||
"modal_sizesconflict_title": "Remotely Save:跳过大文件的时候出现了一些冲突",
|
"modal_sizesconflict_title": "Remotely Save:跳过大文件的时候出现了一些冲突",
|
||||||
@@ -109,7 +115,11 @@
|
|||||||
"modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!",
|
"modal_sizesconflict_copynotice": "所有的文件大小冲突信息,已被复制到剪贴板!",
|
||||||
"settings_basic": "基本设置",
|
"settings_basic": "基本设置",
|
||||||
"settings_password": "密码",
|
"settings_password": "密码",
|
||||||
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。",
|
"settings_password_desc": "端到端加密的密码。不填写则代表没密码。您需要点击“确认”来修改。注意:密码和其它信息都会在本地保存。如果您修改了密码,您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。",
|
||||||
|
"settings_encryptionmethod": "加密方法",
|
||||||
|
"settings_encryptionmethod_desc": "端到端加密的方法。推荐选用 RClone Crypt 方法,但是它没有加密文件路径结构。OpenSSL enc 是本插件一开始就支持的方式。<b>两种方法都和 RClone、OpenSSL 官方产品和社区无利益相关。</b>如果您修改了加密方法,您需要手动删除远端的所有文件,重新同步(从而上传)加密文件。更多详细说明见<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>在线文档</a>。",
|
||||||
|
"settings_encryptionmethod_rclone": "RClone Crypt(推荐)",
|
||||||
|
"settings_encryptionmethod_openssl": "OpenSSL enc(旧方法)",
|
||||||
"settings_autorun": "自动运行",
|
"settings_autorun": "自动运行",
|
||||||
"settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。",
|
"settings_autorun_desc": "每隔一段时间,此插件尝试自动同步。会影响到电池用量。",
|
||||||
"settings_autorun_notset": "(不设置)",
|
"settings_autorun_notset": "(不设置)",
|
||||||
@@ -264,14 +274,18 @@
|
|||||||
"setting_syncdirection_bidirectional_desc": "双向同步(默认)",
|
"setting_syncdirection_bidirectional_desc": "双向同步(默认)",
|
||||||
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:备份模式)",
|
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:备份模式)",
|
||||||
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
||||||
"settings_enablemobilestatusbar": "是否显示手机的状态栏",
|
"settings_enablemobilestatusbar": "手机的状态栏(实验性质)",
|
||||||
"settings_enablemobilestatusbar_desc": "Obsidian 手机版默认隐藏了状态栏。有些用户希望展示它。这里提供了设置选项。",
|
"settings_enablemobilestatusbar_desc": "Obsidian 手机版默认隐藏了状态栏。有些用户希望展示它。这里提供了设置选项。",
|
||||||
"settings_importexport": "导入导出部分设置",
|
"settings_importexport": "导入导出部分设置",
|
||||||
"settings_export": "导出",
|
"settings_export": "导出",
|
||||||
"settings_export_desc": "用 QR 码导出非 oauth2 的设置信息。",
|
"settings_export_desc": "用 QR 码或 URI 导出设置信息。",
|
||||||
"settings_export_desc_button": "生成 QR 码",
|
"settings_export_all_but_oauth2_button": "导出非 Oauth2 部分",
|
||||||
|
"settings_export_dropbox_button": "导出 Dropbox 部分",
|
||||||
|
"settings_export_onedrive_button": "导出 OneDrive 部分",
|
||||||
"settings_import": "导入",
|
"settings_import": "导入",
|
||||||
"settings_import_desc": "您需要使用系统拍摄 app 或者扫描 QR 码的app,来扫描对应的 QR 码。",
|
"settings_import_desc": "粘贴之前导出的 URI 到这里然后点击“导入”。或,使用拍摄 app 或者扫描 QR 码的 app,来扫描对应的 QR 码。",
|
||||||
|
"settings_import_button": "导入",
|
||||||
|
"settings_import_error_notice": "您输入的 URI 是空的或者不准确的!",
|
||||||
"settings_debug": "调试",
|
"settings_debug": "调试",
|
||||||
"settings_debuglevel": "修改同步提示信息",
|
"settings_debuglevel": "修改同步提示信息",
|
||||||
"settings_debuglevel_desc": "默认值为 \"info\"。您可以改为 \"debug\" 从而在同步时候里获取更多信息。",
|
"settings_debuglevel_desc": "默认值为 \"info\"。您可以改为 \"debug\" 从而在同步时候里获取更多信息。",
|
||||||
@@ -285,7 +299,9 @@
|
|||||||
"settings_viewconsolelog_desc": "电脑上,输入“ctrl+shift+i”或“cmd+shift+i”来查看终端输出。手机上,安装第三方插件 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 来导出终端输出到一篇笔记上。",
|
"settings_viewconsolelog_desc": "电脑上,输入“ctrl+shift+i”或“cmd+shift+i”来查看终端输出。手机上,安装第三方插件 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 来导出终端输出到一篇笔记上。",
|
||||||
"settings_syncplans": "导出同步计划",
|
"settings_syncplans": "导出同步计划",
|
||||||
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
|
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
|
||||||
"settings_syncplans_button_json": "导出",
|
"settings_syncplans_button_1": "导出最近 1 次",
|
||||||
|
"settings_syncplans_button_5": "导出最近 5 次",
|
||||||
|
"settings_syncplans_button_all": "导出所有",
|
||||||
"settings_syncplans_notice": "同步计划已导出",
|
"settings_syncplans_notice": "同步计划已导出",
|
||||||
"settings_delsyncplans": "删除数据库里的同步计划历史",
|
"settings_delsyncplans": "删除数据库里的同步计划历史",
|
||||||
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
|
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
|
||||||
@@ -295,6 +311,10 @@
|
|||||||
"settings_delprevsync_desc": "同步算法需要上次成功同步的信息来决定文件变更,这个信息保存在本地的数据库里。如果您想忽略这些信息从而所有文件都被视为新创建的话,可以在此删除之前的信息。",
|
"settings_delprevsync_desc": "同步算法需要上次成功同步的信息来决定文件变更,这个信息保存在本地的数据库里。如果您想忽略这些信息从而所有文件都被视为新创建的话,可以在此删除之前的信息。",
|
||||||
"settings_delprevsync_button": "删除上次同步明细",
|
"settings_delprevsync_button": "删除上次同步明细",
|
||||||
"settings_delprevsync_notice": "(本地数据库里的)上次同步明细已被删除。",
|
"settings_delprevsync_notice": "(本地数据库里的)上次同步明细已被删除。",
|
||||||
|
"settings_profiler_results": "导出性能数据记录",
|
||||||
|
"settings_profiler_results_desc": "插件记录了每次同步每一步的耗时。这里可以导出记录得知哪一步最慢。",
|
||||||
|
"settings_profiler_results_notice": "性能数据已导出",
|
||||||
|
"settings_profiler_results_button_all": "导出所有",
|
||||||
"settings_outputbasepathvaultid": "输出资料库对应的位置和随机分配的 ID",
|
"settings_outputbasepathvaultid": "输出资料库对应的位置和随机分配的 ID",
|
||||||
"settings_outputbasepathvaultid_desc": "用于调试。",
|
"settings_outputbasepathvaultid_desc": "用于调试。",
|
||||||
"settings_outputbasepathvaultid_button": "输出",
|
"settings_outputbasepathvaultid_button": "输出",
|
||||||
@@ -303,7 +323,7 @@
|
|||||||
"settings_resetcache_button": "重设",
|
"settings_resetcache_button": "重设",
|
||||||
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
|
"settings_resetcache_notice": "本地同步缓存和数据库已被删除。请手动重新载入此插件。",
|
||||||
"syncalgov3_title": "Remotely Save 的同步算法有重大更新",
|
"syncalgov3_title": "Remotely Save 的同步算法有重大更新",
|
||||||
"syncalgov3_texts": "欢迎使用 Remotely Save!\n从这个版本开始,插件更新了同步算法:\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本,请阅读和勾选两个勾选框,然后点击“同意”按钮,开始使用插件吧!\n如果您不同意,请点击“不同意”按钮,插件将自动停止运行(unload)。\n此外,请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
|
"syncalgov3_texts": "欢迎使用 Remotely Save!\n从这个版本开始,插件更新了同步算法:\n<ul><li>更稳健的删除同步</li><li>引入冲突处理</li><li>避免上传元数据</li><li>修改删除保护</li><li>备份模式</li><li>新的加密方式</li><li>……</li></ul>\n敬请期待更多更新!详细介绍请参阅<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文档网站</a>。\n如果您同意使用新版本,请阅读和勾选两个勾选框,然后点击“同意”按钮,开始使用插件吧!\n如果您不同意,请点击“不同意”按钮,插件将自动停止运行(unload)。\n此外,请考虑<a href='https://github.com/remotely-save/remotely-save'>访问 GitHub 页面然后点赞 ⭐</a>!您的支持对我十分重要!谢谢!",
|
||||||
"syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库(Vault)。",
|
"syncalgov3_checkbox_manual_backup": "我将会首先手动备份我的库(Vault)。",
|
||||||
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。",
|
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有设备上都更新此插件使之正常运行。",
|
||||||
"syncalgov3_button_agree": "同意",
|
"syncalgov3_button_agree": "同意",
|
||||||
|
|||||||
+30
-11
@@ -5,7 +5,7 @@
|
|||||||
"goback": "返回",
|
"goback": "返回",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"sometext": "這裡有一段文字。",
|
"sometext": "這裡有一段文字。",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} 正處於此階段:{{syncStatus}}!",
|
"syncrun_alreadyrunning": "{{pluginName}} 正處於此階段:{{syncStatus}}! 中斷觸發 {{newTriggerSource}}。",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 觸發執行",
|
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 觸發執行",
|
||||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
||||||
"syncrun_step1": "1/8 Remotely Save 準備同步({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save 準備同步({{serviceType}})",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
"syncrun_shortstep2": "2/2 Remotely Save 已完成同步!",
|
||||||
"syncrun_abort": "{{manifestID}}-{{theDate}}:中斷同步,同步來源={{triggerSource}},出錯階段={{syncStatus}}",
|
"syncrun_abort": "{{manifestID}}-{{theDate}}:中斷同步,同步來源={{triggerSource}},出錯階段={{syncStatus}}",
|
||||||
"syncrun_abort_protectmodifypercentage": "中斷同步!您設定了不允許 >= {{protectModifyPercentage}}% 的變更,但是現在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的檔案會被修改或刪除!如果您確認這次同步是您想要的,那麼請在設定裡修改允許比例。",
|
"syncrun_abort_protectmodifypercentage": "中斷同步!您設定了不允許 >= {{protectModifyPercentage}}% 的變更,但是現在 {{realModifyDeleteCount}}/{{allFilesCount}}={{percent}}% 的檔案會被修改或刪除!如果您確認這次同步是您想要的,那麼請在設定裡修改允許比例。",
|
||||||
"protocol_saveqr": " {{manifestName}} 新的非 oauth2 設定儲存完成。請重啟外掛設定頁使之生效。",
|
"protocol_saveqr": " {{manifestName}} 的新設定匯入完成。請重啟外掛設定頁使之生效。",
|
||||||
"protocol_callbacknotsupported": "您的 uri callback 暫不支援: {{params}}",
|
"protocol_callbacknotsupported": "您的 uri callback 暫不支援: {{params}}",
|
||||||
"protocol_dropbox_connecting": "正在連線 Dropbox……\n請不要關閉此彈窗。",
|
"protocol_dropbox_connecting": "正在連線 Dropbox……\n請不要關閉此彈窗。",
|
||||||
"protocol_dropbox_connect_succ": "好!我們作為使用者 {{username}} 連線上了 Dropbox!",
|
"protocol_dropbox_connect_succ": "好!我們作為使用者 {{username}} 連線上了 Dropbox!",
|
||||||
@@ -38,7 +38,9 @@
|
|||||||
"protocol_onedrive_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
"protocol_onedrive_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
||||||
"command_startsync": "開始同步(start sync)",
|
"command_startsync": "開始同步(start sync)",
|
||||||
"command_drynrun": "開始同步(空跑模式)(start sync (dry run only))",
|
"command_drynrun": "開始同步(空跑模式)(start sync (dry run only))",
|
||||||
"command_exportsyncplans_json": "匯出同步計劃為 json 格式(export sync plans in json format)",
|
"command_exportsyncplans_1": "匯出同步計劃(最近 1 次)(export sync plans (latest 1))",
|
||||||
|
"command_exportsyncplans_5": "匯出同步計劃(最近 5 次)(export sync plans (latest 5))",
|
||||||
|
"command_exportsyncplans_all": "匯出同步計劃(所有)(export sync plans (all))",
|
||||||
"command_exportlogsindb": "從資料庫匯出終端日誌(export logs saved in db)",
|
"command_exportlogsindb": "從資料庫匯出終端日誌(export logs saved in db)",
|
||||||
|
|
||||||
"statusbar_time_years": "{{time}} 年前同步",
|
"statusbar_time_years": "{{time}} 年前同步",
|
||||||
@@ -64,6 +66,8 @@
|
|||||||
"modal_password_attn5": "注意 5/5:密碼越長越好。",
|
"modal_password_attn5": "注意 5/5:密碼越長越好。",
|
||||||
"modal_password_secondconfirm": "再次確認儲存新密碼",
|
"modal_password_secondconfirm": "再次確認儲存新密碼",
|
||||||
"modal_password_notice": "新密碼已儲存!",
|
"modal_password_notice": "新密碼已儲存!",
|
||||||
|
"modal_encryptionmethod_title": "稍等一下,請閱讀下文:",
|
||||||
|
"modal_encryptionmethod_shortdesc": "您正在修改加密方式,但是您已經設定了密碼。\n修改加密方式之後,您需要<b>手動</b>和<b>完全</b>刪除在遠端的之前加密過的庫檔案,然後重新同步(從而重新上傳)新的加密檔案。",
|
||||||
"modal_remotebasedir_title": "您正在修改遠端基資料夾設定",
|
"modal_remotebasedir_title": "您正在修改遠端基資料夾設定",
|
||||||
"modal_remotebasedir_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定會被重設回庫的資料夾名(預設設定)。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
|
"modal_remotebasedir_shortdesc": "1. 本外掛並不會自動在遠端把內容從舊資料夾移動到新資料夾。所有內容都會重新同步。\n2. 如果你使得文字輸入框為空,那麼本設定會被重設回庫的資料夾名(預設設定)。\n3. 即使您設定了端對端加密的密碼,遠端資料夾名稱本身也不會被加密。\n4. 某些特殊字元,如“?”、“/”、“\\”是不允許的。文字前後的空格也會被自動刪去。",
|
||||||
"modal_remotebasedir_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
|
"modal_remotebasedir_invaliddirhint": "您所輸入的內容含有某些特殊字元,如“?”、“/”、“\\”,它們是不允許的。",
|
||||||
@@ -88,6 +92,7 @@
|
|||||||
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作為使用者 {{username}} 連線到 Dropbox。如果您想斷開連線,點選此按鈕。",
|
"modal_dropboxauth_maualinput_conn_succ_revoke": "您已作為使用者 {{username}} 連線到 Dropbox。如果您想斷開連線,點選此按鈕。",
|
||||||
"modal_dropboxauth_maualinput_conn_fail": "連線 Dropbox 途中出錯了。",
|
"modal_dropboxauth_maualinput_conn_fail": "連線 Dropbox 途中出錯了。",
|
||||||
"modal_onedriveauth_shortdesc": "現在只支援個人版 OneDrive,(暫)不支援企業版。\n在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。",
|
"modal_onedriveauth_shortdesc": "現在只支援個人版 OneDrive,(暫)不支援企業版。\n在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。",
|
||||||
|
"modal_onedriveauth_shortdesc_linux": "您正在用 Linux,有可能無法跳轉回來。請考慮<a href=\"https://github.com/remotely-save/remotely-save/issues/415\">使用</a> flatpack 版本的 Obsidian,或建立 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> 檔案</a>。",
|
||||||
"modal_onedriveauth_copybutton": "點選此按鈕從而複製鑑權 url",
|
"modal_onedriveauth_copybutton": "點選此按鈕從而複製鑑權 url",
|
||||||
"modal_onedriveauth_copynotice": "鑑權 url 已複製到剪貼簿!",
|
"modal_onedriveauth_copynotice": "鑑權 url 已複製到剪貼簿!",
|
||||||
"modal_onedriverevokeauth_step1": "第 1 步:用瀏覽器開啟以下地址,點選本外掛對應的“Edit”按鈕,點選“Remove these permissions”按鈕。",
|
"modal_onedriverevokeauth_step1": "第 1 步:用瀏覽器開啟以下地址,點選本外掛對應的“Edit”按鈕,點選“Remove these permissions”按鈕。",
|
||||||
@@ -100,7 +105,7 @@
|
|||||||
"modal_syncconfig_attn": "注意 1/2:此設定只同步(複製)整個 Obsidian 的配置資料夾,但是不會同步其它 . 開頭的資料夾或檔案。除了會忽略 .git 和 node_modules 資料夾之外,它也並不理解配置資料夾的裡各個子檔案或子資料夾的含義。\n注意 2/2:配置資料夾被同步之後,各外掛的設定或許會出錯,且 Obsidian 或許需要重啟來過載各外掛的新配置。\n如果您同意自行承受以上風險,您可以點選以下再次確認按鈕。",
|
"modal_syncconfig_attn": "注意 1/2:此設定只同步(複製)整個 Obsidian 的配置資料夾,但是不會同步其它 . 開頭的資料夾或檔案。除了會忽略 .git 和 node_modules 資料夾之外,它也並不理解配置資料夾的裡各個子檔案或子資料夾的含義。\n注意 2/2:配置資料夾被同步之後,各外掛的設定或許會出錯,且 Obsidian 或許需要重啟來過載各外掛的新配置。\n如果您同意自行承受以上風險,您可以點選以下再次確認按鈕。",
|
||||||
"modal_syncconfig_secondconfirm": "再次確認開啟",
|
"modal_syncconfig_secondconfirm": "再次確認開啟",
|
||||||
"modal_syncconfig_notice": "您已開啟配置資料夾的同步!",
|
"modal_syncconfig_notice": "您已開啟配置資料夾的同步!",
|
||||||
"modal_qr_shortdesc": "這裡可匯出非 oauth2 設定。(意味著:Dropbox 和 OneDrive 資訊不會被匯出。)\n您可以使用另一個裝置來掃描此 QR 碼。\n又或者,您可以點選以下按鈕複製此特殊 URI。",
|
"modal_qr_shortdesc": "這裡可匯出(部分)設定。\n您可以使用另一個裝置來掃描此 QR 碼。\n又或者,您可以點選以下按鈕複製此特殊 URI,然後貼上到另一臺裝置的網路瀏覽器或 Remotely Save 設定裡的匯入部分。",
|
||||||
"modal_qr_button": "點選此按鈕複製特殊 URI",
|
"modal_qr_button": "點選此按鈕複製特殊 URI",
|
||||||
"modal_qr_button_notice": "特殊 URI 已被複制到剪貼簿!",
|
"modal_qr_button_notice": "特殊 URI 已被複制到剪貼簿!",
|
||||||
"modal_sizesconflict_title": "Remotely Save:跳過大檔案的時候出現了一些衝突",
|
"modal_sizesconflict_title": "Remotely Save:跳過大檔案的時候出現了一些衝突",
|
||||||
@@ -109,7 +114,11 @@
|
|||||||
"modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
|
"modal_sizesconflict_copynotice": "所有的檔案大小衝突資訊,已被複制到剪貼簿!",
|
||||||
"settings_basic": "基本設定",
|
"settings_basic": "基本設定",
|
||||||
"settings_password": "密碼",
|
"settings_password": "密碼",
|
||||||
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。",
|
"settings_password_desc": "端到端加密的密碼。不填寫則代表沒密碼。您需要點選“確認”來修改。注意:密碼和其它資訊都會在本地儲存。如果您修改了密碼,您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。",
|
||||||
|
"settings_encryptionmethod": "加密方法",
|
||||||
|
"settings_encryptionmethod_desc": "端到端加密的方法。推薦選用 RClone Crypt 方法,但是它沒有加密檔案路徑結構。OpenSSL enc 是本外掛一開始就支援的方式。<b>兩種方法都和 RClone、OpenSSL 官方產品和社群無利益相關。</b>如果您修改了加密方法,您需要手動刪除遠端的所有檔案,重新同步(從而上傳)加密檔案。更多詳細說明見<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/encryption'>線上文件</a>。",
|
||||||
|
"settings_encryptionmethod_rclone": "RClone Crypt(推薦)",
|
||||||
|
"settings_encryptionmethod_openssl": "OpenSSL enc(舊方法)",
|
||||||
"settings_autorun": "自動執行",
|
"settings_autorun": "自動執行",
|
||||||
"settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。",
|
"settings_autorun_desc": "每隔一段時間,此外掛嘗試自動同步。會影響到電池用量。",
|
||||||
"settings_autorun_notset": "(不設定)",
|
"settings_autorun_notset": "(不設定)",
|
||||||
@@ -264,14 +273,18 @@
|
|||||||
"setting_syncdirection_bidirectional_desc": "雙向同步(預設)",
|
"setting_syncdirection_bidirectional_desc": "雙向同步(預設)",
|
||||||
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:備份模式)",
|
"setting_syncdirection_incremental_push_only_desc": "只增量推送(也即:備份模式)",
|
||||||
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
"setting_syncdirection_incremental_pull_only_desc": "只增量拉取",
|
||||||
"settings_enablemobilestatusbar": "是否顯示手機的狀態列",
|
"settings_enablemobilestatusbar": "手機的狀態列(實驗性質)",
|
||||||
"settings_enablemobilestatusbar_desc": "Obsidian 手機版預設隱藏了狀態列。有些使用者希望展示它。這裡提供了設定選項。",
|
"settings_enablemobilestatusbar_desc": "Obsidian 手機版預設隱藏了狀態列。有些使用者希望展示它。這裡提供了設定選項。",
|
||||||
"settings_importexport": "匯入匯出部分設定",
|
"settings_importexport": "匯入匯出部分設定",
|
||||||
"settings_export": "匯出",
|
"settings_export": "匯出",
|
||||||
"settings_export_desc": "用 QR 碼匯出非 oauth2 的設定資訊。",
|
"settings_export_desc": "用 QR 碼或 URI 匯出設定資訊。",
|
||||||
"settings_export_desc_button": "生成 QR 碼",
|
"settings_export_all_but_oauth2_button": "匯出非 Oauth2 部分",
|
||||||
|
"settings_export_dropbox_button": "匯出 Dropbox 部分",
|
||||||
|
"settings_export_onedrive_button": "匯出 OneDrive 部分",
|
||||||
"settings_import": "匯入",
|
"settings_import": "匯入",
|
||||||
"settings_import_desc": "您需要使用系統拍攝 app 或者掃描 QR 碼的app,來掃描對應的 QR 碼。",
|
"settings_import_desc": "貼上之前匯出的 URI 到這裡然後點選“匯入”。或,使用拍攝 app 或者掃描 QR 碼的 app,來掃描對應的 QR 碼。",
|
||||||
|
"settings_import_button": "匯入",
|
||||||
|
"settings_import_error_notice": "您輸入的 URI 是空的或者不準確的!",
|
||||||
"settings_debug": "除錯",
|
"settings_debug": "除錯",
|
||||||
"settings_debuglevel": "修改同步提示資訊",
|
"settings_debuglevel": "修改同步提示資訊",
|
||||||
"settings_debuglevel_desc": "預設值為 \"info\"。您可以改為 \"debug\" 從而在同步時候裡獲取更多資訊。",
|
"settings_debuglevel_desc": "預設值為 \"info\"。您可以改為 \"debug\" 從而在同步時候裡獲取更多資訊。",
|
||||||
@@ -285,7 +298,9 @@
|
|||||||
"settings_viewconsolelog_desc": "電腦上,輸入“ctrl+shift+i”或“cmd+shift+i”來檢視終端輸出。手機上,安裝第三方外掛 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 來匯出終端輸出到一篇筆記上。",
|
"settings_viewconsolelog_desc": "電腦上,輸入“ctrl+shift+i”或“cmd+shift+i”來檢視終端輸出。手機上,安裝第三方外掛 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 來匯出終端輸出到一篇筆記上。",
|
||||||
"settings_syncplans": "匯出同步計劃",
|
"settings_syncplans": "匯出同步計劃",
|
||||||
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
|
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
|
||||||
"settings_syncplans_button_json": "匯出",
|
"settings_syncplans_button_1": "匯出最近 1 次",
|
||||||
|
"settings_syncplans_button_5": "匯出最近 5 次",
|
||||||
|
"settings_syncplans_button_all": "匯出所有",
|
||||||
"settings_syncplans_notice": "同步計劃已匯出",
|
"settings_syncplans_notice": "同步計劃已匯出",
|
||||||
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
|
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
|
||||||
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
|
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
|
||||||
@@ -295,6 +310,10 @@
|
|||||||
"settings_delprevsync_desc": "同步演算法需要上次成功同步的資訊來決定檔案變更,這個資訊儲存在本地的資料庫裡。如果您想忽略這些資訊從而所有檔案都被視為新建立的話,可以在此刪除之前的資訊。",
|
"settings_delprevsync_desc": "同步演算法需要上次成功同步的資訊來決定檔案變更,這個資訊儲存在本地的資料庫裡。如果您想忽略這些資訊從而所有檔案都被視為新建立的話,可以在此刪除之前的資訊。",
|
||||||
"settings_delprevsync_button": "刪除上次同步明細",
|
"settings_delprevsync_button": "刪除上次同步明細",
|
||||||
"settings_delprevsync_notice": "(本地資料庫裡的)上次同步明細已被刪除。",
|
"settings_delprevsync_notice": "(本地資料庫裡的)上次同步明細已被刪除。",
|
||||||
|
"settings_profiler_results": "匯出效能資料記錄",
|
||||||
|
"settings_profiler_results_desc": "外掛記錄了每次同步每一步的耗時。這裡可以匯出記錄得知哪一步最慢。",
|
||||||
|
"settings_profiler_results_notice": "效能資料已匯出",
|
||||||
|
"settings_profiler_results_button_all": "匯出所有",
|
||||||
"settings_outputbasepathvaultid": "輸出資料庫對應的位置和隨機分配的 ID",
|
"settings_outputbasepathvaultid": "輸出資料庫對應的位置和隨機分配的 ID",
|
||||||
"settings_outputbasepathvaultid_desc": "用於除錯。",
|
"settings_outputbasepathvaultid_desc": "用於除錯。",
|
||||||
"settings_outputbasepathvaultid_button": "輸出",
|
"settings_outputbasepathvaultid_button": "輸出",
|
||||||
@@ -303,7 +322,7 @@
|
|||||||
"settings_resetcache_button": "重設",
|
"settings_resetcache_button": "重設",
|
||||||
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
|
"settings_resetcache_notice": "本地同步快取和資料庫已被刪除。請手動重新載入此外掛。",
|
||||||
"syncalgov3_title": "Remotely Save 的同步演算法有重大更新",
|
"syncalgov3_title": "Remotely Save 的同步演算法有重大更新",
|
||||||
"syncalgov3_texts": "歡迎使用 Remotely Save!\n從這個版本開始,外掛更新了同步演算法:\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本,請閱讀和勾選兩個勾選框,然後點選“同意”按鈕,開始使用外掛吧!\n如果您不同意,請點選“不同意”按鈕,外掛將自動停止執行(unload)。\n此外,請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
|
"syncalgov3_texts": "歡迎使用 Remotely Save!\n從這個版本開始,外掛更新了同步演算法:\n<ul><li>更穩健的刪除同步</li><li>引入衝突處理</li><li>避免上傳元資料</li><li>修改刪除保護</li><li>備份模式</li><li>新的加密方式</li><li>……</li></ul>\n敬請期待更多更新!詳細介紹請參閱<a href='https://github.com/remotely-save/remotely-save/tree/master/docs/sync_algorithm/v3/intro.md'>文件網站</a>。\n如果您同意使用新版本,請閱讀和勾選兩個勾選框,然後點選“同意”按鈕,開始使用外掛吧!\n如果您不同意,請點選“不同意”按鈕,外掛將自動停止執行(unload)。\n此外,請考慮<a href='https://github.com/remotely-save/remotely-save'>訪問 GitHub 頁面然後點贊 ⭐</a>!您的支援對我十分重要!謝謝!",
|
||||||
"syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫(Vault)。",
|
"syncalgov3_checkbox_manual_backup": "我將會首先手動備份我的庫(Vault)。",
|
||||||
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。",
|
"syncalgov3_checkbox_requiremultidevupdate": "我理解,我需要在所有裝置上都更新此外掛使之正常執行。",
|
||||||
"syncalgov3_button_agree": "同意",
|
"syncalgov3_button_agree": "同意",
|
||||||
|
|||||||
+12
-1
@@ -1,16 +1,21 @@
|
|||||||
import { TFile, TFolder, type Vault } from "obsidian";
|
import { TFile, TFolder, type Vault } from "obsidian";
|
||||||
import type { Entity, MixedEntity } from "./baseTypes";
|
import type { Entity, MixedEntity } from "./baseTypes";
|
||||||
import { listFilesInObsFolder } from "./obsFolderLister";
|
import { listFilesInObsFolder } from "./obsFolderLister";
|
||||||
|
import { Profiler } from "./profiler";
|
||||||
|
|
||||||
export const getLocalEntityList = async (
|
export const getLocalEntityList = async (
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
syncConfigDir: boolean,
|
syncConfigDir: boolean,
|
||||||
configDir: string,
|
configDir: string,
|
||||||
pluginID: string
|
pluginID: string,
|
||||||
|
profiler: Profiler
|
||||||
) => {
|
) => {
|
||||||
|
profiler.addIndent();
|
||||||
|
profiler.insert("enter getLocalEntityList");
|
||||||
const local: Entity[] = [];
|
const local: Entity[] = [];
|
||||||
|
|
||||||
const localTAbstractFiles = vault.getAllLoadedFiles();
|
const localTAbstractFiles = vault.getAllLoadedFiles();
|
||||||
|
profiler.insert("finish getting getAllLoadedFiles");
|
||||||
for (const entry of localTAbstractFiles) {
|
for (const entry of localTAbstractFiles) {
|
||||||
let r = {} as Entity;
|
let r = {} as Entity;
|
||||||
let key = entry.path;
|
let key = entry.path;
|
||||||
@@ -54,12 +59,18 @@ export const getLocalEntityList = async (
|
|||||||
local.push(r);
|
local.push(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("finish transforming getAllLoadedFiles");
|
||||||
|
|
||||||
if (syncConfigDir) {
|
if (syncConfigDir) {
|
||||||
|
profiler.insert("into syncConfigDir");
|
||||||
const syncFiles = await listFilesInObsFolder(configDir, vault, pluginID);
|
const syncFiles = await listFilesInObsFolder(configDir, vault, pluginID);
|
||||||
for (const f of syncFiles) {
|
for (const f of syncFiles) {
|
||||||
local.push(f);
|
local.push(f);
|
||||||
}
|
}
|
||||||
|
profiler.insert("finish syncConfigDir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("finish getLocalEntityList");
|
||||||
|
profiler.removeIndent();
|
||||||
return local;
|
return local;
|
||||||
};
|
};
|
||||||
|
|||||||
+51
-3
@@ -17,6 +17,7 @@ export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
|
|||||||
export const DEFAULT_TBL_LOGGER_OUTPUT = "loggeroutput";
|
export const DEFAULT_TBL_LOGGER_OUTPUT = "loggeroutput";
|
||||||
export const DEFAULT_TBL_SIMPLE_KV_FOR_MISC = "simplekvformisc";
|
export const DEFAULT_TBL_SIMPLE_KV_FOR_MISC = "simplekvformisc";
|
||||||
export const DEFAULT_TBL_PREV_SYNC_RECORDS = "prevsyncrecords";
|
export const DEFAULT_TBL_PREV_SYNC_RECORDS = "prevsyncrecords";
|
||||||
|
export const DEFAULT_TBL_PROFILER_RESULTS = "profilerresults";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@@ -58,6 +59,7 @@ export interface InternalDBs {
|
|||||||
loggerOutputTbl: LocalForage;
|
loggerOutputTbl: LocalForage;
|
||||||
simpleKVForMiscTbl: LocalForage;
|
simpleKVForMiscTbl: LocalForage;
|
||||||
prevSyncRecordsTbl: LocalForage;
|
prevSyncRecordsTbl: LocalForage;
|
||||||
|
profilerResultsTbl: LocalForage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@@ -204,6 +206,10 @@ export const prepareDBs = async (
|
|||||||
name: DEFAULT_DB_NAME,
|
name: DEFAULT_DB_NAME,
|
||||||
storeName: DEFAULT_TBL_PREV_SYNC_RECORDS,
|
storeName: DEFAULT_TBL_PREV_SYNC_RECORDS,
|
||||||
}),
|
}),
|
||||||
|
profilerResultsTbl: localforage.createInstance({
|
||||||
|
name: DEFAULT_DB_NAME,
|
||||||
|
storeName: DEFAULT_TBL_PROFILER_RESULTS,
|
||||||
|
}),
|
||||||
|
|
||||||
fileHistoryTbl: localforage.createInstance({
|
fileHistoryTbl: localforage.createInstance({
|
||||||
name: DEFAULT_DB_NAME,
|
name: DEFAULT_DB_NAME,
|
||||||
@@ -382,13 +388,13 @@ export const readAllSyncPlanRecordTextsByVault = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We remove records that are older than 3 days or 100 records.
|
* We remove records that are older than 1 days or 20 records.
|
||||||
* It's a heavy operation, so we shall not place it in the start up.
|
* It's a heavy operation, so we shall not place it in the start up.
|
||||||
* @param db
|
* @param db
|
||||||
*/
|
*/
|
||||||
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
||||||
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 3; // 3 days
|
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 1; // 1 days
|
||||||
const COUNT_TO_MANY = 100;
|
const COUNT_TO_MANY = 20;
|
||||||
|
|
||||||
const currTs = Date.now();
|
const currTs = Date.now();
|
||||||
const expiredTs = currTs - MILLISECONDS_OLD;
|
const expiredTs = currTs - MILLISECONDS_OLD;
|
||||||
@@ -524,3 +530,45 @@ export const upsertPluginVersionByVault = async (
|
|||||||
newVersion: newVersion,
|
newVersion: newVersion,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const insertProfilerResultByVault = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
profilerStr: string,
|
||||||
|
vaultRandomID: string,
|
||||||
|
remoteType: SUPPORTED_SERVICES_TYPE
|
||||||
|
) => {
|
||||||
|
const now = Date.now();
|
||||||
|
await db.profilerResultsTbl.setItem(`${vaultRandomID}\t${now}`, profilerStr);
|
||||||
|
|
||||||
|
// clear older one while writing
|
||||||
|
const records = (await db.profilerResultsTbl.keys())
|
||||||
|
.filter((x) => x.startsWith(`${vaultRandomID}\t`))
|
||||||
|
.map((x) => parseInt(x.split("\t")[1]));
|
||||||
|
records.sort((a, b) => -(a - b)); // descending
|
||||||
|
while (records.length > 5) {
|
||||||
|
const ts = records.pop()!;
|
||||||
|
await db.profilerResultsTbl.removeItem(`${vaultRandomID}\t${ts}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readAllProfilerResultsByVault = async (
|
||||||
|
db: InternalDBs,
|
||||||
|
vaultRandomID: string
|
||||||
|
) => {
|
||||||
|
const records = [] as { val: string; ts: number }[];
|
||||||
|
await db.profilerResultsTbl.iterate((value, key, iterationNumber) => {
|
||||||
|
if (key.startsWith(`${vaultRandomID}\t`)) {
|
||||||
|
records.push({
|
||||||
|
val: value as string,
|
||||||
|
ts: parseInt(key.split("\t")[1]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
records.sort((a, b) => -(a.ts - b.ts)); // descending
|
||||||
|
|
||||||
|
if (records === undefined) {
|
||||||
|
return [] as string[];
|
||||||
|
} else {
|
||||||
|
return records.map((x) => x.val);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
+161
-41
@@ -9,6 +9,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
requestUrl,
|
requestUrl,
|
||||||
requireApiVersion,
|
requireApiVersion,
|
||||||
|
Events,
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
|
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
upsertLastSuccessSyncTimeByVault,
|
upsertLastSuccessSyncTimeByVault,
|
||||||
getLastSuccessSyncTimeByVault,
|
getLastSuccessSyncTimeByVault,
|
||||||
getAllPrevSyncRecordsByVaultAndProfile,
|
getAllPrevSyncRecordsByVaultAndProfile,
|
||||||
|
insertProfilerResultByVault,
|
||||||
} from "./localdb";
|
} from "./localdb";
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
import {
|
import {
|
||||||
@@ -67,6 +69,8 @@ import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
|
|||||||
import AggregateError from "aggregate-error";
|
import AggregateError from "aggregate-error";
|
||||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||||
import { changeMobileStatusBar, compareVersion } from "./misc";
|
import { changeMobileStatusBar, compareVersion } from "./misc";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
import { Profiler } from "./profiler";
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||||
s3: DEFAULT_S3_CONFIG,
|
s3: DEFAULT_S3_CONFIG,
|
||||||
@@ -97,6 +101,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
|||||||
syncDirection: "bidirectional",
|
syncDirection: "bidirectional",
|
||||||
obfuscateSettingFile: true,
|
obfuscateSettingFile: true,
|
||||||
enableMobileStatusBar: false,
|
enableMobileStatusBar: false,
|
||||||
|
encryptionMethod: "unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OAuth2Info {
|
interface OAuth2Info {
|
||||||
@@ -147,8 +152,12 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
i18n!: I18n;
|
i18n!: I18n;
|
||||||
vaultRandomID!: string;
|
vaultRandomID!: string;
|
||||||
debugServerTemp?: string;
|
debugServerTemp?: string;
|
||||||
|
syncEvent?: Events;
|
||||||
|
appContainerObserver?: MutationObserver;
|
||||||
|
|
||||||
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
||||||
|
const profiler = new Profiler("start of syncRun");
|
||||||
|
|
||||||
const t = (x: TransItemType, vars?: any) => {
|
const t = (x: TransItemType, vars?: any) => {
|
||||||
return this.i18n.t(x, vars);
|
return this.i18n.t(x, vars);
|
||||||
};
|
};
|
||||||
@@ -163,15 +172,17 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.syncStatus !== "idle") {
|
if (this.syncStatus !== "idle") {
|
||||||
// here the notice is shown regardless of triggerSource
|
// really, users don't want to see this in auto mode
|
||||||
new Notice(
|
// so we use getNotice to avoid unnecessary show up
|
||||||
|
getNotice(
|
||||||
t("syncrun_alreadyrunning", {
|
t("syncrun_alreadyrunning", {
|
||||||
pluginName: this.manifest.name,
|
pluginName: this.manifest.name,
|
||||||
syncStatus: this.syncStatus,
|
syncStatus: this.syncStatus,
|
||||||
|
newTriggerSource: triggerSource,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
||||||
new Notice(this.currSyncMsg);
|
getNotice(this.currSyncMsg);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -227,6 +238,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.syncStatus = "preparing";
|
this.syncStatus = "preparing";
|
||||||
|
profiler.insert("finish step1");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
// pass
|
// pass
|
||||||
@@ -242,27 +254,34 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.dropbox,
|
this.settings.dropbox,
|
||||||
this.settings.onedrive,
|
this.settings.onedrive,
|
||||||
this.app.vault.getName(),
|
this.app.vault.getName(),
|
||||||
() => self.saveSettings()
|
() => self.saveSettings(),
|
||||||
|
profiler
|
||||||
);
|
);
|
||||||
const remoteEntityList = await client.listAllFromRemote();
|
const remoteEntityList = await client.listAllFromRemote();
|
||||||
console.debug("remoteEntityList:");
|
console.debug("remoteEntityList:");
|
||||||
console.debug(remoteEntityList);
|
console.debug(remoteEntityList);
|
||||||
|
|
||||||
|
profiler.insert("finish step2 (listing remote)");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
getNotice(t("syncrun_step3"));
|
getNotice(t("syncrun_step3"));
|
||||||
}
|
}
|
||||||
this.syncStatus = "checking_password";
|
this.syncStatus = "checking_password";
|
||||||
const passwordCheckResult = await isPasswordOk(
|
|
||||||
remoteEntityList,
|
const cipher = new Cipher(
|
||||||
this.settings.password
|
this.settings.password,
|
||||||
|
this.settings.encryptionMethod ?? "unknown"
|
||||||
);
|
);
|
||||||
|
const passwordCheckResult = await isPasswordOk(remoteEntityList, cipher);
|
||||||
if (!passwordCheckResult.ok) {
|
if (!passwordCheckResult.ok) {
|
||||||
getNotice(t("syncrun_passworderr"));
|
getNotice(t("syncrun_passworderr"));
|
||||||
throw Error(passwordCheckResult.reason);
|
throw Error(passwordCheckResult.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("finish step3 (checking password)");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
@@ -273,11 +292,14 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.settings.syncConfigDir ?? false,
|
this.settings.syncConfigDir ?? false,
|
||||||
this.app.vault.configDir,
|
this.app.vault.configDir,
|
||||||
this.manifest.id
|
this.manifest.id,
|
||||||
|
profiler
|
||||||
);
|
);
|
||||||
console.debug("localEntityList:");
|
console.debug("localEntityList:");
|
||||||
console.debug(localEntityList);
|
console.debug(localEntityList);
|
||||||
|
|
||||||
|
profiler.insert("finish step4 (local meta)");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
@@ -292,6 +314,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
console.debug("prevSyncEntityList:");
|
console.debug("prevSyncEntityList:");
|
||||||
console.debug(prevSyncEntityList);
|
console.debug(prevSyncEntityList);
|
||||||
|
|
||||||
|
profiler.insert("finish step5 (prev sync)");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
// pass
|
// pass
|
||||||
} else {
|
} else {
|
||||||
@@ -306,18 +330,22 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.app.vault.configDir,
|
this.app.vault.configDir,
|
||||||
this.settings.syncUnderscoreItems ?? false,
|
this.settings.syncUnderscoreItems ?? false,
|
||||||
this.settings.ignorePaths ?? [],
|
this.settings.ignorePaths ?? [],
|
||||||
this.settings.password,
|
cipher,
|
||||||
this.settings.serviceType
|
this.settings.serviceType,
|
||||||
|
profiler
|
||||||
);
|
);
|
||||||
|
profiler.insert("finish building partial mixedEntity");
|
||||||
mixedEntityMappings = await getSyncPlanInplace(
|
mixedEntityMappings = await getSyncPlanInplace(
|
||||||
mixedEntityMappings,
|
mixedEntityMappings,
|
||||||
this.settings.howToCleanEmptyFolder ?? "skip",
|
this.settings.howToCleanEmptyFolder ?? "skip",
|
||||||
this.settings.skipSizeLargerThan ?? -1,
|
this.settings.skipSizeLargerThan ?? -1,
|
||||||
this.settings.conflictAction ?? "keep_newer",
|
this.settings.conflictAction ?? "keep_newer",
|
||||||
this.settings.syncDirection ?? "bidirectional"
|
this.settings.syncDirection ?? "bidirectional",
|
||||||
|
profiler
|
||||||
);
|
);
|
||||||
console.info(`mixedEntityMappings:`);
|
console.info(`mixedEntityMappings:`);
|
||||||
console.info(mixedEntityMappings); // for debugging
|
console.info(mixedEntityMappings); // for debugging
|
||||||
|
profiler.insert("finish building full sync plan");
|
||||||
await insertSyncPlanRecordByVault(
|
await insertSyncPlanRecordByVault(
|
||||||
this.db,
|
this.db,
|
||||||
mixedEntityMappings,
|
mixedEntityMappings,
|
||||||
@@ -325,6 +353,9 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
client.serviceType
|
client.serviceType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
profiler.insert("finish writing sync plan");
|
||||||
|
profiler.insert("finish step6 (plan)");
|
||||||
|
|
||||||
// The operations above are almost read only and kind of safe.
|
// The operations above are almost read only and kind of safe.
|
||||||
// The operations below begins to write or delete (!!!) something.
|
// The operations below begins to write or delete (!!!) something.
|
||||||
|
|
||||||
@@ -341,7 +372,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.vaultRandomID,
|
this.vaultRandomID,
|
||||||
profileID,
|
profileID,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.settings.password,
|
cipher,
|
||||||
this.settings.concurrency ?? 5,
|
this.settings.concurrency ?? 5,
|
||||||
(key: string) => self.trash(key),
|
(key: string) => self.trash(key),
|
||||||
this.settings.protectModifyPercentage ?? 50,
|
this.settings.protectModifyPercentage ?? 50,
|
||||||
@@ -372,9 +403,11 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
realCounter,
|
realCounter,
|
||||||
realTotalCount,
|
realTotalCount,
|
||||||
pathName,
|
pathName,
|
||||||
decision
|
decision,
|
||||||
|
triggerSource
|
||||||
),
|
),
|
||||||
this.db
|
this.db,
|
||||||
|
profiler
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.syncStatus = "syncing";
|
this.syncStatus = "syncing";
|
||||||
@@ -385,6 +418,10 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cipher.closeResources();
|
||||||
|
|
||||||
|
profiler.insert("finish step7 (actual sync)");
|
||||||
|
|
||||||
if (this.settings.currLogLevel === "info") {
|
if (this.settings.currLogLevel === "info") {
|
||||||
getNotice(t("syncrun_shortstep2"));
|
getNotice(t("syncrun_shortstep2"));
|
||||||
} else {
|
} else {
|
||||||
@@ -394,6 +431,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.syncStatus = "finish";
|
this.syncStatus = "finish";
|
||||||
this.syncStatus = "idle";
|
this.syncStatus = "idle";
|
||||||
|
|
||||||
|
profiler.insert("finish step8");
|
||||||
|
|
||||||
const lastSuccessSyncMillis = Date.now();
|
const lastSuccessSyncMillis = Date.now();
|
||||||
await upsertLastSuccessSyncTimeByVault(
|
await upsertLastSuccessSyncTimeByVault(
|
||||||
this.db,
|
this.db,
|
||||||
@@ -410,12 +449,14 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.updateLastSuccessSyncMsg(lastSuccessSyncMillis);
|
this.updateLastSuccessSyncMsg(lastSuccessSyncMillis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncEvent?.trigger("SYNC_DONE");
|
||||||
console.info(
|
console.info(
|
||||||
`${
|
`${
|
||||||
this.manifest.id
|
this.manifest.id
|
||||||
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
|
}-${Date.now()}: finish sync, triggerSource=${triggerSource}`
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
profiler.insert("start error branch");
|
||||||
const msg = t("syncrun_abort", {
|
const msg = t("syncrun_abort", {
|
||||||
manifestID: this.manifest.id,
|
manifestID: this.manifest.id,
|
||||||
theDate: `${Date.now()}`,
|
theDate: `${Date.now()}`,
|
||||||
@@ -437,7 +478,19 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
setIcon(this.syncRibbon, iconNameSyncWait);
|
setIcon(this.syncRibbon, iconNameSyncWait);
|
||||||
this.syncRibbon.setAttribute("aria-label", originLabel);
|
this.syncRibbon.setAttribute("aria-label", originLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("finish error branch");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("finish syncRun");
|
||||||
|
console.debug(profiler.toString());
|
||||||
|
insertProfilerResultByVault(
|
||||||
|
this.db,
|
||||||
|
profiler.toString(),
|
||||||
|
this.vaultRandomID,
|
||||||
|
this.settings.serviceType
|
||||||
|
);
|
||||||
|
profiler.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
@@ -459,6 +512,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
|
|
||||||
this.currSyncMsg = "";
|
this.currSyncMsg = "";
|
||||||
|
|
||||||
|
this.syncEvent = new Events();
|
||||||
|
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
|
||||||
// MUST after loadSettings and before prepareDB
|
// MUST after loadSettings and before prepareDB
|
||||||
@@ -509,6 +564,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.syncStatus = "idle";
|
this.syncStatus = "idle";
|
||||||
|
|
||||||
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
|
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
|
||||||
|
// console.debug(inputParams);
|
||||||
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
|
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
|
||||||
if (parsed.status === "error") {
|
if (parsed.status === "error") {
|
||||||
new Notice(parsed.message);
|
new Notice(parsed.message);
|
||||||
@@ -767,14 +823,45 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "export-sync-plans-json",
|
id: "export-sync-plans-1",
|
||||||
name: t("command_exportsyncplans_json"),
|
name: t("command_exportsyncplans_1"),
|
||||||
icon: iconNameLogs,
|
icon: iconNameLogs,
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await exportVaultSyncPlansToFiles(
|
await exportVaultSyncPlansToFiles(
|
||||||
this.db,
|
this.db,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.vaultRandomID
|
this.vaultRandomID,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "export-sync-plans-5",
|
||||||
|
name: t("command_exportsyncplans_5"),
|
||||||
|
icon: iconNameLogs,
|
||||||
|
callback: async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.vaultRandomID,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "export-sync-plans-all",
|
||||||
|
name: t("command_exportsyncplans_all"),
|
||||||
|
icon: iconNameLogs,
|
||||||
|
callback: async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.vaultRandomID,
|
||||||
|
-1
|
||||||
);
|
);
|
||||||
new Notice(t("settings_syncplans_notice"));
|
new Notice(t("settings_syncplans_notice"));
|
||||||
},
|
},
|
||||||
@@ -806,6 +893,10 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
async onunload() {
|
async onunload() {
|
||||||
console.info(`unloading plugin ${this.manifest.id}`);
|
console.info(`unloading plugin ${this.manifest.id}`);
|
||||||
this.syncRibbon = undefined;
|
this.syncRibbon = undefined;
|
||||||
|
if (this.appContainerObserver !== undefined) {
|
||||||
|
this.appContainerObserver.disconnect();
|
||||||
|
this.appContainerObserver = undefined;
|
||||||
|
}
|
||||||
if (this.oauth2Info !== undefined) {
|
if (this.oauth2Info !== undefined) {
|
||||||
this.oauth2Info.helperModal = undefined;
|
this.oauth2Info.helperModal = undefined;
|
||||||
this.oauth2Info = {
|
this.oauth2Info = {
|
||||||
@@ -911,6 +1002,22 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.enableMobileStatusBar = false;
|
this.settings.enableMobileStatusBar = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.settings.encryptionMethod === undefined ||
|
||||||
|
this.settings.encryptionMethod === "unknown"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.settings.password === undefined ||
|
||||||
|
this.settings.password === ""
|
||||||
|
) {
|
||||||
|
// we have a preferred way
|
||||||
|
this.settings.encryptionMethod = "rclone-base64";
|
||||||
|
} else {
|
||||||
|
// likely to be inherited from the old version
|
||||||
|
this.settings.encryptionMethod = "openssl-base64";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1078,7 +1185,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
) {
|
) {
|
||||||
this.app.workspace.onLayoutReady(() => {
|
this.app.workspace.onLayoutReady(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.syncRun("autoOnceInit");
|
this.syncRun("auto_once_init");
|
||||||
}, this.settings.initRunAfterMilliseconds);
|
}, this.settings.initRunAfterMilliseconds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1104,52 +1211,64 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}, scheduleTimeFromNow);
|
}, scheduleTimeFromNow);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.app.workspace.onLayoutReady(() => {
|
const checkCurrFileModified = async (caller: "SYNC" | "FILE_CHANGES") => {
|
||||||
const intervalID = window.setInterval(() => {
|
|
||||||
const currentFile = this.app.workspace.getActiveFile();
|
const currentFile = this.app.workspace.getActiveFile();
|
||||||
|
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
// get the last modified time of the current file
|
// get the last modified time of the current file
|
||||||
// if it has been modified within the last syncOnSaveAfterMilliseconds
|
// if it has modified after lastSuccessSync
|
||||||
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
|
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
|
||||||
const lastModified = currentFile.stat.mtime;
|
const lastModified = currentFile.stat.mtime;
|
||||||
const currentTime = Date.now();
|
const lastSuccessSyncMillis = await getLastSuccessSyncTimeByVault(
|
||||||
|
this.db,
|
||||||
|
this.vaultRandomID
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
currentTime - lastModified <
|
this.syncStatus === "idle" &&
|
||||||
this.settings!.syncOnSaveAfterMilliseconds!
|
lastModified > lastSuccessSyncMillis &&
|
||||||
|
!runScheduled
|
||||||
) {
|
) {
|
||||||
if (
|
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
||||||
!needToRunAgain &&
|
|
||||||
!runScheduled &&
|
|
||||||
this.syncStatus === "idle"
|
|
||||||
) {
|
|
||||||
const scheduleTimeFromNow =
|
|
||||||
this.settings!.syncOnSaveAfterMilliseconds! -
|
|
||||||
(currentTime - lastModified);
|
|
||||||
scheduleSyncOnSave(scheduleTimeFromNow);
|
|
||||||
} else if (
|
} else if (
|
||||||
|
this.syncStatus === "idle" &&
|
||||||
needToRunAgain &&
|
needToRunAgain &&
|
||||||
!runScheduled &&
|
!runScheduled
|
||||||
this.syncStatus === "idle"
|
|
||||||
) {
|
) {
|
||||||
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
||||||
needToRunAgain = false;
|
needToRunAgain = false;
|
||||||
} else {
|
} else {
|
||||||
|
if (caller === "FILE_CHANGES") {
|
||||||
needToRunAgain = true;
|
needToRunAgain = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, this.settings.syncOnSaveAfterMilliseconds);
|
};
|
||||||
this.syncOnSaveIntervalID = intervalID;
|
|
||||||
this.registerInterval(intervalID);
|
this.app.workspace.onLayoutReady(() => {
|
||||||
|
// listen to sync done
|
||||||
|
this.registerEvent(
|
||||||
|
this.syncEvent?.on("SYNC_DONE", () => {
|
||||||
|
checkCurrFileModified("SYNC");
|
||||||
|
})!
|
||||||
|
);
|
||||||
|
|
||||||
|
// listen to current file save changes
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.vault.on("modify", (x) => {
|
||||||
|
// console.debug(`event=modify! file=${x}`);
|
||||||
|
checkCurrFileModified("FILE_CHANGES");
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableMobileStatusBarIfSet() {
|
enableMobileStatusBarIfSet() {
|
||||||
|
this.app.workspace.onLayoutReady(() => {
|
||||||
if (Platform.isMobile && this.settings.enableMobileStatusBar) {
|
if (Platform.isMobile && this.settings.enableMobileStatusBar) {
|
||||||
changeMobileStatusBar("enable");
|
this.appContainerObserver = changeMobileStatusBar("enable");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveAgreeToUseNewSyncAlgorithm() {
|
async saveAgreeToUseNewSyncAlgorithm() {
|
||||||
@@ -1161,9 +1280,10 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
i: number,
|
i: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
pathName: string,
|
pathName: string,
|
||||||
decision: string
|
decision: string,
|
||||||
|
triggerSource: SyncTriggerSourceType
|
||||||
) {
|
) {
|
||||||
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName}`;
|
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName},source=${triggerSource}`;
|
||||||
this.currSyncMsg = msg;
|
this.currSyncMsg = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-9
@@ -1,4 +1,4 @@
|
|||||||
import { Vault } from "obsidian";
|
import { Platform, Vault } from "obsidian";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { base32, base64url } from "rfc4648";
|
import { base32, base64url } from "rfc4648";
|
||||||
@@ -118,6 +118,12 @@ export const base64ToArrayBuffer = (b64text: string) => {
|
|||||||
return bufferToArrayBuffer(Buffer.from(b64text, "base64"));
|
return bufferToArrayBuffer(Buffer.from(b64text, "base64"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const copyArrayBuffer = (src: ArrayBuffer) => {
|
||||||
|
var dst = new ArrayBuffer(src.byteLength);
|
||||||
|
new Uint8Array(dst).set(new Uint8Array(src));
|
||||||
|
return dst;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://stackoverflow.com/questions/43131242
|
* https://stackoverflow.com/questions/43131242
|
||||||
* @param hex
|
* @param hex
|
||||||
@@ -159,6 +165,9 @@ export const base64ToBase64url = (a: string, pad: boolean = false) => {
|
|||||||
* @param a
|
* @param a
|
||||||
*/
|
*/
|
||||||
export const isVaildText = (a: string) => {
|
export const isVaildText = (a: string) => {
|
||||||
|
if (a === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// If the regex matches, the string is invalid.
|
// If the regex matches, the string is invalid.
|
||||||
return !XRegExp("\\p{Cc}|\\p{Cf}|\\p{Co}|\\p{Cn}|\\p{Zl}|\\p{Zp}", "A").test(
|
return !XRegExp("\\p{Cc}|\\p{Cf}|\\p{Co}|\\p{Cn}|\\p{Zl}|\\p{Zp}", "A").test(
|
||||||
a
|
a
|
||||||
@@ -504,19 +513,141 @@ export const stringToFragment = (string: string) => {
|
|||||||
return wrapper.content;
|
return wrapper.content;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://stackoverflow.com/questions/39538473/using-settimeout-on-promise-chain
|
||||||
|
* @param ms
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const delay = (ms: number) =>
|
||||||
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://forum.obsidian.md/t/css-to-show-status-bar-on-mobile-devices/77185
|
* https://forum.obsidian.md/t/css-to-show-status-bar-on-mobile-devices/77185
|
||||||
* @param op
|
* @param op
|
||||||
*/
|
*/
|
||||||
export const changeMobileStatusBar = (op: "enable" | "disable") => {
|
export const changeMobileStatusBar = (
|
||||||
const bar = document.querySelector(
|
op: "enable" | "disable",
|
||||||
|
oldAppContainerObserver?: MutationObserver
|
||||||
|
) => {
|
||||||
|
const appContainer = document.getElementsByClassName("app-container")[0] as
|
||||||
|
| HTMLElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const statusbar = document.querySelector(
|
||||||
".is-mobile .app-container .status-bar"
|
".is-mobile .app-container .status-bar"
|
||||||
) as HTMLElement;
|
) as HTMLElement | undefined;
|
||||||
|
|
||||||
|
if (appContainer === undefined || statusbar === undefined) {
|
||||||
|
// give up, exit
|
||||||
|
console.warn(`give up watching appContainer for statusbar`);
|
||||||
|
console.warn(`appContainer=${appContainer}, statusbar=${statusbar}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (op === "enable") {
|
if (op === "enable") {
|
||||||
bar.style.setProperty("display", "flex");
|
const callback = async (
|
||||||
bar.style.setProperty("margin-bottom", "40px");
|
mutationList: MutationRecord[],
|
||||||
} else {
|
observer: MutationObserver
|
||||||
bar.style.removeProperty("display");
|
) => {
|
||||||
bar.style.removeProperty("margin-bottom");
|
for (const mutation of mutationList) {
|
||||||
|
// console.debug(mutation);
|
||||||
|
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||||
|
const k = mutation.addedNodes[0] as Element;
|
||||||
|
if (
|
||||||
|
k.className.contains("mobile-navbar") ||
|
||||||
|
k.className.contains("mobile-toolbar")
|
||||||
|
) {
|
||||||
|
// have to wait, otherwise the height is not correct??
|
||||||
|
await delay(300);
|
||||||
|
const height = window
|
||||||
|
.getComputedStyle(k as Element)
|
||||||
|
.getPropertyValue("height");
|
||||||
|
|
||||||
|
statusbar.style.setProperty("display", "flex");
|
||||||
|
statusbar.style.setProperty("margin-bottom", height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
observer.observe(appContainer, {
|
||||||
|
attributes: false,
|
||||||
|
childList: true,
|
||||||
|
characterData: false,
|
||||||
|
subtree: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// init, manual call
|
||||||
|
const navBar = document.getElementsByClassName(
|
||||||
|
"mobile-navbar"
|
||||||
|
)[0] as HTMLElement;
|
||||||
|
// thanks to community's solution
|
||||||
|
const height = window.getComputedStyle(navBar).getPropertyValue("height");
|
||||||
|
statusbar.style.setProperty("display", "flex");
|
||||||
|
statusbar.style.setProperty("margin-bottom", height);
|
||||||
|
} catch (e) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
} else {
|
||||||
|
if (oldAppContainerObserver !== undefined) {
|
||||||
|
console.debug(`disconnect oldAppContainerObserver`);
|
||||||
|
oldAppContainerObserver.disconnect();
|
||||||
|
oldAppContainerObserver = undefined;
|
||||||
|
}
|
||||||
|
statusbar.style.removeProperty("display");
|
||||||
|
statusbar.style.removeProperty("margin-bottom");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/remotely-save/remotely-save/issues/567
|
||||||
|
* https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/Case-Sensitivity-in-API-2/td-p/191279
|
||||||
|
* @param entities
|
||||||
|
*/
|
||||||
|
export const fixEntityListCasesInplace = (entities: { keyRaw: string }[]) => {
|
||||||
|
entities.sort((a, b) => a.keyRaw.length - b.keyRaw.length);
|
||||||
|
// console.log(JSON.stringify(entities,null,2));
|
||||||
|
|
||||||
|
const caseMapping: Record<string, string> = { "": "" };
|
||||||
|
for (const e of entities) {
|
||||||
|
// console.log(`looking for: ${JSON.stringify(e, null, 2)}`);
|
||||||
|
|
||||||
|
let parentFolder = getParentFolder(e.keyRaw);
|
||||||
|
if (parentFolder === "/") {
|
||||||
|
parentFolder = "";
|
||||||
|
}
|
||||||
|
const parentFolderLower = parentFolder.toLocaleLowerCase();
|
||||||
|
const segs = e.keyRaw.split("/");
|
||||||
|
if (e.keyRaw.endsWith("/")) {
|
||||||
|
// folder
|
||||||
|
if (caseMapping.hasOwnProperty(parentFolderLower)) {
|
||||||
|
const newKeyRaw = `${caseMapping[parentFolderLower]}${segs
|
||||||
|
.slice(-2)
|
||||||
|
.join("/")}`;
|
||||||
|
caseMapping[newKeyRaw.toLocaleLowerCase()] = newKeyRaw;
|
||||||
|
e.keyRaw = newKeyRaw;
|
||||||
|
// console.log(JSON.stringify(caseMapping,null,2));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw Error(`${parentFolder} doesn't have cases record??`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// file
|
||||||
|
if (caseMapping.hasOwnProperty(parentFolderLower)) {
|
||||||
|
const newKeyRaw = `${caseMapping[parentFolderLower]}${segs
|
||||||
|
.slice(-1)
|
||||||
|
.join("/")}`;
|
||||||
|
e.keyRaw = newKeyRaw;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw Error(`${parentFolder} doesn't have cases record??`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { unixTimeToStr } from "./misc";
|
||||||
|
|
||||||
|
interface BreakPoint {
|
||||||
|
label: string;
|
||||||
|
fakeTimeMilli: number; // it's NOT a unix timestamp
|
||||||
|
indent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Profiler {
|
||||||
|
startTime: number;
|
||||||
|
breakPoints: BreakPoint[];
|
||||||
|
indent: number;
|
||||||
|
constructor(label?: string) {
|
||||||
|
this.breakPoints = [];
|
||||||
|
this.indent = 0;
|
||||||
|
this.startTime = 0;
|
||||||
|
|
||||||
|
if (label !== undefined) {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
this.breakPoints.push({
|
||||||
|
label: label,
|
||||||
|
fakeTimeMilli: performance.now(),
|
||||||
|
indent: this.indent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insert(label: string) {
|
||||||
|
if (this.breakPoints.length === 0) {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
this.breakPoints.push({
|
||||||
|
label: label,
|
||||||
|
fakeTimeMilli: performance.now(),
|
||||||
|
indent: this.indent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addIndent() {
|
||||||
|
this.indent += 2;
|
||||||
|
}
|
||||||
|
removeIndent() {
|
||||||
|
this.indent -= 2;
|
||||||
|
if (this.indent < 0) {
|
||||||
|
this.indent = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.breakPoints = [];
|
||||||
|
this.indent = 0;
|
||||||
|
this.startTime = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
if (this.breakPoints.length === 0) {
|
||||||
|
return "nothing in profiler";
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = `[startTime]: ${unixTimeToStr(this.startTime)}`;
|
||||||
|
for (let i = 0; i < this.breakPoints.length; ++i) {
|
||||||
|
if (i === 0) {
|
||||||
|
res += `\n[${this.breakPoints[i]["label"]}]: start`;
|
||||||
|
} else {
|
||||||
|
const label = this.breakPoints[i]["label"];
|
||||||
|
const indent = this.breakPoints[i]["indent"];
|
||||||
|
const millsec =
|
||||||
|
Math.round(
|
||||||
|
(this.breakPoints[i]["fakeTimeMilli"] -
|
||||||
|
this.breakPoints[i - 1]["fakeTimeMilli"]) *
|
||||||
|
10
|
||||||
|
) / 10.0;
|
||||||
|
res += `\n${" ".repeat(indent)}[${label}]: ${millsec}ms`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-19
@@ -12,6 +12,8 @@ import * as dropbox from "./remoteForDropbox";
|
|||||||
import * as onedrive from "./remoteForOnedrive";
|
import * as onedrive from "./remoteForOnedrive";
|
||||||
import * as s3 from "./remoteForS3";
|
import * as s3 from "./remoteForS3";
|
||||||
import * as webdav from "./remoteForWebdav";
|
import * as webdav from "./remoteForWebdav";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
import { Profiler } from "./profiler";
|
||||||
|
|
||||||
export class RemoteClient {
|
export class RemoteClient {
|
||||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
@@ -30,7 +32,8 @@ export class RemoteClient {
|
|||||||
dropboxConfig?: DropboxConfig,
|
dropboxConfig?: DropboxConfig,
|
||||||
onedriveConfig?: OnedriveConfig,
|
onedriveConfig?: OnedriveConfig,
|
||||||
vaultName?: string,
|
vaultName?: string,
|
||||||
saveUpdatedConfigFunc?: () => Promise<any>
|
saveUpdatedConfigFunc?: () => Promise<any>,
|
||||||
|
profiler?: Profiler
|
||||||
) {
|
) {
|
||||||
this.serviceType = serviceType;
|
this.serviceType = serviceType;
|
||||||
// the client may modify the config inplace,
|
// the client may modify the config inplace,
|
||||||
@@ -105,8 +108,8 @@ export class RemoteClient {
|
|||||||
uploadToRemote = async (
|
uploadToRemote = async (
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault | undefined,
|
vault: Vault | undefined,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
foldersCreatedBefore: Set<string> | undefined = undefined,
|
foldersCreatedBefore: Set<string> | undefined = undefined,
|
||||||
uploadRaw: boolean = false,
|
uploadRaw: boolean = false,
|
||||||
@@ -119,7 +122,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
isRecursively,
|
isRecursively,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
uploadRaw,
|
uploadRaw,
|
||||||
rawContent
|
rawContent
|
||||||
@@ -130,7 +133,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
isRecursively,
|
isRecursively,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
uploadRaw,
|
uploadRaw,
|
||||||
rawContent
|
rawContent
|
||||||
@@ -141,7 +144,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
isRecursively,
|
isRecursively,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
foldersCreatedBefore,
|
foldersCreatedBefore,
|
||||||
uploadRaw,
|
uploadRaw,
|
||||||
@@ -153,7 +156,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
isRecursively,
|
isRecursively,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
foldersCreatedBefore,
|
foldersCreatedBefore,
|
||||||
uploadRaw,
|
uploadRaw,
|
||||||
@@ -185,7 +188,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
skipSaving: boolean = false
|
skipSaving: boolean = false
|
||||||
) => {
|
) => {
|
||||||
@@ -196,7 +199,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
mtime,
|
mtime,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
skipSaving
|
skipSaving
|
||||||
);
|
);
|
||||||
@@ -206,7 +209,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
mtime,
|
mtime,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
skipSaving
|
skipSaving
|
||||||
);
|
);
|
||||||
@@ -216,7 +219,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
mtime,
|
mtime,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
skipSaving
|
skipSaving
|
||||||
);
|
);
|
||||||
@@ -226,7 +229,7 @@ export class RemoteClient {
|
|||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
mtime,
|
mtime,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey,
|
remoteEncryptedKey,
|
||||||
skipSaving
|
skipSaving
|
||||||
);
|
);
|
||||||
@@ -237,36 +240,38 @@ export class RemoteClient {
|
|||||||
|
|
||||||
deleteFromRemote = async (
|
deleteFromRemote = async (
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = "",
|
||||||
|
synthesizedFolder: boolean = false
|
||||||
) => {
|
) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.deleteFromRemote(
|
return await s3.deleteFromRemote(
|
||||||
s3.getS3Client(this.s3Config!),
|
s3.getS3Client(this.s3Config!),
|
||||||
this.s3Config!,
|
this.s3Config!,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey,
|
||||||
|
synthesizedFolder
|
||||||
);
|
);
|
||||||
} else if (this.serviceType === "webdav") {
|
} else if (this.serviceType === "webdav") {
|
||||||
return await webdav.deleteFromRemote(
|
return await webdav.deleteFromRemote(
|
||||||
this.webdavClient!,
|
this.webdavClient!,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey
|
||||||
);
|
);
|
||||||
} else if (this.serviceType === "dropbox") {
|
} else if (this.serviceType === "dropbox") {
|
||||||
return await dropbox.deleteFromRemote(
|
return await dropbox.deleteFromRemote(
|
||||||
this.dropboxClient!,
|
this.dropboxClient!,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey
|
||||||
);
|
);
|
||||||
} else if (this.serviceType === "onedrive") {
|
} else if (this.serviceType === "onedrive") {
|
||||||
return await onedrive.deleteFromRemote(
|
return await onedrive.deleteFromRemote(
|
||||||
this.onedriveClient!,
|
this.onedriveClient!,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
password,
|
cipher,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+24
-19
@@ -1,4 +1,3 @@
|
|||||||
import { rangeDelay } from "delay";
|
|
||||||
import { Dropbox, DropboxAuth } from "dropbox";
|
import { Dropbox, DropboxAuth } from "dropbox";
|
||||||
import type { files, DropboxResponseError, DropboxResponse } from "dropbox";
|
import type { files, DropboxResponseError, DropboxResponse } from "dropbox";
|
||||||
import { Vault } from "obsidian";
|
import { Vault } from "obsidian";
|
||||||
@@ -10,14 +9,17 @@ import {
|
|||||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||||
UploadedType,
|
UploadedType,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
|
||||||
import {
|
import {
|
||||||
bufferToArrayBuffer,
|
bufferToArrayBuffer,
|
||||||
|
delay,
|
||||||
|
fixEntityListCasesInplace,
|
||||||
getFolderLevels,
|
getFolderLevels,
|
||||||
hasEmojiInText,
|
hasEmojiInText,
|
||||||
headersToRecord,
|
headersToRecord,
|
||||||
mkdirpInVault,
|
mkdirpInVault,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
import { random } from "lodash";
|
||||||
|
|
||||||
export { Dropbox } from "dropbox";
|
export { Dropbox } from "dropbox";
|
||||||
|
|
||||||
@@ -291,7 +293,7 @@ async function retryReq<T>(
|
|||||||
2
|
2
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
await rangeDelay(secMin * 1000, secMax * 1000);
|
await delay(random(secMin * 1000, secMax * 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,8 +453,8 @@ export const uploadToRemote = async (
|
|||||||
client: WrappedDropboxClient,
|
client: WrappedDropboxClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault | undefined,
|
vault: Vault | undefined,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
foldersCreatedBefore: Set<string> | undefined = undefined,
|
foldersCreatedBefore: Set<string> | undefined = undefined,
|
||||||
uploadRaw: boolean = false,
|
uploadRaw: boolean = false,
|
||||||
@@ -463,7 +465,7 @@ export const uploadToRemote = async (
|
|||||||
await client.init();
|
await client.init();
|
||||||
|
|
||||||
let uploadFile = fileOrFolderPath;
|
let uploadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
||||||
throw Error(
|
throw Error(
|
||||||
`uploadToRemote(dropbox) you have password but remoteEncryptedKey is empty!`
|
`uploadToRemote(dropbox) you have password but remoteEncryptedKey is empty!`
|
||||||
@@ -483,8 +485,8 @@ export const uploadToRemote = async (
|
|||||||
let ctime = 0;
|
let ctime = 0;
|
||||||
const s = await vault?.adapter?.stat(fileOrFolderPath);
|
const s = await vault?.adapter?.stat(fileOrFolderPath);
|
||||||
if (s !== undefined && s !== null) {
|
if (s !== undefined && s !== null) {
|
||||||
mtime = Math.round(s.mtime / 1000.0) * 1000;
|
mtime = Math.floor(s.mtime / 1000.0) * 1000;
|
||||||
ctime = Math.round(s.ctime / 1000.0) * 1000;
|
ctime = Math.floor(s.ctime / 1000.0) * 1000;
|
||||||
}
|
}
|
||||||
const mtimeStr = new Date(mtime).toISOString().replace(/\.\d{3}Z$/, "Z");
|
const mtimeStr = new Date(mtime).toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||||
|
|
||||||
@@ -497,8 +499,8 @@ export const uploadToRemote = async (
|
|||||||
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
||||||
}
|
}
|
||||||
// folder
|
// folder
|
||||||
if (password === "") {
|
if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
|
||||||
// if not encrypted, mkdir a remote folder
|
// if not encrypted, || encrypted isFolderAware, mkdir a remote folder
|
||||||
if (foldersCreatedBefore?.has(uploadFile)) {
|
if (foldersCreatedBefore?.has(uploadFile)) {
|
||||||
// created, pass
|
// created, pass
|
||||||
} else {
|
} else {
|
||||||
@@ -530,7 +532,8 @@ export const uploadToRemote = async (
|
|||||||
mtimeCli: mtime,
|
mtimeCli: mtime,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// if encrypted, upload a fake file with the encrypted file name
|
// if encrypted && !isFolderAware(),
|
||||||
|
// upload a fake file with the encrypted file name
|
||||||
await retryReq(
|
await retryReq(
|
||||||
() =>
|
() =>
|
||||||
client.dropbox.filesUpload({
|
client.dropbox.filesUpload({
|
||||||
@@ -564,8 +567,8 @@ export const uploadToRemote = async (
|
|||||||
localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
||||||
}
|
}
|
||||||
let remoteContent = localContent;
|
let remoteContent = localContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
remoteContent = await cipher.encryptContent(localContent);
|
||||||
}
|
}
|
||||||
// in dropbox, we don't need to create folders before uploading! cool!
|
// in dropbox, we don't need to create folders before uploading! cool!
|
||||||
// TODO: filesUploadSession for larger files (>=150 MB)
|
// TODO: filesUploadSession for larger files (>=150 MB)
|
||||||
@@ -634,6 +637,8 @@ export const listAllFromRemote = async (client: WrappedDropboxClient) => {
|
|||||||
unifiedContents.push(...unifiedContents2);
|
unifiedContents.push(...unifiedContents2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fixEntityListCasesInplace(unifiedContents);
|
||||||
|
|
||||||
return unifiedContents;
|
return unifiedContents;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -670,7 +675,7 @@ export const downloadFromRemote = async (
|
|||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
skipSaving: boolean = false
|
skipSaving: boolean = false
|
||||||
) => {
|
) => {
|
||||||
@@ -691,14 +696,14 @@ export const downloadFromRemote = async (
|
|||||||
return new ArrayBuffer(0);
|
return new ArrayBuffer(0);
|
||||||
} else {
|
} else {
|
||||||
let downloadFile = fileOrFolderPath;
|
let downloadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
downloadFile = remoteEncryptedKey;
|
downloadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
|
downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
|
||||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||||
let localContent = remoteContent;
|
let localContent = remoteContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
localContent = await cipher.decryptContent(remoteContent);
|
||||||
}
|
}
|
||||||
if (!skipSaving) {
|
if (!skipSaving) {
|
||||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||||
@@ -712,14 +717,14 @@ export const downloadFromRemote = async (
|
|||||||
export const deleteFromRemote = async (
|
export const deleteFromRemote = async (
|
||||||
client: WrappedDropboxClient,
|
client: WrappedDropboxClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
if (fileOrFolderPath === "/") {
|
if (fileOrFolderPath === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let remoteFileName = fileOrFolderPath;
|
let remoteFileName = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);
|
remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);
|
||||||
|
|||||||
+50
-20
@@ -17,13 +17,13 @@ import {
|
|||||||
Entity,
|
Entity,
|
||||||
UploadedType,
|
UploadedType,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
|
||||||
import {
|
import {
|
||||||
bufferToArrayBuffer,
|
bufferToArrayBuffer,
|
||||||
getRandomArrayBuffer,
|
getRandomArrayBuffer,
|
||||||
getRandomIntInclusive,
|
getRandomIntInclusive,
|
||||||
mkdirpInVault,
|
mkdirpInVault,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
|
||||||
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
|
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
|
||||||
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
|
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
|
||||||
@@ -267,6 +267,10 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
// pure english: /drive/root:/Apps/remotely-save/${remoteBaseDir}
|
// pure english: /drive/root:/Apps/remotely-save/${remoteBaseDir}
|
||||||
// or localized, e.g.: /drive/root:/应用/remotely-save/${remoteBaseDir}
|
// or localized, e.g.: /drive/root:/应用/remotely-save/${remoteBaseDir}
|
||||||
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
||||||
|
|
||||||
|
// why?? /drive/root:/Apps/Graph
|
||||||
|
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
||||||
|
|
||||||
// or the root is absolute path /Livefolders,
|
// or the root is absolute path /Livefolders,
|
||||||
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
||||||
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||||
@@ -289,6 +293,7 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
}
|
}
|
||||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||||
|
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
||||||
const matchSecondPrefixRes = fullPathOriginal.match(
|
const matchSecondPrefixRes = fullPathOriginal.match(
|
||||||
SECOND_COMMON_PREFIX_REGEX
|
SECOND_COMMON_PREFIX_REGEX
|
||||||
);
|
);
|
||||||
@@ -299,6 +304,12 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
) {
|
) {
|
||||||
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`;
|
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`;
|
||||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (
|
||||||
|
matchFifthPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchFifthPrefixRes[0]}${remoteBaseDir}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDir}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
} else if (
|
} else if (
|
||||||
matchSecondPrefixRes !== null &&
|
matchSecondPrefixRes !== null &&
|
||||||
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`)
|
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`)
|
||||||
@@ -407,6 +418,19 @@ class MyAuthProvider implements AuthenticationProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* to export the settings in qrcode,
|
||||||
|
* we want to "trim" or "shrink" the settings
|
||||||
|
* @param onedriveConfig
|
||||||
|
*/
|
||||||
|
export const getShrinkedSettings = (onedriveConfig: OnedriveConfig) => {
|
||||||
|
const config = cloneDeep(onedriveConfig);
|
||||||
|
config.accessToken = "x";
|
||||||
|
config.accessTokenExpiresInSeconds = 1;
|
||||||
|
config.accessTokenExpiresAtTime = 1;
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
export class WrappedOnedriveClient {
|
export class WrappedOnedriveClient {
|
||||||
onedriveConfig: OnedriveConfig;
|
onedriveConfig: OnedriveConfig;
|
||||||
remoteBaseDir: string;
|
remoteBaseDir: string;
|
||||||
@@ -471,6 +495,11 @@ export class WrappedOnedriveClient {
|
|||||||
const pathFrag = encodeURI(pathFragOrig);
|
const pathFrag = encodeURI(pathFragOrig);
|
||||||
theUrl = `${API_PREFIX}${pathFrag}`;
|
theUrl = `${API_PREFIX}${pathFrag}`;
|
||||||
}
|
}
|
||||||
|
// we want to support file name with hash #
|
||||||
|
// because every url we construct here do not contain the # symbol
|
||||||
|
// thus it should be safe to directly replace the character
|
||||||
|
theUrl = theUrl.replace(/#/g, "%23");
|
||||||
|
// console.debug(`building url: [${pathFragOrig}] => [${theUrl}]`)
|
||||||
return theUrl;
|
return theUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -550,7 +579,7 @@ export class WrappedOnedriveClient {
|
|||||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||||
// Use fetch everywhere instead!
|
// Use fetch everywhere instead!
|
||||||
if (false /*VALID_REQURL*/) {
|
if (false /*VALID_REQURL*/) {
|
||||||
await requestUrl({
|
const res = await requestUrl({
|
||||||
url: theUrl,
|
url: theUrl,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: payload,
|
body: payload,
|
||||||
@@ -560,8 +589,9 @@ export class WrappedOnedriveClient {
|
|||||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return res.json as DriveItem | UploadSession;
|
||||||
} else {
|
} else {
|
||||||
await fetch(theUrl, {
|
const res = await fetch(theUrl, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: payload,
|
body: payload,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -569,6 +599,7 @@ export class WrappedOnedriveClient {
|
|||||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return (await res.json()) as DriveItem | UploadSession;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -694,8 +725,8 @@ export const uploadToRemote = async (
|
|||||||
client: WrappedOnedriveClient,
|
client: WrappedOnedriveClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault | undefined,
|
vault: Vault | undefined,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
foldersCreatedBefore: Set<string> | undefined = undefined,
|
foldersCreatedBefore: Set<string> | undefined = undefined,
|
||||||
uploadRaw: boolean = false,
|
uploadRaw: boolean = false,
|
||||||
@@ -704,7 +735,7 @@ export const uploadToRemote = async (
|
|||||||
await client.init();
|
await client.init();
|
||||||
|
|
||||||
let uploadFile = fileOrFolderPath;
|
let uploadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
||||||
throw Error(
|
throw Error(
|
||||||
`uploadToRemote(onedrive) you have password but remoteEncryptedKey is empty!`
|
`uploadToRemote(onedrive) you have password but remoteEncryptedKey is empty!`
|
||||||
@@ -734,8 +765,8 @@ export const uploadToRemote = async (
|
|||||||
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
||||||
}
|
}
|
||||||
// folder
|
// folder
|
||||||
if (password === "") {
|
if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
|
||||||
// if not encrypted, mkdir a remote folder
|
// if not encrypted, || encrypted isFolderAware, mkdir a remote folder
|
||||||
if (foldersCreatedBefore?.has(uploadFile)) {
|
if (foldersCreatedBefore?.has(uploadFile)) {
|
||||||
// created, pass
|
// created, pass
|
||||||
} else {
|
} else {
|
||||||
@@ -763,16 +794,15 @@ export const uploadToRemote = async (
|
|||||||
mtimeCli: mtime,
|
mtimeCli: mtime,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// if encrypted,
|
// if encrypted && !isFolderAware(),
|
||||||
// upload a fake, random-size file
|
// upload a fake, random-size file
|
||||||
// with the encrypted file name
|
// with the encrypted file name
|
||||||
const byteLengthRandom = getRandomIntInclusive(
|
const byteLengthRandom = getRandomIntInclusive(
|
||||||
1,
|
1,
|
||||||
65536 /* max allowed */
|
65536 /* max allowed */
|
||||||
);
|
);
|
||||||
const arrBufRandom = await encryptArrayBuffer(
|
const arrBufRandom = await cipher.encryptContent(
|
||||||
getRandomArrayBuffer(byteLengthRandom),
|
getRandomArrayBuffer(byteLengthRandom)
|
||||||
password
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// an encrypted folder is always small, we just use put here
|
// an encrypted folder is always small, we just use put here
|
||||||
@@ -816,8 +846,8 @@ export const uploadToRemote = async (
|
|||||||
localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
||||||
}
|
}
|
||||||
let remoteContent = localContent;
|
let remoteContent = localContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
remoteContent = await cipher.encryptContent(localContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// no need to create parent folders firstly, cool!
|
// no need to create parent folders firstly, cool!
|
||||||
@@ -930,7 +960,7 @@ export const downloadFromRemote = async (
|
|||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
skipSaving: boolean = false
|
skipSaving: boolean = false
|
||||||
) => {
|
) => {
|
||||||
@@ -948,14 +978,14 @@ export const downloadFromRemote = async (
|
|||||||
return new ArrayBuffer(0);
|
return new ArrayBuffer(0);
|
||||||
} else {
|
} else {
|
||||||
let downloadFile = fileOrFolderPath;
|
let downloadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
downloadFile = remoteEncryptedKey;
|
downloadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
|
downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
|
||||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||||
let localContent = remoteContent;
|
let localContent = remoteContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
localContent = await cipher.decryptContent(remoteContent);
|
||||||
}
|
}
|
||||||
if (!skipSaving) {
|
if (!skipSaving) {
|
||||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||||
@@ -969,14 +999,14 @@ export const downloadFromRemote = async (
|
|||||||
export const deleteFromRemote = async (
|
export const deleteFromRemote = async (
|
||||||
client: WrappedOnedriveClient,
|
client: WrappedOnedriveClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
if (fileOrFolderPath === "/") {
|
if (fileOrFolderPath === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let remoteFileName = fileOrFolderPath;
|
let remoteFileName = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);
|
remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);
|
||||||
|
|||||||
+52
-23
@@ -22,7 +22,7 @@ import { buildQueryString } from "@smithy/querystring-builder";
|
|||||||
import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types";
|
import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
import { Vault, requestUrl, RequestUrlParam } from "obsidian";
|
import { Vault, requestUrl, RequestUrlParam, Platform } from "obsidian";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import AggregateError from "aggregate-error";
|
import AggregateError from "aggregate-error";
|
||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
UploadedType,
|
UploadedType,
|
||||||
VALID_REQURL,
|
VALID_REQURL,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
|
||||||
import {
|
import {
|
||||||
arrayBufferToBuffer,
|
arrayBufferToBuffer,
|
||||||
bufferToArrayBuffer,
|
bufferToArrayBuffer,
|
||||||
@@ -43,6 +42,7 @@ import {
|
|||||||
export { S3Client } from "@aws-sdk/client-s3";
|
export { S3Client } from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// special handler using Obsidian requestUrl
|
// special handler using Obsidian requestUrl
|
||||||
@@ -233,7 +233,14 @@ const fromS3ObjectToEntity = (
|
|||||||
if (x.Key! in mtimeRecords) {
|
if (x.Key! in mtimeRecords) {
|
||||||
const m2 = mtimeRecords[x.Key!];
|
const m2 = mtimeRecords[x.Key!];
|
||||||
if (m2 !== 0) {
|
if (m2 !== 0) {
|
||||||
|
// to be compatible with RClone, we read and store the time in seconds in new version!
|
||||||
|
if (m2 >= 1000000000000) {
|
||||||
|
// it's a millsecond, uploaded by old codes..
|
||||||
mtimeCli = m2;
|
mtimeCli = m2;
|
||||||
|
} else {
|
||||||
|
// it's a second, uploaded by new codes of the plugin from March 24, 2024
|
||||||
|
mtimeCli = m2 * 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = getLocalNoPrefixPath(x.Key!, remotePrefix);
|
const key = getLocalNoPrefixPath(x.Key!, remotePrefix);
|
||||||
@@ -243,6 +250,7 @@ const fromS3ObjectToEntity = (
|
|||||||
mtimeCli: mtimeCli,
|
mtimeCli: mtimeCli,
|
||||||
sizeRaw: x.Size!,
|
sizeRaw: x.Size!,
|
||||||
etag: x.ETag,
|
etag: x.ETag,
|
||||||
|
synthesizedFolder: false,
|
||||||
};
|
};
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
@@ -261,7 +269,14 @@ const fromS3HeadObjectToEntity = (
|
|||||||
parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0")
|
parseFloat(x.Metadata.mtime || x.Metadata.MTime || "0")
|
||||||
);
|
);
|
||||||
if (m2 !== 0) {
|
if (m2 !== 0) {
|
||||||
|
// to be compatible with RClone, we read and store the time in seconds in new version!
|
||||||
|
if (m2 >= 1000000000000) {
|
||||||
|
// it's a millsecond, uploaded by old codes..
|
||||||
mtimeCli = m2;
|
mtimeCli = m2;
|
||||||
|
} else {
|
||||||
|
// it's a second, uploaded by new codes of the plugin from March 24, 2024
|
||||||
|
mtimeCli = m2 * 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.debug(
|
// console.debug(
|
||||||
@@ -358,8 +373,8 @@ export const uploadToRemote = async (
|
|||||||
s3Config: S3Config,
|
s3Config: S3Config,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault | undefined,
|
vault: Vault | undefined,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
uploadRaw: boolean = false,
|
uploadRaw: boolean = false,
|
||||||
rawContent: string | ArrayBuffer = "",
|
rawContent: string | ArrayBuffer = "",
|
||||||
@@ -368,7 +383,7 @@ export const uploadToRemote = async (
|
|||||||
): Promise<UploadedType> => {
|
): Promise<UploadedType> => {
|
||||||
console.debug(`uploading ${fileOrFolderPath}`);
|
console.debug(`uploading ${fileOrFolderPath}`);
|
||||||
let uploadFile = fileOrFolderPath;
|
let uploadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
||||||
throw Error(
|
throw Error(
|
||||||
`uploadToRemote(s3) you have password but remoteEncryptedKey is empty!`
|
`uploadToRemote(s3) you have password but remoteEncryptedKey is empty!`
|
||||||
@@ -402,8 +417,8 @@ export const uploadToRemote = async (
|
|||||||
Body: "",
|
Body: "",
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
Metadata: {
|
Metadata: {
|
||||||
MTime: `${mtime}`,
|
MTime: `${mtime / 1000.0}`,
|
||||||
CTime: `${ctime}`,
|
CTime: `${ctime / 1000.0}`,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -416,7 +431,7 @@ export const uploadToRemote = async (
|
|||||||
// file
|
// file
|
||||||
// we ignore isRecursively parameter here
|
// we ignore isRecursively parameter here
|
||||||
let contentType = DEFAULT_CONTENT_TYPE;
|
let contentType = DEFAULT_CONTENT_TYPE;
|
||||||
if (password === "") {
|
if (cipher.isPasswordEmpty()) {
|
||||||
contentType =
|
contentType =
|
||||||
mime.contentType(
|
mime.contentType(
|
||||||
mime.lookup(fileOrFolderPath) || DEFAULT_CONTENT_TYPE
|
mime.lookup(fileOrFolderPath) || DEFAULT_CONTENT_TYPE
|
||||||
@@ -447,8 +462,8 @@ export const uploadToRemote = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let remoteContent = localContent;
|
let remoteContent = localContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
remoteContent = await cipher.encryptContent(localContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytesIn5MB = 5242880;
|
const bytesIn5MB = 5242880;
|
||||||
@@ -465,8 +480,8 @@ export const uploadToRemote = async (
|
|||||||
Body: body,
|
Body: body,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
Metadata: {
|
Metadata: {
|
||||||
MTime: `${mtime}`,
|
MTime: `${mtime / 1000.0}`,
|
||||||
CTime: `${ctime}`,
|
CTime: `${ctime / 1000.0}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -585,7 +600,10 @@ export const listAllFromRemote = async (
|
|||||||
s3Client: S3Client,
|
s3Client: S3Client,
|
||||||
s3Config: S3Config
|
s3Config: S3Config
|
||||||
) => {
|
) => {
|
||||||
return await listFromRemoteRaw(s3Client, s3Config, s3Config.remotePrefix);
|
const res = (
|
||||||
|
await listFromRemoteRaw(s3Client, s3Config, s3Config.remotePrefix)
|
||||||
|
).filter((x) => x.keyRaw !== "" && x.keyRaw !== "/");
|
||||||
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -645,8 +663,8 @@ export const downloadFromRemote = async (
|
|||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string,
|
||||||
skipSaving: boolean = false
|
skipSaving: boolean = false
|
||||||
) => {
|
) => {
|
||||||
const isFolder = fileOrFolderPath.endsWith("/");
|
const isFolder = fileOrFolderPath.endsWith("/");
|
||||||
@@ -664,7 +682,7 @@ export const downloadFromRemote = async (
|
|||||||
return new ArrayBuffer(0);
|
return new ArrayBuffer(0);
|
||||||
} else {
|
} else {
|
||||||
let downloadFile = fileOrFolderPath;
|
let downloadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
downloadFile = remoteEncryptedKey;
|
downloadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
downloadFile = getRemoteWithPrefixPath(
|
downloadFile = getRemoteWithPrefixPath(
|
||||||
@@ -677,8 +695,8 @@ export const downloadFromRemote = async (
|
|||||||
downloadFile
|
downloadFile
|
||||||
);
|
);
|
||||||
let localContent = remoteContent;
|
let localContent = remoteContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
localContent = await cipher.decryptContent(remoteContent);
|
||||||
}
|
}
|
||||||
if (!skipSaving) {
|
if (!skipSaving) {
|
||||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||||
@@ -700,14 +718,18 @@ export const deleteFromRemote = async (
|
|||||||
s3Client: S3Client,
|
s3Client: S3Client,
|
||||||
s3Config: S3Config,
|
s3Config: S3Config,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = "",
|
||||||
|
synthesizedFolder: boolean = false
|
||||||
) => {
|
) => {
|
||||||
if (fileOrFolderPath === "/") {
|
if (fileOrFolderPath === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (synthesizedFolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let remoteFileName = fileOrFolderPath;
|
let remoteFileName = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
remoteFileName = getRemoteWithPrefixPath(
|
remoteFileName = getRemoteWithPrefixPath(
|
||||||
@@ -721,7 +743,7 @@ export const deleteFromRemote = async (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fileOrFolderPath.endsWith("/") && password === "") {
|
if (fileOrFolderPath.endsWith("/") && cipher.isPasswordEmpty()) {
|
||||||
const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName);
|
const x = await listFromRemoteRaw(s3Client, s3Config, remoteFileName);
|
||||||
x.forEach(async (element) => {
|
x.forEach(async (element) => {
|
||||||
await s3Client.send(
|
await s3Client.send(
|
||||||
@@ -731,7 +753,7 @@ export const deleteFromRemote = async (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} else if (fileOrFolderPath.endsWith("/") && password !== "") {
|
} else if (fileOrFolderPath.endsWith("/") && !cipher.isPasswordEmpty()) {
|
||||||
// TODO
|
// TODO
|
||||||
} else {
|
} else {
|
||||||
// pass
|
// pass
|
||||||
@@ -756,6 +778,13 @@ export const checkConnectivity = async (
|
|||||||
callbackFunc?: any
|
callbackFunc?: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
// TODO: no universal way now, just check this in connectivity
|
||||||
|
if (Platform.isIosApp && s3Config.s3Endpoint.startsWith("http://")) {
|
||||||
|
throw Error(
|
||||||
|
`Your s3 endpoint could only be https, not http, because of the iOS restriction.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// const results = await s3Client.send(
|
// const results = await s3Client.send(
|
||||||
// new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
|
// new HeadBucketCommand({ Bucket: s3Config.s3BucketName })
|
||||||
// );
|
// );
|
||||||
|
|||||||
+59
-80
@@ -1,13 +1,14 @@
|
|||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
import { Vault, requestUrl } from "obsidian";
|
import { Platform, Vault, requestUrl } from "obsidian";
|
||||||
|
|
||||||
import { Queue } from "@fyears/tsqueue";
|
import { Queue } from "@fyears/tsqueue";
|
||||||
import chunk from "lodash/chunk";
|
import chunk from "lodash/chunk";
|
||||||
import flatten from "lodash/flatten";
|
import flatten from "lodash/flatten";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import { getReasonPhrase } from "http-status-codes";
|
import { getReasonPhrase } from "http-status-codes";
|
||||||
import { Entity, UploadedType, VALID_REQURL, WebdavConfig } from "./baseTypes";
|
import { Entity, UploadedType, VALID_REQURL, WebdavConfig } from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
|
||||||
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FileStat,
|
FileStat,
|
||||||
@@ -47,34 +48,52 @@ if (VALID_REQURL) {
|
|||||||
delete transformedHeaders["host"];
|
delete transformedHeaders["host"];
|
||||||
delete transformedHeaders["content-length"];
|
delete transformedHeaders["content-length"];
|
||||||
|
|
||||||
|
const reqContentType =
|
||||||
|
transformedHeaders["accept"] ?? transformedHeaders["content-type"];
|
||||||
|
|
||||||
|
const retractedHeaders = { ...transformedHeaders };
|
||||||
|
if (retractedHeaders.hasOwnProperty("authorization")) {
|
||||||
|
retractedHeaders["authorization"] = "<retracted>";
|
||||||
|
}
|
||||||
|
|
||||||
console.debug(`before request:`);
|
console.debug(`before request:`);
|
||||||
console.debug(`url: ${options.url}`);
|
console.debug(`url: ${options.url}`);
|
||||||
console.debug(`method: ${options.method}`);
|
console.debug(`method: ${options.method}`);
|
||||||
console.debug(`headers: ${JSON.stringify(transformedHeaders, null, 2)}`);
|
console.debug(`headers: ${JSON.stringify(retractedHeaders, null, 2)}`);
|
||||||
|
console.debug(`reqContentType: ${reqContentType}`);
|
||||||
|
|
||||||
const r = await requestUrl({
|
let r = await requestUrl({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
method: options.method,
|
method: options.method,
|
||||||
body: options.data as string | ArrayBuffer,
|
body: options.data as string | ArrayBuffer,
|
||||||
headers: transformedHeaders,
|
headers: transformedHeaders,
|
||||||
|
contentType: reqContentType,
|
||||||
throw: false,
|
throw: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
let contentType: string | undefined =
|
if (
|
||||||
r.headers["Content-Type"] || r.headers["content-type"];
|
r.status === 401 &&
|
||||||
if (options.headers !== undefined) {
|
Platform.isIosApp &&
|
||||||
contentType =
|
!options.url.endsWith("/") &&
|
||||||
contentType ||
|
!options.url.endsWith(".md") &&
|
||||||
transformedHeaders["content-type"] ||
|
options.method.toUpperCase() === "PROPFIND"
|
||||||
transformedHeaders["accept"];
|
) {
|
||||||
}
|
// don't ask me why,
|
||||||
if (contentType !== undefined) {
|
// some webdav servers have some mysterious behaviours,
|
||||||
contentType = contentType.toLowerCase();
|
// if a folder doesn't exist without slash, the servers return 401 instead of 404
|
||||||
|
// here is a dirty hack that works
|
||||||
|
console.debug(`so we have 401, try appending request url with slash`);
|
||||||
|
r = await requestUrl({
|
||||||
|
url: `${options.url}/`,
|
||||||
|
method: options.method,
|
||||||
|
body: options.data as string | ArrayBuffer,
|
||||||
|
headers: transformedHeaders,
|
||||||
|
contentType: reqContentType,
|
||||||
|
throw: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`after request:`);
|
console.debug(`after request:`);
|
||||||
console.debug(`contentType: ${contentType}`);
|
|
||||||
|
|
||||||
const rspHeaders = objKeyToLower({ ...r.headers });
|
const rspHeaders = objKeyToLower({ ...r.headers });
|
||||||
console.debug(`rspHeaders: ${JSON.stringify(rspHeaders, null, 2)}`);
|
console.debug(`rspHeaders: ${JSON.stringify(rspHeaders, null, 2)}`);
|
||||||
for (let key in rspHeaders) {
|
for (let key in rspHeaders) {
|
||||||
@@ -98,55 +117,6 @@ if (VALID_REQURL) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.info(`requesting url=${options.url}`);
|
|
||||||
// console.info(`contentType=${contentType}`);
|
|
||||||
// console.info(`rspHeaders=${JSON.stringify(rspHeaders)}`)
|
|
||||||
|
|
||||||
// let r2: Response = undefined;
|
|
||||||
// if (contentType.includes("xml")) {
|
|
||||||
// r2 = new Response(r.text, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (
|
|
||||||
// contentType.includes("json") ||
|
|
||||||
// contentType.includes("javascript")
|
|
||||||
// ) {
|
|
||||||
// console.info('inside json branch');
|
|
||||||
// // const j = r.json;
|
|
||||||
// // console.info(j);
|
|
||||||
// r2 = new Response(
|
|
||||||
// r.text, // yea, here is the text because Response constructor expects a text
|
|
||||||
// {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (contentType.includes("text")) {
|
|
||||||
// // avoid text/json,
|
|
||||||
// // so we split this out from the above xml or json branch
|
|
||||||
// r2 = new Response(r.text, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else if (
|
|
||||||
// contentType.includes("octet-stream") ||
|
|
||||||
// contentType.includes("binary") ||
|
|
||||||
// contentType.includes("buffer")
|
|
||||||
// ) {
|
|
||||||
// // application/octet-stream
|
|
||||||
// r2 = new Response(r.arrayBuffer, {
|
|
||||||
// status: r.status,
|
|
||||||
// statusText: getReasonPhrase(r.status),
|
|
||||||
// headers: rspHeaders,
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// throw Error(
|
|
||||||
// `do not know how to deal with requested content type = ${contentType}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
let r2: Response | undefined = undefined;
|
let r2: Response | undefined = undefined;
|
||||||
const statusText = getReasonPhrase(r.status);
|
const statusText = getReasonPhrase(r.status);
|
||||||
@@ -246,7 +216,8 @@ export class WrappedWebdavClient {
|
|||||||
remoteBaseDir: string,
|
remoteBaseDir: string,
|
||||||
saveUpdatedConfigFunc: () => Promise<any>
|
saveUpdatedConfigFunc: () => Promise<any>
|
||||||
) {
|
) {
|
||||||
this.webdavConfig = webdavConfig;
|
this.webdavConfig = cloneDeep(webdavConfig);
|
||||||
|
this.webdavConfig.address = encodeURI(this.webdavConfig.address);
|
||||||
this.remoteBaseDir = remoteBaseDir;
|
this.remoteBaseDir = remoteBaseDir;
|
||||||
this.vaultFolderExists = false;
|
this.vaultFolderExists = false;
|
||||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||||
@@ -257,6 +228,13 @@ export class WrappedWebdavClient {
|
|||||||
if (this.client !== undefined) {
|
if (this.client !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Platform.isIosApp && !this.webdavConfig.address.startsWith("https")) {
|
||||||
|
throw Error(
|
||||||
|
`Your webdav address could only be https, not http, because of the iOS restriction.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
};
|
};
|
||||||
@@ -350,15 +328,15 @@ export const uploadToRemote = async (
|
|||||||
client: WrappedWebdavClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault | undefined,
|
vault: Vault | undefined,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
uploadRaw: boolean = false,
|
uploadRaw: boolean = false,
|
||||||
rawContent: string | ArrayBuffer = ""
|
rawContent: string | ArrayBuffer = ""
|
||||||
): Promise<UploadedType> => {
|
): Promise<UploadedType> => {
|
||||||
await client.init();
|
await client.init();
|
||||||
let uploadFile = fileOrFolderPath;
|
let uploadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
||||||
throw Error(
|
throw Error(
|
||||||
`uploadToRemote(webdav) you have password but remoteEncryptedKey is empty!`
|
`uploadToRemote(webdav) you have password but remoteEncryptedKey is empty!`
|
||||||
@@ -377,8 +355,8 @@ export const uploadToRemote = async (
|
|||||||
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
throw Error(`you specify uploadRaw, but you also provide a folder key!`);
|
||||||
}
|
}
|
||||||
// folder
|
// folder
|
||||||
if (password === "") {
|
if (cipher.isPasswordEmpty() || cipher.isFolderAware()) {
|
||||||
// if not encrypted, mkdir a remote folder
|
// if not encrypted, || encrypted isFolderAware, mkdir a remote folder
|
||||||
await client.client.createDirectory(uploadFile, {
|
await client.client.createDirectory(uploadFile, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
@@ -387,7 +365,8 @@ export const uploadToRemote = async (
|
|||||||
entity: res,
|
entity: res,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// if encrypted, upload a fake file with the encrypted file name
|
// if encrypted && !isFolderAware(),
|
||||||
|
// upload a fake file with the encrypted file name
|
||||||
await client.client.putFileContents(uploadFile, "", {
|
await client.client.putFileContents(uploadFile, "", {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
onUploadProgress: (progress: any) => {
|
onUploadProgress: (progress: any) => {
|
||||||
@@ -420,8 +399,8 @@ export const uploadToRemote = async (
|
|||||||
mtimeCli = (await vault.adapter.stat(fileOrFolderPath))?.mtime;
|
mtimeCli = (await vault.adapter.stat(fileOrFolderPath))?.mtime;
|
||||||
}
|
}
|
||||||
let remoteContent = localContent;
|
let remoteContent = localContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
remoteContent = await cipher.encryptContent(localContent);
|
||||||
}
|
}
|
||||||
// updated 20220326: the algorithm guarantee this
|
// updated 20220326: the algorithm guarantee this
|
||||||
// // we need to create folders before uploading
|
// // we need to create folders before uploading
|
||||||
@@ -525,7 +504,7 @@ export const downloadFromRemote = async (
|
|||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = "",
|
remoteEncryptedKey: string = "",
|
||||||
skipSaving: boolean = false
|
skipSaving: boolean = false
|
||||||
) => {
|
) => {
|
||||||
@@ -546,15 +525,15 @@ export const downloadFromRemote = async (
|
|||||||
return new ArrayBuffer(0);
|
return new ArrayBuffer(0);
|
||||||
} else {
|
} else {
|
||||||
let downloadFile = fileOrFolderPath;
|
let downloadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
downloadFile = remoteEncryptedKey;
|
downloadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
|
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
|
||||||
// console.info(`downloadFile=${downloadFile}`);
|
// console.info(`downloadFile=${downloadFile}`);
|
||||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||||
let localContent = remoteContent;
|
let localContent = remoteContent;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
localContent = await cipher.decryptContent(remoteContent);
|
||||||
}
|
}
|
||||||
if (!skipSaving) {
|
if (!skipSaving) {
|
||||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||||
@@ -568,14 +547,14 @@ export const downloadFromRemote = async (
|
|||||||
export const deleteFromRemote = async (
|
export const deleteFromRemote = async (
|
||||||
client: WrappedWebdavClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
if (fileOrFolderPath === "/") {
|
if (fileOrFolderPath === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let remoteFileName = fileOrFolderPath;
|
let remoteFileName = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
|
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
|
||||||
|
|||||||
+206
-10
@@ -22,9 +22,18 @@ import {
|
|||||||
VALID_REQURL,
|
VALID_REQURL,
|
||||||
WebdavAuthType,
|
WebdavAuthType,
|
||||||
WebdavDepthType,
|
WebdavDepthType,
|
||||||
|
CipherMethodType,
|
||||||
|
QRExportType,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
import {
|
||||||
import { exportQrCodeUri } from "./importExport";
|
exportVaultProfilerResultsToFiles,
|
||||||
|
exportVaultSyncPlansToFiles,
|
||||||
|
} from "./debugMode";
|
||||||
|
import {
|
||||||
|
exportQrCodeUri,
|
||||||
|
importQrCodeUri,
|
||||||
|
parseUriByHand,
|
||||||
|
} from "./importExport";
|
||||||
import {
|
import {
|
||||||
clearAllPrevSyncRecordByVault,
|
clearAllPrevSyncRecordByVault,
|
||||||
clearAllSyncPlanRecords,
|
clearAllSyncPlanRecords,
|
||||||
@@ -51,6 +60,7 @@ import {
|
|||||||
stringToFragment,
|
stringToFragment,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
import { simpleTransRemotePrefix } from "./remoteForS3";
|
import { simpleTransRemotePrefix } from "./remoteForS3";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
|
|
||||||
class PasswordModal extends Modal {
|
class PasswordModal extends Modal {
|
||||||
plugin: RemotelySavePlugin;
|
plugin: RemotelySavePlugin;
|
||||||
@@ -122,6 +132,45 @@ class PasswordModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EncryptionMethodModal extends Modal {
|
||||||
|
plugin: RemotelySavePlugin;
|
||||||
|
constructor(app: App, plugin: RemotelySavePlugin) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
|
||||||
|
const t = (x: TransItemType, vars?: any) => {
|
||||||
|
return this.plugin.i18n.t(x, vars);
|
||||||
|
};
|
||||||
|
|
||||||
|
// contentEl.setText("Add Or change password.");
|
||||||
|
contentEl.createEl("h2", { text: t("modal_encryptionmethod_title") });
|
||||||
|
t("modal_encryptionmethod_shortdesc")
|
||||||
|
.split("\n")
|
||||||
|
.forEach((val, idx) => {
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: stringToFragment(val),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(contentEl).addButton((button) => {
|
||||||
|
button.setButtonText(t("confirm"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
button.setClass("encryptionmethod-second-confirm");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ChangeRemoteBaseDirModal extends Modal {
|
class ChangeRemoteBaseDirModal extends Modal {
|
||||||
readonly plugin: RemotelySavePlugin;
|
readonly plugin: RemotelySavePlugin;
|
||||||
readonly newRemoteBaseDir: string;
|
readonly newRemoteBaseDir: string;
|
||||||
@@ -504,6 +553,15 @@ export class OnedriveAuthModal extends Modal {
|
|||||||
text: val,
|
text: val,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
t("modal_onedriveauth_shortdesc_linux")
|
||||||
|
.split("\n")
|
||||||
|
.forEach((val) => {
|
||||||
|
contentEl.createEl("p", {
|
||||||
|
text: stringToFragment(val),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
const div2 = contentEl.createDiv();
|
const div2 = contentEl.createDiv();
|
||||||
div2.createEl(
|
div2.createEl(
|
||||||
"button",
|
"button",
|
||||||
@@ -655,9 +713,11 @@ class SyncConfigDirModal extends Modal {
|
|||||||
|
|
||||||
class ExportSettingsQrCodeModal extends Modal {
|
class ExportSettingsQrCodeModal extends Modal {
|
||||||
plugin: RemotelySavePlugin;
|
plugin: RemotelySavePlugin;
|
||||||
constructor(app: App, plugin: RemotelySavePlugin) {
|
exportType: QRExportType;
|
||||||
|
constructor(app: App, plugin: RemotelySavePlugin, exportType: QRExportType) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
this.exportType = exportType;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen() {
|
async onOpen() {
|
||||||
@@ -670,7 +730,8 @@ class ExportSettingsQrCodeModal extends Modal {
|
|||||||
const { rawUri, imgUri } = await exportQrCodeUri(
|
const { rawUri, imgUri } = await exportQrCodeUri(
|
||||||
this.plugin.settings,
|
this.plugin.settings,
|
||||||
this.app.vault.getName(),
|
this.app.vault.getName(),
|
||||||
this.plugin.manifest.version
|
this.plugin.manifest.version,
|
||||||
|
this.exportType
|
||||||
);
|
);
|
||||||
|
|
||||||
const div1 = contentEl.createDiv();
|
const div1 = contentEl.createDiv();
|
||||||
@@ -1634,6 +1695,23 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new Setting(basicDiv)
|
||||||
|
.setName(t("settings_encryptionmethod"))
|
||||||
|
.setDesc(stringToFragment(t("settings_encryptionmethod_desc")))
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown
|
||||||
|
.addOption("rclone-base64", t("settings_encryptionmethod_rclone"))
|
||||||
|
.addOption("openssl-base64", t("settings_encryptionmethod_openssl"))
|
||||||
|
.setValue(this.plugin.settings.encryptionMethod ?? "rclone-base64")
|
||||||
|
.onChange(async (val: string) => {
|
||||||
|
this.plugin.settings.encryptionMethod = val as CipherMethodType;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
if (this.plugin.settings.password !== "") {
|
||||||
|
new EncryptionMethodModal(this.app, this.plugin).open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(basicDiv)
|
new Setting(basicDiv)
|
||||||
.setName(t("settings_autorun"))
|
.setName(t("settings_autorun"))
|
||||||
.setDesc(t("settings_autorun_desc"))
|
.setDesc(t("settings_autorun_desc"))
|
||||||
@@ -2041,10 +2119,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (val) => {
|
.onChange(async (val) => {
|
||||||
if (val === "enable") {
|
if (val === "enable") {
|
||||||
this.plugin.settings.enableMobileStatusBar = true;
|
this.plugin.settings.enableMobileStatusBar = true;
|
||||||
|
this.plugin.appContainerObserver =
|
||||||
changeMobileStatusBar("enable");
|
changeMobileStatusBar("enable");
|
||||||
} else {
|
} else {
|
||||||
this.plugin.settings.enableMobileStatusBar = false;
|
this.plugin.settings.enableMobileStatusBar = false;
|
||||||
changeMobileStatusBar("disable");
|
changeMobileStatusBar(
|
||||||
|
"disable",
|
||||||
|
this.plugin.appContainerObserver
|
||||||
|
);
|
||||||
|
this.plugin.appContainerObserver?.disconnect();
|
||||||
|
this.plugin.appContainerObserver = undefined;
|
||||||
}
|
}
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
});
|
});
|
||||||
@@ -2065,15 +2149,87 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.setName(t("settings_export"))
|
.setName(t("settings_export"))
|
||||||
.setDesc(t("settings_export_desc"))
|
.setDesc(t("settings_export_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText(t("settings_export_desc_button"));
|
button.setButtonText(t("settings_export_all_but_oauth2_button"));
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
new ExportSettingsQrCodeModal(this.app, this.plugin).open();
|
new ExportSettingsQrCodeModal(
|
||||||
|
this.app,
|
||||||
|
this.plugin,
|
||||||
|
"all_but_oauth2"
|
||||||
|
).open();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_export_dropbox_button"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
new ExportSettingsQrCodeModal(
|
||||||
|
this.app,
|
||||||
|
this.plugin,
|
||||||
|
"dropbox"
|
||||||
|
).open();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_export_onedrive_button"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
new ExportSettingsQrCodeModal(
|
||||||
|
this.app,
|
||||||
|
this.plugin,
|
||||||
|
"onedrive"
|
||||||
|
).open();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let importSettingVal = "";
|
||||||
new Setting(importExportDiv)
|
new Setting(importExportDiv)
|
||||||
.setName(t("settings_import"))
|
.setName(t("settings_import"))
|
||||||
.setDesc(t("settings_import_desc"));
|
.setDesc(t("settings_import_desc"))
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder("obsidian://remotely-save?func=settings&...")
|
||||||
|
.setValue("")
|
||||||
|
.onChange((val) => {
|
||||||
|
importSettingVal = val;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("confirm"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
if (importSettingVal !== "") {
|
||||||
|
// console.debug(importSettingVal);
|
||||||
|
try {
|
||||||
|
const inputParams = parseUriByHand(importSettingVal);
|
||||||
|
const parsed = importQrCodeUri(
|
||||||
|
inputParams,
|
||||||
|
this.app.vault.getName()
|
||||||
|
);
|
||||||
|
if (parsed.status === "error") {
|
||||||
|
new Notice(parsed.message);
|
||||||
|
} else {
|
||||||
|
const copied = cloneDeep(parsed.result);
|
||||||
|
// new Notice(JSON.stringify(copied))
|
||||||
|
this.plugin.settings = Object.assign(
|
||||||
|
{},
|
||||||
|
this.plugin.settings,
|
||||||
|
copied
|
||||||
|
);
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
new Notice(
|
||||||
|
t("protocol_saveqr", {
|
||||||
|
manifestName: this.plugin.manifest.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
new Notice(`${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
importSettingVal = "";
|
||||||
|
} else {
|
||||||
|
new Notice(t("settings_import_error_notice"));
|
||||||
|
importSettingVal = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// below for debug
|
// below for debug
|
||||||
@@ -2141,12 +2297,37 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.setName(t("settings_syncplans"))
|
.setName(t("settings_syncplans"))
|
||||||
.setDesc(t("settings_syncplans_desc"))
|
.setDesc(t("settings_syncplans_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText(t("settings_syncplans_button_json"));
|
button.setButtonText(t("settings_syncplans_button_1"));
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
await exportVaultSyncPlansToFiles(
|
await exportVaultSyncPlansToFiles(
|
||||||
this.plugin.db,
|
this.plugin.db,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.plugin.vaultRandomID
|
this.plugin.vaultRandomID,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_syncplans_button_5"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.vaultRandomID,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_syncplans_button_all"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.vaultRandomID,
|
||||||
|
-1
|
||||||
);
|
);
|
||||||
new Notice(t("settings_syncplans_notice"));
|
new Notice(t("settings_syncplans_notice"));
|
||||||
});
|
});
|
||||||
@@ -2177,6 +2358,21 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new Setting(debugDiv)
|
||||||
|
.setName(t("settings_profiler_results"))
|
||||||
|
.setDesc(t("settings_profiler_results_desc"))
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_profiler_results_button_all"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
await exportVaultProfilerResultsToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.vaultRandomID
|
||||||
|
);
|
||||||
|
new Notice(t("settings_profiler_results_notice"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(debugDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_outputbasepathvaultid"))
|
.setName(t("settings_outputbasepathvaultid"))
|
||||||
.setDesc(t("settings_outputbasepathvaultid_desc"))
|
.setDesc(t("settings_outputbasepathvaultid_desc"))
|
||||||
|
|||||||
+309
-151
@@ -1,6 +1,7 @@
|
|||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import XRegExp from "xregexp";
|
import XRegExp from "xregexp";
|
||||||
import type {
|
import type {
|
||||||
|
CipherMethodType,
|
||||||
ConflictActionType,
|
ConflictActionType,
|
||||||
EmptyFolderCleanType,
|
EmptyFolderCleanType,
|
||||||
Entity,
|
Entity,
|
||||||
@@ -17,19 +18,12 @@ import {
|
|||||||
isVaildText,
|
isVaildText,
|
||||||
atWhichLevel,
|
atWhichLevel,
|
||||||
mkdirpInVault,
|
mkdirpInVault,
|
||||||
|
getFolderLevels,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
|
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
|
||||||
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2,
|
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2,
|
||||||
} from "./metadataOnRemote";
|
} from "./metadataOnRemote";
|
||||||
import {
|
|
||||||
MAGIC_ENCRYPTED_PREFIX_BASE32,
|
|
||||||
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
|
|
||||||
decryptBase32ToString,
|
|
||||||
decryptBase64urlToString,
|
|
||||||
encryptStringToBase64url,
|
|
||||||
getSizeFromOrigToEnc,
|
|
||||||
} from "./encrypt";
|
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
import { Vault } from "obsidian";
|
import { Vault } from "obsidian";
|
||||||
|
|
||||||
@@ -39,6 +33,8 @@ import {
|
|||||||
clearPrevSyncRecordByVaultAndProfile,
|
clearPrevSyncRecordByVaultAndProfile,
|
||||||
upsertPrevSyncRecordByVaultAndProfile,
|
upsertPrevSyncRecordByVaultAndProfile,
|
||||||
} from "./localdb";
|
} from "./localdb";
|
||||||
|
import { Cipher } from "./encryptUnified";
|
||||||
|
import { Profiler } from "./profiler";
|
||||||
|
|
||||||
export type SyncStatusType =
|
export type SyncStatusType =
|
||||||
| "idle"
|
| "idle"
|
||||||
@@ -55,19 +51,18 @@ export type SyncStatusType =
|
|||||||
export interface PasswordCheckType {
|
export interface PasswordCheckType {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
reason:
|
reason:
|
||||||
| "ok"
|
|
||||||
| "empty_remote"
|
| "empty_remote"
|
||||||
|
| "unknown_encryption_method"
|
||||||
| "remote_encrypted_local_no_password"
|
| "remote_encrypted_local_no_password"
|
||||||
| "password_matched"
|
| "password_matched"
|
||||||
| "password_not_matched"
|
| "password_or_method_not_matched_or_remote_not_encrypted"
|
||||||
| "invalid_text_after_decryption"
|
| "likely_no_password_both_sides"
|
||||||
| "remote_not_encrypted_local_has_password"
|
| "encryption_method_not_matched";
|
||||||
| "no_password_both_sides";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isPasswordOk = async (
|
export const isPasswordOk = async (
|
||||||
remote: Entity[],
|
remote: Entity[],
|
||||||
password: string = ""
|
cipher: Cipher
|
||||||
): Promise<PasswordCheckType> => {
|
): Promise<PasswordCheckType> => {
|
||||||
if (remote === undefined || remote.length === 0) {
|
if (remote === undefined || remote.length === 0) {
|
||||||
// remote empty
|
// remote empty
|
||||||
@@ -77,82 +72,52 @@ export const isPasswordOk = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const santyCheckKey = remote[0].keyRaw;
|
const santyCheckKey = remote[0].keyRaw;
|
||||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
|
||||||
// this is encrypted using old base32!
|
if (cipher.isPasswordEmpty()) {
|
||||||
// try to decrypt it using the provided password.
|
// TODO: no way to distinguish remote rclone encrypted
|
||||||
if (password === "") {
|
// if local has no password??
|
||||||
|
if (Cipher.isLikelyEncryptedName(santyCheckKey)) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: "remote_encrypted_local_no_password",
|
reason: "remote_encrypted_local_no_password",
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
reason: "likely_no_password_both_sides",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cipher.method === "unknown") {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "unknown_encryption_method",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Cipher.isLikelyEncryptedNameNotMatchMethod(santyCheckKey, cipher.method)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "encryption_method_not_matched",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await decryptBase32ToString(santyCheckKey, password);
|
const k = await cipher.decryptName(santyCheckKey);
|
||||||
|
if (k === undefined) {
|
||||||
// additional test
|
throw Error(`decryption failed`);
|
||||||
// because iOS Safari bypasses decryption with wrong password!
|
}
|
||||||
if (isVaildText(res)) {
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
reason: "password_matched",
|
reason: "password_matched",
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "invalid_text_after_decryption",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
reason: "password_not_matched",
|
reason: "password_or_method_not_matched_or_remote_not_encrypted",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
|
|
||||||
// this is encrypted using new base64url!
|
|
||||||
// try to decrypt it using the provided password.
|
|
||||||
if (password === "") {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "remote_encrypted_local_no_password",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await decryptBase64urlToString(santyCheckKey, password);
|
|
||||||
|
|
||||||
// additional test
|
|
||||||
// because iOS Safari bypasses decryption with wrong password!
|
|
||||||
if (isVaildText(res)) {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
reason: "password_matched",
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "invalid_text_after_decryption",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "password_not_matched",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// it is not encrypted!
|
|
||||||
if (password !== "") {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
reason: "remote_not_encrypted_local_has_password",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
reason: "no_password_both_sides",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSkipItemByName = (
|
const isSkipItemByName = (
|
||||||
@@ -196,7 +161,7 @@ const copyEntityAndFixTimeFormat = (
|
|||||||
if (result.mtimeCli === 0) {
|
if (result.mtimeCli === 0) {
|
||||||
result.mtimeCli = undefined;
|
result.mtimeCli = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (serviceType === "s3") {
|
if (serviceType === "s3" || serviceType === "dropbox") {
|
||||||
// round to second instead of millisecond
|
// round to second instead of millisecond
|
||||||
result.mtimeCli = Math.floor(result.mtimeCli / 1000.0) * 1000;
|
result.mtimeCli = Math.floor(result.mtimeCli / 1000.0) * 1000;
|
||||||
}
|
}
|
||||||
@@ -207,7 +172,7 @@ const copyEntityAndFixTimeFormat = (
|
|||||||
if (result.mtimeSvr === 0) {
|
if (result.mtimeSvr === 0) {
|
||||||
result.mtimeSvr = undefined;
|
result.mtimeSvr = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (serviceType === "s3") {
|
if (serviceType === "s3" || serviceType === "dropbox") {
|
||||||
// round to second instead of millisecond
|
// round to second instead of millisecond
|
||||||
result.mtimeSvr = Math.floor(result.mtimeSvr / 1000.0) * 1000;
|
result.mtimeSvr = Math.floor(result.mtimeSvr / 1000.0) * 1000;
|
||||||
}
|
}
|
||||||
@@ -218,7 +183,7 @@ const copyEntityAndFixTimeFormat = (
|
|||||||
if (result.prevSyncTime === 0) {
|
if (result.prevSyncTime === 0) {
|
||||||
result.prevSyncTime = undefined;
|
result.prevSyncTime = undefined;
|
||||||
} else {
|
} else {
|
||||||
if (serviceType === "s3") {
|
if (serviceType === "s3" || serviceType === "dropbox") {
|
||||||
// round to second instead of millisecond
|
// round to second instead of millisecond
|
||||||
result.prevSyncTime = Math.floor(result.prevSyncTime / 1000.0) * 1000;
|
result.prevSyncTime = Math.floor(result.prevSyncTime / 1000.0) * 1000;
|
||||||
}
|
}
|
||||||
@@ -231,12 +196,9 @@ const copyEntityAndFixTimeFormat = (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inplace, no copy again.
|
* Inplace, no copy again.
|
||||||
* @param remote
|
|
||||||
* @param password
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
const decryptRemoteEntityInplace = async (remote: Entity, password: string) => {
|
const decryptRemoteEntityInplace = async (remote: Entity, cipher: Cipher) => {
|
||||||
if (password == undefined || password === "") {
|
if (cipher?.isPasswordEmpty()) {
|
||||||
remote.key = remote.keyRaw;
|
remote.key = remote.keyRaw;
|
||||||
remote.keyEnc = remote.keyRaw;
|
remote.keyEnc = remote.keyRaw;
|
||||||
remote.size = remote.sizeRaw;
|
remote.size = remote.sizeRaw;
|
||||||
@@ -244,19 +206,9 @@ const decryptRemoteEntityInplace = async (remote: Entity, password: string) => {
|
|||||||
return remote;
|
return remote;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remote.keyRaw.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
|
||||||
remote.keyEnc = remote.keyRaw;
|
remote.keyEnc = remote.keyRaw;
|
||||||
remote.key = await decryptBase32ToString(remote.keyEnc, password);
|
remote.key = await cipher.decryptName(remote.keyEnc);
|
||||||
remote.sizeEnc = remote.sizeRaw;
|
remote.sizeEnc = remote.sizeRaw;
|
||||||
} else if (remote.keyRaw.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
|
|
||||||
remote.keyEnc = remote.keyRaw;
|
|
||||||
remote.key = await decryptBase64urlToString(remote.keyEnc, password);
|
|
||||||
remote.sizeEnc = remote.sizeRaw;
|
|
||||||
} else {
|
|
||||||
throw Error(
|
|
||||||
`unexpected key to decrypt: ${JSON.stringify(remote, null, 2)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// remote.size = getSizeFromEncToOrig(remote.sizeEnc, password);
|
// remote.size = getSizeFromEncToOrig(remote.sizeEnc, password);
|
||||||
@@ -309,13 +261,10 @@ const ensureMTimeOfRemoteEntityValid = (remote: Entity) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inplace, no copy again.
|
* Inplace, no copy again.
|
||||||
* @param local
|
|
||||||
* @param password
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
const encryptLocalEntityInplace = async (
|
const encryptLocalEntityInplace = async (
|
||||||
local: Entity,
|
local: Entity,
|
||||||
password: string,
|
cipher: Cipher,
|
||||||
remoteKeyEnc: string | undefined
|
remoteKeyEnc: string | undefined
|
||||||
) => {
|
) => {
|
||||||
// console.debug(
|
// console.debug(
|
||||||
@@ -333,7 +282,7 @@ const encryptLocalEntityInplace = async (
|
|||||||
throw Error(`local ${local.keyRaw} is abnormal without key`);
|
throw Error(`local ${local.keyRaw} is abnormal without key`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password === undefined || password === "") {
|
if (cipher.isPasswordEmpty()) {
|
||||||
local.sizeEnc = local.sizeRaw; // if no enc, the remote file has the same size
|
local.sizeEnc = local.sizeRaw; // if no enc, the remote file has the same size
|
||||||
local.keyEnc = local.keyRaw;
|
local.keyEnc = local.keyRaw;
|
||||||
return local;
|
return local;
|
||||||
@@ -344,7 +293,7 @@ const encryptLocalEntityInplace = async (
|
|||||||
// it's not filled yet, we fill it
|
// it's not filled yet, we fill it
|
||||||
// local.size is possibly undefined if it's "prevSync" Entity
|
// local.size is possibly undefined if it's "prevSync" Entity
|
||||||
// but local.key should always have value
|
// but local.key should always have value
|
||||||
local.sizeEnc = getSizeFromOrigToEnc(local.size);
|
local.sizeEnc = cipher.getSizeFromOrigToEnc(local.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (local.keyEnc === undefined || local.keyEnc === "") {
|
if (local.keyEnc === undefined || local.keyEnc === "") {
|
||||||
@@ -357,10 +306,7 @@ const encryptLocalEntityInplace = async (
|
|||||||
local.keyEnc = remoteKeyEnc;
|
local.keyEnc = remoteKeyEnc;
|
||||||
} else {
|
} else {
|
||||||
// we assign a new encrypted key because of no remote
|
// we assign a new encrypted key because of no remote
|
||||||
// the old version uses base32
|
local.keyEnc = await cipher.encryptName(local.key);
|
||||||
// local.keyEnc = await encryptStringToBase32(local.key, password);
|
|
||||||
// the new version users base64url
|
|
||||||
local.keyEnc = await encryptStringToBase64url(local.key, password);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return local;
|
return local;
|
||||||
@@ -377,17 +323,25 @@ export const ensembleMixedEnties = async (
|
|||||||
configDir: string,
|
configDir: string,
|
||||||
syncUnderscoreItems: boolean,
|
syncUnderscoreItems: boolean,
|
||||||
ignorePaths: string[],
|
ignorePaths: string[],
|
||||||
password: string,
|
cipher: Cipher,
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE
|
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||||
|
|
||||||
|
profiler: Profiler
|
||||||
): Promise<SyncPlanType> => {
|
): Promise<SyncPlanType> => {
|
||||||
|
profiler.addIndent();
|
||||||
|
profiler.insert("ensembleMixedEnties: enter");
|
||||||
|
|
||||||
const finalMappings: SyncPlanType = {};
|
const finalMappings: SyncPlanType = {};
|
||||||
|
|
||||||
|
const synthFolders: Record<string, Entity> = {};
|
||||||
|
|
||||||
// remote has to be first
|
// remote has to be first
|
||||||
|
// we also have to synthesize folders here
|
||||||
for (const remote of remoteEntityList) {
|
for (const remote of remoteEntityList) {
|
||||||
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
||||||
await decryptRemoteEntityInplace(
|
await decryptRemoteEntityInplace(
|
||||||
copyEntityAndFixTimeFormat(remote, serviceType),
|
copyEntityAndFixTimeFormat(remote, serviceType),
|
||||||
password
|
cipher
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -408,7 +362,47 @@ export const ensembleMixedEnties = async (
|
|||||||
key: key,
|
key: key,
|
||||||
remote: remoteCopied,
|
remote: remoteCopied,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const f of getFolderLevels(key, true)) {
|
||||||
|
if (finalMappings.hasOwnProperty(f)) {
|
||||||
|
delete synthFolders[f];
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!synthFolders.hasOwnProperty(f) ||
|
||||||
|
remoteCopied.mtimeSvr! >= synthFolders[f].mtimeSvr!
|
||||||
|
) {
|
||||||
|
synthFolders[f] = {
|
||||||
|
key: f,
|
||||||
|
keyRaw: `<synth: ${f}>`,
|
||||||
|
keyEnc: `<enc synth: ${f}>`,
|
||||||
|
size: 0,
|
||||||
|
sizeRaw: 0,
|
||||||
|
sizeEnc: 0,
|
||||||
|
mtimeSvr: remoteCopied.mtimeSvr,
|
||||||
|
mtimeSvrFmt: remoteCopied.mtimeSvrFmt,
|
||||||
|
mtimeCli: remoteCopied.mtimeCli,
|
||||||
|
mtimeCliFmt: remoteCopied.mtimeCliFmt,
|
||||||
|
synthesizedFolder: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler.insert("ensembleMixedEnties: finish remote");
|
||||||
|
|
||||||
|
console.debug(`synthFolders:`);
|
||||||
|
console.debug(synthFolders);
|
||||||
|
|
||||||
|
// special: add synth folders
|
||||||
|
for (const key of Object.keys(synthFolders)) {
|
||||||
|
finalMappings[key] = {
|
||||||
|
key: key,
|
||||||
|
remote: synthFolders[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler.insert("ensembleMixedEnties: finish synth");
|
||||||
|
|
||||||
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
||||||
// Special checking:
|
// Special checking:
|
||||||
@@ -436,14 +430,14 @@ export const ensembleMixedEnties = async (
|
|||||||
if (finalMappings.hasOwnProperty(key)) {
|
if (finalMappings.hasOwnProperty(key)) {
|
||||||
const prevSyncCopied = await encryptLocalEntityInplace(
|
const prevSyncCopied = await encryptLocalEntityInplace(
|
||||||
copyEntityAndFixTimeFormat(prevSync, serviceType),
|
copyEntityAndFixTimeFormat(prevSync, serviceType),
|
||||||
password,
|
cipher,
|
||||||
finalMappings[key].remote?.keyEnc
|
finalMappings[key].remote?.keyEnc
|
||||||
);
|
);
|
||||||
finalMappings[key].prevSync = prevSyncCopied;
|
finalMappings[key].prevSync = prevSyncCopied;
|
||||||
} else {
|
} else {
|
||||||
const prevSyncCopied = await encryptLocalEntityInplace(
|
const prevSyncCopied = await encryptLocalEntityInplace(
|
||||||
copyEntityAndFixTimeFormat(prevSync, serviceType),
|
copyEntityAndFixTimeFormat(prevSync, serviceType),
|
||||||
password,
|
cipher,
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
finalMappings[key] = {
|
finalMappings[key] = {
|
||||||
@@ -454,6 +448,8 @@ export const ensembleMixedEnties = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("ensembleMixedEnties: finish prevSync");
|
||||||
|
|
||||||
// local has to be last
|
// local has to be last
|
||||||
// because we want to get keyEnc based on the remote
|
// because we want to get keyEnc based on the remote
|
||||||
// (we don't consume prevSync here because it gains no benefit)
|
// (we don't consume prevSync here because it gains no benefit)
|
||||||
@@ -474,14 +470,14 @@ export const ensembleMixedEnties = async (
|
|||||||
if (finalMappings.hasOwnProperty(key)) {
|
if (finalMappings.hasOwnProperty(key)) {
|
||||||
const localCopied = await encryptLocalEntityInplace(
|
const localCopied = await encryptLocalEntityInplace(
|
||||||
copyEntityAndFixTimeFormat(local, serviceType),
|
copyEntityAndFixTimeFormat(local, serviceType),
|
||||||
password,
|
cipher,
|
||||||
finalMappings[key].remote?.keyEnc
|
finalMappings[key].remote?.keyEnc
|
||||||
);
|
);
|
||||||
finalMappings[key].local = localCopied;
|
finalMappings[key].local = localCopied;
|
||||||
} else {
|
} else {
|
||||||
const localCopied = await encryptLocalEntityInplace(
|
const localCopied = await encryptLocalEntityInplace(
|
||||||
copyEntityAndFixTimeFormat(local, serviceType),
|
copyEntityAndFixTimeFormat(local, serviceType),
|
||||||
password,
|
cipher,
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
finalMappings[key] = {
|
finalMappings[key] = {
|
||||||
@@ -491,8 +487,13 @@ export const ensembleMixedEnties = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("ensembleMixedEnties: finish local");
|
||||||
|
|
||||||
console.debug("in the end of ensembleMixedEnties, finalMappings is:");
|
console.debug("in the end of ensembleMixedEnties, finalMappings is:");
|
||||||
console.debug(finalMappings);
|
console.debug(finalMappings);
|
||||||
|
|
||||||
|
profiler.insert("ensembleMixedEnties: exit");
|
||||||
|
profiler.removeIndent();
|
||||||
return finalMappings;
|
return finalMappings;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -506,12 +507,16 @@ export const getSyncPlanInplace = async (
|
|||||||
howToCleanEmptyFolder: EmptyFolderCleanType,
|
howToCleanEmptyFolder: EmptyFolderCleanType,
|
||||||
skipSizeLargerThan: number,
|
skipSizeLargerThan: number,
|
||||||
conflictAction: ConflictActionType,
|
conflictAction: ConflictActionType,
|
||||||
syncDirection: SyncDirectionType
|
syncDirection: SyncDirectionType,
|
||||||
|
profiler: Profiler
|
||||||
) => {
|
) => {
|
||||||
|
profiler.addIndent();
|
||||||
|
profiler.insert("getSyncPlanInplace: enter");
|
||||||
// from long(deep) to short(shadow)
|
// from long(deep) to short(shadow)
|
||||||
const sortedKeys = Object.keys(mixedEntityMappings).sort(
|
const sortedKeys = Object.keys(mixedEntityMappings).sort(
|
||||||
(k1, k2) => k2.length - k1.length
|
(k1, k2) => k2.length - k1.length
|
||||||
);
|
);
|
||||||
|
profiler.insert("getSyncPlanInplace: finish sorting");
|
||||||
|
|
||||||
const keptFolder = new Set<string>();
|
const keptFolder = new Set<string>();
|
||||||
|
|
||||||
@@ -562,9 +567,41 @@ export const getSyncPlanInplace = async (
|
|||||||
mixedEntry.decisionBranch = 105;
|
mixedEntry.decisionBranch = 105;
|
||||||
mixedEntry.decision = "folder_to_skip";
|
mixedEntry.decision = "folder_to_skip";
|
||||||
} else if (howToCleanEmptyFolder === "clean_both") {
|
} else if (howToCleanEmptyFolder === "clean_both") {
|
||||||
|
if (local !== undefined && remote !== undefined) {
|
||||||
|
if (syncDirection === "bidirectional") {
|
||||||
mixedEntry.decisionBranch = 106;
|
mixedEntry.decisionBranch = 106;
|
||||||
mixedEntry.decision = "folder_to_be_deleted";
|
mixedEntry.decision = "folder_to_be_deleted_on_both";
|
||||||
// TODO: what to do in different sync direction?
|
} else {
|
||||||
|
// right now it does nothing because of "incremental"
|
||||||
|
// TODO: should we delete??
|
||||||
|
mixedEntry.decisionBranch = 109;
|
||||||
|
mixedEntry.decision = "folder_to_skip";
|
||||||
|
}
|
||||||
|
} else if (local !== undefined && remote === undefined) {
|
||||||
|
if (syncDirection === "bidirectional") {
|
||||||
|
mixedEntry.decisionBranch = 110;
|
||||||
|
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
||||||
|
} else {
|
||||||
|
// right now it does nothing because of "incremental"
|
||||||
|
// TODO: should we delete??
|
||||||
|
mixedEntry.decisionBranch = 111;
|
||||||
|
mixedEntry.decision = "folder_to_skip";
|
||||||
|
}
|
||||||
|
} else if (local === undefined && remote !== undefined) {
|
||||||
|
if (syncDirection === "bidirectional") {
|
||||||
|
mixedEntry.decisionBranch = 112;
|
||||||
|
mixedEntry.decision = "folder_to_be_deleted_on_remote";
|
||||||
|
} else {
|
||||||
|
// right now it does nothing because of "incremental"
|
||||||
|
// TODO: should we delete??
|
||||||
|
mixedEntry.decisionBranch = 113;
|
||||||
|
mixedEntry.decision = "folder_to_skip";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no folder to delete, do nothing
|
||||||
|
mixedEntry.decisionBranch = 114;
|
||||||
|
mixedEntry.decision = "folder_to_skip";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
throw Error(
|
||||||
`do not know how to deal with empty folder ${mixedEntry.key}`
|
`do not know how to deal with empty folder ${mixedEntry.key}`
|
||||||
@@ -762,11 +799,9 @@ export const getSyncPlanInplace = async (
|
|||||||
keptFolder.add(getParentFolder(key));
|
keptFolder.add(getParentFolder(key));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
mixedEntry.decisionBranch = 36;
|
||||||
`remote is created (branch 3) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
|
mixedEntry.decision = "remote_is_created_too_large_then_do_nothing";
|
||||||
mixedEntry
|
keptFolder.add(getParentFolder(key));
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(prevSync.mtimeSvr === remote.mtimeCli ||
|
(prevSync.mtimeSvr === remote.mtimeCli ||
|
||||||
@@ -825,11 +860,9 @@ export const getSyncPlanInplace = async (
|
|||||||
keptFolder.add(getParentFolder(key));
|
keptFolder.add(getParentFolder(key));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
mixedEntry.decisionBranch = 37;
|
||||||
`local is created (branch 6) but size larger than ${skipSizeLargerThan}, don't know what to do: ${JSON.stringify(
|
mixedEntry.decision = "local_is_created_too_large_then_do_nothing";
|
||||||
mixedEntry
|
keptFolder.add(getParentFolder(key));
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
(prevSync.mtimeSvr === local.mtimeCli ||
|
(prevSync.mtimeSvr === local.mtimeCli ||
|
||||||
@@ -885,19 +918,38 @@ export const getSyncPlanInplace = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert("getSyncPlanInplace: finish looping");
|
||||||
|
|
||||||
keptFolder.delete("/");
|
keptFolder.delete("/");
|
||||||
keptFolder.delete("");
|
keptFolder.delete("");
|
||||||
if (keptFolder.size > 0) {
|
if (keptFolder.size > 0) {
|
||||||
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
|
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finally we want to make our life easier
|
||||||
|
const currTime = Date.now();
|
||||||
|
const currTimeFmt = unixTimeToStr(currTime);
|
||||||
|
// because the path should not as / in the beginning,
|
||||||
|
// we should be safe to add these keys:
|
||||||
|
mixedEntityMappings["/$@meta"] = {
|
||||||
|
key: "/$@meta", // don't mess up with the types
|
||||||
|
sideNotes: {
|
||||||
|
generateTime: currTime,
|
||||||
|
generateTimeFmt: currTimeFmt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
profiler.insert("getSyncPlanInplace: exit");
|
||||||
|
profiler.removeIndent();
|
||||||
|
|
||||||
return mixedEntityMappings;
|
return mixedEntityMappings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitThreeStepsOnEntityMappings = (
|
const splitFourStepsOnEntityMappings = (
|
||||||
mixedEntityMappings: Record<string, MixedEntity>
|
mixedEntityMappings: Record<string, MixedEntity>
|
||||||
) => {
|
) => {
|
||||||
type StepArrayType = MixedEntity[] | undefined | null;
|
type StepArrayType = MixedEntity[] | undefined | null;
|
||||||
|
const onlyMarkSyncedOps: StepArrayType[] = [];
|
||||||
const folderCreationOps: StepArrayType[] = [];
|
const folderCreationOps: StepArrayType[] = [];
|
||||||
const deletionOps: StepArrayType[] = [];
|
const deletionOps: StepArrayType[] = [];
|
||||||
const uploadDownloads: StepArrayType[] = [];
|
const uploadDownloads: StepArrayType[] = [];
|
||||||
@@ -913,6 +965,11 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
|
|
||||||
for (let i = 0; i < sortedKeys.length; ++i) {
|
for (let i = 0; i < sortedKeys.length; ++i) {
|
||||||
const key = sortedKeys[i];
|
const key = sortedKeys[i];
|
||||||
|
|
||||||
|
if (key === "/$@meta") {
|
||||||
|
continue; // special
|
||||||
|
}
|
||||||
|
|
||||||
const val = mixedEntityMappings[key];
|
const val = mixedEntityMappings[key];
|
||||||
|
|
||||||
if (!key.endsWith("/")) {
|
if (!key.endsWith("/")) {
|
||||||
@@ -920,12 +977,27 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
val.decision === "equal" ||
|
val.decision === "local_is_created_too_large_then_do_nothing" ||
|
||||||
val.decision === "conflict_created_then_do_nothing" ||
|
val.decision === "remote_is_created_too_large_then_do_nothing" ||
|
||||||
val.decision === "folder_existed_both_then_do_nothing" ||
|
|
||||||
val.decision === "folder_to_skip"
|
val.decision === "folder_to_skip"
|
||||||
) {
|
) {
|
||||||
// pass
|
// pass
|
||||||
|
} else if (
|
||||||
|
val.decision === "equal" ||
|
||||||
|
val.decision === "conflict_created_then_do_nothing" ||
|
||||||
|
val.decision === "folder_existed_both_then_do_nothing"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
onlyMarkSyncedOps.length === 0 ||
|
||||||
|
onlyMarkSyncedOps[0] === undefined ||
|
||||||
|
onlyMarkSyncedOps[0] === null
|
||||||
|
) {
|
||||||
|
onlyMarkSyncedOps[0] = [val];
|
||||||
|
} else {
|
||||||
|
onlyMarkSyncedOps[0].push(val); // only one level is needed here
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't need to update realTotalCount here
|
||||||
} else if (
|
} else if (
|
||||||
val.decision === "folder_existed_local_then_also_create_remote" ||
|
val.decision === "folder_existed_local_then_also_create_remote" ||
|
||||||
val.decision === "folder_existed_remote_then_also_create_local" ||
|
val.decision === "folder_existed_remote_then_also_create_local" ||
|
||||||
@@ -945,7 +1017,9 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
val.decision === "only_history" ||
|
val.decision === "only_history" ||
|
||||||
val.decision === "local_is_deleted_thus_also_delete_remote" ||
|
val.decision === "local_is_deleted_thus_also_delete_remote" ||
|
||||||
val.decision === "remote_is_deleted_thus_also_delete_local" ||
|
val.decision === "remote_is_deleted_thus_also_delete_local" ||
|
||||||
val.decision === "folder_to_be_deleted"
|
val.decision === "folder_to_be_deleted_on_both" ||
|
||||||
|
val.decision === "folder_to_be_deleted_on_local" ||
|
||||||
|
val.decision === "folder_to_be_deleted_on_remote"
|
||||||
) {
|
) {
|
||||||
const level = atWhichLevel(key);
|
const level = atWhichLevel(key);
|
||||||
const k = deletionOps[level - 1];
|
const k = deletionOps[level - 1];
|
||||||
@@ -956,7 +1030,11 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
}
|
}
|
||||||
realTotalCount += 1;
|
realTotalCount += 1;
|
||||||
|
|
||||||
if (val.decision.startsWith("deleted")) {
|
if (
|
||||||
|
val.decision.includes("deleted") &&
|
||||||
|
!val.decision.includes("folder")
|
||||||
|
) {
|
||||||
|
// only count files here, skip folder
|
||||||
realModifyDeleteCount += 1;
|
realModifyDeleteCount += 1;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
@@ -983,8 +1061,8 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
realTotalCount += 1;
|
realTotalCount += 1;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
val.decision.startsWith("modified") ||
|
val.decision.includes("modified") ||
|
||||||
val.decision.startsWith("conflict")
|
val.decision.includes("conflict")
|
||||||
) {
|
) {
|
||||||
realModifyDeleteCount += 1;
|
realModifyDeleteCount += 1;
|
||||||
}
|
}
|
||||||
@@ -999,6 +1077,7 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
deletionOps.reverse(); // inplace reverse
|
deletionOps.reverse(); // inplace reverse
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
onlyMarkSyncedOps: onlyMarkSyncedOps,
|
||||||
folderCreationOps: folderCreationOps,
|
folderCreationOps: folderCreationOps,
|
||||||
deletionOps: deletionOps,
|
deletionOps: deletionOps,
|
||||||
uploadDownloads: uploadDownloads,
|
uploadDownloads: uploadDownloads,
|
||||||
@@ -1017,7 +1096,7 @@ const dispatchOperationToActualV3 = async (
|
|||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
localDeleteFunc: any,
|
localDeleteFunc: any,
|
||||||
password: string
|
cipher: Cipher
|
||||||
) => {
|
) => {
|
||||||
// console.debug(
|
// console.debug(
|
||||||
// `inside dispatchOperationToActualV3, key=${key}, r=${JSON.stringify(
|
// `inside dispatchOperationToActualV3, key=${key}, r=${JSON.stringify(
|
||||||
@@ -1028,13 +1107,40 @@ const dispatchOperationToActualV3 = async (
|
|||||||
// );
|
// );
|
||||||
if (r.decision === "only_history") {
|
if (r.decision === "only_history") {
|
||||||
clearPrevSyncRecordByVaultAndProfile(db, vaultRandomID, profileID, key);
|
clearPrevSyncRecordByVaultAndProfile(db, vaultRandomID, profileID, key);
|
||||||
|
} else if (
|
||||||
|
r.decision === "local_is_created_too_large_then_do_nothing" ||
|
||||||
|
r.decision === "remote_is_created_too_large_then_do_nothing" ||
|
||||||
|
r.decision === "folder_to_skip"
|
||||||
|
) {
|
||||||
|
// !! no actual sync being kept happens,
|
||||||
|
// so no sync record here
|
||||||
|
// pass
|
||||||
} else if (
|
} else if (
|
||||||
r.decision === "equal" ||
|
r.decision === "equal" ||
|
||||||
r.decision === "conflict_created_then_do_nothing" ||
|
r.decision === "conflict_created_then_do_nothing" ||
|
||||||
r.decision === "folder_to_skip" ||
|
|
||||||
r.decision === "folder_existed_both_then_do_nothing"
|
r.decision === "folder_existed_both_then_do_nothing"
|
||||||
) {
|
) {
|
||||||
// pass
|
// !! we need to upsert the record,
|
||||||
|
// so that next time we can determine the change delta
|
||||||
|
// if we have prevSync, we store it because it should keep all necessary info
|
||||||
|
let entity = r.prevSync;
|
||||||
|
// if we don't have prevSync, we use remote entity AND local mtime
|
||||||
|
// as if it is "uploaded"
|
||||||
|
if (entity === undefined && r.remote !== undefined) {
|
||||||
|
entity = await decryptRemoteEntityInplace(r.remote, cipher);
|
||||||
|
entity = await fullfillMTimeOfRemoteEntityInplace(
|
||||||
|
entity,
|
||||||
|
r.local?.mtimeCli
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity !== undefined) {
|
||||||
|
await upsertPrevSyncRecordByVaultAndProfile(
|
||||||
|
db,
|
||||||
|
vaultRandomID,
|
||||||
|
profileID,
|
||||||
|
entity
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
r.decision === "local_is_modified_then_push" ||
|
r.decision === "local_is_modified_then_push" ||
|
||||||
r.decision === "local_is_created_then_push" ||
|
r.decision === "local_is_created_then_push" ||
|
||||||
@@ -1045,7 +1151,7 @@ const dispatchOperationToActualV3 = async (
|
|||||||
if (
|
if (
|
||||||
client.serviceType === "onedrive" &&
|
client.serviceType === "onedrive" &&
|
||||||
r.local!.size === 0 &&
|
r.local!.size === 0 &&
|
||||||
password === ""
|
cipher.isPasswordEmpty()
|
||||||
) {
|
) {
|
||||||
// special treatment for empty files for OneDrive
|
// special treatment for empty files for OneDrive
|
||||||
// TODO: it's ugly, any other way?
|
// TODO: it's ugly, any other way?
|
||||||
@@ -1057,10 +1163,10 @@ const dispatchOperationToActualV3 = async (
|
|||||||
r.key,
|
r.key,
|
||||||
vault,
|
vault,
|
||||||
false,
|
false,
|
||||||
password,
|
cipher,
|
||||||
r.local!.keyEnc
|
r.local!.keyEnc
|
||||||
);
|
);
|
||||||
await decryptRemoteEntityInplace(entity, password);
|
await decryptRemoteEntityInplace(entity, cipher);
|
||||||
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
|
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
|
||||||
await upsertPrevSyncRecordByVaultAndProfile(
|
await upsertPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
@@ -1081,7 +1187,7 @@ const dispatchOperationToActualV3 = async (
|
|||||||
r.key,
|
r.key,
|
||||||
vault,
|
vault,
|
||||||
r.remote!.mtimeCli!,
|
r.remote!.mtimeCli!,
|
||||||
password,
|
cipher,
|
||||||
r.remote!.keyEnc
|
r.remote!.keyEnc
|
||||||
);
|
);
|
||||||
await upsertPrevSyncRecordByVaultAndProfile(
|
await upsertPrevSyncRecordByVaultAndProfile(
|
||||||
@@ -1092,7 +1198,12 @@ const dispatchOperationToActualV3 = async (
|
|||||||
);
|
);
|
||||||
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
|
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
|
||||||
// local is deleted, we need to delete remote now
|
// local is deleted, we need to delete remote now
|
||||||
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc);
|
await client.deleteFromRemote(
|
||||||
|
r.key,
|
||||||
|
cipher,
|
||||||
|
r.remote!.keyEnc,
|
||||||
|
r.remote!.synthesizedFolder
|
||||||
|
);
|
||||||
await clearPrevSyncRecordByVaultAndProfile(
|
await clearPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
vaultRandomID,
|
vaultRandomID,
|
||||||
@@ -1119,11 +1230,11 @@ const dispatchOperationToActualV3 = async (
|
|||||||
r.key,
|
r.key,
|
||||||
vault,
|
vault,
|
||||||
false,
|
false,
|
||||||
password,
|
cipher,
|
||||||
r.local!.keyEnc
|
r.local!.keyEnc
|
||||||
);
|
);
|
||||||
// we need to decrypt the key!!!
|
// we need to decrypt the key!!!
|
||||||
await decryptRemoteEntityInplace(entity, password);
|
await decryptRemoteEntityInplace(entity, cipher);
|
||||||
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
|
await fullfillMTimeOfRemoteEntityInplace(entity, mtimeCli);
|
||||||
await upsertPrevSyncRecordByVaultAndProfile(
|
await upsertPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
@@ -1131,9 +1242,28 @@ const dispatchOperationToActualV3 = async (
|
|||||||
profileID,
|
profileID,
|
||||||
entity
|
entity
|
||||||
);
|
);
|
||||||
} else if (r.decision === "folder_to_be_deleted") {
|
} else if (
|
||||||
|
r.decision === "folder_to_be_deleted_on_both" ||
|
||||||
|
r.decision === "folder_to_be_deleted_on_local" ||
|
||||||
|
r.decision === "folder_to_be_deleted_on_remote"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
r.decision === "folder_to_be_deleted_on_both" ||
|
||||||
|
r.decision === "folder_to_be_deleted_on_local"
|
||||||
|
) {
|
||||||
await localDeleteFunc(r.key);
|
await localDeleteFunc(r.key);
|
||||||
await client.deleteFromRemote(r.key, password, r.remote!.keyEnc);
|
}
|
||||||
|
if (
|
||||||
|
r.decision === "folder_to_be_deleted_on_both" ||
|
||||||
|
r.decision === "folder_to_be_deleted_on_remote"
|
||||||
|
) {
|
||||||
|
await client.deleteFromRemote(
|
||||||
|
r.key,
|
||||||
|
cipher,
|
||||||
|
r.remote!.keyEnc,
|
||||||
|
r.remote!.synthesizedFolder
|
||||||
|
);
|
||||||
|
}
|
||||||
await clearPrevSyncRecordByVaultAndProfile(
|
await clearPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
vaultRandomID,
|
vaultRandomID,
|
||||||
@@ -1151,29 +1281,35 @@ export const doActualSync = async (
|
|||||||
vaultRandomID: string,
|
vaultRandomID: string,
|
||||||
profileID: string,
|
profileID: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
password: string,
|
cipher: Cipher,
|
||||||
concurrency: number,
|
concurrency: number,
|
||||||
localDeleteFunc: any,
|
localDeleteFunc: any,
|
||||||
protectModifyPercentage: number,
|
protectModifyPercentage: number,
|
||||||
getProtectModifyPercentageErrorStrFunc: any,
|
getProtectModifyPercentageErrorStrFunc: any,
|
||||||
callbackSyncProcess: any,
|
callbackSyncProcess: any,
|
||||||
db: InternalDBs
|
db: InternalDBs,
|
||||||
|
profiler: Profiler
|
||||||
) => {
|
) => {
|
||||||
|
profiler.addIndent();
|
||||||
|
profiler.insert("doActualSync: enter");
|
||||||
console.debug(`concurrency === ${concurrency}`);
|
console.debug(`concurrency === ${concurrency}`);
|
||||||
const {
|
const {
|
||||||
|
onlyMarkSyncedOps,
|
||||||
folderCreationOps,
|
folderCreationOps,
|
||||||
deletionOps,
|
deletionOps,
|
||||||
uploadDownloads,
|
uploadDownloads,
|
||||||
allFilesCount,
|
allFilesCount,
|
||||||
realModifyDeleteCount,
|
realModifyDeleteCount,
|
||||||
realTotalCount,
|
realTotalCount,
|
||||||
} = splitThreeStepsOnEntityMappings(mixedEntityMappings);
|
} = splitFourStepsOnEntityMappings(mixedEntityMappings);
|
||||||
|
// console.debug(`onlyMarkSyncedOps: ${JSON.stringify(onlyMarkSyncedOps)}`);
|
||||||
// console.debug(`folderCreationOps: ${JSON.stringify(folderCreationOps)}`);
|
// console.debug(`folderCreationOps: ${JSON.stringify(folderCreationOps)}`);
|
||||||
// console.debug(`deletionOps: ${JSON.stringify(deletionOps)}`);
|
// console.debug(`deletionOps: ${JSON.stringify(deletionOps)}`);
|
||||||
// console.debug(`uploadDownloads: ${JSON.stringify(uploadDownloads)}`);
|
// console.debug(`uploadDownloads: ${JSON.stringify(uploadDownloads)}`);
|
||||||
console.debug(`allFilesCount: ${allFilesCount}`);
|
console.debug(`allFilesCount: ${allFilesCount}`);
|
||||||
console.debug(`realModifyDeleteCount: ${realModifyDeleteCount}`);
|
console.debug(`realModifyDeleteCount: ${realModifyDeleteCount}`);
|
||||||
console.debug(`realTotalCount: ${realTotalCount}`);
|
console.debug(`realTotalCount: ${realTotalCount}`);
|
||||||
|
profiler.insert("doActualSync: finish splitting steps");
|
||||||
|
|
||||||
console.debug(`protectModifyPercentage: ${protectModifyPercentage}`);
|
console.debug(`protectModifyPercentage: ${protectModifyPercentage}`);
|
||||||
|
|
||||||
@@ -1183,6 +1319,12 @@ export const doActualSync = async (
|
|||||||
allFilesCount > 0
|
allFilesCount > 0
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
|
protectModifyPercentage === 100 &&
|
||||||
|
realModifyDeleteCount === allFilesCount
|
||||||
|
) {
|
||||||
|
// special treatment for 100%
|
||||||
|
// let it pass, we do nothing here
|
||||||
|
} else if (
|
||||||
realModifyDeleteCount * 100 >=
|
realModifyDeleteCount * 100 >=
|
||||||
allFilesCount * protectModifyPercentage
|
allFilesCount * protectModifyPercentage
|
||||||
) {
|
) {
|
||||||
@@ -1192,19 +1334,29 @@ export const doActualSync = async (
|
|||||||
allFilesCount
|
allFilesCount
|
||||||
);
|
);
|
||||||
|
|
||||||
|
profiler.insert("doActualSync: error branch");
|
||||||
|
profiler.removeIndent();
|
||||||
throw Error(errorStr);
|
throw Error(errorStr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nested = [folderCreationOps, deletionOps, uploadDownloads];
|
const nested = [
|
||||||
|
onlyMarkSyncedOps,
|
||||||
|
folderCreationOps,
|
||||||
|
deletionOps,
|
||||||
|
uploadDownloads,
|
||||||
|
];
|
||||||
const logTexts = [
|
const logTexts = [
|
||||||
`1. create all folders from shadowest to deepest`,
|
`1. record the items already being synced`,
|
||||||
`2. delete files and folders from deepest to shadowest`,
|
`2. create all folders from shadowest to deepest`,
|
||||||
`3. upload or download files in parallel, with the desired concurrency=${concurrency}`,
|
`3. delete files and folders from deepest to shadowest`,
|
||||||
|
`4. upload or download files in parallel, with the desired concurrency=${concurrency}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
let realCounter = 0;
|
let realCounter = 0;
|
||||||
for (let i = 0; i < nested.length; ++i) {
|
for (let i = 0; i < nested.length; ++i) {
|
||||||
|
profiler.addIndent();
|
||||||
|
profiler.insert(`doActualSync: step ${i} start`);
|
||||||
console.debug(logTexts[i]);
|
console.debug(logTexts[i]);
|
||||||
|
|
||||||
const operations = nested[i];
|
const operations = nested[i];
|
||||||
@@ -1252,7 +1404,7 @@ export const doActualSync = async (
|
|||||||
db,
|
db,
|
||||||
vault,
|
vault,
|
||||||
localDeleteFunc,
|
localDeleteFunc,
|
||||||
password
|
cipher
|
||||||
);
|
);
|
||||||
|
|
||||||
console.debug(`finished ${key}`);
|
console.debug(`finished ${key}`);
|
||||||
@@ -1280,5 +1432,11 @@ export const doActualSync = async (
|
|||||||
throw new AggregateError(potentialErrors);
|
throw new AggregateError(potentialErrors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert(`doActualSync: step ${i} end`);
|
||||||
|
profiler.removeIndent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profiler.insert(`doActualSync: exit`);
|
||||||
|
profiler.removeIndent();
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
declare module "*.worker.ts" {
|
||||||
|
class WebpackWorker extends Worker {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
export default WebpackWorker;
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.encryptionmethod-second-confirm {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-auth-related {
|
.settings-auth-related {
|
||||||
border-top: 1px solid var(--background-modifier-border);
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
padding-top: 18px;
|
padding-top: 18px;
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
encryptStringToBase64url,
|
encryptStringToBase64url,
|
||||||
getSizeFromEncToOrig,
|
getSizeFromEncToOrig,
|
||||||
getSizeFromOrigToEnc,
|
getSizeFromOrigToEnc,
|
||||||
} from "../src/encrypt";
|
} from "../src/encryptOpenSSL";
|
||||||
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
|
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
const expect = chai.expect;
|
const expect = chai.expect;
|
||||||
|
|
||||||
describe("Encryption tests", () => {
|
describe("Encryption OpenSSL tests", () => {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
global.window = {
|
global.window = {
|
||||||
crypto: require("crypto").webcrypto,
|
crypto: require("crypto").webcrypto,
|
||||||
@@ -285,3 +285,56 @@ describe("Misc: special char for dir", () => {
|
|||||||
expect(misc.checkHasSpecialCharForDir("xxx?yyy")).to.be.true;
|
expect(misc.checkHasSpecialCharForDir("xxx?yyy")).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Misc: Dropbox: should fix the folder name cases", () => {
|
||||||
|
it("should do nothing on empty folders", () => {
|
||||||
|
const input: any[] = [];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sort folders by length by side effect", () => {
|
||||||
|
const input = [
|
||||||
|
{ keyRaw: "aaaa/" },
|
||||||
|
{ keyRaw: "bbb/" },
|
||||||
|
{ keyRaw: "c/" },
|
||||||
|
{ keyRaw: "dd/" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
{ keyRaw: "c/" },
|
||||||
|
{ keyRaw: "dd/" },
|
||||||
|
{ keyRaw: "bbb/" },
|
||||||
|
{ keyRaw: "aaaa/" },
|
||||||
|
];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.deep.equal(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fix folder names", () => {
|
||||||
|
const input = [
|
||||||
|
{ keyRaw: "AAA/" },
|
||||||
|
{ keyRaw: "aaa/bbb/CCC.md" },
|
||||||
|
{ keyRaw: "aaa/BBB/" },
|
||||||
|
|
||||||
|
{ keyRaw: "ddd/" },
|
||||||
|
{ keyRaw: "DDD/EEE/fff.md" },
|
||||||
|
{ keyRaw: "DDD/eee/" },
|
||||||
|
|
||||||
|
{ keyRaw: "Ggg/" },
|
||||||
|
{ keyRaw: "ggG/hHH你好/Fff世界.md" },
|
||||||
|
{ keyRaw: "ggG/Hhh你好/" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
{ keyRaw: "AAA/" },
|
||||||
|
{ keyRaw: "ddd/" },
|
||||||
|
{ keyRaw: "Ggg/" },
|
||||||
|
{ keyRaw: "AAA/BBB/" },
|
||||||
|
{ keyRaw: "ddd/eee/" },
|
||||||
|
{ keyRaw: "Ggg/Hhh你好/" },
|
||||||
|
{ keyRaw: "AAA/BBB/CCC.md" },
|
||||||
|
{ keyRaw: "ddd/eee/fff.md" },
|
||||||
|
{ keyRaw: "Ggg/Hhh你好/Fff世界.md" },
|
||||||
|
];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.deep.equal(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["dom", "es5", "scripthost", "es2015"]
|
"lib": ["dom", "es5", "scripthost", "es2015", "webworker"]
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"]
|
"include": ["**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.worker\.ts$/,
|
||||||
|
loader: "worker-loader",
|
||||||
|
options: {
|
||||||
|
inline: "no-fallback",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: "ts-loader",
|
use: "ts-loader",
|
||||||
|
|||||||
Reference in New Issue
Block a user