diff --git a/src/encrypt.ts b/src/encrypt.ts index 0a6885d..489ac5a 100644 --- a/src/encrypt.ts +++ b/src/encrypt.ts @@ -1,10 +1,5 @@ import { base32, base64url } from "rfc4648"; -import { - bufferToArrayBuffer, - arrayBufferToBuffer, - hexStringToTypedArray, - arrayBufferToHex, -} from "./misc"; +import { bufferToArrayBuffer, hexStringToTypedArray } from "./misc"; const DEFAULT_ITER = 20000; diff --git a/src/main.ts b/src/main.ts index d99d81e..2a6bd4b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,62 +1,27 @@ +import { Modal, Notice, Plugin, Setting } from "obsidian"; +import type { RemotelySavePluginSettings } from "./baseTypes"; +import { COMMAND_CALLBACK, COMMAND_URI } from "./baseTypes"; +import { importQrCodeUri } from "./importExport"; +import type { InternalDBs } from "./localdb"; import { - App, - Modal, - Notice, - Plugin, - PluginSettingTab, - Setting, - request, - Platform, - TFile, - TFolder, -} from "obsidian"; -import * as CodeMirror from "codemirror"; -import process from "process"; -import { - prepareDBs, - destroyDBs, - loadDeleteRenameHistoryTable, - clearAllSyncPlanRecords, - clearAllSyncMetaMapping, insertDeleteRecord, insertRenameRecord, insertSyncPlanRecord, + loadDeleteRenameHistoryTable, + prepareDBs, } from "./localdb"; -import type { InternalDBs } from "./localdb"; - -import type { SyncStatusType, PasswordCheckType } from "./sync"; -import { isPasswordOk, getSyncPlan, doActualSync } from "./sync"; - +import { RemoteClient } from "./remote"; +import { DEFAULT_DROPBOX_CONFIG } from "./remoteForDropbox"; +import { + AccessCodeResponseSuccessfulType, + DEFAULT_ONEDRIVE_CONFIG, + sendAuthReq as sendAuthReqOnedrive, +} from "./remoteForOnedrive"; import { DEFAULT_S3_CONFIG } from "./remoteForS3"; import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav"; -import { - DEFAULT_DROPBOX_CONFIG, - getAuthUrlAndVerifier as getAuthUrlAndVerifierDropbox, - sendAuthReq as sendAuthReqDropbox, - setConfigBySuccessfullAuthInplace, -} from "./remoteForDropbox"; - -import { RemoteClient } from "./remote"; -import { exportSyncPlansToFiles } from "./debugMode"; -import { COMMAND_URI, COMMAND_CALLBACK } from "./baseTypes"; -import type { - SUPPORTED_SERVICES_TYPE, - S3Config, - DropboxConfig, - WebdavAuthType, - WebdavConfig, - RemotelySavePluginSettings, -} from "./baseTypes"; -import type { ProcessQrCodeResultType } from "./importExport"; -import { exportQrCodeUri, importQrCodeUri } from "./importExport"; - -import { - getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive, - sendAuthReq as sendAuthReqOnedrive, - DEFAULT_ONEDRIVE_CONFIG, - WrappedOnedriveClient, - AccessCodeResponseSuccessfulType, -} from "./remoteForOnedrive"; +import { RemotelySaveSettingTab } from "./settings"; +import type { SyncStatusType } from "./sync"; +import { doActualSync, getSyncPlan, isPasswordOk } from "./sync"; const DEFAULT_SETTINGS: RemotelySavePluginSettings = { s3: DEFAULT_S3_CONFIG, @@ -298,11 +263,6 @@ export default class RemotelySavePlugin extends Plugin { this.addSettingTab(new RemotelySaveSettingTab(this.app, this)); - // this.registerCodeMirror((cm: CodeMirror.Editor) => { - // this.cm = cm; - // console.log("codemirror registered."); - // }); - // this.registerDomEvent(document, "click", (evt: MouseEvent) => { // console.log("click", evt); // }); @@ -340,879 +300,3 @@ export default class RemotelySavePlugin extends Plugin { /* destroyDBs(this.db); */ } } - -export class PasswordModal extends Modal { - plugin: RemotelySavePlugin; - newPassword: string; - constructor(app: App, plugin: RemotelySavePlugin, newPassword: string) { - super(app); - this.plugin = plugin; - this.newPassword = newPassword; - } - - onOpen() { - let { contentEl } = this; - // contentEl.setText("Add Or change password."); - contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." }); - contentEl.createEl("p", { - text: "If the field is not empty, files would be encrypted locally before being uploaded.", - }); - contentEl.createEl("p", { - text: "If the field is empty, then files would be uploaded without encryption.", - }); - - contentEl.createEl("p", { - text: "Attention 1/4: The password itself is stored in PLAIN TEXT LOCALLY.", - cls: "password-disclaimer", - }); - contentEl.createEl("p", { - text: "Attention 2/4: Some metadata are not encrypted or can be easily guessed. (File sizes are closed to their unencrypted ones, and directory path are stored as 0-byte-size object.)", - cls: "password-disclaimer", - }); - contentEl.createEl("p", { - text: "Attention 3/4: You should make sure the remote store IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD, to avoid conflictions.", - }); - contentEl.createEl("p", { - text: "Attention 4/4: The longer the password, the better.", - }); - - new Setting(contentEl) - .addButton((button) => { - button.setButtonText("The Second Confirm to change password."); - button.onClick(async () => { - this.plugin.settings.password = this.newPassword; - await this.plugin.saveSettings(); - new Notice("New password saved!"); - this.close(); - }); - button.setClass("password-second-confirm"); - }) - .addButton((button) => { - button.setButtonText("Go Back"); - button.onClick(() => { - this.close(); - }); - }); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} - -export class DropboxAuthModal extends Modal { - readonly plugin: RemotelySavePlugin; - readonly authDiv: HTMLDivElement; - readonly revokeAuthDiv: HTMLDivElement; - readonly revokeAuthSetting: Setting; - constructor( - app: App, - plugin: RemotelySavePlugin, - authDiv: HTMLDivElement, - revokeAuthDiv: HTMLDivElement, - revokeAuthSetting: Setting - ) { - super(app); - this.plugin = plugin; - this.authDiv = authDiv; - this.revokeAuthDiv = revokeAuthDiv; - this.revokeAuthSetting = revokeAuthSetting; - } - - async onOpen() { - let { contentEl } = this; - - const { authUrl, verifier } = await getAuthUrlAndVerifierDropbox( - this.plugin.settings.dropbox.clientID - ); - - contentEl.createEl("p", { - text: "Step 1: Visit the address in a browser, and follow the steps.", - }); - contentEl.createEl("p").createEl("a", { - href: authUrl, - text: authUrl, - }); - - contentEl.createEl("p", { - text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".', - }); - - let authCode = ""; - new Setting(contentEl) - .setName("Auth Code from web page") - .setDesc('You need to click "Confirm".') - .addText((text) => - text - .setPlaceholder("") - .setValue("") - .onChange((val) => { - authCode = val.trim(); - }) - ) - .addButton(async (button) => { - button.setButtonText("Confirm"); - button.onClick(async () => { - new Notice("Trying to connect to Dropbox"); - try { - const authRes = await sendAuthReqDropbox( - this.plugin.settings.dropbox.clientID, - verifier, - authCode - ); - const self = this; - setConfigBySuccessfullAuthInplace( - this.plugin.settings.dropbox, - authRes, - () => self.plugin.saveSettings() - ); - const client = new RemoteClient( - "dropbox", - undefined, - undefined, - this.plugin.settings.dropbox, - undefined, - this.app.vault.getName(), - () => self.plugin.saveSettings() - ); - const username = await client.getUser(); - this.plugin.settings.dropbox.username = username; - await this.plugin.saveSettings(); - new Notice(`Good! We've connected to Dropbox as user ${username}!`); - this.authDiv.toggleClass( - "dropbox-auth-button-hide", - this.plugin.settings.dropbox.username !== "" - ); - this.revokeAuthDiv.toggleClass( - "dropbox-revoke-auth-button-hide", - this.plugin.settings.dropbox.username === "" - ); - this.revokeAuthSetting.setDesc( - `You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button.` - ); - this.close(); - } catch (err) { - console.error(err); - new Notice("Something goes wrong while connecting to Dropbox."); - } - }); - }); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} - -export class OnedriveAuthModal extends Modal { - readonly plugin: RemotelySavePlugin; - readonly authDiv: HTMLDivElement; - readonly revokeAuthDiv: HTMLDivElement; - readonly revokeAuthSetting: Setting; - constructor( - app: App, - plugin: RemotelySavePlugin, - authDiv: HTMLDivElement, - revokeAuthDiv: HTMLDivElement, - revokeAuthSetting: Setting - ) { - super(app); - this.plugin = plugin; - this.authDiv = authDiv; - this.revokeAuthDiv = revokeAuthDiv; - this.revokeAuthSetting = revokeAuthSetting; - } - - async onOpen() { - let { contentEl } = this; - - const { authUrl, verifier } = await getAuthUrlAndVerifierOnedrive( - this.plugin.settings.onedrive.clientID, - this.plugin.settings.onedrive.authority - ); - this.plugin.oauth2Info.verifier = verifier; - - contentEl.createEl("p", { - text: "Visit the address in a browser, and follow the steps.", - }); - contentEl.createEl("p", { - text: "Finally you should be redirected to Obsidian.", - }); - contentEl.createEl("p").createEl("a", { - href: authUrl, - text: authUrl, - }); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} - -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 - ); - - const div1 = contentEl.createDiv(); - div1.createEl("p", { - text: "You can use another device to scan this qrcode.", - }); - div1.createEl("p", { - text: "Or, you can click the button to copy the special url.", - }); - - const div2 = contentEl.createDiv(); - div2.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!"); - }; - } - ); - - const div3 = contentEl.createDiv(); - div3.createEl( - "img", - { - cls: "qrcode-img", - }, - async (el) => { - el.src = imgUri; - } - ); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} - -class RemotelySaveSettingTab extends PluginSettingTab { - plugin: RemotelySavePlugin; - - constructor(app: App, plugin: RemotelySavePlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - let { containerEl } = this; - - containerEl.empty(); - - containerEl.createEl("h1", { text: "Remotely Save" }); - - const generalDiv = containerEl.createEl("div"); - generalDiv.createEl("h2", { text: "General" }); - - const passwordDiv = generalDiv.createEl("div"); - let newPassword = `${this.plugin.settings.password}`; - new Setting(passwordDiv) - .setName("encryption password") - .setDesc( - 'Password for E2E encryption. Empty for no password. You need to click "Confirm".' - ) - .addText((text) => - text - .setPlaceholder("") - .setValue(`${this.plugin.settings.password}`) - .onChange(async (value) => { - newPassword = value.trim(); - }) - ) - .addButton(async (button) => { - button.setButtonText("Confirm"); - button.onClick(async () => { - new PasswordModal(this.app, this.plugin, newPassword).open(); - }); - }); - - // we need to create the div in advance of any other service divs - const serviceChooserDiv = generalDiv.createEl("div"); - - const s3Div = containerEl.createEl("div", { cls: "s3-hide" }); - s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3"); - s3Div.createEl("h2", { text: "Remote For S3 (-compatible)" }); - - s3Div.createEl("p", { - text: "Disclaimer: This plugin is NOT an official Amazon product.", - cls: "s3-disclaimer", - }); - - s3Div.createEl("p", { - text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.", - cls: "s3-disclaimer", - }); - - s3Div.createEl("p", { - text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", - }); - - s3Div.createEl("p", { - text: "Some Amazon S3 official docs for references:", - }); - - const s3LinksUl = s3Div.createEl("div").createEl("ul"); - - s3LinksUl.createEl("li").createEl("a", { - href: "https://docs.aws.amazon.com/general/latest/gr/s3.html", - text: "Endpoint and region info", - }); - - s3LinksUl.createEl("li").createEl("a", { - href: "https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html", - text: "Access key ID and Secret access key info", - }); - - s3LinksUl.createEl("li").createEl("a", { - href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html", - text: "Configuring CORS", - }); - - new Setting(s3Div) - .setName("s3Endpoint") - .setDesc("s3Endpoint") - .addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.s3.s3Endpoint) - .onChange(async (value) => { - this.plugin.settings.s3.s3Endpoint = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(s3Div) - .setName("s3Region") - .setDesc("s3Region") - .addText((text) => - text - .setPlaceholder("") - .setValue(`${this.plugin.settings.s3.s3Region}`) - .onChange(async (value) => { - this.plugin.settings.s3.s3Region = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(s3Div) - .setName("s3AccessKeyID") - .setDesc("s3AccessKeyID") - .addText((text) => - text - .setPlaceholder("") - .setValue(`${this.plugin.settings.s3.s3AccessKeyID}`) - .onChange(async (value) => { - this.plugin.settings.s3.s3AccessKeyID = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(s3Div) - .setName("s3SecretAccessKey") - .setDesc("s3SecretAccessKey") - .addText((text) => - text - .setPlaceholder("") - .setValue(`${this.plugin.settings.s3.s3SecretAccessKey}`) - .onChange(async (value) => { - this.plugin.settings.s3.s3SecretAccessKey = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(s3Div) - .setName("s3BucketName") - .setDesc("s3BucketName") - .addText((text) => - text - .setPlaceholder("") - .setValue(`${this.plugin.settings.s3.s3BucketName}`) - .onChange(async (value) => { - this.plugin.settings.s3.s3BucketName = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(s3Div) - .setName("check connectivity") - .setDesc("check connectivity") - .addButton(async (button) => { - button.setButtonText("Check"); - button.onClick(async () => { - new Notice("Checking..."); - const client = new RemoteClient("s3", this.plugin.settings.s3); - const res = await client.checkConnectivity(); - if (res) { - new Notice("Great! The bucket can be accessed."); - } else { - new Notice("The S3 bucket cannot be reached."); - } - }); - }); - - const dropboxDiv = containerEl.createEl("div", { cls: "dropbox-hide" }); - dropboxDiv.toggleClass( - "dropbox-hide", - this.plugin.settings.serviceType !== "dropbox" - ); - dropboxDiv.createEl("h2", { text: "Remote For Dropbox" }); - dropboxDiv.createEl("p", { - text: "Disclaimer: This app is NOT an official Dropbox product.", - cls: "dropbox-disclaimer", - }); - dropboxDiv.createEl("p", { - text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .", - cls: "dropbox-disclaimer", - }); - dropboxDiv.createEl("p", { - text: `We will create and sync inside the folder /Apps/${ - this.plugin.manifest.id - }/${this.app.vault.getName()} on your Dropbox.`, - }); - - const dropboxSelectAuthDiv = dropboxDiv.createDiv(); - const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({ - cls: "dropbox-auth-button-hide", - }); - const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({ - cls: "dropbox-revoke-auth-button-hide", - }); - - const dropboxRevokeAuthSetting = new Setting(dropboxRevokeAuthDiv) - .setName("Revoke Auth") - .setDesc( - `You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button` - ) - .addButton(async (button) => { - button.setButtonText("Revoke Auth"); - button.onClick(async () => { - try { - const self = this; - const client = new RemoteClient( - "dropbox", - undefined, - undefined, - this.plugin.settings.dropbox, - undefined, - this.app.vault.getName(), - () => self.plugin.saveSettings() - ); - await client.revokeAuth(); - this.plugin.settings.dropbox = JSON.parse( - JSON.stringify(DEFAULT_DROPBOX_CONFIG) - ); - await this.plugin.saveSettings(); - dropboxAuthDiv.toggleClass( - "dropbox-auth-button-hide", - this.plugin.settings.dropbox.username !== "" - ); - dropboxRevokeAuthDiv.toggleClass( - "dropbox-revoke-auth-button-hide", - this.plugin.settings.dropbox.username === "" - ); - new Notice("Revoked!"); - } catch (err) { - console.error(err); - new Notice("Something goes wrong while revoking"); - } - }); - }); - - new Setting(dropboxAuthDiv) - .setName("Auth") - .setDesc("Auth") - .addButton(async (button) => { - button.setButtonText("Auth"); - button.onClick(async () => { - new DropboxAuthModal( - this.app, - this.plugin, - dropboxAuthDiv, - dropboxRevokeAuthDiv, - dropboxRevokeAuthSetting - ).open(); - }); - }); - - dropboxAuthDiv.toggleClass( - "dropbox-auth-button-hide", - this.plugin.settings.dropbox.username !== "" - ); - dropboxRevokeAuthDiv.toggleClass( - "dropbox-revoke-auth-button-hide", - this.plugin.settings.dropbox.username === "" - ); - - new Setting(dropboxDiv) - .setName("check connectivity") - .setDesc("check connectivity") - .addButton(async (button) => { - button.setButtonText("Check"); - button.onClick(async () => { - new Notice("Checking..."); - const self = this; - const client = new RemoteClient( - "dropbox", - undefined, - undefined, - this.plugin.settings.dropbox, - undefined, - this.app.vault.getName(), - () => self.plugin.saveSettings() - ); - - const res = await client.checkConnectivity(); - if (res) { - new Notice("Great! We can connect to Dropbox!"); - } else { - new Notice("We cannot connect to Dropbox."); - } - }); - }); - - const onedriveDiv = containerEl.createEl("div", { cls: "onedrive-hide" }); - onedriveDiv.toggleClass( - "onedrive-hide", - this.plugin.settings.serviceType !== "onedrive" - ); - onedriveDiv.createEl("h2", { text: "Remote For Onedrive" }); - onedriveDiv.createEl("p", { - text: "Disclaimer: This app is NOT an official Onedrive product.", - cls: "onedrive-disclaimer", - }); - onedriveDiv.createEl("p", { - text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .", - cls: "onedrive-disclaimer", - }); - onedriveDiv.createEl("p", { - text: `We will create and sync inside the folder /Apps/${ - this.plugin.manifest.id - }/${this.app.vault.getName()} on your Onedrive.`, - }); - - const onedriveSelectAuthDiv = onedriveDiv.createDiv(); - const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({ - cls: "onedrive-auth-button-hide", - }); - const onedriveRevokeAuthDiv = onedriveSelectAuthDiv.createDiv({ - cls: "onedrive-revoke-auth-button-hide", - }); - - const onedriveRevokeAuthSetting = new Setting(onedriveRevokeAuthDiv) - .setName("Revoke Auth") - .setDesc( - `You've connected as user ${this.plugin.settings.onedrive.username}. If you want to disconnect, click this button` - ) - .addButton(async (button) => { - button.setButtonText("Revoke Auth"); - button.onClick(async () => { - try { - this.plugin.settings.onedrive = JSON.parse( - JSON.stringify(DEFAULT_ONEDRIVE_CONFIG) - ); - await this.plugin.saveSettings(); - onedriveAuthDiv.toggleClass( - "onedrive-auth-button-hide", - this.plugin.settings.onedrive.username !== "" - ); - onedriveRevokeAuthDiv.toggleClass( - "onedrive-revoke-auth-button-hide", - this.plugin.settings.onedrive.username === "" - ); - new Notice("Revoked!"); - } catch (err) { - console.error(err); - new Notice("Something goes wrong while revoking"); - } - }); - }); - - new Setting(onedriveAuthDiv) - .setName("Auth") - .setDesc("Auth") - .addButton(async (button) => { - button.setButtonText("Auth"); - button.onClick(async () => { - const modal = new OnedriveAuthModal( - this.app, - this.plugin, - onedriveAuthDiv, - onedriveRevokeAuthDiv, - onedriveRevokeAuthSetting - ); - this.plugin.oauth2Info.helperModal = modal; - this.plugin.oauth2Info.authDiv = onedriveAuthDiv; - this.plugin.oauth2Info.revokeDiv = onedriveRevokeAuthDiv; - this.plugin.oauth2Info.revokeAuthSetting = onedriveRevokeAuthSetting; - modal.open(); - }); - }); - - onedriveAuthDiv.toggleClass( - "onedrive-auth-button-hide", - this.plugin.settings.onedrive.username !== "" - ); - onedriveRevokeAuthDiv.toggleClass( - "onedrive-revoke-auth-button-hide", - this.plugin.settings.onedrive.username === "" - ); - - new Setting(onedriveDiv) - .setName("check connectivity") - .setDesc("check connectivity") - .addButton(async (button) => { - button.setButtonText("Check"); - button.onClick(async () => { - new Notice("Checking..."); - const self = this; - const client = new RemoteClient( - "onedrive", - undefined, - undefined, - undefined, - this.plugin.settings.onedrive, - this.app.vault.getName(), - () => self.plugin.saveSettings() - ); - - const res = await client.checkConnectivity(); - if (res) { - new Notice("Great! We can connect to Onedrive!"); - } else { - new Notice("We cannot connect to Onedrive."); - } - }); - }); - - const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" }); - webdavDiv.toggleClass( - "webdav-hide", - this.plugin.settings.serviceType !== "webdav" - ); - - webdavDiv.createEl("h2", { text: "Remote For Webdav" }); - - webdavDiv.createEl("p", { - text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.", - cls: "webdav-disclaimer", - }); - - webdavDiv.createEl("p", { - text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", - }); - - webdavDiv.createEl("p", { - text: `We will create and sync inside the folder /${this.app.vault.getName()} on your server.`, - }); - - new Setting(webdavDiv) - .setName("server address") - .setDesc("server address") - .addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.webdav.address) - .onChange(async (value) => { - this.plugin.settings.webdav.address = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(webdavDiv) - .setName("server username") - .setDesc("server username") - .addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.webdav.username) - .onChange(async (value) => { - this.plugin.settings.webdav.username = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(webdavDiv) - .setName("server password") - .setDesc("server password") - .addText((text) => - text - .setPlaceholder("") - .setValue(this.plugin.settings.webdav.password) - .onChange(async (value) => { - this.plugin.settings.webdav.password = value.trim(); - await this.plugin.saveSettings(); - }) - ); - - new Setting(webdavDiv) - .setName("server auth type") - .setDesc("If no password, this option would be ignored.") - .addDropdown((dropdown) => { - dropdown.addOption("basic", "basic"); - // dropdown.addOption("digest", "digest"); - - dropdown - .setValue(this.plugin.settings.webdav.authType) - .onChange(async (val: WebdavAuthType) => { - this.plugin.settings.webdav.authType = val; - await this.plugin.saveSettings(); - }); - }); - - new Setting(webdavDiv) - .setName("check connectivity") - .setDesc("check connectivity") - .addButton(async (button) => { - button.setButtonText("Check"); - button.onClick(async () => { - new Notice("Checking..."); - const client = new RemoteClient( - "webdav", - undefined, - this.plugin.settings.webdav, - undefined, - undefined, - this.app.vault.getName() - ); - const res = await client.checkConnectivity(); - if (res) { - new Notice("Great! The webdav server can be accessed."); - } else { - new Notice("The webdav server cannot be reached."); - } - }); - }); - - // we need to create chooser - // after s3Div and webdavDiv being created - new Setting(serviceChooserDiv) - .setName("Choose service") - .setDesc("Choose a service.") - .addDropdown(async (dropdown) => { - dropdown.addOption("s3", "S3 (-compatible)"); - dropdown.addOption("dropbox", "Dropbox"); - dropdown.addOption("webdav", "Webdav"); - dropdown.addOption("onedrive", "OneDrive (alpha)"); - dropdown - .setValue(this.plugin.settings.serviceType) - .onChange(async (val: SUPPORTED_SERVICES_TYPE) => { - this.plugin.settings.serviceType = val; - s3Div.toggleClass( - "s3-hide", - this.plugin.settings.serviceType !== "s3" - ); - dropboxDiv.toggleClass( - "dropbox-hide", - this.plugin.settings.serviceType !== "dropbox" - ); - onedriveDiv.toggleClass( - "onedrive-hide", - this.plugin.settings.serviceType !== "onedrive" - ); - webdavDiv.toggleClass( - "webdav-hide", - this.plugin.settings.serviceType !== "webdav" - ); - await this.plugin.saveSettings(); - }); - }); - - // 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 a 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 open a camera or scan-qrcode app, to manually scan the QR code." - ); - - const debugDiv = containerEl.createEl("div"); - debugDiv.createEl("h2", { text: "Debug" }); - const syncPlanDiv = debugDiv.createEl("div"); - new Setting(syncPlanDiv) - .setName("export sync plans") - .setDesc( - "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans" - ) - .addButton(async (button) => { - button.setButtonText("Export"); - button.onClick(async () => { - await exportSyncPlansToFiles(this.plugin.db, this.app.vault); - new Notice("sync plans history exported"); - }); - }); - new Setting(syncPlanDiv) - .setName("delete sync plans history in db") - .setDesc("delete sync plans history in db") - .addButton(async (button) => { - button.setButtonText("Delete History"); - button.onClick(async () => { - await clearAllSyncPlanRecords(this.plugin.db); - new Notice("sync plans history (in db) deleted"); - }); - }); - - const syncMappingDiv = debugDiv.createEl("div"); - new Setting(syncMappingDiv) - .setName("delete sync mappings history in db") - .setDesc( - "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync. Click the button to delete sync mappings history in db" - ) - .addButton(async (button) => { - button.setButtonText("Delete Sync Mappings"); - button.onClick(async () => { - await clearAllSyncMetaMapping(this.plugin.db); - new Notice("sync mappings history (in local db) deleted"); - }); - }); - - const dbsResetDiv = debugDiv.createEl("div"); - new Setting(dbsResetDiv) - .setName("reset local internal cache/databases") - .setDesc( - "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings." - ) - .addButton(async (button) => { - button.setButtonText("Reset"); - button.onClick(async () => { - await destroyDBs(); - new Notice( - "Local internal cache/databases deleted. Please manually reload the plugin." - ); - }); - }); - } -} diff --git a/src/remote.ts b/src/remote.ts index 63c5580..5a1116b 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -1,16 +1,15 @@ import { Vault } from "obsidian"; - import type { - SUPPORTED_SERVICES_TYPE, - S3Config, DropboxConfig, - WebdavConfig, OnedriveConfig, + S3Config, + SUPPORTED_SERVICES_TYPE, + WebdavConfig, } from "./baseTypes"; -import * as s3 from "./remoteForS3"; -import * as webdav from "./remoteForWebdav"; import * as dropbox from "./remoteForDropbox"; import * as onedrive from "./remoteForOnedrive"; +import * as s3 from "./remoteForS3"; +import * as webdav from "./remoteForWebdav"; export class RemoteClient { readonly serviceType: SUPPORTED_SERVICES_TYPE; diff --git a/src/remoteForDropbox.ts b/src/remoteForDropbox.ts index 9ba6129..18c1f53 100644 --- a/src/remoteForDropbox.ts +++ b/src/remoteForDropbox.ts @@ -1,18 +1,11 @@ +import { Dropbox, DropboxAuth, files } from "dropbox"; +import { Vault } from "obsidian"; import * as path from "path"; -import { FileStats, Vault } from "obsidian"; - -import { Dropbox, DropboxAuth, DropboxResponse, files } from "dropbox"; -export { Dropbox } from "dropbox"; -import { RemoteItem, DropboxConfig } from "./baseTypes"; -import { - arrayBufferToBuffer, - bufferToArrayBuffer, - mkdirpInVault, - getPathFolder, - getFolderLevels, - setToString, -} from "./misc"; +import { DropboxConfig, RemoteItem } from "./baseTypes"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; +import { bufferToArrayBuffer, getFolderLevels, mkdirpInVault } from "./misc"; + +export { Dropbox } from "dropbox"; export const DEFAULT_DROPBOX_CONFIG = { accessToken: "", diff --git a/src/remoteForOnedrive.ts b/src/remoteForOnedrive.ts index 09068d4..5776249 100644 --- a/src/remoteForOnedrive.ts +++ b/src/remoteForOnedrive.ts @@ -1,30 +1,26 @@ -import * as path from "path"; -import { request, Vault } from "obsidian"; -import { PublicClientApplication, CryptoProvider } from "@azure/msal-node"; -import { COMMAND_CALLBACK_ONEDRIVE } from "./baseTypes"; -import type { OnedriveConfig, RemoteItem } from "./baseTypes"; - +import { CryptoProvider, PublicClientApplication } from "@azure/msal-node"; import { + AuthenticationProvider, Client, FileUpload, - UploadEventHandlers, - AuthenticationProvider, - AuthenticationProviderOptions, - Range, LargeFileUploadSession, LargeFileUploadTask, LargeFileUploadTaskOptions, + Range, + UploadEventHandlers, UploadResult, } from "@microsoft/microsoft-graph-client"; -import type { Drive, DriveItem, User } from "@microsoft/microsoft-graph-types"; +import type { DriveItem, User } from "@microsoft/microsoft-graph-types"; +import { request, Vault } from "obsidian"; +import * as path from "path"; +import type { OnedriveConfig, RemoteItem } from "./baseTypes"; +import { COMMAND_CALLBACK_ONEDRIVE } from "./baseTypes"; +import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { - getFolderLevels, - getPathFolder, getRandomArrayBuffer, getRandomIntInclusive, mkdirpInVault, } from "./misc"; -import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"]; const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`; diff --git a/src/remoteForS3.ts b/src/remoteForS3.ts index a9b8914..21088dc 100644 --- a/src/remoteForS3.ts +++ b/src/remoteForS3.ts @@ -1,34 +1,29 @@ -import { Buffer } from "buffer"; -import { Readable } from "stream"; - -import { Vault } from "obsidian"; - -import { Upload } from "@aws-sdk/lib-storage"; -import { - S3Client, - ListObjectsV2Command, - PutObjectCommand, - GetObjectCommand, - DeleteObjectCommand, - HeadObjectCommand, - HeadBucketCommand, - ListObjectsV2CommandInput, - ListObjectsV2CommandOutput, - HeadObjectCommandOutput, -} from "@aws-sdk/client-s3"; -export { S3Client } from "@aws-sdk/client-s3"; - import type { _Object } from "@aws-sdk/client-s3"; - +import { + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + HeadObjectCommand, + HeadObjectCommandOutput, + ListObjectsV2Command, + ListObjectsV2CommandInput, + PutObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; +import { Upload } from "@aws-sdk/lib-storage"; +import { Buffer } from "buffer"; +import * as mime from "mime-types"; +import { Vault } from "obsidian"; +import { Readable } from "stream"; +import { RemoteItem, S3Config } from "./baseTypes"; +import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; import { arrayBufferToBuffer, bufferToArrayBuffer, mkdirpInVault, } from "./misc"; -import * as mime from "mime-types"; -import { RemoteItem, S3Config } from "./baseTypes"; -import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; +export { S3Client } from "@aws-sdk/client-s3"; export const DEFAULT_S3_CONFIG = { s3Endpoint: "", diff --git a/src/remoteForWebdav.ts b/src/remoteForWebdav.ts index d8f40fb..9248b3d 100644 --- a/src/remoteForWebdav.ts +++ b/src/remoteForWebdav.ts @@ -1,18 +1,11 @@ import { Buffer } from "buffer"; -import { FileStats, Vault } from "obsidian"; +import { Vault } from "obsidian"; +import type { FileStat, WebDAVClient } from "webdav/web"; import { AuthType, BufferLike, createClient } from "webdav/web"; -import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web"; -export type { WebDAVClient } from "webdav/web"; - -import type { RemoteItem, WebdavAuthType, WebdavConfig } from "./baseTypes"; - -import { - arrayBufferToBuffer, - bufferToArrayBuffer, - mkdirpInVault, - getPathFolder, -} from "./misc"; +import type { RemoteItem, WebdavConfig } from "./baseTypes"; import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt"; +import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc"; +export type { WebDAVClient } from "webdav/web"; export const DEFAULT_WEBDAV_CONFIG = { address: "", diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..dd75d56 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,897 @@ +import { App, Modal, Notice, PluginSettingTab, Setting } from "obsidian"; +import type { SUPPORTED_SERVICES_TYPE, WebdavAuthType } from "./baseTypes"; +import { exportSyncPlansToFiles } from "./debugMode"; +import { exportQrCodeUri } from "./importExport"; +import { + clearAllSyncMetaMapping, + clearAllSyncPlanRecords, + destroyDBs, +} from "./localdb"; +import type RemotelySavePlugin from "./main"; // unavoidable +import { RemoteClient } from "./remote"; +import { + DEFAULT_DROPBOX_CONFIG, + getAuthUrlAndVerifier as getAuthUrlAndVerifierDropbox, + sendAuthReq as sendAuthReqDropbox, + setConfigBySuccessfullAuthInplace, +} from "./remoteForDropbox"; +import { + DEFAULT_ONEDRIVE_CONFIG, + getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive, +} from "./remoteForOnedrive"; + +class PasswordModal extends Modal { + plugin: RemotelySavePlugin; + newPassword: string; + constructor(app: App, plugin: RemotelySavePlugin, newPassword: string) { + super(app); + this.plugin = plugin; + this.newPassword = newPassword; + } + + onOpen() { + let { contentEl } = this; + // contentEl.setText("Add Or change password."); + contentEl.createEl("h2", { text: "Hold on and PLEASE READ ON..." }); + contentEl.createEl("p", { + text: "If the field is not empty, files would be encrypted locally before being uploaded.", + }); + contentEl.createEl("p", { + text: "If the field is empty, then files would be uploaded without encryption.", + }); + + contentEl.createEl("p", { + text: "Attention 1/4: The password itself is stored in PLAIN TEXT LOCALLY.", + cls: "password-disclaimer", + }); + contentEl.createEl("p", { + text: "Attention 2/4: Some metadata are not encrypted or can be easily guessed. (File sizes are closed to their unencrypted ones, and directory path are stored as 0-byte-size object.)", + cls: "password-disclaimer", + }); + contentEl.createEl("p", { + text: "Attention 3/4: You should make sure the remote store IS EMPTY, or REMOTE FILES WERE ENCRYPTED BY THAT NEW PASSWORD, to avoid conflictions.", + }); + contentEl.createEl("p", { + text: "Attention 4/4: The longer the password, the better.", + }); + + new Setting(contentEl) + .addButton((button) => { + button.setButtonText("The Second Confirm to change password."); + button.onClick(async () => { + this.plugin.settings.password = this.newPassword; + await this.plugin.saveSettings(); + new Notice("New password saved!"); + this.close(); + }); + button.setClass("password-second-confirm"); + }) + .addButton((button) => { + button.setButtonText("Go Back"); + button.onClick(() => { + this.close(); + }); + }); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} + +class DropboxAuthModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly authDiv: HTMLDivElement; + readonly revokeAuthDiv: HTMLDivElement; + readonly revokeAuthSetting: Setting; + constructor( + app: App, + plugin: RemotelySavePlugin, + authDiv: HTMLDivElement, + revokeAuthDiv: HTMLDivElement, + revokeAuthSetting: Setting + ) { + super(app); + this.plugin = plugin; + this.authDiv = authDiv; + this.revokeAuthDiv = revokeAuthDiv; + this.revokeAuthSetting = revokeAuthSetting; + } + + async onOpen() { + let { contentEl } = this; + + const { authUrl, verifier } = await getAuthUrlAndVerifierDropbox( + this.plugin.settings.dropbox.clientID + ); + + contentEl.createEl("p", { + text: "Step 1: Visit the address in a browser, and follow the steps.", + }); + contentEl.createEl("p").createEl("a", { + href: authUrl, + text: authUrl, + }); + + contentEl.createEl("p", { + text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".', + }); + + let authCode = ""; + new Setting(contentEl) + .setName("Auth Code from web page") + .setDesc('You need to click "Confirm".') + .addText((text) => + text + .setPlaceholder("") + .setValue("") + .onChange((val) => { + authCode = val.trim(); + }) + ) + .addButton(async (button) => { + button.setButtonText("Confirm"); + button.onClick(async () => { + new Notice("Trying to connect to Dropbox"); + try { + const authRes = await sendAuthReqDropbox( + this.plugin.settings.dropbox.clientID, + verifier, + authCode + ); + const self = this; + setConfigBySuccessfullAuthInplace( + this.plugin.settings.dropbox, + authRes, + () => self.plugin.saveSettings() + ); + const client = new RemoteClient( + "dropbox", + undefined, + undefined, + this.plugin.settings.dropbox, + undefined, + this.app.vault.getName(), + () => self.plugin.saveSettings() + ); + const username = await client.getUser(); + this.plugin.settings.dropbox.username = username; + await this.plugin.saveSettings(); + new Notice(`Good! We've connected to Dropbox as user ${username}!`); + this.authDiv.toggleClass( + "dropbox-auth-button-hide", + this.plugin.settings.dropbox.username !== "" + ); + this.revokeAuthDiv.toggleClass( + "dropbox-revoke-auth-button-hide", + this.plugin.settings.dropbox.username === "" + ); + this.revokeAuthSetting.setDesc( + `You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button.` + ); + this.close(); + } catch (err) { + console.error(err); + new Notice("Something goes wrong while connecting to Dropbox."); + } + }); + }); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} + +export class OnedriveAuthModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly authDiv: HTMLDivElement; + readonly revokeAuthDiv: HTMLDivElement; + readonly revokeAuthSetting: Setting; + constructor( + app: App, + plugin: RemotelySavePlugin, + authDiv: HTMLDivElement, + revokeAuthDiv: HTMLDivElement, + revokeAuthSetting: Setting + ) { + super(app); + this.plugin = plugin; + this.authDiv = authDiv; + this.revokeAuthDiv = revokeAuthDiv; + this.revokeAuthSetting = revokeAuthSetting; + } + + async onOpen() { + let { contentEl } = this; + + const { authUrl, verifier } = await getAuthUrlAndVerifierOnedrive( + this.plugin.settings.onedrive.clientID, + this.plugin.settings.onedrive.authority + ); + this.plugin.oauth2Info.verifier = verifier; + + contentEl.createEl("p", { + text: "Visit the address in a browser, and follow the steps.", + }); + contentEl.createEl("p", { + text: "Finally you should be redirected to Obsidian.", + }); + contentEl.createEl("p").createEl("a", { + href: authUrl, + text: authUrl, + }); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} + +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 + ); + + const div1 = contentEl.createDiv(); + div1.createEl("p", { + text: "You can use another device to scan this qrcode.", + }); + div1.createEl("p", { + text: "Or, you can click the button to copy the special url.", + }); + + const div2 = contentEl.createDiv(); + div2.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!"); + }; + } + ); + + const div3 = contentEl.createDiv(); + div3.createEl( + "img", + { + cls: "qrcode-img", + }, + async (el) => { + el.src = imgUri; + } + ); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} + +export class RemotelySaveSettingTab extends PluginSettingTab { + plugin: RemotelySavePlugin; + + constructor(app: App, plugin: RemotelySavePlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + let { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl("h1", { text: "Remotely Save" }); + + const generalDiv = containerEl.createEl("div"); + generalDiv.createEl("h2", { text: "General" }); + + const passwordDiv = generalDiv.createEl("div"); + let newPassword = `${this.plugin.settings.password}`; + new Setting(passwordDiv) + .setName("encryption password") + .setDesc( + 'Password for E2E encryption. Empty for no password. You need to click "Confirm".' + ) + .addText((text) => + text + .setPlaceholder("") + .setValue(`${this.plugin.settings.password}`) + .onChange(async (value) => { + newPassword = value.trim(); + }) + ) + .addButton(async (button) => { + button.setButtonText("Confirm"); + button.onClick(async () => { + new PasswordModal(this.app, this.plugin, newPassword).open(); + }); + }); + + // we need to create the div in advance of any other service divs + const serviceChooserDiv = generalDiv.createEl("div"); + + const s3Div = containerEl.createEl("div", { cls: "s3-hide" }); + s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3"); + s3Div.createEl("h2", { text: "Remote For S3 (-compatible)" }); + + s3Div.createEl("p", { + text: "Disclaimer: This plugin is NOT an official Amazon product.", + cls: "s3-disclaimer", + }); + + s3Div.createEl("p", { + text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your bucket, please immediately delete the access key on your AWS (or other S3-service provider) settings.", + cls: "s3-disclaimer", + }); + + s3Div.createEl("p", { + text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", + }); + + s3Div.createEl("p", { + text: "Some Amazon S3 official docs for references:", + }); + + const s3LinksUl = s3Div.createEl("div").createEl("ul"); + + s3LinksUl.createEl("li").createEl("a", { + href: "https://docs.aws.amazon.com/general/latest/gr/s3.html", + text: "Endpoint and region info", + }); + + s3LinksUl.createEl("li").createEl("a", { + href: "https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html", + text: "Access key ID and Secret access key info", + }); + + s3LinksUl.createEl("li").createEl("a", { + href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html", + text: "Configuring CORS", + }); + + new Setting(s3Div) + .setName("s3Endpoint") + .setDesc("s3Endpoint") + .addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.s3.s3Endpoint) + .onChange(async (value) => { + this.plugin.settings.s3.s3Endpoint = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(s3Div) + .setName("s3Region") + .setDesc("s3Region") + .addText((text) => + text + .setPlaceholder("") + .setValue(`${this.plugin.settings.s3.s3Region}`) + .onChange(async (value) => { + this.plugin.settings.s3.s3Region = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(s3Div) + .setName("s3AccessKeyID") + .setDesc("s3AccessKeyID") + .addText((text) => + text + .setPlaceholder("") + .setValue(`${this.plugin.settings.s3.s3AccessKeyID}`) + .onChange(async (value) => { + this.plugin.settings.s3.s3AccessKeyID = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(s3Div) + .setName("s3SecretAccessKey") + .setDesc("s3SecretAccessKey") + .addText((text) => + text + .setPlaceholder("") + .setValue(`${this.plugin.settings.s3.s3SecretAccessKey}`) + .onChange(async (value) => { + this.plugin.settings.s3.s3SecretAccessKey = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(s3Div) + .setName("s3BucketName") + .setDesc("s3BucketName") + .addText((text) => + text + .setPlaceholder("") + .setValue(`${this.plugin.settings.s3.s3BucketName}`) + .onChange(async (value) => { + this.plugin.settings.s3.s3BucketName = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(s3Div) + .setName("check connectivity") + .setDesc("check connectivity") + .addButton(async (button) => { + button.setButtonText("Check"); + button.onClick(async () => { + new Notice("Checking..."); + const client = new RemoteClient("s3", this.plugin.settings.s3); + const res = await client.checkConnectivity(); + if (res) { + new Notice("Great! The bucket can be accessed."); + } else { + new Notice("The S3 bucket cannot be reached."); + } + }); + }); + + const dropboxDiv = containerEl.createEl("div", { cls: "dropbox-hide" }); + dropboxDiv.toggleClass( + "dropbox-hide", + this.plugin.settings.serviceType !== "dropbox" + ); + dropboxDiv.createEl("h2", { text: "Remote For Dropbox" }); + dropboxDiv.createEl("p", { + text: "Disclaimer: This app is NOT an official Dropbox product.", + cls: "dropbox-disclaimer", + }); + dropboxDiv.createEl("p", { + text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Dropbox, please immediately disconnect this app on https://www.dropbox.com/account/connected_apps .", + cls: "dropbox-disclaimer", + }); + dropboxDiv.createEl("p", { + text: `We will create and sync inside the folder /Apps/${ + this.plugin.manifest.id + }/${this.app.vault.getName()} on your Dropbox.`, + }); + + const dropboxSelectAuthDiv = dropboxDiv.createDiv(); + const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({ + cls: "dropbox-auth-button-hide", + }); + const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({ + cls: "dropbox-revoke-auth-button-hide", + }); + + const dropboxRevokeAuthSetting = new Setting(dropboxRevokeAuthDiv) + .setName("Revoke Auth") + .setDesc( + `You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button` + ) + .addButton(async (button) => { + button.setButtonText("Revoke Auth"); + button.onClick(async () => { + try { + const self = this; + const client = new RemoteClient( + "dropbox", + undefined, + undefined, + this.plugin.settings.dropbox, + undefined, + this.app.vault.getName(), + () => self.plugin.saveSettings() + ); + await client.revokeAuth(); + this.plugin.settings.dropbox = JSON.parse( + JSON.stringify(DEFAULT_DROPBOX_CONFIG) + ); + await this.plugin.saveSettings(); + dropboxAuthDiv.toggleClass( + "dropbox-auth-button-hide", + this.plugin.settings.dropbox.username !== "" + ); + dropboxRevokeAuthDiv.toggleClass( + "dropbox-revoke-auth-button-hide", + this.plugin.settings.dropbox.username === "" + ); + new Notice("Revoked!"); + } catch (err) { + console.error(err); + new Notice("Something goes wrong while revoking"); + } + }); + }); + + new Setting(dropboxAuthDiv) + .setName("Auth") + .setDesc("Auth") + .addButton(async (button) => { + button.setButtonText("Auth"); + button.onClick(async () => { + new DropboxAuthModal( + this.app, + this.plugin, + dropboxAuthDiv, + dropboxRevokeAuthDiv, + dropboxRevokeAuthSetting + ).open(); + }); + }); + + dropboxAuthDiv.toggleClass( + "dropbox-auth-button-hide", + this.plugin.settings.dropbox.username !== "" + ); + dropboxRevokeAuthDiv.toggleClass( + "dropbox-revoke-auth-button-hide", + this.plugin.settings.dropbox.username === "" + ); + + new Setting(dropboxDiv) + .setName("check connectivity") + .setDesc("check connectivity") + .addButton(async (button) => { + button.setButtonText("Check"); + button.onClick(async () => { + new Notice("Checking..."); + const self = this; + const client = new RemoteClient( + "dropbox", + undefined, + undefined, + this.plugin.settings.dropbox, + undefined, + this.app.vault.getName(), + () => self.plugin.saveSettings() + ); + + const res = await client.checkConnectivity(); + if (res) { + new Notice("Great! We can connect to Dropbox!"); + } else { + new Notice("We cannot connect to Dropbox."); + } + }); + }); + + const onedriveDiv = containerEl.createEl("div", { cls: "onedrive-hide" }); + onedriveDiv.toggleClass( + "onedrive-hide", + this.plugin.settings.serviceType !== "onedrive" + ); + onedriveDiv.createEl("h2", { text: "Remote For Onedrive" }); + onedriveDiv.createEl("p", { + text: "Disclaimer: This app is NOT an official Onedrive product.", + cls: "onedrive-disclaimer", + }); + onedriveDiv.createEl("p", { + text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .", + cls: "onedrive-disclaimer", + }); + onedriveDiv.createEl("p", { + text: `We will create and sync inside the folder /Apps/${ + this.plugin.manifest.id + }/${this.app.vault.getName()} on your Onedrive.`, + }); + + const onedriveSelectAuthDiv = onedriveDiv.createDiv(); + const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({ + cls: "onedrive-auth-button-hide", + }); + const onedriveRevokeAuthDiv = onedriveSelectAuthDiv.createDiv({ + cls: "onedrive-revoke-auth-button-hide", + }); + + const onedriveRevokeAuthSetting = new Setting(onedriveRevokeAuthDiv) + .setName("Revoke Auth") + .setDesc( + `You've connected as user ${this.plugin.settings.onedrive.username}. If you want to disconnect, click this button` + ) + .addButton(async (button) => { + button.setButtonText("Revoke Auth"); + button.onClick(async () => { + try { + this.plugin.settings.onedrive = JSON.parse( + JSON.stringify(DEFAULT_ONEDRIVE_CONFIG) + ); + await this.plugin.saveSettings(); + onedriveAuthDiv.toggleClass( + "onedrive-auth-button-hide", + this.plugin.settings.onedrive.username !== "" + ); + onedriveRevokeAuthDiv.toggleClass( + "onedrive-revoke-auth-button-hide", + this.plugin.settings.onedrive.username === "" + ); + new Notice("Revoked!"); + } catch (err) { + console.error(err); + new Notice("Something goes wrong while revoking"); + } + }); + }); + + new Setting(onedriveAuthDiv) + .setName("Auth") + .setDesc("Auth") + .addButton(async (button) => { + button.setButtonText("Auth"); + button.onClick(async () => { + const modal = new OnedriveAuthModal( + this.app, + this.plugin, + onedriveAuthDiv, + onedriveRevokeAuthDiv, + onedriveRevokeAuthSetting + ); + this.plugin.oauth2Info.helperModal = modal; + this.plugin.oauth2Info.authDiv = onedriveAuthDiv; + this.plugin.oauth2Info.revokeDiv = onedriveRevokeAuthDiv; + this.plugin.oauth2Info.revokeAuthSetting = onedriveRevokeAuthSetting; + modal.open(); + }); + }); + + onedriveAuthDiv.toggleClass( + "onedrive-auth-button-hide", + this.plugin.settings.onedrive.username !== "" + ); + onedriveRevokeAuthDiv.toggleClass( + "onedrive-revoke-auth-button-hide", + this.plugin.settings.onedrive.username === "" + ); + + new Setting(onedriveDiv) + .setName("check connectivity") + .setDesc("check connectivity") + .addButton(async (button) => { + button.setButtonText("Check"); + button.onClick(async () => { + new Notice("Checking..."); + const self = this; + const client = new RemoteClient( + "onedrive", + undefined, + undefined, + undefined, + this.plugin.settings.onedrive, + this.app.vault.getName(), + () => self.plugin.saveSettings() + ); + + const res = await client.checkConnectivity(); + if (res) { + new Notice("Great! We can connect to Onedrive!"); + } else { + new Notice("We cannot connect to Onedrive."); + } + }); + }); + + const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" }); + webdavDiv.toggleClass( + "webdav-hide", + this.plugin.settings.serviceType !== "webdav" + ); + + webdavDiv.createEl("h2", { text: "Remote For Webdav" }); + + webdavDiv.createEl("p", { + text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may read the info. If you see any unintentional access to your webdav server, please immediately change the username and password.", + cls: "webdav-disclaimer", + }); + + webdavDiv.createEl("p", { + text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost", + }); + + webdavDiv.createEl("p", { + text: `We will create and sync inside the folder /${this.app.vault.getName()} on your server.`, + }); + + new Setting(webdavDiv) + .setName("server address") + .setDesc("server address") + .addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.webdav.address) + .onChange(async (value) => { + this.plugin.settings.webdav.address = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(webdavDiv) + .setName("server username") + .setDesc("server username") + .addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.webdav.username) + .onChange(async (value) => { + this.plugin.settings.webdav.username = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(webdavDiv) + .setName("server password") + .setDesc("server password") + .addText((text) => + text + .setPlaceholder("") + .setValue(this.plugin.settings.webdav.password) + .onChange(async (value) => { + this.plugin.settings.webdav.password = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(webdavDiv) + .setName("server auth type") + .setDesc("If no password, this option would be ignored.") + .addDropdown((dropdown) => { + dropdown.addOption("basic", "basic"); + // dropdown.addOption("digest", "digest"); + + dropdown + .setValue(this.plugin.settings.webdav.authType) + .onChange(async (val: WebdavAuthType) => { + this.plugin.settings.webdav.authType = val; + await this.plugin.saveSettings(); + }); + }); + + new Setting(webdavDiv) + .setName("check connectivity") + .setDesc("check connectivity") + .addButton(async (button) => { + button.setButtonText("Check"); + button.onClick(async () => { + new Notice("Checking..."); + const client = new RemoteClient( + "webdav", + undefined, + this.plugin.settings.webdav, + undefined, + undefined, + this.app.vault.getName() + ); + const res = await client.checkConnectivity(); + if (res) { + new Notice("Great! The webdav server can be accessed."); + } else { + new Notice("The webdav server cannot be reached."); + } + }); + }); + + // we need to create chooser + // after s3Div and webdavDiv being created + new Setting(serviceChooserDiv) + .setName("Choose service") + .setDesc("Choose a service.") + .addDropdown(async (dropdown) => { + dropdown.addOption("s3", "S3 (-compatible)"); + dropdown.addOption("dropbox", "Dropbox"); + dropdown.addOption("webdav", "Webdav"); + dropdown.addOption("onedrive", "OneDrive (alpha)"); + dropdown + .setValue(this.plugin.settings.serviceType) + .onChange(async (val: SUPPORTED_SERVICES_TYPE) => { + this.plugin.settings.serviceType = val; + s3Div.toggleClass( + "s3-hide", + this.plugin.settings.serviceType !== "s3" + ); + dropboxDiv.toggleClass( + "dropbox-hide", + this.plugin.settings.serviceType !== "dropbox" + ); + onedriveDiv.toggleClass( + "onedrive-hide", + this.plugin.settings.serviceType !== "onedrive" + ); + webdavDiv.toggleClass( + "webdav-hide", + this.plugin.settings.serviceType !== "webdav" + ); + await this.plugin.saveSettings(); + }); + }); + + // 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 a 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 open a camera or scan-qrcode app, to manually scan the QR code." + ); + + const debugDiv = containerEl.createEl("div"); + debugDiv.createEl("h2", { text: "Debug" }); + const syncPlanDiv = debugDiv.createEl("div"); + new Setting(syncPlanDiv) + .setName("export sync plans") + .setDesc( + "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans" + ) + .addButton(async (button) => { + button.setButtonText("Export"); + button.onClick(async () => { + await exportSyncPlansToFiles(this.plugin.db, this.app.vault); + new Notice("sync plans history exported"); + }); + }); + new Setting(syncPlanDiv) + .setName("delete sync plans history in db") + .setDesc("delete sync plans history in db") + .addButton(async (button) => { + button.setButtonText("Delete History"); + button.onClick(async () => { + await clearAllSyncPlanRecords(this.plugin.db); + new Notice("sync plans history (in db) deleted"); + }); + }); + + const syncMappingDiv = debugDiv.createEl("div"); + new Setting(syncMappingDiv) + .setName("delete sync mappings history in db") + .setDesc( + "Sync mappings history stores the actual LOCAL last modified time of the REMOTE objects. Clearing it may cause unnecessary data exchanges in next-time sync. Click the button to delete sync mappings history in db" + ) + .addButton(async (button) => { + button.setButtonText("Delete Sync Mappings"); + button.onClick(async () => { + await clearAllSyncMetaMapping(this.plugin.db); + new Notice("sync mappings history (in local db) deleted"); + }); + }); + + const dbsResetDiv = debugDiv.createEl("div"); + new Setting(dbsResetDiv) + .setName("reset local internal cache/databases") + .setDesc( + "Reset local internal caches/databases (for debugging purposes). You would want to reload the plugin after resetting this. This option will not empty the {s3, password...} settings." + ) + .addButton(async (button) => { + button.setButtonText("Reset"); + button.onClick(async () => { + await destroyDBs(); + new Notice( + "Local internal cache/databases deleted. Please manually reload the plugin." + ); + }); + }); + } +} diff --git a/src/sync.ts b/src/sync.ts index 405d222..051971a 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -1,23 +1,20 @@ -import { TAbstractFile, TFolder, TFile, Vault } from "obsidian"; - -import { - clearDeleteRenameHistoryOfKey, - upsertSyncMetaMappingData, - getSyncMetaMappingByRemoteKey, -} from "./localdb"; -import type { FileFolderHistoryRecord, InternalDBs } from "./localdb"; - -import { RemoteClient } from "./remote"; -import type { SUPPORTED_SERVICES_TYPE, RemoteItem } from "./baseTypes"; -import { mkdirpInVault, isHiddenPath, isVaildText, setToString } from "./misc"; +import { TAbstractFile, TFile, TFolder, Vault } from "obsidian"; +import type { RemoteItem, SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import { decryptBase32ToString, - encryptStringToBase32, - MAGIC_ENCRYPTED_PREFIX_BASE32, decryptBase64urlToString, encryptStringToBase64url, + MAGIC_ENCRYPTED_PREFIX_BASE32, MAGIC_ENCRYPTED_PREFIX_BASE64URL, } from "./encrypt"; +import type { FileFolderHistoryRecord, InternalDBs } from "./localdb"; +import { + clearDeleteRenameHistoryOfKey, + getSyncMetaMappingByRemoteKey, + upsertSyncMetaMappingData, +} from "./localdb"; +import { isHiddenPath, isVaildText, mkdirpInVault } from "./misc"; +import { RemoteClient } from "./remote"; export type SyncStatusType = | "idle" diff --git a/tests/encrypt.test.ts b/tests/encrypt.test.ts index a6415ad..b5f1866 100644 --- a/tests/encrypt.test.ts +++ b/tests/encrypt.test.ts @@ -1,8 +1,7 @@ -import * as fs from "fs"; -import * as path from "path"; import * as chai from "chai"; import chaiAsPromised from "chai-as-promised"; -import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc"; +import * as fs from "fs"; +import * as path from "path"; import { decryptArrayBuffer, decryptBase32ToString, @@ -10,6 +9,7 @@ import { encryptStringToBase32, encryptStringToBase64url, } from "../src/encrypt"; +import { base64ToBase64url, bufferToArrayBuffer } from "../src/misc"; chai.use(chaiAsPromised); const expect = chai.expect; diff --git a/tests/misc.test.ts b/tests/misc.test.ts index c3cb791..e4c892e 100644 --- a/tests/misc.test.ts +++ b/tests/misc.test.ts @@ -1,8 +1,5 @@ -import * as fs from "fs"; -import * as path from "path"; import { expect } from "chai"; import { JSDOM } from "jsdom"; - import * as misc from "../src/misc"; describe("Misc: hidden file", () => {