Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e8bec9511 | ||
|
|
c98d91ab60 | ||
|
|
8bd456b08e | ||
|
|
a5c25aecd0 | ||
|
|
806438964c | ||
|
|
069e993fb0 | ||
|
|
7c2a17c404 | ||
|
|
f26e491922 | ||
|
|
dbf80c3359 | ||
|
|
9ddb840077 | ||
|
|
17135abbd4 | ||
|
|
af8357ba6e | ||
|
|
bfa23eb8a5 | ||
|
|
d3d26bfe49 | ||
|
|
f6f7d83a35 | ||
|
|
9e29405103 | ||
|
|
3e7e05fc3d | ||
|
|
391df3ed2d | ||
|
|
c542789c93 | ||
|
|
48e5601a52 | ||
|
|
054a9a3c01 | ||
|
|
66dcaf4e4c | ||
|
|
6d29554033 | ||
|
|
4ed0d735a2 | ||
|
|
26dff02daa | ||
|
|
1b1083d746 | ||
|
|
6db005018d | ||
|
|
8d25ddb636 | ||
|
|
b0d41478a3 | ||
|
|
f69adf273a | ||
|
|
4b4a9b0989 | ||
|
|
bfe11f44dc | ||
|
|
d25303cfac | ||
|
|
ebd62b1581 | ||
|
|
13537d1481 | ||
|
|
2eda5618df |
@@ -92,6 +92,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
|
- If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
|
||||||
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
|
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
|
||||||
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
|
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
|
||||||
|
- **Unofficial** workaround: NextCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can install [WebAppPassword](https://apps.nextcloud.com/apps/webapppassword) app, and add `app://obsidian.md`, `capacitor://localhost`, `http://localhost` to `Allowed origins`
|
||||||
|
- **Unofficial** workaround: OwnCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can download `.tar.gz` of `WebAppPassword` above and manually install and configure it on their instances.
|
||||||
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
|
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
|
||||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# How To Configure Apache CORS Rules for Webdav
|
||||||
|
|
||||||
|
This method is [contributed by community](https://github.com/remotely-save/remotely-save/pull/31).
|
||||||
|
|
||||||
|
You should evaluate the risk by yourself before trying this method.
|
||||||
|
|
||||||
|
**You are strongly advised to backup your original Apache configuration files before making any changes.**
|
||||||
|
|
||||||
|
```apacheconf
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Obsidian webdav
|
||||||
|
SetEnvIf Origin "^app://obsidian.md$" IS_OBSIDIAN
|
||||||
|
SetEnvIf Origin "^capacitor://localhost$" IS_OBSIDIAN
|
||||||
|
SetEnvIf Origin "^http://localhost$" IS_OBSIDIAN
|
||||||
|
Header always set Access-Control-Allow-Origin "*" env=IS_OBSIDIAN
|
||||||
|
Header always set Access-Control-Allow-Methods "GET, HEAD, POST, PUT, OPTIONS, MOVE, DELETE, COPY, LOCK, UNLOCK, PROPFIND" env=IS_OBSIDIAN
|
||||||
|
Header always set Access-Control-Allow-Headers "Authorization, Depth, DNT, User-Agent, Keep-Alive, Content-Type, accept, origin, X-Requested-With" env=IS_OBSIDIAN
|
||||||
|
Header always set Access-Control-Expose-Headers "etag, dav" env=IS_OBSIDIAN
|
||||||
|
|
||||||
|
# Allow OPTION request without authentication and respond with status 200
|
||||||
|
RewriteCond %{ENV:IS_OBSIDIAN} 1
|
||||||
|
RewriteCond %{REQUEST_METHOD} OPTIONS
|
||||||
|
RewriteRule ^(.*)$ $1 [R=200,L]
|
||||||
|
</IfModule>
|
||||||
|
```
|
||||||
@@ -4,6 +4,11 @@ The plugin is developed for the browser environment. The "fake" browser behind t
|
|||||||
|
|
||||||
[MDN has a doc about CORS.](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
[MDN has a doc about CORS.](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
||||||
|
|
||||||
From Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, Obsidian [provides a new API `requiestUrl`](https://forum.obsidian.md/t/obsidian-release-v0-13-25-insider-build/32701), that allows the plugin to fully bypass the CORS issue. As of Mar 2022, the latest public-released Obsidian desktop has supported this API, but the Obsidian mobile still stays in insider.
|
1. From Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, Obsidian [provides a new API `requiestUrl`](https://forum.obsidian.md/t/obsidian-release-v0-13-25-insider-build/32701), that allows the plugin to fully bypass the CORS issue. As of Mar 2022, the latest public-released Obsidian desktop has supported this API, but the Obsidian mobile still stays in insider.
|
||||||
|
|
||||||
For using this plugin in Obsidian desktop < 0.13.25 or mobile < 1.1.1, we need to configure the server side to return the header `Access-Control-Allow-Origin` allowing the origins `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. Here is an example [configuration for Amazon S3](./s3_cors_configure.md).
|
2. For using this plugin in Obsidian desktop < 0.13.25 or mobile < 1.1.1, we need to configure the server side to return the header `Access-Control-Allow-Origin` allowing the origins `app://obsidian.md` and `capacitor://localhost` and `http://localhost`.
|
||||||
|
|
||||||
|
Example configurations:
|
||||||
|
|
||||||
|
- [Amazon S3](./s3_cors_configure.md)
|
||||||
|
- [Apache](./apache_cors_configure.md) ([contributed by community](https://github.com/remotely-save/remotely-save/pull/31))
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Sync Ignoring Large Files
|
||||||
|
|
||||||
|
Initially, the plugin does not ignore large files.
|
||||||
|
|
||||||
|
From the new version in May 2022, it can ignore all files with some sizes. But we need some rules to make the function compatible with existing conditions.
|
||||||
|
|
||||||
|
1. If users are using E2E password mode, then the file sizes are compared on the **encrypted sizes**, rather than the original unencripted file sizes. The reasons are: the encrypted ones are in transferations, and the encrypted sizes can be computed from unencrypted sizes but not the reverse.
|
||||||
|
|
||||||
|
2. Assuming the file A, is already synced between local device and remote service before.
|
||||||
|
|
||||||
|
- If the local size and remote size are both below the threshold, then the file can be synced normally.
|
||||||
|
- If the local size and remote size are both above the threshold, then the file will be ignored normally.
|
||||||
|
- If the local size is below the threshold, and the remote size is above the threshold, then the plugin **rejects** the sync, and throws the error to the user.
|
||||||
|
- If the local size is above the threshold, and the remote size is below the threshold, then the plugin **rejects** the sync, and throws the error to the user.
|
||||||
|
- When it somes to deletions, the same rules apply.
|
||||||
|
|
||||||
|
The main point is that, if the file sizes "cross the line", the plugin does not introduce any further trouble and just reject to work for this file.
|
||||||
+4
-1
@@ -31,11 +31,11 @@ esbuild
|
|||||||
"obsidian",
|
"obsidian",
|
||||||
"electron",
|
"electron",
|
||||||
"fs",
|
"fs",
|
||||||
"crypto",
|
|
||||||
"tls",
|
"tls",
|
||||||
"net",
|
"net",
|
||||||
// ...builtins
|
// ...builtins
|
||||||
],
|
],
|
||||||
|
inject: ["./esbuild.injecthelper.mjs"],
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
watch: !prod,
|
watch: !prod,
|
||||||
target: "es2016",
|
target: "es2016",
|
||||||
@@ -48,6 +48,9 @@ esbuild
|
|||||||
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
|
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
|
||||||
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
|
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
|
||||||
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
|
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
|
||||||
|
global: "window",
|
||||||
|
"process.env.NODE_DEBUG": `undefined`, // ugly fix
|
||||||
|
"process.env.DEBUG": `undefined`, // ugly fix
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.catch(() => process.exit(1));
|
.catch(() => process.exit(1));
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export let Buffer = require("buffer").Buffer;
|
||||||
|
export let process = require("process/browser");
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.3.19",
|
"version": "0.3.24",
|
||||||
"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",
|
||||||
|
|||||||
+39
-39
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.3.19",
|
"version": "0.3.24",
|
||||||
"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",
|
"dev2": "node esbuild.config.mjs",
|
||||||
@@ -23,73 +23,73 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/microsoft-graph-types": "^2.11.0",
|
"@microsoft/microsoft-graph-types": "^2.19.0",
|
||||||
"@types/chai": "^4.2.22",
|
"@types/chai": "^4.3.1",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/jsdom": "^16.2.13",
|
"@types/jsdom": "^16.2.14",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.182",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.1.1",
|
||||||
"@types/mustache": "^4.1.2",
|
"@types/mustache": "^4.1.2",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^17.0.30",
|
||||||
"@types/qrcode": "^1.4.1",
|
"@types/qrcode": "^1.4.2",
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "^3.2.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.6",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"esbuild": "^0.14.27",
|
"esbuild": "^0.14.38",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"mocha": "^9.1.3",
|
"mocha": "^9.2.2",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.6.2",
|
||||||
"ts-loader": "^9.2.6",
|
"ts-loader": "^9.2.9",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.7.0",
|
||||||
"tslib": "^2.2.0",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.6.4",
|
||||||
"webdav-server": "^2.6.2",
|
"webdav-server": "^2.6.2",
|
||||||
"webpack": "^5.64.4",
|
"webpack": "^5.72.0",
|
||||||
"webpack-cli": "^4.9.1"
|
"webpack-cli": "^4.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.67.0",
|
"@aws-sdk/client-s3": "^3.81.0",
|
||||||
"@aws-sdk/fetch-http-handler": "^3.58.0",
|
"@aws-sdk/fetch-http-handler": "^3.78.0",
|
||||||
"@aws-sdk/lib-storage": "^3.67.0",
|
"@aws-sdk/lib-storage": "^3.81.0",
|
||||||
"@aws-sdk/protocol-http": "^3.58.0",
|
"@aws-sdk/protocol-http": "^3.78.0",
|
||||||
"@aws-sdk/querystring-builder": "^3.55.0",
|
"@aws-sdk/querystring-builder": "^3.78.0",
|
||||||
"@aws-sdk/signature-v4-crt": "^3.66.0",
|
"@aws-sdk/signature-v4-crt": "^3.78.0",
|
||||||
"@aws-sdk/types": "^3.55.0",
|
"@aws-sdk/types": "^3.78.0",
|
||||||
"@azure/msal-node": "^1.6.0",
|
"@azure/msal-node": "^1.8.0",
|
||||||
"@fyears/tsqueue": "^1.0.1",
|
"@fyears/tsqueue": "^1.0.1",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
"@microsoft/microsoft-graph-client": "^3.0.2",
|
||||||
"acorn": "^8.5.0",
|
"acorn": "^8.7.1",
|
||||||
"aggregate-error": "^4.0.0",
|
"aggregate-error": "^4.0.0",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"aws-crt": "^1.10.1",
|
"aws-crt": "^1.12.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"delay": "^5.0.0",
|
"delay": "^5.0.0",
|
||||||
"dropbox": "^10.22.0",
|
"dropbox": "^10.28.0",
|
||||||
"emoji-regex": "^10.1.0",
|
"emoji-regex": "^10.1.0",
|
||||||
"http-status-codes": "^2.2.0",
|
"http-status-codes": "^2.2.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
"lucide": "^0.17.12",
|
"lucide": "^0.35.0",
|
||||||
"mime-types": "^2.1.33",
|
"mime-types": "^2.1.35",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nanoid": "^3.1.30",
|
"nanoid": "^3.3.3",
|
||||||
"obsidian": "^0.13.26",
|
"obsidian": "^0.14.6",
|
||||||
"p-queue": "^7.2.0",
|
"p-queue": "^7.2.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qrcode": "^1.5.0",
|
"qrcode": "^1.5.0",
|
||||||
"rfc4648": "^1.5.0",
|
"rfc4648": "^1.5.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"util": "^0.12.4",
|
"util": "^0.12.4",
|
||||||
"webdav": "^4.7.0",
|
"webdav": "^4.9.0",
|
||||||
"webdav-fs": "^4.0.0",
|
"webdav-fs": "^4.0.1",
|
||||||
"xregexp": "^5.1.0"
|
"xregexp": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -85,6 +85,7 @@ export interface RemotelySavePluginSettings {
|
|||||||
syncUnderscoreItems?: boolean;
|
syncUnderscoreItems?: boolean;
|
||||||
lang?: LangTypeAndAuto;
|
lang?: LangTypeAndAuto;
|
||||||
logToDB?: boolean;
|
logToDB?: boolean;
|
||||||
|
skipSizeLargerThan?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@@ -122,13 +123,24 @@ type DecisionTypeForFile =
|
|||||||
| "uploadLocalToRemote" // "skipLocal && uploadLocalToRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
| "uploadLocalToRemote" // "skipLocal && uploadLocalToRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
||||||
| "downloadRemoteToLocal"; // "downloadRemoteToLocal && skipRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
| "downloadRemoteToLocal"; // "downloadRemoteToLocal && skipRemote && cleanLocalDelHist && cleanRemoteDelHist"
|
||||||
|
|
||||||
|
type DecisionTypeForFileSize =
|
||||||
|
| "skipUploadingTooLarge"
|
||||||
|
| "skipDownloadingTooLarge"
|
||||||
|
| "skipUsingLocalDelTooLarge"
|
||||||
|
| "skipUsingRemoteDelTooLarge"
|
||||||
|
| "errorLocalTooLargeConflictRemote"
|
||||||
|
| "errorRemoteTooLargeConflictLocal";
|
||||||
|
|
||||||
type DecisionTypeForFolder =
|
type DecisionTypeForFolder =
|
||||||
| "createFolder"
|
| "createFolder"
|
||||||
| "uploadLocalDelHistToRemoteFolder"
|
| "uploadLocalDelHistToRemoteFolder"
|
||||||
| "keepRemoteDelHistFolder"
|
| "keepRemoteDelHistFolder"
|
||||||
| "skipFolder";
|
| "skipFolder";
|
||||||
|
|
||||||
export type DecisionType = DecisionTypeForFile | DecisionTypeForFolder;
|
export type DecisionType =
|
||||||
|
| DecisionTypeForFile
|
||||||
|
| DecisionTypeForFileSize
|
||||||
|
| DecisionTypeForFolder;
|
||||||
|
|
||||||
export interface FileOrFolderMixedState {
|
export interface FileOrFolderMixedState {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -139,7 +151,9 @@ export interface FileOrFolderMixedState {
|
|||||||
deltimeLocal?: number;
|
deltimeLocal?: number;
|
||||||
deltimeRemote?: number;
|
deltimeRemote?: number;
|
||||||
sizeLocal?: number;
|
sizeLocal?: number;
|
||||||
|
sizeLocalEnc?: number;
|
||||||
sizeRemote?: number;
|
sizeRemote?: number;
|
||||||
|
sizeRemoteEnc?: number;
|
||||||
changeRemoteMtimeUsingMapping?: boolean;
|
changeRemoteMtimeUsingMapping?: boolean;
|
||||||
changeLocalMtimeUsingMapping?: boolean;
|
changeLocalMtimeUsingMapping?: boolean;
|
||||||
decision?: DecisionType;
|
decision?: DecisionType;
|
||||||
@@ -165,3 +179,5 @@ 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 type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ const turnSyncPlanToTable = (record: string) => {
|
|||||||
"remoteEncryptedKey",
|
"remoteEncryptedKey",
|
||||||
"existLocal",
|
"existLocal",
|
||||||
"sizeLocal",
|
"sizeLocal",
|
||||||
|
"sizeLocalEnc",
|
||||||
"mtimeLocal",
|
"mtimeLocal",
|
||||||
"deltimeLocal",
|
"deltimeLocal",
|
||||||
"changeLocalMtimeUsingMapping",
|
"changeLocalMtimeUsingMapping",
|
||||||
"existRemote",
|
"existRemote",
|
||||||
"sizeRemote",
|
"sizeRemote",
|
||||||
|
"sizeRemoteEnc",
|
||||||
"mtimeRemote",
|
"mtimeRemote",
|
||||||
"deltimeRemote",
|
"deltimeRemote",
|
||||||
"changeRemoteMtimeUsingMapping",
|
"changeRemoteMtimeUsingMapping",
|
||||||
|
|||||||
+1
-1
Submodule src/langs updated: 82d0619761...42eab5d544
+20
-6
@@ -9,7 +9,11 @@ import {
|
|||||||
} 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";
|
||||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
import type {
|
||||||
|
FileOrFolderMixedState,
|
||||||
|
RemotelySavePluginSettings,
|
||||||
|
SyncTriggerSourceType,
|
||||||
|
} from "./baseTypes";
|
||||||
import {
|
import {
|
||||||
COMMAND_CALLBACK,
|
COMMAND_CALLBACK,
|
||||||
COMMAND_CALLBACK_ONEDRIVE,
|
COMMAND_CALLBACK_ONEDRIVE,
|
||||||
@@ -61,6 +65,7 @@ import {
|
|||||||
exportVaultLoggerOutputToFiles,
|
exportVaultLoggerOutputToFiles,
|
||||||
exportVaultSyncPlansToFiles,
|
exportVaultSyncPlansToFiles,
|
||||||
} from "./debugMode";
|
} from "./debugMode";
|
||||||
|
import { SizesConflictModal } from "./syncSizesConflictNotice";
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||||
s3: DEFAULT_S3_CONFIG,
|
s3: DEFAULT_S3_CONFIG,
|
||||||
@@ -79,6 +84,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
|||||||
syncUnderscoreItems: false,
|
syncUnderscoreItems: false,
|
||||||
lang: "auto",
|
lang: "auto",
|
||||||
logToDB: false,
|
logToDB: false,
|
||||||
|
skipSizeLargerThan: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface OAuth2Info {
|
interface OAuth2Info {
|
||||||
@@ -89,8 +95,6 @@ interface OAuth2Info {
|
|||||||
revokeAuthSetting?: Setting;
|
revokeAuthSetting?: Setting;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
|
||||||
|
|
||||||
const iconNameSyncWait = `remotely-save-sync-wait`;
|
const iconNameSyncWait = `remotely-save-sync-wait`;
|
||||||
const iconNameSyncRunning = `remotely-save-sync-running`;
|
const iconNameSyncRunning = `remotely-save-sync-running`;
|
||||||
const iconNameLogs = `remotely-save-logs`;
|
const iconNameLogs = `remotely-save-logs`;
|
||||||
@@ -279,23 +283,23 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.syncStatus = "generating_plan";
|
this.syncStatus = "generating_plan";
|
||||||
const { plan, sortedKeys, deletions } = await getSyncPlan(
|
const { plan, sortedKeys, deletions, sizesGoWrong } = await getSyncPlan(
|
||||||
remoteStates,
|
remoteStates,
|
||||||
local,
|
local,
|
||||||
localConfigDirContents,
|
localConfigDirContents,
|
||||||
origMetadataOnRemote.deletions,
|
origMetadataOnRemote.deletions,
|
||||||
localHistory,
|
localHistory,
|
||||||
client.serviceType,
|
client.serviceType,
|
||||||
|
triggerSource,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.settings.syncConfigDir,
|
this.settings.syncConfigDir,
|
||||||
this.app.vault.configDir,
|
this.app.vault.configDir,
|
||||||
this.settings.syncUnderscoreItems,
|
this.settings.syncUnderscoreItems,
|
||||||
|
this.settings.skipSizeLargerThan,
|
||||||
this.settings.password
|
this.settings.password
|
||||||
);
|
);
|
||||||
log.info(plan.mixedStates); // for debugging
|
log.info(plan.mixedStates); // for debugging
|
||||||
if (triggerSource !== "dry") {
|
|
||||||
await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
|
await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -317,10 +321,20 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
sortedKeys,
|
sortedKeys,
|
||||||
metadataFile,
|
metadataFile,
|
||||||
origMetadataOnRemote,
|
origMetadataOnRemote,
|
||||||
|
sizesGoWrong,
|
||||||
deletions,
|
deletions,
|
||||||
(key: string) => self.trash(key),
|
(key: string) => self.trash(key),
|
||||||
this.settings.password,
|
this.settings.password,
|
||||||
this.settings.concurrency,
|
this.settings.concurrency,
|
||||||
|
(ss: FileOrFolderMixedState[]) => {
|
||||||
|
new SizesConflictModal(
|
||||||
|
self.app,
|
||||||
|
self,
|
||||||
|
this.settings.skipSizeLargerThan,
|
||||||
|
ss,
|
||||||
|
this.settings.password !== ""
|
||||||
|
).open();
|
||||||
|
},
|
||||||
(i: number, totalCount: number, pathName: string, decision: string) =>
|
(i: number, totalCount: number, pathName: string, decision: string) =>
|
||||||
self.setCurrSyncMsg(i, totalCount, pathName, decision)
|
self.setCurrSyncMsg(i, totalCount, pathName, decision)
|
||||||
);
|
);
|
||||||
|
|||||||
+34
-8
@@ -1,6 +1,7 @@
|
|||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
import { base64url } from "rfc4648";
|
import { base64url } from "rfc4648";
|
||||||
import { reverseString } from "./misc";
|
import { reverseString } from "./misc";
|
||||||
|
import { log } from "./moreOnLog";
|
||||||
|
|
||||||
const DEFAULT_README_FOR_METADATAONREMOTE =
|
const DEFAULT_README_FOR_METADATAONREMOTE =
|
||||||
"Do NOT edit or delete the file manually. This file is for the plugin remotely-save to store some necessary meta data on the remote services. Its content is slightly obfuscated.";
|
"Do NOT edit or delete the file manually. This file is for the plugin remotely-save to store some necessary meta data on the remote services. Its content is slightly obfuscated.";
|
||||||
@@ -69,19 +70,44 @@ export const deserializeMetadataOnRemote = (x: string | ArrayBuffer) => {
|
|||||||
} else {
|
} else {
|
||||||
y1 = new TextDecoder().decode(x);
|
y1 = new TextDecoder().decode(x);
|
||||||
}
|
}
|
||||||
const y2: any = JSON.parse(y1);
|
|
||||||
|
|
||||||
if (!("readme" in y2 && "d" in y2)) {
|
let y2: any;
|
||||||
throw Error("invalid remote meta data file!");
|
try {
|
||||||
|
y2 = JSON.parse(y1);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid remote meta data file with first few chars: ${y1.slice(0, 5)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const y3 = JSON.parse(
|
if (!("readme" in y2 && "d" in y2)) {
|
||||||
(
|
throw new Error(
|
||||||
|
'invalid remote meta data file (no "readme" or "d" fields)!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let y3: string;
|
||||||
|
try {
|
||||||
|
y3 = (
|
||||||
base64url.parse(reverseString(y2["d"]), {
|
base64url.parse(reverseString(y2["d"]), {
|
||||||
out: Buffer.allocUnsafe as any,
|
out: Buffer.allocUnsafe as any,
|
||||||
loose: true,
|
loose: true,
|
||||||
}) as Buffer
|
}) as Buffer
|
||||||
).toString("utf-8")
|
).toString("utf-8");
|
||||||
) as MetadataOnRemote;
|
} catch (e) {
|
||||||
return y3;
|
throw new Error('invalid remote meta data file (invalid "d" field)!');
|
||||||
|
}
|
||||||
|
|
||||||
|
let y4: MetadataOnRemote;
|
||||||
|
try {
|
||||||
|
y4 = JSON.parse(y3) as MetadataOnRemote;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid remote meta data file with \"d\" field with first few chars: ${y3.slice(
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return y4;
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-8
@@ -15,7 +15,6 @@ import { log } from "./moreOnLog";
|
|||||||
|
|
||||||
export class RemoteClient {
|
export class RemoteClient {
|
||||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
readonly s3Client?: s3.S3Client;
|
|
||||||
readonly s3Config?: S3Config;
|
readonly s3Config?: S3Config;
|
||||||
readonly webdavClient?: webdav.WrappedWebdavClient;
|
readonly webdavClient?: webdav.WrappedWebdavClient;
|
||||||
readonly webdavConfig?: WebdavConfig;
|
readonly webdavConfig?: WebdavConfig;
|
||||||
@@ -38,7 +37,6 @@ export class RemoteClient {
|
|||||||
// so we use a ref not copy of config here
|
// so we use a ref not copy of config here
|
||||||
if (serviceType === "s3") {
|
if (serviceType === "s3") {
|
||||||
this.s3Config = s3Config;
|
this.s3Config = s3Config;
|
||||||
this.s3Client = s3.getS3Client(this.s3Config);
|
|
||||||
} else if (serviceType === "webdav") {
|
} else if (serviceType === "webdav") {
|
||||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||||
throw Error(
|
throw Error(
|
||||||
@@ -86,7 +84,7 @@ export class RemoteClient {
|
|||||||
getRemoteMeta = async (fileOrFolderPath: string) => {
|
getRemoteMeta = async (fileOrFolderPath: string) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.getRemoteMeta(
|
return await s3.getRemoteMeta(
|
||||||
this.s3Client,
|
s3.getS3Client(this.s3Config),
|
||||||
this.s3Config,
|
this.s3Config,
|
||||||
fileOrFolderPath
|
fileOrFolderPath
|
||||||
);
|
);
|
||||||
@@ -116,7 +114,7 @@ export class RemoteClient {
|
|||||||
) => {
|
) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.uploadToRemote(
|
return await s3.uploadToRemote(
|
||||||
this.s3Client,
|
s3.getS3Client(this.s3Config),
|
||||||
this.s3Config,
|
this.s3Config,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
@@ -168,7 +166,11 @@ export class RemoteClient {
|
|||||||
|
|
||||||
listFromRemote = async (prefix?: string) => {
|
listFromRemote = async (prefix?: string) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.listFromRemote(this.s3Client, this.s3Config, prefix);
|
return await s3.listFromRemote(
|
||||||
|
s3.getS3Client(this.s3Config),
|
||||||
|
this.s3Config,
|
||||||
|
prefix
|
||||||
|
);
|
||||||
} else if (this.serviceType === "webdav") {
|
} else if (this.serviceType === "webdav") {
|
||||||
return await webdav.listFromRemote(this.webdavClient, prefix);
|
return await webdav.listFromRemote(this.webdavClient, prefix);
|
||||||
} else if (this.serviceType === "dropbox") {
|
} else if (this.serviceType === "dropbox") {
|
||||||
@@ -190,7 +192,7 @@ export class RemoteClient {
|
|||||||
) => {
|
) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.downloadFromRemote(
|
return await s3.downloadFromRemote(
|
||||||
this.s3Client,
|
s3.getS3Client(this.s3Config),
|
||||||
this.s3Config,
|
this.s3Config,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
vault,
|
vault,
|
||||||
@@ -241,7 +243,7 @@ export class RemoteClient {
|
|||||||
) => {
|
) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.deleteFromRemote(
|
return await s3.deleteFromRemote(
|
||||||
this.s3Client,
|
s3.getS3Client(this.s3Config),
|
||||||
this.s3Config,
|
this.s3Config,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
password,
|
password,
|
||||||
@@ -276,7 +278,7 @@ export class RemoteClient {
|
|||||||
checkConnectivity = async (callbackFunc?: any) => {
|
checkConnectivity = async (callbackFunc?: any) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.checkConnectivity(
|
return await s3.checkConnectivity(
|
||||||
this.s3Client,
|
s3.getS3Client(this.s3Config),
|
||||||
this.s3Config,
|
this.s3Config,
|
||||||
callbackFunc
|
callbackFunc
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -371,9 +371,13 @@ export class WrappedDropboxClient {
|
|||||||
throw Error("The user has not manually auth yet.");
|
throw Error("The user has not manually auth yet.");
|
||||||
}
|
}
|
||||||
const currentTs = Date.now();
|
const currentTs = Date.now();
|
||||||
|
const customHeaders = {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
};
|
||||||
if (this.dropboxConfig.accessTokenExpiresAtTime > currentTs) {
|
if (this.dropboxConfig.accessTokenExpiresAtTime > currentTs) {
|
||||||
this.dropbox = new Dropbox({
|
this.dropbox = new Dropbox({
|
||||||
accessToken: this.dropboxConfig.accessToken,
|
accessToken: this.dropboxConfig.accessToken,
|
||||||
|
customHeaders: customHeaders,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (this.dropboxConfig.refreshToken === "") {
|
if (this.dropboxConfig.refreshToken === "") {
|
||||||
@@ -393,6 +397,7 @@ export class WrappedDropboxClient {
|
|||||||
);
|
);
|
||||||
this.dropbox = new Dropbox({
|
this.dropbox = new Dropbox({
|
||||||
accessToken: this.dropboxConfig.accessToken,
|
accessToken: this.dropboxConfig.accessToken,
|
||||||
|
customHeaders: customHeaders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -444,6 +444,7 @@ export class WrappedOnedriveClient {
|
|||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -565,6 +566,7 @@ export class WrappedOnedriveClient {
|
|||||||
headers: {
|
headers: {
|
||||||
// no "Content-Length" allowed here
|
// no "Content-Length" allowed here
|
||||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||||
|
/* "Cache-Control": "no-cache", not allowed here!!! */
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res.json as DriveItem | UploadSession;
|
return res.json as DriveItem | UploadSession;
|
||||||
@@ -576,6 +578,7 @@ export class WrappedOnedriveClient {
|
|||||||
"Content-Length": `${rangeEnd - rangeStart}`,
|
"Content-Length": `${rangeEnd - rangeStart}`,
|
||||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||||
|
/* "Cache-Control": "no-cache", not allowed here!!! */
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (await res.json()) as DriveItem | UploadSession;
|
return (await res.json()) as DriveItem | UploadSession;
|
||||||
@@ -809,10 +812,16 @@ const downloadFromRemoteRaw = async (
|
|||||||
);
|
);
|
||||||
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
||||||
if (VALID_REQURL) {
|
if (VALID_REQURL) {
|
||||||
const content = (await requestUrl({ url: downloadUrl })).arrayBuffer;
|
const content = (
|
||||||
|
await requestUrl({
|
||||||
|
url: downloadUrl,
|
||||||
|
headers: { "Cache-Control": "no-cache" },
|
||||||
|
})
|
||||||
|
).arrayBuffer;
|
||||||
return content;
|
return content;
|
||||||
} else {
|
} else {
|
||||||
const content = await (await fetch(downloadUrl)).arrayBuffer();
|
const content = await // cannot set no-cache here, will have cors error
|
||||||
|
(await fetch(downloadUrl)).arrayBuffer();
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+36
-9
@@ -24,6 +24,7 @@ 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 } from "obsidian";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
import AggregateError from "aggregate-error";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CONTENT_TYPE,
|
DEFAULT_CONTENT_TYPE,
|
||||||
RemoteItem,
|
RemoteItem,
|
||||||
@@ -83,11 +84,14 @@ class ObsHttpHandler extends FetchHttpHandler {
|
|||||||
const body =
|
const body =
|
||||||
method === "GET" || method === "HEAD" ? undefined : request.body;
|
method === "GET" || method === "HEAD" ? undefined : request.body;
|
||||||
|
|
||||||
const transformedHeaders = { ...request.headers };
|
const transformedHeaders: Record<string, string> = {};
|
||||||
delete transformedHeaders["host"];
|
for (const key of Object.keys(request.headers)) {
|
||||||
delete transformedHeaders["Host"];
|
const keyLower = key.toLowerCase();
|
||||||
delete transformedHeaders["content-length"];
|
if (keyLower === "host" || keyLower === "content-length") {
|
||||||
delete transformedHeaders["Content-Length"];
|
continue;
|
||||||
|
}
|
||||||
|
transformedHeaders[keyLower] = request.headers[key];
|
||||||
|
}
|
||||||
|
|
||||||
let contentType: string = undefined;
|
let contentType: string = undefined;
|
||||||
if (transformedHeaders["content-type"] !== undefined) {
|
if (transformedHeaders["content-type"] !== undefined) {
|
||||||
@@ -192,8 +196,10 @@ export const getS3Client = (s3Config: S3Config) => {
|
|||||||
endpoint = `https://${endpoint}`;
|
endpoint = `https://${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let s3Client: S3Client;
|
||||||
|
|
||||||
if (VALID_REQURL && s3Config.bypassCorsLocally) {
|
if (VALID_REQURL && s3Config.bypassCorsLocally) {
|
||||||
const s3Client = new S3Client({
|
s3Client = new S3Client({
|
||||||
region: s3Config.s3Region,
|
region: s3Config.s3Region,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
forcePathStyle: s3Config.forcePathStyle,
|
forcePathStyle: s3Config.forcePathStyle,
|
||||||
@@ -203,9 +209,8 @@ export const getS3Client = (s3Config: S3Config) => {
|
|||||||
},
|
},
|
||||||
requestHandler: new ObsHttpHandler(),
|
requestHandler: new ObsHttpHandler(),
|
||||||
});
|
});
|
||||||
return s3Client;
|
|
||||||
} else {
|
} else {
|
||||||
const s3Client = new S3Client({
|
s3Client = new S3Client({
|
||||||
region: s3Config.s3Region,
|
region: s3Config.s3Region,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
forcePathStyle: s3Config.forcePathStyle,
|
forcePathStyle: s3Config.forcePathStyle,
|
||||||
@@ -214,8 +219,19 @@ export const getS3Client = (s3Config: S3Config) => {
|
|||||||
secretAccessKey: s3Config.s3SecretAccessKey,
|
secretAccessKey: s3Config.s3SecretAccessKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return s3Client;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s3Client.middlewareStack.add(
|
||||||
|
(next, context) => (args) => {
|
||||||
|
(args.request as any).headers["cache-control"] = "no-cache";
|
||||||
|
return next(args);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: "build",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return s3Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRemoteMeta = async (
|
export const getRemoteMeta = async (
|
||||||
@@ -522,8 +538,19 @@ export const checkConnectivity = async (
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.debug(err);
|
log.debug(err);
|
||||||
if (callbackFunc !== undefined) {
|
if (callbackFunc !== undefined) {
|
||||||
|
if (s3Config.s3Endpoint.contains(s3Config.s3BucketName)) {
|
||||||
|
const err2 = new AggregateError([
|
||||||
|
err,
|
||||||
|
new Error(
|
||||||
|
"Maybe you've included the bucket name inside the endpoint setting. Please remove the bucket name and try again."
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
callbackFunc(err2);
|
||||||
|
} else {
|
||||||
callbackFunc(err);
|
callbackFunc(err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ export class WrappedWebdavClient {
|
|||||||
|
|
||||||
init = async () => {
|
init = async () => {
|
||||||
// init client if not inited
|
// init client if not inited
|
||||||
|
const headers = {
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
};
|
||||||
if (this.client === undefined) {
|
if (this.client === undefined) {
|
||||||
if (
|
if (
|
||||||
this.webdavConfig.username !== "" &&
|
this.webdavConfig.username !== "" &&
|
||||||
@@ -191,6 +194,7 @@ export class WrappedWebdavClient {
|
|||||||
this.client = createClient(this.webdavConfig.address, {
|
this.client = createClient(this.webdavConfig.address, {
|
||||||
username: this.webdavConfig.username,
|
username: this.webdavConfig.username,
|
||||||
password: this.webdavConfig.password,
|
password: this.webdavConfig.password,
|
||||||
|
headers: headers,
|
||||||
authType:
|
authType:
|
||||||
this.webdavConfig.authType === "digest"
|
this.webdavConfig.authType === "digest"
|
||||||
? AuthType.Digest
|
? AuthType.Digest
|
||||||
@@ -198,7 +202,9 @@ export class WrappedWebdavClient {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info("no password");
|
log.info("no password");
|
||||||
this.client = createClient(this.webdavConfig.address);
|
this.client = createClient(this.webdavConfig.address, {
|
||||||
|
headers: headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+158
-134
@@ -645,8 +645,9 @@ const wrapTextWithPasswordHide = (text: TextComponent) => {
|
|||||||
hider.innerHTML = eyeOff;
|
hider.innerHTML = eyeOff;
|
||||||
hider.addEventListener("click", (e) => {
|
hider.addEventListener("click", (e) => {
|
||||||
const isText = text.inputEl.getAttribute("type") === "text";
|
const isText = text.inputEl.getAttribute("type") === "text";
|
||||||
text.inputEl.setAttribute("type", isText ? "password" : "text");
|
|
||||||
hider.innerHTML = isText ? eyeOff : eye;
|
hider.innerHTML = isText ? eyeOff : eye;
|
||||||
|
text.inputEl.setAttribute("type", isText ? "password" : "text");
|
||||||
|
text.inputEl.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// the init type of text el is password
|
// the init type of text el is password
|
||||||
@@ -674,100 +675,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
containerEl.createEl("h1", { text: "Remotely Save" });
|
containerEl.createEl("h1", { text: "Remotely Save" });
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// below for general
|
// below for service chooser (part 1/2)
|
||||||
//////////////////////////////////////////////////
|
|
||||||
|
|
||||||
const generalDiv = containerEl.createEl("div");
|
|
||||||
generalDiv.createEl("h2", { text: t("settings_general") });
|
|
||||||
|
|
||||||
const passwordDiv = generalDiv.createEl("div");
|
|
||||||
let newPassword = `${this.plugin.settings.password}`;
|
|
||||||
new Setting(passwordDiv)
|
|
||||||
.setName(t("settings_password"))
|
|
||||||
.setDesc(t("settings_password_desc"))
|
|
||||||
.addText((text) => {
|
|
||||||
wrapTextWithPasswordHide(text);
|
|
||||||
text
|
|
||||||
.setPlaceholder("")
|
|
||||||
.setValue(`${this.plugin.settings.password}`)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
newPassword = value.trim();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.addButton(async (button) => {
|
|
||||||
button.setButtonText(t("confirm"));
|
|
||||||
button.onClick(async () => {
|
|
||||||
new PasswordModal(this.app, this.plugin, newPassword).open();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const scheduleDiv = generalDiv.createEl("div");
|
|
||||||
new Setting(scheduleDiv)
|
|
||||||
.setName(t("settings_autorun"))
|
|
||||||
.setDesc(t("settings_autorun_desc"))
|
|
||||||
.addDropdown((dropdown) => {
|
|
||||||
dropdown.addOption("-1", t("settings_autorun_notset"));
|
|
||||||
dropdown.addOption(`${1000 * 60 * 1}`, t("settings_autorun_1min"));
|
|
||||||
dropdown.addOption(`${1000 * 60 * 5}`, t("settings_autorun_5min"));
|
|
||||||
dropdown.addOption(`${1000 * 60 * 10}`, t("settings_autorun_10min"));
|
|
||||||
dropdown.addOption(`${1000 * 60 * 30}`, t("settings_autorun_30min"));
|
|
||||||
|
|
||||||
dropdown
|
|
||||||
.setValue(`${this.plugin.settings.autoRunEveryMilliseconds}`)
|
|
||||||
.onChange(async (val: string) => {
|
|
||||||
const realVal = parseInt(val);
|
|
||||||
this.plugin.settings.autoRunEveryMilliseconds = realVal;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
if (
|
|
||||||
(realVal === undefined || realVal === null || realVal <= 0) &&
|
|
||||||
this.plugin.autoRunIntervalID !== undefined
|
|
||||||
) {
|
|
||||||
// clear
|
|
||||||
window.clearInterval(this.plugin.autoRunIntervalID);
|
|
||||||
this.plugin.autoRunIntervalID = undefined;
|
|
||||||
} else if (
|
|
||||||
realVal !== undefined &&
|
|
||||||
realVal !== null &&
|
|
||||||
realVal > 0
|
|
||||||
) {
|
|
||||||
const intervalID = window.setInterval(() => {
|
|
||||||
this.plugin.syncRun("auto");
|
|
||||||
}, realVal);
|
|
||||||
this.plugin.autoRunIntervalID = intervalID;
|
|
||||||
this.plugin.registerInterval(intervalID);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const runOnceStartUpDiv = generalDiv.createEl("div");
|
|
||||||
new Setting(runOnceStartUpDiv)
|
|
||||||
.setName(t("settings_runoncestartup"))
|
|
||||||
.setDesc(t("settings_runoncestartup_desc"))
|
|
||||||
.addDropdown((dropdown) => {
|
|
||||||
dropdown.addOption("-1", t("settings_runoncestartup_notset"));
|
|
||||||
dropdown.addOption(
|
|
||||||
`${1000 * 10 * 1}`,
|
|
||||||
t("settings_runoncestartup_10sec")
|
|
||||||
);
|
|
||||||
dropdown.addOption(
|
|
||||||
`${1000 * 30 * 1}`,
|
|
||||||
t("settings_runoncestartup_30sec")
|
|
||||||
);
|
|
||||||
dropdown
|
|
||||||
.setValue(`${this.plugin.settings.initRunAfterMilliseconds}`)
|
|
||||||
.onChange(async (val: string) => {
|
|
||||||
const realVal = parseInt(val);
|
|
||||||
this.plugin.settings.initRunAfterMilliseconds = realVal;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
|
||||||
// below for general chooser (part 1/2)
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
|
||||||
// we need to create the div in advance of any other service divs
|
// we need to create the div in advance of any other service divs
|
||||||
const serviceChooserDiv = generalDiv.createEl("div");
|
const serviceChooserDiv = containerEl.createDiv();
|
||||||
|
serviceChooserDiv.createEl("h2", { text: t("settings_chooseservice") });
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// below for s3
|
// below for s3
|
||||||
@@ -777,27 +690,29 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
|
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
|
||||||
s3Div.createEl("h2", { text: t("settings_s3") });
|
s3Div.createEl("h2", { text: t("settings_s3") });
|
||||||
|
|
||||||
|
const s3LongDescDiv = s3Div.createEl("div", { cls: "settings-long-desc" });
|
||||||
|
|
||||||
for (const c of [
|
for (const c of [
|
||||||
t("settings_s3_disclaimer1"),
|
t("settings_s3_disclaimer1"),
|
||||||
t("settings_s3_disclaimer2"),
|
t("settings_s3_disclaimer2"),
|
||||||
]) {
|
]) {
|
||||||
s3Div.createEl("p", {
|
s3LongDescDiv.createEl("p", {
|
||||||
text: c,
|
text: c,
|
||||||
cls: "s3-disclaimer",
|
cls: "s3-disclaimer",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!VALID_REQURL) {
|
if (!VALID_REQURL) {
|
||||||
s3Div.createEl("p", {
|
s3LongDescDiv.createEl("p", {
|
||||||
text: t("settings_s3_cors"),
|
text: t("settings_s3_cors"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
s3Div.createEl("p", {
|
s3LongDescDiv.createEl("p", {
|
||||||
text: t("settings_s3_prod"),
|
text: t("settings_s3_prod"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const s3LinksUl = s3Div.createEl("div").createEl("ul");
|
const s3LinksUl = s3LongDescDiv.createEl("ul");
|
||||||
|
|
||||||
s3LinksUl.createEl("li").createEl("a", {
|
s3LinksUl.createEl("li").createEl("a", {
|
||||||
href: "https://docs.aws.amazon.com/general/latest/gr/s3.html",
|
href: "https://docs.aws.amazon.com/general/latest/gr/s3.html",
|
||||||
@@ -930,8 +845,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const partsConcurrencyDiv = s3Div.createEl("div");
|
new Setting(s3Div)
|
||||||
new Setting(partsConcurrencyDiv)
|
|
||||||
.setName(t("settings_s3_parts"))
|
.setName(t("settings_s3_parts"))
|
||||||
.setDesc(t("settings_s3_parts_desc"))
|
.setDesc(t("settings_s3_parts_desc"))
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
@@ -983,16 +897,20 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.serviceType !== "dropbox"
|
this.plugin.settings.serviceType !== "dropbox"
|
||||||
);
|
);
|
||||||
dropboxDiv.createEl("h2", { text: t("settings_dropbox") });
|
dropboxDiv.createEl("h2", { text: t("settings_dropbox") });
|
||||||
|
|
||||||
|
const dropboxLongDescDiv = dropboxDiv.createEl("div", {
|
||||||
|
cls: "settings-long-desc",
|
||||||
|
});
|
||||||
for (const c of [
|
for (const c of [
|
||||||
t("settings_dropbox_disclaimer1"),
|
t("settings_dropbox_disclaimer1"),
|
||||||
t("settings_dropbox_disclaimer2"),
|
t("settings_dropbox_disclaimer2"),
|
||||||
]) {
|
]) {
|
||||||
dropboxDiv.createEl("p", {
|
dropboxLongDescDiv.createEl("p", {
|
||||||
text: c,
|
text: c,
|
||||||
cls: "dropbox-disclaimer",
|
cls: "dropbox-disclaimer",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dropboxDiv.createEl("p", {
|
dropboxLongDescDiv.createEl("p", {
|
||||||
text: t("settings_dropbox_folder", {
|
text: t("settings_dropbox_folder", {
|
||||||
pluginID: this.plugin.manifest.id,
|
pluginID: this.plugin.manifest.id,
|
||||||
remoteBaseDir:
|
remoteBaseDir:
|
||||||
@@ -1003,10 +921,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const dropboxSelectAuthDiv = dropboxDiv.createDiv();
|
const dropboxSelectAuthDiv = dropboxDiv.createDiv();
|
||||||
const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({
|
const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({
|
||||||
cls: "dropbox-auth-button-hide",
|
cls: "dropbox-auth-button-hide settings-auth-related",
|
||||||
});
|
});
|
||||||
const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({
|
const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({
|
||||||
cls: "dropbox-revoke-auth-button-hide",
|
cls: "dropbox-revoke-auth-button-hide settings-auth-related",
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropboxRevokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
|
const dropboxRevokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
|
||||||
@@ -1169,17 +1087,20 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.serviceType !== "onedrive"
|
this.plugin.settings.serviceType !== "onedrive"
|
||||||
);
|
);
|
||||||
onedriveDiv.createEl("h2", { text: t("settings_onedrive") });
|
onedriveDiv.createEl("h2", { text: t("settings_onedrive") });
|
||||||
|
const onedriveLongDescDiv = onedriveDiv.createEl("div", {
|
||||||
|
cls: "settings-long-desc",
|
||||||
|
});
|
||||||
for (const c of [
|
for (const c of [
|
||||||
t("settings_onedrive_disclaimer1"),
|
t("settings_onedrive_disclaimer1"),
|
||||||
t("settings_onedrive_disclaimer2"),
|
t("settings_onedrive_disclaimer2"),
|
||||||
]) {
|
]) {
|
||||||
onedriveDiv.createEl("p", {
|
onedriveLongDescDiv.createEl("p", {
|
||||||
text: c,
|
text: c,
|
||||||
cls: "onedrive-disclaimer",
|
cls: "onedrive-disclaimer",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onedriveDiv.createEl("p", {
|
onedriveLongDescDiv.createEl("p", {
|
||||||
text: t("settings_onedrive_folder", {
|
text: t("settings_onedrive_folder", {
|
||||||
pluginID: this.plugin.manifest.id,
|
pluginID: this.plugin.manifest.id,
|
||||||
remoteBaseDir:
|
remoteBaseDir:
|
||||||
@@ -1188,16 +1109,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
onedriveDiv.createEl("p", {
|
onedriveLongDescDiv.createEl("p", {
|
||||||
text: t("settings_onedrive_nobiz"),
|
text: t("settings_onedrive_nobiz"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onedriveSelectAuthDiv = onedriveDiv.createDiv();
|
const onedriveSelectAuthDiv = onedriveDiv.createDiv();
|
||||||
const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({
|
const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({
|
||||||
cls: "onedrive-auth-button-hide",
|
cls: "onedrive-auth-button-hide settings-auth-related",
|
||||||
});
|
});
|
||||||
const onedriveRevokeAuthDiv = onedriveSelectAuthDiv.createDiv({
|
const onedriveRevokeAuthDiv = onedriveSelectAuthDiv.createDiv({
|
||||||
cls: "onedrive-revoke-auth-button-hide",
|
cls: "onedrive-revoke-auth-button-hide settings-auth-related",
|
||||||
});
|
});
|
||||||
|
|
||||||
const onedriveRevokeAuthSetting = new Setting(onedriveRevokeAuthDiv)
|
const onedriveRevokeAuthSetting = new Setting(onedriveRevokeAuthDiv)
|
||||||
@@ -1317,22 +1238,26 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
webdavDiv.createEl("h2", { text: t("settings_webdav") });
|
webdavDiv.createEl("h2", { text: t("settings_webdav") });
|
||||||
|
|
||||||
webdavDiv.createEl("p", {
|
const webdavLongDescDiv = webdavDiv.createEl("div", {
|
||||||
|
cls: "settings-long-desc",
|
||||||
|
});
|
||||||
|
|
||||||
|
webdavLongDescDiv.createEl("p", {
|
||||||
text: t("settings_webdav_disclaimer1"),
|
text: t("settings_webdav_disclaimer1"),
|
||||||
cls: "webdav-disclaimer",
|
cls: "webdav-disclaimer",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!VALID_REQURL) {
|
if (!VALID_REQURL) {
|
||||||
webdavDiv.createEl("p", {
|
webdavLongDescDiv.createEl("p", {
|
||||||
text: t("settings_webdav_cors_os"),
|
text: t("settings_webdav_cors_os"),
|
||||||
});
|
});
|
||||||
|
|
||||||
webdavDiv.createEl("p", {
|
webdavLongDescDiv.createEl("p", {
|
||||||
text: t("settings_webdav_cors"),
|
text: t("settings_webdav_cors"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
webdavDiv.createEl("p", {
|
webdavLongDescDiv.createEl("p", {
|
||||||
text: t("settings_webdav_folder", {
|
text: t("settings_webdav_folder", {
|
||||||
remoteBaseDir:
|
remoteBaseDir:
|
||||||
this.plugin.settings.webdav.remoteBaseDir || this.app.vault.getName(),
|
this.plugin.settings.webdav.remoteBaseDir || this.app.vault.getName(),
|
||||||
@@ -1564,6 +1489,113 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
// below for basic settings
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const basicDiv = containerEl.createEl("div");
|
||||||
|
basicDiv.createEl("h2", { text: t("settings_basic") });
|
||||||
|
|
||||||
|
let newPassword = `${this.plugin.settings.password}`;
|
||||||
|
new Setting(basicDiv)
|
||||||
|
.setName(t("settings_password"))
|
||||||
|
.setDesc(t("settings_password_desc"))
|
||||||
|
.addText((text) => {
|
||||||
|
wrapTextWithPasswordHide(text);
|
||||||
|
text
|
||||||
|
.setPlaceholder("")
|
||||||
|
.setValue(`${this.plugin.settings.password}`)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
newPassword = value.trim();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("confirm"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
new PasswordModal(this.app, this.plugin, newPassword).open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(basicDiv)
|
||||||
|
.setName(t("settings_autorun"))
|
||||||
|
.setDesc(t("settings_autorun_desc"))
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption("-1", t("settings_autorun_notset"));
|
||||||
|
dropdown.addOption(`${1000 * 60 * 1}`, t("settings_autorun_1min"));
|
||||||
|
dropdown.addOption(`${1000 * 60 * 5}`, t("settings_autorun_5min"));
|
||||||
|
dropdown.addOption(`${1000 * 60 * 10}`, t("settings_autorun_10min"));
|
||||||
|
dropdown.addOption(`${1000 * 60 * 30}`, t("settings_autorun_30min"));
|
||||||
|
|
||||||
|
dropdown
|
||||||
|
.setValue(`${this.plugin.settings.autoRunEveryMilliseconds}`)
|
||||||
|
.onChange(async (val: string) => {
|
||||||
|
const realVal = parseInt(val);
|
||||||
|
this.plugin.settings.autoRunEveryMilliseconds = realVal;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
if (
|
||||||
|
(realVal === undefined || realVal === null || realVal <= 0) &&
|
||||||
|
this.plugin.autoRunIntervalID !== undefined
|
||||||
|
) {
|
||||||
|
// clear
|
||||||
|
window.clearInterval(this.plugin.autoRunIntervalID);
|
||||||
|
this.plugin.autoRunIntervalID = undefined;
|
||||||
|
} else if (
|
||||||
|
realVal !== undefined &&
|
||||||
|
realVal !== null &&
|
||||||
|
realVal > 0
|
||||||
|
) {
|
||||||
|
const intervalID = window.setInterval(() => {
|
||||||
|
this.plugin.syncRun("auto");
|
||||||
|
}, realVal);
|
||||||
|
this.plugin.autoRunIntervalID = intervalID;
|
||||||
|
this.plugin.registerInterval(intervalID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(basicDiv)
|
||||||
|
.setName(t("settings_runoncestartup"))
|
||||||
|
.setDesc(t("settings_runoncestartup_desc"))
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption("-1", t("settings_runoncestartup_notset"));
|
||||||
|
dropdown.addOption(
|
||||||
|
`${1000 * 1 * 1}`,
|
||||||
|
t("settings_runoncestartup_1sec")
|
||||||
|
);
|
||||||
|
dropdown.addOption(
|
||||||
|
`${1000 * 10 * 1}`,
|
||||||
|
t("settings_runoncestartup_10sec")
|
||||||
|
);
|
||||||
|
dropdown.addOption(
|
||||||
|
`${1000 * 30 * 1}`,
|
||||||
|
t("settings_runoncestartup_30sec")
|
||||||
|
);
|
||||||
|
dropdown
|
||||||
|
.setValue(`${this.plugin.settings.initRunAfterMilliseconds}`)
|
||||||
|
.onChange(async (val: string) => {
|
||||||
|
const realVal = parseInt(val);
|
||||||
|
this.plugin.settings.initRunAfterMilliseconds = realVal;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new Setting(basicDiv)
|
||||||
|
.setName(t("settings_skiplargefiles"))
|
||||||
|
.setDesc(t("settings_skiplargefiles_desc"))
|
||||||
|
.addDropdown((dropdown) => {
|
||||||
|
dropdown.addOption("-1", t("settings_skiplargefiles_notset"));
|
||||||
|
|
||||||
|
const mbs = [1, 5, 10, 50, 100, 500, 1000];
|
||||||
|
for (const mb of mbs) {
|
||||||
|
dropdown.addOption(`${mb * 1000 * 1000}`, `${mb} MB`);
|
||||||
|
}
|
||||||
|
dropdown
|
||||||
|
.setValue(`${this.plugin.settings.skipSizeLargerThan}`)
|
||||||
|
.onChange(async (val) => {
|
||||||
|
this.plugin.settings.skipSizeLargerThan = parseInt(val);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// below for advanced settings
|
// below for advanced settings
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
@@ -1572,8 +1604,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
text: t("settings_adv"),
|
text: t("settings_adv"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const concurrencyDiv = advDiv.createEl("div");
|
new Setting(advDiv)
|
||||||
new Setting(concurrencyDiv)
|
|
||||||
.setName(t("settings_concurrency"))
|
.setName(t("settings_concurrency"))
|
||||||
.setDesc(t("settings_concurrency_desc"))
|
.setDesc(t("settings_concurrency_desc"))
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
@@ -1594,8 +1625,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncUnderscoreItemsDiv = advDiv.createEl("div");
|
new Setting(advDiv)
|
||||||
new Setting(syncUnderscoreItemsDiv)
|
|
||||||
.setName(t("settings_syncunderscore"))
|
.setName(t("settings_syncunderscore"))
|
||||||
.setDesc(t("settings_syncunderscore_desc"))
|
.setDesc(t("settings_syncunderscore_desc"))
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
@@ -1611,8 +1641,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncConfigDirDiv = advDiv.createEl("div");
|
new Setting(advDiv)
|
||||||
new Setting(syncConfigDirDiv)
|
|
||||||
.setName(t("settings_configdir"))
|
.setName(t("settings_configdir"))
|
||||||
.setDesc(
|
.setDesc(
|
||||||
t("settings_configdir_desc", {
|
t("settings_configdir_desc", {
|
||||||
@@ -1676,8 +1705,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
const debugDiv = containerEl.createEl("div");
|
const debugDiv = containerEl.createEl("div");
|
||||||
debugDiv.createEl("h2", { text: t("settings_debug") });
|
debugDiv.createEl("h2", { text: t("settings_debug") });
|
||||||
|
|
||||||
const setConsoleLogLevelDiv = debugDiv.createDiv("div");
|
new Setting(debugDiv)
|
||||||
new Setting(setConsoleLogLevelDiv)
|
|
||||||
.setName(t("settings_debuglevel"))
|
.setName(t("settings_debuglevel"))
|
||||||
.setDesc(t("settings_debuglevel_desc"))
|
.setDesc(t("settings_debuglevel_desc"))
|
||||||
.addDropdown(async (dropdown) => {
|
.addDropdown(async (dropdown) => {
|
||||||
@@ -1692,8 +1720,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
log.info(`the log level is changed to ${val}`);
|
log.info(`the log level is changed to ${val}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const outputCurrSettingsDiv = debugDiv.createDiv("div");
|
|
||||||
new Setting(outputCurrSettingsDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_outputsettingsconsole"))
|
.setName(t("settings_outputsettingsconsole"))
|
||||||
.setDesc(t("settings_outputsettingsconsole_desc"))
|
.setDesc(t("settings_outputsettingsconsole_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1704,8 +1732,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
new Notice(t("settings_outputsettingsconsole_notice"));
|
new Notice(t("settings_outputsettingsconsole_notice"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const syncPlanDiv = debugDiv.createEl("div");
|
|
||||||
new Setting(syncPlanDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_syncplans"))
|
.setName(t("settings_syncplans"))
|
||||||
.setDesc(t("settings_syncplans_desc"))
|
.setDesc(t("settings_syncplans_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1732,7 +1760,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
new Notice(t("settings_syncplans_notice"));
|
new Notice(t("settings_syncplans_notice"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
new Setting(syncPlanDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_delsyncplans"))
|
.setName(t("settings_delsyncplans"))
|
||||||
.setDesc(t("settings_delsyncplans_desc"))
|
.setDesc(t("settings_delsyncplans_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1743,8 +1771,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const logToDBDiv = debugDiv.createEl("div");
|
new Setting(debugDiv)
|
||||||
new Setting(logToDBDiv)
|
|
||||||
.setName(t("settings_logtodb"))
|
.setName(t("settings_logtodb"))
|
||||||
.setDesc(t("settings_logtodb_desc"))
|
.setDesc(t("settings_logtodb_desc"))
|
||||||
.addDropdown(async (dropdown) => {
|
.addDropdown(async (dropdown) => {
|
||||||
@@ -1771,7 +1798,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(logToDBDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_logtodbexport"))
|
.setName(t("settings_logtodbexport"))
|
||||||
.setDesc(
|
.setDesc(
|
||||||
t("settings_logtodbexport_desc", {
|
t("settings_logtodbexport_desc", {
|
||||||
@@ -1790,7 +1817,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(logToDBDiv)
|
new Setting(debugDiv)
|
||||||
.setName(t("settings_logtodbclear"))
|
.setName(t("settings_logtodbclear"))
|
||||||
.setDesc(t("settings_logtodbclear_desc"))
|
.setDesc(t("settings_logtodbclear_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1801,8 +1828,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncMappingDiv = debugDiv.createEl("div");
|
new Setting(debugDiv)
|
||||||
new Setting(syncMappingDiv)
|
|
||||||
.setName(t("settings_delsyncmap"))
|
.setName(t("settings_delsyncmap"))
|
||||||
.setDesc(t("settings_delsyncmap_desc"))
|
.setDesc(t("settings_delsyncmap_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1813,8 +1839,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputCurrBasePathVaultIDDiv = debugDiv.createDiv("div");
|
new Setting(debugDiv)
|
||||||
new Setting(outputCurrBasePathVaultIDDiv)
|
|
||||||
.setName(t("settings_outputbasepathvaultid"))
|
.setName(t("settings_outputbasepathvaultid"))
|
||||||
.setDesc(t("settings_outputbasepathvaultid_desc"))
|
.setDesc(t("settings_outputbasepathvaultid_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
@@ -1825,8 +1850,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbsResetDiv = debugDiv.createEl("div");
|
new Setting(debugDiv)
|
||||||
new Setting(dbsResetDiv)
|
|
||||||
.setName(t("settings_resetcache"))
|
.setName(t("settings_resetcache"))
|
||||||
.setDesc(t("settings_resetcache_desc"))
|
.setDesc(t("settings_resetcache_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
|
|||||||
+252
-17
@@ -7,17 +7,19 @@ import {
|
|||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import AggregateError from "aggregate-error";
|
import AggregateError from "aggregate-error";
|
||||||
import PQueue from "p-queue";
|
import PQueue from "p-queue";
|
||||||
import {
|
import type {
|
||||||
RemoteItem,
|
RemoteItem,
|
||||||
SUPPORTED_SERVICES_TYPE,
|
SyncTriggerSourceType,
|
||||||
DecisionType,
|
DecisionType,
|
||||||
FileOrFolderMixedState,
|
FileOrFolderMixedState,
|
||||||
API_VER_STAT_FOLDER,
|
SUPPORTED_SERVICES_TYPE,
|
||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
|
import { API_VER_STAT_FOLDER } from "./baseTypes";
|
||||||
import {
|
import {
|
||||||
decryptBase32ToString,
|
decryptBase32ToString,
|
||||||
decryptBase64urlToString,
|
decryptBase64urlToString,
|
||||||
encryptStringToBase64url,
|
encryptStringToBase64url,
|
||||||
|
getSizeFromOrigToEnc,
|
||||||
MAGIC_ENCRYPTED_PREFIX_BASE32,
|
MAGIC_ENCRYPTED_PREFIX_BASE32,
|
||||||
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
|
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
|
||||||
} from "./encrypt";
|
} from "./encrypt";
|
||||||
@@ -65,6 +67,7 @@ export type SyncStatusType =
|
|||||||
export interface SyncPlanType {
|
export interface SyncPlanType {
|
||||||
ts: number;
|
ts: number;
|
||||||
tsFmt?: string;
|
tsFmt?: string;
|
||||||
|
syncTriggerSource?: SyncTriggerSourceType;
|
||||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||||
mixedStates: Record<string, FileOrFolderMixedState>;
|
mixedStates: Record<string, FileOrFolderMixedState>;
|
||||||
}
|
}
|
||||||
@@ -215,22 +218,29 @@ export const parseRemoteItems = async (
|
|||||||
if (backwardMapping !== undefined) {
|
if (backwardMapping !== undefined) {
|
||||||
key = backwardMapping.localKey;
|
key = backwardMapping.localKey;
|
||||||
const mtimeRemote = backwardMapping.localMtime || entry.lastModified;
|
const mtimeRemote = backwardMapping.localMtime || entry.lastModified;
|
||||||
|
|
||||||
|
// the backwardMapping.localSize is the file BEFORE encryption
|
||||||
|
// we want to split two sizes for comparation later
|
||||||
|
|
||||||
r = {
|
r = {
|
||||||
key: key,
|
key: key,
|
||||||
existRemote: true,
|
existRemote: true,
|
||||||
mtimeRemote: mtimeRemote,
|
mtimeRemote: mtimeRemote,
|
||||||
mtimeRemoteFmt: unixTimeToStr(mtimeRemote),
|
mtimeRemoteFmt: unixTimeToStr(mtimeRemote),
|
||||||
sizeRemote: backwardMapping.localSize || entry.size,
|
sizeRemote: backwardMapping.localSize,
|
||||||
|
sizeRemoteEnc: password === "" ? undefined : entry.size,
|
||||||
remoteEncryptedKey: remoteEncryptedKey,
|
remoteEncryptedKey: remoteEncryptedKey,
|
||||||
changeRemoteMtimeUsingMapping: true,
|
changeRemoteMtimeUsingMapping: true,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
// do not have backwardMapping
|
||||||
r = {
|
r = {
|
||||||
key: key,
|
key: key,
|
||||||
existRemote: true,
|
existRemote: true,
|
||||||
mtimeRemote: entry.lastModified,
|
mtimeRemote: entry.lastModified,
|
||||||
mtimeRemoteFmt: unixTimeToStr(entry.lastModified),
|
mtimeRemoteFmt: unixTimeToStr(entry.lastModified),
|
||||||
sizeRemote: entry.size,
|
sizeRemote: password === "" ? entry.size : undefined,
|
||||||
|
sizeRemoteEnc: password === "" ? undefined : entry.size,
|
||||||
remoteEncryptedKey: remoteEncryptedKey,
|
remoteEncryptedKey: remoteEncryptedKey,
|
||||||
changeRemoteMtimeUsingMapping: false,
|
changeRemoteMtimeUsingMapping: false,
|
||||||
};
|
};
|
||||||
@@ -303,7 +313,8 @@ const ensembleMixedStates = async (
|
|||||||
localFileHistory: FileFolderHistoryRecord[],
|
localFileHistory: FileFolderHistoryRecord[],
|
||||||
syncConfigDir: boolean,
|
syncConfigDir: boolean,
|
||||||
configDir: string,
|
configDir: string,
|
||||||
syncUnderscoreItems: boolean
|
syncUnderscoreItems: boolean,
|
||||||
|
password: string
|
||||||
) => {
|
) => {
|
||||||
const results = {} as Record<string, FileOrFolderMixedState>;
|
const results = {} as Record<string, FileOrFolderMixedState>;
|
||||||
|
|
||||||
@@ -332,6 +343,8 @@ const ensembleMixedStates = async (
|
|||||||
mtimeLocal: mtimeLocal,
|
mtimeLocal: mtimeLocal,
|
||||||
mtimeLocalFmt: unixTimeToStr(mtimeLocal),
|
mtimeLocalFmt: unixTimeToStr(mtimeLocal),
|
||||||
sizeLocal: entry.stat.size,
|
sizeLocal: entry.stat.size,
|
||||||
|
sizeLocalEnc:
|
||||||
|
password === "" ? undefined : getSizeFromOrigToEnc(entry.stat.size),
|
||||||
};
|
};
|
||||||
} else if (entry instanceof TFolder) {
|
} else if (entry instanceof TFolder) {
|
||||||
key = `${entry.path}/`;
|
key = `${entry.path}/`;
|
||||||
@@ -341,6 +354,7 @@ const ensembleMixedStates = async (
|
|||||||
mtimeLocal: undefined,
|
mtimeLocal: undefined,
|
||||||
mtimeLocalFmt: undefined,
|
mtimeLocalFmt: undefined,
|
||||||
sizeLocal: 0,
|
sizeLocal: 0,
|
||||||
|
sizeLocalEnc: password === "" ? undefined : getSizeFromOrigToEnc(0),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw Error(`unexpected ${entry}`);
|
throw Error(`unexpected ${entry}`);
|
||||||
@@ -356,6 +370,7 @@ const ensembleMixedStates = async (
|
|||||||
results[key].mtimeLocal = r.mtimeLocal;
|
results[key].mtimeLocal = r.mtimeLocal;
|
||||||
results[key].mtimeLocalFmt = r.mtimeLocalFmt;
|
results[key].mtimeLocalFmt = r.mtimeLocalFmt;
|
||||||
results[key].sizeLocal = r.sizeLocal;
|
results[key].sizeLocal = r.sizeLocal;
|
||||||
|
results[key].sizeLocalEnc = r.sizeLocalEnc;
|
||||||
} else {
|
} else {
|
||||||
results[key] = r;
|
results[key] = r;
|
||||||
results[key].existRemote = false;
|
results[key].existRemote = false;
|
||||||
@@ -372,6 +387,8 @@ const ensembleMixedStates = async (
|
|||||||
mtimeLocal: mtimeLocal,
|
mtimeLocal: mtimeLocal,
|
||||||
mtimeLocalFmt: unixTimeToStr(mtimeLocal),
|
mtimeLocalFmt: unixTimeToStr(mtimeLocal),
|
||||||
sizeLocal: entry.size,
|
sizeLocal: entry.size,
|
||||||
|
sizeLocalEnc:
|
||||||
|
password === "" ? undefined : getSizeFromOrigToEnc(entry.size),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (results.hasOwnProperty(key)) {
|
if (results.hasOwnProperty(key)) {
|
||||||
@@ -380,6 +397,7 @@ const ensembleMixedStates = async (
|
|||||||
results[key].mtimeLocal = r.mtimeLocal;
|
results[key].mtimeLocal = r.mtimeLocal;
|
||||||
results[key].mtimeLocalFmt = r.mtimeLocalFmt;
|
results[key].mtimeLocalFmt = r.mtimeLocalFmt;
|
||||||
results[key].sizeLocal = r.sizeLocal;
|
results[key].sizeLocal = r.sizeLocal;
|
||||||
|
results[key].sizeLocalEnc = r.sizeLocalEnc;
|
||||||
} else {
|
} else {
|
||||||
results[key] = r;
|
results[key] = r;
|
||||||
results[key].existRemote = false;
|
results[key].existRemote = false;
|
||||||
@@ -482,6 +500,7 @@ const ensembleMixedStates = async (
|
|||||||
const assignOperationToFileInplace = (
|
const assignOperationToFileInplace = (
|
||||||
origRecord: FileOrFolderMixedState,
|
origRecord: FileOrFolderMixedState,
|
||||||
keptFolder: Set<string>,
|
keptFolder: Set<string>,
|
||||||
|
skipSizeLargerThan: number,
|
||||||
password: string = ""
|
password: string = ""
|
||||||
) => {
|
) => {
|
||||||
let r = origRecord;
|
let r = origRecord;
|
||||||
@@ -524,6 +543,18 @@ const assignOperationToFileInplace = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(r.existLocal && password !== "" && r.sizeLocalEnc === undefined) ||
|
||||||
|
(r.existRemote && password !== "" && r.sizeRemoteEnc === undefined)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Error: No encryption sizes: ${JSON.stringify(r, null, 2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeLocalComp = password === "" ? r.sizeLocal : r.sizeLocalEnc;
|
||||||
|
const sizeRemoteComp = password === "" ? r.sizeRemote : r.sizeRemoteEnc;
|
||||||
|
|
||||||
// 1. mtimeLocal
|
// 1. mtimeLocal
|
||||||
if (r.existLocal) {
|
if (r.existLocal) {
|
||||||
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
|
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
|
||||||
@@ -534,26 +565,79 @@ const assignOperationToFileInplace = (
|
|||||||
r.mtimeLocal >= deltimeLocal &&
|
r.mtimeLocal >= deltimeLocal &&
|
||||||
r.mtimeLocal >= deltimeRemote
|
r.mtimeLocal >= deltimeRemote
|
||||||
) {
|
) {
|
||||||
|
if (sizeLocalComp === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Error: no local size but has local mtime: ${JSON.stringify(
|
||||||
|
r,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (r.mtimeLocal === r.mtimeRemote) {
|
if (r.mtimeLocal === r.mtimeRemote) {
|
||||||
// mtime the same
|
// local and remote both exist and mtimes are the same
|
||||||
if (password === "") {
|
if (sizeLocalComp === sizeRemoteComp) {
|
||||||
// no password, we can also compare the sizes!
|
// do not need to consider skipSizeLargerThan in this case
|
||||||
if (r.sizeLocal === r.sizeRemote) {
|
|
||||||
r.decision = "skipUploading";
|
r.decision = "skipUploading";
|
||||||
r.decisionBranch = 1;
|
r.decisionBranch = 1;
|
||||||
} else {
|
} else {
|
||||||
|
if (skipSizeLargerThan <= 0) {
|
||||||
r.decision = "uploadLocalToRemote";
|
r.decision = "uploadLocalToRemote";
|
||||||
r.decisionBranch = 2;
|
r.decisionBranch = 2;
|
||||||
|
} else {
|
||||||
|
// limit the sizes
|
||||||
|
if (sizeLocalComp <= skipSizeLargerThan) {
|
||||||
|
if (sizeRemoteComp <= skipSizeLargerThan) {
|
||||||
|
r.decision = "uploadLocalToRemote";
|
||||||
|
r.decisionBranch = 18;
|
||||||
|
} else {
|
||||||
|
r.decision = "errorRemoteTooLargeConflictLocal";
|
||||||
|
r.decisionBranch = 19;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// we have password, then the sizes are always unequal
|
if (sizeRemoteComp <= skipSizeLargerThan) {
|
||||||
// we can only rely on mtime
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
r.decision = "skipUploading";
|
r.decisionBranch = 20;
|
||||||
r.decisionBranch = 3;
|
} else {
|
||||||
|
r.decision = "skipUploadingTooLarge";
|
||||||
|
r.decisionBranch = 21;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// we have local laregest mtime,
|
||||||
|
// and the remote not existing or smaller mtime
|
||||||
|
if (skipSizeLargerThan <= 0) {
|
||||||
|
// no need to consider sizes
|
||||||
r.decision = "uploadLocalToRemote";
|
r.decision = "uploadLocalToRemote";
|
||||||
r.decisionBranch = 4;
|
r.decisionBranch = 4;
|
||||||
|
} else {
|
||||||
|
// need to consider sizes
|
||||||
|
if (sizeLocalComp <= skipSizeLargerThan) {
|
||||||
|
if (sizeRemoteComp === undefined) {
|
||||||
|
r.decision = "uploadLocalToRemote";
|
||||||
|
r.decisionBranch = 22;
|
||||||
|
} else if (sizeRemoteComp <= skipSizeLargerThan) {
|
||||||
|
r.decision = "uploadLocalToRemote";
|
||||||
|
r.decisionBranch = 23;
|
||||||
|
} else {
|
||||||
|
r.decision = "errorRemoteTooLargeConflictLocal";
|
||||||
|
r.decisionBranch = 24;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sizeRemoteComp === undefined) {
|
||||||
|
r.decision = "skipUploadingTooLarge";
|
||||||
|
r.decisionBranch = 25;
|
||||||
|
} else if (sizeRemoteComp <= skipSizeLargerThan) {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 26;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipUploadingTooLarge";
|
||||||
|
r.decisionBranch = 27;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
keptFolder.add(getParentFolder(r.key));
|
keptFolder.add(getParentFolder(r.key));
|
||||||
return r;
|
return r;
|
||||||
@@ -570,8 +654,49 @@ const assignOperationToFileInplace = (
|
|||||||
r.mtimeRemote >= deltimeLocal &&
|
r.mtimeRemote >= deltimeLocal &&
|
||||||
r.mtimeRemote >= deltimeRemote
|
r.mtimeRemote >= deltimeRemote
|
||||||
) {
|
) {
|
||||||
|
// we have remote laregest mtime,
|
||||||
|
// and the local not existing or smaller mtime
|
||||||
|
if (sizeRemoteComp === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Error: no remote size but has remote mtime: ${JSON.stringify(
|
||||||
|
r,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipSizeLargerThan <= 0) {
|
||||||
|
// no need to consider sizes
|
||||||
r.decision = "downloadRemoteToLocal";
|
r.decision = "downloadRemoteToLocal";
|
||||||
r.decisionBranch = 5;
|
r.decisionBranch = 5;
|
||||||
|
} else {
|
||||||
|
// need to consider sizes
|
||||||
|
if (sizeRemoteComp <= skipSizeLargerThan) {
|
||||||
|
if (sizeLocalComp === undefined) {
|
||||||
|
r.decision = "downloadRemoteToLocal";
|
||||||
|
r.decisionBranch = 28;
|
||||||
|
} else if (sizeLocalComp <= skipSizeLargerThan) {
|
||||||
|
r.decision = "downloadRemoteToLocal";
|
||||||
|
r.decisionBranch = 29;
|
||||||
|
} else {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 30;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (sizeLocalComp === undefined) {
|
||||||
|
r.decision = "skipDownloadingTooLarge";
|
||||||
|
r.decisionBranch = 31;
|
||||||
|
} else if (sizeLocalComp <= skipSizeLargerThan) {
|
||||||
|
r.decision = "errorRemoteTooLargeConflictLocal";
|
||||||
|
r.decisionBranch = 32;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipDownloadingTooLarge";
|
||||||
|
r.decisionBranch = 33;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keptFolder.add(getParentFolder(r.key));
|
keptFolder.add(getParentFolder(r.key));
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
@@ -587,11 +712,45 @@ const assignOperationToFileInplace = (
|
|||||||
r.deltimeLocal >= mtimeRemote &&
|
r.deltimeLocal >= mtimeRemote &&
|
||||||
r.deltimeLocal >= deltimeRemote
|
r.deltimeLocal >= deltimeRemote
|
||||||
) {
|
) {
|
||||||
|
if (skipSizeLargerThan <= 0) {
|
||||||
r.decision = "uploadLocalDelHistToRemote";
|
r.decision = "uploadLocalDelHistToRemote";
|
||||||
r.decisionBranch = 6;
|
r.decisionBranch = 6;
|
||||||
if (r.existLocal || r.existRemote) {
|
if (r.existLocal || r.existRemote) {
|
||||||
// actual deletion would happen
|
// actual deletion would happen
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const localTooLargeToDelete =
|
||||||
|
r.existLocal && sizeLocalComp > skipSizeLargerThan;
|
||||||
|
const remoteTooLargeToDelete =
|
||||||
|
r.existRemote && sizeRemoteComp > skipSizeLargerThan;
|
||||||
|
if (localTooLargeToDelete) {
|
||||||
|
if (remoteTooLargeToDelete) {
|
||||||
|
r.decision = "skipUsingLocalDelTooLarge";
|
||||||
|
r.decisionBranch = 34;
|
||||||
|
} else {
|
||||||
|
if (r.existRemote) {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 35;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipUsingLocalDelTooLarge";
|
||||||
|
r.decisionBranch = 36;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (remoteTooLargeToDelete) {
|
||||||
|
if (r.existLocal) {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 37;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipUsingLocalDelTooLarge";
|
||||||
|
r.decisionBranch = 38;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.decision = "uploadLocalDelHistToRemote";
|
||||||
|
r.decisionBranch = 39;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,11 +765,45 @@ const assignOperationToFileInplace = (
|
|||||||
r.deltimeRemote >= mtimeRemote &&
|
r.deltimeRemote >= mtimeRemote &&
|
||||||
r.deltimeRemote >= deltimeLocal
|
r.deltimeRemote >= deltimeLocal
|
||||||
) {
|
) {
|
||||||
|
if (skipSizeLargerThan <= 0) {
|
||||||
r.decision = "keepRemoteDelHist";
|
r.decision = "keepRemoteDelHist";
|
||||||
r.decisionBranch = 7;
|
r.decisionBranch = 7;
|
||||||
if (r.existLocal || r.existRemote) {
|
if (r.existLocal || r.existRemote) {
|
||||||
// actual deletion would happen
|
// actual deletion would happen
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const localTooLargeToDelete =
|
||||||
|
r.existLocal && sizeLocalComp > skipSizeLargerThan;
|
||||||
|
const remoteTooLargeToDelete =
|
||||||
|
r.existRemote && sizeRemoteComp > skipSizeLargerThan;
|
||||||
|
if (localTooLargeToDelete) {
|
||||||
|
if (remoteTooLargeToDelete) {
|
||||||
|
r.decision = "skipUsingRemoteDelTooLarge";
|
||||||
|
r.decisionBranch = 40;
|
||||||
|
} else {
|
||||||
|
if (r.existRemote) {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 41;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipUsingRemoteDelTooLarge";
|
||||||
|
r.decisionBranch = 42;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (remoteTooLargeToDelete) {
|
||||||
|
if (r.existLocal) {
|
||||||
|
r.decision = "errorLocalTooLargeConflictRemote";
|
||||||
|
r.decisionBranch = 43;
|
||||||
|
} else {
|
||||||
|
r.decision = "skipUsingRemoteDelTooLarge";
|
||||||
|
r.decisionBranch = 44;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.decision = "keepRemoteDelHist";
|
||||||
|
r.decisionBranch = 45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -744,6 +937,10 @@ const DELETION_DECISIONS: Set<DecisionType> = new Set([
|
|||||||
"uploadLocalDelHistToRemoteFolder",
|
"uploadLocalDelHistToRemoteFolder",
|
||||||
"keepRemoteDelHistFolder",
|
"keepRemoteDelHistFolder",
|
||||||
]);
|
]);
|
||||||
|
const SIZES_GO_WRONG_DECISIONS: Set<DecisionType> = new Set([
|
||||||
|
"errorLocalTooLargeConflictRemote",
|
||||||
|
"errorRemoteTooLargeConflictLocal",
|
||||||
|
]);
|
||||||
|
|
||||||
export const getSyncPlan = async (
|
export const getSyncPlan = async (
|
||||||
remoteStates: FileOrFolderMixedState[],
|
remoteStates: FileOrFolderMixedState[],
|
||||||
@@ -752,10 +949,12 @@ export const getSyncPlan = async (
|
|||||||
remoteDeleteHistory: DeletionOnRemote[],
|
remoteDeleteHistory: DeletionOnRemote[],
|
||||||
localFileHistory: FileFolderHistoryRecord[],
|
localFileHistory: FileFolderHistoryRecord[],
|
||||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||||
|
triggerSource: SyncTriggerSourceType,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
syncConfigDir: boolean,
|
syncConfigDir: boolean,
|
||||||
configDir: string,
|
configDir: string,
|
||||||
syncUnderscoreItems: boolean,
|
syncUnderscoreItems: boolean,
|
||||||
|
skipSizeLargerThan: number,
|
||||||
password: string = ""
|
password: string = ""
|
||||||
) => {
|
) => {
|
||||||
const mixedStates = await ensembleMixedStates(
|
const mixedStates = await ensembleMixedStates(
|
||||||
@@ -766,13 +965,15 @@ export const getSyncPlan = async (
|
|||||||
localFileHistory,
|
localFileHistory,
|
||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
configDir,
|
configDir,
|
||||||
syncUnderscoreItems
|
syncUnderscoreItems,
|
||||||
|
password
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedKeys = Object.keys(mixedStates).sort(
|
const sortedKeys = Object.keys(mixedStates).sort(
|
||||||
(k1, k2) => k2.length - k1.length
|
(k1, k2) => k2.length - k1.length
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sizesGoWrong: FileOrFolderMixedState[] = [];
|
||||||
const deletions: DeletionOnRemote[] = [];
|
const deletions: DeletionOnRemote[] = [];
|
||||||
|
|
||||||
const keptFolder = new Set<string>();
|
const keptFolder = new Set<string>();
|
||||||
@@ -788,7 +989,16 @@ export const getSyncPlan = async (
|
|||||||
} else {
|
} else {
|
||||||
// get all operations of files
|
// get all operations of files
|
||||||
// and at the same time get some helper info for folders
|
// and at the same time get some helper info for folders
|
||||||
assignOperationToFileInplace(val, keptFolder, password);
|
assignOperationToFileInplace(
|
||||||
|
val,
|
||||||
|
keptFolder,
|
||||||
|
skipSizeLargerThan,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SIZES_GO_WRONG_DECISIONS.has(val.decision)) {
|
||||||
|
sizesGoWrong.push(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DELETION_DECISIONS.has(val.decision)) {
|
if (DELETION_DECISIONS.has(val.decision)) {
|
||||||
@@ -824,12 +1034,14 @@ export const getSyncPlan = async (
|
|||||||
ts: currTs,
|
ts: currTs,
|
||||||
tsFmt: currTsFmt,
|
tsFmt: currTsFmt,
|
||||||
remoteType: remoteType,
|
remoteType: remoteType,
|
||||||
|
syncTriggerSource: triggerSource,
|
||||||
mixedStates: mixedStates,
|
mixedStates: mixedStates,
|
||||||
} as SyncPlanType;
|
} as SyncPlanType;
|
||||||
return {
|
return {
|
||||||
plan: plan,
|
plan: plan,
|
||||||
sortedKeys: sortedKeys,
|
sortedKeys: sortedKeys,
|
||||||
deletions: deletions,
|
deletions: deletions,
|
||||||
|
sizesGoWrong: sizesGoWrong,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1011,6 +1223,14 @@ const dispatchOperationToActual = async (
|
|||||||
await clearDeleteRenameHistoryOfKeyAndVault(db, r.key, vaultRandomID);
|
await clearDeleteRenameHistoryOfKeyAndVault(db, r.key, vaultRandomID);
|
||||||
} else if (r.decision === "skipFolder") {
|
} else if (r.decision === "skipFolder") {
|
||||||
// do nothing!
|
// do nothing!
|
||||||
|
} else if (r.decision === "skipUploadingTooLarge") {
|
||||||
|
// do nothing!
|
||||||
|
} else if (r.decision === "skipDownloadingTooLarge") {
|
||||||
|
// do nothing!
|
||||||
|
} else if (r.decision === "skipUsingLocalDelTooLarge") {
|
||||||
|
// do nothing!
|
||||||
|
} else if (r.decision === "skipUsingRemoteDelTooLarge") {
|
||||||
|
// do nothing!
|
||||||
} else {
|
} else {
|
||||||
throw Error(`unknown decision in ${JSON.stringify(r)}`);
|
throw Error(`unknown decision in ${JSON.stringify(r)}`);
|
||||||
}
|
}
|
||||||
@@ -1029,7 +1249,14 @@ const splitThreeSteps = (syncPlan: SyncPlanType, sortedKeys: string[]) => {
|
|||||||
const key = sortedKeys[i];
|
const key = sortedKeys[i];
|
||||||
const val: FileOrFolderMixedState = Object.assign({}, mixedStates[key]); // copy to avoid issue
|
const val: FileOrFolderMixedState = Object.assign({}, mixedStates[key]); // copy to avoid issue
|
||||||
|
|
||||||
if (val.decision === "skipFolder" || val.decision === "skipUploading") {
|
if (
|
||||||
|
val.decision === "skipFolder" ||
|
||||||
|
val.decision === "skipUploading" ||
|
||||||
|
val.decision === "skipDownloadingTooLarge" ||
|
||||||
|
val.decision === "skipUploadingTooLarge" ||
|
||||||
|
val.decision === "skipUsingLocalDelTooLarge" ||
|
||||||
|
val.decision === "skipUsingRemoteDelTooLarge"
|
||||||
|
) {
|
||||||
// pass
|
// pass
|
||||||
} else if (val.decision === "createFolder") {
|
} else if (val.decision === "createFolder") {
|
||||||
const level = atWhichLevel(key);
|
const level = atWhichLevel(key);
|
||||||
@@ -1089,15 +1316,23 @@ export const doActualSync = async (
|
|||||||
sortedKeys: string[],
|
sortedKeys: string[],
|
||||||
metadataFile: FileOrFolderMixedState,
|
metadataFile: FileOrFolderMixedState,
|
||||||
origMetadata: MetadataOnRemote,
|
origMetadata: MetadataOnRemote,
|
||||||
|
sizesGoWrong: FileOrFolderMixedState[],
|
||||||
deletions: DeletionOnRemote[],
|
deletions: DeletionOnRemote[],
|
||||||
localDeleteFunc: any,
|
localDeleteFunc: any,
|
||||||
password: string = "",
|
password: string = "",
|
||||||
concurrency: number = 1,
|
concurrency: number = 1,
|
||||||
|
callbackSizesGoWrong?: any,
|
||||||
callbackSyncProcess?: any
|
callbackSyncProcess?: any
|
||||||
) => {
|
) => {
|
||||||
const mixedStates = syncPlan.mixedStates;
|
const mixedStates = syncPlan.mixedStates;
|
||||||
const totalCount = sortedKeys.length || 0;
|
const totalCount = sortedKeys.length || 0;
|
||||||
|
|
||||||
|
if (sizesGoWrong.length > 0) {
|
||||||
|
log.debug(`some sizes are larger than the threshold, abort and show hints`);
|
||||||
|
callbackSizesGoWrong(sizesGoWrong);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(`start syncing extra data firstly`);
|
log.debug(`start syncing extra data firstly`);
|
||||||
await uploadExtraMeta(
|
await uploadExtraMeta(
|
||||||
client,
|
client,
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||||
|
import type RemotelySavePlugin from "./main"; // unavoidable
|
||||||
|
import type { TransItemType } from "./i18n";
|
||||||
|
import type { FileOrFolderMixedState } from "./baseTypes";
|
||||||
|
|
||||||
|
import { log } from "./moreOnLog";
|
||||||
|
|
||||||
|
export class SizesConflictModal extends Modal {
|
||||||
|
readonly plugin: RemotelySavePlugin;
|
||||||
|
readonly skipSizeLargerThan: number;
|
||||||
|
readonly sizesGoWrong: FileOrFolderMixedState[];
|
||||||
|
readonly hasPassword: boolean;
|
||||||
|
constructor(
|
||||||
|
app: App,
|
||||||
|
plugin: RemotelySavePlugin,
|
||||||
|
skipSizeLargerThan: number,
|
||||||
|
sizesGoWrong: FileOrFolderMixedState[],
|
||||||
|
hasPassword: boolean
|
||||||
|
) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.skipSizeLargerThan = skipSizeLargerThan;
|
||||||
|
this.sizesGoWrong = sizesGoWrong;
|
||||||
|
this.hasPassword = hasPassword;
|
||||||
|
}
|
||||||
|
onOpen() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
const t = (x: TransItemType, vars?: any) => {
|
||||||
|
return this.plugin.i18n.t(x, vars);
|
||||||
|
};
|
||||||
|
|
||||||
|
contentEl.createEl("h2", {
|
||||||
|
text: t("modal_sizesconflict_title"),
|
||||||
|
});
|
||||||
|
|
||||||
|
t("modal_sizesconflict_desc", {
|
||||||
|
thresholdMB: `${this.skipSizeLargerThan / 1000 / 1000}`,
|
||||||
|
thresholdBytes: `${this.skipSizeLargerThan}`,
|
||||||
|
})
|
||||||
|
.split("\n")
|
||||||
|
.forEach((val) => {
|
||||||
|
contentEl.createEl("p", { text: val });
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = this.serialize();
|
||||||
|
|
||||||
|
contentEl.createDiv().createEl(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
text: t("modal_sizesconflict_copybutton"),
|
||||||
|
},
|
||||||
|
(el) => {
|
||||||
|
el.onclick = async () => {
|
||||||
|
await navigator.clipboard.writeText(info);
|
||||||
|
new Notice(t("modal_sizesconflict_copynotice"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
contentEl.createEl("pre", {
|
||||||
|
text: info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
return this.sizesGoWrong
|
||||||
|
.map((x) => {
|
||||||
|
return [
|
||||||
|
x.key,
|
||||||
|
this.hasPassword
|
||||||
|
? `encrypted name: ${x.remoteEncryptedKey}`
|
||||||
|
: undefined,
|
||||||
|
`local ${this.hasPassword ? "encrypted " : ""}bytes: ${
|
||||||
|
this.hasPassword ? x.sizeLocalEnc : x.sizeLocal
|
||||||
|
}`,
|
||||||
|
`remote ${this.hasPassword ? "encrypted " : ""}bytes: ${
|
||||||
|
this.hasPassword ? x.sizeRemoteEnc : x.sizeRemote
|
||||||
|
}`,
|
||||||
|
]
|
||||||
|
.filter((tmp) => tmp !== undefined)
|
||||||
|
.join("\n");
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,11 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-auth-related {
|
||||||
|
border-top: 1px solid var(--background-modifier-border);
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.s3-disclaimer {
|
.s3-disclaimer {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"0.3.2": "0.12.15",
|
"0.3.2": "0.12.15",
|
||||||
"0.3.19": "0.13.21"
|
"0.3.24": "0.13.21"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user