Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06b52d2f5 | ||
|
|
89f2ac035c | ||
|
|
bd3e4e91e6 | ||
|
|
e03522efe4 | ||
|
|
39a33243e8 | ||
|
|
79d00f28cc | ||
|
|
72c92018d5 | ||
|
|
510f0c2081 | ||
|
|
953507ec43 | ||
|
|
714ea43613 | ||
|
|
1013daa9fd |
+1
-1
@@ -23,6 +23,6 @@ indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
# Indentation override for all JS under lib directory
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,json,py}]
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# modified from https://github.com/argenos/nldates-obsidian/blob/master/.github/workflows/release.yml
|
||||
# which is released under MIT License
|
||||
|
||||
# trigger:
|
||||
# modify manifest.json, package.json, versions.json
|
||||
# git tag <version number>
|
||||
# git push origin --tags
|
||||
|
||||
name: Release A New Version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment: env-for-buildci
|
||||
|
||||
env:
|
||||
DROPBOX_APP_KEY: ${{secrets.DROPBOX_APP_KEY}}
|
||||
ONEDRIVE_CLIENT_ID: ${{secrets.ONEDRIVE_CLIENT_ID}}
|
||||
ONEDRIVE_AUTHORITY: ${{secrets.ONEDRIVE_AUTHORITY}}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout codes
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
submodules: recursive
|
||||
- name: Checkout LFS
|
||||
run: git lfs checkout
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ github.ref }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
- name: Upload main.js
|
||||
id: upload-main
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./main.js
|
||||
asset_name: main.js
|
||||
asset_content_type: text/javascript
|
||||
- name: Upload manifest.json
|
||||
id: upload-manifest
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./manifest.json
|
||||
asset_name: manifest.json
|
||||
asset_content_type: application/json
|
||||
- name: Upload styles.css
|
||||
id: upload-styles
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./styles.css
|
||||
asset_name: styles.css
|
||||
asset_content_type: text/css
|
||||
@@ -4,9 +4,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
|
||||
|
||||
[](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
|
||||
|
||||
[](https://github.com/fyears/remotely-save/releases)
|
||||
|
||||
[](https://github.com/fyears/remotely-save/releases)
|
||||
[](https://github.com/fyears/remotely-save/releases)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.3.14",
|
||||
"version": "0.3.16",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.3.14",
|
||||
"version": "0.3.16",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs",
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
* To avoid circular dependency.
|
||||
*/
|
||||
|
||||
import { Platform, requireApiVersion } from "obsidian";
|
||||
import type { LangType, LangTypeAndAuto } from "./i18n";
|
||||
|
||||
export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||
|
||||
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 +35,7 @@ export interface DropboxConfig {
|
||||
accountID: string;
|
||||
username: string;
|
||||
credentialsShouldBeDeletedAtTime?: number;
|
||||
remoteBaseDir?: string;
|
||||
}
|
||||
|
||||
export type WebdavAuthType = "digest" | "basic";
|
||||
@@ -44,6 +53,7 @@ export interface WebdavConfig {
|
||||
authType: WebdavAuthType;
|
||||
manualRecursive: boolean; // deprecated in 0.3.6, use depth
|
||||
depth?: WebdavDepthType;
|
||||
remoteBaseDir?: string;
|
||||
}
|
||||
|
||||
export interface OnedriveConfig {
|
||||
@@ -56,6 +66,7 @@ export interface OnedriveConfig {
|
||||
deltaLink: string;
|
||||
username: string;
|
||||
credentialsShouldBeDeletedAtTime?: number;
|
||||
remoteBaseDir?: string;
|
||||
}
|
||||
|
||||
export interface RemotelySavePluginSettings {
|
||||
@@ -138,3 +149,5 @@ export interface FileOrFolderMixedState {
|
||||
|
||||
export const API_VER_STAT_FOLDER = "0.13.27";
|
||||
export const API_VER_REQURL = "0.13.26";
|
||||
export const VALID_REQURL =
|
||||
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
|
||||
|
||||
+1
-1
Submodule src/langs updated: 230fee440e...75a3b2a830
@@ -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;
|
||||
}
|
||||
|
||||
@@ -299,3 +299,7 @@ export const atWhichLevel = (x: string) => {
|
||||
}
|
||||
return y.split("/").length;
|
||||
};
|
||||
|
||||
export const checkHasSpecialCharForDir = (x: string) => {
|
||||
return /[?/\\]/.test(x);
|
||||
};
|
||||
|
||||
+6
-3
@@ -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 {
|
||||
|
||||
+40
-35
@@ -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<any>;
|
||||
dropbox: Dropbox;
|
||||
vaultFolderExists: boolean;
|
||||
constructor(
|
||||
dropboxConfig: DropboxConfig,
|
||||
vaultName: string,
|
||||
remoteBaseDir: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
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<any>
|
||||
) => {
|
||||
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 {
|
||||
|
||||
+53
-42
@@ -11,6 +11,7 @@ import { request, requestUrl, requireApiVersion, Vault } from "obsidian";
|
||||
import {
|
||||
API_VER_REQURL,
|
||||
COMMAND_CALLBACK_ONEDRIVE,
|
||||
DEFAULT_CONTENT_TYPE,
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
OnedriveConfig,
|
||||
RemoteItem,
|
||||
@@ -204,9 +205,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 +226,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 +249,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 +271,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/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
||||
// /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
|
||||
// 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 +370,17 @@ class MyAuthProvider implements AuthenticationProvider {
|
||||
|
||||
export class WrappedOnedriveClient {
|
||||
onedriveConfig: OnedriveConfig;
|
||||
vaultName: string;
|
||||
remoteBaseDir: string;
|
||||
vaultFolderExists: boolean;
|
||||
authGetter: MyAuthProvider;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
remoteBaseDir: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.vaultName = vaultName;
|
||||
this.remoteBaseDir = remoteBaseDir;
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
this.authGetter = new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc);
|
||||
@@ -395,26 +396,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`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -505,12 +506,17 @@ export class WrappedOnedriveClient {
|
||||
putArrayBuffer = async (pathFragOrig: string, payload: ArrayBuffer) => {
|
||||
const theUrl = this.buildUrl(pathFragOrig);
|
||||
log.debug(`putArrayBuffer, theUrl=${theUrl}`);
|
||||
if (requireApiVersion(API_VER_REQURL)) {
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
if (false /*requireApiVersion(API_VER_REQURL)*/) {
|
||||
await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
contentType: DEFAULT_CONTENT_TYPE,
|
||||
headers: {
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
@@ -519,6 +525,7 @@ export class WrappedOnedriveClient {
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
@@ -547,15 +554,18 @@ export class WrappedOnedriveClient {
|
||||
}, len=${rangeEnd - rangeStart}, size=${size}`
|
||||
);
|
||||
// NO AUTH HEADER here!
|
||||
if (requireApiVersion(API_VER_REQURL)) {
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
if (false /*requireApiVersion(API_VER_REQURL)*/) {
|
||||
const res = await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
body: bufferToArrayBuffer(payload.subarray(rangeStart, rangeEnd)),
|
||||
contentType: DEFAULT_CONTENT_TYPE,
|
||||
headers: {
|
||||
// no "Content-Length" allowed here
|
||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
});
|
||||
return res.json as DriveItem | UploadSession;
|
||||
@@ -566,7 +576,7 @@ export class WrappedOnedriveClient {
|
||||
headers: {
|
||||
"Content-Length": `${rangeEnd - rangeStart}`,
|
||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
},
|
||||
});
|
||||
return (await res.json()) as DriveItem | UploadSession;
|
||||
@@ -576,12 +586,12 @@ export class WrappedOnedriveClient {
|
||||
|
||||
export const getOnedriveClient = (
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
remoteBaseDir: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) => {
|
||||
return new WrappedOnedriveClient(
|
||||
onedriveConfig,
|
||||
vaultName,
|
||||
remoteBaseDir,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
};
|
||||
@@ -605,7 +615,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 +632,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 +645,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 +674,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("/");
|
||||
@@ -737,8 +747,9 @@ export const uploadToRemote = async (
|
||||
// hard code range size
|
||||
const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB
|
||||
const RANGE_SIZE = MIN_UNIT * 20; // about 6.5536 MB
|
||||
const DIRECT_UPLOAD_MAX_SIZE = 1000 * 1000 * 4; // 4 Megabyte
|
||||
|
||||
if (remoteContent.byteLength <= RANGE_SIZE) {
|
||||
if (remoteContent.byteLength < DIRECT_UPLOAD_MAX_SIZE) {
|
||||
// directly using put!
|
||||
await client.putArrayBuffer(
|
||||
`${uploadFile}:/content?${new URLSearchParams({
|
||||
@@ -751,7 +762,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 +803,7 @@ const downloadFromRemoteRaw = async (
|
||||
fileOrFolderPath: string
|
||||
): Promise<ArrayBuffer> => {
|
||||
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 +843,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 +871,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);
|
||||
|
||||
+8
-11
@@ -22,15 +22,14 @@ import { buildQueryString } from "@aws-sdk/querystring-builder";
|
||||
import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types";
|
||||
import { Buffer } from "buffer";
|
||||
import * as mime from "mime-types";
|
||||
import {
|
||||
Vault,
|
||||
requestUrl,
|
||||
RequestUrlParam,
|
||||
RequestUrlResponse,
|
||||
requireApiVersion,
|
||||
} from "obsidian";
|
||||
import { Vault, requestUrl, RequestUrlParam } from "obsidian";
|
||||
import { Readable } from "stream";
|
||||
import { API_VER_REQURL, RemoteItem, S3Config } from "./baseTypes";
|
||||
import {
|
||||
DEFAULT_CONTENT_TYPE,
|
||||
RemoteItem,
|
||||
S3Config,
|
||||
VALID_REQURL,
|
||||
} from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import {
|
||||
arrayBufferToBuffer,
|
||||
@@ -189,7 +188,7 @@ export const getS3Client = (s3Config: S3Config) => {
|
||||
endpoint = `https://${endpoint}`;
|
||||
}
|
||||
|
||||
if (requireApiVersion(API_VER_REQURL) && s3Config.bypassCorsLocally) {
|
||||
if (VALID_REQURL && s3Config.bypassCorsLocally) {
|
||||
const s3Client = new S3Client({
|
||||
region: s3Config.s3Region,
|
||||
endpoint: endpoint,
|
||||
@@ -247,8 +246,6 @@ export const uploadToRemote = async (
|
||||
}
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||
|
||||
if (isFolder && isRecursively) {
|
||||
throw Error("upload function doesn't implement recursive function yet!");
|
||||
} else if (isFolder && !isRecursively) {
|
||||
|
||||
+38
-40
@@ -1,19 +1,11 @@
|
||||
import { Buffer } from "buffer";
|
||||
import {
|
||||
Vault,
|
||||
request,
|
||||
requestUrl,
|
||||
requireApiVersion,
|
||||
Platform,
|
||||
Notice,
|
||||
RequestUrlResponse,
|
||||
} from "obsidian";
|
||||
import { Vault, requestUrl } from "obsidian";
|
||||
|
||||
import { Queue } from "@fyears/tsqueue";
|
||||
import chunk from "lodash/chunk";
|
||||
import flatten from "lodash/flatten";
|
||||
import { getReasonPhrase } from "http-status-codes";
|
||||
import { API_VER_REQURL, RemoteItem, WebdavConfig } from "./baseTypes";
|
||||
import { RemoteItem, VALID_REQURL, WebdavConfig } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
|
||||
|
||||
@@ -28,7 +20,7 @@ import type {
|
||||
ResponseDataDetailed,
|
||||
} from "webdav/web";
|
||||
import { getPatcher } from "webdav/web";
|
||||
if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
|
||||
if (VALID_REQURL) {
|
||||
getPatcher().patch(
|
||||
"request",
|
||||
async (
|
||||
@@ -127,37 +119,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 +167,17 @@ const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
|
||||
|
||||
export class WrappedWebdavClient {
|
||||
webdavConfig: WebdavConfig;
|
||||
vaultName: string;
|
||||
remoteBaseDir: string;
|
||||
client: WebDAVClient;
|
||||
vaultFolderExists: boolean;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
webdavConfig: WebdavConfig,
|
||||
vaultName: string,
|
||||
remoteBaseDir: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.webdavConfig = webdavConfig;
|
||||
this.vaultName = vaultName;
|
||||
this.remoteBaseDir = remoteBaseDir;
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
}
|
||||
@@ -212,13 +207,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 +223,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 +242,16 @@ export class WrappedWebdavClient {
|
||||
}
|
||||
if (!testPassed) {
|
||||
try {
|
||||
const res = await this.client.customRequest(`/${this.vaultName}/`, {
|
||||
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 +275,12 @@ export class WrappedWebdavClient {
|
||||
|
||||
export const getWebdavClient = (
|
||||
webdavConfig: WebdavConfig,
|
||||
vaultName: string,
|
||||
remoteBaseDir: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) => {
|
||||
return new WrappedWebdavClient(
|
||||
webdavConfig,
|
||||
vaultName,
|
||||
remoteBaseDir,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
};
|
||||
@@ -292,12 +290,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 +313,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 +392,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 +427,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 +440,7 @@ export const listFromRemote = async (
|
||||
}
|
||||
return {
|
||||
Contents: contents.map((x) =>
|
||||
fromWebdavItemToRemoteItem(x, client.vaultName)
|
||||
fromWebdavItemToRemoteItem(x, client.remoteBaseDir)
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -453,7 +451,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 +490,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 +518,7 @@ export const deleteFromRemote = async (
|
||||
if (password !== "") {
|
||||
remoteFileName = remoteEncryptedKey;
|
||||
}
|
||||
remoteFileName = getWebdavPath(remoteFileName, client.vaultName);
|
||||
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
|
||||
|
||||
await client.init();
|
||||
try {
|
||||
|
||||
+209
-13
@@ -10,6 +10,8 @@ import {
|
||||
import {
|
||||
API_VER_REQURL,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR,
|
||||
VALID_REQURL,
|
||||
WebdavAuthType,
|
||||
WebdavDepthType,
|
||||
} from "./baseTypes";
|
||||
@@ -36,6 +38,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 +111,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;
|
||||
@@ -651,7 +748,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
}
|
||||
|
||||
if (!requireApiVersion(API_VER_REQURL)) {
|
||||
if (!VALID_REQURL) {
|
||||
s3Div.createEl("p", {
|
||||
text: t("settings_s3_cors"),
|
||||
});
|
||||
@@ -673,7 +770,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
text: t("settings_s3_prod2"),
|
||||
});
|
||||
|
||||
if (!requireApiVersion(API_VER_REQURL)) {
|
||||
if (!VALID_REQURL) {
|
||||
s3LinksUl.createEl("li").createEl("a", {
|
||||
href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html",
|
||||
text: t("settings_s3_prod3"),
|
||||
@@ -766,7 +863,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
if (requireApiVersion(API_VER_REQURL)) {
|
||||
if (VALID_REQURL) {
|
||||
new Setting(s3Div)
|
||||
.setName(t("settings_s3_bypasscorslocally"))
|
||||
.setDesc(
|
||||
@@ -861,7 +958,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(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -915,6 +1014,28 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(dropboxRevokeAuthDiv)
|
||||
.setName(t("settings_dropbox_clearlocal"))
|
||||
.setDesc(t("settings_dropbox_clearlocal_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_dropbox_clearlocal_button"));
|
||||
button.onClick(async () => {
|
||||
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(t("settings_dropbox_clearlocal_notice"));
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(dropboxAuthDiv)
|
||||
.setName(t("settings_dropbox_auth"))
|
||||
.setDesc(t("settings_dropbox_auth_desc"))
|
||||
@@ -945,6 +1066,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 +1145,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 +1212,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"))
|
||||
@@ -1105,9 +1278,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.serviceType !== "webdav"
|
||||
);
|
||||
|
||||
const webdavReq =
|
||||
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
|
||||
|
||||
webdavDiv.createEl("h2", { text: t("settings_webdav") });
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
@@ -1115,7 +1285,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
cls: "webdav-disclaimer",
|
||||
});
|
||||
|
||||
if (!webdavReq) {
|
||||
if (!VALID_REQURL) {
|
||||
if (Platform.isAndroidApp) {
|
||||
webdavDiv.createEl("p", {
|
||||
text: t("settings_webdav_cors_android"),
|
||||
@@ -1133,7 +1303,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(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1199,12 +1370,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
.setDesc(t("settings_webdav_auth_desc"))
|
||||
.addDropdown(async (dropdown) => {
|
||||
dropdown.addOption("basic", "basic");
|
||||
if (webdavReq) {
|
||||
if (VALID_REQURL) {
|
||||
dropdown.addOption("digest", "digest");
|
||||
}
|
||||
|
||||
// new version config, copied to old version, we need to reset it
|
||||
if (!webdavReq && this.plugin.settings.webdav.authType !== "basic") {
|
||||
if (!VALID_REQURL && this.plugin.settings.webdav.authType !== "basic") {
|
||||
this.plugin.settings.webdav.authType = "basic";
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
@@ -1253,6 +1424,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"))
|
||||
@@ -1277,7 +1473,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
if (res) {
|
||||
new Notice(t("settings_webdav_connect_succ"));
|
||||
} else {
|
||||
if (webdavReq) {
|
||||
if (VALID_REQURL) {
|
||||
new Notice(t("settings_webdav_connect_fail"));
|
||||
} else {
|
||||
new Notice(t("settings_webdav_connect_fail_withcors"));
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"0.3.2": "0.12.15",
|
||||
"0.3.14": "0.13.21"
|
||||
"0.3.16": "0.13.21"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user