Compare commits

...
24 Commits
Author SHA1 Message Date
fyears e06b52d2f5 add comments
Release A New Version / build (16.x) (push) Failing after 45s
2022-04-03 15:31:48 +08:00
fyears 89f2ac035c bump to 0.3.16 2022-04-03 15:30:35 +08:00
fyears bd3e4e91e6 use ci to release 2022-04-03 15:27:51 +08:00
fyears e03522efe4 unified requrl everywhere 2022-04-03 15:05:55 +08:00
fyears 39a33243e8 fix typo 2022-04-03 14:53:39 +08:00
fyears 79d00f28cc update download stats 2022-04-03 13:16:23 +08:00
fyears 72c92018d5 bump to 0.3.15 2022-04-02 00:01:58 +08:00
fyears 510f0c2081 add dbx clearlocal 2022-04-02 00:01:20 +08:00
fyears 953507ec43 fix onedrive upload issue! 2022-04-01 23:39:41 +08:00
fyears 714ea43613 update .editorconfig 2022-03-29 00:32:08 +08:00
fyears 1013daa9fd add remoteBaseDir support 2022-03-29 00:18:04 +08:00
fyears b13bd8708b bump to 0.3.14 2022-03-27 00:55:12 +08:00
fyears 83c3aac317 some logic to move away vaultRandomID from data.json 2022-03-27 00:52:10 +08:00
fyears 712979fb0d reset depth settings of webdav 2022-03-26 22:58:57 +08:00
fyears e460c563af skip extra checking folders for webdav 2022-03-26 22:41:10 +08:00
fyears 580397fbf9 update text 2022-03-26 15:11:41 +08:00
fyears 537fae1a2b add tracking for local movement 2022-03-26 15:06:34 +08:00
fyears dbc9e02ff3 update bug report tmpl 2022-03-26 11:24:20 +08:00
fyears 6474faad51 fix memory. Squashed commit of the following:
commit 1c24e48f2528238aeac417866ccec9ee3deced5f
Author: fyears <1142836+fyears@users.noreply.github.com>
Date:   Fri Mar 25 02:08:05 2022 +0800

    just some trying
