Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2576354269 | ||
|
|
122fec1d75 | ||
|
|
27477f8d97 | ||
|
|
e9890c3c79 | ||
|
|
44480be707 | ||
|
|
437e041e71 | ||
|
|
9e4fed4504 | ||
|
|
70e2884f89 | ||
|
|
fb63a38fa0 | ||
|
|
d21d90868a | ||
|
|
e6591721e2 | ||
|
|
b6bae792c6 | ||
|
|
8624141780 | ||
|
|
563c2c3ff6 | ||
|
|
9a72ac8d66 | ||
|
|
d980bcbc1b | ||
|
|
6ebe1a8966 | ||
|
|
d0ab1d8abe | ||
|
|
b86d116830 | ||
|
|
7d5eaad36f | ||
|
|
7a13f4566c | ||
|
|
d355f72b7a | ||
|
|
9b1eebdf90 | ||
|
|
2ce461ed6a | ||
|
|
3aa91a2a1e | ||
|
|
2a3e0524c5 | ||
|
|
2f20ca61ce | ||
|
|
68ec8b5454 | ||
|
|
2bbbd569c5 | ||
|
|
62936c4473 |
@@ -3,3 +3,5 @@
|
|||||||
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
||||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||||
*.enc filter=lfs diff=lfs merge=lfs -text
|
*.enc filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.svg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.excalidraw filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
environment: env-for-buildci
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x]
|
node-version: [16.x]
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ This is yet another unofficial sync plugin for Obsidian.
|
|||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
- **This is NOT the [official sync service](https://github.com/fyears/obsidian-remotely-save.git) provided by Obsidian.**
|
- **This is NOT the [official sync service](https://obsidian.md/sync) provided by Obsidian.**
|
||||||
|
|
||||||
## !!!Caution!!!
|
## !!!Caution!!!
|
||||||
|
|
||||||
As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for any serious vaults.** **Backup your vault before using this plugin.** Don't be surprise to data loss!
|
As of Dec 2021, the plugin is considered in BETA stage. **DO NOT USE IT for any serious vaults.** **ALWAYS, ALWAYS, backup your vault before using this plugin.**
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Amazon S3 or S3-compatible, and Dropbox services are supported.** Webdav supports on the half way.
|
- Supports:
|
||||||
|
- Amazon S3 or S3-compatible
|
||||||
|
- Dropbox
|
||||||
|
- Webdav
|
||||||
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
||||||
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
||||||
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
||||||
@@ -27,20 +30,30 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
|
|||||||
- **Cloud services cost you money.** Always be aware of the costs and pricing.
|
- **Cloud services cost you money.** Always be aware of the costs and pricing.
|
||||||
- **All files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced.
|
- **All files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced.
|
||||||
|
|
||||||
|
## Questions, Suggestions, Or Bugs
|
||||||
|
|
||||||
|
You are greatly welcome to ask questions, post any suggestions, or report any bugs! The project is mainly maintained on GitHub:
|
||||||
|
|
||||||
|
- Questions: [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions)
|
||||||
|
- Suggestions: also in [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions)
|
||||||
|
- Bugs: [GitHub repo Issues](https://github.com/fyears/remotely-save/issues) (NOT Discussion)
|
||||||
|
|
||||||
|
Additionally, the plugin author may occasionally visit Obsidian official forum and official Discord server, and pay attention to this-plugin-related information there.
|
||||||
|
|
||||||
## Download and Install
|
## Download and Install
|
||||||
|
|
||||||
- Option #1: [](https://github.com/fyears/obsidian-remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds.
|
- Option #1: You can search, download, and install the plugin in official "community plugin list".
|
||||||
- Option #2: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
|
- Option #2: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds.
|
||||||
- Option #3: The pluin would be submitted to the official "community plugin list" in near future.
|
- Option #3: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### s3
|
### S3
|
||||||
|
|
||||||
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
||||||
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
||||||
- Download and enable this plugin.
|
- Download and enable this plugin.
|
||||||
- Enter your infomation to the settings of this plugin.
|
- Enter your information to the settings of this plugin.
|
||||||
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
||||||
- Click the new "switch" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (No "auto sync" yet.)
|
- Click the new "switch" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (No "auto sync" yet.)
|
||||||
- **Be patient while syncing.** Especially in the first-time sync.
|
- **Be patient while syncing.** Especially in the first-time sync.
|
||||||
@@ -49,12 +62,31 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
|
|||||||
|
|
||||||
- **This plugin's function for Dropbox is not as mature as functions for S3.**
|
- **This plugin's function for Dropbox is not as mature as functions for S3.**
|
||||||
- **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API.
|
- **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API.
|
||||||
- After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/obsidian-remotely-save` folder.
|
- After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/remotely-save/${vaultName}` folder.
|
||||||
- If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions.
|
- If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions.
|
||||||
- Password-based end-to-end encryption is also supported.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
|
|
||||||
### webdav
|
### webdav
|
||||||
|
|
||||||
- **webdav support is buggy (as of now, 20211122) and considered experimental, so it's hidden by default.** Highly recommend to use the more stable s3.
|
- **webdav support is considered experimental.**
|
||||||
- If you decide to give it a try, open settings, and click "Choose service" area five times, then a Notice should show up. Close and open settings again then you will be able to select webdav.
|
- Currently only supports BASIC authorization method.
|
||||||
- Currently webdav server should enable CORS for requests, because of technical limitations of mobile.
|
- Currently 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.
|
||||||
|
- Popular software NextCloud and OwnCloud 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.
|
||||||
|
- 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**.
|
||||||
|
|
||||||
|
## Import And Export Plugin Settings By QR Code
|
||||||
|
|
||||||
|
It's often verbose / inconvenient to input credentials on mobile. (On different desktop computers, it's trivial to copy and paste `data.json` across different devices.)
|
||||||
|
|
||||||
|
So this plugin has a helper function to export settings as a QR code, then you could use mobile device's camera to import the settings.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Configure the plugin settings on one device, and make sure the sync functions work. (E.g, you could sync notes using S3 credentials.)
|
||||||
|
2. Open plugin settings page. Then scroll down the page, until the section "Import and Export Settings". Click the button "Get QR Code". A new modal should show up and you should see a QR code.
|
||||||
|
3. On a second device, make sure the vault name is the same as the first device's.
|
||||||
|
4. On that second device, use its camera app, or any apps that support scanning QR codes, to scan the QR code from the first device. (On latest iOS, the system's built in camera app should work. On Android, at least one open source app [Binary Eye](https://github.com/markusfisch/BinaryEye) is tested to be working.)
|
||||||
|
5. A link / url / address should be identified in the scan-QR-code app, and you could follow the instruction in the app, and then you should be redirected to open the Obsidian app.
|
||||||
|
6. And finally, there should be a new notice in the Obsidian app saying the settings are successfully imported. Otherwise please check the error message in the notice.
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6b33b2a71f2d91988416c17015b59ef205dbf6a8c704ecac1cc95ed03ccc2c8e
|
||||||
|
size 16537
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:98358aa726dcb5565164ff260634f5c527223d1f2ec1d0410b400625c8dad704
|
||||||
|
size 14082
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:02a5057ebe38e4c3835c8354f53aca01c4c2335d8a0435e22f509b4ca4a73ca4
|
||||||
|
size 12889
|
||||||
@@ -18,12 +18,16 @@ esbuild
|
|||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
|
loader: {
|
||||||
|
".svg": "text",
|
||||||
|
},
|
||||||
entryPoints: ["./src/main.ts"],
|
entryPoints: ["./src/main.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
external: [
|
||||||
"obsidian",
|
"obsidian",
|
||||||
"electron",
|
"electron",
|
||||||
"fs",
|
"fs",
|
||||||
|
"crypto",
|
||||||
// ...builtins
|
// ...builtins
|
||||||
],
|
],
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.1.10",
|
"version": "0.2.4",
|
||||||
"minAppVersion": "0.12.15",
|
"minAppVersion": "0.12.15",
|
||||||
"description": "Yet another unofficial plugin allowing users to sync 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",
|
||||||
"authorUrl": "https://github.com/fyears",
|
"authorUrl": "https://github.com/fyears",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
|
|||||||
+7
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.1.10",
|
"version": "0.2.4",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
@@ -13,10 +13,7 @@
|
|||||||
"browser": {
|
"browser": {
|
||||||
"path": "path-browserify",
|
"path": "path-browserify",
|
||||||
"process": "process/browser",
|
"process": "process/browser",
|
||||||
"stream": "stream-browserify",
|
"stream": "stream-browserify"
|
||||||
"crypto": "crypto-browserify",
|
|
||||||
"util": "util/",
|
|
||||||
"assert": "assert/"
|
|
||||||
},
|
},
|
||||||
"source": "main.ts",
|
"source": "main.ts",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
@@ -25,15 +22,18 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.22",
|
"@types/chai": "^4.2.22",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
|
"@types/jsdom": "^16.2.13",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
|
"@types/qrcode": "^1.4.1",
|
||||||
"builtin-modules": "^3.2.0",
|
"builtin-modules": "^3.2.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"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": "^10.0.0",
|
||||||
"esbuild": "^0.14.0",
|
"esbuild": "^0.14.0",
|
||||||
|
"jsdom": "^19.0.0",
|
||||||
"mocha": "^9.1.3",
|
"mocha": "^9.1.3",
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"ts-loader": "^9.2.6",
|
"ts-loader": "^9.2.6",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"obsidian": "^0.12.0",
|
"obsidian": "^0.12.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
|
"qrcode": "^1.5.0",
|
||||||
"rfc4648": "^1.5.0",
|
"rfc4648": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
|
|||||||
@@ -1,9 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* Only type defs here.
|
* Only type defs here.
|
||||||
|
* To avoid circular dependency.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
s3Endpoint: string;
|
||||||
|
s3Region: string;
|
||||||
|
s3AccessKeyID: string;
|
||||||
|
s3SecretAccessKey: string;
|
||||||
|
s3BucketName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropboxConfig {
|
||||||
|
accessToken: string;
|
||||||
|
clientID: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessTokenExpiresInSeconds: number;
|
||||||
|
accessTokenExpiresAtTime: number;
|
||||||
|
accountID: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebdavAuthType = "digest" | "basic";
|
||||||
|
|
||||||
|
export interface WebdavConfig {
|
||||||
|
address: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
authType: WebdavAuthType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemotelySavePluginSettings {
|
||||||
|
s3: S3Config;
|
||||||
|
webdav: WebdavConfig;
|
||||||
|
dropbox: DropboxConfig;
|
||||||
|
password: string;
|
||||||
|
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RemoteItem {
|
export interface RemoteItem {
|
||||||
key: string;
|
key: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
@@ -11,3 +47,13 @@ export interface RemoteItem {
|
|||||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||||
etag?: string;
|
etag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COMMAND_URI = "remotely-save";
|
||||||
|
export const COMMAND_CALLBACK = "remotely-save-cb";
|
||||||
|
|
||||||
|
export interface UriParams {
|
||||||
|
func?: string;
|
||||||
|
vault?: string;
|
||||||
|
ver?: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
COMMAND_URI,
|
||||||
|
UriParams,
|
||||||
|
RemotelySavePluginSettings,
|
||||||
|
} from "./baseTypes";
|
||||||
|
|
||||||
|
export const exportQrCodeUri = async (
|
||||||
|
settings: RemotelySavePluginSettings,
|
||||||
|
currentVaultName: string,
|
||||||
|
pluginVersion: string
|
||||||
|
) => {
|
||||||
|
const data = encodeURIComponent(JSON.stringify(settings));
|
||||||
|
const vault = encodeURIComponent(currentVaultName);
|
||||||
|
const version = encodeURIComponent(pluginVersion);
|
||||||
|
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
|
||||||
|
// console.log(uri)
|
||||||
|
const imgUri = await QRCode.toDataURL(rawUri);
|
||||||
|
return {
|
||||||
|
rawUri,
|
||||||
|
imgUri,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProcessQrCodeResultType {
|
||||||
|
status: "error" | "ok";
|
||||||
|
message: string;
|
||||||
|
result?: RemotelySavePluginSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importQrCodeUri = (
|
||||||
|
inputParams: any,
|
||||||
|
currentVaultName: string
|
||||||
|
): ProcessQrCodeResultType => {
|
||||||
|
let params = inputParams as UriParams;
|
||||||
|
if (
|
||||||
|
params.func === undefined ||
|
||||||
|
params.func !== "settings" ||
|
||||||
|
params.vault === undefined ||
|
||||||
|
params.data === undefined
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `the uri is not for exporting/importing settings: ${JSON.stringify(
|
||||||
|
inputParams
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.vault !== currentVaultName) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `the target vault is ${
|
||||||
|
params.vault
|
||||||
|
} but you are currently in ${currentVaultName}: ${JSON.stringify(
|
||||||
|
inputParams
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = {} as RemotelySavePluginSettings;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse(params.data);
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `errors while parsing settings: ${JSON.stringify(inputParams)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
message: "ok",
|
||||||
|
result: settings,
|
||||||
|
};
|
||||||
|
};
|
||||||
+168
-117
@@ -27,29 +27,28 @@ import type { InternalDBs } from "./localdb";
|
|||||||
import type { SyncStatusType, PasswordCheckType } from "./sync";
|
import type { SyncStatusType, PasswordCheckType } from "./sync";
|
||||||
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
|
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
|
||||||
|
|
||||||
import { S3Config, DEFAULT_S3_CONFIG } from "./s3";
|
import { DEFAULT_S3_CONFIG } from "./remoteForS3";
|
||||||
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
|
import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav";
|
||||||
import {
|
import {
|
||||||
DropboxConfig,
|
|
||||||
DEFAULT_DROPBOX_CONFIG,
|
DEFAULT_DROPBOX_CONFIG,
|
||||||
getCodeVerifierAndChallenge,
|
getAuthUrlAndVerifier,
|
||||||
getAuthUrl,
|
|
||||||
sendAuthReq,
|
sendAuthReq,
|
||||||
setConfigBySuccessfullAuthInplace,
|
setConfigBySuccessfullAuthInplace,
|
||||||
} from "./remoteForDropbox";
|
} from "./remoteForDropbox";
|
||||||
|
|
||||||
import { RemoteClient } from "./remote";
|
import { RemoteClient } from "./remote";
|
||||||
import { exportSyncPlansToFiles } from "./debugMode";
|
import { exportSyncPlansToFiles } from "./debugMode";
|
||||||
import { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
import { COMMAND_URI, COMMAND_CALLBACK } from "./baseTypes";
|
||||||
|
import type {
|
||||||
interface RemotelySavePluginSettings {
|
SUPPORTED_SERVICES_TYPE,
|
||||||
s3: S3Config;
|
S3Config,
|
||||||
webdav: WebdavConfig;
|
DropboxConfig,
|
||||||
dropbox: DropboxConfig;
|
WebdavAuthType,
|
||||||
password: string;
|
WebdavConfig,
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
RemotelySavePluginSettings,
|
||||||
enableExperimentService: boolean;
|
} from "./baseTypes";
|
||||||
}
|
import type { ProcessQrCodeResultType } from "./importExport";
|
||||||
|
import { exportQrCodeUri, importQrCodeUri } from "./importExport";
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||||
s3: DEFAULT_S3_CONFIG,
|
s3: DEFAULT_S3_CONFIG,
|
||||||
@@ -57,17 +56,16 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
|||||||
dropbox: DEFAULT_DROPBOX_CONFIG,
|
dropbox: DEFAULT_DROPBOX_CONFIG,
|
||||||
password: "",
|
password: "",
|
||||||
serviceType: "s3",
|
serviceType: "s3",
|
||||||
enableExperimentService: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RemotelySavePlugin extends Plugin {
|
export default class RemotelySavePlugin extends Plugin {
|
||||||
settings: RemotelySavePluginSettings;
|
settings: RemotelySavePluginSettings;
|
||||||
cm: CodeMirror.Editor;
|
// cm: CodeMirror.Editor;
|
||||||
db: InternalDBs;
|
db: InternalDBs;
|
||||||
syncStatus: SyncStatusType;
|
syncStatus: SyncStatusType;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
console.log("loading plugin obsidian-remotely-save");
|
console.log(`loading plugin ${this.manifest.id}`);
|
||||||
|
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
|
||||||
@@ -87,6 +85,32 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
|
||||||
|
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
|
||||||
|
if (parsed.status === "error") {
|
||||||
|
new Notice(parsed.message);
|
||||||
|
} else {
|
||||||
|
const copied = JSON.parse(JSON.stringify(parsed.result));
|
||||||
|
// new Notice(JSON.stringify(copied))
|
||||||
|
this.settings = copied;
|
||||||
|
this.saveSettings();
|
||||||
|
new Notice(
|
||||||
|
`New settings for ${this.manifest.name} saved. Reopen the plugin Settings to the effect.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.registerObsidianProtocolHandler(
|
||||||
|
COMMAND_CALLBACK,
|
||||||
|
async (inputParams) => {
|
||||||
|
new Notice(
|
||||||
|
`Your uri call a callback that's not supported yet: ${JSON.stringify(
|
||||||
|
inputParams
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
||||||
if (this.syncStatus !== "idle") {
|
if (this.syncStatus !== "idle") {
|
||||||
new Notice(
|
new Notice(
|
||||||
@@ -191,7 +215,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
console.log("unloading plugin obsidian-remotely-save");
|
console.log(`unloading plugin ${this.manifest.id}`);
|
||||||
this.destroyDBs();
|
this.destroyDBs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,20 +257,22 @@ export class PasswordModal extends Modal {
|
|||||||
// contentEl.setText("Add Or change password.");
|
// contentEl.setText("Add Or change password.");
|
||||||
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
|
contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." });
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "If the field is not empty, files are enctrypted using the password locally before sent to remote.",
|
text: "If the field is not empty, files would be encrypted locally before being uploaded.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "If the field is empty, then no password is used, and files would be sent without encryption.",
|
text: "If the field is empty, then files would be uploaded without encryption.",
|
||||||
});
|
});
|
||||||
|
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 1/4: The password itself is stored in PLAIN TEXT LOCALLY and would not be sent to remote by this plugin.",
|
text: "Attention 1/4: The password itself is stored in PLAIN TEXT LOCALLY.",
|
||||||
|
cls: "password-disclaimer",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 2/4: Non-empty file contents are encrypted using openssl format. File/directory path are also encrypted then applied base32. BUT, some metadata such as file sizes and directory structures are not encrypted or can be easily guessed, and directory path are stored as 0-byte-size object remotely.",
|
text: "Attention 2/4: Some metadata are not encrypted or can be easily guessed. (File sizes are closed to their unencrypted ones, and directory path are stored as 0-byte-size object.)",
|
||||||
|
cls: "password-disclaimer",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 3/4: Before changing password, you should make sure the remote store (s3/webdav/...) IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD. OTHERWISE SOMETHING BAD WOULD HAPPEN!",
|
text: "Attention 3/4: You should make sure the remote store IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD, to avoid conflictions.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Attention 4/4: The longer the password, the better.",
|
text: "Attention 4/4: The longer the password, the better.",
|
||||||
@@ -261,7 +287,7 @@ export class PasswordModal extends Modal {
|
|||||||
new Notice("New password saved!");
|
new Notice("New password saved!");
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
button.setClass("password_second_confirm");
|
button.setClass("password-second-confirm");
|
||||||
})
|
})
|
||||||
.addButton((button) => {
|
.addButton((button) => {
|
||||||
button.setButtonText("Go Back");
|
button.setButtonText("Go Back");
|
||||||
@@ -296,17 +322,15 @@ export class DropboxAuthModal extends Modal {
|
|||||||
this.revokeAuthSetting = revokeAuthSetting;
|
this.revokeAuthSetting = revokeAuthSetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
async onOpen() {
|
||||||
let { contentEl } = this;
|
let { contentEl } = this;
|
||||||
|
|
||||||
const k = getCodeVerifierAndChallenge();
|
const { authUrl, verifier } = await getAuthUrlAndVerifier(
|
||||||
const authUrl = getAuthUrl(
|
this.plugin.settings.dropbox.clientID
|
||||||
this.plugin.settings.dropbox.clientID,
|
|
||||||
k.challenge
|
|
||||||
);
|
);
|
||||||
|
|
||||||
contentEl.createEl("p", {
|
contentEl.createEl("p", {
|
||||||
text: "Step 1: Visit the following address in a browser, and follow the steps on the web page to authorize.",
|
text: "Step 1: Visit the address in a browser, and follow the steps.",
|
||||||
});
|
});
|
||||||
contentEl.createEl("p").createEl("a", {
|
contentEl.createEl("p").createEl("a", {
|
||||||
href: authUrl,
|
href: authUrl,
|
||||||
@@ -336,7 +360,7 @@ export class DropboxAuthModal extends Modal {
|
|||||||
try {
|
try {
|
||||||
const authRes = await sendAuthReq(
|
const authRes = await sendAuthReq(
|
||||||
this.plugin.settings.dropbox.clientID,
|
this.plugin.settings.dropbox.clientID,
|
||||||
k.verifier,
|
verifier,
|
||||||
authCode
|
authCode
|
||||||
);
|
);
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -366,7 +390,7 @@ export class DropboxAuthModal extends Modal {
|
|||||||
this.plugin.settings.dropbox.username === ""
|
this.plugin.settings.dropbox.username === ""
|
||||||
);
|
);
|
||||||
this.revokeAuthSetting.setDesc(
|
this.revokeAuthSetting.setDesc(
|
||||||
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
|
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button.`
|
||||||
);
|
);
|
||||||
this.close();
|
this.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -383,6 +407,62 @@ export class DropboxAuthModal extends Modal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ExportSettingsQrCodeModal extends Modal {
|
||||||
|
plugin: RemotelySavePlugin;
|
||||||
|
constructor(app: App, plugin: RemotelySavePlugin) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
|
||||||
|
const { rawUri, imgUri } = await exportQrCodeUri(
|
||||||
|
this.plugin.settings,
|
||||||
|
this.app.vault.getName(),
|
||||||
|
this.plugin.manifest.version
|
||||||
|
);
|
||||||
|
|
||||||
|
const div1 = contentEl.createDiv();
|
||||||
|
div1.createEl("p", {
|
||||||
|
text: "You can use another device to scan this qrcode.",
|
||||||
|
});
|
||||||
|
div1.createEl("p", {
|
||||||
|
text: "Or, you can click the button to copy the special url.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const div2 = contentEl.createDiv();
|
||||||
|
div2.createEl(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
text: "Click to copy the special URI",
|
||||||
|
},
|
||||||
|
(el) => {
|
||||||
|
el.onclick = async () => {
|
||||||
|
await navigator.clipboard.writeText(rawUri);
|
||||||
|
new Notice("special uri copied to clipboard!");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const div3 = contentEl.createDiv();
|
||||||
|
div3.createEl(
|
||||||
|
"img",
|
||||||
|
{
|
||||||
|
cls: "qrcode-img",
|
||||||
|
},
|
||||||
|
async (el) => {
|
||||||
|
el.src = imgUri;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
let { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RemotelySaveSettingTab extends PluginSettingTab {
|
class RemotelySaveSettingTab extends PluginSettingTab {
|
||||||
plugin: RemotelySavePlugin;
|
plugin: RemotelySavePlugin;
|
||||||
|
|
||||||
@@ -426,49 +506,17 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
// 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 = generalDiv.createEl("div");
|
||||||
|
|
||||||
let clickChooserTimes = 0;
|
|
||||||
serviceChooserDiv.onClickEvent(async (x) => {
|
|
||||||
if (Platform.isIosApp) {
|
|
||||||
// downgrade the experiment
|
|
||||||
// because iOS doesn't support x.detail
|
|
||||||
clickChooserTimes += 1;
|
|
||||||
setTimeout(function () {
|
|
||||||
clickChooserTimes = 0;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((Platform.isIosApp && clickChooserTimes === 5) || x.detail === 5) {
|
|
||||||
if (this.plugin.settings.serviceType === "webdav") {
|
|
||||||
new Notice(
|
|
||||||
"You've enabled hidden unstable experimental webdav support before. Nothing changes."
|
|
||||||
);
|
|
||||||
} else if (!this.plugin.settings.enableExperimentService) {
|
|
||||||
this.plugin.settings.enableExperimentService = true;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
new Notice(
|
|
||||||
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
|
|
||||||
);
|
|
||||||
} else if (this.plugin.settings.enableExperimentService) {
|
|
||||||
this.plugin.settings.enableExperimentService = false;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
new Notice(
|
|
||||||
"You've disabled hidden unstable experimental webdav support. Reopen settings again."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
const s3Div = containerEl.createEl("div", { cls: "s3-hide" });
|
const s3Div = containerEl.createEl("div", { cls: "s3-hide" });
|
||||||
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
|
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
|
||||||
s3Div.createEl("h2", { text: "S3 (-compatible) Service" });
|
s3Div.createEl("h2", { text: "Remote For S3 (-compatible)" });
|
||||||
|
|
||||||
s3Div.createEl("p", {
|
s3Div.createEl("p", {
|
||||||
text: "You can use Amazon S3 or another S3-compatible service to sync your vault. Enter your bucket information below.",
|
text: "Disclaimer: This plugin is NOT an official Amazon product.",
|
||||||
|
cls: "s3-disclaimer",
|
||||||
});
|
});
|
||||||
|
|
||||||
s3Div.createEl("p", {
|
s3Div.createEl("p", {
|
||||||
text: "Disclaimer: The infomation is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may or may not be able to read the info. If you see any unintentional access to your S3 bucket, please immediately delete the access key to stop further accessment.",
|
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.",
|
||||||
cls: "s3-disclaimer",
|
cls: "s3-disclaimer",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -477,7 +525,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
s3Div.createEl("p", {
|
s3Div.createEl("p", {
|
||||||
text: "Some Amazon S3 official docs:",
|
text: "Some Amazon S3 official docs for references:",
|
||||||
});
|
});
|
||||||
|
|
||||||
const s3LinksUl = s3Div.createEl("div").createEl("ul");
|
const s3LinksUl = s3Div.createEl("div").createEl("ul");
|
||||||
@@ -588,21 +636,19 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
"dropbox-hide",
|
"dropbox-hide",
|
||||||
this.plugin.settings.serviceType !== "dropbox"
|
this.plugin.settings.serviceType !== "dropbox"
|
||||||
);
|
);
|
||||||
dropboxDiv.createEl("h2", { text: "for Dropbox" });
|
dropboxDiv.createEl("h2", { text: "Remote For Dropbox" });
|
||||||
dropboxDiv.createEl("p", {
|
dropboxDiv.createEl("p", {
|
||||||
text: "Disclaimer: Sync support for Dropbox are more experimental, and s3 functions are more stable now.",
|
text: "Disclaimer: This app is NOT an official Dropbox product.",
|
||||||
cls: "dropbox-disclaimer",
|
cls: "dropbox-disclaimer",
|
||||||
});
|
});
|
||||||
dropboxDiv.createEl("p", {
|
dropboxDiv.createEl("p", {
|
||||||
text: "Disclaimer: This app is NOT an official Dropbox product. It just uses Dropbox open api.",
|
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .",
|
||||||
cls: "dropbox-disclaimer",
|
cls: "dropbox-disclaimer",
|
||||||
});
|
});
|
||||||
dropboxDiv.createEl("p", {
|
dropboxDiv.createEl("p", {
|
||||||
text: "Disclaimer: The infomation is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may or may not be able to read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .",
|
text: `We will create and sync inside the folder /Apps/${
|
||||||
cls: "dropbox-disclaimer",
|
this.plugin.manifest.id
|
||||||
});
|
}/${this.app.vault.getName()} on your Dropbox.`,
|
||||||
dropboxDiv.createEl("p", {
|
|
||||||
text: "We will create a folder App/obsidian-remotely-save on your Dropbox. All files/folders sync would happen inside this folder.",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropboxSelectAuthDiv = dropboxDiv.createDiv();
|
const dropboxSelectAuthDiv = dropboxDiv.createDiv();
|
||||||
@@ -709,15 +755,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.serviceType !== "webdav"
|
this.plugin.settings.serviceType !== "webdav"
|
||||||
);
|
);
|
||||||
|
|
||||||
webdavDiv.createEl("h2", { text: "Webdav Service" });
|
webdavDiv.createEl("h2", { text: "Remote For Webdav" });
|
||||||
|
|
||||||
webdavDiv.createEl("p", {
|
webdavDiv.createEl("p", {
|
||||||
text: "Disclaimer: Webdav functions are more experimental, and s3 functions are more stable now.",
|
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.",
|
||||||
cls: "webdav-disclaimer",
|
|
||||||
});
|
|
||||||
|
|
||||||
webdavDiv.createEl("p", {
|
|
||||||
text: "Disclaimer: The infomation is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may or may not be able to read the info. If you see any unintentional access to your webdav server, please immediately change the username and/or password to stop further accessment.",
|
|
||||||
cls: "webdav-disclaimer",
|
cls: "webdav-disclaimer",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -725,6 +766,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost",
|
text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
webdavDiv.createEl("p", {
|
||||||
|
text: `We will create and sync inside the folder /${this.app.vault.getName()} on your server.`,
|
||||||
|
});
|
||||||
|
|
||||||
new Setting(webdavDiv)
|
new Setting(webdavDiv)
|
||||||
.setName("server address")
|
.setName("server address")
|
||||||
.setDesc("server address")
|
.setDesc("server address")
|
||||||
@@ -766,12 +811,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
new Setting(webdavDiv)
|
new Setting(webdavDiv)
|
||||||
.setName("server auth type")
|
.setName("server auth type")
|
||||||
.setDesc(
|
.setDesc("If no password, this option would be ignored.")
|
||||||
"Server auth type. If you do not set password, this option would be ignored."
|
|
||||||
)
|
|
||||||
.addDropdown((dropdown) => {
|
.addDropdown((dropdown) => {
|
||||||
dropdown.addOption("basic", "basic");
|
dropdown.addOption("basic", "basic");
|
||||||
dropdown.addOption("digest", "digest");
|
// dropdown.addOption("digest", "digest");
|
||||||
|
|
||||||
dropdown
|
dropdown
|
||||||
.setValue(this.plugin.settings.webdav.authType)
|
.setValue(this.plugin.settings.webdav.authType)
|
||||||
@@ -791,7 +834,9 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
const client = new RemoteClient(
|
const client = new RemoteClient(
|
||||||
"webdav",
|
"webdav",
|
||||||
undefined,
|
undefined,
|
||||||
this.plugin.settings.webdav
|
this.plugin.settings.webdav,
|
||||||
|
undefined,
|
||||||
|
this.app.vault.getName()
|
||||||
);
|
);
|
||||||
const res = await client.checkConnectivity();
|
const res = await client.checkConnectivity();
|
||||||
if (res) {
|
if (res) {
|
||||||
@@ -806,21 +851,13 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
// after s3Div and webdavDiv being created
|
// after s3Div and webdavDiv being created
|
||||||
new Setting(serviceChooserDiv)
|
new Setting(serviceChooserDiv)
|
||||||
.setName("Choose service")
|
.setName("Choose service")
|
||||||
.setDesc("Choose a service, by default s3")
|
.setDesc("Choose a service.")
|
||||||
.addDropdown(async (dropdown) => {
|
.addDropdown(async (dropdown) => {
|
||||||
const currService = this.plugin.settings.serviceType;
|
const currService = this.plugin.settings.serviceType;
|
||||||
const enableExperimentService =
|
|
||||||
this.plugin.settings.enableExperimentService;
|
|
||||||
|
|
||||||
dropdown.addOption("s3", "s3 (-compatible)");
|
dropdown.addOption("s3", "S3 (-compatible)");
|
||||||
dropdown.addOption("dropbox", "Dropbox");
|
dropdown.addOption("dropbox", "Dropbox");
|
||||||
if (currService === "webdav" || enableExperimentService) {
|
dropdown.addOption("webdav", "Webdav");
|
||||||
dropdown.addOption("webdav", "webdav (experimental)");
|
|
||||||
if (!enableExperimentService) {
|
|
||||||
this.plugin.settings.enableExperimentService = true;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dropdown
|
dropdown
|
||||||
.setValue(this.plugin.settings.serviceType)
|
.setValue(this.plugin.settings.serviceType)
|
||||||
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
|
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
|
||||||
@@ -841,16 +878,34 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// import and export
|
||||||
|
const importExportDiv = containerEl.createEl("div");
|
||||||
|
importExportDiv.createEl("h2", { text: "Import and Export Settings" });
|
||||||
|
|
||||||
|
new Setting(importExportDiv)
|
||||||
|
.setName("export")
|
||||||
|
.setDesc("Export all settings by generating a qrcode.")
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText("Get QR Code");
|
||||||
|
button.onClick(async () => {
|
||||||
|
new ExportSettingsQrCodeModal(this.app, this.plugin).open();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(importExportDiv)
|
||||||
|
.setName("import")
|
||||||
|
.setDesc(
|
||||||
|
"You should open a camera or scan-qrcode app, to manually scan the QR code."
|
||||||
|
);
|
||||||
|
|
||||||
const debugDiv = containerEl.createEl("div");
|
const debugDiv = containerEl.createEl("div");
|
||||||
debugDiv.createEl("h2", { text: "Debug" });
|
debugDiv.createEl("h2", { text: "Debug" });
|
||||||
const syncPlanDiv = debugDiv.createEl("div");
|
const syncPlanDiv = debugDiv.createEl("div");
|
||||||
syncPlanDiv.createEl("p", {
|
|
||||||
text: "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync.",
|
|
||||||
});
|
|
||||||
|
|
||||||
new Setting(syncPlanDiv)
|
new Setting(syncPlanDiv)
|
||||||
.setName("export sync plans")
|
.setName("export sync plans")
|
||||||
.setDesc("export sync plans")
|
.setDesc(
|
||||||
|
"Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans"
|
||||||
|
)
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText("Export");
|
button.setButtonText("Export");
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
@@ -858,7 +913,6 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
new Notice("sync plans history exported");
|
new Notice("sync plans history exported");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(syncPlanDiv)
|
new Setting(syncPlanDiv)
|
||||||
.setName("delete sync plans history in db")
|
.setName("delete sync plans history in db")
|
||||||
.setDesc("delete sync plans history in db")
|
.setDesc("delete sync plans history in db")
|
||||||
@@ -871,13 +925,11 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const syncMappingDiv = debugDiv.createEl("div");
|
const syncMappingDiv = debugDiv.createEl("div");
|
||||||
syncMappingDiv.createEl("p", {
|
|
||||||
text: "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync.",
|
|
||||||
});
|
|
||||||
|
|
||||||
new Setting(syncMappingDiv)
|
new Setting(syncMappingDiv)
|
||||||
.setName("delete sync mappings history in db")
|
.setName("delete sync mappings history in db")
|
||||||
.setDesc("delete sync mappings history in db")
|
.setDesc(
|
||||||
|
"Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync. Click the button to delete sync mappings history in db"
|
||||||
|
)
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText("Delete Sync Mappings");
|
button.setButtonText("Delete Sync Mappings");
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
@@ -887,12 +939,11 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const dbsResetDiv = debugDiv.createEl("div");
|
const dbsResetDiv = debugDiv.createEl("div");
|
||||||
syncMappingDiv.createEl("p", {
|
new Setting(dbsResetDiv)
|
||||||
text: "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings.",
|
|
||||||
});
|
|
||||||
new Setting(syncMappingDiv)
|
|
||||||
.setName("reset local internal cache/databases")
|
.setName("reset local internal cache/databases")
|
||||||
.setDesc("reset local internal cache/databases")
|
.setDesc(
|
||||||
|
"Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings."
|
||||||
|
)
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText("Reset");
|
button.setButtonText("Reset");
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
|
|||||||
@@ -155,3 +155,11 @@ export const getPathFolder = (a: string) => {
|
|||||||
export const setToString = (a: Set<string>, delimiter: string = ",") => {
|
export const setToString = (a: Set<string>, delimiter: string = ",") => {
|
||||||
return [...a].join(delimiter);
|
return [...a].join(delimiter);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractSvgSub = (x: string, subEl: string = "rect") => {
|
||||||
|
const parser = new window.DOMParser();
|
||||||
|
const dom = parser.parseFromString(x, "image/svg+xml");
|
||||||
|
const svg = dom.querySelector("svg");
|
||||||
|
svg.setAttribute("viewbox", "0 0 10 10");
|
||||||
|
return svg.innerHTML;
|
||||||
|
};
|
||||||
|
|||||||
+19
-11
@@ -1,24 +1,29 @@
|
|||||||
import { Vault } from "obsidian";
|
import { Vault } from "obsidian";
|
||||||
|
|
||||||
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
import type {
|
||||||
import * as s3 from "./s3";
|
SUPPORTED_SERVICES_TYPE,
|
||||||
import * as webdav from "./webdav";
|
S3Config,
|
||||||
|
DropboxConfig,
|
||||||
|
WebdavConfig,
|
||||||
|
} from "./baseTypes";
|
||||||
|
import * as s3 from "./remoteForS3";
|
||||||
|
import * as webdav from "./remoteForWebdav";
|
||||||
import * as dropbox from "./remoteForDropbox";
|
import * as dropbox from "./remoteForDropbox";
|
||||||
|
|
||||||
export class RemoteClient {
|
export class RemoteClient {
|
||||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
readonly s3Client?: s3.S3Client;
|
readonly s3Client?: s3.S3Client;
|
||||||
readonly s3Config?: s3.S3Config;
|
readonly s3Config?: S3Config;
|
||||||
readonly webdavClient?: webdav.WebDAVClient;
|
readonly webdavClient?: webdav.WrappedWebdavClient;
|
||||||
readonly webdavConfig?: webdav.WebdavConfig;
|
readonly webdavConfig?: WebdavConfig;
|
||||||
readonly dropboxClient?: dropbox.WrappedDropboxClient;
|
readonly dropboxClient?: dropbox.WrappedDropboxClient;
|
||||||
readonly dropboxConfig?: dropbox.DropboxConfig;
|
readonly dropboxConfig?: DropboxConfig;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||||
s3Config?: s3.S3Config,
|
s3Config?: S3Config,
|
||||||
webdavConfig?: webdav.WebdavConfig,
|
webdavConfig?: WebdavConfig,
|
||||||
dropboxConfig?: dropbox.DropboxConfig,
|
dropboxConfig?: DropboxConfig,
|
||||||
vaultName?: string,
|
vaultName?: string,
|
||||||
saveUpdatedConfigFunc?: () => Promise<any>
|
saveUpdatedConfigFunc?: () => Promise<any>
|
||||||
) {
|
) {
|
||||||
@@ -29,8 +34,11 @@ export class RemoteClient {
|
|||||||
this.s3Config = s3Config;
|
this.s3Config = s3Config;
|
||||||
this.s3Client = s3.getS3Client(this.s3Config);
|
this.s3Client = s3.getS3Client(this.s3Config);
|
||||||
} else if (serviceType === "webdav") {
|
} else if (serviceType === "webdav") {
|
||||||
|
if (vaultName === undefined) {
|
||||||
|
throw Error("remember to provide vault name while init webdav client");
|
||||||
|
}
|
||||||
this.webdavConfig = webdavConfig;
|
this.webdavConfig = webdavConfig;
|
||||||
this.webdavClient = webdav.getWebdavClient(this.webdavConfig);
|
this.webdavClient = webdav.getWebdavClient(this.webdavConfig, vaultName);
|
||||||
} else if (serviceType === "dropbox") {
|
} else if (serviceType === "dropbox") {
|
||||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||||
throw Error(
|
throw Error(
|
||||||
|
|||||||
+49
-37
@@ -1,11 +1,9 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { FileStats, Vault } from "obsidian";
|
import { FileStats, Vault } from "obsidian";
|
||||||
import { Buffer } from "buffer";
|
|
||||||
import * as crypto from "crypto";
|
|
||||||
|
|
||||||
import { Dropbox, DropboxResponse, files } from "dropbox";
|
import { Dropbox, DropboxAuth, DropboxResponse, files } from "dropbox";
|
||||||
export { Dropbox } from "dropbox";
|
export { Dropbox } from "dropbox";
|
||||||
import { RemoteItem } from "./baseTypes";
|
import { RemoteItem, DropboxConfig } from "./baseTypes";
|
||||||
import {
|
import {
|
||||||
arrayBufferToBuffer,
|
arrayBufferToBuffer,
|
||||||
bufferToArrayBuffer,
|
bufferToArrayBuffer,
|
||||||
@@ -16,16 +14,6 @@ import {
|
|||||||
} from "./misc";
|
} from "./misc";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
|
|
||||||
export interface DropboxConfig {
|
|
||||||
accessToken: string;
|
|
||||||
clientID: string;
|
|
||||||
refreshToken: string;
|
|
||||||
accessTokenExpiresInSeconds: number;
|
|
||||||
accessTokenExpiresAtTime: number;
|
|
||||||
accountID: string;
|
|
||||||
username: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_DROPBOX_CONFIG = {
|
export const DEFAULT_DROPBOX_CONFIG = {
|
||||||
accessToken: "",
|
accessToken: "",
|
||||||
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
|
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
|
||||||
@@ -38,6 +26,10 @@ export const DEFAULT_DROPBOX_CONFIG = {
|
|||||||
|
|
||||||
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
|
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||||
let key = fileOrFolderPath;
|
let key = fileOrFolderPath;
|
||||||
|
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
|
||||||
|
// special
|
||||||
|
key = `/${vaultName}`;
|
||||||
|
}
|
||||||
if (!fileOrFolderPath.startsWith("/")) {
|
if (!fileOrFolderPath.startsWith("/")) {
|
||||||
// then this is original path in Obsidian
|
// then this is original path in Obsidian
|
||||||
key = `/${vaultName}/${fileOrFolderPath}`;
|
key = `/${vaultName}/${fileOrFolderPath}`;
|
||||||
@@ -162,32 +154,28 @@ const fixLastModifiedTimeInplace = (allFilesFolders: RemoteItem[]) => {
|
|||||||
// see https://dropbox.tech/developers/pkce--what-and-why-
|
// see https://dropbox.tech/developers/pkce--what-and-why-
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
const specialBase64Encode = (str: Buffer) => {
|
export const getAuthUrlAndVerifier = async (appKey: string) => {
|
||||||
return str
|
const auth = new DropboxAuth({
|
||||||
.toString("base64")
|
clientId: appKey,
|
||||||
.replace(/\+/g, "-")
|
});
|
||||||
.replace(/\//g, "_")
|
const authUrl = (
|
||||||
.replace(/=/g, "");
|
await auth.getAuthenticationUrl(
|
||||||
};
|
undefined,
|
||||||
const sha256 = (buffer: string) => {
|
undefined,
|
||||||
return crypto.createHash("sha256").update(buffer).digest();
|
"code",
|
||||||
};
|
"offline",
|
||||||
|
undefined,
|
||||||
export const getCodeVerifierAndChallenge = () => {
|
"none",
|
||||||
const codeVerifier = specialBase64Encode(crypto.randomBytes(32));
|
true
|
||||||
// console.log(`Client generated code_verifier: ${codeVerifier}`);
|
)
|
||||||
const codeChallenge = specialBase64Encode(sha256(codeVerifier));
|
).toString();
|
||||||
// console.log(`Client generated code_challenge: ${codeChallenge}`);
|
const verifier = auth.getCodeVerifier();
|
||||||
return {
|
return {
|
||||||
verifier: codeVerifier,
|
authUrl: authUrl,
|
||||||
challenge: codeChallenge,
|
verifier: verifier,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAuthUrl = (appKey: string, challenge: string) => {
|
|
||||||
return `https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&token_access_type=offline`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DropboxSuccessAuthRes {
|
export interface DropboxSuccessAuthRes {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: "bearer";
|
token_type: "bearer";
|
||||||
@@ -336,6 +324,7 @@ export class WrappedDropboxClient {
|
|||||||
path: `/${this.vaultName}`,
|
path: `/${this.vaultName}`,
|
||||||
});
|
});
|
||||||
console.log(`remote folder /${this.vaultName} created`);
|
console.log(`remote folder /${this.vaultName} created`);
|
||||||
|
this.vaultFolderExists = true;
|
||||||
} else {
|
} else {
|
||||||
// console.log(`remote folder /${this.vaultName} exists`);
|
// console.log(`remote folder /${this.vaultName} exists`);
|
||||||
}
|
}
|
||||||
@@ -487,20 +476,41 @@ export const listFromRemote = async (
|
|||||||
throw Error("prefix not supported (yet)");
|
throw Error("prefix not supported (yet)");
|
||||||
}
|
}
|
||||||
await client.init();
|
await client.init();
|
||||||
const res = await client.dropbox.filesListFolder({
|
let res = await client.dropbox.filesListFolder({
|
||||||
path: `/${client.vaultName}`,
|
path: `/${client.vaultName}`,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
|
include_deleted: false,
|
||||||
|
limit: 1000,
|
||||||
});
|
});
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
throw Error(JSON.stringify(res));
|
throw Error(JSON.stringify(res));
|
||||||
}
|
}
|
||||||
// console.log(res);
|
// console.log(res);
|
||||||
|
|
||||||
const contents = res.result.entries;
|
const contents = res.result.entries;
|
||||||
const unifiedContents = contents
|
const unifiedContents = contents
|
||||||
.filter((x) => x[".tag"] !== "deleted")
|
.filter((x) => x[".tag"] !== "deleted")
|
||||||
.filter((x) => x.path_display !== `/${client.vaultName}`)
|
.filter((x) => x.path_display !== `/${client.vaultName}`)
|
||||||
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
|
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
|
||||||
|
|
||||||
|
while (res.result.has_more) {
|
||||||
|
res = await client.dropbox.filesListFolderContinue({
|
||||||
|
cursor: res.result.cursor,
|
||||||
|
});
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw Error(JSON.stringify(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents2 = res.result.entries;
|
||||||
|
const unifiedContents2 = contents2
|
||||||
|
.filter((x) => x[".tag"] !== "deleted")
|
||||||
|
.filter((x) => x.path_display !== `/${client.vaultName}`)
|
||||||
|
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
|
||||||
|
unifiedContents.push(...unifiedContents2);
|
||||||
|
}
|
||||||
|
|
||||||
fixLastModifiedTimeInplace(unifiedContents);
|
fixLastModifiedTimeInplace(unifiedContents);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Contents: unifiedContents,
|
Contents: unifiedContents,
|
||||||
};
|
};
|
||||||
@@ -536,6 +546,8 @@ export const downloadFromRemote = async (
|
|||||||
password: string = "",
|
password: string = "",
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
|
await client.init();
|
||||||
|
|
||||||
const isFolder = fileOrFolderPath.endsWith("/");
|
const isFolder = fileOrFolderPath.endsWith("/");
|
||||||
|
|
||||||
await mkdirpInVault(fileOrFolderPath, vault);
|
await mkdirpInVault(fileOrFolderPath, vault);
|
||||||
|
|||||||
@@ -27,17 +27,9 @@ import {
|
|||||||
} from "./misc";
|
} from "./misc";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
|
|
||||||
import { RemoteItem } from "./baseTypes";
|
import { RemoteItem, S3Config } from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
|
|
||||||
export interface S3Config {
|
|
||||||
s3Endpoint: string;
|
|
||||||
s3Region: string;
|
|
||||||
s3AccessKeyID: string;
|
|
||||||
s3SecretAccessKey: string;
|
|
||||||
s3BucketName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_S3_CONFIG = {
|
export const DEFAULT_S3_CONFIG = {
|
||||||
s3Endpoint: "",
|
s3Endpoint: "",
|
||||||
s3Region: "",
|
s3Region: "",
|
||||||
@@ -4,7 +4,7 @@ import { AuthType, BufferLike, createClient } from "webdav/web";
|
|||||||
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
||||||
export type { WebDAVClient } from "webdav/web";
|
export type { WebDAVClient } from "webdav/web";
|
||||||
|
|
||||||
import type { RemoteItem } from "./baseTypes";
|
import type { RemoteItem, WebdavAuthType, WebdavConfig } from "./baseTypes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayBufferToBuffer,
|
arrayBufferToBuffer,
|
||||||
@@ -14,15 +14,6 @@ import {
|
|||||||
} from "./misc";
|
} from "./misc";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
|
|
||||||
export type WebdavAuthType = "digest" | "basic";
|
|
||||||
|
|
||||||
export interface WebdavConfig {
|
|
||||||
address: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
authType: WebdavAuthType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_WEBDAV_CONFIG = {
|
export const DEFAULT_WEBDAV_CONFIG = {
|
||||||
address: "",
|
address: "",
|
||||||
username: "",
|
username: "",
|
||||||
@@ -30,22 +21,35 @@ export const DEFAULT_WEBDAV_CONFIG = {
|
|||||||
authType: "basic",
|
authType: "basic",
|
||||||
} as WebdavConfig;
|
} as WebdavConfig;
|
||||||
|
|
||||||
const getWebdavPath = (fileOrFolderPath: string) => {
|
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||||
|
let key = fileOrFolderPath;
|
||||||
|
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
|
||||||
|
// special
|
||||||
|
key = `/${vaultName}/`;
|
||||||
|
}
|
||||||
if (!fileOrFolderPath.startsWith("/")) {
|
if (!fileOrFolderPath.startsWith("/")) {
|
||||||
return `/${fileOrFolderPath}`;
|
key = `/${vaultName}/${fileOrFolderPath}`;
|
||||||
}
|
}
|
||||||
return fileOrFolderPath;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNormPath = (fileOrFolderPath: string) => {
|
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||||
if (fileOrFolderPath.startsWith("/")) {
|
if (
|
||||||
return fileOrFolderPath.slice(1);
|
!(
|
||||||
|
fileOrFolderPath === `/${vaultName}` ||
|
||||||
|
fileOrFolderPath.startsWith(`/${vaultName}/`)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
|
||||||
}
|
}
|
||||||
return fileOrFolderPath;
|
// if (fileOrFolderPath.startsWith("/")) {
|
||||||
|
// return fileOrFolderPath.slice(1);
|
||||||
|
// }
|
||||||
|
return fileOrFolderPath.slice(`/${vaultName}/`.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromWebdavItemToRemoteItem = (x: FileStat) => {
|
const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
|
||||||
let key = getNormPath(x.filename);
|
let key = getNormPath(x.filename, vaultName);
|
||||||
if (x.type === "directory" && !key.endsWith("/")) {
|
if (x.type === "directory" && !key.endsWith("/")) {
|
||||||
key = `${key}/`;
|
key = `${key}/`;
|
||||||
}
|
}
|
||||||
@@ -58,45 +62,90 @@ const fromWebdavItemToRemoteItem = (x: FileStat) => {
|
|||||||
} as RemoteItem;
|
} as RemoteItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWebdavClient = (webdavConfig: WebdavConfig) => {
|
export class WrappedWebdavClient {
|
||||||
if (webdavConfig.username !== "" && webdavConfig.password !== "") {
|
webdavConfig: WebdavConfig;
|
||||||
return createClient(webdavConfig.address, {
|
vaultName: string;
|
||||||
username: webdavConfig.username,
|
client: WebDAVClient;
|
||||||
password: webdavConfig.password,
|
vaultFolderExists: boolean;
|
||||||
authType:
|
constructor(webdavConfig: WebdavConfig, vaultName: string) {
|
||||||
webdavConfig.authType === "digest"
|
this.webdavConfig = webdavConfig;
|
||||||
? AuthType.Digest
|
this.vaultName = vaultName;
|
||||||
: AuthType.Password,
|
this.vaultFolderExists = false;
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("no password");
|
|
||||||
return createClient(webdavConfig.address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init = async () => {
|
||||||
|
// init client if not inited
|
||||||
|
if (this.client === undefined) {
|
||||||
|
if (
|
||||||
|
this.webdavConfig.username !== "" &&
|
||||||
|
this.webdavConfig.password !== ""
|
||||||
|
) {
|
||||||
|
this.client = createClient(this.webdavConfig.address, {
|
||||||
|
username: this.webdavConfig.username,
|
||||||
|
password: this.webdavConfig.password,
|
||||||
|
authType:
|
||||||
|
this.webdavConfig.authType === "digest"
|
||||||
|
? AuthType.Digest
|
||||||
|
: AuthType.Password,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("no password");
|
||||||
|
this.client = createClient(this.webdavConfig.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check vault folder
|
||||||
|
if (this.vaultFolderExists) {
|
||||||
|
// pass
|
||||||
|
} else {
|
||||||
|
const res = await this.client.exists(`/${this.vaultName}`);
|
||||||
|
if (res) {
|
||||||
|
console.log("exits!");
|
||||||
|
this.vaultFolderExists = true;
|
||||||
|
} else {
|
||||||
|
console.log("not exists, creating");
|
||||||
|
await this.client.createDirectory(`/${this.vaultName}`);
|
||||||
|
console.log("created!");
|
||||||
|
this.vaultFolderExists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWebdavClient = (
|
||||||
|
webdavConfig: WebdavConfig,
|
||||||
|
vaultName: string
|
||||||
|
) => {
|
||||||
|
return new WrappedWebdavClient(webdavConfig, vaultName);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRemoteMeta = async (
|
export const getRemoteMeta = async (
|
||||||
client: WebDAVClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string
|
fileOrFolderPath: string
|
||||||
) => {
|
) => {
|
||||||
const res = (await client.stat(getWebdavPath(fileOrFolderPath), {
|
await client.init();
|
||||||
|
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName);
|
||||||
|
console.log(`remotePath = ${remotePath}`);
|
||||||
|
const res = (await client.client.stat(remotePath, {
|
||||||
details: false,
|
details: false,
|
||||||
})) as FileStat;
|
})) as FileStat;
|
||||||
return fromWebdavItemToRemoteItem(res);
|
return fromWebdavItemToRemoteItem(res, client.vaultName);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadToRemote = async (
|
export const uploadToRemote = async (
|
||||||
client: WebDAVClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
isRecursively: boolean = false,
|
isRecursively: boolean = false,
|
||||||
password: string = "",
|
password: string = "",
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
|
await client.init();
|
||||||
let uploadFile = fileOrFolderPath;
|
let uploadFile = fileOrFolderPath;
|
||||||
if (password !== "") {
|
if (password !== "") {
|
||||||
uploadFile = remoteEncryptedKey;
|
uploadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
uploadFile = getWebdavPath(uploadFile);
|
uploadFile = getWebdavPath(uploadFile, client.vaultName);
|
||||||
|
|
||||||
const isFolder = fileOrFolderPath.endsWith("/");
|
const isFolder = fileOrFolderPath.endsWith("/");
|
||||||
|
|
||||||
@@ -106,14 +155,14 @@ export const uploadToRemote = async (
|
|||||||
// folder
|
// folder
|
||||||
if (password === "") {
|
if (password === "") {
|
||||||
// if not encrypted, mkdir a remote folder
|
// if not encrypted, mkdir a remote folder
|
||||||
await client.createDirectory(uploadFile, {
|
await client.client.createDirectory(uploadFile, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
const res = await getRemoteMeta(client, uploadFile);
|
const res = await getRemoteMeta(client, uploadFile);
|
||||||
return res;
|
return res;
|
||||||
} else {
|
} else {
|
||||||
// if encrypted, upload a fake file with the encrypted file name
|
// if encrypted, upload a fake file with the encrypted file name
|
||||||
await client.putFileContents(uploadFile, "", {
|
await client.client.putFileContents(uploadFile, "", {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
onUploadProgress: (progress) => {
|
onUploadProgress: (progress) => {
|
||||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||||
@@ -133,9 +182,9 @@ export const uploadToRemote = async (
|
|||||||
// we need to create folders before uploading
|
// we need to create folders before uploading
|
||||||
const dir = getPathFolder(uploadFile);
|
const dir = getPathFolder(uploadFile);
|
||||||
if (dir !== "/" && dir !== "") {
|
if (dir !== "/" && dir !== "") {
|
||||||
await client.createDirectory(dir, { recursive: true });
|
await client.client.createDirectory(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
await client.putFileContents(uploadFile, remoteContent, {
|
await client.client.putFileContents(uploadFile, remoteContent, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
onUploadProgress: (progress) => {
|
onUploadProgress: (progress) => {
|
||||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||||
@@ -146,26 +195,36 @@ export const uploadToRemote = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listFromRemote = async (client: WebDAVClient, prefix?: string) => {
|
export const listFromRemote = async (
|
||||||
|
client: WrappedWebdavClient,
|
||||||
|
prefix?: string
|
||||||
|
) => {
|
||||||
if (prefix !== undefined) {
|
if (prefix !== undefined) {
|
||||||
throw Error("prefix not supported");
|
throw Error("prefix not supported");
|
||||||
}
|
}
|
||||||
const contents = (await client.getDirectoryContents("/", {
|
await client.init();
|
||||||
deep: true,
|
const contents = (await client.client.getDirectoryContents(
|
||||||
details: false /* no need for verbose details here */,
|
`/${client.vaultName}`,
|
||||||
glob: "/**" /* avoid dot files by using glob */,
|
{
|
||||||
})) as FileStat[];
|
deep: true,
|
||||||
|
details: false /* no need for verbose details here */,
|
||||||
|
glob: "/**" /* avoid dot files by using glob */,
|
||||||
|
}
|
||||||
|
)) as FileStat[];
|
||||||
return {
|
return {
|
||||||
Contents: contents.map((x) => fromWebdavItemToRemoteItem(x)),
|
Contents: contents.map((x) =>
|
||||||
|
fromWebdavItemToRemoteItem(x, client.vaultName)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadFromRemoteRaw = async (
|
const downloadFromRemoteRaw = async (
|
||||||
client: WebDAVClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string
|
fileOrFolderPath: string
|
||||||
) => {
|
) => {
|
||||||
const buff = (await client.getFileContents(
|
await client.init();
|
||||||
getWebdavPath(fileOrFolderPath)
|
const buff = (await client.client.getFileContents(
|
||||||
|
getWebdavPath(fileOrFolderPath, client.vaultName)
|
||||||
)) as BufferLike;
|
)) as BufferLike;
|
||||||
if (buff instanceof ArrayBuffer) {
|
if (buff instanceof ArrayBuffer) {
|
||||||
return buff;
|
return buff;
|
||||||
@@ -176,13 +235,15 @@ const downloadFromRemoteRaw = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const downloadFromRemote = async (
|
export const downloadFromRemote = async (
|
||||||
client: WebDAVClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
mtime: number,
|
mtime: number,
|
||||||
password: string = "",
|
password: string = "",
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
) => {
|
) => {
|
||||||
|
await client.init();
|
||||||
|
|
||||||
const isFolder = fileOrFolderPath.endsWith("/");
|
const isFolder = fileOrFolderPath.endsWith("/");
|
||||||
|
|
||||||
await mkdirpInVault(fileOrFolderPath, vault);
|
await mkdirpInVault(fileOrFolderPath, vault);
|
||||||
@@ -198,7 +259,7 @@ export const downloadFromRemote = async (
|
|||||||
if (password !== "") {
|
if (password !== "") {
|
||||||
downloadFile = remoteEncryptedKey;
|
downloadFile = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
downloadFile = getWebdavPath(downloadFile);
|
downloadFile = getWebdavPath(downloadFile, client.vaultName);
|
||||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||||
let localContent = remoteContent;
|
let localContent = remoteContent;
|
||||||
if (password !== "") {
|
if (password !== "") {
|
||||||
@@ -211,7 +272,7 @@ export const downloadFromRemote = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteFromRemote = async (
|
export const deleteFromRemote = async (
|
||||||
client: WebDAVClient,
|
client: WrappedWebdavClient,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
password: string = "",
|
password: string = "",
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = ""
|
||||||
@@ -223,17 +284,20 @@ export const deleteFromRemote = async (
|
|||||||
if (password !== "") {
|
if (password !== "") {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
}
|
}
|
||||||
remoteFileName = getWebdavPath(remoteFileName);
|
remoteFileName = getWebdavPath(remoteFileName, client.vaultName);
|
||||||
|
|
||||||
|
await client.init();
|
||||||
try {
|
try {
|
||||||
await client.deleteFile(remoteFileName);
|
await client.client.deleteFile(remoteFileName);
|
||||||
console.log(`delete ${remoteFileName} succeeded`);
|
// console.log(`delete ${remoteFileName} succeeded`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("some error while deleting");
|
console.error("some error while deleting");
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkConnectivity = async (client: WebDAVClient) => {
|
export const checkConnectivity = async (client: WrappedWebdavClient) => {
|
||||||
|
await client.init();
|
||||||
try {
|
try {
|
||||||
const results = await getRemoteMeta(client, "/");
|
const results = await getRemoteMeta(client, "/");
|
||||||
if (results === undefined) {
|
if (results === undefined) {
|
||||||
+10
-1
@@ -1,6 +1,10 @@
|
|||||||
/* set the styles */
|
/* set the styles */
|
||||||
|
|
||||||
.password_second_confirm {
|
.password-second-confirm {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-disclaimer {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,3 +36,8 @@
|
|||||||
.webdav-hide {
|
.webdav-hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode-img {
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
import * as misc from "../src/misc";
|
import * as misc from "../src/misc";
|
||||||
|
|
||||||
@@ -133,3 +134,17 @@ describe("Misc: get dirname", () => {
|
|||||||
expect(y).to.equal("/");
|
expect(y).to.equal("/");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Misc: extract svg", () => {
|
||||||
|
beforeEach(function () {
|
||||||
|
const fakeBrowser = new JSDOM("");
|
||||||
|
global.window = fakeBrowser.window as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should extract rect from svg correctly", () => {
|
||||||
|
const x = "<svg><rect/><g/></svg>";
|
||||||
|
const y = misc.extractSvgSub(x);
|
||||||
|
// console.log(x)
|
||||||
|
expect(y).to.equal("<rect/><g/>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"0.1.10": "0.12.15"
|
"0.2.4": "0.12.15"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -33,6 +33,10 @@ module.exports = {
|
|||||||
use: "ts-loader",
|
use: "ts-loader",
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg?$/,
|
||||||
|
type: "asset/source",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -43,7 +47,8 @@ module.exports = {
|
|||||||
// buffer: require.resolve("buffer/"),
|
// buffer: require.resolve("buffer/"),
|
||||||
// console: require.resolve("console-browserify"),
|
// console: require.resolve("console-browserify"),
|
||||||
// constants: require.resolve("constants-browserify"),
|
// constants: require.resolve("constants-browserify"),
|
||||||
crypto: require.resolve("crypto-browserify"),
|
// crypto: require.resolve("crypto-browserify"),
|
||||||
|
crypto: false,
|
||||||
// domain: require.resolve("domain-browser"),
|
// domain: require.resolve("domain-browser"),
|
||||||
// events: require.resolve("events"),
|
// events: require.resolve("events"),
|
||||||
// http: require.resolve("stream-http"),
|
// http: require.resolve("stream-http"),
|
||||||
|
|||||||
Reference in New Issue
Block a user