Compare commits

..
24 Commits
Author SHA1 Message Date
fyears 9a72ac8d66 0.2.2 2021-12-10 01:14:12 +08:00
fyears d980bcbc1b rename titles 2021-12-10 01:12:31 +08:00
fyears 6ebe1a8966 no support digest webdav now 2021-12-10 01:10:38 +08:00
fyears d0ab1d8abe remove webdav hide 2021-12-10 01:09:51 +08:00
fyears b86d116830 add desc for webdav readme 2021-12-10 01:05:47 +08:00
fyears 7d5eaad36f remove unnecessary log 2021-12-10 00:40:54 +08:00
fyears 7a13f4566c full word 2021-12-10 00:18:26 +08:00
fyears d355f72b7a update to 0.2.1 2021-12-10 00:17:31 +08:00
fyears 9b1eebdf90 standarized codes 2021-12-10 00:16:28 +08:00
fyears 2ce461ed6a allow multi pages in dropbox 2021-12-10 00:11:53 +08:00
fyears 3aa91a2a1e add env to build 2021-12-04 01:46:40 +08:00
fyears 2a3e0524c5 fix terrible typo 2021-12-04 01:21:37 +08:00
fyears 2f20ca61ce rebranding 2021-12-04 01:21:15 +08:00
fyears 68ec8b5454 bump version and update name 2021-12-04 01:18:20 +08:00
fyears 2bbbd569c5 fix typo 2021-12-04 01:11:17 +08:00
fyears 62936c4473 attempt to reduce size and use native crypto 2021-12-04 00:55:31 +08:00
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
12 changed files with 242 additions and 132 deletions
+2
View File
@@ -13,6 +13,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: env-for-buildci
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [16.x]
+12 -7
View File
@@ -4,7 +4,7 @@ This is yet another unofficial sync plugin for Obsidian.
## Disclaimer ## Disclaimer
- **This is NOT the [official sync service](https://github.com/fyears/obsidian-remotely-save.git) provided by Obsidian.** - **This is NOT the [official sync service](https://obsidian.md/sync) provided by Obsidian.**
## !!!Caution!!! ## !!!Caution!!!
@@ -12,7 +12,10 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
## Features ## Features
- **Amazon S3 or S3-compatible, and Dropbox services are supported.** Webdav supports on the half way. - Supports:
- Amazon S3 or S3-compatible
- Dropbox
- Webdav
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker". - **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password. - **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).** - **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
@@ -29,7 +32,7 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
## Download and Install ## Download and Install
- Option #1: [![BuildCI](https://github.com/fyears/obsidian-remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/obsidian-remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. - Option #1: [![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds.
- Option #2: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. - Option #2: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
- Option #3: The pluin would be submitted to the official "community plugin list" in near future. - Option #3: The pluin would be submitted to the official "community plugin list" in near future.
@@ -49,12 +52,14 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
- **This plugin's function for Dropbox is not as mature as functions for S3.** - **This plugin's function for Dropbox is not as mature as functions for S3.**
- **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API. - **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API.
- After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/App/obsidian-remotely-save` folder. - After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/remotely-save` folder.
- If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions. - If you decide to authorize this plugin to connect to Dropbox, please go to plugin's settings, and choose Dropbox then follow the instructions.
- Password-based end-to-end encryption is also supported. - Password-based end-to-end encryption is also supported.
### webdav ### webdav
- **webdav support is buggy (as of now, 20211122) and considered experimental, so it's hidden by default.** Highly recommend to use the more stable s3. - **webdav support is considered experimental.**
- If you decide to give it a try, open settings, and click "Choose service" area five times, then a Notice should show up. Close and open settings again then you will be able to select webdav. - Currently only supports BASIC authorization method.
- Currently webdav server should enable CORS for requests, because of technical limitations of mobile. - Currently webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods and webdav headers, because of technical limitations of mobile.
- Popular software NextCloud and OwnCloud do **NOT** enable CORS by default. If you are using any of them, you should 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 details.
+1
View File
@@ -24,6 +24,7 @@ esbuild
"obsidian", "obsidian",
"electron", "electron",
"fs", "fs",
"crypto",
// ...builtins // ...builtins
], ],
format: "cjs", format: "cjs",
+3 -3
View File
@@ -1,9 +1,9 @@
{ {
"id": "obsidian-remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.1.7", "version": "0.2.2",
"minAppVersion": "0.12.15", "minAppVersion": "0.12.15",
"description": "Yet another unofficial plugin allowing users to sync notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
"authorUrl": "https://github.com/fyears", "authorUrl": "https://github.com/fyears",
"isDesktopOnly": false "isDesktopOnly": false
+8 -8
View File
@@ -1,10 +1,11 @@
{ {
"name": "obsidian-remotely-save", "name": "remotely-save",
"version": "0.1.7", "version": "0.2.2",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs 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'"
@@ -12,10 +13,7 @@
"browser": { "browser": {
"path": "path-browserify", "path": "path-browserify",
"process": "process/browser", "process": "process/browser",
"stream": "stream-browserify", "stream": "stream-browserify"
"crypto": "crypto-browserify",
"util": "util/",
"assert": "assert/"
}, },
"source": "main.ts", "source": "main.ts",
"keywords": [], "keywords": [],
@@ -39,7 +37,9 @@
"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.64.4",
"webpack-cli": "^4.9.1"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.37.0", "@aws-sdk/client-s3": "^3.37.0",
+23 -61
View File
@@ -27,13 +27,16 @@ 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 "./remoteForS3";
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav"; import {
WebdavConfig,
DEFAULT_WEBDAV_CONFIG,
WebdavAuthType,
} from "./remoteForWebdav";
import { import {
DropboxConfig, DropboxConfig,
DEFAULT_DROPBOX_CONFIG, DEFAULT_DROPBOX_CONFIG,
getCodeVerifierAndChallenge, getAuthUrlAndVerifier,
getAuthUrl,
sendAuthReq, sendAuthReq,
setConfigBySuccessfullAuthInplace, setConfigBySuccessfullAuthInplace,
} from "./remoteForDropbox"; } from "./remoteForDropbox";
@@ -48,7 +51,6 @@ interface RemotelySavePluginSettings {
dropbox: DropboxConfig; dropbox: DropboxConfig;
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;
enableExperimentService: boolean;
} }
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
@@ -57,7 +59,6 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
dropbox: DEFAULT_DROPBOX_CONFIG, dropbox: DEFAULT_DROPBOX_CONFIG,
password: "", password: "",
serviceType: "s3", serviceType: "s3",
enableExperimentService: false,
}; };
export default class RemotelySavePlugin extends Plugin { export default class RemotelySavePlugin extends Plugin {
@@ -67,7 +68,7 @@ export default class RemotelySavePlugin extends Plugin {
syncStatus: SyncStatusType; syncStatus: SyncStatusType;
async onload() { async onload() {
console.log("loading plugin obsidian-remotely-save"); console.log("loading plugin remotely-save");
await this.loadSettings(); await this.loadSettings();
@@ -110,6 +111,7 @@ export default class RemotelySavePlugin extends Plugin {
this.settings.s3, this.settings.s3,
this.settings.webdav, this.settings.webdav,
this.settings.dropbox, this.settings.dropbox,
this.app.vault.getName(),
() => self.saveSettings() () => self.saveSettings()
); );
const remoteRsp = await client.listFromRemote(); const remoteRsp = await client.listFromRemote();
@@ -190,7 +192,7 @@ export default class RemotelySavePlugin extends Plugin {
} }
onunload() { onunload() {
console.log("unloading plugin obsidian-remotely-save"); console.log("unloading plugin remotely-save");
this.destroyDBs(); this.destroyDBs();
} }
@@ -295,13 +297,11 @@ export class DropboxAuthModal extends Modal {
this.revokeAuthSetting = revokeAuthSetting; this.revokeAuthSetting = revokeAuthSetting;
} }
onOpen() { async onOpen() {
let { contentEl } = this; let { contentEl } = this;
const k = getCodeVerifierAndChallenge(); const { authUrl, verifier } = await getAuthUrlAndVerifier(
const authUrl = getAuthUrl( this.plugin.settings.dropbox.clientID
this.plugin.settings.dropbox.clientID,
k.challenge
); );
contentEl.createEl("p", { contentEl.createEl("p", {
@@ -335,7 +335,7 @@ export class DropboxAuthModal extends Modal {
try { try {
const authRes = await sendAuthReq( const authRes = await sendAuthReq(
this.plugin.settings.dropbox.clientID, this.plugin.settings.dropbox.clientID,
k.verifier, verifier,
authCode authCode
); );
const self = this; const self = this;
@@ -349,6 +349,7 @@ export class DropboxAuthModal extends Modal {
undefined, undefined,
undefined, undefined,
this.plugin.settings.dropbox, this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
const username = await client.getUser(); const username = await client.getUser();
@@ -424,42 +425,9 @@ class RemotelySaveSettingTab extends PluginSettingTab {
// we need to create the div in advance of any other service divs // we need to create the div in advance of any other service divs
const serviceChooserDiv = generalDiv.createEl("div"); const serviceChooserDiv = generalDiv.createEl("div");
let clickChooserTimes = 0;
serviceChooserDiv.onClickEvent(async (x) => {
if (Platform.isIosApp) {
// downgrade the experiment
// because iOS doesn't support x.detail
clickChooserTimes += 1;
setTimeout(function () {
clickChooserTimes = 0;
}, 2000);
}
if ((Platform.isIosApp && clickChooserTimes === 5) || x.detail === 5) {
if (this.plugin.settings.serviceType === "webdav") {
new Notice(
"You've enabled hidden unstable experimental webdav support before. Nothing changes."
);
} else if (!this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = true;
await this.plugin.saveSettings();
new Notice(
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
);
} else if (this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = false;
await this.plugin.saveSettings();
new Notice(
"You've disabled hidden unstable experimental webdav support. Reopen settings again."
);
}
}
x.preventDefault();
});
const s3Div = containerEl.createEl("div", { cls: "s3-hide" }); const s3Div = containerEl.createEl("div", { cls: "s3-hide" });
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3"); s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
s3Div.createEl("h2", { text: "S3 (-compatible) Service" }); s3Div.createEl("h2", { text: "Remote For S3 (-compatible)" });
s3Div.createEl("p", { s3Div.createEl("p", {
text: "You can use Amazon S3 or another S3-compatible service to sync your vault. Enter your bucket information below.", text: "You can use Amazon S3 or another S3-compatible service to sync your vault. Enter your bucket information below.",
@@ -586,7 +554,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
"dropbox-hide", "dropbox-hide",
this.plugin.settings.serviceType !== "dropbox" this.plugin.settings.serviceType !== "dropbox"
); );
dropboxDiv.createEl("h2", { text: "for Dropbox" }); dropboxDiv.createEl("h2", { text: "Remote For Dropbox" });
dropboxDiv.createEl("p", { dropboxDiv.createEl("p", {
text: "Disclaimer: Sync support for Dropbox are more experimental, and s3 functions are more stable now.", text: "Disclaimer: Sync support for Dropbox are more experimental, and s3 functions are more stable now.",
cls: "dropbox-disclaimer", cls: "dropbox-disclaimer",
@@ -600,7 +568,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
cls: "dropbox-disclaimer", cls: "dropbox-disclaimer",
}); });
dropboxDiv.createEl("p", { 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.", text: "We will create a folder Apps/remotely-save on your Dropbox. All files/folders sync would happen inside this folder.",
}); });
const dropboxSelectAuthDiv = dropboxDiv.createDiv(); const dropboxSelectAuthDiv = dropboxDiv.createDiv();
@@ -626,6 +594,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
undefined, undefined,
undefined, undefined,
this.plugin.settings.dropbox, this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
await client.revokeAuth(); await client.revokeAuth();
@@ -687,6 +656,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
undefined, undefined,
undefined, undefined,
this.plugin.settings.dropbox, this.plugin.settings.dropbox,
this.app.vault.getName(),
() => self.plugin.saveSettings() () => self.plugin.saveSettings()
); );
@@ -705,7 +675,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.serviceType !== "webdav" this.plugin.settings.serviceType !== "webdav"
); );
webdavDiv.createEl("h2", { text: "Webdav Service" }); webdavDiv.createEl("h2", { text: "Remote For Webdav" });
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: "Disclaimer: Webdav functions are more experimental, and s3 functions are more stable now.", text: "Disclaimer: Webdav functions are more experimental, and s3 functions are more stable now.",
@@ -767,7 +737,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
) )
.addDropdown((dropdown) => { .addDropdown((dropdown) => {
dropdown.addOption("basic", "basic"); dropdown.addOption("basic", "basic");
dropdown.addOption("digest", "digest"); // dropdown.addOption("digest", "digest");
dropdown dropdown
.setValue(this.plugin.settings.webdav.authType) .setValue(this.plugin.settings.webdav.authType)
@@ -805,18 +775,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
.setDesc("Choose a service, by default s3") .setDesc("Choose a service, by default s3")
.addDropdown(async (dropdown) => { .addDropdown(async (dropdown) => {
const currService = this.plugin.settings.serviceType; const currService = this.plugin.settings.serviceType;
const enableExperimentService =
this.plugin.settings.enableExperimentService;
dropdown.addOption("s3", "s3 (-compatible)"); dropdown.addOption("s3", "s3 (-compatible)");
dropdown.addOption("dropbox", "Dropbox"); dropdown.addOption("dropbox", "Dropbox");
if (currService === "webdav" || enableExperimentService) { dropdown.addOption("webdav", "webdav (experimental)");
dropdown.addOption("webdav", "webdav (experimental)");
if (!enableExperimentService) {
this.plugin.settings.enableExperimentService = true;
await this.plugin.saveSettings();
}
}
dropdown dropdown
.setValue(this.plugin.settings.serviceType) .setValue(this.plugin.settings.serviceType)
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => { .onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
+8 -4
View File
@@ -1,8 +1,8 @@
import { Vault } from "obsidian"; 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 "./remoteForS3";
import * as webdav from "./webdav"; import * as webdav from "./remoteForWebdav";
import * as dropbox from "./remoteForDropbox"; import * as dropbox from "./remoteForDropbox";
export class RemoteClient { export class RemoteClient {
@@ -19,6 +19,7 @@ export class RemoteClient {
s3Config?: s3.S3Config, s3Config?: s3.S3Config,
webdavConfig?: webdav.WebdavConfig, webdavConfig?: webdav.WebdavConfig,
dropboxConfig?: dropbox.DropboxConfig, dropboxConfig?: dropbox.DropboxConfig,
vaultName?: string,
saveUpdatedConfigFunc?: () => Promise<any> saveUpdatedConfigFunc?: () => Promise<any>
) { ) {
this.serviceType = serviceType; this.serviceType = serviceType;
@@ -31,12 +32,15 @@ export class RemoteClient {
this.webdavConfig = webdavConfig; this.webdavConfig = webdavConfig;
this.webdavClient = webdav.getWebdavClient(this.webdavConfig); this.webdavClient = webdav.getWebdavClient(this.webdavConfig);
} else if (serviceType === "dropbox") { } else if (serviceType === "dropbox") {
if (saveUpdatedConfigFunc === undefined) { if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
throw Error("remember to provide callback while init dropbox client"); throw Error(
"remember to provide vault name and callback while init dropbox client"
);
} }
this.dropboxConfig = dropboxConfig; this.dropboxConfig = dropboxConfig;
this.dropboxClient = dropbox.getDropboxClient( this.dropboxClient = dropbox.getDropboxClient(
this.dropboxConfig, this.dropboxConfig,
vaultName,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
} else { } else {
+108 -47
View File
@@ -1,9 +1,7 @@
import * as path from "path"; import * as path from "path";
import { FileStats, Vault } from "obsidian"; import { FileStats, Vault } from "obsidian";
import { Buffer } from "buffer";
import * as crypto from "crypto";
import { Dropbox, DropboxResponse, files } from "dropbox"; import { Dropbox, DropboxAuth, DropboxResponse, files } from "dropbox";
export { Dropbox } from "dropbox"; export { Dropbox } from "dropbox";
import { RemoteItem } from "./baseTypes"; import { RemoteItem } from "./baseTypes";
import { import {
@@ -15,7 +13,6 @@ import {
setToString, setToString,
} from "./misc"; } from "./misc";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { strict as assert } from "assert";
export interface DropboxConfig { export interface DropboxConfig {
accessToken: string; accessToken: string;
@@ -37,10 +34,11 @@ export const DEFAULT_DROPBOX_CONFIG = {
username: "", username: "",
}; };
export const getDropboxPath = (fileOrFolderPath: string) => { export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
let key = fileOrFolderPath; let key = fileOrFolderPath;
if (!fileOrFolderPath.startsWith("/")) { if (!fileOrFolderPath.startsWith("/")) {
key = `/${fileOrFolderPath}`; // then this is original path in Obsidian
key = `/${vaultName}/${fileOrFolderPath}`;
} }
if (key.endsWith("/")) { if (key.endsWith("/")) {
key = key.slice(0, key.length - 1); key = key.slice(0, key.length - 1);
@@ -48,20 +46,26 @@ export const getDropboxPath = (fileOrFolderPath: string) => {
return key; return key;
}; };
const getNormPath = (fileOrFolderPath: string) => { const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
if (fileOrFolderPath.startsWith("/")) { if (
return fileOrFolderPath.slice(1); !(
fileOrFolderPath === `/${vaultName}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`)
)
) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
} }
return fileOrFolderPath; return fileOrFolderPath.slice(`/${vaultName}/`.length);
}; };
const fromDropboxItemToRemoteItem = ( const fromDropboxItemToRemoteItem = (
x: x:
| files.FileMetadataReference | files.FileMetadataReference
| files.FolderMetadataReference | files.FolderMetadataReference
| files.DeletedMetadataReference | files.DeletedMetadataReference,
vaultName: string
): RemoteItem => { ): RemoteItem => {
let key = getNormPath(x.path_display); let key = getNormPath(x.path_display, vaultName);
if (x[".tag"] === "folder" && !key.endsWith("/")) { if (x[".tag"] === "folder" && !key.endsWith("/")) {
key = `${key}/`; key = `${key}/`;
} }
@@ -139,7 +143,6 @@ const fixLastModifiedTimeInplace = (allFilesFolders: RemoteItem[]) => {
if (item.lastModified !== undefined) { if (item.lastModified !== undefined) {
continue; // don't need to deal with it continue; // don't need to deal with it
} }
assert(!(item.key in potentialMTime));
const parent = `${path.posix.dirname(item.key)}/`; const parent = `${path.posix.dirname(item.key)}/`;
if (parent in potentialMTime) { if (parent in potentialMTime) {
item.lastModified = potentialMTime[parent]; item.lastModified = potentialMTime[parent];
@@ -157,32 +160,28 @@ const fixLastModifiedTimeInplace = (allFilesFolders: RemoteItem[]) => {
// see https://dropbox.tech/developers/pkce--what-and-why- // see https://dropbox.tech/developers/pkce--what-and-why-
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const specialBase64Encode = (str: Buffer) => { export const getAuthUrlAndVerifier = async (appKey: string) => {
return str const auth = new DropboxAuth({
.toString("base64") clientId: appKey,
.replace(/\+/g, "-") });
.replace(/\//g, "_") const authUrl = (
.replace(/=/g, ""); await auth.getAuthenticationUrl(
}; undefined,
const sha256 = (buffer: string) => { undefined,
return crypto.createHash("sha256").update(buffer).digest(); "code",
}; "offline",
undefined,
export const getCodeVerifierAndChallenge = () => { "none",
const codeVerifier = specialBase64Encode(crypto.randomBytes(32)); true
// console.log(`Client generated code_verifier: ${codeVerifier}`); )
const codeChallenge = specialBase64Encode(sha256(codeVerifier)); ).toString();
// console.log(`Client generated code_challenge: ${codeChallenge}`); const verifier = auth.getCodeVerifier();
return { return {
verifier: codeVerifier, authUrl: authUrl,
challenge: codeChallenge, verifier: verifier,
}; };
}; };
export const getAuthUrl = (appKey: string, challenge: string) => {
return `https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&token_access_type=offline`;
};
export interface DropboxSuccessAuthRes { export interface DropboxSuccessAuthRes {
access_token: string; access_token: string;
token_type: "bearer"; token_type: "bearer";
@@ -261,17 +260,23 @@ export const setConfigBySuccessfullAuthInplace = async (
export class WrappedDropboxClient { export class WrappedDropboxClient {
dropboxConfig: DropboxConfig; dropboxConfig: DropboxConfig;
vaultName: string;
saveUpdatedConfigFunc: () => Promise<any>; saveUpdatedConfigFunc: () => Promise<any>;
dropbox: Dropbox; dropbox: Dropbox;
vaultFolderExists: boolean;
constructor( constructor(
dropboxConfig: DropboxConfig, dropboxConfig: DropboxConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) { ) {
this.dropboxConfig = dropboxConfig; this.dropboxConfig = dropboxConfig;
this.vaultName = vaultName;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.vaultFolderExists = false;
} }
init = async () => { init = async () => {
// check token
if ( if (
this.dropboxConfig.accessToken === "" || this.dropboxConfig.accessToken === "" ||
this.dropboxConfig.refreshToken === "" this.dropboxConfig.refreshToken === ""
@@ -303,6 +308,33 @@ export class WrappedDropboxClient {
accessToken: this.dropboxConfig.accessToken, 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; return this.dropbox;
}; };
} }
@@ -313,9 +345,14 @@ export class WrappedDropboxClient {
*/ */
export const getDropboxClient = ( export const getDropboxClient = (
dropboxConfig: DropboxConfig, dropboxConfig: DropboxConfig,
vaultName: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) => { ) => {
return new WrappedDropboxClient(dropboxConfig, saveUpdatedConfigFunc); return new WrappedDropboxClient(
dropboxConfig,
vaultName,
saveUpdatedConfigFunc
);
}; };
export const getRemoteMeta = async ( export const getRemoteMeta = async (
@@ -328,7 +365,7 @@ export const getRemoteMeta = async (
// we instead try to list files // we instead try to list files
// if no error occurs, we ensemble a fake result. // if no error occurs, we ensemble a fake result.
const rsp = await client.dropbox.filesListFolder({ const rsp = await client.dropbox.filesListFolder({
path: "", path: `/${client.vaultName}`,
recursive: false, // don't need to recursive here recursive: false, // don't need to recursive here
}); });
if (rsp.status !== 200) { if (rsp.status !== 200) {
@@ -343,7 +380,7 @@ export const getRemoteMeta = async (
} as RemoteItem; } as RemoteItem;
} }
const key = getDropboxPath(fileOrFolderPath); const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const rsp = await client.dropbox.filesGetMetadata({ const rsp = await client.dropbox.filesGetMetadata({
path: key, path: key,
@@ -351,7 +388,7 @@ export const getRemoteMeta = async (
if (rsp.status !== 200) { if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp)); throw Error(JSON.stringify(rsp));
} }
return fromDropboxItemToRemoteItem(rsp.result); return fromDropboxItemToRemoteItem(rsp.result, client.vaultName);
}; };
export const uploadToRemote = async ( export const uploadToRemote = async (
@@ -369,7 +406,7 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
uploadFile = remoteEncryptedKey; uploadFile = remoteEncryptedKey;
} }
uploadFile = getDropboxPath(uploadFile); uploadFile = getDropboxPath(uploadFile, client.vaultName);
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
@@ -425,7 +462,9 @@ export const uploadToRemote = async (
}); });
// we want to mark that parent folders are created // we want to mark that parent folders are created
if (foldersCreatedBefore !== undefined) { if (foldersCreatedBefore !== undefined) {
const dirs = getFolderLevels(uploadFile).map(getDropboxPath); const dirs = getFolderLevels(uploadFile).map((x) =>
getDropboxPath(x, client.vaultName)
);
for (const dir of dirs) { for (const dir of dirs) {
foldersCreatedBefore?.add(dir); foldersCreatedBefore?.add(dir);
} }
@@ -442,19 +481,41 @@ export const listFromRemote = async (
throw Error("prefix not supported (yet)"); throw Error("prefix not supported (yet)");
} }
await client.init(); await client.init();
const res = await client.dropbox.filesListFolder({ let res = await client.dropbox.filesListFolder({
path: "", path: `/${client.vaultName}`,
recursive: true, recursive: true,
include_deleted: false,
limit: 1000,
}); });
if (res.status !== 200) { if (res.status !== 200) {
throw Error(JSON.stringify(res)); throw Error(JSON.stringify(res));
} }
// console.log(res); // console.log(res);
const contents = res.result.entries; const contents = res.result.entries;
const unifiedContents = contents const unifiedContents = contents
.filter((x) => x[".tag"] !== "deleted") .filter((x) => x[".tag"] !== "deleted")
.map(fromDropboxItemToRemoteItem); .filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
while (res.result.has_more) {
res = await client.dropbox.filesListFolderContinue({
cursor: res.result.cursor,
});
if (res.status !== 200) {
throw Error(JSON.stringify(res));
}
const contents2 = res.result.entries;
const unifiedContents2 = contents2
.filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
unifiedContents.push(...unifiedContents2);
}
fixLastModifiedTimeInplace(unifiedContents); fixLastModifiedTimeInplace(unifiedContents);
return { return {
Contents: unifiedContents, Contents: unifiedContents,
}; };
@@ -465,7 +526,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPath: string fileOrFolderPath: string
) => { ) => {
await client.init(); await client.init();
const key = getDropboxPath(fileOrFolderPath); const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const rsp = await client.dropbox.filesDownload({ const rsp = await client.dropbox.filesDownload({
path: key, path: key,
}); });
@@ -505,7 +566,7 @@ export const downloadFromRemote = async (
if (password !== "") { if (password !== "") {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getDropboxPath(downloadFile); downloadFile = getDropboxPath(downloadFile, client.vaultName);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (password !== "") {
@@ -530,7 +591,7 @@ export const deleteFromRemote = async (
if (password !== "") { if (password !== "") {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getDropboxPath(remoteFileName); remoteFileName = getDropboxPath(remoteFileName, client.vaultName);
await client.init(); await client.init();
try { try {
View File
+1 -1
View File
@@ -226,7 +226,7 @@ export const deleteFromRemote = async (
remoteFileName = getWebdavPath(remoteFileName); remoteFileName = getWebdavPath(remoteFileName);
try { try {
await client.deleteFile(remoteFileName); await client.deleteFile(remoteFileName);
console.log(`delete ${remoteFileName} succeeded`); // console.log(`delete ${remoteFileName} succeeded`);
} catch (err) { } catch (err) {
console.error("some error while deleting"); console.error("some error while deleting");
console.log(err); console.log(err);
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"0.1.7": "0.12.15" "0.2.2": "0.12.15"
} }
+75
View File
@@ -0,0 +1,75 @@
require("dotenv").config();
const path = require("path");
const webpack = require("webpack");
const TerserPlugin = require("terser-webpack-plugin");
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
module.exports = {
entry: "./src/main.ts",
target: "web",
output: {
filename: "main.js",
path: __dirname,
libraryTarget: "commonjs",
},
plugins: [
new webpack.DefinePlugin({
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
}),
// Work around for Buffer is undefined:
// https://github.com/webpack/changelog-v5/issues/10
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"],
}),
new webpack.ProvidePlugin({
process: "process/browser",
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
mainFields: ["browser", "module", "main"],
fallback: {
// assert: require.resolve("assert"),
// buffer: require.resolve("buffer/"),
// console: require.resolve("console-browserify"),
// constants: require.resolve("constants-browserify"),
// crypto: require.resolve("crypto-browserify"),
crypto: false,
// domain: require.resolve("domain-browser"),
// events: require.resolve("events"),
// http: require.resolve("stream-http"),
// https: require.resolve("https-browserify"),
// os: require.resolve("os-browserify/browser"),
path: require.resolve("path-browserify"),
// punycode: require.resolve("punycode"),
process: require.resolve("process/browser"),
// querystring: require.resolve("querystring-es3"),
stream: require.resolve("stream-browserify"),
// string_decoder: require.resolve("string_decoder"),
// sys: require.resolve("util"),
// timers: require.resolve("timers-browserify"),
// tty: require.resolve("tty-browserify"),
// url: require.resolve("url"),
// util: require.resolve("util"),
// vm: require.resolve("vm-browserify"),
// zlib: require.resolve("browserify-zlib"),
},
},
externals: {
obsidian: "commonjs2 obsidian",
},
optimization: {
minimize: true,
minimizer: [new TerserPlugin({ extractComments: false })],
},
};