Compare commits

...
22 Commits
Author SHA1 Message Date
fyears 789d07261e 0.1.10 2021-12-01 10:11:20 +08:00
fyears ccd3b49b4b inject env var in webpack 2021-12-01 10:10:39 +08:00
fyears 2930fe5350 0.1.9 2021-12-01 01:50:00 +08:00
fyears a9abf6d834 webpack is slow but works 2021-12-01 01:49:29 +08:00
fyears bdf09e24c3 remove assert 2021-12-01 01:49:07 +08:00
fyears 8dc4b835c9 0.1.8 2021-12-01 00:34:02 +08:00
fyears 2e800af4f8 dropbox should use sub folder 2021-12-01 00:33:24 +08:00
fyears e9e9e2b6eb fix typo 2021-11-30 00:44:26 +08:00
fyears 274e0b8b28 bump to 0.1.7 2021-11-30 00:42:05 +08:00
fyears 2147332cf4 more about dropbox 2021-11-30 00:41:41 +08:00
fyears 262bb9a1fe more disclaimer 2021-11-30 00:32:43 +08:00
fyears 2669a3e8ad fix auth dropbox 2021-11-30 00:21:15 +08:00
fyears bdac16ee24 define clientID or AppKey by env var 2021-11-29 01:03:17 +08:00
fyears 0a7de6588a ugly way to refresh token 2021-11-29 00:04:24 +08:00
fyears 1effcdd7f7 basic dropbox auth flow 2021-11-28 16:28:52 +08:00
fyears 00c840a758 basically working dropbox 2021-11-28 12:20:38 +08:00
fyears 2eb1545f9d more misc 2021-11-28 12:18:44 +08:00
fyears 0d75ea92b1 polish get parent folder 2021-11-28 10:45:46 +08:00
fyears d6cbf4260a esbuild is way faster 2021-11-27 14:49:07 +08:00
fyears 9389039c7a avoid using for await 2021-11-27 14:43:19 +08:00
fyears aa3aecb074 0.1.6 2021-11-27 00:51:01 +08:00
fyears 9128f9ed5b add support for ios 5-times click 2021-11-27 00:49:21 +08:00
16 changed files with 1138 additions and 48 deletions
+9 -1
View File
@@ -12,7 +12,7 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
## Features ## Features
- **Amazon S3 or S3-compatible services are supported.** Webdav supports on the half way. - **Amazon S3 or S3-compatible, and Dropbox services are supported.** Webdav supports on the half way.
- **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).**
@@ -45,6 +45,14 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
- 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.
### Dropbox
- **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/obsidian-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.
### 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 buggy (as of now, 20211122) and considered experimental, so it's hidden by default.** Highly recommend to use the more stable s3.
+41
View File
@@ -0,0 +1,41 @@
import dotenv from "dotenv/config";
import esbuild from "esbuild";
import process from "process";
// import builtins from 'builtin-modules'
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = process.argv[2] === "production";
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
esbuild
.build({
banner: {
js: banner,
},
entryPoints: ["./src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"fs",
// ...builtins
],
format: "cjs",
watch: !prod,
target: "es2016",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
minify: prod,
outfile: "main.js",
define: {
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
},
})
.catch(() => process.exit(1));
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "obsidian-remotely-save", "id": "obsidian-remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.1.5", "version": "0.1.10",
"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 sync notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+19 -4
View File
@@ -1,14 +1,23 @@
{ {
"name": "obsidian-remotely-save", "name": "obsidian-remotely-save",
"version": "0.1.5", "version": "0.1.10",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev": "webpack --mode development --watch", "dev": "node esbuild.config.mjs",
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"build": "webpack --mode production", "build": "webpack --mode production",
"format": "npx prettier --write .", "format": "npx prettier --write .",
"clean": "npx rimraf main.js", "clean": "npx rimraf main.js",
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'" "test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts'"
}, },
"browser": {
"path": "path-browserify",
"process": "process/browser",
"stream": "stream-browserify",
"crypto": "crypto-browserify",
"util": "util/",
"assert": "assert/"
},
"source": "main.ts", "source": "main.ts",
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -19,18 +28,20 @@
"@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",
"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",
"esbuild": "^0.14.0",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"terser-webpack-plugin": "^5.2.4",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"tslib": "^2.2.0", "tslib": "^2.2.0",
"typescript": "^4.4.4", "typescript": "^4.4.4",
"webdav-server": "^2.6.2", "webdav-server": "^2.6.2",
"webpack": "^5.58.2", "webpack": "^5.64.4",
"webpack-cli": "^4.9.1" "webpack-cli": "^4.9.1"
}, },
"dependencies": { "dependencies": {
@@ -38,9 +49,12 @@
"@aws-sdk/lib-storage": "^3.40.1", "@aws-sdk/lib-storage": "^3.40.1",
"@aws-sdk/signature-v4-crt": "^3.37.0", "@aws-sdk/signature-v4-crt": "^3.37.0",
"acorn": "^8.5.0", "acorn": "^8.5.0",
"assert": "^2.0.0",
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"codemirror": "^5.63.1", "codemirror": "^5.63.1",
"crypto-browserify": "^3.12.0",
"dropbox": "^10.22.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"mime-types": "^2.1.33", "mime-types": "^2.1.33",
"obsidian": "^0.12.0", "obsidian": "^0.12.0",
@@ -49,6 +63,7 @@
"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",
"util": "^0.12.4",
"webdav": "^4.7.0", "webdav": "^4.7.0",
"webdav-fs": "^4.0.0", "webdav-fs": "^4.0.0",
"xregexp": "^5.1.0" "xregexp": "^5.1.0"
+1 -1
View File
@@ -2,7 +2,7 @@
* Only type defs here. * Only type defs here.
*/ */
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav"; export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
export interface RemoteItem { export interface RemoteItem {
key: string; key: string;
+274 -8
View File
@@ -11,6 +11,7 @@ import {
TFolder, TFolder,
} from "obsidian"; } from "obsidian";
import * as CodeMirror from "codemirror"; import * as CodeMirror from "codemirror";
import process from "process";
import { import {
prepareDBs, prepareDBs,
destroyDBs, destroyDBs,
@@ -25,8 +26,18 @@ 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 { S3Config, DEFAULT_S3_CONFIG } from "./s3";
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav"; import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
import {
DropboxConfig,
DEFAULT_DROPBOX_CONFIG,
getCodeVerifierAndChallenge,
getAuthUrl,
sendAuthReq,
setConfigBySuccessfullAuthInplace,
} 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 { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
@@ -34,6 +45,7 @@ import { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
interface RemotelySavePluginSettings { interface RemotelySavePluginSettings {
s3: S3Config; s3: S3Config;
webdav: WebdavConfig; webdav: WebdavConfig;
dropbox: DropboxConfig;
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;
enableExperimentService: boolean; enableExperimentService: boolean;
@@ -42,6 +54,7 @@ interface RemotelySavePluginSettings {
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG, s3: DEFAULT_S3_CONFIG,
webdav: DEFAULT_WEBDAV_CONFIG, webdav: DEFAULT_WEBDAV_CONFIG,
dropbox: DEFAULT_DROPBOX_CONFIG,
password: "", password: "",
serviceType: "s3", serviceType: "s3",
enableExperimentService: false, enableExperimentService: false,
@@ -91,18 +104,22 @@ export default class RemotelySavePlugin extends Plugin {
new Notice("2/6 Starting to fetch remote meta data."); new Notice("2/6 Starting to fetch remote meta data.");
this.syncStatus = "getting_remote_meta"; this.syncStatus = "getting_remote_meta";
const self = this;
const client = new RemoteClient( const client = new RemoteClient(
this.settings.serviceType, this.settings.serviceType,
this.settings.s3, this.settings.s3,
this.settings.webdav this.settings.webdav,
this.settings.dropbox,
this.app.vault.getName(),
() => self.saveSettings()
); );
const remoteRsp = await client.listFromRemote(); const remoteRsp = await client.listFromRemote();
// console.log(remoteRsp);
new Notice("3/6 Starting to fetch local meta data."); new Notice("3/6 Starting to fetch local meta data.");
this.syncStatus = "getting_local_meta"; this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles(); const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadDeleteRenameHistoryTable(this.db); const localHistory = await loadDeleteRenameHistoryTable(this.db);
// console.log(remoteRsp);
// console.log(local); // console.log(local);
// console.log(localHistory); // console.log(localHistory);
@@ -179,7 +196,14 @@ export default class RemotelySavePlugin extends Plugin {
} }
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign(
{},
JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) /* copy an object */,
await this.loadData()
);
if (this.settings.dropbox.clientID === "") {
this.settings.dropbox.clientID = DEFAULT_SETTINGS.dropbox.clientID;
}
} }
async saveSettings() { async saveSettings() {
@@ -253,6 +277,112 @@ export class PasswordModal extends Modal {
} }
} }
export class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
readonly revokeAuthDiv: HTMLDivElement;
readonly revokeAuthSetting: Setting;
constructor(
app: App,
plugin: RemotelySavePlugin,
authDiv: HTMLDivElement,
revokeAuthDiv: HTMLDivElement,
revokeAuthSetting: Setting
) {
super(app);
this.plugin = plugin;
this.authDiv = authDiv;
this.revokeAuthDiv = revokeAuthDiv;
this.revokeAuthSetting = revokeAuthSetting;
}
onOpen() {
let { contentEl } = this;
const k = getCodeVerifierAndChallenge();
const authUrl = getAuthUrl(
this.plugin.settings.dropbox.clientID,
k.challenge
);
contentEl.createEl("p", {
text: "Step 1: Visit the following address in a browser, and follow the steps on the web page to authorize.",
});
contentEl.createEl("p").createEl("a", {
href: authUrl,
text: authUrl,
});
contentEl.createEl("p", {
text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".',
});
let authCode = "";
new Setting(contentEl)
.setName("Auth Code from web page")
.setDesc('You need to click "Confirm".')
.addText((text) =>
text
.setPlaceholder("")
.setValue("")
.onChange((val) => {
authCode = val.trim();
})
)
.addButton(async (button) => {
button.setButtonText("Confirm");
button.onClick(async () => {
new Notice("Trying to connect to Dropbox");
try {
const authRes = await sendAuthReq(
this.plugin.settings.dropbox.clientID,
k.verifier,
authCode
);
const self = this;
setConfigBySuccessfullAuthInplace(
this.plugin.settings.dropbox,
authRes,
() => self.plugin.saveSettings()
);
const client = new RemoteClient(
"dropbox",
undefined,
undefined,
this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings()
);
const username = await client.getUser();
this.plugin.settings.dropbox.username = username;
await this.plugin.saveSettings();
new Notice(`Good! We've connected to Dropbox as user ${username}!`);
this.authDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
this.revokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
this.revokeAuthSetting.setDesc(
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
);
this.close();
} catch (err) {
console.error(err);
new Notice("Something goes wrong while connecting to Dropbox.");
}
});
});
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class RemotelySaveSettingTab extends PluginSettingTab { class RemotelySaveSettingTab extends PluginSettingTab {
plugin: RemotelySavePlugin; plugin: RemotelySavePlugin;
@@ -293,29 +423,40 @@ class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
// we need to create the div in advance of s3Div and webdavDiv // we need to create the div in advance of any other service divs
const serviceChooserDiv = generalDiv.createEl("div"); const serviceChooserDiv = generalDiv.createEl("div");
serviceChooserDiv.onClickEvent((x) => { let clickChooserTimes = 0;
if (x.detail === 5) { 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") { if (this.plugin.settings.serviceType === "webdav") {
new Notice( new Notice(
"You've enabled hidden unstable experimental webdav support before. Nothing changes." "You've enabled hidden unstable experimental webdav support before. Nothing changes."
); );
} else if (!this.plugin.settings.enableExperimentService) { } else if (!this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = true; this.plugin.settings.enableExperimentService = true;
this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice( new Notice(
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution." "You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
); );
} else if (this.plugin.settings.enableExperimentService) { } else if (this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = false; this.plugin.settings.enableExperimentService = false;
this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice( new Notice(
"You've disabled hidden unstable experimental webdav support. Reopen settings again." "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" });
@@ -442,6 +583,126 @@ class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
const dropboxDiv = containerEl.createEl("div", { cls: "dropbox-hide" });
dropboxDiv.toggleClass(
"dropbox-hide",
this.plugin.settings.serviceType !== "dropbox"
);
dropboxDiv.createEl("h2", { text: "for Dropbox" });
dropboxDiv.createEl("p", {
text: "Disclaimer: Sync support for Dropbox are more experimental, and s3 functions are more stable now.",
cls: "dropbox-disclaimer",
});
dropboxDiv.createEl("p", {
text: "Disclaimer: This app is NOT an official Dropbox product. It just uses Dropbox open api.",
cls: "dropbox-disclaimer",
});
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 .",
cls: "dropbox-disclaimer",
});
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 dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({
cls: "dropbox-auth-button-hide",
});
const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({
cls: "dropbox-revoke-auth-button-hide",
});
const revokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
.setName("Revoke Auth")
.setDesc(
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
)
.addButton(async (button) => {
button.setButtonText("Revoke Auth");
button.onClick(async () => {
try {
const self = this;
const client = new RemoteClient(
"dropbox",
undefined,
undefined,
this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings()
);
await client.revokeAuth();
this.plugin.settings.dropbox = JSON.parse(
JSON.stringify(DEFAULT_DROPBOX_CONFIG)
);
await this.plugin.saveSettings();
dropboxAuthDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
dropboxRevokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
new Notice("Revoked!");
} catch (err) {
console.error(err);
new Notice("Something goes wrong while revoking");
}
});
});
new Setting(dropboxAuthDiv)
.setName("Auth")
.setDesc("Auth")
.addButton(async (button) => {
button.setButtonText("Auth");
button.onClick(async () => {
new DropboxAuthModal(
this.app,
this.plugin,
dropboxAuthDiv,
dropboxRevokeAuthDiv,
revokeAuthSetting
).open();
});
});
dropboxAuthDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
dropboxRevokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
new Setting(dropboxDiv)
.setName("check connectivity")
.setDesc("check connectivity")
.addButton(async (button) => {
button.setButtonText("Check");
button.onClick(async () => {
new Notice("Checking...");
const self = this;
const client = new RemoteClient(
"dropbox",
undefined,
undefined,
this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings()
);
const res = await client.checkConnectivity();
if (res) {
new Notice("Great! We can connect to Dropbox!");
} else {
new Notice("We cannot connect to Dropbox.");
}
});
});
const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" }); const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" });
webdavDiv.toggleClass( webdavDiv.toggleClass(
"webdav-hide", "webdav-hide",
@@ -552,6 +813,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.enableExperimentService; this.plugin.settings.enableExperimentService;
dropdown.addOption("s3", "s3 (-compatible)"); dropdown.addOption("s3", "s3 (-compatible)");
dropdown.addOption("dropbox", "Dropbox");
if (currService === "webdav" || enableExperimentService) { if (currService === "webdav" || enableExperimentService) {
dropdown.addOption("webdav", "webdav (experimental)"); dropdown.addOption("webdav", "webdav (experimental)");
if (!enableExperimentService) { if (!enableExperimentService) {
@@ -567,6 +829,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
"s3-hide", "s3-hide",
this.plugin.settings.serviceType !== "s3" this.plugin.settings.serviceType !== "s3"
); );
dropboxDiv.toggleClass(
"dropbox-hide",
this.plugin.settings.serviceType !== "dropbox"
);
webdavDiv.toggleClass( webdavDiv.toggleClass(
"webdav-hide", "webdav-hide",
this.plugin.settings.serviceType !== "webdav" this.plugin.settings.serviceType !== "webdav"
+14 -1
View File
@@ -45,7 +45,10 @@ export const getFolderLevels = (x: string) => {
const y1 = x.split("/"); const y1 = x.split("/");
let i = 0; let i = 0;
for (let index = 0; index + 1 < y1.length; index++) { for (let index = 0; index + 1 < y1.length; index++) {
res.push(y1.slice(0, index + 1).join("/")); const k = y1.slice(0, index + 1).join("/");
if (k !== "" && k !== "/") {
res.push(k);
}
} }
return res; return res;
}; };
@@ -142,3 +145,13 @@ export const getPathFolder = (a: string) => {
const b = path.posix.dirname(a); const b = path.posix.dirname(a);
return b.endsWith("/") ? b : `${b}/`; return b.endsWith("/") ? b : `${b}/`;
}; };
/**
* https://stackoverflow.com/questions/54511144
* @param a
* @param delimiter
* @returns
*/
export const setToString = (a: Set<string>, delimiter: string = ",") => {
return [...a].join(delimiter);
};
+73 -4
View File
@@ -3,6 +3,7 @@ import { Vault } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
import * as s3 from "./s3"; import * as s3 from "./s3";
import * as webdav from "./webdav"; import * as webdav from "./webdav";
import * as dropbox from "./remoteForDropbox";
export class RemoteClient { export class RemoteClient {
readonly serviceType: SUPPORTED_SERVICES_TYPE; readonly serviceType: SUPPORTED_SERVICES_TYPE;
@@ -10,19 +11,38 @@ export class RemoteClient {
readonly s3Config?: s3.S3Config; readonly s3Config?: s3.S3Config;
readonly webdavClient?: webdav.WebDAVClient; readonly webdavClient?: webdav.WebDAVClient;
readonly webdavConfig?: webdav.WebdavConfig; readonly webdavConfig?: webdav.WebdavConfig;
readonly dropboxClient?: dropbox.WrappedDropboxClient;
readonly dropboxConfig?: dropbox.DropboxConfig;
constructor( constructor(
serviceType: SUPPORTED_SERVICES_TYPE, serviceType: SUPPORTED_SERVICES_TYPE,
s3Config?: s3.S3Config, s3Config?: s3.S3Config,
webdavConfig?: webdav.WebdavConfig webdavConfig?: webdav.WebdavConfig,
dropboxConfig?: dropbox.DropboxConfig,
vaultName?: string,
saveUpdatedConfigFunc?: () => Promise<any>
) { ) {
this.serviceType = serviceType; this.serviceType = serviceType;
// the client may modify the config inplace,
// so we use a ref not copy of config here
if (serviceType === "s3") { if (serviceType === "s3") {
this.s3Config = s3Config; this.s3Config = s3Config;
this.s3Client = s3.getS3Client(s3Config); this.s3Client = s3.getS3Client(this.s3Config);
} else if (serviceType === "webdav") { } else if (serviceType === "webdav") {
this.webdavConfig = webdavConfig; this.webdavConfig = webdavConfig;
this.webdavClient = webdav.getWebdavClient(webdavConfig); this.webdavClient = webdav.getWebdavClient(this.webdavConfig);
} else if (serviceType === "dropbox") {
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
throw Error(
"remember to provide vault name and callback while init dropbox client"
);
}
this.dropboxConfig = dropboxConfig;
this.dropboxClient = dropbox.getDropboxClient(
this.dropboxConfig,
vaultName,
saveUpdatedConfigFunc
);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -37,6 +57,8 @@ export class RemoteClient {
); );
} else if (this.serviceType === "webdav") { } else if (this.serviceType === "webdav") {
return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath); return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath);
} else if (this.serviceType === "dropbox") {
return await dropbox.getRemoteMeta(this.dropboxClient, fileOrFolderPath);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -47,7 +69,8 @@ export class RemoteClient {
vault: Vault, vault: Vault,
isRecursively: boolean = false, isRecursively: boolean = false,
password: string = "", password: string = "",
remoteEncryptedKey: string = "" remoteEncryptedKey: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined
) => { ) => {
if (this.serviceType === "s3") { if (this.serviceType === "s3") {
return await s3.uploadToRemote( return await s3.uploadToRemote(
@@ -68,6 +91,16 @@ export class RemoteClient {
password, password,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "dropbox") {
return await dropbox.uploadToRemote(
this.dropboxClient,
fileOrFolderPath,
vault,
isRecursively,
password,
remoteEncryptedKey,
foldersCreatedBefore
);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -78,6 +111,8 @@ export class RemoteClient {
return await s3.listFromRemote(this.s3Client, this.s3Config, prefix); return await s3.listFromRemote(this.s3Client, this.s3Config, prefix);
} else if (this.serviceType === "webdav") { } else if (this.serviceType === "webdav") {
return await webdav.listFromRemote(this.webdavClient, prefix); return await webdav.listFromRemote(this.webdavClient, prefix);
} else if (this.serviceType === "dropbox") {
return await dropbox.listFromRemote(this.dropboxClient, prefix);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -109,6 +144,15 @@ export class RemoteClient {
password, password,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "dropbox") {
return await dropbox.downloadFromRemote(
this.dropboxClient,
fileOrFolderPath,
vault,
mtime,
password,
remoteEncryptedKey
);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -134,6 +178,13 @@ export class RemoteClient {
password, password,
remoteEncryptedKey remoteEncryptedKey
); );
} else if (this.serviceType === "dropbox") {
return await dropbox.deleteFromRemote(
this.dropboxClient,
fileOrFolderPath,
password,
remoteEncryptedKey
);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
@@ -144,6 +195,24 @@ export class RemoteClient {
return await s3.checkConnectivity(this.s3Client, this.s3Config); return await s3.checkConnectivity(this.s3Client, this.s3Config);
} else if (this.serviceType === "webdav") { } else if (this.serviceType === "webdav") {
return await webdav.checkConnectivity(this.webdavClient); return await webdav.checkConnectivity(this.webdavClient);
} else if (this.serviceType === "dropbox") {
return await dropbox.checkConnectivity(this.dropboxClient);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
};
getUser = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.getUserDisplayName(this.dropboxClient);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
};
revokeAuth = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.revokeAuth(this.dropboxClient);
} else { } else {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
+615
View File
@@ -0,0 +1,615 @@
import * as path from "path";
import { FileStats, Vault } from "obsidian";
import { Buffer } from "buffer";
import * as crypto from "crypto";
import { Dropbox, DropboxResponse, files } from "dropbox";
export { Dropbox } from "dropbox";
import { RemoteItem } from "./baseTypes";
import {
arrayBufferToBuffer,
bufferToArrayBuffer,
mkdirpInVault,
getPathFolder,
getFolderLevels,
setToString,
} from "./misc";
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 = {
accessToken: "",
clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
refreshToken: "",
accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0,
accountID: "",
username: "",
};
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
let key = fileOrFolderPath;
if (!fileOrFolderPath.startsWith("/")) {
// then this is original path in Obsidian
key = `/${vaultName}/${fileOrFolderPath}`;
}
if (key.endsWith("/")) {
key = key.slice(0, key.length - 1);
}
return key;
};
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
if (
!(
fileOrFolderPath === `/${vaultName}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`)
)
) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
}
return fileOrFolderPath.slice(`/${vaultName}/`.length);
};
const fromDropboxItemToRemoteItem = (
x:
| files.FileMetadataReference
| files.FolderMetadataReference
| files.DeletedMetadataReference,
vaultName: string
): RemoteItem => {
let key = getNormPath(x.path_display, vaultName);
if (x[".tag"] === "folder" && !key.endsWith("/")) {
key = `${key}/`;
}
if (x[".tag"] === "folder") {
return {
key: key,
lastModified: undefined,
size: 0,
remoteType: "dropbox",
etag: `${x.id}\t`,
} as RemoteItem;
} else if (x[".tag"] === "file") {
return {
key: key,
lastModified: Date.parse(x.server_modified).valueOf(),
size: x.size,
remoteType: "dropbox",
etag: `${x.id}\t${x.content_hash}`,
} as RemoteItem;
} else if (x[".tag"] === "deleted") {
throw Error("do not support deleted tag");
}
};
/**
* Dropbox api doesn't return mtime for folders.
* This is a try to assign mtime by using files in folder.
* @param allFilesFolders
* @returns
*/
const fixLastModifiedTimeInplace = (allFilesFolders: RemoteItem[]) => {
if (allFilesFolders.length === 0) {
return;
}
// sort by longer to shorter
allFilesFolders.sort((a, b) => b.key.length - a.key.length);
// a "map" from dir to mtime
let potentialMTime = {} as Record<string, number>;
// first sort pass, from buttom to up
for (const item of allFilesFolders) {
if (item.key.endsWith("/")) {
// itself is a folder, and initially doesn't have mtime
if (item.lastModified === undefined && item.key in potentialMTime) {
// previously we gathered all sub info of this folder
item.lastModified = potentialMTime[item.key];
}
}
const parent = `${path.posix.dirname(item.key)}/`;
if (item.lastModified !== undefined) {
if (parent in potentialMTime) {
potentialMTime[parent] = Math.max(
potentialMTime[parent],
item.lastModified
);
} else {
potentialMTime[parent] = item.lastModified;
}
}
}
// second pass, from up to buttom.
// fill mtime by parent folder or Date.Now() if still not available.
// this is only possible if no any sub-folder-files recursively.
// we do not sort the array again, just iterate over it by reverse
// using good old for loop.
for (let i = allFilesFolders.length - 1; i >= 0; --i) {
const item = allFilesFolders[i];
if (!item.key.endsWith("/")) {
continue; // skip files
}
if (item.lastModified !== undefined) {
continue; // don't need to deal with it
}
const parent = `${path.posix.dirname(item.key)}/`;
if (parent in potentialMTime) {
item.lastModified = potentialMTime[parent];
} else {
item.lastModified = Date.now().valueOf();
potentialMTime[item.key] = item.lastModified;
}
}
return allFilesFolders;
};
////////////////////////////////////////////////////////////////////////////////
// Dropbox authorization using PKCE
// see https://dropbox.tech/developers/pkce--what-and-why-
////////////////////////////////////////////////////////////////////////////////
const specialBase64Encode = (str: Buffer) => {
return str
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
};
const sha256 = (buffer: string) => {
return crypto.createHash("sha256").update(buffer).digest();
};
export const getCodeVerifierAndChallenge = () => {
const codeVerifier = specialBase64Encode(crypto.randomBytes(32));
// console.log(`Client generated code_verifier: ${codeVerifier}`);
const codeChallenge = specialBase64Encode(sha256(codeVerifier));
// console.log(`Client generated code_challenge: ${codeChallenge}`);
return {
verifier: codeVerifier,
challenge: codeChallenge,
};
};
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 {
access_token: string;
token_type: "bearer";
expires_in: string;
refresh_token?: string;
scope?: string;
uid?: string;
account_id?: string;
}
export const sendAuthReq = async (
appKey: string,
verifier: string,
authCode: string
) => {
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
code: authCode,
grant_type: "authorization_code",
code_verifier: verifier,
client_id: appKey,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
return resp2;
};
export const sendRefreshTokenReq = async (
appKey: string,
refreshToken: string
) => {
console.log("start auto getting refreshed Dropbox access token.");
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: appKey,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
console.log("finish auto getting refreshed Dropbox access token.");
return resp2;
};
export const setConfigBySuccessfullAuthInplace = async (
config: DropboxConfig,
authRes: DropboxSuccessAuthRes,
saveUpdatedConfigFunc: () => Promise<any> | undefined
) => {
console.log("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;
if (authRes.refresh_token !== undefined) {
config.refreshToken = authRes.refresh_token;
}
if (authRes.refresh_token !== undefined) {
config.accountID = authRes.account_id;
}
if (saveUpdatedConfigFunc !== undefined) {
await saveUpdatedConfigFunc();
}
console.log("finish updating local info of Dropbox token");
};
////////////////////////////////////////////////////////////////////////////////
// Other usual common methods
////////////////////////////////////////////////////////////////////////////////
export class WrappedDropboxClient {
dropboxConfig: DropboxConfig;
vaultName: string;
saveUpdatedConfigFunc: () => Promise<any>;
dropbox: Dropbox;
vaultFolderExists: boolean;
constructor(
dropboxConfig: DropboxConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
this.dropboxConfig = dropboxConfig;
this.vaultName = vaultName;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.vaultFolderExists = false;
}
init = async () => {
// check token
if (
this.dropboxConfig.accessToken === "" ||
this.dropboxConfig.refreshToken === ""
) {
throw Error("The user has not manually auth yet.");
}
const currentTs = Date.now();
if (this.dropboxConfig.accessTokenExpiresAtTime > currentTs) {
this.dropbox = new Dropbox({
accessToken: this.dropboxConfig.accessToken,
});
} else {
if (this.dropboxConfig.refreshToken === "") {
throw Error(
"We need to automatically refresh token but none is stored."
);
}
const resp = await sendRefreshTokenReq(
this.dropboxConfig.clientID,
this.dropboxConfig.refreshToken
);
setConfigBySuccessfullAuthInplace(
this.dropboxConfig,
resp,
this.saveUpdatedConfigFunc
);
this.dropbox = new Dropbox({
accessToken: this.dropboxConfig.accessToken,
});
}
// check vault folder
// console.log(`checking remote has folder /${this.vaultName}`);
if (this.vaultFolderExists) {
// console.log(`already checked, /${this.vaultName} exist before`)
} else {
const res = await this.dropbox.filesListFolder({
path: "",
recursive: false,
});
for (const item of res.result.entries) {
if (item.path_display === `/${this.vaultName}`) {
this.vaultFolderExists = true;
break;
}
}
if (!this.vaultFolderExists) {
console.log(`remote does not have folder /${this.vaultName}`);
await this.dropbox.filesCreateFolderV2({
path: `/${this.vaultName}`,
});
console.log(`remote folder /${this.vaultName} created`);
} else {
// console.log(`remote folder /${this.vaultName} exists`);
}
}
return this.dropbox;
};
}
/**
* @param dropboxConfig
* @returns
*/
export const getDropboxClient = (
dropboxConfig: DropboxConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any>
) => {
return new WrappedDropboxClient(
dropboxConfig,
vaultName,
saveUpdatedConfigFunc
);
};
export const getRemoteMeta = async (
client: WrappedDropboxClient,
fileOrFolderPath: string
) => {
await client.init();
if (fileOrFolderPath === "" || fileOrFolderPath === "/") {
// filesGetMetadata doesn't support root folder
// we instead try to list files
// if no error occurs, we ensemble a fake result.
const rsp = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`,
recursive: false, // don't need to recursive here
});
if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp));
}
return {
key: fileOrFolderPath,
lastModified: undefined,
size: 0,
remoteType: "dropbox",
etag: undefined,
} as RemoteItem;
}
const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const rsp = await client.dropbox.filesGetMetadata({
path: key,
});
if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp));
}
return fromDropboxItemToRemoteItem(rsp.result, client.vaultName);
};
export const uploadToRemote = async (
client: WrappedDropboxClient,
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 = getDropboxPath(uploadFile, client.vaultName);
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 {
try {
await client.dropbox.filesCreateFolderV2({
path: uploadFile,
});
foldersCreatedBefore?.add(uploadFile);
} catch (err) {
if (err.status === 409) {
// pass
foldersCreatedBefore?.add(uploadFile);
} else {
throw err;
}
}
}
const res = await getRemoteMeta(client, uploadFile);
return res;
} else {
// if encrypted, upload a fake file with the encrypted file name
await client.dropbox.filesUpload({
path: uploadFile,
contents: "",
});
return await getRemoteMeta(client, uploadFile);
}
} else {
// file
// we ignore isRecursively parameter here
const localContent = await vault.adapter.readBinary(fileOrFolderPath);
let remoteContent = localContent;
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
}
// in dropbox, we don't need to create folders before uploading! cool!
// TODO: filesUploadSession for larger files (>=150 MB)
await client.dropbox.filesUpload({
path: uploadFile,
contents: remoteContent,
mode: {
".tag": "overwrite",
},
});
// we want to mark that parent folders are created
if (foldersCreatedBefore !== undefined) {
const dirs = getFolderLevels(uploadFile).map((x) =>
getDropboxPath(x, client.vaultName)
);
for (const dir of dirs) {
foldersCreatedBefore?.add(dir);
}
}
return await getRemoteMeta(client, uploadFile);
}
};
export const listFromRemote = async (
client: WrappedDropboxClient,
prefix?: string
) => {
if (prefix !== undefined) {
throw Error("prefix not supported (yet)");
}
await client.init();
const res = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`,
recursive: true,
});
if (res.status !== 200) {
throw Error(JSON.stringify(res));
}
// console.log(res);
const contents = res.result.entries;
const unifiedContents = contents
.filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
fixLastModifiedTimeInplace(unifiedContents);
return {
Contents: unifiedContents,
};
};
const downloadFromRemoteRaw = async (
client: WrappedDropboxClient,
fileOrFolderPath: string
) => {
await client.init();
const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const rsp = await client.dropbox.filesDownload({
path: key,
});
if ((rsp.result as any).fileBlob !== undefined) {
// we get a Blob
const content = (rsp.result as any).fileBlob as Blob;
return await content.arrayBuffer();
} else if ((rsp.result as any).fileBinary !== undefined) {
// we get a Buffer
const content = (rsp.result as any).fileBinary as Buffer;
return bufferToArrayBuffer(content);
} else {
throw Error(`unknown rsp from dropbox download: ${rsp}`);
}
};
export const downloadFromRemote = async (
client: WrappedDropboxClient,
fileOrFolderPath: string,
vault: Vault,
mtime: number,
password: string = "",
remoteEncryptedKey: string = ""
) => {
const isFolder = fileOrFolderPath.endsWith("/");
await mkdirpInVault(fileOrFolderPath, vault);
// the file is always local file
// we need to encrypt it
if (isFolder) {
// mkdirp locally is enough
// do nothing here
} else {
let downloadFile = fileOrFolderPath;
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getDropboxPath(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: WrappedDropboxClient,
fileOrFolderPath: string,
password: string = "",
remoteEncryptedKey: string = ""
) => {
if (fileOrFolderPath === "/") {
return;
}
let remoteFileName = fileOrFolderPath;
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getDropboxPath(remoteFileName, client.vaultName);
await client.init();
try {
await client.dropbox.filesDeleteV2({
path: remoteFileName,
});
} catch (err) {
console.error("some error while deleting");
console.error(err);
}
};
export const checkConnectivity = async (client: WrappedDropboxClient) => {
try {
const results = await getRemoteMeta(client, "/");
if (results === undefined) {
return false;
}
return true;
} catch (err) {
console.error("dropbox connectivity error:");
console.error(err);
return false;
}
};
export const getUserDisplayName = async (client: WrappedDropboxClient) => {
await client.init();
const acct = await client.dropbox.usersGetCurrentAccount();
return acct.result.name.display_name;
};
export const revokeAuth = async (client: WrappedDropboxClient) => {
await client.init();
await client.dropbox.authTokenRevoke();
};
+6 -6
View File
@@ -225,12 +225,12 @@ const getObjectBodyToArrayBuffer = async (
b: Readable | ReadableStream | Blob b: Readable | ReadableStream | Blob
) => { ) => {
if (b instanceof Readable) { if (b instanceof Readable) {
const chunks: Uint8Array[] = []; return (await new Promise((resolve, reject) => {
for await (let chunk of b) { const chunks: Uint8Array[] = [];
chunks.push(chunk); b.on("data", (chunk) => chunks.push(chunk));
} b.on("error", reject);
const buf = Buffer.concat(chunks); b.on("end", () => resolve(bufferToArrayBuffer(Buffer.concat(chunks))));
return bufferToArrayBuffer(buf); })) as ArrayBuffer;
} else if (b instanceof ReadableStream) { } else if (b instanceof ReadableStream) {
return await new Response(b, {}).arrayBuffer(); return await new Response(b, {}).arrayBuffer();
} else if (b instanceof Blob) { } else if (b instanceof Blob) {
+39 -19
View File
@@ -9,7 +9,7 @@ import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import type { SUPPORTED_SERVICES_TYPE, RemoteItem } from "./baseTypes"; import type { SUPPORTED_SERVICES_TYPE, RemoteItem } from "./baseTypes";
import { mkdirpInVault, isHiddenPath, isVaildText } from "./misc"; import { mkdirpInVault, isHiddenPath, isVaildText, setToString } from "./misc";
import { import {
decryptBase32ToString, decryptBase32ToString,
encryptStringToBase32, encryptStringToBase32,
@@ -389,7 +389,7 @@ const getOperation = (
} }
if (r.decision === "unknown") { if (r.decision === "unknown") {
throw Error(`unknown decision for ${r}`); throw Error(`unknown decision for ${JSON.stringify(r)}`);
} }
return r; return r;
@@ -428,7 +428,8 @@ const dispatchOperationToActual = async (
client: RemoteClient, client: RemoteClient,
db: InternalDBs, db: InternalDBs,
vault: Vault, vault: Vault,
password: string = "" password: string = "",
foldersCreatedBefore: Set<string> | undefined = undefined
) => { ) => {
let remoteEncryptedKey = key; let remoteEncryptedKey = key;
if (password !== "") { if (password !== "") {
@@ -461,7 +462,8 @@ const dispatchOperationToActual = async (
vault, vault,
false, false,
password, password,
remoteEncryptedKey remoteEncryptedKey,
foldersCreatedBefore
); );
await upsertSyncMetaMappingData( await upsertSyncMetaMappingData(
client.serviceType, client.serviceType,
@@ -493,7 +495,8 @@ const dispatchOperationToActual = async (
vault, vault,
false, false,
password, password,
remoteEncryptedKey remoteEncryptedKey,
foldersCreatedBefore
); );
await upsertSyncMetaMappingData( await upsertSyncMetaMappingData(
client.serviceType, client.serviceType,
@@ -521,18 +524,35 @@ export const doActualSync = async (
password: string = "" password: string = ""
) => { ) => {
const keyStates = syncPlan.mixedStates; const keyStates = syncPlan.mixedStates;
await Promise.all( const foldersCreatedBefore = new Set<string>();
Object.entries(keyStates) for (const [k, v] of Object.entries(keyStates).sort(
.sort((k, v) => -(k as string).length) ([k1, v1], [k2, v2]) => k2.length - k1.length
.map(async ([k, v]) => )) {
dispatchOperationToActual( const k2 = k as string;
k as string, const v2 = v as FileOrFolderMixedState;
v as FileOrFolderMixedState, await dispatchOperationToActual(
client, k as string,
db, v as FileOrFolderMixedState,
vault, client,
password db,
) vault,
) password,
); foldersCreatedBefore
);
// console.log(`finished ${k}, with ${setToString(foldersCreatedBefore)}`);
}
// await Promise.all(
// Object.entries(keyStates)
// .map(async ([k, v]) =>
// dispatchOperationToActual(
// k as string,
// v as FileOrFolderMixedState,
// client,
// db,
// vault,
// password,
// foldersCreatedBefore
// )
// )
// );
}; };
+15 -1
View File
@@ -11,10 +11,24 @@
display: none; display: none;
} }
.dropbox-disclaimer {
font-weight: bold;
}
.dropbox-hide {
display: none;
}
.dropbox-auth-button-hide {
display: none;
}
.dropbox-revoke-auth-button-hide {
display: none;
}
.webdav-disclaimer { .webdav-disclaimer {
font-weight: bold; font-weight: bold;
} }
.webdav-hide { .webdav-hide {
display: none; display: none;
} }
+22
View File
@@ -69,6 +69,28 @@ describe("Misc: get folder levels", () => {
const res3 = ["xxx", "xxx/yyy", "xxx/yyy/zzz"]; const res3 = ["xxx", "xxx/yyy", "xxx/yyy/zzz"];
expect(misc.getFolderLevels(item3)).to.deep.equal(res3); expect(misc.getFolderLevels(item3)).to.deep.equal(res3);
}); });
it("should treat path starting with / correctly", () => {
const item = "/xxx/yyy/zzz.md";
const res = ["/xxx", "/xxx/yyy"];
expect(misc.getFolderLevels(item)).to.deep.equal(res);
const item2 = "/xxx/yyy/zzz";
const res2 = ["/xxx", "/xxx/yyy"];
expect(misc.getFolderLevels(item2)).to.deep.equal(res2);
const item3 = "/xxx/yyy/zzz/";
const res3 = ["/xxx", "/xxx/yyy", "/xxx/yyy/zzz"];
expect(misc.getFolderLevels(item3)).to.deep.equal(res3);
const item4 = "/xxx";
const res4 = [] as string[];
expect(misc.getFolderLevels(item4)).to.deep.equal(res4);
const item5 = "/";
const res5 = [] as string[];
expect(misc.getFolderLevels(item5)).to.deep.equal(res5);
});
}); });
describe("Misc: vaild file name tests", () => { describe("Misc: vaild file name tests", () => {
+1
View File
@@ -11,6 +11,7 @@
// "allowSyntheticDefaultImports": true, // "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"importHelpers": true, "importHelpers": true,
"isolatedModules": true,
"lib": ["dom", "es5", "scripthost", "es2015"] "lib": ["dom", "es5", "scripthost", "es2015"]
}, },
"include": ["**/*.ts"] "include": ["**/*.ts"]
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"0.1.5": "0.12.15" "0.1.10": "0.12.15"
} }
+7 -1
View File
@@ -1,7 +1,10 @@
require("dotenv").config();
const path = require("path"); const path = require("path");
const webpack = require("webpack"); const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
module.exports = { module.exports = {
entry: "./src/main.ts", entry: "./src/main.ts",
target: "web", target: "web",
@@ -11,6 +14,9 @@ module.exports = {
libraryTarget: "commonjs", libraryTarget: "commonjs",
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
}),
// Work around for Buffer is undefined: // Work around for Buffer is undefined:
// https://github.com/webpack/changelog-v5/issues/10 // https://github.com/webpack/changelog-v5/issues/10
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
@@ -37,7 +43,7 @@ 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"),
// 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"),