Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07896f92a3 | ||
|
|
9eddfb02d6 | ||
|
|
f0dd3bb5ef | ||
|
|
b91c0e694c | ||
|
|
84b03d2c2b | ||
|
|
313eff6a14 | ||
|
|
8671ba4660 | ||
|
|
3b195f33c8 | ||
|
|
65e18aa4b7 | ||
|
|
2cedef8168 | ||
|
|
22bc74f4d7 | ||
|
|
277793ccf4 | ||
|
|
9a1b11b811 | ||
|
|
3d6746ed99 | ||
|
|
2a797f8c40 | ||
|
|
0f7b13ec27 | ||
|
|
abd124b155 | ||
|
|
2a42abb9cc | ||
|
|
e9bb61ced4 | ||
|
|
5b703334d7 | ||
|
|
4639b93cfa | ||
|
|
92f12cd5f4 | ||
|
|
72417cd075 | ||
|
|
f8a930eb4e | ||
|
|
6832c9f649 | ||
|
|
34d1b01c7b | ||
|
|
f26ee8bbc5 | ||
|
|
414a3f5e4a | ||
|
|
02e5f9b5a0 | ||
|
|
d53569fe71 | ||
|
|
4865a797aa | ||
|
|
5a5898f315 | ||
|
|
d5ba0a0e0d | ||
|
|
dde7d9a732 | ||
|
|
1bc6ba4b01 | ||
|
|
792703f8b6 | ||
|
|
6552fd32d1 | ||
|
|
f3e6bc2fdd | ||
|
|
63d52df378 | ||
|
|
8833532370 | ||
|
|
8cca96e138 | ||
|
|
804afabdeb |
@@ -1,6 +1,10 @@
|
||||
# Remotely Save
|
||||
|
||||
This is yet another unofficial sync plugin for Obsidian.
|
||||
This is yet another unofficial sync plugin for Obsidian. If you like it or find it useful, please consider give it a [star ](https://github.com/fyears/remotely-save) on Github.
|
||||
|
||||
[](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
|
||||
|
||||
[](https://github.com/fyears/remotely-save/releases) [](https://github.com/fyears/remotely-save/releases) [](https://github.com/fyears/remotely-save/releases)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -8,13 +12,14 @@ This is yet another unofficial sync plugin for Obsidian.
|
||||
|
||||
## !!!Caution!!!
|
||||
|
||||
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.**
|
||||
As of Jan 2022, 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
|
||||
|
||||
- Supports:
|
||||
- Amazon S3 or S3-compatible
|
||||
- Dropbox
|
||||
- OneDrive for personal
|
||||
- Webdav
|
||||
- **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.
|
||||
@@ -43,8 +48,9 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
## Download and Install
|
||||
|
||||
- Option #1: You can search, download, and install the plugin in official "community plugin list".
|
||||
- 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: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
|
||||
- Option #2: You can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. Input `fyears/remotely-save` in the configuration of BRAT.
|
||||
- Option #3: [](https://github.com/fyears/remotely-save/releases) Manually download assets (`main.js`, `manifest.json`, `styles.css`) from the latest release.
|
||||
- Option #4: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. It's automatically generated by every commit, may break something.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -62,25 +68,39 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
|
||||
- **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.
|
||||
- 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.
|
||||
- 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` 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.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
|
||||
### OneDrive for personal
|
||||
|
||||
- **This plugin's function for OneDrive is not as mature as functions for S3.**
|
||||
- **This plugin is NOT an official Microsoft / OneDrive product.** The plugin just uses Microsoft's [OneDrive's public API](https://docs.microsoft.com/en-us/onedrive/developer/rest-api).
|
||||
- This plugin only works for "OneDrive for personal", and not works for "OneDrive for Business" (yet). See [#11](https://github.com/fyears/remotely-save/issues/11) to further details.
|
||||
- After the authorization, the plugin can read your name and email, and read and write files in your OneDrive's `/Apps/remotely-save` folder.
|
||||
- If you decide to authorize this plugin to connect to OneDrive, please go to plugin's settings, and choose OneDrive then follow the instructions.
|
||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||
|
||||
### webdav
|
||||
|
||||
- **webdav support is considered experimental.**
|
||||
- Currently only supports BASIC authorization method.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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
|
||||
## Import And Export Not-Oauth2 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.)
|
||||
It's often verbose / inconvenient to input not-oauth2 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.
|
||||
So this plugin has a helper function to export those settings as a QR code, then you could use mobile device's camera to import the settings.
|
||||
|
||||
Attention:
|
||||
|
||||
1. Oauth2 - related information is omitted. It means that Dropbox, OneDrive login credentials are NOT included in the QR Code.
|
||||
2. Please, NEVER share the QR Code to others. It's as equivalent to the login credentials.
|
||||
|
||||
Steps:
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9bf1cc2403c3a32e7a6f5d5829bb065573922dc050ae2fdb8b3d34b4c87ecf5
|
||||
size 8296
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6990a598379c457442345e50c3f056c4598a121523bc10980835c08fc7447a54
|
||||
size 10119
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:432198e63fa9676541a41436917e025d1ea6af3772139c1dea450fcc0155cfd0
|
||||
size 13054
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fe3b5b8bf9c32e57c0af366c455b11ae68eb9fb8a32f371c9c445b3e7e89cc02
|
||||
size 1120
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f68df7bae94eb4c4cddfa6af47d7819d01a6482d4f6afe6ef3236f72f9adf15f
|
||||
size 1523
|
||||
@@ -0,0 +1 @@
|
||||
This work is licensed under CC BY-SA 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
|
||||
@@ -0,0 +1,75 @@
|
||||
# the logo
|
||||
|
||||
Some different sizes' logos are needed.
|
||||
|
||||
## license
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
|
||||
## the process of creation
|
||||
|
||||
No professional designers are here. Thus the following steps involve many programer styles. :-)
|
||||
|
||||
1. use excalidraw and export png and svg.
|
||||
|
||||
```bash
|
||||
# results
|
||||
logo.excalidraw
|
||||
logo.png
|
||||
logo.svg
|
||||
```
|
||||
|
||||
2. manually edit the `logo.svg` and make background transparent.
|
||||
|
||||
```bash
|
||||
# results
|
||||
logo-transparent.svg
|
||||
```
|
||||
|
||||
3. use python library [`svgutils`](https://github.com/btel/svg_utils) to make a strictly square figure. The [doc](https://svgutils.readthedocs.io/en/latest/tutorials/composing_multipanel_figures.html) is very useful.
|
||||
|
||||
```python
|
||||
from svgutils.compose import *
|
||||
def get_standard_300x300(file_name):
|
||||
fig = Figure(300, 300,
|
||||
Panel(
|
||||
SVG(file_name),
|
||||
).move(-3, 12),
|
||||
)
|
||||
return fig
|
||||
|
||||
get_standard_300x300('logo-transparent.svg').save('300x300.svg')
|
||||
|
||||
# def get_other_size_from_standard(file_name, px):
|
||||
# fig = Figure(px, px,
|
||||
# Panel(
|
||||
# SVG(file_name).scale(px/300.0),
|
||||
# ).move(-3*px/300.0, 12*px/300.0),
|
||||
# )
|
||||
# return fig
|
||||
|
||||
# get_other_size_from_standard('logo.svg',256).save('256x256.svg')
|
||||
```
|
||||
|
||||
```bash
|
||||
# results
|
||||
300x300.svg
|
||||
```
|
||||
|
||||
4. use `inkscape` command line to get different sizes' `.png` files.
|
||||
|
||||
```bash
|
||||
inkscape 300x300.svg -o 300x300.png
|
||||
|
||||
inkscape 300x300.svg -o 50x50.png -w 50 -h 50
|
||||
|
||||
inkscape 300x300.svg -o 64x64.png -w 64 -h 64
|
||||
inkscape 300x300.svg -o 256x256.png -w 256 -h 256
|
||||
```
|
||||
|
||||
```bash
|
||||
# results
|
||||
50x50.png
|
||||
64x64.png
|
||||
256x256.png
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:daedd262170200cb5e8cc288898b40892771b3962cf2bc17dc443f1c82a4aa94
|
||||
size 12886
|
||||
+10
-5
@@ -15,15 +15,20 @@ The encryption algorithm is delibrately designed to be aligned with openssl form
|
||||
openssl enc -d -p -aes-256-cbc -pbkdf2 -iter 20000 -pass pass:somepassword -in ./sometext.txt.enc -out ./sometext.txt
|
||||
```
|
||||
|
||||
3. The file/directory path strings, are encrypted using openssl in binary mode and then `base32` is applied.
|
||||
3. The file/directory path strings, are encrypted using openssl in binary mode and then `base64url without padding` is applied.
|
||||
Assuming the file path is `a-folder-文件夹/a-file-文件.md`, then the following commands are equivilent:
|
||||
|
||||
```bash
|
||||
# pure string encryption then base32
|
||||
echo -n 'a-folder-文件夹/a-file-文件.md' | openssl enc -aes-256-cbc -pbkdf2 -iter 20000 -pass pass:mylongpassword | base32 -w 0
|
||||
# prepare the functions
|
||||
# https://unix.stackexchange.com/questions/628842
|
||||
base64url::encode () { base64 -w0 | tr '+/' '-_' | tr -d '='; }
|
||||
base64url::decode () { awk '{ if (length($0) % 4 == 3) print $0"="; else if (length($0) % 4 == 2) print $0"=="; else print $0; }' | tr -- '-_' '+/' | base64 -d; }
|
||||
|
||||
# pure string base32 then decryption
|
||||
echo -n 'KNQWY5DFMRPV7UHRWVYFSHE2XVVVZCFN65SR7ETEKO5L6EYGXCVEPT4A2LVTW4W2ZHXWF3K22SVA562CCZ6SALARXJY6AAXXHLK5UOA=' | base32 -d -w 00 | openssl enc -d -aes-256-cbc -pbkdf2 -iter 20000 -pass pass:mylongpassword
|
||||
# pure string encryption then base32
|
||||
echo -n 'a-folder-文件夹/a-file-文件.md' | openssl enc -aes-256-cbc -pbkdf2 -iter 20000 -pass pass:mylongpassword | base64url::encode
|
||||
|
||||
# pure string base64url then decryption
|
||||
echo -n 'U2FsdGVkX19tNkdFL5rZeHxbe7FL-Pp5mkZJkDNFJWFT6lldZlfa57j0C_cKn0I3PZ9YDvOkyoKqfF6lbn0_yg' | base64url::decode | openssl enc -d -aes-256-cbc -pbkdf2 -iter 20000 -pass pass:mylongpassword
|
||||
```
|
||||
|
||||
4. The directory is considered as special "0-byte" object on remote s3. So this meta infomation may be easily guessed if some third party can access the remote bucket.
|
||||
|
||||
@@ -12,6 +12,8 @@ if you want to view the source, please visit the github repository of this plugi
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||
const DEFAULT_ONEDRIVE_CLIENT_ID = process.env.ONEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_ONEDRIVE_AUTHORITY = process.env.ONEDRIVE_AUTHORITY || "";
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
@@ -40,6 +42,8 @@ esbuild
|
||||
outfile: "main.js",
|
||||
define: {
|
||||
"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}"`,
|
||||
},
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.9",
|
||||
"minAppVersion": "0.12.15",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.9",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
@@ -13,16 +13,19 @@
|
||||
"browser": {
|
||||
"path": "path-browserify",
|
||||
"process": "process/browser",
|
||||
"stream": "stream-browserify"
|
||||
"stream": "stream-browserify",
|
||||
"crypto": "crypto-browserify"
|
||||
},
|
||||
"source": "main.ts",
|
||||
"keywords": [],
|
||||
"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",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.37",
|
||||
@@ -48,6 +51,8 @@
|
||||
"@aws-sdk/client-s3": "^3.37.0",
|
||||
"@aws-sdk/lib-storage": "^3.40.1",
|
||||
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
||||
"@azure/msal-node": "^1.4.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
||||
"acorn": "^8.5.0",
|
||||
"assert": "^2.0.0",
|
||||
"aws-crt": "^1.10.1",
|
||||
@@ -56,6 +61,8 @@
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"dropbox": "^10.22.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"loglevel": "^1.8.0",
|
||||
"mime-types": "^2.1.33",
|
||||
"obsidian": "^0.12.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
|
||||
+21
-1
@@ -3,7 +3,7 @@
|
||||
* To avoid circular dependency.
|
||||
*/
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive";
|
||||
|
||||
export interface S3Config {
|
||||
s3Endpoint: string;
|
||||
@@ -21,6 +21,7 @@ export interface DropboxConfig {
|
||||
accessTokenExpiresAtTime: number;
|
||||
accountID: string;
|
||||
username: string;
|
||||
credentialsShouldBeDeletedAtTime?: number;
|
||||
}
|
||||
|
||||
export type WebdavAuthType = "digest" | "basic";
|
||||
@@ -32,12 +33,26 @@ export interface WebdavConfig {
|
||||
authType: WebdavAuthType;
|
||||
}
|
||||
|
||||
export interface OnedriveConfig {
|
||||
accessToken: string;
|
||||
clientID: string;
|
||||
authority: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
accessTokenExpiresAtTime: number;
|
||||
deltaLink: string;
|
||||
username: string;
|
||||
credentialsShouldBeDeletedAtTime?: number;
|
||||
}
|
||||
|
||||
export interface RemotelySavePluginSettings {
|
||||
s3: S3Config;
|
||||
webdav: WebdavConfig;
|
||||
dropbox: DropboxConfig;
|
||||
onedrive: OnedriveConfig;
|
||||
password: string;
|
||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
currLogLevel?: string;
|
||||
}
|
||||
|
||||
export interface RemoteItem {
|
||||
@@ -50,6 +65,8 @@ export interface RemoteItem {
|
||||
|
||||
export const COMMAND_URI = "remotely-save";
|
||||
export const COMMAND_CALLBACK = "remotely-save-cb";
|
||||
export const COMMAND_CALLBACK_ONEDRIVE = "remotely-save-cb-onedrive";
|
||||
export const COMMAND_CALLBACK_DROPBOX = "remotely-save-cb-dropbox";
|
||||
|
||||
export interface UriParams {
|
||||
func?: string;
|
||||
@@ -57,3 +74,6 @@ export interface UriParams {
|
||||
ver?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
// 80 days
|
||||
export const OAUTH2_FORCE_EXPIRE_MILLISECONDS = 1000 * 60 * 60 * 24 * 80;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { base64, base64url } from "rfc4648";
|
||||
import { reverseString } from "./misc";
|
||||
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_README: string =
|
||||
"Do NOT modify this manually. It's generated automatically.";
|
||||
|
||||
interface MessyConfigType {
|
||||
readme: string;
|
||||
d: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* this should accept the result after loadData();
|
||||
*/
|
||||
export const messyConfigToNormal = (
|
||||
x: MessyConfigType | RemotelySavePluginSettings
|
||||
): RemotelySavePluginSettings => {
|
||||
log.debug("loading, original config on disk:");
|
||||
log.debug(x);
|
||||
if ("readme" in x && "d" in x) {
|
||||
// we should decode
|
||||
const y = JSON.parse(
|
||||
(
|
||||
base64url.parse(reverseString(x["d"]), {
|
||||
out: Buffer.allocUnsafe as any,
|
||||
loose: true,
|
||||
}) as Buffer
|
||||
).toString("utf-8")
|
||||
);
|
||||
log.debug("loading, parsed config is:");
|
||||
log.debug(y);
|
||||
return y;
|
||||
} else {
|
||||
// return as is
|
||||
log.debug("loading, parsed config is the same");
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* this should accept the result of original config
|
||||
*/
|
||||
export const normalConfigToMessy = (x: RemotelySavePluginSettings) => {
|
||||
const y = {
|
||||
readme: DEFAULT_README,
|
||||
d: reverseString(
|
||||
base64url.stringify(Buffer.from(JSON.stringify(x), "utf-8"), {
|
||||
pad: false,
|
||||
})
|
||||
),
|
||||
};
|
||||
log.debug("encoding, encoded config is:");
|
||||
log.debug(y);
|
||||
return y;
|
||||
};
|
||||
+5
-2
@@ -5,11 +5,14 @@ import { readAllSyncPlanRecordTexts } from "./localdb";
|
||||
import type { InternalDBs } from "./localdb";
|
||||
import { mkdirpInVault } from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_DEBUG_FOLDER = "_debug_remotely_save/";
|
||||
const DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX = "sync_plans_hist_exported_on_";
|
||||
|
||||
export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
|
||||
console.log("exporting");
|
||||
log.info("exporting");
|
||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||
const records = await readAllSyncPlanRecordTexts(db);
|
||||
let md = "";
|
||||
@@ -25,5 +28,5 @@ export const exportSyncPlansToFiles = async (db: InternalDBs, vault: Vault) => {
|
||||
await vault.create(filePath, md, {
|
||||
mtime: ts,
|
||||
});
|
||||
console.log("finish exporting");
|
||||
log.info("finish exporting");
|
||||
};
|
||||
|
||||
+38
-9
@@ -1,15 +1,15 @@
|
||||
import { base32, base64 } from "rfc4648";
|
||||
import {
|
||||
bufferToArrayBuffer,
|
||||
arrayBufferToBuffer,
|
||||
hexStringToTypedArray,
|
||||
arrayBufferToHex,
|
||||
} from "./misc";
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import { bufferToArrayBuffer, hexStringToTypedArray } from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const DEFAULT_ITER = 20000;
|
||||
|
||||
// base32.stringify(Buffer.from('Salted__'))
|
||||
export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV";
|
||||
// base64.stringify(Buffer.from('Salted__'))
|
||||
export const MAGIC_ENCRYPTED_PREFIX_BASE64URL = "U2FsdGVkX";
|
||||
|
||||
const getKeyIVFromPassword = async (
|
||||
salt: Uint8Array,
|
||||
@@ -120,7 +120,7 @@ export const encryptStringToBase32 = async (
|
||||
rounds,
|
||||
saltHex
|
||||
);
|
||||
return base32.stringify(new Uint8Array(enc));
|
||||
return base32.stringify(new Uint8Array(enc), { pad: false });
|
||||
};
|
||||
|
||||
export const decryptBase32ToString = async (
|
||||
@@ -130,7 +130,36 @@ export const decryptBase32ToString = async (
|
||||
) => {
|
||||
return new TextDecoder().decode(
|
||||
await decryptArrayBuffer(
|
||||
bufferToArrayBuffer(base32.parse(text)),
|
||||
bufferToArrayBuffer(base32.parse(text, { loose: true })),
|
||||
password,
|
||||
rounds
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const encryptStringToBase64url = async (
|
||||
text: string,
|
||||
password: string,
|
||||
rounds: number = DEFAULT_ITER,
|
||||
saltHex: string = ""
|
||||
) => {
|
||||
const enc = await encryptArrayBuffer(
|
||||
bufferToArrayBuffer(new TextEncoder().encode(text)),
|
||||
password,
|
||||
rounds,
|
||||
saltHex
|
||||
);
|
||||
return base64url.stringify(new Uint8Array(enc), { pad: false });
|
||||
};
|
||||
|
||||
export const decryptBase64urlToString = async (
|
||||
text: string,
|
||||
password: string,
|
||||
rounds: number = DEFAULT_ITER
|
||||
) => {
|
||||
return new TextDecoder().decode(
|
||||
await decryptArrayBuffer(
|
||||
bufferToArrayBuffer(base64url.parse(text, { loose: true })),
|
||||
password,
|
||||
rounds
|
||||
)
|
||||
|
||||
+9
-2
@@ -1,4 +1,5 @@
|
||||
import QRCode from "qrcode";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
|
||||
import {
|
||||
COMMAND_URI,
|
||||
@@ -6,16 +7,22 @@ import {
|
||||
RemotelySavePluginSettings,
|
||||
} from "./baseTypes";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const exportQrCodeUri = async (
|
||||
settings: RemotelySavePluginSettings,
|
||||
currentVaultName: string,
|
||||
pluginVersion: string
|
||||
) => {
|
||||
const data = encodeURIComponent(JSON.stringify(settings));
|
||||
const settings2 = cloneDeep(settings);
|
||||
delete settings2.dropbox;
|
||||
delete settings2.onedrive;
|
||||
const data = encodeURIComponent(JSON.stringify(settings2));
|
||||
const vault = encodeURIComponent(currentVaultName);
|
||||
const version = encodeURIComponent(pluginVersion);
|
||||
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
|
||||
// console.log(uri)
|
||||
// log.info(uri)
|
||||
const imgUri = await QRCode.toDataURL(rawUri);
|
||||
return {
|
||||
rawUri,
|
||||
|
||||
+8
-5
@@ -6,6 +6,9 @@ import type { SyncPlanType } from "./sync";
|
||||
|
||||
export type LocalForage = typeof localforage;
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_DB_VERSION_NUMBER: number = 20211114;
|
||||
export const DEFAULT_DB_NAME = "remotelysavedb";
|
||||
export const DEFAULT_TBL_VERSION = "schemaversion";
|
||||
@@ -78,7 +81,7 @@ export const prepareDBs = async () => {
|
||||
await migrateDBs(db, originalVersion, DEFAULT_DB_VERSION_NUMBER);
|
||||
}
|
||||
|
||||
console.log("db connected");
|
||||
log.info("db connected");
|
||||
return db;
|
||||
};
|
||||
|
||||
@@ -86,10 +89,10 @@ export const destroyDBs = async () => {
|
||||
// await localforage.dropInstance({
|
||||
// name: DEFAULT_DB_NAME,
|
||||
// });
|
||||
// console.log("db deleted");
|
||||
// log.info("db deleted");
|
||||
const req = indexedDB.deleteDatabase(DEFAULT_DB_NAME);
|
||||
req.onsuccess = (event) => {
|
||||
console.log("db deleted");
|
||||
log.info("db deleted");
|
||||
};
|
||||
req.onblocked = (event) => {
|
||||
console.warn("trying to delete db but it was blocked");
|
||||
@@ -128,7 +131,7 @@ export const insertDeleteRecord = async (
|
||||
db: InternalDBs,
|
||||
fileOrFolder: TAbstractFile
|
||||
) => {
|
||||
// console.log(fileOrFolder);
|
||||
// log.info(fileOrFolder);
|
||||
let k: FileFolderHistoryRecord;
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k = {
|
||||
@@ -165,7 +168,7 @@ export const insertRenameRecord = async (
|
||||
fileOrFolder: TAbstractFile,
|
||||
oldPath: string
|
||||
) => {
|
||||
// console.log(fileOrFolder);
|
||||
// log.info(fileOrFolder);
|
||||
let k: FileFolderHistoryRecord;
|
||||
if (fileOrFolder instanceof TFile) {
|
||||
k = {
|
||||
|
||||
+304
-779
File diff suppressed because it is too large
Load Diff
+51
-6
@@ -1,9 +1,12 @@
|
||||
import { Vault } from "obsidian";
|
||||
import * as path from "path";
|
||||
|
||||
import { base32 } from "rfc4648";
|
||||
import { base32, base64url } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
/**
|
||||
* If any part of the file starts with '.' or '_' then it's a hidden file.
|
||||
* @param item
|
||||
@@ -13,7 +16,7 @@ import XRegExp from "xregexp";
|
||||
export const isHiddenPath = (item: string, loose: boolean = true) => {
|
||||
const k = path.posix.normalize(item); // TODO: only unix path now
|
||||
const k2 = k.split("/"); // TODO: only unix path now
|
||||
// console.log(k2)
|
||||
// log.info(k2)
|
||||
for (const singlePart of k2) {
|
||||
if (singlePart === "." || singlePart === ".." || singlePart === "") {
|
||||
continue;
|
||||
@@ -54,14 +57,14 @@ export const getFolderLevels = (x: string) => {
|
||||
};
|
||||
|
||||
export const mkdirpInVault = async (thePath: string, vault: Vault) => {
|
||||
// console.log(thePath);
|
||||
// log.info(thePath);
|
||||
const foldersToBuild = getFolderLevels(thePath);
|
||||
// console.log(foldersToBuild);
|
||||
// log.info(foldersToBuild);
|
||||
for (const folder of foldersToBuild) {
|
||||
const r = await vault.adapter.exists(folder);
|
||||
// console.log(r);
|
||||
// log.info(r);
|
||||
if (!r) {
|
||||
console.log(`mkdir ${folder}`);
|
||||
log.info(`mkdir ${folder}`);
|
||||
await vault.adapter.mkdir(folder);
|
||||
}
|
||||
}
|
||||
@@ -114,6 +117,14 @@ export const base64ToBase32 = (a: string) => {
|
||||
return base32.stringify(Buffer.from(a, "base64"));
|
||||
};
|
||||
|
||||
export const base64ToBase64url = (a: string, pad: boolean = false) => {
|
||||
let b = a.replace(/\+/g, "-").replace(/\//g, "_");
|
||||
if (!pad) {
|
||||
b = b.replace(/=/g, "");
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
/**
|
||||
* iOS Safari could decrypt string with invalid password!
|
||||
* So we need an extra way to test the decrypted result.
|
||||
@@ -163,3 +174,37 @@ export const extractSvgSub = (x: string, subEl: string = "rect") => {
|
||||
svg.setAttribute("viewbox", "0 0 10 10");
|
||||
return svg.innerHTML;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/18230217
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomIntInclusive = (min: number, max: number) => {
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(randomBuffer);
|
||||
let randomNumber = randomBuffer[0] / (0xffffffff + 1);
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(randomNumber * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
/**
|
||||
* Random buffer
|
||||
* @param byteLength
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomArrayBuffer = (byteLength: number) => {
|
||||
const k = window.crypto.getRandomValues(new Uint8Array(byteLength));
|
||||
return bufferToArrayBuffer(k);
|
||||
};
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/958908
|
||||
* @param x
|
||||
* @returns
|
||||
*/
|
||||
export const reverseString = (x: string) => {
|
||||
return [...x].reverse().join("");
|
||||
};
|
||||
|
||||
+60
-4
@@ -1,14 +1,18 @@
|
||||
import { Vault } from "obsidian";
|
||||
|
||||
import type {
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
S3Config,
|
||||
DropboxConfig,
|
||||
OnedriveConfig,
|
||||
S3Config,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
WebdavConfig,
|
||||
} from "./baseTypes";
|
||||
import * as dropbox from "./remoteForDropbox";
|
||||
import * as onedrive from "./remoteForOnedrive";
|
||||
import * as s3 from "./remoteForS3";
|
||||
import * as webdav from "./remoteForWebdav";
|
||||
import * as dropbox from "./remoteForDropbox";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export class RemoteClient {
|
||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
@@ -18,12 +22,15 @@ export class RemoteClient {
|
||||
readonly webdavConfig?: WebdavConfig;
|
||||
readonly dropboxClient?: dropbox.WrappedDropboxClient;
|
||||
readonly dropboxConfig?: DropboxConfig;
|
||||
readonly onedriveClient?: onedrive.WrappedOnedriveClient;
|
||||
readonly onedriveConfig?: OnedriveConfig;
|
||||
|
||||
constructor(
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
s3Config?: S3Config,
|
||||
webdavConfig?: WebdavConfig,
|
||||
dropboxConfig?: DropboxConfig,
|
||||
onedriveConfig?: OnedriveConfig,
|
||||
vaultName?: string,
|
||||
saveUpdatedConfigFunc?: () => Promise<any>
|
||||
) {
|
||||
@@ -51,6 +58,18 @@ export class RemoteClient {
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
} else if (serviceType === "onedrive") {
|
||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||
throw Error(
|
||||
"remember to provide vault name and callback while init onedrive client"
|
||||
);
|
||||
}
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.onedriveClient = onedrive.getOnedriveClient(
|
||||
this.onedriveConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -67,6 +86,11 @@ export class RemoteClient {
|
||||
return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.getRemoteMeta(this.dropboxClient, fileOrFolderPath);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.getRemoteMeta(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -109,6 +133,16 @@ export class RemoteClient {
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.uploadToRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
isRecursively,
|
||||
password,
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -121,6 +155,8 @@ export class RemoteClient {
|
||||
return await webdav.listFromRemote(this.webdavClient, prefix);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.listFromRemote(this.dropboxClient, prefix);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.listFromRemote(this.onedriveClient, prefix);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -161,6 +197,15 @@ export class RemoteClient {
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.downloadFromRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
mtime,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -193,6 +238,13 @@ export class RemoteClient {
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.deleteFromRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -205,6 +257,8 @@ export class RemoteClient {
|
||||
return await webdav.checkConnectivity(this.webdavClient);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.checkConnectivity(this.dropboxClient);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.checkConnectivity(this.onedriveClient);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@@ -213,6 +267,8 @@ export class RemoteClient {
|
||||
getUser = async () => {
|
||||
if (this.serviceType === "dropbox") {
|
||||
return await dropbox.getUserDisplayName(this.dropboxClient);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.getUserDisplayName(this.onedriveClient);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
|
||||
+31
-24
@@ -1,20 +1,21 @@
|
||||
import { Dropbox, DropboxAuth, files } from "dropbox";
|
||||
import { Vault } from "obsidian";
|
||||
import * as path from "path";
|
||||
import { FileStats, Vault } from "obsidian";
|
||||
|
||||
import { Dropbox, DropboxAuth, DropboxResponse, files } from "dropbox";
|
||||
export { Dropbox } from "dropbox";
|
||||
import { RemoteItem, DropboxConfig } from "./baseTypes";
|
||||
import {
|
||||
arrayBufferToBuffer,
|
||||
bufferToArrayBuffer,
|
||||
mkdirpInVault,
|
||||
getPathFolder,
|
||||
getFolderLevels,
|
||||
setToString,
|
||||
} from "./misc";
|
||||
DropboxConfig,
|
||||
RemoteItem,
|
||||
COMMAND_CALLBACK_DROPBOX,
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
} from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import { bufferToArrayBuffer, getFolderLevels, mkdirpInVault } from "./misc";
|
||||
|
||||
export const DEFAULT_DROPBOX_CONFIG = {
|
||||
export { Dropbox } from "dropbox";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
|
||||
accessToken: "",
|
||||
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
|
||||
refreshToken: "",
|
||||
@@ -22,6 +23,7 @@ export const DEFAULT_DROPBOX_CONFIG = {
|
||||
accessTokenExpiresAtTime: 0,
|
||||
accountID: "",
|
||||
username: "",
|
||||
credentialsShouldBeDeletedAtTime: 0,
|
||||
};
|
||||
|
||||
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
@@ -160,7 +162,7 @@ export const getAuthUrlAndVerifier = async (appKey: string) => {
|
||||
});
|
||||
const authUrl = (
|
||||
await auth.getAuthenticationUrl(
|
||||
undefined,
|
||||
`obsidian://${COMMAND_CALLBACK_DROPBOX}`,
|
||||
undefined,
|
||||
"code",
|
||||
"offline",
|
||||
@@ -198,6 +200,7 @@ export const sendAuthReq = async (
|
||||
grant_type: "authorization_code",
|
||||
code_verifier: verifier,
|
||||
client_id: appKey,
|
||||
redirect_uri: `obsidian://${COMMAND_CALLBACK_DROPBOX}`,
|
||||
}),
|
||||
});
|
||||
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
|
||||
@@ -208,7 +211,7 @@ export const sendRefreshTokenReq = async (
|
||||
appKey: string,
|
||||
refreshToken: string
|
||||
) => {
|
||||
console.log("start auto getting refreshed Dropbox access token.");
|
||||
log.info("start auto getting refreshed Dropbox access token.");
|
||||
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams({
|
||||
@@ -218,7 +221,7 @@ export const sendRefreshTokenReq = async (
|
||||
}),
|
||||
});
|
||||
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
|
||||
console.log("finish auto getting refreshed Dropbox access token.");
|
||||
log.info("finish auto getting refreshed Dropbox access token.");
|
||||
return resp2;
|
||||
};
|
||||
|
||||
@@ -227,13 +230,17 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
authRes: DropboxSuccessAuthRes,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
console.log("start updating local info of Dropbox token");
|
||||
log.info("start updating local info of Dropbox token");
|
||||
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresInSeconds = parseInt(authRes.expires_in);
|
||||
config.accessTokenExpiresAtTime =
|
||||
Date.now() + parseInt(authRes.expires_in) * 1000 - 10 * 1000;
|
||||
|
||||
// manually set it expired after 80 days;
|
||||
config.credentialsShouldBeDeletedAtTime =
|
||||
Date.now() + OAUTH2_FORCE_EXPIRE_MILLISECONDS;
|
||||
|
||||
if (authRes.refresh_token !== undefined) {
|
||||
config.refreshToken = authRes.refresh_token;
|
||||
}
|
||||
@@ -245,7 +252,7 @@ export const setConfigBySuccessfullAuthInplace = async (
|
||||
await saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
console.log("finish updating local info of Dropbox token");
|
||||
log.info("finish updating local info of Dropbox token");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -304,9 +311,9 @@ export class WrappedDropboxClient {
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// console.log(`checking remote has folder /${this.vaultName}`);
|
||||
// log.info(`checking remote has folder /${this.vaultName}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// console.log(`already checked, /${this.vaultName} exist before`)
|
||||
// log.info(`already checked, /${this.vaultName} exist before`)
|
||||
} else {
|
||||
const res = await this.dropbox.filesListFolder({
|
||||
path: "",
|
||||
@@ -319,14 +326,14 @@ export class WrappedDropboxClient {
|
||||
}
|
||||
}
|
||||
if (!this.vaultFolderExists) {
|
||||
console.log(`remote does not have folder /${this.vaultName}`);
|
||||
log.info(`remote does not have folder /${this.vaultName}`);
|
||||
await this.dropbox.filesCreateFolderV2({
|
||||
path: `/${this.vaultName}`,
|
||||
});
|
||||
console.log(`remote folder /${this.vaultName} created`);
|
||||
log.info(`remote folder /${this.vaultName} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// console.log(`remote folder /${this.vaultName} exists`);
|
||||
// log.info(`remote folder /${this.vaultName} exists`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +492,7 @@ export const listFromRemote = async (
|
||||
if (res.status !== 200) {
|
||||
throw Error(JSON.stringify(res));
|
||||
}
|
||||
// console.log(res);
|
||||
// log.info(res);
|
||||
|
||||
const contents = res.result.entries;
|
||||
const unifiedContents = contents
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
|
||||
import {
|
||||
AuthenticationProvider,
|
||||
Client,
|
||||
FileUpload,
|
||||
LargeFileUploadSession,
|
||||
LargeFileUploadTask,
|
||||
LargeFileUploadTaskOptions,
|
||||
Range,
|
||||
UploadEventHandlers,
|
||||
UploadResult,
|
||||
} from "@microsoft/microsoft-graph-client";
|
||||
import type { DriveItem, User } from "@microsoft/microsoft-graph-types";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { request, Vault } from "obsidian";
|
||||
import * as path from "path";
|
||||
import {
|
||||
DropboxConfig,
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
OnedriveConfig,
|
||||
RemoteItem,
|
||||
} from "./baseTypes";
|
||||
import { COMMAND_CALLBACK_ONEDRIVE } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import {
|
||||
getRandomArrayBuffer,
|
||||
getRandomIntInclusive,
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
|
||||
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
|
||||
|
||||
export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = {
|
||||
accessToken: "",
|
||||
clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID,
|
||||
authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY,
|
||||
refreshToken: "",
|
||||
accessTokenExpiresInSeconds: 0,
|
||||
accessTokenExpiresAtTime: 0,
|
||||
deltaLink: "",
|
||||
username: "",
|
||||
credentialsShouldBeDeletedAtTime: 0,
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Onedrive authorization using PKCE
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export async function getAuthUrlAndVerifier(
|
||||
clientID: string,
|
||||
authority: string
|
||||
) {
|
||||
const cryptoProvider = new CryptoProvider();
|
||||
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
|
||||
|
||||
const pkceCodes = {
|
||||
challengeMethod: "S256", // Use SHA256 Algorithm
|
||||
verifier: verifier,
|
||||
challenge: challenge,
|
||||
};
|
||||
|
||||
const authCodeUrlParams = {
|
||||
redirectUri: REDIRECT_URI,
|
||||
scopes: SCOPES,
|
||||
codeChallenge: pkceCodes.challenge, // PKCE Code Challenge
|
||||
codeChallengeMethod: pkceCodes.challengeMethod, // PKCE Code Challenge Method
|
||||
};
|
||||
|
||||
const pca = new PublicClientApplication({
|
||||
auth: {
|
||||
clientId: clientID,
|
||||
authority: authority,
|
||||
},
|
||||
});
|
||||
const authCodeUrl = await pca.getAuthCodeUrl(authCodeUrlParams);
|
||||
|
||||
return {
|
||||
authUrl: authCodeUrl,
|
||||
verifier: verifier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check doc from
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
* https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow
|
||||
*/
|
||||
export interface AccessCodeResponseSuccessfulType {
|
||||
token_type: "Bearer" | "bearer";
|
||||
expires_in: number;
|
||||
ext_expires_in?: number;
|
||||
scope: string;
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
id_token?: string;
|
||||
}
|
||||
export interface AccessCodeResponseFailedType {
|
||||
error: string;
|
||||
error_description: string;
|
||||
error_codes: number[];
|
||||
timestamp: string;
|
||||
trace_id: string;
|
||||
correlation_id: string;
|
||||
}
|
||||
|
||||
export const sendAuthReq = async (
|
||||
clientID: string,
|
||||
authority: string,
|
||||
authCode: string,
|
||||
verifier: string
|
||||
) => {
|
||||
// // original code snippets for references
|
||||
// const authResponse = await pca.acquireTokenByCode({
|
||||
// redirectUri: REDIRECT_URI,
|
||||
// scopes: SCOPES,
|
||||
// code: authCode,
|
||||
// codeVerifier: verifier, // PKCE Code Verifier
|
||||
// });
|
||||
// log.info('authResponse')
|
||||
// log.info(authResponse)
|
||||
// return authResponse;
|
||||
|
||||
// Because of the CORS problem,
|
||||
// we need to construct raw request using Obsidian request,
|
||||
// instead of using msal
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow
|
||||
const rsp1 = await request({
|
||||
url: `${authority}/oauth2/v2.0/token`,
|
||||
method: "POST",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: new URLSearchParams({
|
||||
tenant: "consumers",
|
||||
client_id: clientID,
|
||||
scope: SCOPES.join(" "),
|
||||
code: authCode,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
grant_type: "authorization_code",
|
||||
code_verifier: verifier,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// log.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendRefreshTokenReq = async (
|
||||
clientID: string,
|
||||
authority: string,
|
||||
refreshToken: string
|
||||
) => {
|
||||
// also use Obsidian request to bypass CORS issue.
|
||||
const rsp1 = await request({
|
||||
url: `${authority}/oauth2/v2.0/token`,
|
||||
method: "POST",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: new URLSearchParams({
|
||||
tenant: "consumers",
|
||||
client_id: clientID,
|
||||
scope: SCOPES.join(" "),
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// log.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
};
|
||||
|
||||
export const setConfigBySuccessfullAuthInplace = async (
|
||||
config: OnedriveConfig,
|
||||
authRes: AccessCodeResponseSuccessfulType,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
log.info("start updating local info of OneDrive token");
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresAtTime =
|
||||
Date.now() + authRes.expires_in - 5 * 60 * 1000;
|
||||
config.accessTokenExpiresInSeconds = authRes.expires_in;
|
||||
config.refreshToken = authRes.refresh_token;
|
||||
|
||||
// manually set it expired after 80 days;
|
||||
config.credentialsShouldBeDeletedAtTime =
|
||||
Date.now() + OAUTH2_FORCE_EXPIRE_MILLISECONDS;
|
||||
|
||||
if (saveUpdatedConfigFunc !== undefined) {
|
||||
await saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
log.info("finish updating local info of Onedrive token");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Other usual common methods
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/special-folders-appfolder?view=odsp-graph-online
|
||||
const prefix = `/drive/special/approot:/${vaultName}`;
|
||||
if (fileOrFolderPath.startsWith(prefix)) {
|
||||
// already transformed, return as is
|
||||
return fileOrFolderPath;
|
||||
}
|
||||
|
||||
let key = fileOrFolderPath;
|
||||
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
|
||||
// special
|
||||
return prefix;
|
||||
}
|
||||
if (key.endsWith("/")) {
|
||||
key = key.slice(0, key.length - 1);
|
||||
}
|
||||
|
||||
key = `${prefix}/${key}`;
|
||||
return key;
|
||||
};
|
||||
|
||||
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
const prefix = `/drive/special/approot:/${vaultName}`;
|
||||
|
||||
if (
|
||||
!(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`))
|
||||
) {
|
||||
throw Error(
|
||||
`"${fileOrFolderPath}" doesn't starts with "${prefix}/" or equals to "${prefix}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (fileOrFolderPath === prefix) {
|
||||
return "/";
|
||||
}
|
||||
return fileOrFolderPath.slice(`${prefix}/`.length);
|
||||
};
|
||||
|
||||
const constructFromDriveItemToRemoteItemError = (x: DriveItem) => {
|
||||
return `parentPath="${x.parentReference.path}", selfName="${x.name}"`;
|
||||
};
|
||||
|
||||
const fromDriveItemToRemoteItem = (
|
||||
x: DriveItem,
|
||||
vaultName: string
|
||||
): RemoteItem => {
|
||||
let key = "";
|
||||
|
||||
// possible prefix:
|
||||
// pure english: /drive/root:/Apps/remotely-save/${vaultName}
|
||||
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName}
|
||||
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
|
||||
|
||||
// another possibile prefix
|
||||
const SECOND_COMMON_PREFIX_RAW = `/drive/items/`;
|
||||
|
||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||
if (
|
||||
matchFirstPrefixRes !== null &&
|
||||
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`)
|
||||
) {
|
||||
const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`;
|
||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||
} else if (x.parentReference.path.startsWith(SECOND_COMMON_PREFIX_RAW)) {
|
||||
// it's something like
|
||||
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
||||
// with uri encoded!
|
||||
const parPath = decodeURIComponent(x.parentReference.path);
|
||||
key = parPath.substring(parPath.indexOf(":") + 1);
|
||||
if (key.startsWith(`/${vaultName}/`)) {
|
||||
key = key.substring(`/${vaultName}/`.length);
|
||||
key = `${key}/${x.name}`;
|
||||
} else if (key === `/${vaultName}`) {
|
||||
key = x.name;
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToRemoteItemError(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToRemoteItemError(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const isFolder = "folder" in x;
|
||||
if (isFolder) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
return {
|
||||
key: key,
|
||||
lastModified: Date.parse(x.fileSystemInfo.lastModifiedDateTime),
|
||||
size: isFolder ? 0 : x.size,
|
||||
remoteType: "onedrive",
|
||||
etag: x.eTag || x.cTag || "",
|
||||
};
|
||||
};
|
||||
|
||||
// to adapt to the required interface
|
||||
class MyAuthProvider implements AuthenticationProvider {
|
||||
onedriveConfig: OnedriveConfig;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveConfig: OnedriveConfig,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
}
|
||||
getAccessToken = async () => {
|
||||
if (
|
||||
this.onedriveConfig.accessToken === "" ||
|
||||
this.onedriveConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
const currentTs = Date.now();
|
||||
if (this.onedriveConfig.accessTokenExpiresAtTime > currentTs) {
|
||||
return this.onedriveConfig.accessToken;
|
||||
} else {
|
||||
// use refreshToken to refresh
|
||||
const r = await sendRefreshTokenReq(
|
||||
this.onedriveConfig.clientID,
|
||||
this.onedriveConfig.authority,
|
||||
this.onedriveConfig.refreshToken
|
||||
);
|
||||
if ((r as any).error !== undefined) {
|
||||
const r2 = r as AccessCodeResponseFailedType;
|
||||
throw Error(
|
||||
`Error while refreshing accessToken: ${r2.error}, ${r2.error_codes}: ${r2.error_description}`
|
||||
);
|
||||
}
|
||||
const r2 = r as AccessCodeResponseSuccessfulType;
|
||||
this.onedriveConfig.accessToken = r2.access_token;
|
||||
this.onedriveConfig.refreshToken = r2.refresh_token;
|
||||
this.onedriveConfig.accessTokenExpiresInSeconds = r2.expires_in;
|
||||
this.onedriveConfig.accessTokenExpiresAtTime =
|
||||
currentTs + r2.expires_in * 1000 - 60 * 2 * 1000;
|
||||
await this.saveUpdatedConfigFunc();
|
||||
log.info("Onedrive accessToken updated");
|
||||
return this.onedriveConfig.accessToken;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class WrappedOnedriveClient {
|
||||
onedriveConfig: OnedriveConfig;
|
||||
vaultName: string;
|
||||
client: Client;
|
||||
vaultFolderExists: boolean;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.vaultName = vaultName;
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
this.client = Client.initWithMiddleware({
|
||||
authProvider: new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc),
|
||||
});
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
// check token
|
||||
if (
|
||||
this.onedriveConfig.accessToken === "" ||
|
||||
this.onedriveConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// log.info(`checking remote has folder /${this.vaultName}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// log.info(`already checked, /${this.vaultName} exist before`)
|
||||
} else {
|
||||
const k = await this.client.api("/drive/special/approot/children").get();
|
||||
// log.info(k);
|
||||
this.vaultFolderExists =
|
||||
(k.value as DriveItem[]).filter((x) => x.name === this.vaultName)
|
||||
.length > 0;
|
||||
if (!this.vaultFolderExists) {
|
||||
log.info(`remote does not have folder /${this.vaultName}`);
|
||||
await this.client.api("/drive/special/approot/children").post({
|
||||
name: `${this.vaultName}`,
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
log.info(`remote folder /${this.vaultName} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// log.info(`remote folder /${this.vaultName} exists`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getOnedriveClient = (
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) => {
|
||||
return new WrappedOnedriveClient(
|
||||
onedriveConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use delta api to list all files and folders
|
||||
* https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta?view=odsp-graph-online
|
||||
* @param client
|
||||
* @param prefix
|
||||
*/
|
||||
export const listFromRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
prefix?: string
|
||||
) => {
|
||||
if (prefix !== undefined) {
|
||||
throw Error("prefix not supported (yet)");
|
||||
}
|
||||
await client.init();
|
||||
|
||||
const NEXT_LINK_KEY = "@odata.nextLink";
|
||||
const DELTA_LINK_KEY = "@odata.deltaLink";
|
||||
let res = await client.client
|
||||
.api(`/drive/special/approot:/${client.vaultName}:/delta`)
|
||||
.get();
|
||||
let driveItems = res.value as DriveItem[];
|
||||
|
||||
while (NEXT_LINK_KEY in res) {
|
||||
res = await client.client.api(res[NEXT_LINK_KEY]).get();
|
||||
driveItems.push(...cloneDeep(res.value as DriveItem[]));
|
||||
}
|
||||
|
||||
// lastly we should have delta link?
|
||||
if (DELTA_LINK_KEY in res) {
|
||||
client.onedriveConfig.deltaLink = res[DELTA_LINK_KEY];
|
||||
await client.saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
driveItems = driveItems.map((x) => {
|
||||
const y = cloneDeep(x);
|
||||
y.parentReference.path = y.parentReference.path.replace("/Apps", "/应用");
|
||||
return y;
|
||||
});
|
||||
|
||||
// unify everything to RemoteItem
|
||||
const unifiedContents = driveItems
|
||||
.map((x) => fromDriveItemToRemoteItem(x, client.vaultName))
|
||||
.filter((x) => x.key !== "/");
|
||||
return {
|
||||
Contents: unifiedContents,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRemoteMeta = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string
|
||||
) => {
|
||||
await client.init();
|
||||
const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName);
|
||||
// log.info(`remotePath=${remotePath}`);
|
||||
const rsp = await client.client
|
||||
.api(remotePath)
|
||||
.select("cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size")
|
||||
.get();
|
||||
// log.info(rsp);
|
||||
const driveItem = rsp as DriveItem;
|
||||
const res = fromDriveItemToRemoteItem(driveItem, client.vaultName);
|
||||
// log.info(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
export const uploadToRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
isRecursively: boolean = false,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = "",
|
||||
foldersCreatedBefore: Set<string> | undefined = undefined
|
||||
) => {
|
||||
await client.init();
|
||||
|
||||
let uploadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
uploadFile = remoteEncryptedKey;
|
||||
}
|
||||
uploadFile = getOnedrivePath(uploadFile, client.vaultName);
|
||||
// log.info(`uploadFile=${uploadFile}`);
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
if (isFolder && isRecursively) {
|
||||
throw Error("upload function doesn't implement recursive function yet!");
|
||||
} else if (isFolder && !isRecursively) {
|
||||
// folder
|
||||
if (password === "") {
|
||||
// if not encrypted, mkdir a remote folder
|
||||
if (foldersCreatedBefore?.has(uploadFile)) {
|
||||
// created, pass
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/56479865/creating-nested-folders-in-one-go-onedrive-api
|
||||
// use PATCH to create folder recursively!!!
|
||||
await client.client.api(uploadFile).patch({
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
}
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
} else {
|
||||
// if encrypted,
|
||||
// upload a fake, random-size file
|
||||
// with the encrypted file name
|
||||
const byteLengthRandom = getRandomIntInclusive(
|
||||
1,
|
||||
65536 /* max allowed */
|
||||
);
|
||||
const arrBufRandom = await encryptArrayBuffer(
|
||||
getRandomArrayBuffer(byteLengthRandom),
|
||||
password
|
||||
);
|
||||
|
||||
const uploadSession: LargeFileUploadSession =
|
||||
await LargeFileUploadTask.createUploadSession(
|
||||
client.client,
|
||||
`https://graph.microsoft.com/v1.0/me${encodeURIComponent(
|
||||
uploadFile
|
||||
)}:/createUploadSession`,
|
||||
{
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
},
|
||||
}
|
||||
);
|
||||
const task = new LargeFileUploadTask(
|
||||
client.client,
|
||||
new FileUpload(
|
||||
arrBufRandom,
|
||||
path.posix.basename(uploadFile),
|
||||
arrBufRandom.byteLength
|
||||
),
|
||||
uploadSession,
|
||||
{
|
||||
rangeSize: 1024 * 1024,
|
||||
uploadEventHandlers: {
|
||||
progress: (range?: Range) => {
|
||||
// Handle progress event
|
||||
// log.info(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
},
|
||||
} as UploadEventHandlers,
|
||||
} as LargeFileUploadTaskOptions
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// log.info(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
// file
|
||||
// we ignore isRecursively parameter here
|
||||
const localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
||||
let remoteContent = localContent;
|
||||
if (password !== "") {
|
||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
||||
}
|
||||
|
||||
// no need to create parent folders firstly, cool!
|
||||
|
||||
// we need to customize the special root folder,
|
||||
// so use LargeFileUploadTask instead of OneDriveLargeFileUploadTask
|
||||
const progress = (range?: Range) => {
|
||||
// Handle progress event
|
||||
// log.info(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
};
|
||||
const uploadEventHandlers: UploadEventHandlers = {
|
||||
progress: progress,
|
||||
};
|
||||
const options: LargeFileUploadTaskOptions = {
|
||||
rangeSize: 1024 * 1024,
|
||||
uploadEventHandlers: uploadEventHandlers,
|
||||
};
|
||||
const payload = {
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
},
|
||||
};
|
||||
// uploadFile already starts with /drive/special/approot:/${vaultName}
|
||||
const uploadSession: LargeFileUploadSession =
|
||||
await LargeFileUploadTask.createUploadSession(
|
||||
client.client,
|
||||
`https://graph.microsoft.com/v1.0/me${encodeURIComponent(
|
||||
uploadFile
|
||||
)}:/createUploadSession`,
|
||||
payload
|
||||
);
|
||||
const fileObject = new FileUpload(
|
||||
remoteContent,
|
||||
path.posix.basename(uploadFile),
|
||||
remoteContent.byteLength
|
||||
);
|
||||
const task = new LargeFileUploadTask(
|
||||
client.client,
|
||||
fileObject,
|
||||
uploadSession,
|
||||
options
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// log.info(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFromRemoteRaw = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string
|
||||
): Promise<ArrayBuffer> => {
|
||||
await client.init();
|
||||
const key = getOnedrivePath(fileOrFolderPath, client.vaultName);
|
||||
const rsp = await client.client
|
||||
.api(key)
|
||||
.select("@microsoft.graph.downloadUrl")
|
||||
.get();
|
||||
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
||||
const content = await (await fetch(downloadUrl)).arrayBuffer();
|
||||
return content;
|
||||
};
|
||||
|
||||
export const downloadFromRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
mtime: number,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
await client.init();
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
await mkdirpInVault(fileOrFolderPath, vault);
|
||||
|
||||
if (isFolder) {
|
||||
// mkdirp locally is enough
|
||||
// do nothing here
|
||||
} else {
|
||||
let downloadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
downloadFile = remoteEncryptedKey;
|
||||
}
|
||||
downloadFile = getOnedrivePath(downloadFile, client.vaultName);
|
||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||
let localContent = remoteContent;
|
||||
if (password !== "") {
|
||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
||||
}
|
||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||
mtime: mtime,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFromRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (fileOrFolderPath === "/") {
|
||||
return;
|
||||
}
|
||||
let remoteFileName = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
remoteFileName = remoteEncryptedKey;
|
||||
}
|
||||
remoteFileName = getOnedrivePath(remoteFileName, client.vaultName);
|
||||
|
||||
await client.init();
|
||||
await client.client.api(remoteFileName).delete();
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedOnedriveClient) => {
|
||||
try {
|
||||
const k = await getUserDisplayName(client);
|
||||
return k !== "<unknown display name>";
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDisplayName = async (client: WrappedOnedriveClient) => {
|
||||
await client.init();
|
||||
const res: User = await client.client.api("/me").select("displayName").get();
|
||||
return res.displayName || "<unknown display name>";
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
|
||||
* https://docs.microsoft.com/en-us/graph/api/user-revokesigninsessions
|
||||
* https://docs.microsoft.com/en-us/graph/api/user-invalidateallrefreshtokens
|
||||
* @param client
|
||||
*/
|
||||
// export const revokeAuth = async (client: WrappedOnedriveClient) => {
|
||||
// await client.init();
|
||||
// await client.client.api('/me/revokeSignInSessions').post(undefined);
|
||||
// };
|
||||
|
||||
export const getRevokeAddr = async () => {
|
||||
return "https://account.live.com/consent/Manage";
|
||||
};
|
||||
+23
-25
@@ -1,34 +1,32 @@
|
||||
import { Buffer } from "buffer";
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { Vault } from "obsidian";
|
||||
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import {
|
||||
S3Client,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
GetObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
HeadObjectCommand,
|
||||
HeadBucketCommand,
|
||||
ListObjectsV2CommandInput,
|
||||
ListObjectsV2CommandOutput,
|
||||
HeadObjectCommandOutput,
|
||||
} from "@aws-sdk/client-s3";
|
||||
export { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import type { _Object } from "@aws-sdk/client-s3";
|
||||
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommand,
|
||||
HeadObjectCommand,
|
||||
HeadObjectCommandOutput,
|
||||
ListObjectsV2Command,
|
||||
ListObjectsV2CommandInput,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import { Buffer } from "buffer";
|
||||
import * as mime from "mime-types";
|
||||
import { Vault } from "obsidian";
|
||||
import { Readable } from "stream";
|
||||
import { RemoteItem, S3Config } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import {
|
||||
arrayBufferToBuffer,
|
||||
bufferToArrayBuffer,
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
import * as mime from "mime-types";
|
||||
|
||||
import { RemoteItem, S3Config } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
export { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_S3_CONFIG = {
|
||||
s3Endpoint: "",
|
||||
@@ -155,7 +153,7 @@ export const uploadToRemote = async (
|
||||
},
|
||||
});
|
||||
upload.on("httpUploadProgress", (progress) => {
|
||||
// console.log(progress);
|
||||
// log.info(progress);
|
||||
});
|
||||
await upload.done();
|
||||
|
||||
|
||||
+17
-21
@@ -1,18 +1,14 @@
|
||||
import { Buffer } from "buffer";
|
||||
import { FileStats, Vault } from "obsidian";
|
||||
import { Vault } from "obsidian";
|
||||
import type { FileStat, WebDAVClient } from "webdav/web";
|
||||
import { AuthType, BufferLike, createClient } from "webdav/web";
|
||||
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
||||
import type { RemoteItem, WebdavConfig } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
||||
export type { WebDAVClient } from "webdav/web";
|
||||
|
||||
import type { RemoteItem, WebdavAuthType, WebdavConfig } from "./baseTypes";
|
||||
|
||||
import {
|
||||
arrayBufferToBuffer,
|
||||
bufferToArrayBuffer,
|
||||
mkdirpInVault,
|
||||
getPathFolder,
|
||||
} from "./misc";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export const DEFAULT_WEBDAV_CONFIG = {
|
||||
address: "",
|
||||
@@ -89,7 +85,7 @@ export class WrappedWebdavClient {
|
||||
: AuthType.Password,
|
||||
});
|
||||
} else {
|
||||
console.log("no password");
|
||||
log.info("no password");
|
||||
this.client = createClient(this.webdavConfig.address);
|
||||
}
|
||||
}
|
||||
@@ -100,12 +96,12 @@ export class WrappedWebdavClient {
|
||||
} else {
|
||||
const res = await this.client.exists(`/${this.vaultName}`);
|
||||
if (res) {
|
||||
console.log("exits!");
|
||||
// log.info("remote vault folder exits!");
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
console.log("not exists, creating");
|
||||
log.info("remote vault folder not exists, creating");
|
||||
await this.client.createDirectory(`/${this.vaultName}`);
|
||||
console.log("created!");
|
||||
log.info("remote vault folder created!");
|
||||
this.vaultFolderExists = true;
|
||||
}
|
||||
}
|
||||
@@ -125,7 +121,7 @@ export const getRemoteMeta = async (
|
||||
) => {
|
||||
await client.init();
|
||||
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName);
|
||||
console.log(`remotePath = ${remotePath}`);
|
||||
// log.info(`remotePath = ${remotePath}`);
|
||||
const res = (await client.client.stat(remotePath, {
|
||||
details: false,
|
||||
})) as FileStat;
|
||||
@@ -165,7 +161,7 @@ export const uploadToRemote = async (
|
||||
await client.client.putFileContents(uploadFile, "", {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
// log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -187,7 +183,7 @@ export const uploadToRemote = async (
|
||||
await client.client.putFileContents(uploadFile, remoteContent, {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
log.info(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -289,16 +285,16 @@ export const deleteFromRemote = async (
|
||||
await client.init();
|
||||
try {
|
||||
await client.client.deleteFile(remoteFileName);
|
||||
// console.log(`delete ${remoteFileName} succeeded`);
|
||||
// log.info(`delete ${remoteFileName} succeeded`);
|
||||
} catch (err) {
|
||||
console.error("some error while deleting");
|
||||
console.log(err);
|
||||
log.info(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedWebdavClient) => {
|
||||
await client.init();
|
||||
try {
|
||||
await client.init();
|
||||
const results = await getRemoteMeta(client, "/");
|
||||
if (results === undefined) {
|
||||
return false;
|
||||
|
||||
+1003
File diff suppressed because it is too large
Load Diff
+65
-18
@@ -1,20 +1,23 @@
|
||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||
|
||||
import {
|
||||
clearDeleteRenameHistoryOfKey,
|
||||
upsertSyncMetaMappingData,
|
||||
getSyncMetaMappingByRemoteKey,
|
||||
} from "./localdb";
|
||||
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
||||
|
||||
import { RemoteClient } from "./remote";
|
||||
import type { SUPPORTED_SERVICES_TYPE, RemoteItem } from "./baseTypes";
|
||||
import { mkdirpInVault, isHiddenPath, isVaildText, setToString } from "./misc";
|
||||
import { TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import type { RemoteItem, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
import {
|
||||
decryptBase32ToString,
|
||||
encryptStringToBase32,
|
||||
decryptBase64urlToString,
|
||||
encryptStringToBase64url,
|
||||
MAGIC_ENCRYPTED_PREFIX_BASE32,
|
||||
MAGIC_ENCRYPTED_PREFIX_BASE64URL,
|
||||
} from "./encrypt";
|
||||
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
||||
import {
|
||||
clearDeleteRenameHistoryOfKey,
|
||||
getSyncMetaMappingByRemoteKey,
|
||||
upsertSyncMetaMappingData,
|
||||
} from "./localdb";
|
||||
import { isHiddenPath, isVaildText, mkdirpInVault } from "./misc";
|
||||
import { RemoteClient } from "./remote";
|
||||
|
||||
import * as origLog from "loglevel";
|
||||
const log = origLog.getLogger("rs-default");
|
||||
|
||||
export type SyncStatusType =
|
||||
| "idle"
|
||||
@@ -85,7 +88,7 @@ export const isPasswordOk = async (
|
||||
}
|
||||
const santyCheckKey = remote[0].key;
|
||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
||||
// this is encrypted!
|
||||
// this is encrypted using old base32!
|
||||
// try to decrypt it using the provided password.
|
||||
if (password === "") {
|
||||
return {
|
||||
@@ -96,6 +99,38 @@ export const isPasswordOk = async (
|
||||
try {
|
||||
const res = await decryptBase32ToString(santyCheckKey, password);
|
||||
|
||||
// additional test
|
||||
// because iOS Safari bypasses decryption with wrong password!
|
||||
if (isVaildText(res)) {
|
||||
return {
|
||||
ok: true,
|
||||
reason: "password_matched",
|
||||
} as PasswordCheckType;
|
||||
} else {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "invalid_text_after_decryption",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "password_not_matched",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
}
|
||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)) {
|
||||
// this is encrypted using new base64url!
|
||||
// try to decrypt it using the provided password.
|
||||
if (password === "") {
|
||||
return {
|
||||
ok: false,
|
||||
reason: "remote_encrypted_local_no_password",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
try {
|
||||
const res = await decryptBase64urlToString(santyCheckKey, password);
|
||||
|
||||
// additional test
|
||||
// because iOS Safari bypasses decryption with wrong password!
|
||||
if (isVaildText(res)) {
|
||||
@@ -145,7 +180,15 @@ const ensembleMixedStates = async (
|
||||
const remoteEncryptedKey = entry.key;
|
||||
let key = remoteEncryptedKey;
|
||||
if (password !== "") {
|
||||
if (remoteEncryptedKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
||||
key = await decryptBase32ToString(remoteEncryptedKey, password);
|
||||
} else if (
|
||||
remoteEncryptedKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE64URL)
|
||||
) {
|
||||
key = await decryptBase64urlToString(remoteEncryptedKey, password);
|
||||
} else {
|
||||
throw Error(`unexpected key=${remoteEncryptedKey}`);
|
||||
}
|
||||
}
|
||||
const backwardMapping = await getSyncMetaMappingByRemoteKey(
|
||||
remoteType,
|
||||
@@ -435,7 +478,10 @@ const dispatchOperationToActual = async (
|
||||
if (password !== "") {
|
||||
remoteEncryptedKey = state.remote_encrypted_key;
|
||||
if (remoteEncryptedKey === undefined || remoteEncryptedKey === "") {
|
||||
remoteEncryptedKey = await encryptStringToBase32(key, password);
|
||||
// the old version uses base32
|
||||
// remoteEncryptedKey = await encryptStringToBase32(key, password);
|
||||
// the new version users base64url
|
||||
remoteEncryptedKey = await encryptStringToBase64url(key, password);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,16 +576,17 @@ export const doActualSync = async (
|
||||
)) {
|
||||
const k2 = k as string;
|
||||
const v2 = v as FileOrFolderMixedState;
|
||||
log.debug(`start syncing "${k2}" with plan ${JSON.stringify(v2)}`);
|
||||
await dispatchOperationToActual(
|
||||
k as string,
|
||||
v as FileOrFolderMixedState,
|
||||
k2,
|
||||
v2,
|
||||
client,
|
||||
db,
|
||||
vault,
|
||||
password,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
// console.log(`finished ${k}, with ${setToString(foldersCreatedBefore)}`);
|
||||
// log.info(`finished ${k}, with ${setToString(foldersCreatedBefore)}`);
|
||||
}
|
||||
// await Promise.all(
|
||||
// Object.entries(keyStates)
|
||||
|
||||
+15
@@ -30,6 +30,21 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
.onedrive-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-revoke-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.webdav-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
|
||||
import { RemotelySavePluginSettings } from "../src/baseTypes";
|
||||
import { messyConfigToNormal, normalConfigToMessy } from "../src/configPersist";
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: {
|
||||
s3AccessKeyID: "acc",
|
||||
} as any,
|
||||
webdav: {
|
||||
address: "addr",
|
||||
} as any,
|
||||
dropbox: {
|
||||
username: "测试中文",
|
||||
} as any,
|
||||
onedrive: {
|
||||
username: "test 🍎 emoji",
|
||||
} as any,
|
||||
password: "password",
|
||||
serviceType: "s3",
|
||||
currLogLevel: "info",
|
||||
};
|
||||
|
||||
describe("Config Persist tests", () => {
|
||||
it("should encrypt go back and forth conrrectly", async () => {
|
||||
const k = DEFAULT_SETTINGS;
|
||||
const k2 = normalConfigToMessy(k);
|
||||
const k3 = messyConfigToNormal(k2);
|
||||
expect(k3).to.deep.equal(k);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
import { base64ToBase32, bufferToArrayBuffer } from "../src/misc";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
decryptBase32ToString,
|
||||
encryptArrayBuffer,
|
||||
encryptStringToBase32,
|
||||
encryptStringToBase64url,
|
||||
} from "../src/encrypt";
|
||||
import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc";
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
@@ -52,7 +53,7 @@ describe("Encryption tests", () => {
|
||||
).toString("utf-8");
|
||||
const password = "somepassword";
|
||||
const saltHex = "8302F586FAB491EC";
|
||||
const enc = await encryptStringToBase32(
|
||||
const enc = await encryptStringToBase64url(
|
||||
fileContent,
|
||||
password,
|
||||
undefined,
|
||||
@@ -65,9 +66,9 @@ describe("Encryption tests", () => {
|
||||
const opensslBase64Res =
|
||||
"U2FsdGVkX1+DAvWG+rSR7BPXMnlvSSVGMdjsx7kE1CTH+28P+yAZRdDGgFWMGkMd";
|
||||
// we output base32, so we need some transformation
|
||||
const opensslBase32Res = base64ToBase32(opensslBase64Res);
|
||||
const opensslBase64urlRes = base64ToBase64url(opensslBase64Res);
|
||||
|
||||
expect(enc).equal(opensslBase32Res);
|
||||
expect(enc).equal(opensslBase64urlRes);
|
||||
});
|
||||
|
||||
it("should encrypt binary file and get the same result as openssl", async () => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { expect } from "chai";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import * as misc from "../src/misc";
|
||||
|
||||
describe("Misc: hidden file", () => {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"0.2.4": "0.12.15"
|
||||
"0.2.9": "0.12.15"
|
||||
}
|
||||
|
||||
+6
-2
@@ -4,6 +4,8 @@ const webpack = require("webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||
const DEFAULT_ONEDRIVE_CLIENT_ID = process.env.ONEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_ONEDRIVE_AUTHORITY = process.env.ONEDRIVE_AUTHORITY || "";
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/main.ts",
|
||||
@@ -16,6 +18,8 @@ module.exports = {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"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}"`,
|
||||
}),
|
||||
// Work around for Buffer is undefined:
|
||||
// https://github.com/webpack/changelog-v5/issues/10
|
||||
@@ -47,8 +51,8 @@ module.exports = {
|
||||
// buffer: require.resolve("buffer/"),
|
||||
// console: require.resolve("console-browserify"),
|
||||
// constants: require.resolve("constants-browserify"),
|
||||
// crypto: require.resolve("crypto-browserify"),
|
||||
crypto: false,
|
||||
crypto: require.resolve("crypto-browserify"),
|
||||
// crypto: false,
|
||||
// domain: require.resolve("domain-browser"),
|
||||
// events: require.resolve("events"),
|
||||
// http: require.resolve("stream-http"),
|
||||
|
||||
Reference in New Issue
Block a user