2022-03-26 09:45:48 +08:00
fyears a5e415aeaf Merge branch 'master' of https://github.com/fyears/remotely-save 2022-03-26 00:34:13 +08:00
fyears 3e75e31696 change wording in config file 2022-03-26 00:34:00 +08:00
fyears 9c8cc1c8b8 update esbuild 2022-03-25 23:41:29 +08:00
fyears cccc4969cd Update browser_env.md 2022-03-23 23:38:48 +08:00
fyears 1ba1591c75 Update README.md 2022-03-23 23:37:49 +08:00
22 changed files with 897 additions and 280 deletions
+1 -1
View File
@@ -23,6 +23,6 @@ indent_size = 4
indent_style = tab indent_style = tab
# Indentation override for all JS under lib directory # 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_style = space
indent_size = 2 indent_size = 2
+9 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: File a bug report description: Fire a bug report
title: "[Bug]: " title: "[Bug]: "
labels: ["bug"] labels: ["bug"]
body: body:
@@ -55,6 +55,14 @@ body:
description: What version of Obsidian are you running? description: What version of Obsidian are you running?
validations: validations:
required: false required: false
- type: checkboxes
id: using-password
attributes:
label: Using password or not
description: Are you using password (end-to-end encryption) or not?
options:
- label: Yes.
required: false
- type: checkboxes - type: checkboxes
id: ensure-no-sensitive-information id: ensure-no-sensitive-information
attributes: attributes:
+88
View File
@@ -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
+3 -5
View File
@@ -4,9 +4,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
[![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) [![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
[![total downloads auto count](https://remotely-save.github.io/auto-download-stats/totalDownloads.svg)](https://github.com/fyears/remotely-save/releases) [![downloads of latest version](https://img.shields.io/github/downloads-pre/remotely-save/remotely-save/latest/main.js?sort=semver)](https://github.com/fyears/remotely-save/releases)
[![downloads of latest version](https://remotely-save.github.io/auto-download-stats/latestVersionDownloads.svg)](https://github.com/fyears/remotely-save/releases)
## Disclaimer ## Disclaimer
@@ -87,8 +85,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### webdav ### webdav
- About CORS: - About CORS:
- If you are using Obsidian desktop >= 0.13.25 or mobile >= 1.1.1, you can skip this CORS part. - If you are using Obsidian desktop >= 0.13.25 or iOS >= 1.1.1, you can skip this CORS part.
- If you are using Obsidian desktop < 0.13.25 or mobile < 1.1.1: - If you are using Obsidian desktop < 0.13.25 or iOS < 1.1.1 or any Android version:
- The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version. - The webdav server has to be enabled CORS for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`, **AND** all webdav HTTP methods, **AND** all webdav headers. These are required, because Obsidian mobile works like a browser and mobile plugins are limited by CORS policies unless under a upgraded Obsidian version.
- Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version. - Popular software NextCloud, OwnCloud, `rclone serve webdav` do **NOT** enable CORS by default. If you are using any of them, you should evaluate the risk, and find a way to enable CORS, before using this plugin, or use a upgraded Obsidian version.
- The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details. - The plugin is tested successfully under python package [`wsgidav` (version 4.0)](https://github.com/mar10/wsgidav). See [this issue](https://github.com/mar10/wsgidav/issues/239) for some details.
+1 -1
View File
@@ -6,7 +6,7 @@ Technically, the plugin (or any plugin?) runs in the js environment provided by
Then some limitations are applied: Then some limitations are applied:
1. [The CORS issue (solved in the new Obsidian version).](./browser_env_cors.md) 1. [The CORS issue (solved in the new Obsidian version on some platforms).](./browser_env_cors.md)
2. [No Node.js environment.](./browser_env_no_nodejs.md) 2. [No Node.js environment.](./browser_env_no_nodejs.md)
3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md). 3. If the cloud service uses OAuth flow, it needs to support PKCE. More details are [here](./browser_env_oauth2_pkce.md).
4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md) 4. [No background running after Obsidian is closes.](./browser_env_no_background_after_closing.md)
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.3.13", "version": "0.3.16",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+2 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.3.13", "version": "0.3.16",
"description": "This is yet another sync plugin for Obsidian app.", "description": "This is yet another sync plugin for Obsidian app.",
"scripts": { "scripts": {
"dev2": "node esbuild.config.mjs", "dev2": "node esbuild.config.mjs",
@@ -38,7 +38,7 @@
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "^0.14.3", "esbuild": "^0.14.27",
"jsdom": "^19.0.0", "jsdom": "^19.0.0",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
@@ -65,7 +65,6 @@
"assert": "^2.0.0", "assert": "^2.0.0",
"aws-crt": "^1.10.1", "aws-crt": "^1.10.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"codemirror": "^5.63.1",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"dropbox": "^10.22.0", "dropbox": "^10.22.0",
"http-status-codes": "^2.2.0", "http-status-codes": "^2.2.0",
+20 -2
View File
@@ -3,10 +3,18 @@
* To avoid circular dependency. * To avoid circular dependency.
*/ */
import { Platform, requireApiVersion } from "obsidian";
import type { LangType, LangTypeAndAuto } from "./i18n"; 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 = "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 +35,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 +53,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 +66,7 @@ export interface OnedriveConfig {
deltaLink: string; deltaLink: string;
username: string; username: string;
credentialsShouldBeDeletedAtTime?: number; credentialsShouldBeDeletedAtTime?: number;
remoteBaseDir?: string;
} }
export interface RemotelySavePluginSettings { export interface RemotelySavePluginSettings {
@@ -66,7 +77,6 @@ export interface RemotelySavePluginSettings {
password: string; password: string;
serviceType: SUPPORTED_SERVICES_TYPE; serviceType: SUPPORTED_SERVICES_TYPE;
currLogLevel?: string; currLogLevel?: string;
vaultRandomID?: string;
autoRunEveryMilliseconds?: number; autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number; initRunAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean; agreeToUploadExtraMetadata?: boolean;
@@ -74,6 +84,11 @@ export interface RemotelySavePluginSettings {
syncConfigDir?: boolean; syncConfigDir?: boolean;
syncUnderscoreItems?: boolean; syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto; lang?: LangTypeAndAuto;
/**
* @deprecated
*/
vaultRandomID?: string;
} }
export interface RemoteItem { export interface RemoteItem {
@@ -124,7 +139,8 @@ export interface FileOrFolderMixedState {
deltimeRemote?: number; deltimeRemote?: number;
sizeLocal?: number; sizeLocal?: number;
sizeRemote?: number; sizeRemote?: number;
changeMtimeUsingMapping?: boolean; changeRemoteMtimeUsingMapping?: boolean;
changeLocalMtimeUsingMapping?: boolean;
decision?: DecisionType; decision?: DecisionType;
decisionBranch?: number; decisionBranch?: number;
syncDone?: "done"; syncDone?: "done";
@@ -133,3 +149,5 @@ export interface FileOrFolderMixedState {
export const API_VER_STAT_FOLDER = "0.13.27"; export const API_VER_STAT_FOLDER = "0.13.27";
export const API_VER_REQURL = "0.13.26"; export const API_VER_REQURL = "0.13.26";
export const VALID_REQURL =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
+1 -1
View File
@@ -7,7 +7,7 @@ import * as origLog from "loglevel";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DEFAULT_README: string = const DEFAULT_README: string =
"Do NOT modify this manually. It's generated automatically."; "The file contains sensitive info, so DO NOT take screenshot of, copy, or share it to anyone! It's also generated automatically, so do not edit it manually.";
interface MessyConfigType { interface MessyConfigType {
readme: string; readme: string;
+163 -33
View File
@@ -1,21 +1,23 @@
import localforage from "localforage"; import localforage from "localforage";
import { TAbstractFile, TFile, TFolder } from "obsidian"; import { requireApiVersion, TAbstractFile, TFile, TFolder } from "obsidian";
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes"; import { API_VER_STAT_FOLDER, SUPPORTED_SERVICES_TYPE } from "./baseTypes";
import type { SyncPlanType } from "./sync"; import type { SyncPlanType } from "./sync";
export type LocalForage = typeof localforage; export type LocalForage = typeof localforage;
import * as origLog from "loglevel"; import * as origLog from "loglevel";
import { nanoid } from "nanoid";
const log = origLog.getLogger("rs-default"); const log = origLog.getLogger("rs-default");
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108]; const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326];
export const DEFAULT_DB_VERSION_NUMBER: number = 20220108; export const DEFAULT_DB_VERSION_NUMBER: number = 20220326;
export const DEFAULT_DB_NAME = "remotelysavedb"; export const DEFAULT_DB_NAME = "remotelysavedb";
export const DEFAULT_TBL_VERSION = "schemaversion"; export const DEFAULT_TBL_VERSION = "schemaversion";
export const DEFAULT_TBL_DELETE_HISTORY = "filefolderoperationhistory"; export const DEFAULT_TBL_FILE_HISTORY = "filefolderoperationhistory";
export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory"; export const DEFAULT_TBL_SYNC_MAPPING = "syncmetadatahistory";
export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory"; export const DEFAULT_SYNC_PLANS_HISTORY = "syncplanshistory";
export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
export interface FileFolderHistoryRecord { export interface FileFolderHistoryRecord {
key: string; key: string;
@@ -23,7 +25,7 @@ export interface FileFolderHistoryRecord {
mtime: number; mtime: number;
size: number; size: number;
actionWhen: number; actionWhen: number;
actionType: "delete" | "rename"; actionType: "delete" | "rename" | "renameDestination";
keyType: "folder" | "file"; keyType: "folder" | "file";
renameTo: string; renameTo: string;
vaultRandomID: string; vaultRandomID: string;
@@ -51,9 +53,10 @@ interface SyncPlanRecord {
export interface InternalDBs { export interface InternalDBs {
versionTbl: LocalForage; versionTbl: LocalForage;
deleteHistoryTbl: LocalForage; fileHistoryTbl: LocalForage;
syncMappingTbl: LocalForage; syncMappingTbl: LocalForage;
syncPlansTbl: LocalForage; syncPlansTbl: LocalForage;
vaultRandomIDMappingTbl: LocalForage;
} }
/** /**
@@ -72,12 +75,12 @@ const migrateDBsFrom20211114To20220108 = async (
const allPromisesToWait: Promise<any>[] = []; const allPromisesToWait: Promise<any>[] = [];
log.debug("assign vault id to any delete history"); log.debug("assign vault id to any delete history");
const keysInDeleteHistoryTbl = await db.deleteHistoryTbl.keys(); const keysInDeleteHistoryTbl = await db.fileHistoryTbl.keys();
for (const key of keysInDeleteHistoryTbl) { for (const key of keysInDeleteHistoryTbl) {
if (key.startsWith(vaultRandomID)) { if (key.startsWith(vaultRandomID)) {
continue; continue;
} }
const value = (await db.deleteHistoryTbl.getItem( const value = (await db.fileHistoryTbl.getItem(
key key
)) as FileFolderHistoryRecord; )) as FileFolderHistoryRecord;
if (value === null || value === undefined) { if (value === null || value === undefined) {
@@ -87,8 +90,8 @@ const migrateDBsFrom20211114To20220108 = async (
value.vaultRandomID = vaultRandomID; value.vaultRandomID = vaultRandomID;
} }
const newKey = `${vaultRandomID}\t${key}`; const newKey = `${vaultRandomID}\t${key}`;
allPromisesToWait.push(db.deleteHistoryTbl.setItem(newKey, value)); allPromisesToWait.push(db.fileHistoryTbl.setItem(newKey, value));
allPromisesToWait.push(db.deleteHistoryTbl.removeItem(key)); allPromisesToWait.push(db.fileHistoryTbl.removeItem(key));
} }
log.debug("assign vault id to any sync mapping"); log.debug("assign vault id to any sync mapping");
@@ -136,6 +139,23 @@ const migrateDBsFrom20211114To20220108 = async (
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`); log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
}; };
/**
* no need to do anything except changing version
* we just add more file operations in db, and no schema is changed.
* @param db
* @param vaultRandomID
*/
const migrateDBsFrom20220108To20220326 = async (
db: InternalDBs,
vaultRandomID: string
) => {
const oldVer = 20220108;
const newVer = 20220326;
log.debug(`start upgrading internal db from ${oldVer} to ${newVer}`);
await db.versionTbl.setItem("version", newVer);
log.debug(`finish upgrading internal db from ${oldVer} to ${newVer}`);
};
const migrateDBs = async ( const migrateDBs = async (
db: InternalDBs, db: InternalDBs,
oldVer: number, oldVer: number,
@@ -148,19 +168,36 @@ const migrateDBs = async (
if (oldVer === 20211114 && newVer === 20220108) { if (oldVer === 20211114 && newVer === 20220108) {
return await migrateDBsFrom20211114To20220108(db, vaultRandomID); return await migrateDBsFrom20211114To20220108(db, vaultRandomID);
} }
if (oldVer === 20220108 && newVer === 20220326) {
return await migrateDBsFrom20220108To20220326(db, vaultRandomID);
}
if (oldVer === 20211114 && newVer === 20220326) {
// TODO: more steps with more versions in the future
await migrateDBsFrom20211114To20220108(db, vaultRandomID);
await migrateDBsFrom20220108To20220326(db, vaultRandomID);
return;
}
if (newVer < oldVer) {
throw Error(
"You've installed a new version, but then downgrade to an old version. Stop working!"
);
}
// not implemented // not implemented
throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`); throw Error(`not supported internal db changes from ${oldVer} to ${newVer}`);
}; };
export const prepareDBs = async (vaultRandomID: string) => { export const prepareDBs = async (
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) => {
const db = { const db = {
versionTbl: localforage.createInstance({ versionTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VERSION, storeName: DEFAULT_TBL_VERSION,
}), }),
deleteHistoryTbl: localforage.createInstance({ fileHistoryTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_DELETE_HISTORY, storeName: DEFAULT_TBL_FILE_HISTORY,
}), }),
syncMappingTbl: localforage.createInstance({ syncMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
@@ -170,9 +207,42 @@ export const prepareDBs = async (vaultRandomID: string) => {
name: DEFAULT_DB_NAME, name: DEFAULT_DB_NAME,
storeName: DEFAULT_SYNC_PLANS_HISTORY, storeName: DEFAULT_SYNC_PLANS_HISTORY,
}), }),
vaultRandomIDMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING,
}),
} as InternalDBs; } as InternalDBs;
const originalVersion = (await db.versionTbl.getItem("version")) as number; // try to get vaultRandomID firstly
let vaultRandomID = "";
const vaultRandomIDInDB: string | null =
await db.vaultRandomIDMappingTbl.getItem(`path2id\t${vaultBasePath}`);
if (vaultRandomIDInDB === null) {
if (vaultRandomIDFromOldConfigFile !== "") {
// reuse the old config id
vaultRandomID = vaultRandomIDFromOldConfigFile;
} else {
// no old config id, we create a random one
vaultRandomID = nanoid();
}
// save the id back
await db.vaultRandomIDMappingTbl.setItem(
`path2id\t${vaultBasePath}`,
vaultRandomID
);
await db.vaultRandomIDMappingTbl.setItem(
`id2path\t${vaultRandomID}`,
vaultBasePath
);
} else {
vaultRandomID = vaultRandomIDInDB;
}
if (vaultRandomID === "") {
throw Error("no vaultRandomID found or generated");
}
const originalVersion: number | null = await db.versionTbl.getItem("version");
if (originalVersion === null) { if (originalVersion === null) {
log.debug( log.debug(
`no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}` `no internal db version, setting it to ${DEFAULT_DB_VERSION_NUMBER}`
@@ -193,7 +263,10 @@ export const prepareDBs = async (vaultRandomID: string) => {
} }
log.info("db connected"); log.info("db connected");
return db; return {
db: db,
vaultRandomID: vaultRandomID,
};
}; };
export const destroyDBs = async () => { export const destroyDBs = async () => {
@@ -206,20 +279,20 @@ export const destroyDBs = async () => {
log.info("db deleted"); log.info("db deleted");
}; };
req.onblocked = (event) => { req.onblocked = (event) => {
console.warn("trying to delete db but it was blocked"); log.warn("trying to delete db but it was blocked");
}; };
req.onerror = (event) => { req.onerror = (event) => {
console.error("tried to delete db but something bad!"); log.error("tried to delete db but something goes wrong!");
console.error(event); log.error(event);
}; };
}; };
export const loadDeleteRenameHistoryTableByVault = async ( export const loadFileHistoryTableByVault = async (
db: InternalDBs, db: InternalDBs,
vaultRandomID: string vaultRandomID: string
) => { ) => {
const records = [] as FileFolderHistoryRecord[]; const records = [] as FileFolderHistoryRecord[];
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => { await db.fileHistoryTbl.iterate((value, key, iterationNumber) => {
if (key.startsWith(`${vaultRandomID}\t`)) { if (key.startsWith(`${vaultRandomID}\t`)) {
records.push(value as FileFolderHistoryRecord); records.push(value as FileFolderHistoryRecord);
} }
@@ -233,7 +306,16 @@ export const clearDeleteRenameHistoryOfKeyAndVault = async (
key: string, key: string,
vaultRandomID: string vaultRandomID: string
) => { ) => {
await db.deleteHistoryTbl.removeItem(`${vaultRandomID}\t${key}`); const fullKey = `${vaultRandomID}\t${key}`;
const item: FileFolderHistoryRecord | null = await db.fileHistoryTbl.getItem(
fullKey
);
if (
item !== null &&
(item.actionType === "delete" || item.actionType === "rename")
) {
await db.fileHistoryTbl.removeItem(fullKey);
}
}; };
export const insertDeleteRecordByVault = async ( export const insertDeleteRecordByVault = async (
@@ -260,10 +342,12 @@ export const insertDeleteRecordByVault = async (
const key = fileOrFolder.path.endsWith("/") const key = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
const ctime = 0; // they are deleted, so no way to get ctime, mtime
const mtime = 0; // they are deleted, so no way to get ctime, mtime
k = { k = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: Date.now(),
actionType: "delete", actionType: "delete",
@@ -272,9 +356,19 @@ export const insertDeleteRecordByVault = async (
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k);
}; };
/**
* A file/folder is renamed from A to B
* We insert two records:
* A with actionType="rename"
* B with actionType="renameDestination"
* @param db
* @param fileOrFolder
* @param oldPath
* @param vaultRandomID
*/
export const insertRenameRecordByVault = async ( export const insertRenameRecordByVault = async (
db: InternalDBs, db: InternalDBs,
fileOrFolder: TAbstractFile, fileOrFolder: TAbstractFile,
@@ -282,37 +376,73 @@ export const insertRenameRecordByVault = async (
vaultRandomID: string vaultRandomID: string
) => { ) => {
// log.info(fileOrFolder); // log.info(fileOrFolder);
let k: FileFolderHistoryRecord; let k1: FileFolderHistoryRecord;
let k2: FileFolderHistoryRecord;
const actionWhen = Date.now();
if (fileOrFolder instanceof TFile) { if (fileOrFolder instanceof TFile) {
k = { k1 = {
key: oldPath, key: oldPath,
ctime: fileOrFolder.stat.ctime, ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime, mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size, size: fileOrFolder.stat.size,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "file", keyType: "file",
renameTo: fileOrFolder.path, renameTo: fileOrFolder.path,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: fileOrFolder.path,
ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "file",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} else if (fileOrFolder instanceof TFolder) { } else if (fileOrFolder instanceof TFolder) {
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`; const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
const renameTo = fileOrFolder.path.endsWith("/") const renameTo = fileOrFolder.path.endsWith("/")
? fileOrFolder.path ? fileOrFolder.path
: `${fileOrFolder.path}/`; : `${fileOrFolder.path}/`;
k = { let ctime = 0;
let mtime = 0;
if (requireApiVersion(API_VER_STAT_FOLDER)) {
// TAbstractFile does not contain these info
// but from API_VER_STAT_FOLDER we can manually stat them by path.
const s = await fileOrFolder.vault.adapter.stat(fileOrFolder.path);
ctime = s.ctime;
mtime = s.mtime;
}
k1 = {
key: key, key: key,
ctime: 0, ctime: ctime,
mtime: 0, mtime: mtime,
size: 0, size: 0,
actionWhen: Date.now(), actionWhen: actionWhen,
actionType: "rename", actionType: "rename",
keyType: "folder", keyType: "folder",
renameTo: renameTo, renameTo: renameTo,
vaultRandomID: vaultRandomID, vaultRandomID: vaultRandomID,
}; };
k2 = {
key: renameTo,
ctime: ctime,
mtime: mtime,
size: 0,
actionWhen: actionWhen,
actionType: "renameDestination",
keyType: "folder",
renameTo: "", // itself is the destination, so no need to set this field
vaultRandomID: vaultRandomID,
};
} }
await db.deleteHistoryTbl.setItem(`${vaultRandomID}\t${k.key}`, k); await Promise.all([
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k1.key}`, k1),
db.fileHistoryTbl.setItem(`${vaultRandomID}\t${k2.key}`, k2),
]);
}; };
export const upsertSyncMetaMappingDataByVault = async ( export const upsertSyncMetaMappingDataByVault = async (
+98 -35
View File
@@ -1,6 +1,13 @@
import { Modal, Notice, Plugin, Setting, addIcon, setIcon } from "obsidian"; import {
Modal,
Notice,
Plugin,
Setting,
addIcon,
setIcon,
FileSystemAdapter,
} from "obsidian";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import { nanoid } from "nanoid";
import { createElement, RotateCcw, RefreshCcw } from "lucide"; import { createElement, RotateCcw, RefreshCcw } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes"; import type { RemotelySavePluginSettings } from "./baseTypes";
import { import {
@@ -10,13 +17,13 @@ import {
COMMAND_URI, COMMAND_URI,
} from "./baseTypes"; } from "./baseTypes";
import { importQrCodeUri } from "./importExport"; import { importQrCodeUri } from "./importExport";
import type { InternalDBs } from "./localdb";
import { import {
insertDeleteRecordByVault, insertDeleteRecordByVault,
insertRenameRecordByVault, insertRenameRecordByVault,
insertSyncPlanRecordByVault, insertSyncPlanRecordByVault,
loadDeleteRenameHistoryTableByVault, loadFileHistoryTableByVault,
prepareDBs, prepareDBs,
InternalDBs,
} from "./localdb"; } from "./localdb";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { import {
@@ -54,7 +61,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
password: "", password: "",
serviceType: "s3", serviceType: "s3",
currLogLevel: "info", currLogLevel: "info",
vaultRandomID: "", // vaultRandomID: "", // deprecated
autoRunEveryMilliseconds: -1, autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1, initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false, agreeToUploadExtraMetadata: false,
@@ -77,16 +84,25 @@ type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`; const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`; const iconNameSyncRunning = `remotely-save-sync-running`;
const getIconSvg = () => {
const iconSvgSyncWait = createElement(RotateCcw); const iconSvgSyncWait = createElement(RotateCcw);
iconSvgSyncWait.setAttribute("width", "100"); iconSvgSyncWait.setAttribute("width", "100");
iconSvgSyncWait.setAttribute("height", "100"); iconSvgSyncWait.setAttribute("height", "100");
const iconSvgSyncRunning = createElement(RefreshCcw); const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("width", "100"); iconSvgSyncRunning.setAttribute("width", "100");
iconSvgSyncRunning.setAttribute("height", "100"); iconSvgSyncRunning.setAttribute("height", "100");
const res = {
iconSvgSyncWait: iconSvgSyncWait.outerHTML,
iconSvgSyncRunning: iconSvgSyncRunning.outerHTML,
};
iconSvgSyncWait.empty();
iconSvgSyncRunning.empty();
return res;
};
export default class RemotelySavePlugin extends Plugin { export default class RemotelySavePlugin extends Plugin {
settings: RemotelySavePluginSettings; settings: RemotelySavePluginSettings;
// cm: CodeMirror.Editor;
db: InternalDBs; db: InternalDBs;
syncStatus: SyncStatusType; syncStatus: SyncStatusType;
oauth2Info: OAuth2Info; oauth2Info: OAuth2Info;
@@ -95,6 +111,7 @@ export default class RemotelySavePlugin extends Plugin {
syncRibbon?: HTMLElement; syncRibbon?: HTMLElement;
autoRunIntervalID?: number; autoRunIntervalID?: number;
i18n: I18n; i18n: I18n;
vaultRandomID: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") { async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
const t = (x: TransItemType, vars?: any) => { const t = (x: TransItemType, vars?: any) => {
@@ -207,7 +224,7 @@ export default class RemotelySavePlugin extends Plugin {
const { remoteStates, metadataFile } = await parseRemoteItems( const { remoteStates, metadataFile } = await parseRemoteItems(
remoteRsp.Contents, remoteRsp.Contents,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
client.serviceType, client.serviceType,
this.settings.password this.settings.password
); );
@@ -225,9 +242,9 @@ export default class RemotelySavePlugin extends Plugin {
); );
this.syncStatus = "getting_local_meta"; this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles(); const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadDeleteRenameHistoryTableByVault( const localHistory = await loadFileHistoryTableByVault(
this.db, this.db,
this.settings.vaultRandomID this.vaultRandomID
); );
let localConfigDirContents: ObsConfigDirFileType[] = undefined; let localConfigDirContents: ObsConfigDirFileType[] = undefined;
if (this.settings.syncConfigDir) { if (this.settings.syncConfigDir) {
@@ -261,11 +278,7 @@ export default class RemotelySavePlugin extends Plugin {
); );
log.info(plan.mixedStates); // for debugging log.info(plan.mixedStates); // for debugging
if (triggerSource !== "dry") { if (triggerSource !== "dry") {
await insertSyncPlanRecordByVault( await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
this.db,
plan,
this.settings.vaultRandomID
);
} }
// The operations above are almost read only and kind of safe. // The operations above are almost read only and kind of safe.
@@ -282,7 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
await doActualSync( await doActualSync(
client, client,
this.db, this.db,
this.settings.vaultRandomID, this.vaultRandomID,
this.app.vault, this.app.vault,
plan, plan,
sortedKeys, sortedKeys,
@@ -344,8 +357,10 @@ export default class RemotelySavePlugin extends Plugin {
async onload() { async onload() {
log.info(`loading plugin ${this.manifest.id}`); log.info(`loading plugin ${this.manifest.id}`);
addIcon(iconNameSyncWait, iconSvgSyncWait.outerHTML); const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg();
addIcon(iconNameSyncRunning, iconSvgSyncRunning.outerHTML);
addIcon(iconNameSyncWait, iconSvgSyncWait);
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
this.oauth2Info = { this.oauth2Info = {
verifier: "", verifier: "",
@@ -373,12 +388,27 @@ export default class RemotelySavePlugin extends Plugin {
} }
await this.checkIfOauthExpires(); await this.checkIfOauthExpires();
await this.checkIfVaultIDAssigned(); // MUST before prepareDB()
// MUST before prepareDB()
// And, it's also possible to be an empty string,
// which means the vaultRandomID is read from db later!
const vaultRandomIDFromOldConfigFile =
await this.getVaultRandomIDFromOldConfigFile();
// no need to await this // no need to await this
this.tryToAddIgnoreFile(); this.tryToAddIgnoreFile();
await this.prepareDB(); const vaultBasePath = this.getVaultBasePath();
try {
await this.prepareDBAndVaultRandomID(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
} catch (err) {
new Notice(err.message, 10 * 1000);
throw err;
}
this.syncStatus = "idle"; this.syncStatus = "idle";
@@ -387,7 +417,7 @@ export default class RemotelySavePlugin extends Plugin {
await insertDeleteRecordByVault( await insertDeleteRecordByVault(
this.db, this.db,
fileOrFolder, fileOrFolder,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -398,7 +428,7 @@ export default class RemotelySavePlugin extends Plugin {
this.db, this.db,
fileOrFolder, fileOrFolder,
oldPath, oldPath,
this.settings.vaultRandomID this.vaultRandomID
); );
}) })
); );
@@ -629,9 +659,13 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
onunload() { async onunload() {
log.info(`unloading plugin ${this.manifest.id}`); log.info(`unloading plugin ${this.manifest.id}`);
this.destroyDBs(); this.syncRibbon = undefined;
if (this.oauth2Info !== undefined) {
this.oauth2Info.helperModal = undefined;
this.oauth2Info = undefined;
}
} }
async loadSettings() { async loadSettings() {
@@ -643,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;
} }
@@ -737,14 +780,20 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async checkIfVaultIDAssigned() { async getVaultRandomIDFromOldConfigFile() {
if ( let vaultRandomID = "";
this.settings.vaultRandomID === undefined || if (this.settings.vaultRandomID !== undefined) {
this.settings.vaultRandomID === "" // In old version, the vault id is saved in data.json
) { // But we want to store it in localForage later
this.settings.vaultRandomID = nanoid(); if (this.settings.vaultRandomID !== "") {
// a real string was assigned before
vaultRandomID = this.settings.vaultRandomID;
}
log.debug("vaultRandomID is no longer saved in data.json");
delete this.settings.vaultRandomID;
await this.saveSettings(); await this.saveSettings();
} }
return vaultRandomID;
} }
async trash(x: string) { async trash(x: string) {
@@ -753,8 +802,26 @@ export default class RemotelySavePlugin extends Plugin {
} }
} }
async prepareDB() { getVaultBasePath() {
this.db = await prepareDBs(this.settings.vaultRandomID); if (this.app.vault.adapter instanceof FileSystemAdapter) {
// in desktop
return this.app.vault.adapter.getBasePath().split("?")[0];
} else {
// in mobile
return this.app.vault.adapter.getResourcePath("").split("?")[0];
}
}
async prepareDBAndVaultRandomID(
vaultBasePath: string,
vaultRandomIDFromOldConfigFile: string
) {
const { db, vaultRandomID } = await prepareDBs(
vaultBasePath,
vaultRandomIDFromOldConfigFile
);
this.db = db;
this.vaultRandomID = vaultRandomID;
} }
enableAutoSyncIfSet() { enableAutoSyncIfSet() {
@@ -792,10 +859,6 @@ export default class RemotelySavePlugin extends Plugin {
await this.saveSettings(); await this.saveSettings();
} }
destroyDBs() {
/* destroyDBs(this.db); */
}
async setCurrSyncMsg( async setCurrSyncMsg(
i: number, i: number,
totalCount: number, totalCount: number,
+4
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);
};
+6 -3
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 {
+40 -35
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 {
+53 -42
View File
@@ -11,6 +11,7 @@ import { request, requestUrl, requireApiVersion, Vault } from "obsidian";
import { import {
API_VER_REQURL, API_VER_REQURL,
COMMAND_CALLBACK_ONEDRIVE, COMMAND_CALLBACK_ONEDRIVE,
DEFAULT_CONTENT_TYPE,
OAUTH2_FORCE_EXPIRE_MILLISECONDS, OAUTH2_FORCE_EXPIRE_MILLISECONDS,
OnedriveConfig, OnedriveConfig,
RemoteItem, RemoteItem,
@@ -204,9 +205,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 +226,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 +249,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 +271,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 +370,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 +396,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`);
} }
} }
}; };
@@ -505,12 +506,17 @@ export class WrappedOnedriveClient {
putArrayBuffer = async (pathFragOrig: string, payload: ArrayBuffer) => { putArrayBuffer = async (pathFragOrig: string, payload: ArrayBuffer) => {
const theUrl = this.buildUrl(pathFragOrig); const theUrl = this.buildUrl(pathFragOrig);
log.debug(`putArrayBuffer, theUrl=${theUrl}`); 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({ await requestUrl({
url: theUrl, url: theUrl,
method: "PUT", method: "PUT",
body: payload, body: payload,
contentType: DEFAULT_CONTENT_TYPE,
headers: { headers: {
"Content-Type": DEFAULT_CONTENT_TYPE,
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
}, },
}); });
@@ -519,6 +525,7 @@ export class WrappedOnedriveClient {
method: "PUT", method: "PUT",
body: payload, body: payload,
headers: { headers: {
"Content-Type": DEFAULT_CONTENT_TYPE,
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`, Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
}, },
}); });
@@ -547,15 +554,18 @@ export class WrappedOnedriveClient {
}, len=${rangeEnd - rangeStart}, size=${size}` }, len=${rangeEnd - rangeStart}, size=${size}`
); );
// NO AUTH HEADER here! // 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({ const res = await requestUrl({
url: theUrl, url: theUrl,
method: "PUT", method: "PUT",
body: bufferToArrayBuffer(payload.subarray(rangeStart, rangeEnd)), body: bufferToArrayBuffer(payload.subarray(rangeStart, rangeEnd)),
contentType: DEFAULT_CONTENT_TYPE,
headers: { headers: {
// no "Content-Length" allowed here // no "Content-Length" allowed here
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`, "Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
"Content-Type": "application/octet-stream",
}, },
}); });
return res.json as DriveItem | UploadSession; return res.json as DriveItem | UploadSession;
@@ -566,7 +576,7 @@ export class WrappedOnedriveClient {
headers: { headers: {
"Content-Length": `${rangeEnd - rangeStart}`, "Content-Length": `${rangeEnd - rangeStart}`,
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`, "Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
"Content-Type": "application/octet-stream", "Content-Type": DEFAULT_CONTENT_TYPE,
}, },
}); });
return (await res.json()) as DriveItem | UploadSession; return (await res.json()) as DriveItem | UploadSession;
@@ -576,12 +586,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 +615,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 +632,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 +645,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 +674,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("/");
@@ -737,8 +747,9 @@ export const uploadToRemote = async (
// hard code range size // hard code range size
const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB
const RANGE_SIZE = MIN_UNIT * 20; // about 6.5536 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! // directly using put!
await client.putArrayBuffer( await client.putArrayBuffer(
`${uploadFile}:/content?${new URLSearchParams({ `${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 // 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 +803,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 +843,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 +871,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);
+8 -11
View File
@@ -22,15 +22,14 @@ import { buildQueryString } from "@aws-sdk/querystring-builder";
import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types"; import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import * as mime from "mime-types"; import * as mime from "mime-types";
import { import { Vault, requestUrl, RequestUrlParam } from "obsidian";
Vault,
requestUrl,
RequestUrlParam,
RequestUrlResponse,
requireApiVersion,
} from "obsidian";
import { Readable } from "stream"; 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 { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { import {
arrayBufferToBuffer, arrayBufferToBuffer,
@@ -189,7 +188,7 @@ export const getS3Client = (s3Config: S3Config) => {
endpoint = `https://${endpoint}`; endpoint = `https://${endpoint}`;
} }
if (requireApiVersion(API_VER_REQURL) && s3Config.bypassCorsLocally) { if (VALID_REQURL && s3Config.bypassCorsLocally) {
const s3Client = new S3Client({ const s3Client = new S3Client({
region: s3Config.s3Region, region: s3Config.s3Region,
endpoint: endpoint, endpoint: endpoint,
@@ -247,8 +246,6 @@ export const uploadToRemote = async (
} }
const isFolder = fileOrFolderPath.endsWith("/"); const isFolder = fileOrFolderPath.endsWith("/");
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
if (isFolder && isRecursively) { if (isFolder && isRecursively) {
throw Error("upload function doesn't implement recursive function yet!"); throw Error("upload function doesn't implement recursive function yet!");
} else if (isFolder && !isRecursively) { } else if (isFolder && !isRecursively) {
+45 -45
View File
@@ -1,17 +1,11 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { import { Vault, requestUrl } from "obsidian";
Vault,
request,
requestUrl,
requireApiVersion,
Platform,
} from "obsidian";
import { Queue } from "@fyears/tsqueue"; import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk"; import chunk from "lodash/chunk";
import flatten from "lodash/flatten"; import flatten from "lodash/flatten";
import { getReasonPhrase } from "http-status-codes"; 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 { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc"; import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
@@ -26,7 +20,7 @@ import type {
ResponseDataDetailed, ResponseDataDetailed,
} from "webdav/web"; } from "webdav/web";
import { getPatcher } from "webdav/web"; import { getPatcher } from "webdav/web";
if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) { if (VALID_REQURL) {
getPatcher().patch( getPatcher().patch(
"request", "request",
async ( async (
@@ -116,7 +110,6 @@ if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
// return r; // return r;
// }); // });
import { AuthType, BufferLike, createClient } from "webdav/web"; import { AuthType, BufferLike, createClient } from "webdav/web";
import { Plane } from "lucide";
export type { WebDAVClient } from "webdav/web"; export type { WebDAVClient } from "webdav/web";
export const DEFAULT_WEBDAV_CONFIG = { export const DEFAULT_WEBDAV_CONFIG = {
@@ -126,37 +119,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}/`;
} }
@@ -171,17 +167,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;
} }
@@ -211,13 +207,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;
} }
@@ -227,7 +223,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",
@@ -246,13 +242,16 @@ export class WrappedWebdavClient {
} }
if (!testPassed) { if (!testPassed) {
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: "1", Depth: "1",
}, },
responseType: "text", responseType: "text",
}); }
);
testPassed = true; testPassed = true;
this.webdavConfig.depth = "auto_1"; this.webdavConfig.depth = "auto_1";
this.webdavConfig.manualRecursive = true; this.webdavConfig.manualRecursive = true;
@@ -276,12 +275,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
); );
}; };
@@ -291,12 +290,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 (
@@ -314,7 +313,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("/");
@@ -328,7 +327,7 @@ export const uploadToRemote = async (
if (password === "") { if (password === "") {
// if not encrypted, mkdir a remote folder // if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, { await client.client.createDirectory(uploadFile, {
recursive: true, recursive: false, // the sync algo should guarantee no need to recursive
}); });
const res = await getRemoteMeta(client, uploadFile); const res = await getRemoteMeta(client, uploadFile);
return res; return res;
@@ -360,11 +359,12 @@ export const uploadToRemote = async (
if (password !== "") { if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password); remoteContent = await encryptArrayBuffer(localContent, password);
} }
// we need to create folders before uploading // updated 20220326: the algorithm guarantee this
const dir = getPathFolder(uploadFile); // // we need to create folders before uploading
if (dir !== "/" && dir !== "") { // const dir = getPathFolder(uploadFile);
await client.client.createDirectory(dir, { recursive: true }); // if (dir !== "/" && dir !== "") {
} // await client.client.createDirectory(dir, { recursive: false });
// }
await client.client.putFileContents(uploadFile, remoteContent, { await client.client.putFileContents(uploadFile, remoteContent, {
overwrite: true, overwrite: true,
onUploadProgress: (progress: any) => { onUploadProgress: (progress: any) => {
@@ -392,7 +392,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 = [];
@@ -427,7 +427,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 */,
@@ -440,7 +440,7 @@ export const listFromRemote = async (
} }
return { return {
Contents: contents.map((x) => Contents: contents.map((x) =>
fromWebdavItemToRemoteItem(x, client.vaultName) fromWebdavItemToRemoteItem(x, client.remoteBaseDir)
), ),
}; };
}; };
@@ -451,7 +451,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;
@@ -490,7 +490,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 !== "") {
@@ -518,7 +518,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 {
+248 -21
View File
@@ -10,6 +10,8 @@ import {
import { import {
API_VER_REQURL, API_VER_REQURL,
SUPPORTED_SERVICES_TYPE, SUPPORTED_SERVICES_TYPE,
SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR,
VALID_REQURL,
WebdavAuthType, WebdavAuthType,
WebdavDepthType, WebdavDepthType,
} from "./baseTypes"; } from "./baseTypes";
@@ -36,6 +38,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 +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 { class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin; readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement; readonly authDiv: HTMLDivElement;
@@ -651,7 +748,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
} }
if (!requireApiVersion(API_VER_REQURL)) { if (!VALID_REQURL) {
s3Div.createEl("p", { s3Div.createEl("p", {
text: t("settings_s3_cors"), text: t("settings_s3_cors"),
}); });
@@ -673,7 +770,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
text: t("settings_s3_prod2"), text: t("settings_s3_prod2"),
}); });
if (!requireApiVersion(API_VER_REQURL)) { if (!VALID_REQURL) {
s3LinksUl.createEl("li").createEl("a", { s3LinksUl.createEl("li").createEl("a", {
href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html", href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html",
text: t("settings_s3_prod3"), 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) new Setting(s3Div)
.setName(t("settings_s3_bypasscorslocally")) .setName(t("settings_s3_bypasscorslocally"))
.setDesc( .setDesc(
@@ -861,7 +958,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(),
}), }),
}); });
@@ -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) new Setting(dropboxAuthDiv)
.setName(t("settings_dropbox_auth")) .setName(t("settings_dropbox_auth"))
.setDesc(t("settings_dropbox_auth_desc")) .setDesc(t("settings_dropbox_auth_desc"))
@@ -945,6 +1066,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 +1145,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 +1212,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"))
@@ -1105,9 +1278,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.serviceType !== "webdav" this.plugin.settings.serviceType !== "webdav"
); );
const webdavReq =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
webdavDiv.createEl("h2", { text: t("settings_webdav") }); webdavDiv.createEl("h2", { text: t("settings_webdav") });
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
@@ -1115,7 +1285,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
cls: "webdav-disclaimer", cls: "webdav-disclaimer",
}); });
if (!webdavReq) { if (!VALID_REQURL) {
if (Platform.isAndroidApp) { if (Platform.isAndroidApp) {
webdavDiv.createEl("p", { webdavDiv.createEl("p", {
text: t("settings_webdav_cors_android"), text: t("settings_webdav_cors_android"),
@@ -1133,7 +1303,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(),
}), }),
}); });
@@ -1146,6 +1317,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.address) .setValue(this.plugin.settings.webdav.address)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.address = value.trim(); this.plugin.settings.webdav.address = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1159,6 +1336,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.username) .setValue(this.plugin.settings.webdav.username)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.username = value.trim(); this.plugin.settings.webdav.username = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1172,6 +1355,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.password) .setValue(this.plugin.settings.webdav.password)
.onChange(async (value) => { .onChange(async (value) => {
this.plugin.settings.webdav.password = value.trim(); this.plugin.settings.webdav.password = value.trim();
if (
this.plugin.settings.webdav.depth === "auto_1" ||
this.plugin.settings.webdav.depth === "auto_infinity"
) {
this.plugin.settings.webdav.depth = "auto_unknown";
}
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
@@ -1181,12 +1370,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setDesc(t("settings_webdav_auth_desc")) .setDesc(t("settings_webdav_auth_desc"))
.addDropdown(async (dropdown) => { .addDropdown(async (dropdown) => {
dropdown.addOption("basic", "basic"); dropdown.addOption("basic", "basic");
if (webdavReq) { if (VALID_REQURL) {
dropdown.addOption("digest", "digest"); dropdown.addOption("digest", "digest");
} }
// new version config, copied to old version, we need to reset it // 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"; this.plugin.settings.webdav.authType = "basic";
await this.plugin.saveSettings(); await this.plugin.saveSettings();
} }
@@ -1235,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) new Setting(webdavDiv)
.setName(t("settings_checkonnectivity")) .setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc")) .setDesc(t("settings_checkonnectivity_desc"))
@@ -1259,7 +1473,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
if (res) { if (res) {
new Notice(t("settings_webdav_connect_succ")); new Notice(t("settings_webdav_connect_succ"));
} else { } else {
if (webdavReq) { if (VALID_REQURL) {
new Notice(t("settings_webdav_connect_fail")); new Notice(t("settings_webdav_connect_fail"));
} else { } else {
new Notice(t("settings_webdav_connect_fail_withcors")); new Notice(t("settings_webdav_connect_fail_withcors"));
@@ -1376,15 +1590,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.onChange(async (val) => { .onChange(async (val) => {
if (val === "enable" && !bridge.secondConfirm) { if (val === "enable" && !bridge.secondConfirm) {
dropdown.setValue("disable"); dropdown.setValue("disable");
const modal = new SyncConfigDirModal( new SyncConfigDirModal(this.app, this.plugin, () => {
this.app,
this.plugin,
() => {
bridge.secondConfirm = true; bridge.secondConfirm = true;
dropdown.setValue("enable"); dropdown.setValue("enable");
} }).open();
);
modal.open();
} else { } else {
bridge.secondConfirm = false; bridge.secondConfirm = false;
this.plugin.settings.syncConfigDir = false; this.plugin.settings.syncConfigDir = false;
@@ -1466,7 +1675,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await exportVaultSyncPlansToFiles( await exportVaultSyncPlansToFiles(
this.plugin.db, this.plugin.db,
this.app.vault, this.app.vault,
this.plugin.settings.vaultRandomID this.plugin.vaultRandomID
); );
new Notice(t("settings_syncplans_notice")); new Notice(t("settings_syncplans_notice"));
}); });
@@ -1494,6 +1703,18 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
const outputCurrBasePathVaultIDDiv = debugDiv.createDiv("div");
new Setting(outputCurrBasePathVaultIDDiv)
.setName(t("settings_outputbasepathvaultid"))
.setDesc(t("settings_outputbasepathvaultid_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_outputbasepathvaultid_button"));
button.onClick(async () => {
new Notice(this.plugin.getVaultBasePath());
new Notice(this.plugin.vaultRandomID);
});
});
const dbsResetDiv = debugDiv.createEl("div"); const dbsResetDiv = debugDiv.createEl("div");
new Setting(dbsResetDiv) new Setting(dbsResetDiv)
.setName(t("settings_resetcache")) .setName(t("settings_resetcache"))
@@ -1506,4 +1727,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
} }
hide() {
let { containerEl } = this;
containerEl.empty();
super.hide();
}
} }
+68 -21
View File
@@ -218,7 +218,7 @@ export const parseRemoteItems = async (
mtimeRemote: backwardMapping.localMtime || entry.lastModified, mtimeRemote: backwardMapping.localMtime || entry.lastModified,
sizeRemote: backwardMapping.localSize || entry.size, sizeRemote: backwardMapping.localSize || entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: true, changeRemoteMtimeUsingMapping: true,
}; };
} else { } else {
r = { r = {
@@ -227,7 +227,7 @@ export const parseRemoteItems = async (
mtimeRemote: entry.lastModified, mtimeRemote: entry.lastModified,
sizeRemote: entry.size, sizeRemote: entry.size,
remoteEncryptedKey: remoteEncryptedKey, remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: false, changeRemoteMtimeUsingMapping: false,
}; };
} }
@@ -295,7 +295,7 @@ const ensembleMixedStates = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
syncConfigDir: boolean, syncConfigDir: boolean,
configDir: string, configDir: string,
syncUnderscoreItems: boolean syncUnderscoreItems: boolean
@@ -397,7 +397,7 @@ const ensembleMixedStates = async (
} }
} }
for (const entry of localDeleteHistory) { for (const entry of localFileHistory) {
let key = entry.key; let key = entry.key;
if (entry.keyType === "folder") { if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) { if (!entry.key.endsWith("/")) {
@@ -409,23 +409,45 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`); throw Error(`unexpected ${entry}`);
} }
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue;
}
if (entry.actionType === "delete" || entry.actionType === "rename") {
const r = { const r = {
key: key, key: key,
deltimeLocal: entry.actionWhen, deltimeLocal: entry.actionWhen,
} as FileOrFolderMixedState; } as FileOrFolderMixedState;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue;
}
if (results.hasOwnProperty(key)) { if (results.hasOwnProperty(key)) {
results[key].key = r.key;
results[key].deltimeLocal = r.deltimeLocal; results[key].deltimeLocal = r.deltimeLocal;
} else { } else {
results[key] = r; results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existLocal = false; results[key].existRemote = false; // we have already checked remote
results[key].existRemote = false; }
} else if (entry.actionType === "renameDestination") {
const r = {
key: key,
mtimeLocal: entry.actionWhen,
changeLocalMtimeUsingMapping: true,
};
if (results.hasOwnProperty(key)) {
results[key].mtimeLocal = Math.max(
r.mtimeLocal || 0,
results[key].mtimeLocal || 0
);
results[key].changeLocalMtimeUsingMapping =
r.changeLocalMtimeUsingMapping;
} else {
results[key] = r;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} else {
throw Error(
`do not know how to deal with local file history ${entry.key} with ${entry.actionType}`
);
} }
} }
@@ -480,12 +502,12 @@ const assignOperationToFileInplace = (
// 1. mtimeLocal // 1. mtimeLocal
if (r.existLocal) { if (r.existLocal) {
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeLocal >= mtimeRemote && r.mtimeLocal >= mtimeRemote &&
r.mtimeLocal >= deltimeLocal && r.mtimeLocal >= deltimeLocal &&
r.mtimeLocal >= deltime_remote r.mtimeLocal >= deltimeRemote
) { ) {
if (r.mtimeLocal === r.mtimeRemote) { if (r.mtimeLocal === r.mtimeRemote) {
// mtime the same // mtime the same
@@ -516,12 +538,12 @@ const assignOperationToFileInplace = (
// 2. mtimeRemote // 2. mtimeRemote
if (r.existRemote) { if (r.existRemote) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1; const deltimeLocal = r.deltimeLocal !== undefined ? r.deltimeLocal : -1;
if ( if (
r.mtimeRemote > mtimeLocal && r.mtimeRemote > mtimeLocal &&
r.mtimeRemote >= deltimeLocal && r.mtimeRemote >= deltimeLocal &&
r.mtimeRemote >= deltime_remote r.mtimeRemote >= deltimeRemote
) { ) {
r.decision = "downloadRemoteToLocal"; r.decision = "downloadRemoteToLocal";
r.decisionBranch = 5; r.decisionBranch = 5;
@@ -534,11 +556,11 @@ const assignOperationToFileInplace = (
if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) { if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
const deltime_remote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1; const deltimeRemote = r.deltimeRemote !== undefined ? r.deltimeRemote : -1;
if ( if (
r.deltimeLocal >= mtimeLocal && r.deltimeLocal >= mtimeLocal &&
r.deltimeLocal >= mtimeRemote && r.deltimeLocal >= mtimeRemote &&
r.deltimeLocal >= deltime_remote r.deltimeLocal >= deltimeRemote
) { ) {
r.decision = "uploadLocalDelHistToRemote"; r.decision = "uploadLocalDelHistToRemote";
r.decisionBranch = 6; r.decisionBranch = 6;
@@ -549,7 +571,7 @@ const assignOperationToFileInplace = (
} }
} }
// 4. deltime_remote // 4. deltimeRemote
if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) { if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -1; const mtimeLocal = r.existLocal ? r.mtimeLocal : -1;
const mtimeRemote = r.existRemote ? r.mtimeRemote : -1; const mtimeRemote = r.existRemote ? r.mtimeRemote : -1;
@@ -617,6 +639,31 @@ const assignOperationToFolderInplace = async (
} }
} }
// If it was moved to here, after deletion, we should keep it as is.
// The logic not necessarily needs API_VER_STAT_FOLDER.
// The folder needs this logic because it's also determined by file children.
// But the file do not need this logic because the mtimeLocal is checked firstly.
if (
r.existLocal &&
r.changeLocalMtimeUsingMapping &&
r.mtimeLocal > 0 &&
r.mtimeLocal > deltimeLocal &&
r.mtimeLocal > deltimeRemote
) {
keptFolder.add(getParentFolder(r.key));
if (r.existLocal && r.existRemote) {
r.decision = "skipFolder";
r.decisionBranch = 16;
} else if (r.existLocal || r.existRemote) {
r.decision = "createFolder";
r.decisionBranch = 17;
} else {
throw Error(
`Error: Folder ${r.key} doesn't exist locally and remotely but is marked must be kept. Abort.`
);
}
}
if (r.decision === undefined) { if (r.decision === undefined) {
// not yet decided by the above reason // not yet decided by the above reason
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) { if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
@@ -678,7 +725,7 @@ export const getSyncPlan = async (
local: TAbstractFile[], local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined, localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[], remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[], localFileHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE, remoteType: SUPPORTED_SERVICES_TYPE,
vault: Vault, vault: Vault,
syncConfigDir: boolean, syncConfigDir: boolean,
@@ -691,7 +738,7 @@ export const getSyncPlan = async (
local, local,
localConfigDirContents, localConfigDirContents,
remoteDeleteHistory, remoteDeleteHistory,
localDeleteHistory, localFileHistory,
syncConfigDir, syncConfigDir,
configDir, configDir,
syncUnderscoreItems syncUnderscoreItems
+19
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;
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"0.3.2": "0.12.15", "0.3.2": "0.12.15",
"0.3.13": "0.13.21" "0.3.16": "0.13.21"
} }