add remoteBaseDir support

This commit is contained in:
fyears 2022-03-29 00:12:58 +08:00
parent b13bd8708b
commit 1013daa9fd
10 changed files with 343 additions and 113 deletions

View File

@ -7,6 +7,11 @@ import type { LangType, LangTypeAndAuto } from "./i18n";
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive"; export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive";
export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR =
| "webdav"
| "dropbox"
| "onedrive";
export interface S3Config { export interface S3Config {
s3Endpoint: string; s3Endpoint: string;
s3Region: string; s3Region: string;
@ -27,6 +32,7 @@ export interface DropboxConfig {
accountID: string; accountID: string;
username: string; username: string;
credentialsShouldBeDeletedAtTime?: number; credentialsShouldBeDeletedAtTime?: number;
remoteBaseDir?: string;
} }
export type WebdavAuthType = "digest" | "basic"; export type WebdavAuthType = "digest" | "basic";
@ -44,6 +50,7 @@ export interface WebdavConfig {
authType: WebdavAuthType; authType: WebdavAuthType;
manualRecursive: boolean; // deprecated in 0.3.6, use depth manualRecursive: boolean; // deprecated in 0.3.6, use depth
depth?: WebdavDepthType; depth?: WebdavDepthType;
remoteBaseDir?: string;
} }
export interface OnedriveConfig { export interface OnedriveConfig {
@ -56,6 +63,7 @@ export interface OnedriveConfig {
deltaLink: string; deltaLink: string;
username: string; username: string;
credentialsShouldBeDeletedAtTime?: number; credentialsShouldBeDeletedAtTime?: number;
remoteBaseDir?: string;
} }
export interface RemotelySavePluginSettings { export interface RemotelySavePluginSettings {

@ -1 +1 @@
Subproject commit 230fee440e72736f7582372cbf9dc4ff648457de Subproject commit c729c117e810fd6e01c52fe6af4f7c4764f19e48

View File

@ -677,18 +677,27 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.dropbox.clientID === "") { if (this.settings.dropbox.clientID === "") {
this.settings.dropbox.clientID = DEFAULT_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 === "") { if (this.settings.onedrive.clientID === "") {
this.settings.onedrive.clientID = DEFAULT_SETTINGS.onedrive.clientID; this.settings.onedrive.clientID = DEFAULT_SETTINGS.onedrive.clientID;
} }
if (this.settings.onedrive.authority === "") { if (this.settings.onedrive.authority === "") {
this.settings.onedrive.authority = DEFAULT_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) { if (this.settings.webdav.manualRecursive === undefined) {
this.settings.webdav.manualRecursive = false; this.settings.webdav.manualRecursive = false;
} }
if (this.settings.webdav.depth === undefined) { if (this.settings.webdav.depth === undefined) {
this.settings.webdav.depth = "auto_unknown"; this.settings.webdav.depth = "auto_unknown";
} }
if (this.settings.webdav.remoteBaseDir === undefined) {
this.settings.webdav.remoteBaseDir = "";
}
if (this.settings.s3.partsConcurrency === undefined) { if (this.settings.s3.partsConcurrency === undefined) {
this.settings.s3.partsConcurrency = 20; this.settings.s3.partsConcurrency = 20;
} }

View File

@ -299,3 +299,7 @@ export const atWhichLevel = (x: string) => {
} }
return y.split("/").length; return y.split("/").length;
}; };
export const checkHasSpecialCharForDir = (x: string) => {
return /[?/\\]/.test(x);
};

View File

@ -46,10 +46,11 @@ export class RemoteClient {
"remember to provide vault name and callback while init webdav client" "remember to provide vault name and callback while init webdav client"
); );
} }
const remoteBaseDir = webdavConfig.remoteBaseDir || vaultName;
this.webdavConfig = webdavConfig; this.webdavConfig = webdavConfig;
this.webdavClient = webdav.getWebdavClient( this.webdavClient = webdav.getWebdavClient(
this.webdavConfig, this.webdavConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
} else if (serviceType === "dropbox") { } else if (serviceType === "dropbox") {
@ -58,10 +59,11 @@ export class RemoteClient {
"remember to provide vault name and callback while init dropbox client" "remember to provide vault name and callback while init dropbox client"
); );
} }
const remoteBaseDir = dropboxConfig.remoteBaseDir || vaultName;
this.dropboxConfig = dropboxConfig; this.dropboxConfig = dropboxConfig;
this.dropboxClient = dropbox.getDropboxClient( this.dropboxClient = dropbox.getDropboxClient(
this.dropboxConfig, this.dropboxConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
} else if (serviceType === "onedrive") { } else if (serviceType === "onedrive") {
@ -70,10 +72,11 @@ export class RemoteClient {
"remember to provide vault name and callback while init onedrive client" "remember to provide vault name and callback while init onedrive client"
); );
} }
const remoteBaseDir = onedriveConfig.remoteBaseDir || vaultName;
this.onedriveConfig = onedriveConfig; this.onedriveConfig = onedriveConfig;
this.onedriveClient = onedrive.getOnedriveClient( this.onedriveClient = onedrive.getOnedriveClient(
this.onedriveConfig, this.onedriveConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
} else { } else {

View File

@ -26,15 +26,18 @@ export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
credentialsShouldBeDeletedAtTime: 0, credentialsShouldBeDeletedAtTime: 0,
}; };
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => { export const getDropboxPath = (
fileOrFolderPath: string,
remoteBaseDir: string
) => {
let key = fileOrFolderPath; let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") { if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special // special
key = `/${vaultName}`; key = `/${remoteBaseDir}`;
} }
if (!fileOrFolderPath.startsWith("/")) { if (!fileOrFolderPath.startsWith("/")) {
// then this is original path in Obsidian // then this is original path in Obsidian
key = `/${vaultName}/${fileOrFolderPath}`; key = `/${remoteBaseDir}/${fileOrFolderPath}`;
} }
if (key.endsWith("/")) { if (key.endsWith("/")) {
key = key.slice(0, key.length - 1); key = key.slice(0, key.length - 1);
@ -42,16 +45,18 @@ export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
return key; return key;
}; };
const getNormPath = (fileOrFolderPath: string, vaultName: string) => { const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
if ( if (
!( !(
fileOrFolderPath === `/${vaultName}` || fileOrFolderPath === `/${remoteBaseDir}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`) 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 = ( const fromDropboxItemToRemoteItem = (
@ -59,9 +64,9 @@ const fromDropboxItemToRemoteItem = (
| files.FileMetadataReference | files.FileMetadataReference
| files.FolderMetadataReference | files.FolderMetadataReference
| files.DeletedMetadataReference, | files.DeletedMetadataReference,
vaultName: string remoteBaseDir: string
): RemoteItem => { ): RemoteItem => {
let key = getNormPath(x.path_display, vaultName); let key = getNormPath(x.path_display, remoteBaseDir);
if (x[".tag"] === "folder" && !key.endsWith("/")) { if (x[".tag"] === "folder" && !key.endsWith("/")) {
key = `${key}/`; key = `${key}/`;
} }
@ -268,17 +273,17 @@ export const setConfigBySuccessfullAuthInplace = async (
export class WrappedDropboxClient { export class WrappedDropboxClient {
dropboxConfig: DropboxConfig; dropboxConfig: DropboxConfig;
vaultName: string; remoteBaseDir: string;
saveUpdatedConfigFunc: () => Promise<any>; saveUpdatedConfigFunc: () => Promise<any>;
dropbox: Dropbox; dropbox: Dropbox;
vaultFolderExists: boolean; vaultFolderExists: boolean;
constructor( constructor(
dropboxConfig: DropboxConfig, dropboxConfig: DropboxConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) { ) {
this.dropboxConfig = dropboxConfig; this.dropboxConfig = dropboxConfig;
this.vaultName = vaultName; this.remoteBaseDir = remoteBaseDir;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.vaultFolderExists = false; this.vaultFolderExists = false;
} }
@ -318,29 +323,29 @@ export class WrappedDropboxClient {
} }
// check vault folder // check vault folder
// log.info(`checking remote has folder /${this.vaultName}`); // log.info(`checking remote has folder /${this.remoteBaseDir}`);
if (this.vaultFolderExists) { if (this.vaultFolderExists) {
// log.info(`already checked, /${this.vaultName} exist before`) // log.info(`already checked, /${this.remoteBaseDir} exist before`)
} else { } else {
const res = await this.dropbox.filesListFolder({ const res = await this.dropbox.filesListFolder({
path: "", path: "",
recursive: false, recursive: false,
}); });
for (const item of res.result.entries) { for (const item of res.result.entries) {
if (item.path_display === `/${this.vaultName}`) { if (item.path_display === `/${this.remoteBaseDir}`) {
this.vaultFolderExists = true; this.vaultFolderExists = true;
break; break;
} }
} }
if (!this.vaultFolderExists) { 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({ 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; this.vaultFolderExists = true;
} else { } 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 = ( export const getDropboxClient = (
dropboxConfig: DropboxConfig, dropboxConfig: DropboxConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) => { ) => {
return new WrappedDropboxClient( return new WrappedDropboxClient(
dropboxConfig, dropboxConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
}; };
@ -374,7 +379,7 @@ export const getRemoteMeta = async (
// we instead try to list files // we instead try to list files
// if no error occurs, we ensemble a fake result. // if no error occurs, we ensemble a fake result.
const rsp = await client.dropbox.filesListFolder({ const rsp = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`, path: `/${client.remoteBaseDir}`,
recursive: false, // don't need to recursive here recursive: false, // don't need to recursive here
}); });
if (rsp.status !== 200) { if (rsp.status !== 200) {
@ -389,7 +394,7 @@ export const getRemoteMeta = async (
} as RemoteItem; } as RemoteItem;
} }
const key = getDropboxPath(fileOrFolderPath, client.vaultName); const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesGetMetadata({ const rsp = await client.dropbox.filesGetMetadata({
path: key, path: key,
@ -397,7 +402,7 @@ export const getRemoteMeta = async (
if (rsp.status !== 200) { if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp)); throw Error(JSON.stringify(rsp));
} }
return fromDropboxItemToRemoteItem(rsp.result, client.vaultName); return fromDropboxItemToRemoteItem(rsp.result, client.remoteBaseDir);
}; };
export const uploadToRemote = async ( export const uploadToRemote = async (
@ -417,7 +422,7 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
uploadFile = remoteEncryptedKey; uploadFile = remoteEncryptedKey;
} }
uploadFile = getDropboxPath(uploadFile, client.vaultName); uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir);
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
@ -486,7 +491,7 @@ export const uploadToRemote = async (
// we want to mark that parent folders are created // we want to mark that parent folders are created
if (foldersCreatedBefore !== undefined) { if (foldersCreatedBefore !== undefined) {
const dirs = getFolderLevels(uploadFile).map((x) => const dirs = getFolderLevels(uploadFile).map((x) =>
getDropboxPath(x, client.vaultName) getDropboxPath(x, client.remoteBaseDir)
); );
for (const dir of dirs) { for (const dir of dirs) {
foldersCreatedBefore?.add(dir); foldersCreatedBefore?.add(dir);
@ -505,7 +510,7 @@ export const listFromRemote = async (
} }
await client.init(); await client.init();
let res = await client.dropbox.filesListFolder({ let res = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`, path: `/${client.remoteBaseDir}`,
recursive: true, recursive: true,
include_deleted: false, include_deleted: false,
limit: 1000, limit: 1000,
@ -518,8 +523,8 @@ export const listFromRemote = async (
const contents = res.result.entries; const contents = res.result.entries;
const unifiedContents = contents const unifiedContents = contents
.filter((x) => x[".tag"] !== "deleted") .filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`) .filter((x) => x.path_display !== `/${client.remoteBaseDir}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName)); .map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir));
while (res.result.has_more) { while (res.result.has_more) {
res = await client.dropbox.filesListFolderContinue({ res = await client.dropbox.filesListFolderContinue({
@ -532,8 +537,8 @@ export const listFromRemote = async (
const contents2 = res.result.entries; const contents2 = res.result.entries;
const unifiedContents2 = contents2 const unifiedContents2 = contents2
.filter((x) => x[".tag"] !== "deleted") .filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`) .filter((x) => x.path_display !== `/${client.remoteBaseDir}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName)); .map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir));
unifiedContents.push(...unifiedContents2); unifiedContents.push(...unifiedContents2);
} }
@ -549,7 +554,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPath: string fileOrFolderPath: string
) => { ) => {
await client.init(); await client.init();
const key = getDropboxPath(fileOrFolderPath, client.vaultName); const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesDownload({ const rsp = await client.dropbox.filesDownload({
path: key, path: key,
}); });
@ -595,7 +600,7 @@ export const downloadFromRemote = async (
if (password !== "") { if (password !== "") {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getDropboxPath(downloadFile, client.vaultName); downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (password !== "") {
@ -623,7 +628,7 @@ export const deleteFromRemote = async (
if (password !== "") { if (password !== "") {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getDropboxPath(remoteFileName, client.vaultName); remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);
await client.init(); await client.init();
try { try {

View File

@ -204,9 +204,9 @@ export const setConfigBySuccessfullAuthInplace = async (
// Other usual common methods // 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 // 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)) { if (fileOrFolderPath.startsWith(prefix)) {
// already transformed, return as is // already transformed, return as is
return fileOrFolderPath; return fileOrFolderPath;
@ -225,8 +225,8 @@ const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => {
return key; return key;
}; };
const getNormPath = (fileOrFolderPath: string, vaultName: string) => { const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
const prefix = `/drive/special/approot:/${vaultName}`; const prefix = `/drive/special/approot:/${remoteBaseDir}`;
if ( if (
!(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`)) !(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`))
@ -248,16 +248,16 @@ const constructFromDriveItemToRemoteItemError = (x: DriveItem) => {
const fromDriveItemToRemoteItem = ( const fromDriveItemToRemoteItem = (
x: DriveItem, x: DriveItem,
vaultName: string remoteBaseDir: string
): RemoteItem => { ): RemoteItem => {
let key = ""; let key = "";
// possible prefix: // possible prefix:
// pure english: /drive/root:/Apps/remotely-save/${vaultName} // pure english: /drive/root:/Apps/remotely-save/${remoteBaseDir}
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName} // or localized, e.g.: /drive/root:/应用/remotely-save/${remoteBaseDir}
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g; const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
// or the root is absolute path /Livefolders, // 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; const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
// another possibile prefix // another possibile prefix
@ -270,26 +270,26 @@ const fromDriveItemToRemoteItem = (
); );
if ( if (
matchFirstPrefixRes !== null && 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); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if ( } else if (
matchSecondPrefixRes !== null && 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); key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) { } else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) {
// it's something like // it's something like
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder> // /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
// with uri encoded! // with uri encoded!
const parPath = decodeURIComponent(x.parentReference.path); const parPath = decodeURIComponent(x.parentReference.path);
key = parPath.substring(parPath.indexOf(":") + 1); key = parPath.substring(parPath.indexOf(":") + 1);
if (key.startsWith(`/${vaultName}/`)) { if (key.startsWith(`/${remoteBaseDir}/`)) {
key = key.substring(`/${vaultName}/`.length); key = key.substring(`/${remoteBaseDir}/`.length);
key = `${key}/${x.name}`; key = `${key}/${x.name}`;
} else if (key === `/${vaultName}`) { } else if (key === `/${remoteBaseDir}`) {
key = x.name; key = x.name;
} else { } else {
throw Error( throw Error(
@ -369,17 +369,17 @@ class MyAuthProvider implements AuthenticationProvider {
export class WrappedOnedriveClient { export class WrappedOnedriveClient {
onedriveConfig: OnedriveConfig; onedriveConfig: OnedriveConfig;
vaultName: string; remoteBaseDir: string;
vaultFolderExists: boolean; vaultFolderExists: boolean;
authGetter: MyAuthProvider; authGetter: MyAuthProvider;
saveUpdatedConfigFunc: () => Promise<any>; saveUpdatedConfigFunc: () => Promise<any>;
constructor( constructor(
onedriveConfig: OnedriveConfig, onedriveConfig: OnedriveConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) { ) {
this.onedriveConfig = onedriveConfig; this.onedriveConfig = onedriveConfig;
this.vaultName = vaultName; this.remoteBaseDir = remoteBaseDir;
this.vaultFolderExists = false; this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.authGetter = new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc); this.authGetter = new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc);
@ -395,26 +395,26 @@ export class WrappedOnedriveClient {
} }
// check vault folder // check vault folder
// log.info(`checking remote has folder /${this.vaultName}`); // log.info(`checking remote has folder /${this.remoteBaseDir}`);
if (this.vaultFolderExists) { if (this.vaultFolderExists) {
// log.info(`already checked, /${this.vaultName} exist before`) // log.info(`already checked, /${this.remoteBaseDir} exist before`)
} else { } else {
const k = await this.getJson("/drive/special/approot/children"); const k = await this.getJson("/drive/special/approot/children");
log.debug(k); log.debug(k);
this.vaultFolderExists = this.vaultFolderExists =
(k.value as DriveItem[]).filter((x) => x.name === this.vaultName) (k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir)
.length > 0; .length > 0;
if (!this.vaultFolderExists) { 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", { await this.postJson("/drive/special/approot/children", {
name: `${this.vaultName}`, name: `${this.remoteBaseDir}`,
folder: {}, folder: {},
"@microsoft.graph.conflictBehavior": "replace", "@microsoft.graph.conflictBehavior": "replace",
}); });
log.info(`remote folder /${this.vaultName} created`); log.info(`remote folder /${this.remoteBaseDir} created`);
this.vaultFolderExists = true; this.vaultFolderExists = true;
} else { } 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 = ( export const getOnedriveClient = (
onedriveConfig: OnedriveConfig, onedriveConfig: OnedriveConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) => { ) => {
return new WrappedOnedriveClient( return new WrappedOnedriveClient(
onedriveConfig, onedriveConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
}; };
@ -605,7 +605,7 @@ export const listFromRemote = async (
const DELTA_LINK_KEY = "@odata.deltaLink"; const DELTA_LINK_KEY = "@odata.deltaLink";
let res = await client.getJson( let res = await client.getJson(
`/drive/special/approot:/${client.vaultName}:/delta` `/drive/special/approot:/${client.remoteBaseDir}:/delta`
); );
let driveItems = res.value as DriveItem[]; let driveItems = res.value as DriveItem[];
@ -622,7 +622,7 @@ export const listFromRemote = async (
// unify everything to RemoteItem // unify everything to RemoteItem
const unifiedContents = driveItems const unifiedContents = driveItems
.map((x) => fromDriveItemToRemoteItem(x, client.vaultName)) .map((x) => fromDriveItemToRemoteItem(x, client.remoteBaseDir))
.filter((x) => x.key !== "/"); .filter((x) => x.key !== "/");
return { return {
@ -635,14 +635,14 @@ export const getRemoteMeta = async (
fileOrFolderPath: string fileOrFolderPath: string
) => { ) => {
await client.init(); await client.init();
const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName); const remotePath = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath=${remotePath}`); // log.info(`remotePath=${remotePath}`);
const rsp = await client.getJson( const rsp = await client.getJson(
`${remotePath}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size` `${remotePath}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size`
); );
// log.info(rsp); // log.info(rsp);
const driveItem = rsp as DriveItem; const driveItem = rsp as DriveItem;
const res = fromDriveItemToRemoteItem(driveItem, client.vaultName); const res = fromDriveItemToRemoteItem(driveItem, client.remoteBaseDir);
// log.info(res); // log.info(res);
return res; return res;
}; };
@ -664,7 +664,7 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
uploadFile = remoteEncryptedKey; uploadFile = remoteEncryptedKey;
} }
uploadFile = getOnedrivePath(uploadFile, client.vaultName); uploadFile = getOnedrivePath(uploadFile, client.remoteBaseDir);
log.debug(`uploadFile=${uploadFile}`); log.debug(`uploadFile=${uploadFile}`);
const isFolder = fileOrFolderPath.endsWith("/"); 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 // ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
// 1. create uploadSession // 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( const s: UploadSession = await client.postJson(
`${uploadFile}:/createUploadSession`, `${uploadFile}:/createUploadSession`,
{ {
@ -792,7 +792,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPath: string fileOrFolderPath: string
): Promise<ArrayBuffer> => { ): Promise<ArrayBuffer> => {
await client.init(); await client.init();
const key = getOnedrivePath(fileOrFolderPath, client.vaultName); const key = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.getJson( const rsp = await client.getJson(
`${key}?$select=@microsoft.graph.downloadUrl` `${key}?$select=@microsoft.graph.downloadUrl`
); );
@ -832,7 +832,7 @@ export const downloadFromRemote = async (
if (password !== "") { if (password !== "") {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getOnedrivePath(downloadFile, client.vaultName); downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (password !== "") {
@ -860,7 +860,7 @@ export const deleteFromRemote = async (
if (password !== "") { if (password !== "") {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getOnedrivePath(remoteFileName, client.vaultName); remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);
await client.init(); await client.init();
await client.deleteJson(remoteFileName); await client.deleteJson(remoteFileName);

View File

@ -127,37 +127,40 @@ export const DEFAULT_WEBDAV_CONFIG = {
authType: "basic", authType: "basic",
manualRecursive: false, manualRecursive: false,
depth: "auto_unknown", depth: "auto_unknown",
remoteBaseDir: "",
} as WebdavConfig; } as WebdavConfig;
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => { const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
let key = fileOrFolderPath; let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") { if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special // special
key = `/${vaultName}/`; key = `/${remoteBaseDir}/`;
} }
if (!fileOrFolderPath.startsWith("/")) { if (!fileOrFolderPath.startsWith("/")) {
key = `/${vaultName}/${fileOrFolderPath}`; key = `/${remoteBaseDir}/${fileOrFolderPath}`;
} }
return key; return key;
}; };
const getNormPath = (fileOrFolderPath: string, vaultName: string) => { const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
if ( if (
!( !(
fileOrFolderPath === `/${vaultName}` || fileOrFolderPath === `/${remoteBaseDir}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`) fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
) )
) { ) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`); throw Error(
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
);
} }
// if (fileOrFolderPath.startsWith("/")) { // if (fileOrFolderPath.startsWith("/")) {
// return fileOrFolderPath.slice(1); // return fileOrFolderPath.slice(1);
// } // }
return fileOrFolderPath.slice(`/${vaultName}/`.length); return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
}; };
const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => { const fromWebdavItemToRemoteItem = (x: FileStat, remoteBaseDir: string) => {
let key = getNormPath(x.filename, vaultName); let key = getNormPath(x.filename, remoteBaseDir);
if (x.type === "directory" && !key.endsWith("/")) { if (x.type === "directory" && !key.endsWith("/")) {
key = `${key}/`; key = `${key}/`;
} }
@ -172,17 +175,17 @@ const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
export class WrappedWebdavClient { export class WrappedWebdavClient {
webdavConfig: WebdavConfig; webdavConfig: WebdavConfig;
vaultName: string; remoteBaseDir: string;
client: WebDAVClient; client: WebDAVClient;
vaultFolderExists: boolean; vaultFolderExists: boolean;
saveUpdatedConfigFunc: () => Promise<any>; saveUpdatedConfigFunc: () => Promise<any>;
constructor( constructor(
webdavConfig: WebdavConfig, webdavConfig: WebdavConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) { ) {
this.webdavConfig = webdavConfig; this.webdavConfig = webdavConfig;
this.vaultName = vaultName; this.remoteBaseDir = remoteBaseDir;
this.vaultFolderExists = false; this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
} }
@ -212,13 +215,13 @@ export class WrappedWebdavClient {
if (this.vaultFolderExists) { if (this.vaultFolderExists) {
// pass // pass
} else { } else {
const res = await this.client.exists(`/${this.vaultName}/`); const res = await this.client.exists(`/${this.remoteBaseDir}/`);
if (res) { if (res) {
// log.info("remote vault folder exits!"); // log.info("remote vault folder exits!");
this.vaultFolderExists = true; this.vaultFolderExists = true;
} else { } else {
log.info("remote vault folder not exists, creating"); 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!"); log.info("remote vault folder created!");
this.vaultFolderExists = true; this.vaultFolderExists = true;
} }
@ -228,7 +231,7 @@ export class WrappedWebdavClient {
if (this.webdavConfig.depth === "auto_unknown") { if (this.webdavConfig.depth === "auto_unknown") {
let testPassed = false; let testPassed = false;
try { try {
const res = await this.client.customRequest(`/${this.vaultName}/`, { const res = await this.client.customRequest(`/${this.remoteBaseDir}/`, {
method: "PROPFIND", method: "PROPFIND",
headers: { headers: {
Depth: "infinity", Depth: "infinity",
@ -247,13 +250,16 @@ export class WrappedWebdavClient {
} }
if (!testPassed) { if (!testPassed) {
try { try {
const res = await this.client.customRequest(`/${this.vaultName}/`, { const res = await this.client.customRequest(
method: "PROPFIND", `/${this.remoteBaseDir}/`,
headers: { {
Depth: "1", method: "PROPFIND",
}, headers: {
responseType: "text", Depth: "1",
}); },
responseType: "text",
}
);
testPassed = true; testPassed = true;
this.webdavConfig.depth = "auto_1"; this.webdavConfig.depth = "auto_1";
this.webdavConfig.manualRecursive = true; this.webdavConfig.manualRecursive = true;
@ -277,12 +283,12 @@ export class WrappedWebdavClient {
export const getWebdavClient = ( export const getWebdavClient = (
webdavConfig: WebdavConfig, webdavConfig: WebdavConfig,
vaultName: string, remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any> saveUpdatedConfigFunc: () => Promise<any>
) => { ) => {
return new WrappedWebdavClient( return new WrappedWebdavClient(
webdavConfig, webdavConfig,
vaultName, remoteBaseDir,
saveUpdatedConfigFunc saveUpdatedConfigFunc
); );
}; };
@ -292,12 +298,12 @@ export const getRemoteMeta = async (
fileOrFolderPath: string fileOrFolderPath: string
) => { ) => {
await client.init(); await client.init();
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName); const remotePath = getWebdavPath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath = ${remotePath}`); // log.info(`remotePath = ${remotePath}`);
const res = (await client.client.stat(remotePath, { const res = (await client.client.stat(remotePath, {
details: false, details: false,
})) as FileStat; })) as FileStat;
return fromWebdavItemToRemoteItem(res, client.vaultName); return fromWebdavItemToRemoteItem(res, client.remoteBaseDir);
}; };
export const uploadToRemote = async ( export const uploadToRemote = async (
@ -315,7 +321,7 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
uploadFile = remoteEncryptedKey; uploadFile = remoteEncryptedKey;
} }
uploadFile = getWebdavPath(uploadFile, client.vaultName); uploadFile = getWebdavPath(uploadFile, client.remoteBaseDir);
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
@ -394,7 +400,7 @@ export const listFromRemote = async (
) { ) {
// the remote doesn't support infinity propfind, // the remote doesn't support infinity propfind,
// we need to do a bfs here // we need to do a bfs here
const q = new Queue([`/${client.vaultName}`]); const q = new Queue([`/${client.remoteBaseDir}`]);
const CHUNK_SIZE = 10; const CHUNK_SIZE = 10;
while (q.length > 0) { while (q.length > 0) {
const itemsToFetch = []; const itemsToFetch = [];
@ -429,7 +435,7 @@ export const listFromRemote = async (
} else { } else {
// the remote supports infinity propfind // the remote supports infinity propfind
contents = (await client.client.getDirectoryContents( contents = (await client.client.getDirectoryContents(
`/${client.vaultName}`, `/${client.remoteBaseDir}`,
{ {
deep: true, deep: true,
details: false /* no need for verbose details here */, details: false /* no need for verbose details here */,
@ -442,7 +448,7 @@ export const listFromRemote = async (
} }
return { return {
Contents: contents.map((x) => Contents: contents.map((x) =>
fromWebdavItemToRemoteItem(x, client.vaultName) fromWebdavItemToRemoteItem(x, client.remoteBaseDir)
), ),
}; };
}; };
@ -453,7 +459,7 @@ const downloadFromRemoteRaw = async (
) => { ) => {
await client.init(); await client.init();
const buff = (await client.client.getFileContents( const buff = (await client.client.getFileContents(
getWebdavPath(fileOrFolderPath, client.vaultName) getWebdavPath(fileOrFolderPath, client.remoteBaseDir)
)) as BufferLike; )) as BufferLike;
if (buff instanceof ArrayBuffer) { if (buff instanceof ArrayBuffer) {
return buff; return buff;
@ -492,7 +498,7 @@ export const downloadFromRemote = async (
if (password !== "") { if (password !== "") {
downloadFile = remoteEncryptedKey; downloadFile = remoteEncryptedKey;
} }
downloadFile = getWebdavPath(downloadFile, client.vaultName); downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile); const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent; let localContent = remoteContent;
if (password !== "") { if (password !== "") {
@ -520,7 +526,7 @@ export const deleteFromRemote = async (
if (password !== "") { if (password !== "") {
remoteFileName = remoteEncryptedKey; remoteFileName = remoteEncryptedKey;
} }
remoteFileName = getWebdavPath(remoteFileName, client.vaultName); remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
await client.init(); await client.init();
try { try {

View File

@ -10,6 +10,7 @@ import {
import { import {
API_VER_REQURL, API_VER_REQURL,
SUPPORTED_SERVICES_TYPE, SUPPORTED_SERVICES_TYPE,
SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR,
WebdavAuthType, WebdavAuthType,
WebdavDepthType, WebdavDepthType,
} from "./baseTypes"; } from "./baseTypes";
@ -36,6 +37,7 @@ import { messyConfigToNormal } from "./configPersist";
import type { TransItemType } from "./i18n"; import type { TransItemType } from "./i18n";
import * as origLog from "loglevel"; import * as origLog from "loglevel";
import { checkHasSpecialCharForDir } from "./misc";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
class PasswordModal extends Modal { 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 { class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin; readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement; readonly authDiv: HTMLDivElement;
@ -861,7 +957,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropboxDiv.createEl("p", { dropboxDiv.createEl("p", {
text: t("settings_dropbox_folder", { text: t("settings_dropbox_folder", {
pluginID: this.plugin.manifest.id, 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 === "" 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) new Setting(dropboxDiv)
.setName(t("settings_checkonnectivity")) .setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc")) .setDesc(t("settings_checkonnectivity_desc"))
@ -999,7 +1122,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
onedriveDiv.createEl("p", { onedriveDiv.createEl("p", {
text: t("settings_onedrive_folder", { text: t("settings_onedrive_folder", {
pluginID: this.plugin.manifest.id, 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 === "" 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) new Setting(onedriveDiv)
.setName(t("settings_checkonnectivity")) .setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc")) .setDesc(t("settings_checkonnectivity_desc"))
@ -1133,7 +1283,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: t("settings_webdav_folder", { 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) new Setting(webdavDiv)
.setName(t("settings_checkonnectivity")) .setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc")) .setDesc(t("settings_checkonnectivity_desc"))

View File

@ -266,3 +266,22 @@ describe("Misc: at which level", () => {
expect(misc.atWhichLevel("x/y/z.md")).to.be.equal(3); 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("yyyxxx")).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;
});
});