From 1013daa9fd38b3993479b83b27c2af2de8678c42 Mon Sep 17 00:00:00 2001 From: fyears <1142836+fyears@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:12:58 +0800 Subject: [PATCH] add remoteBaseDir support --- src/baseTypes.ts | 8 ++ src/langs | 2 +- src/main.ts | 9 ++ src/misc.ts | 4 + src/remote.ts | 9 +- src/remoteForDropbox.ts | 75 ++++++++-------- src/remoteForOnedrive.ts | 74 ++++++++-------- src/remoteForWebdav.ts | 74 ++++++++-------- src/settings.ts | 182 ++++++++++++++++++++++++++++++++++++++- tests/misc.test.ts | 19 ++++ 10 files changed, 343 insertions(+), 113 deletions(-) diff --git a/src/baseTypes.ts b/src/baseTypes.ts index d508efd..8527af3 100644 --- a/src/baseTypes.ts +++ b/src/baseTypes.ts @@ -7,6 +7,11 @@ import type { LangType, LangTypeAndAuto } from "./i18n"; export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive"; +export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR = + | "webdav" + | "dropbox" + | "onedrive"; + export interface S3Config { s3Endpoint: string; s3Region: string; @@ -27,6 +32,7 @@ export interface DropboxConfig { accountID: string; username: string; credentialsShouldBeDeletedAtTime?: number; + remoteBaseDir?: string; } export type WebdavAuthType = "digest" | "basic"; @@ -44,6 +50,7 @@ export interface WebdavConfig { authType: WebdavAuthType; manualRecursive: boolean; // deprecated in 0.3.6, use depth depth?: WebdavDepthType; + remoteBaseDir?: string; } export interface OnedriveConfig { @@ -56,6 +63,7 @@ export interface OnedriveConfig { deltaLink: string; username: string; credentialsShouldBeDeletedAtTime?: number; + remoteBaseDir?: string; } export interface RemotelySavePluginSettings { diff --git a/src/langs b/src/langs index 230fee4..c729c11 160000 --- a/src/langs +++ b/src/langs @@ -1 +1 @@ -Subproject commit 230fee440e72736f7582372cbf9dc4ff648457de +Subproject commit c729c117e810fd6e01c52fe6af4f7c4764f19e48 diff --git a/src/main.ts b/src/main.ts index 47cb71a..363cc4e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -677,18 +677,27 @@ export default class RemotelySavePlugin extends Plugin { if (this.settings.dropbox.clientID === "") { this.settings.dropbox.clientID = DEFAULT_SETTINGS.dropbox.clientID; } + if (this.settings.dropbox.remoteBaseDir === undefined) { + this.settings.dropbox.remoteBaseDir = ""; + } if (this.settings.onedrive.clientID === "") { this.settings.onedrive.clientID = DEFAULT_SETTINGS.onedrive.clientID; } if (this.settings.onedrive.authority === "") { this.settings.onedrive.authority = DEFAULT_SETTINGS.onedrive.authority; } + if (this.settings.onedrive.remoteBaseDir === undefined) { + this.settings.onedrive.remoteBaseDir = ""; + } if (this.settings.webdav.manualRecursive === undefined) { this.settings.webdav.manualRecursive = false; } if (this.settings.webdav.depth === undefined) { this.settings.webdav.depth = "auto_unknown"; } + if (this.settings.webdav.remoteBaseDir === undefined) { + this.settings.webdav.remoteBaseDir = ""; + } if (this.settings.s3.partsConcurrency === undefined) { this.settings.s3.partsConcurrency = 20; } diff --git a/src/misc.ts b/src/misc.ts index a56cffd..460ea17 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -299,3 +299,7 @@ export const atWhichLevel = (x: string) => { } return y.split("/").length; }; + +export const checkHasSpecialCharForDir = (x: string) => { + return /[?/\\]/.test(x); +}; diff --git a/src/remote.ts b/src/remote.ts index 44dc5b8..b18d914 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -46,10 +46,11 @@ export class RemoteClient { "remember to provide vault name and callback while init webdav client" ); } + const remoteBaseDir = webdavConfig.remoteBaseDir || vaultName; this.webdavConfig = webdavConfig; this.webdavClient = webdav.getWebdavClient( this.webdavConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); } else if (serviceType === "dropbox") { @@ -58,10 +59,11 @@ export class RemoteClient { "remember to provide vault name and callback while init dropbox client" ); } + const remoteBaseDir = dropboxConfig.remoteBaseDir || vaultName; this.dropboxConfig = dropboxConfig; this.dropboxClient = dropbox.getDropboxClient( this.dropboxConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); } else if (serviceType === "onedrive") { @@ -70,10 +72,11 @@ export class RemoteClient { "remember to provide vault name and callback while init onedrive client" ); } + const remoteBaseDir = onedriveConfig.remoteBaseDir || vaultName; this.onedriveConfig = onedriveConfig; this.onedriveClient = onedrive.getOnedriveClient( this.onedriveConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); } else { diff --git a/src/remoteForDropbox.ts b/src/remoteForDropbox.ts index 7f8256b..0d4241d 100644 --- a/src/remoteForDropbox.ts +++ b/src/remoteForDropbox.ts @@ -26,15 +26,18 @@ export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = { credentialsShouldBeDeletedAtTime: 0, }; -export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => { +export const getDropboxPath = ( + fileOrFolderPath: string, + remoteBaseDir: string +) => { let key = fileOrFolderPath; if (fileOrFolderPath === "/" || fileOrFolderPath === "") { // special - key = `/${vaultName}`; + key = `/${remoteBaseDir}`; } if (!fileOrFolderPath.startsWith("/")) { // then this is original path in Obsidian - key = `/${vaultName}/${fileOrFolderPath}`; + key = `/${remoteBaseDir}/${fileOrFolderPath}`; } if (key.endsWith("/")) { key = key.slice(0, key.length - 1); @@ -42,16 +45,18 @@ export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => { return key; }; -const getNormPath = (fileOrFolderPath: string, vaultName: string) => { +const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => { if ( !( - fileOrFolderPath === `/${vaultName}` || - fileOrFolderPath.startsWith(`/${vaultName}/`) + fileOrFolderPath === `/${remoteBaseDir}` || + fileOrFolderPath.startsWith(`/${remoteBaseDir}/`) ) ) { - throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`); + throw Error( + `"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"` + ); } - return fileOrFolderPath.slice(`/${vaultName}/`.length); + return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length); }; const fromDropboxItemToRemoteItem = ( @@ -59,9 +64,9 @@ const fromDropboxItemToRemoteItem = ( | files.FileMetadataReference | files.FolderMetadataReference | files.DeletedMetadataReference, - vaultName: string + remoteBaseDir: string ): RemoteItem => { - let key = getNormPath(x.path_display, vaultName); + let key = getNormPath(x.path_display, remoteBaseDir); if (x[".tag"] === "folder" && !key.endsWith("/")) { key = `${key}/`; } @@ -268,17 +273,17 @@ export const setConfigBySuccessfullAuthInplace = async ( export class WrappedDropboxClient { dropboxConfig: DropboxConfig; - vaultName: string; + remoteBaseDir: string; saveUpdatedConfigFunc: () => Promise; dropbox: Dropbox; vaultFolderExists: boolean; constructor( dropboxConfig: DropboxConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) { this.dropboxConfig = dropboxConfig; - this.vaultName = vaultName; + this.remoteBaseDir = remoteBaseDir; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.vaultFolderExists = false; } @@ -318,29 +323,29 @@ export class WrappedDropboxClient { } // check vault folder - // log.info(`checking remote has folder /${this.vaultName}`); + // log.info(`checking remote has folder /${this.remoteBaseDir}`); if (this.vaultFolderExists) { - // log.info(`already checked, /${this.vaultName} exist before`) + // log.info(`already checked, /${this.remoteBaseDir} exist before`) } else { const res = await this.dropbox.filesListFolder({ path: "", recursive: false, }); for (const item of res.result.entries) { - if (item.path_display === `/${this.vaultName}`) { + if (item.path_display === `/${this.remoteBaseDir}`) { this.vaultFolderExists = true; break; } } if (!this.vaultFolderExists) { - log.info(`remote does not have folder /${this.vaultName}`); + log.info(`remote does not have folder /${this.remoteBaseDir}`); await this.dropbox.filesCreateFolderV2({ - path: `/${this.vaultName}`, + path: `/${this.remoteBaseDir}`, }); - log.info(`remote folder /${this.vaultName} created`); + log.info(`remote folder /${this.remoteBaseDir} created`); this.vaultFolderExists = true; } else { - // log.info(`remote folder /${this.vaultName} exists`); + // log.info(`remote folder /${this.remoteBaseDir} exists`); } } @@ -354,12 +359,12 @@ export class WrappedDropboxClient { */ export const getDropboxClient = ( dropboxConfig: DropboxConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) => { return new WrappedDropboxClient( dropboxConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); }; @@ -374,7 +379,7 @@ export const getRemoteMeta = async ( // we instead try to list files // if no error occurs, we ensemble a fake result. const rsp = await client.dropbox.filesListFolder({ - path: `/${client.vaultName}`, + path: `/${client.remoteBaseDir}`, recursive: false, // don't need to recursive here }); if (rsp.status !== 200) { @@ -389,7 +394,7 @@ export const getRemoteMeta = async ( } as RemoteItem; } - const key = getDropboxPath(fileOrFolderPath, client.vaultName); + const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir); const rsp = await client.dropbox.filesGetMetadata({ path: key, @@ -397,7 +402,7 @@ export const getRemoteMeta = async ( if (rsp.status !== 200) { throw Error(JSON.stringify(rsp)); } - return fromDropboxItemToRemoteItem(rsp.result, client.vaultName); + return fromDropboxItemToRemoteItem(rsp.result, client.remoteBaseDir); }; export const uploadToRemote = async ( @@ -417,7 +422,7 @@ export const uploadToRemote = async ( if (password !== "") { uploadFile = remoteEncryptedKey; } - uploadFile = getDropboxPath(uploadFile, client.vaultName); + uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir); const isFolder = fileOrFolderPath.endsWith("/"); @@ -486,7 +491,7 @@ export const uploadToRemote = async ( // we want to mark that parent folders are created if (foldersCreatedBefore !== undefined) { const dirs = getFolderLevels(uploadFile).map((x) => - getDropboxPath(x, client.vaultName) + getDropboxPath(x, client.remoteBaseDir) ); for (const dir of dirs) { foldersCreatedBefore?.add(dir); @@ -505,7 +510,7 @@ export const listFromRemote = async ( } await client.init(); let res = await client.dropbox.filesListFolder({ - path: `/${client.vaultName}`, + path: `/${client.remoteBaseDir}`, recursive: true, include_deleted: false, limit: 1000, @@ -518,8 +523,8 @@ export const listFromRemote = async ( const contents = res.result.entries; const unifiedContents = contents .filter((x) => x[".tag"] !== "deleted") - .filter((x) => x.path_display !== `/${client.vaultName}`) - .map((x) => fromDropboxItemToRemoteItem(x, client.vaultName)); + .filter((x) => x.path_display !== `/${client.remoteBaseDir}`) + .map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir)); while (res.result.has_more) { res = await client.dropbox.filesListFolderContinue({ @@ -532,8 +537,8 @@ export const listFromRemote = async ( 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)); + .filter((x) => x.path_display !== `/${client.remoteBaseDir}`) + .map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir)); unifiedContents.push(...unifiedContents2); } @@ -549,7 +554,7 @@ const downloadFromRemoteRaw = async ( fileOrFolderPath: string ) => { await client.init(); - const key = getDropboxPath(fileOrFolderPath, client.vaultName); + const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir); const rsp = await client.dropbox.filesDownload({ path: key, }); @@ -595,7 +600,7 @@ export const downloadFromRemote = async ( if (password !== "") { downloadFile = remoteEncryptedKey; } - downloadFile = getDropboxPath(downloadFile, client.vaultName); + downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir); const remoteContent = await downloadFromRemoteRaw(client, downloadFile); let localContent = remoteContent; if (password !== "") { @@ -623,7 +628,7 @@ export const deleteFromRemote = async ( if (password !== "") { remoteFileName = remoteEncryptedKey; } - remoteFileName = getDropboxPath(remoteFileName, client.vaultName); + remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir); await client.init(); try { diff --git a/src/remoteForOnedrive.ts b/src/remoteForOnedrive.ts index ca919e4..da411da 100644 --- a/src/remoteForOnedrive.ts +++ b/src/remoteForOnedrive.ts @@ -204,9 +204,9 @@ export const setConfigBySuccessfullAuthInplace = async ( // Other usual common methods //////////////////////////////////////////////////////////////////////////////// -const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => { +const getOnedrivePath = (fileOrFolderPath: string, remoteBaseDir: string) => { // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/special-folders-appfolder?view=odsp-graph-online - const prefix = `/drive/special/approot:/${vaultName}`; + const prefix = `/drive/special/approot:/${remoteBaseDir}`; if (fileOrFolderPath.startsWith(prefix)) { // already transformed, return as is return fileOrFolderPath; @@ -225,8 +225,8 @@ const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => { return key; }; -const getNormPath = (fileOrFolderPath: string, vaultName: string) => { - const prefix = `/drive/special/approot:/${vaultName}`; +const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => { + const prefix = `/drive/special/approot:/${remoteBaseDir}`; if ( !(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`)) @@ -248,16 +248,16 @@ const constructFromDriveItemToRemoteItemError = (x: DriveItem) => { const fromDriveItemToRemoteItem = ( x: DriveItem, - vaultName: string + remoteBaseDir: string ): RemoteItem => { let key = ""; // possible prefix: - // pure english: /drive/root:/Apps/remotely-save/${vaultName} - // or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName} + // pure english: /drive/root:/Apps/remotely-save/${remoteBaseDir} + // or localized, e.g.: /drive/root:/应用/remotely-save/${remoteBaseDir} const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g; // or the root is absolute path /Livefolders, - // e.g.: /Livefolders/应用/remotely-save/${vaultName} + // e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir} const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g; // another possibile prefix @@ -270,26 +270,26 @@ const fromDriveItemToRemoteItem = ( ); if ( matchFirstPrefixRes !== null && - fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`) + fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`) ) { - const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`; + const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`; key = fullPathOriginal.substring(foundPrefix.length + 1); } else if ( matchSecondPrefixRes !== null && - fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${vaultName}`) + fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`) ) { - const foundPrefix = `${matchSecondPrefixRes[0]}${vaultName}`; + const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`; key = fullPathOriginal.substring(foundPrefix.length + 1); } else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) { // it's something like - // /drive/items/!:/${vaultName}/ + // /drive/items/!:/${remoteBaseDir}/ // with uri encoded! const parPath = decodeURIComponent(x.parentReference.path); key = parPath.substring(parPath.indexOf(":") + 1); - if (key.startsWith(`/${vaultName}/`)) { - key = key.substring(`/${vaultName}/`.length); + if (key.startsWith(`/${remoteBaseDir}/`)) { + key = key.substring(`/${remoteBaseDir}/`.length); key = `${key}/${x.name}`; - } else if (key === `/${vaultName}`) { + } else if (key === `/${remoteBaseDir}`) { key = x.name; } else { throw Error( @@ -369,17 +369,17 @@ class MyAuthProvider implements AuthenticationProvider { export class WrappedOnedriveClient { onedriveConfig: OnedriveConfig; - vaultName: string; + remoteBaseDir: string; vaultFolderExists: boolean; authGetter: MyAuthProvider; saveUpdatedConfigFunc: () => Promise; constructor( onedriveConfig: OnedriveConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) { this.onedriveConfig = onedriveConfig; - this.vaultName = vaultName; + this.remoteBaseDir = remoteBaseDir; this.vaultFolderExists = false; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.authGetter = new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc); @@ -395,26 +395,26 @@ export class WrappedOnedriveClient { } // check vault folder - // log.info(`checking remote has folder /${this.vaultName}`); + // log.info(`checking remote has folder /${this.remoteBaseDir}`); if (this.vaultFolderExists) { - // log.info(`already checked, /${this.vaultName} exist before`) + // log.info(`already checked, /${this.remoteBaseDir} exist before`) } else { const k = await this.getJson("/drive/special/approot/children"); log.debug(k); this.vaultFolderExists = - (k.value as DriveItem[]).filter((x) => x.name === this.vaultName) + (k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir) .length > 0; if (!this.vaultFolderExists) { - log.info(`remote does not have folder /${this.vaultName}`); + log.info(`remote does not have folder /${this.remoteBaseDir}`); await this.postJson("/drive/special/approot/children", { - name: `${this.vaultName}`, + name: `${this.remoteBaseDir}`, folder: {}, "@microsoft.graph.conflictBehavior": "replace", }); - log.info(`remote folder /${this.vaultName} created`); + log.info(`remote folder /${this.remoteBaseDir} created`); this.vaultFolderExists = true; } else { - // log.info(`remote folder /${this.vaultName} exists`); + // log.info(`remote folder /${this.remoteBaseDir} exists`); } } }; @@ -576,12 +576,12 @@ export class WrappedOnedriveClient { export const getOnedriveClient = ( onedriveConfig: OnedriveConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) => { return new WrappedOnedriveClient( onedriveConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); }; @@ -605,7 +605,7 @@ export const listFromRemote = async ( const DELTA_LINK_KEY = "@odata.deltaLink"; let res = await client.getJson( - `/drive/special/approot:/${client.vaultName}:/delta` + `/drive/special/approot:/${client.remoteBaseDir}:/delta` ); let driveItems = res.value as DriveItem[]; @@ -622,7 +622,7 @@ export const listFromRemote = async ( // unify everything to RemoteItem const unifiedContents = driveItems - .map((x) => fromDriveItemToRemoteItem(x, client.vaultName)) + .map((x) => fromDriveItemToRemoteItem(x, client.remoteBaseDir)) .filter((x) => x.key !== "/"); return { @@ -635,14 +635,14 @@ export const getRemoteMeta = async ( fileOrFolderPath: string ) => { await client.init(); - const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName); + const remotePath = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir); // log.info(`remotePath=${remotePath}`); const rsp = await client.getJson( `${remotePath}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size` ); // log.info(rsp); const driveItem = rsp as DriveItem; - const res = fromDriveItemToRemoteItem(driveItem, client.vaultName); + const res = fromDriveItemToRemoteItem(driveItem, client.remoteBaseDir); // log.info(res); return res; }; @@ -664,7 +664,7 @@ export const uploadToRemote = async ( if (password !== "") { uploadFile = remoteEncryptedKey; } - uploadFile = getOnedrivePath(uploadFile, client.vaultName); + uploadFile = getOnedrivePath(uploadFile, client.remoteBaseDir); log.debug(`uploadFile=${uploadFile}`); const isFolder = fileOrFolderPath.endsWith("/"); @@ -751,7 +751,7 @@ export const uploadToRemote = async ( // ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online // 1. create uploadSession - // uploadFile already starts with /drive/special/approot:/${vaultName} + // uploadFile already starts with /drive/special/approot:/${remoteBaseDir} const s: UploadSession = await client.postJson( `${uploadFile}:/createUploadSession`, { @@ -792,7 +792,7 @@ const downloadFromRemoteRaw = async ( fileOrFolderPath: string ): Promise => { await client.init(); - const key = getOnedrivePath(fileOrFolderPath, client.vaultName); + const key = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir); const rsp = await client.getJson( `${key}?$select=@microsoft.graph.downloadUrl` ); @@ -832,7 +832,7 @@ export const downloadFromRemote = async ( if (password !== "") { downloadFile = remoteEncryptedKey; } - downloadFile = getOnedrivePath(downloadFile, client.vaultName); + downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir); const remoteContent = await downloadFromRemoteRaw(client, downloadFile); let localContent = remoteContent; if (password !== "") { @@ -860,7 +860,7 @@ export const deleteFromRemote = async ( if (password !== "") { remoteFileName = remoteEncryptedKey; } - remoteFileName = getOnedrivePath(remoteFileName, client.vaultName); + remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir); await client.init(); await client.deleteJson(remoteFileName); diff --git a/src/remoteForWebdav.ts b/src/remoteForWebdav.ts index 64a5161..b366a95 100644 --- a/src/remoteForWebdav.ts +++ b/src/remoteForWebdav.ts @@ -127,37 +127,40 @@ export const DEFAULT_WEBDAV_CONFIG = { authType: "basic", manualRecursive: false, depth: "auto_unknown", + remoteBaseDir: "", } as WebdavConfig; -const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => { +const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => { let key = fileOrFolderPath; if (fileOrFolderPath === "/" || fileOrFolderPath === "") { // special - key = `/${vaultName}/`; + key = `/${remoteBaseDir}/`; } if (!fileOrFolderPath.startsWith("/")) { - key = `/${vaultName}/${fileOrFolderPath}`; + key = `/${remoteBaseDir}/${fileOrFolderPath}`; } return key; }; -const getNormPath = (fileOrFolderPath: string, vaultName: string) => { +const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => { if ( !( - fileOrFolderPath === `/${vaultName}` || - fileOrFolderPath.startsWith(`/${vaultName}/`) + fileOrFolderPath === `/${remoteBaseDir}` || + fileOrFolderPath.startsWith(`/${remoteBaseDir}/`) ) ) { - throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`); + throw Error( + `"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"` + ); } // if (fileOrFolderPath.startsWith("/")) { // return fileOrFolderPath.slice(1); // } - return fileOrFolderPath.slice(`/${vaultName}/`.length); + return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length); }; -const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => { - let key = getNormPath(x.filename, vaultName); +const fromWebdavItemToRemoteItem = (x: FileStat, remoteBaseDir: string) => { + let key = getNormPath(x.filename, remoteBaseDir); if (x.type === "directory" && !key.endsWith("/")) { key = `${key}/`; } @@ -172,17 +175,17 @@ const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => { export class WrappedWebdavClient { webdavConfig: WebdavConfig; - vaultName: string; + remoteBaseDir: string; client: WebDAVClient; vaultFolderExists: boolean; saveUpdatedConfigFunc: () => Promise; constructor( webdavConfig: WebdavConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) { this.webdavConfig = webdavConfig; - this.vaultName = vaultName; + this.remoteBaseDir = remoteBaseDir; this.vaultFolderExists = false; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; } @@ -212,13 +215,13 @@ export class WrappedWebdavClient { if (this.vaultFolderExists) { // pass } else { - const res = await this.client.exists(`/${this.vaultName}/`); + const res = await this.client.exists(`/${this.remoteBaseDir}/`); if (res) { // log.info("remote vault folder exits!"); this.vaultFolderExists = true; } else { log.info("remote vault folder not exists, creating"); - await this.client.createDirectory(`/${this.vaultName}/`); + await this.client.createDirectory(`/${this.remoteBaseDir}/`); log.info("remote vault folder created!"); this.vaultFolderExists = true; } @@ -228,7 +231,7 @@ export class WrappedWebdavClient { if (this.webdavConfig.depth === "auto_unknown") { let testPassed = false; try { - const res = await this.client.customRequest(`/${this.vaultName}/`, { + const res = await this.client.customRequest(`/${this.remoteBaseDir}/`, { method: "PROPFIND", headers: { Depth: "infinity", @@ -247,13 +250,16 @@ export class WrappedWebdavClient { } if (!testPassed) { try { - const res = await this.client.customRequest(`/${this.vaultName}/`, { - method: "PROPFIND", - headers: { - Depth: "1", - }, - responseType: "text", - }); + const res = await this.client.customRequest( + `/${this.remoteBaseDir}/`, + { + method: "PROPFIND", + headers: { + Depth: "1", + }, + responseType: "text", + } + ); testPassed = true; this.webdavConfig.depth = "auto_1"; this.webdavConfig.manualRecursive = true; @@ -277,12 +283,12 @@ export class WrappedWebdavClient { export const getWebdavClient = ( webdavConfig: WebdavConfig, - vaultName: string, + remoteBaseDir: string, saveUpdatedConfigFunc: () => Promise ) => { return new WrappedWebdavClient( webdavConfig, - vaultName, + remoteBaseDir, saveUpdatedConfigFunc ); }; @@ -292,12 +298,12 @@ export const getRemoteMeta = async ( fileOrFolderPath: string ) => { await client.init(); - const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName); + const remotePath = getWebdavPath(fileOrFolderPath, client.remoteBaseDir); // log.info(`remotePath = ${remotePath}`); const res = (await client.client.stat(remotePath, { details: false, })) as FileStat; - return fromWebdavItemToRemoteItem(res, client.vaultName); + return fromWebdavItemToRemoteItem(res, client.remoteBaseDir); }; export const uploadToRemote = async ( @@ -315,7 +321,7 @@ export const uploadToRemote = async ( if (password !== "") { uploadFile = remoteEncryptedKey; } - uploadFile = getWebdavPath(uploadFile, client.vaultName); + uploadFile = getWebdavPath(uploadFile, client.remoteBaseDir); const isFolder = fileOrFolderPath.endsWith("/"); @@ -394,7 +400,7 @@ export const listFromRemote = async ( ) { // the remote doesn't support infinity propfind, // we need to do a bfs here - const q = new Queue([`/${client.vaultName}`]); + const q = new Queue([`/${client.remoteBaseDir}`]); const CHUNK_SIZE = 10; while (q.length > 0) { const itemsToFetch = []; @@ -429,7 +435,7 @@ export const listFromRemote = async ( } else { // the remote supports infinity propfind contents = (await client.client.getDirectoryContents( - `/${client.vaultName}`, + `/${client.remoteBaseDir}`, { deep: true, details: false /* no need for verbose details here */, @@ -442,7 +448,7 @@ export const listFromRemote = async ( } return { Contents: contents.map((x) => - fromWebdavItemToRemoteItem(x, client.vaultName) + fromWebdavItemToRemoteItem(x, client.remoteBaseDir) ), }; }; @@ -453,7 +459,7 @@ const downloadFromRemoteRaw = async ( ) => { await client.init(); const buff = (await client.client.getFileContents( - getWebdavPath(fileOrFolderPath, client.vaultName) + getWebdavPath(fileOrFolderPath, client.remoteBaseDir) )) as BufferLike; if (buff instanceof ArrayBuffer) { return buff; @@ -492,7 +498,7 @@ export const downloadFromRemote = async ( if (password !== "") { downloadFile = remoteEncryptedKey; } - downloadFile = getWebdavPath(downloadFile, client.vaultName); + downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir); const remoteContent = await downloadFromRemoteRaw(client, downloadFile); let localContent = remoteContent; if (password !== "") { @@ -520,7 +526,7 @@ export const deleteFromRemote = async ( if (password !== "") { remoteFileName = remoteEncryptedKey; } - remoteFileName = getWebdavPath(remoteFileName, client.vaultName); + remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir); await client.init(); try { diff --git a/src/settings.ts b/src/settings.ts index 4f406ba..7c81033 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -10,6 +10,7 @@ import { import { API_VER_REQURL, SUPPORTED_SERVICES_TYPE, + SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR, WebdavAuthType, WebdavDepthType, } from "./baseTypes"; @@ -36,6 +37,7 @@ import { messyConfigToNormal } from "./configPersist"; import type { TransItemType } from "./i18n"; import * as origLog from "loglevel"; +import { checkHasSpecialCharForDir } from "./misc"; const log = origLog.getLogger("rs-default"); class PasswordModal extends Modal { @@ -108,6 +110,100 @@ class PasswordModal extends Modal { } } +class ChangeRemoteBaseDirModal extends Modal { + readonly plugin: RemotelySavePlugin; + readonly newRemoteBaseDir: string; + readonly service: SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR; + constructor( + app: App, + plugin: RemotelySavePlugin, + newRemoteBaseDir: string, + service: SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR + ) { + super(app); + this.plugin = plugin; + this.newRemoteBaseDir = newRemoteBaseDir; + this.service = service; + } + + onOpen() { + let { contentEl } = this; + + const t = (x: TransItemType, vars?: any) => { + return this.plugin.i18n.t(x, vars); + }; + + contentEl.createEl("h2", { text: t("modal_remotebasedir_title") }); + t("modal_remotebasedir_shortdesc") + .split("\n") + .forEach((val, idx) => { + contentEl.createEl("p", { + text: val, + }); + }); + + if ( + this.newRemoteBaseDir === "" || + this.newRemoteBaseDir === this.app.vault.getName() + ) { + new Setting(contentEl) + .addButton((button) => { + button.setButtonText( + t("modal_remotebasedir_secondconfirm_vaultname") + ); + button.onClick(async () => { + // in the settings, the value is reset to the special case "" + this.plugin.settings[this.service].remoteBaseDir = ""; + await this.plugin.saveSettings(); + new Notice(t("modal_remotebasedir_notice")); + this.close(); + }); + button.setClass("remotebasedir-second-confirm"); + }) + .addButton((button) => { + button.setButtonText(t("goback")); + button.onClick(() => { + this.close(); + }); + }); + } else if (checkHasSpecialCharForDir(this.newRemoteBaseDir)) { + contentEl.createEl("p", { + text: t("modal_remotebasedir_invaliddirhint"), + }); + new Setting(contentEl).addButton((button) => { + button.setButtonText(t("goback")); + button.onClick(() => { + this.close(); + }); + }); + } else { + new Setting(contentEl) + .addButton((button) => { + button.setButtonText(t("modal_remotebasedir_secondconfirm_change")); + button.onClick(async () => { + this.plugin.settings[this.service].remoteBaseDir = + this.newRemoteBaseDir; + await this.plugin.saveSettings(); + new Notice(t("modal_remotebasedir_notice")); + this.close(); + }); + button.setClass("remotebasedir-second-confirm"); + }) + .addButton((button) => { + button.setButtonText(t("goback")); + button.onClick(() => { + this.close(); + }); + }); + } + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} + class DropboxAuthModal extends Modal { readonly plugin: RemotelySavePlugin; readonly authDiv: HTMLDivElement; @@ -861,7 +957,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab { dropboxDiv.createEl("p", { text: t("settings_dropbox_folder", { pluginID: this.plugin.manifest.id, - vaultName: this.app.vault.getName(), + remoteBaseDir: + this.plugin.settings.dropbox.remoteBaseDir || + this.app.vault.getName(), }), }); @@ -945,6 +1043,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab { this.plugin.settings.dropbox.username === "" ); + let newDropboxRemoteBaseDir = + this.plugin.settings.dropbox.remoteBaseDir || ""; + new Setting(dropboxDiv) + .setName(t("settings_remotebasedir")) + .setDesc(t("settings_remotebasedir_desc")) + .addText((text) => + text + .setPlaceholder(this.app.vault.getName()) + .setValue(newDropboxRemoteBaseDir) + .onChange((value) => { + newDropboxRemoteBaseDir = value.trim(); + }) + ) + .addButton((button) => { + button.setButtonText(t("confirm")); + button.onClick(() => { + new ChangeRemoteBaseDirModal( + this.app, + this.plugin, + newDropboxRemoteBaseDir, + "dropbox" + ).open(); + }); + }); + new Setting(dropboxDiv) .setName(t("settings_checkonnectivity")) .setDesc(t("settings_checkonnectivity_desc")) @@ -999,7 +1122,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab { onedriveDiv.createEl("p", { text: t("settings_onedrive_folder", { pluginID: this.plugin.manifest.id, - vaultName: this.app.vault.getName(), + remoteBaseDir: + this.plugin.settings.onedrive.remoteBaseDir || + this.app.vault.getName(), }), }); @@ -1064,6 +1189,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab { this.plugin.settings.onedrive.username === "" ); + let newOnedriveRemoteBaseDir = + this.plugin.settings.onedrive.remoteBaseDir || ""; + new Setting(onedriveDiv) + .setName(t("settings_remotebasedir")) + .setDesc(t("settings_remotebasedir_desc")) + .addText((text) => + text + .setPlaceholder(this.app.vault.getName()) + .setValue(newOnedriveRemoteBaseDir) + .onChange((value) => { + newOnedriveRemoteBaseDir = value.trim(); + }) + ) + .addButton((button) => { + button.setButtonText(t("confirm")); + button.onClick(() => { + new ChangeRemoteBaseDirModal( + this.app, + this.plugin, + newOnedriveRemoteBaseDir, + "onedrive" + ).open(); + }); + }); + new Setting(onedriveDiv) .setName(t("settings_checkonnectivity")) .setDesc(t("settings_checkonnectivity_desc")) @@ -1133,7 +1283,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab { webdavDiv.createEl("p", { text: t("settings_webdav_folder", { - vaultName: this.app.vault.getName(), + remoteBaseDir: + this.plugin.settings.webdav.remoteBaseDir || this.app.vault.getName(), }), }); @@ -1253,6 +1404,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab { }); }); + let newWebdavRemoteBaseDir = + this.plugin.settings.webdav.remoteBaseDir || ""; + new Setting(webdavDiv) + .setName(t("settings_remotebasedir")) + .setDesc(t("settings_remotebasedir_desc")) + .addText((text) => + text + .setPlaceholder(this.app.vault.getName()) + .setValue(newWebdavRemoteBaseDir) + .onChange((value) => { + newWebdavRemoteBaseDir = value.trim(); + }) + ) + .addButton((button) => { + button.setButtonText(t("confirm")); + button.onClick(() => { + new ChangeRemoteBaseDirModal( + this.app, + this.plugin, + newWebdavRemoteBaseDir, + "webdav" + ).open(); + }); + }); + new Setting(webdavDiv) .setName(t("settings_checkonnectivity")) .setDesc(t("settings_checkonnectivity_desc")) diff --git a/tests/misc.test.ts b/tests/misc.test.ts index 85944e9..606ea31 100644 --- a/tests/misc.test.ts +++ b/tests/misc.test.ts @@ -266,3 +266,22 @@ describe("Misc: at which level", () => { expect(misc.atWhichLevel("x/y/z.md")).to.be.equal(3); }); }); + +describe("Misc: special char for dir", () => { + it("should return false for normal string", () => { + expect(misc.checkHasSpecialCharForDir("")).to.be.false; + expect(misc.checkHasSpecialCharForDir("xxx")).to.be.false; + expect(misc.checkHasSpecialCharForDir("yyy_xxx")).to.be.false; + expect(misc.checkHasSpecialCharForDir("yyy.xxx")).to.be.false; + expect(misc.checkHasSpecialCharForDir("yyy?xxx")).to.be.false; + }); + + it("should return true for special cases", () => { + expect(misc.checkHasSpecialCharForDir("?")).to.be.true; + expect(misc.checkHasSpecialCharForDir("/")).to.be.true; + expect(misc.checkHasSpecialCharForDir("\\")).to.be.true; + expect(misc.checkHasSpecialCharForDir("xxx/yyy")).to.be.true; + expect(misc.checkHasSpecialCharForDir("xxx\\yyy")).to.be.true; + expect(misc.checkHasSpecialCharForDir("xxx?yyy")).to.be.true; + }); +});