Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e5601a52 | ||
|
|
054a9a3c01 | ||
|
|
66dcaf4e4c | ||
|
|
6d29554033 | ||
|
|
4ed0d735a2 | ||
|
|
26dff02daa | ||
|
|
1b1083d746 | ||
|
|
6db005018d | ||
|
|
8d25ddb636 | ||
|
|
b0d41478a3 | ||
|
|
f69adf273a | ||
|
|
4b4a9b0989 | ||
|
|
bfe11f44dc | ||
|
|
d25303cfac | ||
|
|
ebd62b1581 | ||
|
|
13537d1481 | ||
|
|
6fce079481 | ||
|
|
baf4e6b369 | ||
|
|
dd39e229d8 | ||
|
|
ba6e8be304 | ||
|
|
cb7a372b69 | ||
|
|
9a4dd0186d | ||
|
|
f9cdf323b8 | ||
|
|
474216b9ab | ||
|
|
99f143115b | ||
|
|
39940deb3f | ||
|
|
df53ea957f | ||
|
|
0cf8023278 | ||
|
|
be1386b345 | ||
|
|
9a189d63c9 | ||
|
|
00d0c557df | ||
|
|
a70a53eeec | ||
|
|
8cec2b00eb | ||
|
|
6efe52b26e | ||
|
|
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:
|
||||
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
|
||||
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
|
||||
- **Unofficial** workaround: NextCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can install [WebAppPassword](https://apps.nextcloud.com/apps/webapppassword) app, and add `app://obsidian.md`, `capacitor://localhost`, `http://localhost` to `Allowed origins`
|
||||
- **Unofficial** workaround: OwnCloud users can **evaluate the risk by themselves**, and if decide to accept the risk, they can download `.tar.gz` of `WebAppPassword` above and manually install and configure it on their instances.
|
||||
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
|
||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
|
||||
@@ -0,0 +1,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)
|
||||
|
||||
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))
|
||||
|
||||
@@ -20,7 +20,7 @@ The list is for information purposes only.
|
||||
| [Nginx `ngx_http_dav_module`](http://nginx.org/en/docs/http/ngx_http_dav_module.html) | Yes? | | Yes? | | Yes? | ? |
|
||||
| NextCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| OwnCloud | Yes? | | Yes? | | Yes? | No CORS config by default. |
|
||||
| Seafile | Yes? | | Yes? | | Yes? | |
|
||||
| Seafile | Yes | | Yes | | Yes? | No CORS config by default. |
|
||||
| `rclone serve webdav` | Yes | | Yes | | Yes | No CORS support. |
|
||||
| [Nutstore 坚果云](https://www.jianguoyun.com/) | Yes | | Yes | | Yes | No CORS support. |
|
||||
| [TeraCLOUD](https://teracloud.jp/en/) | Yes | | Yes | | Yes | No CORS support. |
|
||||
|
||||
+4
-1
@@ -31,11 +31,11 @@ esbuild
|
||||
"obsidian",
|
||||
"electron",
|
||||
"fs",
|
||||
"crypto",
|
||||
"tls",
|
||||
"net",
|
||||
// ...builtins
|
||||
],
|
||||
inject: ["./esbuild.injecthelper.mjs"],
|
||||
format: "cjs",
|
||||
watch: !prod,
|
||||
target: "es2016",
|
||||
@@ -48,6 +48,9 @@ esbuild
|
||||
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
|
||||
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
|
||||
"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));
|
||||
|
||||
@@ -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",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.3.17",
|
||||
"version": "0.3.20",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+41
-39
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.3.17",
|
||||
"version": "0.3.20",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
@@ -23,71 +23,73 @@
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.11.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@microsoft/microsoft-graph-types": "^2.19.0",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/mustache": "^4.1.2",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/qrcode": "^1.4.1",
|
||||
"@types/node": "^17.0.30",
|
||||
"@types/qrcode": "^1.4.2",
|
||||
"builtin-modules": "^3.2.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.14.27",
|
||||
"dotenv": "^16.0.0",
|
||||
"esbuild": "^0.14.38",
|
||||
"jsdom": "^19.0.0",
|
||||
"mocha": "^9.1.3",
|
||||
"prettier": "^2.4.1",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"tslib": "^2.2.0",
|
||||
"typescript": "^4.4.4",
|
||||
"mocha": "^9.2.2",
|
||||
"prettier": "^2.6.2",
|
||||
"ts-loader": "^9.2.9",
|
||||
"ts-node": "^10.7.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.6.4",
|
||||
"webdav-server": "^2.6.2",
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.54.0",
|
||||
"@aws-sdk/fetch-http-handler": "^3.54.0",
|
||||
"@aws-sdk/lib-storage": "^3.54.0",
|
||||
"@aws-sdk/protocol-http": "^3.54.0",
|
||||
"@aws-sdk/querystring-builder": "^3.54.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.54.0",
|
||||
"@aws-sdk/types": "^3.54.0",
|
||||
"@azure/msal-node": "^1.6.0",
|
||||
"@aws-sdk/client-s3": "^3.81.0",
|
||||
"@aws-sdk/fetch-http-handler": "^3.78.0",
|
||||
"@aws-sdk/lib-storage": "^3.81.0",
|
||||
"@aws-sdk/protocol-http": "^3.78.0",
|
||||
"@aws-sdk/querystring-builder": "^3.78.0",
|
||||
"@aws-sdk/signature-v4-crt": "^3.78.0",
|
||||
"@aws-sdk/types": "^3.78.0",
|
||||
"@azure/msal-node": "^1.8.0",
|
||||
"@fyears/tsqueue": "^1.0.1",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
||||
"acorn": "^8.5.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.2",
|
||||
"acorn": "^8.7.1",
|
||||
"aggregate-error": "^4.0.0",
|
||||
"assert": "^2.0.0",
|
||||
"aws-crt": "^1.10.1",
|
||||
"aws-crt": "^1.12.1",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"dropbox": "^10.22.0",
|
||||
"delay": "^5.0.0",
|
||||
"dropbox": "^10.28.0",
|
||||
"emoji-regex": "^10.1.0",
|
||||
"http-status-codes": "^2.2.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"lucide": "^0.17.12",
|
||||
"mime-types": "^2.1.33",
|
||||
"lucide": "^0.35.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^3.1.30",
|
||||
"obsidian": "^0.13.26",
|
||||
"nanoid": "^3.3.3",
|
||||
"obsidian": "^0.14.6",
|
||||
"p-queue": "^7.2.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"qrcode": "^1.5.0",
|
||||
"rfc4648": "^1.5.0",
|
||||
"rfc4648": "^1.5.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.4",
|
||||
"webdav": "^4.7.0",
|
||||
"webdav-fs": "^4.0.0",
|
||||
"webdav": "^4.9.0",
|
||||
"webdav-fs": "^4.0.1",
|
||||
"xregexp": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -154,11 +154,16 @@ export interface FileOrFolderMixedState {
|
||||
}
|
||||
|
||||
export const API_VER_STAT_FOLDER = "0.13.27";
|
||||
export const API_VER_REQURL = "0.13.26";
|
||||
export const API_VER_REQURL = "0.13.26"; // desktop ver 0.13.26, iOS ver 1.1.1
|
||||
export const API_VER_REQURL_ANDROID = "0.14.6"; // Android ver 1.2.1
|
||||
|
||||
export const VALID_REQURL =
|
||||
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
|
||||
(!Platform.isAndroidApp && requireApiVersion(API_VER_REQURL)) ||
|
||||
(Platform.isAndroidApp && requireApiVersion(API_VER_REQURL_ANDROID));
|
||||
|
||||
export const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
||||
export const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX =
|
||||
"sync_plans_hist_exported_on_";
|
||||
export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
|
||||
|
||||
export type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
||||
|
||||
+67
-1
@@ -11,14 +11,73 @@ import {
|
||||
DEFAULT_DEBUG_FOLDER,
|
||||
DEFAULT_LOG_HISTORY_FILE_PREFIX,
|
||||
DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX,
|
||||
FileOrFolderMixedState,
|
||||
} from "./baseTypes";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const turnSyncPlanToTable = (record: string) => {
|
||||
const syncPlan: SyncPlanType = JSON.parse(record);
|
||||
const { ts, tsFmt, remoteType, mixedStates } = syncPlan;
|
||||
|
||||
type allowedHeadersType = keyof FileOrFolderMixedState;
|
||||
const headers: allowedHeadersType[] = [
|
||||
"key",
|
||||
"remoteEncryptedKey",
|
||||
"existLocal",
|
||||
"sizeLocal",
|
||||
"mtimeLocal",
|
||||
"deltimeLocal",
|
||||
"changeLocalMtimeUsingMapping",
|
||||
"existRemote",
|
||||
"sizeRemote",
|
||||
"mtimeRemote",
|
||||
"deltimeRemote",
|
||||
"changeRemoteMtimeUsingMapping",
|
||||
"decision",
|
||||
"decisionBranch",
|
||||
];
|
||||
|
||||
const lines = [
|
||||
`ts: ${ts}${tsFmt !== undefined ? " / " + tsFmt : ""}`,
|
||||
`remoteType: ${remoteType}`,
|
||||
`| ${headers.join(" | ")} |`,
|
||||
`| ${headers.map((x) => "---").join(" | ")} |`,
|
||||
];
|
||||
for (const [k1, v1] of Object.entries(syncPlan.mixedStates)) {
|
||||
const k = k1 as string;
|
||||
const v = v1 as FileOrFolderMixedState;
|
||||
const singleLine = [];
|
||||
for (const h of headers) {
|
||||
const field = v[h];
|
||||
if (field === undefined) {
|
||||
singleLine.push("");
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
h === "mtimeLocal" ||
|
||||
h === "deltimeLocal" ||
|
||||
h === "mtimeRemote" ||
|
||||
h === "deltimeRemote"
|
||||
) {
|
||||
const fmt = v[(h + "Fmt") as allowedHeadersType] as string;
|
||||
const s = `${field}${fmt !== undefined ? " / " + fmt : ""}`;
|
||||
singleLine.push(s);
|
||||
} else {
|
||||
singleLine.push(field);
|
||||
}
|
||||
}
|
||||
lines.push(`| ${singleLine.join(" | ")} |`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
export const exportVaultSyncPlansToFiles = async (
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
vaultRandomID: string
|
||||
vaultRandomID: string,
|
||||
toFormat: "table" | "json" = "json"
|
||||
) => {
|
||||
log.info("exporting");
|
||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||
@@ -27,9 +86,16 @@ export const exportVaultSyncPlansToFiles = async (
|
||||
if (records.length === 0) {
|
||||
md = "No sync plans history found";
|
||||
} else {
|
||||
if (toFormat === "json") {
|
||||
md =
|
||||
"Sync plans found:\n\n" +
|
||||
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
||||
} else if (toFormat === "table") {
|
||||
md =
|
||||
"Sync plans found:\n\n" + records.map(turnSyncPlanToTable).join("\n\n");
|
||||
} else {
|
||||
const _: never = toFormat;
|
||||
}
|
||||
}
|
||||
const ts = Date.now();
|
||||
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
||||
|
||||
+1
-1
Submodule src/langs updated: c46d302441...82d0619761
+63
-8
@@ -8,8 +8,11 @@ import {
|
||||
FileSystemAdapter,
|
||||
} from "obsidian";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { createElement, RotateCcw, RefreshCcw } from "lucide";
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
|
||||
import type {
|
||||
RemotelySavePluginSettings,
|
||||
SyncTriggerSourceType,
|
||||
} from "./baseTypes";
|
||||
import {
|
||||
COMMAND_CALLBACK,
|
||||
COMMAND_CALLBACK_ONEDRIVE,
|
||||
@@ -57,6 +60,10 @@ import { applyPresetRulesInplace } from "./presetRules";
|
||||
|
||||
import { applyLogWriterInplace, log } from "./moreOnLog";
|
||||
import AggregateError from "aggregate-error";
|
||||
import {
|
||||
exportVaultLoggerOutputToFiles,
|
||||
exportVaultSyncPlansToFiles,
|
||||
} from "./debugMode";
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: DEFAULT_S3_CONFIG,
|
||||
@@ -85,10 +92,9 @@ interface OAuth2Info {
|
||||
revokeAuthSetting?: Setting;
|
||||
}
|
||||
|
||||
type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
|
||||
|
||||
const iconNameSyncWait = `remotely-save-sync-wait`;
|
||||
const iconNameSyncRunning = `remotely-save-sync-running`;
|
||||
const iconNameLogs = `remotely-save-logs`;
|
||||
|
||||
const getIconSvg = () => {
|
||||
const iconSvgSyncWait = createElement(RotateCcw);
|
||||
@@ -97,13 +103,18 @@ const getIconSvg = () => {
|
||||
const iconSvgSyncRunning = createElement(RefreshCcw);
|
||||
iconSvgSyncRunning.setAttribute("width", "100");
|
||||
iconSvgSyncRunning.setAttribute("height", "100");
|
||||
const iconSvgLogs = createElement(FileText);
|
||||
iconSvgLogs.setAttribute("width", "100");
|
||||
iconSvgLogs.setAttribute("height", "100");
|
||||
const res = {
|
||||
iconSvgSyncWait: iconSvgSyncWait.outerHTML,
|
||||
iconSvgSyncRunning: iconSvgSyncRunning.outerHTML,
|
||||
iconSvgLogs: iconSvgLogs.outerHTML,
|
||||
};
|
||||
|
||||
iconSvgSyncWait.empty();
|
||||
iconSvgSyncRunning.empty();
|
||||
iconSvgLogs.empty();
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -204,7 +215,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
() => self.saveSettings()
|
||||
);
|
||||
const remoteRsp = await client.listFromRemote();
|
||||
log.debug(remoteRsp);
|
||||
// log.debug(remoteRsp);
|
||||
|
||||
getNotice(
|
||||
t("syncrun_step3", {
|
||||
@@ -276,6 +287,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
origMetadataOnRemote.deletions,
|
||||
localHistory,
|
||||
client.serviceType,
|
||||
triggerSource,
|
||||
this.app.vault,
|
||||
this.settings.syncConfigDir,
|
||||
this.app.vault.configDir,
|
||||
@@ -283,9 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.password
|
||||
);
|
||||
log.info(plan.mixedStates); // for debugging
|
||||
if (triggerSource !== "dry") {
|
||||
await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
|
||||
}
|
||||
|
||||
// The operations above are almost read only and kind of safe.
|
||||
// The operations below begins to write or delete (!!!) something.
|
||||
@@ -369,10 +379,11 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
async onload() {
|
||||
log.info(`loading plugin ${this.manifest.id}`);
|
||||
|
||||
const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg();
|
||||
const { iconSvgSyncWait, iconSvgSyncRunning, iconSvgLogs } = getIconSvg();
|
||||
|
||||
addIcon(iconNameSyncWait, iconSvgSyncWait);
|
||||
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
|
||||
addIcon(iconNameLogs, iconSvgLogs);
|
||||
|
||||
this.oauth2Info = {
|
||||
verifier: "",
|
||||
@@ -664,6 +675,50 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-sync-plans-json",
|
||||
name: t("command_exportsyncplans_json"),
|
||||
icon: iconNameLogs,
|
||||
callback: async () => {
|
||||
await exportVaultSyncPlansToFiles(
|
||||
this.db,
|
||||
this.app.vault,
|
||||
this.vaultRandomID,
|
||||
"json"
|
||||
);
|
||||
new Notice(t("settings_syncplans_notice"));
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-sync-plans-table",
|
||||
name: t("command_exportsyncplans_table"),
|
||||
icon: iconNameLogs,
|
||||
callback: async () => {
|
||||
await exportVaultSyncPlansToFiles(
|
||||
this.db,
|
||||
this.app.vault,
|
||||
this.vaultRandomID,
|
||||
"table"
|
||||
);
|
||||
new Notice(t("settings_syncplans_notice"));
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "export-logs-in-db",
|
||||
name: t("command_exportlogsindb"),
|
||||
icon: iconNameLogs,
|
||||
callback: async () => {
|
||||
await exportVaultLoggerOutputToFiles(
|
||||
this.db,
|
||||
this.app.vault,
|
||||
this.vaultRandomID
|
||||
);
|
||||
new Notice(t("settings_logtodbexport_notice"));
|
||||
},
|
||||
});
|
||||
|
||||
this.addSettingTab(new RemotelySaveSettingTab(this.app, this));
|
||||
|
||||
// this.registerDomEvent(document, "click", (evt: MouseEvent) => {
|
||||
|
||||
+34
-8
@@ -1,6 +1,7 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { base64url } from "rfc4648";
|
||||
import { reverseString } from "./misc";
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
const DEFAULT_README_FOR_METADATAONREMOTE =
|
||||
"Do NOT edit or delete the file manually. This file is for the plugin remotely-save to store some necessary meta data on the remote services. Its content is slightly obfuscated.";
|
||||
@@ -69,19 +70,44 @@ export const deserializeMetadataOnRemote = (x: string | ArrayBuffer) => {
|
||||
} else {
|
||||
y1 = new TextDecoder().decode(x);
|
||||
}
|
||||
const y2: any = JSON.parse(y1);
|
||||
|
||||
if (!("readme" in y2 && "d" in y2)) {
|
||||
throw Error("invalid remote meta data file!");
|
||||
let y2: any;
|
||||
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"]), {
|
||||
out: Buffer.allocUnsafe as any,
|
||||
loose: true,
|
||||
}) as Buffer
|
||||
).toString("utf-8")
|
||||
) as MetadataOnRemote;
|
||||
return y3;
|
||||
).toString("utf-8");
|
||||
} catch (e) {
|
||||
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;
|
||||
};
|
||||
|
||||
+29
@@ -3,6 +3,7 @@ import * as path from "path";
|
||||
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
import emojiRegex from "emoji-regex";
|
||||
|
||||
import { log } from "./moreOnLog";
|
||||
|
||||
@@ -162,6 +163,34 @@ export const isVaildText = (a: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use regex to detect a text contains emoji or not.
|
||||
* @param a
|
||||
* @returns
|
||||
*/
|
||||
export const hasEmojiInText = (a: string) => {
|
||||
const regex = emojiRegex();
|
||||
return regex.test(a);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the headers to a normal object.
|
||||
* @param h
|
||||
* @param toLower
|
||||
* @returns
|
||||
*/
|
||||
export const headersToRecord = (h: Headers, toLower: boolean = true) => {
|
||||
const res: Record<string, string> = {};
|
||||
h.forEach((v, k) => {
|
||||
if (toLower) {
|
||||
res[k.toLowerCase()] = v;
|
||||
} else {
|
||||
res[k] = v;
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* If input is already a folder, returns it as is;
|
||||
* And if input is a file, returns its direname.
|
||||
|
||||
+10
-8
@@ -15,7 +15,6 @@ import { log } from "./moreOnLog";
|
||||
|
||||
export class RemoteClient {
|
||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
readonly s3Client?: s3.S3Client;
|
||||
readonly s3Config?: S3Config;
|
||||
readonly webdavClient?: webdav.WrappedWebdavClient;
|
||||
readonly webdavConfig?: WebdavConfig;
|
||||
@@ -38,7 +37,6 @@ export class RemoteClient {
|
||||
// so we use a ref not copy of config here
|
||||
if (serviceType === "s3") {
|
||||
this.s3Config = s3Config;
|
||||
this.s3Client = s3.getS3Client(this.s3Config);
|
||||
} else if (serviceType === "webdav") {
|
||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||
throw Error(
|
||||
@@ -86,7 +84,7 @@ export class RemoteClient {
|
||||
getRemoteMeta = async (fileOrFolderPath: string) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.getRemoteMeta(
|
||||
this.s3Client,
|
||||
s3.getS3Client(this.s3Config),
|
||||
this.s3Config,
|
||||
fileOrFolderPath
|
||||
);
|
||||
@@ -116,7 +114,7 @@ export class RemoteClient {
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.uploadToRemote(
|
||||
this.s3Client,
|
||||
s3.getS3Client(this.s3Config),
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
@@ -168,7 +166,11 @@ export class RemoteClient {
|
||||
|
||||
listFromRemote = async (prefix?: string) => {
|
||||
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") {
|
||||
return await webdav.listFromRemote(this.webdavClient, prefix);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
@@ -190,7 +192,7 @@ export class RemoteClient {
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.downloadFromRemote(
|
||||
this.s3Client,
|
||||
s3.getS3Client(this.s3Config),
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
@@ -241,7 +243,7 @@ export class RemoteClient {
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.deleteFromRemote(
|
||||
this.s3Client,
|
||||
s3.getS3Client(this.s3Config),
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
password,
|
||||
@@ -276,7 +278,7 @@ export class RemoteClient {
|
||||
checkConnectivity = async (callbackFunc?: any) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.checkConnectivity(
|
||||
this.s3Client,
|
||||
s3.getS3Client(this.s3Config),
|
||||
this.s3Config,
|
||||
callbackFunc
|
||||
);
|
||||
|
||||
+135
-17
@@ -1,4 +1,6 @@
|
||||
import { Dropbox, DropboxAuth, files } from "dropbox";
|
||||
import delay from "delay";
|
||||
import { Dropbox, DropboxAuth } from "dropbox";
|
||||
import type { files, DropboxResponseError, DropboxResponse } from "dropbox";
|
||||
import { Vault } from "obsidian";
|
||||
import * as path from "path";
|
||||
import {
|
||||
@@ -8,7 +10,13 @@ import {
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
} from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import { bufferToArrayBuffer, getFolderLevels, mkdirpInVault } from "./misc";
|
||||
import {
|
||||
bufferToArrayBuffer,
|
||||
getFolderLevels,
|
||||
hasEmojiInText,
|
||||
headersToRecord,
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
|
||||
export { Dropbox } from "dropbox";
|
||||
|
||||
@@ -270,6 +278,73 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
// Other usual common methods
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
interface ErrSubType {
|
||||
error: {
|
||||
retry_after: number;
|
||||
};
|
||||
}
|
||||
|
||||
async function retryReq<T>(
|
||||
reqFunc: () => Promise<DropboxResponse<T>>,
|
||||
extraHint: string = ""
|
||||
): Promise<DropboxResponse<T>> {
|
||||
const waitSeconds = [1, 2, 4, 8]; // hard code exponential backoff
|
||||
for (let idx = 0; idx < waitSeconds.length; ++idx) {
|
||||
try {
|
||||
if (idx !== 0) {
|
||||
log.warn(
|
||||
`${extraHint === "" ? "" : extraHint + ": "}The ${
|
||||
idx + 1
|
||||
}-th try starts at time ${Date.now()}`
|
||||
);
|
||||
}
|
||||
return await reqFunc();
|
||||
} catch (e: unknown) {
|
||||
const err = e as DropboxResponseError<ErrSubType>;
|
||||
if (err.status === undefined) {
|
||||
// then the err is not DropboxResponseError
|
||||
throw err;
|
||||
}
|
||||
if (err.status !== 429) {
|
||||
// then the err is not "too many requests", give up
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (idx === waitSeconds.length - 1) {
|
||||
// the last retry also failed, give up
|
||||
throw new Error(
|
||||
`${
|
||||
extraHint === "" ? "" : extraHint + ": "
|
||||
}"429 too many requests", after retrying for ${
|
||||
idx + 1
|
||||
} times still failed.`
|
||||
);
|
||||
}
|
||||
|
||||
const headers = headersToRecord(err.headers);
|
||||
const svrSec =
|
||||
err.error.error.retry_after ||
|
||||
parseInt(headers["retry-after"] || "1") ||
|
||||
1;
|
||||
const fallbackSec = waitSeconds[idx];
|
||||
const secMin = Math.max(svrSec, fallbackSec);
|
||||
const secMax = Math.max(secMin * 1.8, 2);
|
||||
log.warn(
|
||||
`${
|
||||
extraHint === "" ? "" : extraHint + ": "
|
||||
}We have "429 too many requests" error of ${
|
||||
idx + 1
|
||||
}-th try, at time ${Date.now()}, and wait for ${secMin} ~ ${secMax} seconds to retry. Original info: ${JSON.stringify(
|
||||
err.error,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
await delay.range(secMin * 1000, secMax * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WrappedDropboxClient {
|
||||
dropboxConfig: DropboxConfig;
|
||||
remoteBaseDir: string;
|
||||
@@ -338,6 +413,13 @@ export class WrappedDropboxClient {
|
||||
}
|
||||
if (!this.vaultFolderExists) {
|
||||
log.info(`remote does not have folder /${this.remoteBaseDir}`);
|
||||
|
||||
if (hasEmojiInText(`/${this.remoteBaseDir}`)) {
|
||||
throw new Error(
|
||||
`/${this.remoteBaseDir}: Error: Dropbox does not support emoji in folder names.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.dropbox.filesCreateFolderV2({
|
||||
path: `/${this.remoteBaseDir}`,
|
||||
});
|
||||
@@ -377,10 +459,12 @@ export const getRemoteMeta = async (
|
||||
// filesGetMetadata doesn't support root folder
|
||||
// we instead try to list files
|
||||
// if no error occurs, we ensemble a fake result.
|
||||
const rsp = await client.dropbox.filesListFolder({
|
||||
const rsp = await retryReq(() =>
|
||||
client.dropbox.filesListFolder({
|
||||
path: `/${client.remoteBaseDir}`,
|
||||
recursive: false, // don't need to recursive here
|
||||
});
|
||||
})
|
||||
);
|
||||
if (rsp.status !== 200) {
|
||||
throw Error(JSON.stringify(rsp));
|
||||
}
|
||||
@@ -395,9 +479,11 @@ export const getRemoteMeta = async (
|
||||
|
||||
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
|
||||
|
||||
const rsp = await client.dropbox.filesGetMetadata({
|
||||
const rsp = await retryReq(() =>
|
||||
client.dropbox.filesGetMetadata({
|
||||
path: key,
|
||||
});
|
||||
})
|
||||
);
|
||||
if (rsp.status !== 200) {
|
||||
throw Error(JSON.stringify(rsp));
|
||||
}
|
||||
@@ -423,6 +509,12 @@ export const uploadToRemote = async (
|
||||
}
|
||||
uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir);
|
||||
|
||||
if (hasEmojiInText(uploadFile)) {
|
||||
throw new Error(
|
||||
`${uploadFile}: Error: Dropbox does not support emoji in file / folder names.`
|
||||
);
|
||||
}
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
if (isFolder && isRecursively) {
|
||||
@@ -438,11 +530,19 @@ export const uploadToRemote = async (
|
||||
// created, pass
|
||||
} else {
|
||||
try {
|
||||
await client.dropbox.filesCreateFolderV2({
|
||||
await retryReq(
|
||||
() =>
|
||||
client.dropbox.filesCreateFolderV2({
|
||||
path: uploadFile,
|
||||
});
|
||||
}),
|
||||
fileOrFolderPath
|
||||
);
|
||||
foldersCreatedBefore?.add(uploadFile);
|
||||
} catch (err) {
|
||||
} catch (e: unknown) {
|
||||
const err = e as DropboxResponseError<files.CreateFolderError>;
|
||||
if (err.status === undefined) {
|
||||
throw err;
|
||||
}
|
||||
if (err.status === 409) {
|
||||
// pass
|
||||
foldersCreatedBefore?.add(uploadFile);
|
||||
@@ -455,10 +555,14 @@ export const uploadToRemote = async (
|
||||
return res;
|
||||
} else {
|
||||
// if encrypted, upload a fake file with the encrypted file name
|
||||
await client.dropbox.filesUpload({
|
||||
await retryReq(
|
||||
() =>
|
||||
client.dropbox.filesUpload({
|
||||
path: uploadFile,
|
||||
contents: "",
|
||||
});
|
||||
}),
|
||||
fileOrFolderPath
|
||||
);
|
||||
return await getRemoteMeta(client, uploadFile);
|
||||
}
|
||||
} else {
|
||||
@@ -480,13 +584,19 @@ export const uploadToRemote = async (
|
||||
}
|
||||
// in dropbox, we don't need to create folders before uploading! cool!
|
||||
// TODO: filesUploadSession for larger files (>=150 MB)
|
||||
await client.dropbox.filesUpload({
|
||||
|
||||
await retryReq(
|
||||
() =>
|
||||
client.dropbox.filesUpload({
|
||||
path: uploadFile,
|
||||
contents: remoteContent,
|
||||
mode: {
|
||||
".tag": "overwrite",
|
||||
},
|
||||
});
|
||||
}),
|
||||
fileOrFolderPath
|
||||
);
|
||||
|
||||
// we want to mark that parent folders are created
|
||||
if (foldersCreatedBefore !== undefined) {
|
||||
const dirs = getFolderLevels(uploadFile).map((x) =>
|
||||
@@ -554,9 +664,13 @@ const downloadFromRemoteRaw = async (
|
||||
) => {
|
||||
await client.init();
|
||||
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
|
||||
const rsp = await client.dropbox.filesDownload({
|
||||
const rsp = await retryReq(
|
||||
() =>
|
||||
client.dropbox.filesDownload({
|
||||
path: key,
|
||||
});
|
||||
}),
|
||||
fileOrFolderPath
|
||||
);
|
||||
if ((rsp.result as any).fileBlob !== undefined) {
|
||||
// we get a Blob
|
||||
const content = (rsp.result as any).fileBlob as Blob;
|
||||
@@ -631,9 +745,13 @@ export const deleteFromRemote = async (
|
||||
|
||||
await client.init();
|
||||
try {
|
||||
await client.dropbox.filesDeleteV2({
|
||||
await retryReq(
|
||||
() =>
|
||||
client.dropbox.filesDeleteV2({
|
||||
path: remoteFileName,
|
||||
});
|
||||
}),
|
||||
fileOrFolderPath
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("some error while deleting");
|
||||
console.error(err);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { request, requestUrl, requireApiVersion, Vault } from "obsidian";
|
||||
import {
|
||||
API_VER_REQURL,
|
||||
VALID_REQURL,
|
||||
COMMAND_CALLBACK_ONEDRIVE,
|
||||
DEFAULT_CONTENT_TYPE,
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
@@ -400,7 +400,7 @@ export class WrappedOnedriveClient {
|
||||
// log.info(`already checked, /${this.remoteBaseDir} exist before`)
|
||||
} else {
|
||||
const k = await this.getJson("/drive/special/approot/children");
|
||||
log.debug(k);
|
||||
// log.debug(k);
|
||||
this.vaultFolderExists =
|
||||
(k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir)
|
||||
.length > 0;
|
||||
@@ -484,7 +484,7 @@ export class WrappedOnedriveClient {
|
||||
deleteJson = async (pathFragOrig: string) => {
|
||||
const theUrl = this.buildUrl(pathFragOrig);
|
||||
log.debug(`deleteJson, theUrl=${theUrl}`);
|
||||
if (requireApiVersion(API_VER_REQURL)) {
|
||||
if (VALID_REQURL) {
|
||||
await requestUrl({
|
||||
url: theUrl,
|
||||
method: "DELETE",
|
||||
@@ -508,7 +508,7 @@ export class WrappedOnedriveClient {
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
if (false /*requireApiVersion(API_VER_REQURL)*/) {
|
||||
if (false /*VALID_REQURL*/) {
|
||||
await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
@@ -556,7 +556,7 @@ export class WrappedOnedriveClient {
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
if (false /*requireApiVersion(API_VER_REQURL)*/) {
|
||||
if (false /*VALID_REQURL*/) {
|
||||
const res = await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
@@ -617,6 +617,7 @@ export const listFromRemote = async (
|
||||
`/drive/special/approot:/${client.remoteBaseDir}:/delta`
|
||||
);
|
||||
let driveItems = res.value as DriveItem[];
|
||||
// log.debug(driveItems);
|
||||
|
||||
while (NEXT_LINK_KEY in res) {
|
||||
res = await client.getJson(res[NEXT_LINK_KEY]);
|
||||
@@ -807,7 +808,7 @@ const downloadFromRemoteRaw = async (
|
||||
`${key}?$select=@microsoft.graph.downloadUrl`
|
||||
);
|
||||
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
||||
if (requireApiVersion(API_VER_REQURL)) {
|
||||
if (VALID_REQURL) {
|
||||
const content = (await requestUrl({ url: downloadUrl })).arrayBuffer;
|
||||
return content;
|
||||
} else {
|
||||
|
||||
+19
-6
@@ -83,17 +83,25 @@ class ObsHttpHandler extends FetchHttpHandler {
|
||||
const body =
|
||||
method === "GET" || method === "HEAD" ? undefined : request.body;
|
||||
|
||||
const transformedHeaders = { ...request.headers };
|
||||
delete transformedHeaders["host"];
|
||||
delete transformedHeaders["Host"];
|
||||
delete transformedHeaders["content-length"];
|
||||
delete transformedHeaders["Content-Length"];
|
||||
const transformedHeaders: Record<string, string> = {};
|
||||
for (const key of Object.keys(request.headers)) {
|
||||
const keyLower = key.toLowerCase();
|
||||
if (keyLower === "host" || keyLower === "content-length") {
|
||||
continue;
|
||||
}
|
||||
transformedHeaders[keyLower] = request.headers[key];
|
||||
}
|
||||
|
||||
let contentType: string = undefined;
|
||||
if (transformedHeaders["content-type"] !== undefined) {
|
||||
contentType = transformedHeaders["content-type"];
|
||||
}
|
||||
|
||||
if (transformedHeaders["cache-control"] === undefined) {
|
||||
// every time is a new request
|
||||
transformedHeaders["cache-control"] = "no-cache";
|
||||
}
|
||||
|
||||
let transformedBody: any = body;
|
||||
if (ArrayBuffer.isView(body)) {
|
||||
transformedBody = bufferToArrayBuffer(body);
|
||||
@@ -109,6 +117,11 @@ class ObsHttpHandler extends FetchHttpHandler {
|
||||
|
||||
const raceOfPromises = [
|
||||
requestUrl(param).then((rsp) => {
|
||||
const headers = rsp.headers;
|
||||
const headersLower: Record<string, string> = {};
|
||||
for (const key of Object.keys(headers)) {
|
||||
headersLower[key.toLowerCase()] = headers[key];
|
||||
}
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array(rsp.arrayBuffer));
|
||||
@@ -117,7 +130,7 @@ class ObsHttpHandler extends FetchHttpHandler {
|
||||
});
|
||||
return {
|
||||
response: new HttpResponse({
|
||||
headers: rsp.headers,
|
||||
headers: headersLower,
|
||||
statusCode: rsp.status,
|
||||
body: stream,
|
||||
}),
|
||||
|
||||
+66
-35
@@ -7,6 +7,8 @@ import {
|
||||
Platform,
|
||||
requireApiVersion,
|
||||
} from "obsidian";
|
||||
import type { TextComponent } from "obsidian";
|
||||
import { createElement, Eye, EyeOff } from "lucide";
|
||||
import {
|
||||
API_VER_REQURL,
|
||||
DEFAULT_DEBUG_FOLDER,
|
||||
@@ -627,8 +629,33 @@ class ExportSettingsQrCodeModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
const getEyesElements = () => {
|
||||
const eyeEl = createElement(Eye);
|
||||
const eyeOffEl = createElement(EyeOff);
|
||||
return {
|
||||
eye: eyeEl.outerHTML,
|
||||
eyeOff: eyeOffEl.outerHTML,
|
||||
};
|
||||
};
|
||||
|
||||
const wrapTextWithPasswordHide = (text: TextComponent) => {
|
||||
const { eye, eyeOff } = getEyesElements();
|
||||
const hider = text.inputEl.insertAdjacentElement("afterend", createSpan());
|
||||
// the init type of hider is "hidden" === eyeOff === password
|
||||
hider.innerHTML = eyeOff;
|
||||
hider.addEventListener("click", (e) => {
|
||||
const isText = text.inputEl.getAttribute("type") === "text";
|
||||
text.inputEl.setAttribute("type", isText ? "password" : "text");
|
||||
hider.innerHTML = isText ? eyeOff : eye;
|
||||
});
|
||||
|
||||
// the init type of text el is password
|
||||
text.inputEl.setAttribute("type", "password");
|
||||
return text;
|
||||
};
|
||||
|
||||
export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
plugin: RemotelySavePlugin;
|
||||
readonly plugin: RemotelySavePlugin;
|
||||
|
||||
constructor(app: App, plugin: RemotelySavePlugin) {
|
||||
super(app, plugin);
|
||||
@@ -658,14 +685,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
new Setting(passwordDiv)
|
||||
.setName(t("settings_password"))
|
||||
.setDesc(t("settings_password_desc"))
|
||||
.addText((text) =>
|
||||
.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 () => {
|
||||
@@ -816,29 +844,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
|
||||
new Setting(s3Div)
|
||||
.setName(t("settings_s3_accesskeyid"))
|
||||
.setDesc(t("settings_s3_accesskeyid"))
|
||||
.addText((text) =>
|
||||
.setDesc(t("settings_s3_accesskeyid_desc"))
|
||||
.addText((text) => {
|
||||
wrapTextWithPasswordHide(text);
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(`${this.plugin.settings.s3.s3AccessKeyID}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.s3.s3AccessKeyID = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(s3Div)
|
||||
.setName(t("settings_s3_secretaccesskey"))
|
||||
.setDesc(t("settings_s3_secretaccesskey"))
|
||||
.addText((text) =>
|
||||
.setDesc(t("settings_s3_secretaccesskey_desc"))
|
||||
.addText((text) => {
|
||||
wrapTextWithPasswordHide(text);
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(`${this.plugin.settings.s3.s3SecretAccessKey}`)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.s3.s3SecretAccessKey = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(s3Div)
|
||||
.setName(t("settings_s3_bucketname"))
|
||||
@@ -877,11 +907,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
if (VALID_REQURL) {
|
||||
new Setting(s3Div)
|
||||
.setName(t("settings_s3_bypasscorslocally"))
|
||||
.setDesc(
|
||||
t("settings_s3_bypasscorslocally_desc", {
|
||||
ver: API_VER_REQURL,
|
||||
})
|
||||
)
|
||||
.setDesc(t("settings_s3_bypasscorslocally_desc"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("disable", t("disable"))
|
||||
@@ -1297,15 +1323,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
if (!VALID_REQURL) {
|
||||
if (Platform.isAndroidApp) {
|
||||
webdavDiv.createEl("p", {
|
||||
text: t("settings_webdav_cors_android"),
|
||||
text: t("settings_webdav_cors_os"),
|
||||
});
|
||||
} else {
|
||||
webdavDiv.createEl("p", {
|
||||
text: t("settings_webdav_cors_otheros"),
|
||||
});
|
||||
}
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
text: t("settings_webdav_cors"),
|
||||
@@ -1346,7 +1366,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
new Setting(webdavDiv)
|
||||
.setName(t("settings_webdav_user"))
|
||||
.setDesc(t("settings_webdav_user_desc"))
|
||||
.addText((text) =>
|
||||
.addText((text) => {
|
||||
wrapTextWithPasswordHide(text);
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.webdav.username)
|
||||
@@ -1359,13 +1380,14 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.webdav.depth = "auto_unknown";
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName(t("settings_webdav_password"))
|
||||
.setDesc(t("settings_webdav_password_desc"))
|
||||
.addText((text) =>
|
||||
.addText((text) => {
|
||||
wrapTextWithPasswordHide(text);
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.webdav.password)
|
||||
@@ -1378,8 +1400,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.webdav.depth = "auto_unknown";
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName(t("settings_webdav_auth"))
|
||||
@@ -1678,11 +1700,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
button.setButtonText(t("settings_outputsettingsconsole_button"));
|
||||
button.onClick(async () => {
|
||||
const c = messyConfigToNormal(await this.plugin.loadData());
|
||||
if (c.currLogLevel === "debug") {
|
||||
// no need to ouput it again because debug mode already output it
|
||||
} else {
|
||||
log.info(c);
|
||||
}
|
||||
new Notice(t("settings_outputsettingsconsole_notice"));
|
||||
});
|
||||
});
|
||||
@@ -1691,12 +1709,25 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
.setName(t("settings_syncplans"))
|
||||
.setDesc(t("settings_syncplans_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_syncplans_button"));
|
||||
button.setButtonText(t("settings_syncplans_button_json"));
|
||||
button.onClick(async () => {
|
||||
await exportVaultSyncPlansToFiles(
|
||||
this.plugin.db,
|
||||
this.app.vault,
|
||||
this.plugin.vaultRandomID
|
||||
this.plugin.vaultRandomID,
|
||||
"json"
|
||||
);
|
||||
new Notice(t("settings_syncplans_notice"));
|
||||
});
|
||||
})
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_syncplans_button_table"));
|
||||
button.onClick(async () => {
|
||||
await exportVaultSyncPlansToFiles(
|
||||
this.plugin.db,
|
||||
this.app.vault,
|
||||
this.plugin.vaultRandomID,
|
||||
"table"
|
||||
);
|
||||
new Notice(t("settings_syncplans_notice"));
|
||||
});
|
||||
|
||||
+16
-6
@@ -7,13 +7,14 @@ import {
|
||||
} from "obsidian";
|
||||
import AggregateError from "aggregate-error";
|
||||
import PQueue from "p-queue";
|
||||
import {
|
||||
import type {
|
||||
RemoteItem,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
SyncTriggerSourceType,
|
||||
DecisionType,
|
||||
FileOrFolderMixedState,
|
||||
API_VER_STAT_FOLDER,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
} from "./baseTypes";
|
||||
import { API_VER_STAT_FOLDER } from "./baseTypes";
|
||||
import {
|
||||
decryptBase32ToString,
|
||||
decryptBase64urlToString,
|
||||
@@ -65,6 +66,7 @@ export type SyncStatusType =
|
||||
export interface SyncPlanType {
|
||||
ts: number;
|
||||
tsFmt?: string;
|
||||
syncTriggerSource?: SyncTriggerSourceType;
|
||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||
mixedStates: Record<string, FileOrFolderMixedState>;
|
||||
}
|
||||
@@ -459,9 +461,15 @@ const ensembleMixedStates = async (
|
||||
results[key].changeLocalMtimeUsingMapping =
|
||||
r.changeLocalMtimeUsingMapping;
|
||||
} else {
|
||||
results[key] = r;
|
||||
results[key].existLocal = false; // we have already checked local
|
||||
results[key].existRemote = false; // we have already checked remote
|
||||
// So, the file doesn't exist,
|
||||
// except that it existed in the "renamed to" history records.
|
||||
// Most likely because that the user deleted the file while Obsidian was closed,
|
||||
// so Obsidian could not track the deletions.
|
||||
// We are not sure how to deal with this, so do not generate anything here!
|
||||
// // // The following 3 lines are of old logic, and have been removed:
|
||||
// // results[key] = r;
|
||||
// // results[key].existLocal = false; // we have already checked local
|
||||
// // results[key].existRemote = false; // we have already checked remote
|
||||
}
|
||||
} else {
|
||||
throw Error(
|
||||
@@ -746,6 +754,7 @@ export const getSyncPlan = async (
|
||||
remoteDeleteHistory: DeletionOnRemote[],
|
||||
localFileHistory: FileFolderHistoryRecord[],
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
triggerSource: SyncTriggerSourceType,
|
||||
vault: Vault,
|
||||
syncConfigDir: boolean,
|
||||
configDir: string,
|
||||
@@ -818,6 +827,7 @@ export const getSyncPlan = async (
|
||||
ts: currTs,
|
||||
tsFmt: currTsFmt,
|
||||
remoteType: remoteType,
|
||||
syncTriggerSource: triggerSource,
|
||||
mixedStates: mixedStates,
|
||||
} as SyncPlanType;
|
||||
return {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"0.3.2": "0.12.15",
|
||||
"0.3.17": "0.13.21"
|
||||
"0.3.20": "0.13.21"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user