Compare commits

...
19 Commits
Author SHA1 Message Date
fyears b6bae792c6 0.2.3 2021-12-11 17:34:45 +08:00
fyears 8624141780 add qrcode export import 2021-12-11 17:33:55 +08:00
fyears 563c2c3ff6 readme 2021-12-10 01:34:19 +08:00
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
15 changed files with 351 additions and 159 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]
+27 -11
View File
@@ -4,15 +4,18 @@ This is yet another unofficial sync plugin for Obsidian.
## Disclaimer ## Disclaimer
- **This is NOT the [official sync service](https://github.com/fyears/obsidian-remotely-save.git) provided by Obsidian.** - **This is NOT the [official sync service](https://obsidian.md/sync) provided by Obsidian.**
## !!!Caution!!! ## !!!Caution!!!
As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for any serious vaults.** **Backup your vault before using this plugin.** Don't be surprise to data loss! As of Dec 2021, the plugin is considered in BETA stage. **DO NOT USE IT for any serious vaults.** **ALWAYS, ALWAYS, backup your vault before using this plugin.**
## Features ## Features
- **Amazon S3 or S3-compatible, and Dropbox services are supported.** Webdav supports on the half way. - Supports:
- Amazon S3 or S3-compatible
- Dropbox
- Webdav
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker". - **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password. - **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).** - **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
@@ -27,15 +30,25 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
- **Cloud services cost you money.** Always be aware of the costs and pricing. - **Cloud services cost you money.** Always be aware of the costs and pricing.
- **All files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced. - **All files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced.
## Questions, Suggestions, Or Bugs
You are greatly welcome to ask questions, post any suggestions, or report any bugs! The project is mainly maintained on GitHub:
- Questions: [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions)
- Suggestions: also in [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions)
- Bugs: [GitHub repo Issues](https://github.com/fyears/remotely-save/issues) (NOT Discussion)
Additionally, the plugin author may occasionally visit Obsidian official forum and official Discord server, and pay attention to this-plugin-related information there.
## Download and Install ## Download and Install
- Option #1: [![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: You can search, download, and install the plugin in official "community plugin list".
- Option #2: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. - Option #2: [![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 #3: The pluin would be submitted to the official "community plugin list" in near future. - Option #3: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
## Usage ## Usage
### s3 ### S3
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault. - Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android. - Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
@@ -49,12 +62,15 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
- **This plugin's function for Dropbox is not as mature as functions for S3.** - **This plugin's function for Dropbox is not as mature as functions for S3.**
- **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API. - **This plugin is NOT an official Dropbox product.** The plugin just uses Dropbox's public API.
- After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/obsidian-remotely-save` folder. - After the authorization, the plugin can read your name and email (which cannot be unselected on Dropbox api), and read and write files in your Dropbox's `/Apps/remotely-save` 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** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies.
- Popular software NextCloud and OwnCloud do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin.
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
- Password-based end-to-end encryption is also supported.
+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.10", "version": "0.2.3",
"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
+5 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsidian-remotely-save", "name": "remotely-save",
"version": "0.1.10", "version": "0.2.3",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
@@ -13,10 +13,7 @@
"browser": { "browser": {
"path": "path-browserify", "path": "path-browserify",
"process": "process/browser", "process": "process/browser",
"stream": "stream-browserify", "stream": "stream-browserify"
"crypto": "crypto-browserify",
"util": "util/",
"assert": "assert/"
}, },
"source": "main.ts", "source": "main.ts",
"keywords": [], "keywords": [],
@@ -28,6 +25,7 @@
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node": "^14.14.37", "@types/node": "^14.14.37",
"@types/qrcode": "^1.4.1",
"builtin-modules": "^3.2.0", "builtin-modules": "^3.2.0",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
@@ -60,6 +58,7 @@
"obsidian": "^0.12.0", "obsidian": "^0.12.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"process": "^0.11.10", "process": "^0.11.10",
"qrcode": "^1.5.0",
"rfc4648": "^1.5.0", "rfc4648": "^1.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
+46
View File
@@ -1,9 +1,45 @@
/** /**
* Only type defs here. * Only type defs here.
* To avoid circular dependency.
*/ */
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox"; export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
export interface S3Config {
s3Endpoint: string;
s3Region: string;
s3AccessKeyID: string;
s3SecretAccessKey: string;
s3BucketName: string;
}
export interface DropboxConfig {
accessToken: string;
clientID: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
accessTokenExpiresAtTime: number;
accountID: string;
username: string;
}
export type WebdavAuthType = "digest" | "basic";
export interface WebdavConfig {
address: string;
username: string;
password: string;
authType: WebdavAuthType;
}
export interface RemotelySavePluginSettings {
s3: S3Config;
webdav: WebdavConfig;
dropbox: DropboxConfig;
password: string;
serviceType: SUPPORTED_SERVICES_TYPE;
}
export interface RemoteItem { export interface RemoteItem {
key: string; key: string;
lastModified: number; lastModified: number;
@@ -11,3 +47,13 @@ export interface RemoteItem {
remoteType: SUPPORTED_SERVICES_TYPE; remoteType: SUPPORTED_SERVICES_TYPE;
etag?: string; etag?: string;
} }
export const COMMAND_URI = "remotely-save";
export const COMMAND_CALLBACK = "remotely-save-cb";
export interface UriParams {
func?: string;
vault?: string;
ver?: string;
data?: string;
}
+76
View File
@@ -0,0 +1,76 @@
import QRCode from "qrcode";
import {
COMMAND_URI,
UriParams,
RemotelySavePluginSettings,
} from "./baseTypes";
export const exportQrCodeUri = async (
settings: RemotelySavePluginSettings,
currentVaultName: string,
pluginVersion: string
) => {
const data = encodeURIComponent(JSON.stringify(settings));
const vault = encodeURIComponent(currentVaultName);
const version = encodeURIComponent(pluginVersion);
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
// console.log(uri)
const imgUri = await QRCode.toDataURL(rawUri);
return {
rawUri,
imgUri,
};
};
export interface ProcessQrCodeResultType {
status: "error" | "ok";
message: string;
result?: RemotelySavePluginSettings;
}
export const importQrCodeUri = (
inputParams: any,
currentVaultName: string
): ProcessQrCodeResultType => {
let params = inputParams as UriParams;
if (
params.func === undefined ||
params.func !== "settings" ||
params.vault === undefined ||
params.data === undefined
) {
return {
status: "error",
message: `the uri is not for exporting/importing settings: ${JSON.stringify(
inputParams
)}`,
};
}
if (params.vault !== currentVaultName) {
return {
status: "error",
message: `the target vault is ${
params.vault
} but you are currently in ${currentVaultName}: ${JSON.stringify(
inputParams
)}`,
};
}
let settings = {} as RemotelySavePluginSettings;
try {
settings = JSON.parse(params.data);
} catch (e) {
return {
status: "error",
message: `errors while parsing settings: ${JSON.stringify(inputParams)}`,
};
}
return {
status: "ok",
message: "ok",
result: settings,
};
};
+123 -70
View File
@@ -27,29 +27,28 @@ import type { InternalDBs } from "./localdb";
import type { SyncStatusType, PasswordCheckType } from "./sync"; import type { SyncStatusType, PasswordCheckType } from "./sync";
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync"; import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
import { S3Config, DEFAULT_S3_CONFIG } from "./s3"; import { DEFAULT_S3_CONFIG } from "./remoteForS3";
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav"; import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav";
import { import {
DropboxConfig,
DEFAULT_DROPBOX_CONFIG, DEFAULT_DROPBOX_CONFIG,
getCodeVerifierAndChallenge, getAuthUrlAndVerifier,
getAuthUrl,
sendAuthReq, sendAuthReq,
setConfigBySuccessfullAuthInplace, setConfigBySuccessfullAuthInplace,
} from "./remoteForDropbox"; } from "./remoteForDropbox";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { exportSyncPlansToFiles } from "./debugMode"; import { exportSyncPlansToFiles } from "./debugMode";
import { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import { COMMAND_URI, COMMAND_CALLBACK } from "./baseTypes";
import type {
interface RemotelySavePluginSettings { SUPPORTED_SERVICES_TYPE,
s3: S3Config; S3Config,
webdav: WebdavConfig; DropboxConfig,
dropbox: DropboxConfig; WebdavAuthType,
password: string; WebdavConfig,
serviceType: SUPPORTED_SERVICES_TYPE; RemotelySavePluginSettings,
enableExperimentService: boolean; } from "./baseTypes";
} import type { ProcessQrCodeResultType } from "./importExport";
import { exportQrCodeUri, importQrCodeUri } from "./importExport";
const DEFAULT_SETTINGS: RemotelySavePluginSettings = { const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
s3: DEFAULT_S3_CONFIG, s3: DEFAULT_S3_CONFIG,
@@ -57,7 +56,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 +65,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();
@@ -87,6 +85,32 @@ export default class RemotelySavePlugin extends Plugin {
}) })
); );
this.registerObsidianProtocolHandler(COMMAND_URI, async (inputParams) => {
const parsed = importQrCodeUri(inputParams, this.app.vault.getName());
if (parsed.status === "error") {
new Notice(parsed.message);
} else {
const copied = JSON.parse(JSON.stringify(parsed.result));
// new Notice(JSON.stringify(copied))
this.settings = copied;
this.saveSettings();
new Notice(
`New settings for ${this.manifest.name} saved. Reopen the plugin Settings to the effect.`
);
}
});
this.registerObsidianProtocolHandler(
COMMAND_CALLBACK,
async (inputParams) => {
new Notice(
`Your uri call a callback that's not supported yet: ${JSON.stringify(
inputParams
)}`
);
}
);
this.addRibbonIcon("switch", "Remotely Save", async () => { this.addRibbonIcon("switch", "Remotely Save", async () => {
if (this.syncStatus !== "idle") { if (this.syncStatus !== "idle") {
new Notice( new Notice(
@@ -191,7 +215,7 @@ export default class RemotelySavePlugin extends Plugin {
} }
onunload() { onunload() {
console.log("unloading plugin obsidian-remotely-save"); console.log("unloading plugin remotely-save");
this.destroyDBs(); this.destroyDBs();
} }
@@ -296,13 +320,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", {
@@ -336,7 +358,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;
@@ -383,6 +405,60 @@ export class DropboxAuthModal extends Modal {
} }
} }
export class ExportSettingsQrCodeModal extends Modal {
plugin: RemotelySavePlugin;
constructor(app: App, plugin: RemotelySavePlugin) {
super(app);
this.plugin = plugin;
}
async onOpen() {
let { contentEl } = this;
const { rawUri, imgUri } = await exportQrCodeUri(
this.plugin.settings,
this.app.vault.getName(),
this.plugin.manifest.version
);
contentEl.createEl("p", {
text: "You can use another device to scan this qrcode.",
});
contentEl.createEl("p", {
text: "Or, you can click the button to copy the special url.",
});
contentEl.createEl(
"button",
{
text: "Click to copy the special URI",
},
(el) => {
el.onclick = async () => {
await navigator.clipboard.writeText(rawUri);
new Notice("special uri copied to clipboard!");
};
}
);
contentEl.createEl(
"img",
{
cls: "qrcode-img",
},
async (el) => {
el.src = imgUri;
}
);
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class RemotelySaveSettingTab extends PluginSettingTab { class RemotelySaveSettingTab extends PluginSettingTab {
plugin: RemotelySavePlugin; plugin: RemotelySavePlugin;
@@ -426,42 +502,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.",
@@ -588,7 +631,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",
@@ -602,7 +645,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();
@@ -709,7 +752,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.",
@@ -771,7 +814,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)
@@ -809,18 +852,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) => {
@@ -841,6 +876,24 @@ class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
// import and export
const importExportDiv = containerEl.createEl("div");
importExportDiv.createEl("h2", { text: "Import and Export Settings" });
new Setting(importExportDiv)
.setName("export")
.setDesc("export all settings by generating qrcode")
.addButton(async (button) => {
button.setButtonText("Get QR Code");
button.onClick(async () => {
new ExportSettingsQrCodeModal(this.app, this.plugin).open();
});
});
new Setting(importExportDiv)
.setName("import")
.setDesc("You should manually open a camera app to scan the QR code");
const debugDiv = containerEl.createEl("div"); const debugDiv = containerEl.createEl("div");
debugDiv.createEl("h2", { text: "Debug" }); debugDiv.createEl("h2", { text: "Debug" });
const syncPlanDiv = debugDiv.createEl("div"); const syncPlanDiv = debugDiv.createEl("div");
+14 -9
View File
@@ -1,24 +1,29 @@
import { Vault } from "obsidian"; import { Vault } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import type {
import * as s3 from "./s3"; SUPPORTED_SERVICES_TYPE,
import * as webdav from "./webdav"; S3Config,
DropboxConfig,
WebdavConfig,
} from "./baseTypes";
import * as s3 from "./remoteForS3";
import * as webdav from "./remoteForWebdav";
import * as dropbox from "./remoteForDropbox"; import * as dropbox from "./remoteForDropbox";
export class RemoteClient { export class RemoteClient {
readonly serviceType: SUPPORTED_SERVICES_TYPE; readonly serviceType: SUPPORTED_SERVICES_TYPE;
readonly s3Client?: s3.S3Client; readonly s3Client?: s3.S3Client;
readonly s3Config?: s3.S3Config; readonly s3Config?: S3Config;
readonly webdavClient?: webdav.WebDAVClient; readonly webdavClient?: webdav.WebDAVClient;
readonly webdavConfig?: webdav.WebdavConfig; readonly webdavConfig?: WebdavConfig;
readonly dropboxClient?: dropbox.WrappedDropboxClient; readonly dropboxClient?: dropbox.WrappedDropboxClient;
readonly dropboxConfig?: dropbox.DropboxConfig; readonly dropboxConfig?: DropboxConfig;
constructor( constructor(
serviceType: SUPPORTED_SERVICES_TYPE, serviceType: SUPPORTED_SERVICES_TYPE,
s3Config?: s3.S3Config, s3Config?: S3Config,
webdavConfig?: webdav.WebdavConfig, webdavConfig?: WebdavConfig,
dropboxConfig?: dropbox.DropboxConfig, dropboxConfig?: DropboxConfig,
vaultName?: string, vaultName?: string,
saveUpdatedConfigFunc?: () => Promise<any> saveUpdatedConfigFunc?: () => Promise<any>
) { ) {
+42 -37
View File
@@ -1,11 +1,9 @@
import * as path from "path"; import * as path from "path";
import { FileStats, Vault } from "obsidian"; import { FileStats, Vault } from "obsidian";
import { Buffer } from "buffer";
import * as crypto from "crypto";
import { Dropbox, DropboxResponse, files } from "dropbox"; import { Dropbox, DropboxAuth, DropboxResponse, files } from "dropbox";
export { Dropbox } from "dropbox"; export { Dropbox } from "dropbox";
import { RemoteItem } from "./baseTypes"; import { RemoteItem, DropboxConfig } from "./baseTypes";
import { import {
arrayBufferToBuffer, arrayBufferToBuffer,
bufferToArrayBuffer, bufferToArrayBuffer,
@@ -16,16 +14,6 @@ import {
} from "./misc"; } from "./misc";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
export interface DropboxConfig {
accessToken: string;
clientID: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
accessTokenExpiresAtTime: number;
accountID: string;
username: string;
}
export const DEFAULT_DROPBOX_CONFIG = { export const DEFAULT_DROPBOX_CONFIG = {
accessToken: "", accessToken: "",
clientID: process.env.DEFAULT_DROPBOX_APP_KEY, clientID: process.env.DEFAULT_DROPBOX_APP_KEY,
@@ -162,32 +150,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";
@@ -487,20 +471,41 @@ export const listFromRemote = async (
throw Error("prefix not supported (yet)"); throw Error("prefix not supported (yet)");
} }
await client.init(); await client.init();
const res = await client.dropbox.filesListFolder({ let res = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`, path: `/${client.vaultName}`,
recursive: true, recursive: true,
include_deleted: false,
limit: 1000,
}); });
if (res.status !== 200) { if (res.status !== 200) {
throw Error(JSON.stringify(res)); throw Error(JSON.stringify(res));
} }
// console.log(res); // console.log(res);
const contents = res.result.entries; const contents = res.result.entries;
const unifiedContents = contents const unifiedContents = contents
.filter((x) => x[".tag"] !== "deleted") .filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`) .filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName)); .map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
while (res.result.has_more) {
res = await client.dropbox.filesListFolderContinue({
cursor: res.result.cursor,
});
if (res.status !== 200) {
throw Error(JSON.stringify(res));
}
const contents2 = res.result.entries;
const unifiedContents2 = contents2
.filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
unifiedContents.push(...unifiedContents2);
}
fixLastModifiedTimeInplace(unifiedContents); fixLastModifiedTimeInplace(unifiedContents);
return { return {
Contents: unifiedContents, Contents: unifiedContents,
}; };
+1 -9
View File
@@ -27,17 +27,9 @@ import {
} from "./misc"; } from "./misc";
import * as mime from "mime-types"; import * as mime from "mime-types";
import { RemoteItem } from "./baseTypes"; import { RemoteItem, S3Config } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
export interface S3Config {
s3Endpoint: string;
s3Region: string;
s3AccessKeyID: string;
s3SecretAccessKey: string;
s3BucketName: string;
}
export const DEFAULT_S3_CONFIG = { export const DEFAULT_S3_CONFIG = {
s3Endpoint: "", s3Endpoint: "",
s3Region: "", s3Region: "",
+2 -11
View File
@@ -4,7 +4,7 @@ import { AuthType, BufferLike, createClient } from "webdav/web";
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web"; import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
export type { WebDAVClient } from "webdav/web"; export type { WebDAVClient } from "webdav/web";
import type { RemoteItem } from "./baseTypes"; import type { RemoteItem, WebdavAuthType, WebdavConfig } from "./baseTypes";
import { import {
arrayBufferToBuffer, arrayBufferToBuffer,
@@ -14,15 +14,6 @@ import {
} from "./misc"; } from "./misc";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
export type WebdavAuthType = "digest" | "basic";
export interface WebdavConfig {
address: string;
username: string;
password: string;
authType: WebdavAuthType;
}
export const DEFAULT_WEBDAV_CONFIG = { export const DEFAULT_WEBDAV_CONFIG = {
address: "", address: "",
username: "", username: "",
@@ -226,7 +217,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);
+5
View File
@@ -32,3 +32,8 @@
.webdav-hide { .webdav-hide {
display: none; display: none;
} }
.qrcode-img {
width: 350px;
height: 350px;
}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"0.1.10": "0.12.15" "0.2.3": "0.12.15"
} }
+2 -1
View File
@@ -43,7 +43,8 @@ module.exports = {
// buffer: require.resolve("buffer/"), // buffer: require.resolve("buffer/"),
// console: require.resolve("console-browserify"), // console: require.resolve("console-browserify"),
// constants: require.resolve("constants-browserify"), // constants: require.resolve("constants-browserify"),
crypto: require.resolve("crypto-browserify"), // crypto: require.resolve("crypto-browserify"),
crypto: false,
// domain: require.resolve("domain-browser"), // domain: require.resolve("domain-browser"),
// events: require.resolve("events"), // events: require.resolve("events"),
// http: require.resolve("stream-http"), // http: require.resolve("stream-http"),