Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6bae792c6 | ||
|
|
8624141780 | ||
|
|
563c2c3ff6 | ||
|
|
9a72ac8d66 | ||
|
|
d980bcbc1b | ||
|
|
6ebe1a8966 | ||
|
|
d0ab1d8abe | ||
|
|
b86d116830 | ||
|
|
7d5eaad36f | ||
|
|
7a13f4566c | ||
|
|
d355f72b7a | ||
|
|
9b1eebdf90 | ||
|
|
2ce461ed6a | ||
|
|
3aa91a2a1e |
@@ -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]
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ This is yet another unofficial sync plugin for 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: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds.
|
- Option #1: You can search, download, and install the plugin in official "community plugin list".
|
||||||
- Option #2: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
|
- Option #2: [](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds.
|
||||||
- Option #3: The pluin would be submitted to the official "community plugin list" in near future.
|
- Option #3: Besides manually downloading the files, you can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### s3
|
### S3
|
||||||
|
|
||||||
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
||||||
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
||||||
@@ -55,6 +68,9 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
|
|||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.2.0",
|
"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
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.2.0",
|
"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",
|
||||||
@@ -25,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",
|
||||||
@@ -57,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",
|
||||||
|
|||||||
@@ -1,9 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* Only type defs here.
|
* Only type defs here.
|
||||||
|
* To avoid circular dependency.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
||||||
|
|
||||||
|
export interface S3Config {
|
||||||
|
s3Endpoint: string;
|
||||||
|
s3Region: string;
|
||||||
|
s3AccessKeyID: string;
|
||||||
|
s3SecretAccessKey: string;
|
||||||
|
s3BucketName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropboxConfig {
|
||||||
|
accessToken: string;
|
||||||
|
clientID: string;
|
||||||
|
refreshToken: string;
|
||||||
|
accessTokenExpiresInSeconds: number;
|
||||||
|
accessTokenExpiresAtTime: number;
|
||||||
|
accountID: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebdavAuthType = "digest" | "basic";
|
||||||
|
|
||||||
|
export interface WebdavConfig {
|
||||||
|
address: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
authType: WebdavAuthType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemotelySavePluginSettings {
|
||||||
|
s3: S3Config;
|
||||||
|
webdav: WebdavConfig;
|
||||||
|
dropbox: DropboxConfig;
|
||||||
|
password: string;
|
||||||
|
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RemoteItem {
|
export interface RemoteItem {
|
||||||
key: string;
|
key: string;
|
||||||
lastModified: number;
|
lastModified: number;
|
||||||
@@ -11,3 +47,13 @@ export interface RemoteItem {
|
|||||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||||
etag?: string;
|
etag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const COMMAND_URI = "remotely-save";
|
||||||
|
export const COMMAND_CALLBACK = "remotely-save-cb";
|
||||||
|
|
||||||
|
export interface UriParams {
|
||||||
|
func?: string;
|
||||||
|
vault?: string;
|
||||||
|
ver?: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import QRCode from "qrcode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
COMMAND_URI,
|
||||||
|
UriParams,
|
||||||
|
RemotelySavePluginSettings,
|
||||||
|
} from "./baseTypes";
|
||||||
|
|
||||||
|
export const exportQrCodeUri = async (
|
||||||
|
settings: RemotelySavePluginSettings,
|
||||||
|
currentVaultName: string,
|
||||||
|
pluginVersion: string
|
||||||
|
) => {
|
||||||
|
const data = encodeURIComponent(JSON.stringify(settings));
|
||||||
|
const vault = encodeURIComponent(currentVaultName);
|
||||||
|
const version = encodeURIComponent(pluginVersion);
|
||||||
|
const rawUri = `obsidian://${COMMAND_URI}?func=settings&version=${version}&vault=${vault}&data=${data}`;
|
||||||
|
// console.log(uri)
|
||||||
|
const imgUri = await QRCode.toDataURL(rawUri);
|
||||||
|
return {
|
||||||
|
rawUri,
|
||||||
|
imgUri,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ProcessQrCodeResultType {
|
||||||
|
status: "error" | "ok";
|
||||||
|
message: string;
|
||||||
|
result?: RemotelySavePluginSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importQrCodeUri = (
|
||||||
|
inputParams: any,
|
||||||
|
currentVaultName: string
|
||||||
|
): ProcessQrCodeResultType => {
|
||||||
|
let params = inputParams as UriParams;
|
||||||
|
if (
|
||||||
|
params.func === undefined ||
|
||||||
|
params.func !== "settings" ||
|
||||||
|
params.vault === undefined ||
|
||||||
|
params.data === undefined
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `the uri is not for exporting/importing settings: ${JSON.stringify(
|
||||||
|
inputParams
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.vault !== currentVaultName) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `the target vault is ${
|
||||||
|
params.vault
|
||||||
|
} but you are currently in ${currentVaultName}: ${JSON.stringify(
|
||||||
|
inputParams
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = {} as RemotelySavePluginSettings;
|
||||||
|
try {
|
||||||
|
settings = JSON.parse(params.data);
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
message: `errors while parsing settings: ${JSON.stringify(inputParams)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
message: "ok",
|
||||||
|
result: settings,
|
||||||
|
};
|
||||||
|
};
|
||||||
+116
-60
@@ -27,10 +27,9 @@ 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,
|
||||||
getAuthUrlAndVerifier,
|
getAuthUrlAndVerifier,
|
||||||
sendAuthReq,
|
sendAuthReq,
|
||||||
@@ -39,16 +38,17 @@ import {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -56,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 {
|
||||||
@@ -86,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(
|
||||||
@@ -380,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;
|
||||||
|
|
||||||
@@ -423,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.",
|
||||||
@@ -585,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",
|
||||||
@@ -706,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.",
|
||||||
@@ -768,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)
|
||||||
@@ -806,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) => {
|
||||||
@@ -838,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
@@ -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>
|
||||||
) {
|
) {
|
||||||
|
|||||||
+23
-12
@@ -3,7 +3,7 @@ import { FileStats, Vault } from "obsidian";
|
|||||||
|
|
||||||
import { Dropbox, DropboxAuth, 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,
|
||||||
@@ -14,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,
|
||||||
@@ -481,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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,17 +27,9 @@ import {
|
|||||||
} from "./misc";
|
} from "./misc";
|
||||||
import * as mime from "mime-types";
|
import * as mime from "mime-types";
|
||||||
|
|
||||||
import { RemoteItem } from "./baseTypes";
|
import { RemoteItem, S3Config } from "./baseTypes";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
|
|
||||||
export interface S3Config {
|
|
||||||
s3Endpoint: string;
|
|
||||||
s3Region: string;
|
|
||||||
s3AccessKeyID: string;
|
|
||||||
s3SecretAccessKey: string;
|
|
||||||
s3BucketName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_S3_CONFIG = {
|
export const DEFAULT_S3_CONFIG = {
|
||||||
s3Endpoint: "",
|
s3Endpoint: "",
|
||||||
s3Region: "",
|
s3Region: "",
|
||||||
@@ -4,7 +4,7 @@ import { AuthType, BufferLike, createClient } from "webdav/web";
|
|||||||
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
||||||
export type { WebDAVClient } from "webdav/web";
|
export type { WebDAVClient } from "webdav/web";
|
||||||
|
|
||||||
import type { RemoteItem } from "./baseTypes";
|
import type { RemoteItem, WebdavAuthType, WebdavConfig } from "./baseTypes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayBufferToBuffer,
|
arrayBufferToBuffer,
|
||||||
@@ -14,15 +14,6 @@ import {
|
|||||||
} from "./misc";
|
} from "./misc";
|
||||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||||
|
|
||||||
export type WebdavAuthType = "digest" | "basic";
|
|
||||||
|
|
||||||
export interface WebdavConfig {
|
|
||||||
address: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
authType: WebdavAuthType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_WEBDAV_CONFIG = {
|
export const DEFAULT_WEBDAV_CONFIG = {
|
||||||
address: "",
|
address: "",
|
||||||
username: "",
|
username: "",
|
||||||
@@ -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);
|
||||||
@@ -32,3 +32,8 @@
|
|||||||
.webdav-hide {
|
.webdav-hide {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qrcode-img {
|
||||||
|
width: 350px;
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"0.2.0": "0.12.15"
|
"0.2.3": "0.12.15"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user