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
# Indentation override for all JS under lib directory
[*.{js,jsx,mjs,cjs,ts,tsx,json,py}]
[*.{js,jsx,mjs,cjs,ts,tsx,json}]
indent_style = space
indent_size = 2
+9 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report
description: File a bug report
description: Fire a bug report
title: "[Bug]: "
labels: ["bug"]
body:
@@ -55,6 +55,14 @@ body:
description: What version of Obsidian are you running?
validations:
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
id: ensure-no-sensitive-information
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)
[![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://remotely-save.github.io/auto-download-stats/latestVersionDownloads.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)
## Disclaimer
@@ -87,8 +85,8 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
### webdav
- 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 mobile < 1.1.1:
- 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 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.
- 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.
+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:
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)
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)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.3.13",
"version": "0.3.16",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "remotely-save",
"version": "0.3.13",
"version": "0.3.16",
"description": "This is yet another sync plugin for Obsidian app.",
"scripts": {
"dev2": "node esbuild.config.mjs",
@@ -38,7 +38,7 @@
"chai-as-promised": "^7.1.1",
"cross-env": "^7.0.3",
"dotenv": "^10.0.0",
"esbuild": "^0.14.3",
"esbuild": "^0.14.27",
"jsdom": "^19.0.0",
"mocha": "^9.1.3",
"prettier": "^2.4.1",
@@ -65,7 +65,6 @@
"assert": "^2.0.0",
"aws-crt": "^1.10.1",
"buffer": "^6.0.3",
"codemirror": "^5.63.1",
"crypto-browserify": "^3.12.0",
"dropbox": "^10.22.0",
"http-status-codes": "^2.2.0",
+20 -2
View File
@@ -3,10 +3,18 @@
* To avoid circular dependency.
*/
import { Platform, requireApiVersion } from "obsidian";
import type { LangType, LangTypeAndAuto } from "./i18n";
export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive";
export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR =
| "webdav"
| "dropbox"
| "onedrive";
export interface S3Config {
s3Endpoint: string;
s3Region: string;
@@ -27,6 +35,7 @@ export interface DropboxConfig {
accountID: string;
username: string;
credentialsShouldBeDeletedAtTime?: number;
remoteBaseDir?: string;
}
export type WebdavAuthType = "digest" | "basic";
@@ -44,6 +53,7 @@ export interface WebdavConfig {
authType: WebdavAuthType;
manualRecursive: boolean; // deprecated in 0.3.6, use depth
depth?: WebdavDepthType;
remoteBaseDir?: string;
}
export interface OnedriveConfig {
@@ -56,6 +66,7 @@ export interface OnedriveConfig {
deltaLink: string;
username: string;
credentialsShouldBeDeletedAtTime?: number;
remoteBaseDir?: string;
}
export interface RemotelySavePluginSettings {
@@ -66,7 +77,6 @@ export interface RemotelySavePluginSettings {
password: string;
serviceType: SUPPORTED_SERVICES_TYPE;
currLogLevel?: string;
vaultRandomID?: string;
autoRunEveryMilliseconds?: number;
initRunAfterMilliseconds?: number;
agreeToUploadExtraMetadata?: boolean;
@@ -74,6 +84,11 @@ export interface RemotelySavePluginSettings {
syncConfigDir?: boolean;
syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto;
/**
* @deprecated
*/
vaultRandomID?: string;
}
export interface RemoteItem {
@@ -124,7 +139,8 @@ export interface FileOrFolderMixedState {
deltimeRemote?: number;
sizeLocal?: number;
sizeRemote?: number;
changeMtimeUsingMapping?: boolean;
changeRemoteMtimeUsingMapping?: boolean;
changeLocalMtimeUsingMapping?: boolean;
decision?: DecisionType;
decisionBranch?: number;
syncDone?: "done";
@@ -133,3 +149,5 @@ export interface FileOrFolderMixedState {
export const API_VER_STAT_FOLDER = "0.13.27";
export const API_VER_REQURL = "0.13.26";
export const VALID_REQURL =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
+1 -1
View File
@@ -7,7 +7,7 @@ import * as origLog from "loglevel";
const log = origLog.getLogger("rs-default");
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 {
readme: string;
+163 -33
View File
@@ -1,21 +1,23 @@
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";
export type LocalForage = typeof localforage;
import * as origLog from "loglevel";
import { nanoid } from "nanoid";
const log = origLog.getLogger("rs-default");
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108];
export const DEFAULT_DB_VERSION_NUMBER: number = 20220108;
const DB_VERSION_NUMBER_IN_HISTORY = [20211114, 20220108, 20220326];
export const DEFAULT_DB_VERSION_NUMBER: number = 20220326;
export const DEFAULT_DB_NAME = "remotelysavedb";
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_SYNC_PLANS_HISTORY = "syncplanshistory";
export const DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING = "vaultrandomidmapping";
export interface FileFolderHistoryRecord {
key: string;
@@ -23,7 +25,7 @@ export interface FileFolderHistoryRecord {
mtime: number;
size: number;
actionWhen: number;
actionType: "delete" | "rename";
actionType: "delete" | "rename" | "renameDestination";
keyType: "folder" | "file";
renameTo: string;
vaultRandomID: string;
@@ -51,9 +53,10 @@ interface SyncPlanRecord {
export interface InternalDBs {
versionTbl: LocalForage;
deleteHistoryTbl: LocalForage;
fileHistoryTbl: LocalForage;
syncMappingTbl: LocalForage;
syncPlansTbl: LocalForage;
vaultRandomIDMappingTbl: LocalForage;
}
/**
@@ -72,12 +75,12 @@ const migrateDBsFrom20211114To20220108 = async (
const allPromisesToWait: Promise<any>[] = [];
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) {
if (key.startsWith(vaultRandomID)) {
continue;
}
const value = (await db.deleteHistoryTbl.getItem(
const value = (await db.fileHistoryTbl.getItem(
key
)) as FileFolderHistoryRecord;
if (value === null || value === undefined) {
@@ -87,8 +90,8 @@ const migrateDBsFrom20211114To20220108 = async (
value.vaultRandomID = vaultRandomID;
}
const newKey = `${vaultRandomID}\t${key}`;
allPromisesToWait.push(db.deleteHistoryTbl.setItem(newKey, value));
allPromisesToWait.push(db.deleteHistoryTbl.removeItem(key));
allPromisesToWait.push(db.fileHistoryTbl.setItem(newKey, value));
allPromisesToWait.push(db.fileHistoryTbl.removeItem(key));
}
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}`);
};
/**
* 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 (
db: InternalDBs,
oldVer: number,
@@ -148,19 +168,36 @@ const migrateDBs = async (
if (oldVer === 20211114 && newVer === 20220108) {
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
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 = {
versionTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VERSION,
}),
deleteHistoryTbl: localforage.createInstance({
fileHistoryTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_DELETE_HISTORY,
storeName: DEFAULT_TBL_FILE_HISTORY,
}),
syncMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
@@ -170,9 +207,42 @@ export const prepareDBs = async (vaultRandomID: string) => {
name: DEFAULT_DB_NAME,
storeName: DEFAULT_SYNC_PLANS_HISTORY,
}),
vaultRandomIDMappingTbl: localforage.createInstance({
name: DEFAULT_DB_NAME,
storeName: DEFAULT_TBL_VAULT_RANDOM_ID_MAPPING,
}),
} 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) {
log.debug(
`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");
return db;
return {
db: db,
vaultRandomID: vaultRandomID,
};
};
export const destroyDBs = async () => {
@@ -206,20 +279,20 @@ export const destroyDBs = async () => {
log.info("db deleted");
};
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) => {
console.error("tried to delete db but something bad!");
console.error(event);
log.error("tried to delete db but something goes wrong!");
log.error(event);
};
};
export const loadDeleteRenameHistoryTableByVault = async (
export const loadFileHistoryTableByVault = async (
db: InternalDBs,
vaultRandomID: string
) => {
const records = [] as FileFolderHistoryRecord[];
await db.deleteHistoryTbl.iterate((value, key, iterationNumber) => {
await db.fileHistoryTbl.iterate((value, key, iterationNumber) => {
if (key.startsWith(`${vaultRandomID}\t`)) {
records.push(value as FileFolderHistoryRecord);
}
@@ -233,7 +306,16 @@ export const clearDeleteRenameHistoryOfKeyAndVault = async (
key: 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 (
@@ -260,10 +342,12 @@ export const insertDeleteRecordByVault = async (
const key = fileOrFolder.path.endsWith("/")
? 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 = {
key: key,
ctime: 0,
mtime: 0,
ctime: ctime,
mtime: mtime,
size: 0,
actionWhen: Date.now(),
actionType: "delete",
@@ -272,9 +356,19 @@ export const insertDeleteRecordByVault = async (
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 (
db: InternalDBs,
fileOrFolder: TAbstractFile,
@@ -282,37 +376,73 @@ export const insertRenameRecordByVault = async (
vaultRandomID: string
) => {
// log.info(fileOrFolder);
let k: FileFolderHistoryRecord;
let k1: FileFolderHistoryRecord;
let k2: FileFolderHistoryRecord;
const actionWhen = Date.now();
if (fileOrFolder instanceof TFile) {
k = {
k1 = {
key: oldPath,
ctime: fileOrFolder.stat.ctime,
mtime: fileOrFolder.stat.mtime,
size: fileOrFolder.stat.size,
actionWhen: Date.now(),
actionWhen: actionWhen,
actionType: "rename",
keyType: "file",
renameTo: fileOrFolder.path,
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) {
const key = oldPath.endsWith("/") ? oldPath : `${oldPath}/`;
const renameTo = fileOrFolder.path.endsWith("/")
? 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,
ctime: 0,
mtime: 0,
ctime: ctime,
mtime: mtime,
size: 0,
actionWhen: Date.now(),
actionWhen: actionWhen,
actionType: "rename",
keyType: "folder",
renameTo: renameTo,
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 (
+104 -41
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 { nanoid } from "nanoid";
import { createElement, RotateCcw, RefreshCcw } from "lucide";
import type { RemotelySavePluginSettings } from "./baseTypes";
import {
@@ -10,13 +17,13 @@ import {
COMMAND_URI,
} from "./baseTypes";
import { importQrCodeUri } from "./importExport";
import type { InternalDBs } from "./localdb";
import {
insertDeleteRecordByVault,
insertRenameRecordByVault,
insertSyncPlanRecordByVault,
loadDeleteRenameHistoryTableByVault,
loadFileHistoryTableByVault,
prepareDBs,
InternalDBs,
} from "./localdb";
import { RemoteClient } from "./remote";
import {
@@ -54,7 +61,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
password: "",
serviceType: "s3",
currLogLevel: "info",
vaultRandomID: "",
// vaultRandomID: "", // deprecated
autoRunEveryMilliseconds: -1,
initRunAfterMilliseconds: -1,
agreeToUploadExtraMetadata: false,
@@ -77,16 +84,25 @@ type SyncTriggerSourceType = "manual" | "auto" | "dry" | "autoOnceInit";
const iconNameSyncWait = `remotely-save-sync-wait`;
const iconNameSyncRunning = `remotely-save-sync-running`;
const iconSvgSyncWait = createElement(RotateCcw);
iconSvgSyncWait.setAttribute("width", "100");
iconSvgSyncWait.setAttribute("height", "100");
const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("width", "100");
iconSvgSyncRunning.setAttribute("height", "100");
const getIconSvg = () => {
const iconSvgSyncWait = createElement(RotateCcw);
iconSvgSyncWait.setAttribute("width", "100");
iconSvgSyncWait.setAttribute("height", "100");
const iconSvgSyncRunning = createElement(RefreshCcw);
iconSvgSyncRunning.setAttribute("width", "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 {
settings: RemotelySavePluginSettings;
// cm: CodeMirror.Editor;
db: InternalDBs;
syncStatus: SyncStatusType;
oauth2Info: OAuth2Info;
@@ -95,6 +111,7 @@ export default class RemotelySavePlugin extends Plugin {
syncRibbon?: HTMLElement;
autoRunIntervalID?: number;
i18n: I18n;
vaultRandomID: string;
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
const t = (x: TransItemType, vars?: any) => {
@@ -207,7 +224,7 @@ export default class RemotelySavePlugin extends Plugin {
const { remoteStates, metadataFile } = await parseRemoteItems(
remoteRsp.Contents,
this.db,
this.settings.vaultRandomID,
this.vaultRandomID,
client.serviceType,
this.settings.password
);
@@ -225,9 +242,9 @@ export default class RemotelySavePlugin extends Plugin {
);
this.syncStatus = "getting_local_meta";
const local = this.app.vault.getAllLoadedFiles();
const localHistory = await loadDeleteRenameHistoryTableByVault(
const localHistory = await loadFileHistoryTableByVault(
this.db,
this.settings.vaultRandomID
this.vaultRandomID
);
let localConfigDirContents: ObsConfigDirFileType[] = undefined;
if (this.settings.syncConfigDir) {
@@ -261,11 +278,7 @@ export default class RemotelySavePlugin extends Plugin {
);
log.info(plan.mixedStates); // for debugging
if (triggerSource !== "dry") {
await insertSyncPlanRecordByVault(
this.db,
plan,
this.settings.vaultRandomID
);
await insertSyncPlanRecordByVault(this.db, plan, this.vaultRandomID);
}
// The operations above are almost read only and kind of safe.
@@ -282,7 +295,7 @@ export default class RemotelySavePlugin extends Plugin {
await doActualSync(
client,
this.db,
this.settings.vaultRandomID,
this.vaultRandomID,
this.app.vault,
plan,
sortedKeys,
@@ -344,8 +357,10 @@ export default class RemotelySavePlugin extends Plugin {
async onload() {
log.info(`loading plugin ${this.manifest.id}`);
addIcon(iconNameSyncWait, iconSvgSyncWait.outerHTML);
addIcon(iconNameSyncRunning, iconSvgSyncRunning.outerHTML);
const { iconSvgSyncWait, iconSvgSyncRunning } = getIconSvg();
addIcon(iconNameSyncWait, iconSvgSyncWait);
addIcon(iconNameSyncRunning, iconSvgSyncRunning);
this.oauth2Info = {
verifier: "",
@@ -373,12 +388,27 @@ export default class RemotelySavePlugin extends Plugin {
}
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
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";
@@ -387,7 +417,7 @@ export default class RemotelySavePlugin extends Plugin {
await insertDeleteRecordByVault(
this.db,
fileOrFolder,
this.settings.vaultRandomID
this.vaultRandomID
);
})
);
@@ -398,7 +428,7 @@ export default class RemotelySavePlugin extends Plugin {
this.db,
fileOrFolder,
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}`);
this.destroyDBs();
this.syncRibbon = undefined;
if (this.oauth2Info !== undefined) {
this.oauth2Info.helperModal = undefined;
this.oauth2Info = undefined;
}
}
async loadSettings() {
@@ -643,18 +677,27 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.dropbox.clientID === "") {
this.settings.dropbox.clientID = DEFAULT_SETTINGS.dropbox.clientID;
}
if (this.settings.dropbox.remoteBaseDir === undefined) {
this.settings.dropbox.remoteBaseDir = "";
}
if (this.settings.onedrive.clientID === "") {
this.settings.onedrive.clientID = DEFAULT_SETTINGS.onedrive.clientID;
}
if (this.settings.onedrive.authority === "") {
this.settings.onedrive.authority = DEFAULT_SETTINGS.onedrive.authority;
}
if (this.settings.onedrive.remoteBaseDir === undefined) {
this.settings.onedrive.remoteBaseDir = "";
}
if (this.settings.webdav.manualRecursive === undefined) {
this.settings.webdav.manualRecursive = false;
}
if (this.settings.webdav.depth === undefined) {
this.settings.webdav.depth = "auto_unknown";
}
if (this.settings.webdav.remoteBaseDir === undefined) {
this.settings.webdav.remoteBaseDir = "";
}
if (this.settings.s3.partsConcurrency === undefined) {
this.settings.s3.partsConcurrency = 20;
}
@@ -737,14 +780,20 @@ export default class RemotelySavePlugin extends Plugin {
}
}
async checkIfVaultIDAssigned() {
if (
this.settings.vaultRandomID === undefined ||
this.settings.vaultRandomID === ""
) {
this.settings.vaultRandomID = nanoid();
async getVaultRandomIDFromOldConfigFile() {
let vaultRandomID = "";
if (this.settings.vaultRandomID !== undefined) {
// In old version, the vault id is saved in data.json
// But we want to store it in localForage later
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();
}
return vaultRandomID;
}
async trash(x: string) {
@@ -753,8 +802,26 @@ export default class RemotelySavePlugin extends Plugin {
}
}
async prepareDB() {
this.db = await prepareDBs(this.settings.vaultRandomID);
getVaultBasePath() {
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() {
@@ -792,10 +859,6 @@ export default class RemotelySavePlugin extends Plugin {
await this.saveSettings();
}
destroyDBs() {
/* destroyDBs(this.db); */
}
async setCurrSyncMsg(
i: number,
totalCount: number,
+4
View File
@@ -299,3 +299,7 @@ export const atWhichLevel = (x: string) => {
}
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"
);
}
const remoteBaseDir = webdavConfig.remoteBaseDir || vaultName;
this.webdavConfig = webdavConfig;
this.webdavClient = webdav.getWebdavClient(
this.webdavConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
} else if (serviceType === "dropbox") {
@@ -58,10 +59,11 @@ export class RemoteClient {
"remember to provide vault name and callback while init dropbox client"
);
}
const remoteBaseDir = dropboxConfig.remoteBaseDir || vaultName;
this.dropboxConfig = dropboxConfig;
this.dropboxClient = dropbox.getDropboxClient(
this.dropboxConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
} else if (serviceType === "onedrive") {
@@ -70,10 +72,11 @@ export class RemoteClient {
"remember to provide vault name and callback while init onedrive client"
);
}
const remoteBaseDir = onedriveConfig.remoteBaseDir || vaultName;
this.onedriveConfig = onedriveConfig;
this.onedriveClient = onedrive.getOnedriveClient(
this.onedriveConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
} else {
+40 -35
View File
@@ -26,15 +26,18 @@ export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
credentialsShouldBeDeletedAtTime: 0,
};
export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
export const getDropboxPath = (
fileOrFolderPath: string,
remoteBaseDir: string
) => {
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `/${vaultName}`;
key = `/${remoteBaseDir}`;
}
if (!fileOrFolderPath.startsWith("/")) {
// then this is original path in Obsidian
key = `/${vaultName}/${fileOrFolderPath}`;
key = `/${remoteBaseDir}/${fileOrFolderPath}`;
}
if (key.endsWith("/")) {
key = key.slice(0, key.length - 1);
@@ -42,16 +45,18 @@ export const getDropboxPath = (fileOrFolderPath: string, vaultName: string) => {
return key;
};
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
if (
!(
fileOrFolderPath === `/${vaultName}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`)
fileOrFolderPath === `/${remoteBaseDir}` ||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
)
) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
throw Error(
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
);
}
return fileOrFolderPath.slice(`/${vaultName}/`.length);
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
};
const fromDropboxItemToRemoteItem = (
@@ -59,9 +64,9 @@ const fromDropboxItemToRemoteItem = (
| files.FileMetadataReference
| files.FolderMetadataReference
| files.DeletedMetadataReference,
vaultName: string
remoteBaseDir: string
): RemoteItem => {
let key = getNormPath(x.path_display, vaultName);
let key = getNormPath(x.path_display, remoteBaseDir);
if (x[".tag"] === "folder" && !key.endsWith("/")) {
key = `${key}/`;
}
@@ -268,17 +273,17 @@ export const setConfigBySuccessfullAuthInplace = async (
export class WrappedDropboxClient {
dropboxConfig: DropboxConfig;
vaultName: string;
remoteBaseDir: string;
saveUpdatedConfigFunc: () => Promise<any>;
dropbox: Dropbox;
vaultFolderExists: boolean;
constructor(
dropboxConfig: DropboxConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
this.dropboxConfig = dropboxConfig;
this.vaultName = vaultName;
this.remoteBaseDir = remoteBaseDir;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.vaultFolderExists = false;
}
@@ -318,29 +323,29 @@ export class WrappedDropboxClient {
}
// check vault folder
// log.info(`checking remote has folder /${this.vaultName}`);
// log.info(`checking remote has folder /${this.remoteBaseDir}`);
if (this.vaultFolderExists) {
// log.info(`already checked, /${this.vaultName} exist before`)
// log.info(`already checked, /${this.remoteBaseDir} exist before`)
} else {
const res = await this.dropbox.filesListFolder({
path: "",
recursive: false,
});
for (const item of res.result.entries) {
if (item.path_display === `/${this.vaultName}`) {
if (item.path_display === `/${this.remoteBaseDir}`) {
this.vaultFolderExists = true;
break;
}
}
if (!this.vaultFolderExists) {
log.info(`remote does not have folder /${this.vaultName}`);
log.info(`remote does not have folder /${this.remoteBaseDir}`);
await this.dropbox.filesCreateFolderV2({
path: `/${this.vaultName}`,
path: `/${this.remoteBaseDir}`,
});
log.info(`remote folder /${this.vaultName} created`);
log.info(`remote folder /${this.remoteBaseDir} created`);
this.vaultFolderExists = true;
} else {
// log.info(`remote folder /${this.vaultName} exists`);
// log.info(`remote folder /${this.remoteBaseDir} exists`);
}
}
@@ -354,12 +359,12 @@ export class WrappedDropboxClient {
*/
export const getDropboxClient = (
dropboxConfig: DropboxConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) => {
return new WrappedDropboxClient(
dropboxConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
};
@@ -374,7 +379,7 @@ export const getRemoteMeta = async (
// we instead try to list files
// if no error occurs, we ensemble a fake result.
const rsp = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`,
path: `/${client.remoteBaseDir}`,
recursive: false, // don't need to recursive here
});
if (rsp.status !== 200) {
@@ -389,7 +394,7 @@ export const getRemoteMeta = async (
} as RemoteItem;
}
const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesGetMetadata({
path: key,
@@ -397,7 +402,7 @@ export const getRemoteMeta = async (
if (rsp.status !== 200) {
throw Error(JSON.stringify(rsp));
}
return fromDropboxItemToRemoteItem(rsp.result, client.vaultName);
return fromDropboxItemToRemoteItem(rsp.result, client.remoteBaseDir);
};
export const uploadToRemote = async (
@@ -417,7 +422,7 @@ export const uploadToRemote = async (
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getDropboxPath(uploadFile, client.vaultName);
uploadFile = getDropboxPath(uploadFile, client.remoteBaseDir);
const isFolder = fileOrFolderPath.endsWith("/");
@@ -486,7 +491,7 @@ export const uploadToRemote = async (
// we want to mark that parent folders are created
if (foldersCreatedBefore !== undefined) {
const dirs = getFolderLevels(uploadFile).map((x) =>
getDropboxPath(x, client.vaultName)
getDropboxPath(x, client.remoteBaseDir)
);
for (const dir of dirs) {
foldersCreatedBefore?.add(dir);
@@ -505,7 +510,7 @@ export const listFromRemote = async (
}
await client.init();
let res = await client.dropbox.filesListFolder({
path: `/${client.vaultName}`,
path: `/${client.remoteBaseDir}`,
recursive: true,
include_deleted: false,
limit: 1000,
@@ -518,8 +523,8 @@ export const listFromRemote = async (
const contents = res.result.entries;
const unifiedContents = contents
.filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
.filter((x) => x.path_display !== `/${client.remoteBaseDir}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir));
while (res.result.has_more) {
res = await client.dropbox.filesListFolderContinue({
@@ -532,8 +537,8 @@ export const listFromRemote = async (
const contents2 = res.result.entries;
const unifiedContents2 = contents2
.filter((x) => x[".tag"] !== "deleted")
.filter((x) => x.path_display !== `/${client.vaultName}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.vaultName));
.filter((x) => x.path_display !== `/${client.remoteBaseDir}`)
.map((x) => fromDropboxItemToRemoteItem(x, client.remoteBaseDir));
unifiedContents.push(...unifiedContents2);
}
@@ -549,7 +554,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPath: string
) => {
await client.init();
const key = getDropboxPath(fileOrFolderPath, client.vaultName);
const key = getDropboxPath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.dropbox.filesDownload({
path: key,
});
@@ -595,7 +600,7 @@ export const downloadFromRemote = async (
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getDropboxPath(downloadFile, client.vaultName);
downloadFile = getDropboxPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
@@ -623,7 +628,7 @@ export const deleteFromRemote = async (
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getDropboxPath(remoteFileName, client.vaultName);
remoteFileName = getDropboxPath(remoteFileName, client.remoteBaseDir);
await client.init();
try {
+53 -42
View File
@@ -11,6 +11,7 @@ import { request, requestUrl, requireApiVersion, Vault } from "obsidian";
import {
API_VER_REQURL,
COMMAND_CALLBACK_ONEDRIVE,
DEFAULT_CONTENT_TYPE,
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
OnedriveConfig,
RemoteItem,
@@ -204,9 +205,9 @@ export const setConfigBySuccessfullAuthInplace = async (
// Other usual common methods
////////////////////////////////////////////////////////////////////////////////
const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => {
const getOnedrivePath = (fileOrFolderPath: string, remoteBaseDir: string) => {
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/special-folders-appfolder?view=odsp-graph-online
const prefix = `/drive/special/approot:/${vaultName}`;
const prefix = `/drive/special/approot:/${remoteBaseDir}`;
if (fileOrFolderPath.startsWith(prefix)) {
// already transformed, return as is
return fileOrFolderPath;
@@ -225,8 +226,8 @@ const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => {
return key;
};
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
const prefix = `/drive/special/approot:/${vaultName}`;
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
const prefix = `/drive/special/approot:/${remoteBaseDir}`;
if (
!(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`))
@@ -248,16 +249,16 @@ const constructFromDriveItemToRemoteItemError = (x: DriveItem) => {
const fromDriveItemToRemoteItem = (
x: DriveItem,
vaultName: string
remoteBaseDir: string
): RemoteItem => {
let key = "";
// possible prefix:
// pure english: /drive/root:/Apps/remotely-save/${vaultName}
// or localized, e.g.: /drive/root:/应用/remotely-save/${vaultName}
// pure english: /drive/root:/Apps/remotely-save/${remoteBaseDir}
// or localized, e.g.: /drive/root:/应用/remotely-save/${remoteBaseDir}
const FIRST_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/remotely-save\//g;
// or the root is absolute path /Livefolders,
// e.g.: /Livefolders/应用/remotely-save/${vaultName}
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
// another possibile prefix
@@ -270,26 +271,26 @@ const fromDriveItemToRemoteItem = (
);
if (
matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${vaultName}`)
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchFirstPrefixRes[0]}${vaultName}`;
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (
matchSecondPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${vaultName}`)
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchSecondPrefixRes[0]}${vaultName}`;
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(THIRD_COMMON_PREFIX_RAW)) {
// it's something like
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
// /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
// with uri encoded!
const parPath = decodeURIComponent(x.parentReference.path);
key = parPath.substring(parPath.indexOf(":") + 1);
if (key.startsWith(`/${vaultName}/`)) {
key = key.substring(`/${vaultName}/`.length);
if (key.startsWith(`/${remoteBaseDir}/`)) {
key = key.substring(`/${remoteBaseDir}/`.length);
key = `${key}/${x.name}`;
} else if (key === `/${vaultName}`) {
} else if (key === `/${remoteBaseDir}`) {
key = x.name;
} else {
throw Error(
@@ -369,17 +370,17 @@ class MyAuthProvider implements AuthenticationProvider {
export class WrappedOnedriveClient {
onedriveConfig: OnedriveConfig;
vaultName: string;
remoteBaseDir: string;
vaultFolderExists: boolean;
authGetter: MyAuthProvider;
saveUpdatedConfigFunc: () => Promise<any>;
constructor(
onedriveConfig: OnedriveConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
this.onedriveConfig = onedriveConfig;
this.vaultName = vaultName;
this.remoteBaseDir = remoteBaseDir;
this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.authGetter = new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc);
@@ -395,26 +396,26 @@ export class WrappedOnedriveClient {
}
// check vault folder
// log.info(`checking remote has folder /${this.vaultName}`);
// log.info(`checking remote has folder /${this.remoteBaseDir}`);
if (this.vaultFolderExists) {
// log.info(`already checked, /${this.vaultName} exist before`)
// log.info(`already checked, /${this.remoteBaseDir} exist before`)
} else {
const k = await this.getJson("/drive/special/approot/children");
log.debug(k);
this.vaultFolderExists =
(k.value as DriveItem[]).filter((x) => x.name === this.vaultName)
(k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir)
.length > 0;
if (!this.vaultFolderExists) {
log.info(`remote does not have folder /${this.vaultName}`);
log.info(`remote does not have folder /${this.remoteBaseDir}`);
await this.postJson("/drive/special/approot/children", {
name: `${this.vaultName}`,
name: `${this.remoteBaseDir}`,
folder: {},
"@microsoft.graph.conflictBehavior": "replace",
});
log.info(`remote folder /${this.vaultName} created`);
log.info(`remote folder /${this.remoteBaseDir} created`);
this.vaultFolderExists = true;
} else {
// log.info(`remote folder /${this.vaultName} exists`);
// log.info(`remote folder /${this.remoteBaseDir} exists`);
}
}
};
@@ -505,12 +506,17 @@ export class WrappedOnedriveClient {
putArrayBuffer = async (pathFragOrig: string, payload: ArrayBuffer) => {
const theUrl = this.buildUrl(pathFragOrig);
log.debug(`putArrayBuffer, theUrl=${theUrl}`);
if (requireApiVersion(API_VER_REQURL)) {
// TODO:
// 20220401: On Android, requestUrl has issue that text becomes base64.
// Use fetch everywhere instead!
if (false /*requireApiVersion(API_VER_REQURL)*/) {
await requestUrl({
url: theUrl,
method: "PUT",
body: payload,
contentType: DEFAULT_CONTENT_TYPE,
headers: {
"Content-Type": DEFAULT_CONTENT_TYPE,
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
},
});
@@ -519,6 +525,7 @@ export class WrappedOnedriveClient {
method: "PUT",
body: payload,
headers: {
"Content-Type": DEFAULT_CONTENT_TYPE,
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
},
});
@@ -547,15 +554,18 @@ export class WrappedOnedriveClient {
}, len=${rangeEnd - rangeStart}, size=${size}`
);
// NO AUTH HEADER here!
if (requireApiVersion(API_VER_REQURL)) {
// TODO:
// 20220401: On Android, requestUrl has issue that text becomes base64.
// Use fetch everywhere instead!
if (false /*requireApiVersion(API_VER_REQURL)*/) {
const res = await requestUrl({
url: theUrl,
method: "PUT",
body: bufferToArrayBuffer(payload.subarray(rangeStart, rangeEnd)),
contentType: DEFAULT_CONTENT_TYPE,
headers: {
// no "Content-Length" allowed here
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
"Content-Type": "application/octet-stream",
},
});
return res.json as DriveItem | UploadSession;
@@ -566,7 +576,7 @@ export class WrappedOnedriveClient {
headers: {
"Content-Length": `${rangeEnd - rangeStart}`,
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
"Content-Type": "application/octet-stream",
"Content-Type": DEFAULT_CONTENT_TYPE,
},
});
return (await res.json()) as DriveItem | UploadSession;
@@ -576,12 +586,12 @@ export class WrappedOnedriveClient {
export const getOnedriveClient = (
onedriveConfig: OnedriveConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) => {
return new WrappedOnedriveClient(
onedriveConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
};
@@ -605,7 +615,7 @@ export const listFromRemote = async (
const DELTA_LINK_KEY = "@odata.deltaLink";
let res = await client.getJson(
`/drive/special/approot:/${client.vaultName}:/delta`
`/drive/special/approot:/${client.remoteBaseDir}:/delta`
);
let driveItems = res.value as DriveItem[];
@@ -622,7 +632,7 @@ export const listFromRemote = async (
// unify everything to RemoteItem
const unifiedContents = driveItems
.map((x) => fromDriveItemToRemoteItem(x, client.vaultName))
.map((x) => fromDriveItemToRemoteItem(x, client.remoteBaseDir))
.filter((x) => x.key !== "/");
return {
@@ -635,14 +645,14 @@ export const getRemoteMeta = async (
fileOrFolderPath: string
) => {
await client.init();
const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName);
const remotePath = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath=${remotePath}`);
const rsp = await client.getJson(
`${remotePath}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size`
);
// log.info(rsp);
const driveItem = rsp as DriveItem;
const res = fromDriveItemToRemoteItem(driveItem, client.vaultName);
const res = fromDriveItemToRemoteItem(driveItem, client.remoteBaseDir);
// log.info(res);
return res;
};
@@ -664,7 +674,7 @@ export const uploadToRemote = async (
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getOnedrivePath(uploadFile, client.vaultName);
uploadFile = getOnedrivePath(uploadFile, client.remoteBaseDir);
log.debug(`uploadFile=${uploadFile}`);
const isFolder = fileOrFolderPath.endsWith("/");
@@ -737,8 +747,9 @@ export const uploadToRemote = async (
// hard code range size
const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB
const RANGE_SIZE = MIN_UNIT * 20; // about 6.5536 MB
const DIRECT_UPLOAD_MAX_SIZE = 1000 * 1000 * 4; // 4 Megabyte
if (remoteContent.byteLength <= RANGE_SIZE) {
if (remoteContent.byteLength < DIRECT_UPLOAD_MAX_SIZE) {
// directly using put!
await client.putArrayBuffer(
`${uploadFile}:/content?${new URLSearchParams({
@@ -751,7 +762,7 @@ export const uploadToRemote = async (
// ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
// 1. create uploadSession
// uploadFile already starts with /drive/special/approot:/${vaultName}
// uploadFile already starts with /drive/special/approot:/${remoteBaseDir}
const s: UploadSession = await client.postJson(
`${uploadFile}:/createUploadSession`,
{
@@ -792,7 +803,7 @@ const downloadFromRemoteRaw = async (
fileOrFolderPath: string
): Promise<ArrayBuffer> => {
await client.init();
const key = getOnedrivePath(fileOrFolderPath, client.vaultName);
const key = getOnedrivePath(fileOrFolderPath, client.remoteBaseDir);
const rsp = await client.getJson(
`${key}?$select=@microsoft.graph.downloadUrl`
);
@@ -832,7 +843,7 @@ export const downloadFromRemote = async (
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getOnedrivePath(downloadFile, client.vaultName);
downloadFile = getOnedrivePath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
@@ -860,7 +871,7 @@ export const deleteFromRemote = async (
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getOnedrivePath(remoteFileName, client.vaultName);
remoteFileName = getOnedrivePath(remoteFileName, client.remoteBaseDir);
await client.init();
await client.deleteJson(remoteFileName);
+8 -11
View File
@@ -22,15 +22,14 @@ import { buildQueryString } from "@aws-sdk/querystring-builder";
import { HeaderBag, HttpHandlerOptions, Provider } from "@aws-sdk/types";
import { Buffer } from "buffer";
import * as mime from "mime-types";
import {
Vault,
requestUrl,
RequestUrlParam,
RequestUrlResponse,
requireApiVersion,
} from "obsidian";
import { Vault, requestUrl, RequestUrlParam } from "obsidian";
import { Readable } from "stream";
import { API_VER_REQURL, RemoteItem, S3Config } from "./baseTypes";
import {
DEFAULT_CONTENT_TYPE,
RemoteItem,
S3Config,
VALID_REQURL,
} from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import {
arrayBufferToBuffer,
@@ -189,7 +188,7 @@ export const getS3Client = (s3Config: S3Config) => {
endpoint = `https://${endpoint}`;
}
if (requireApiVersion(API_VER_REQURL) && s3Config.bypassCorsLocally) {
if (VALID_REQURL && s3Config.bypassCorsLocally) {
const s3Client = new S3Client({
region: s3Config.s3Region,
endpoint: endpoint,
@@ -247,8 +246,6 @@ export const uploadToRemote = async (
}
const isFolder = fileOrFolderPath.endsWith("/");
const DEFAULT_CONTENT_TYPE = "application/octet-stream";
if (isFolder && isRecursively) {
throw Error("upload function doesn't implement recursive function yet!");
} else if (isFolder && !isRecursively) {
+45 -45
View File
@@ -1,17 +1,11 @@
import { Buffer } from "buffer";
import {
Vault,
request,
requestUrl,
requireApiVersion,
Platform,
} from "obsidian";
import { Vault, requestUrl } from "obsidian";
import { Queue } from "@fyears/tsqueue";
import chunk from "lodash/chunk";
import flatten from "lodash/flatten";
import { getReasonPhrase } from "http-status-codes";
import { API_VER_REQURL, RemoteItem, WebdavConfig } from "./baseTypes";
import { RemoteItem, VALID_REQURL, WebdavConfig } from "./baseTypes";
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
import { bufferToArrayBuffer, getPathFolder, mkdirpInVault } from "./misc";
@@ -26,7 +20,7 @@ import type {
ResponseDataDetailed,
} from "webdav/web";
import { getPatcher } from "webdav/web";
if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
if (VALID_REQURL) {
getPatcher().patch(
"request",
async (
@@ -116,7 +110,6 @@ if (requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp) {
// return r;
// });
import { AuthType, BufferLike, createClient } from "webdav/web";
import { Plane } from "lucide";
export type { WebDAVClient } from "webdav/web";
export const DEFAULT_WEBDAV_CONFIG = {
@@ -126,37 +119,40 @@ export const DEFAULT_WEBDAV_CONFIG = {
authType: "basic",
manualRecursive: false,
depth: "auto_unknown",
remoteBaseDir: "",
} as WebdavConfig;
const getWebdavPath = (fileOrFolderPath: string, vaultName: string) => {
const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
let key = fileOrFolderPath;
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
// special
key = `/${vaultName}/`;
key = `/${remoteBaseDir}/`;
}
if (!fileOrFolderPath.startsWith("/")) {
key = `/${vaultName}/${fileOrFolderPath}`;
key = `/${remoteBaseDir}/${fileOrFolderPath}`;
}
return key;
};
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
if (
!(
fileOrFolderPath === `/${vaultName}` ||
fileOrFolderPath.startsWith(`/${vaultName}/`)
fileOrFolderPath === `/${remoteBaseDir}` ||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
)
) {
throw Error(`"${fileOrFolderPath}" doesn't starts with "/${vaultName}/"`);
throw Error(
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
);
}
// if (fileOrFolderPath.startsWith("/")) {
// return fileOrFolderPath.slice(1);
// }
return fileOrFolderPath.slice(`/${vaultName}/`.length);
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
};
const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
let key = getNormPath(x.filename, vaultName);
const fromWebdavItemToRemoteItem = (x: FileStat, remoteBaseDir: string) => {
let key = getNormPath(x.filename, remoteBaseDir);
if (x.type === "directory" && !key.endsWith("/")) {
key = `${key}/`;
}
@@ -171,17 +167,17 @@ const fromWebdavItemToRemoteItem = (x: FileStat, vaultName: string) => {
export class WrappedWebdavClient {
webdavConfig: WebdavConfig;
vaultName: string;
remoteBaseDir: string;
client: WebDAVClient;
vaultFolderExists: boolean;
saveUpdatedConfigFunc: () => Promise<any>;
constructor(
webdavConfig: WebdavConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) {
this.webdavConfig = webdavConfig;
this.vaultName = vaultName;
this.remoteBaseDir = remoteBaseDir;
this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
}
@@ -211,13 +207,13 @@ export class WrappedWebdavClient {
if (this.vaultFolderExists) {
// pass
} else {
const res = await this.client.exists(`/${this.vaultName}/`);
const res = await this.client.exists(`/${this.remoteBaseDir}/`);
if (res) {
// log.info("remote vault folder exits!");
this.vaultFolderExists = true;
} else {
log.info("remote vault folder not exists, creating");
await this.client.createDirectory(`/${this.vaultName}`);
await this.client.createDirectory(`/${this.remoteBaseDir}/`);
log.info("remote vault folder created!");
this.vaultFolderExists = true;
}
@@ -227,7 +223,7 @@ export class WrappedWebdavClient {
if (this.webdavConfig.depth === "auto_unknown") {
let testPassed = false;
try {
const res = await this.client.customRequest(`/${this.vaultName}`, {
const res = await this.client.customRequest(`/${this.remoteBaseDir}/`, {
method: "PROPFIND",
headers: {
Depth: "infinity",
@@ -246,13 +242,16 @@ export class WrappedWebdavClient {
}
if (!testPassed) {
try {
const res = await this.client.customRequest(`/${this.vaultName}`, {
const res = await this.client.customRequest(
`/${this.remoteBaseDir}/`,
{
method: "PROPFIND",
headers: {
Depth: "1",
},
responseType: "text",
});
}
);
testPassed = true;
this.webdavConfig.depth = "auto_1";
this.webdavConfig.manualRecursive = true;
@@ -276,12 +275,12 @@ export class WrappedWebdavClient {
export const getWebdavClient = (
webdavConfig: WebdavConfig,
vaultName: string,
remoteBaseDir: string,
saveUpdatedConfigFunc: () => Promise<any>
) => {
return new WrappedWebdavClient(
webdavConfig,
vaultName,
remoteBaseDir,
saveUpdatedConfigFunc
);
};
@@ -291,12 +290,12 @@ export const getRemoteMeta = async (
fileOrFolderPath: string
) => {
await client.init();
const remotePath = getWebdavPath(fileOrFolderPath, client.vaultName);
const remotePath = getWebdavPath(fileOrFolderPath, client.remoteBaseDir);
// log.info(`remotePath = ${remotePath}`);
const res = (await client.client.stat(remotePath, {
details: false,
})) as FileStat;
return fromWebdavItemToRemoteItem(res, client.vaultName);
return fromWebdavItemToRemoteItem(res, client.remoteBaseDir);
};
export const uploadToRemote = async (
@@ -314,7 +313,7 @@ export const uploadToRemote = async (
if (password !== "") {
uploadFile = remoteEncryptedKey;
}
uploadFile = getWebdavPath(uploadFile, client.vaultName);
uploadFile = getWebdavPath(uploadFile, client.remoteBaseDir);
const isFolder = fileOrFolderPath.endsWith("/");
@@ -328,7 +327,7 @@ export const uploadToRemote = async (
if (password === "") {
// if not encrypted, mkdir a remote folder
await client.client.createDirectory(uploadFile, {
recursive: true,
recursive: false, // the sync algo should guarantee no need to recursive
});
const res = await getRemoteMeta(client, uploadFile);
return res;
@@ -360,11 +359,12 @@ export const uploadToRemote = async (
if (password !== "") {
remoteContent = await encryptArrayBuffer(localContent, password);
}
// we need to create folders before uploading
const dir = getPathFolder(uploadFile);
if (dir !== "/" && dir !== "") {
await client.client.createDirectory(dir, { recursive: true });
}
// updated 20220326: the algorithm guarantee this
// // we need to create folders before uploading
// const dir = getPathFolder(uploadFile);
// if (dir !== "/" && dir !== "") {
// await client.client.createDirectory(dir, { recursive: false });
// }
await client.client.putFileContents(uploadFile, remoteContent, {
overwrite: true,
onUploadProgress: (progress: any) => {
@@ -392,7 +392,7 @@ export const listFromRemote = async (
) {
// the remote doesn't support infinity propfind,
// we need to do a bfs here
const q = new Queue([`/${client.vaultName}`]);
const q = new Queue([`/${client.remoteBaseDir}`]);
const CHUNK_SIZE = 10;
while (q.length > 0) {
const itemsToFetch = [];
@@ -427,7 +427,7 @@ export const listFromRemote = async (
} else {
// the remote supports infinity propfind
contents = (await client.client.getDirectoryContents(
`/${client.vaultName}`,
`/${client.remoteBaseDir}`,
{
deep: true,
details: false /* no need for verbose details here */,
@@ -440,7 +440,7 @@ export const listFromRemote = async (
}
return {
Contents: contents.map((x) =>
fromWebdavItemToRemoteItem(x, client.vaultName)
fromWebdavItemToRemoteItem(x, client.remoteBaseDir)
),
};
};
@@ -451,7 +451,7 @@ const downloadFromRemoteRaw = async (
) => {
await client.init();
const buff = (await client.client.getFileContents(
getWebdavPath(fileOrFolderPath, client.vaultName)
getWebdavPath(fileOrFolderPath, client.remoteBaseDir)
)) as BufferLike;
if (buff instanceof ArrayBuffer) {
return buff;
@@ -490,7 +490,7 @@ export const downloadFromRemote = async (
if (password !== "") {
downloadFile = remoteEncryptedKey;
}
downloadFile = getWebdavPath(downloadFile, client.vaultName);
downloadFile = getWebdavPath(downloadFile, client.remoteBaseDir);
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
let localContent = remoteContent;
if (password !== "") {
@@ -518,7 +518,7 @@ export const deleteFromRemote = async (
if (password !== "") {
remoteFileName = remoteEncryptedKey;
}
remoteFileName = getWebdavPath(remoteFileName, client.vaultName);
remoteFileName = getWebdavPath(remoteFileName, client.remoteBaseDir);
await client.init();
try {
+248 -21
View File
@@ -10,6 +10,8 @@ import {
import {
API_VER_REQURL,
SUPPORTED_SERVICES_TYPE,
SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR,
VALID_REQURL,
WebdavAuthType,
WebdavDepthType,
} from "./baseTypes";
@@ -36,6 +38,7 @@ import { messyConfigToNormal } from "./configPersist";
import type { TransItemType } from "./i18n";
import * as origLog from "loglevel";
import { checkHasSpecialCharForDir } from "./misc";
const log = origLog.getLogger("rs-default");
class PasswordModal extends Modal {
@@ -108,6 +111,100 @@ class PasswordModal extends Modal {
}
}
class ChangeRemoteBaseDirModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly newRemoteBaseDir: string;
readonly service: SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR;
constructor(
app: App,
plugin: RemotelySavePlugin,
newRemoteBaseDir: string,
service: SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR
) {
super(app);
this.plugin = plugin;
this.newRemoteBaseDir = newRemoteBaseDir;
this.service = service;
}
onOpen() {
let { contentEl } = this;
const t = (x: TransItemType, vars?: any) => {
return this.plugin.i18n.t(x, vars);
};
contentEl.createEl("h2", { text: t("modal_remotebasedir_title") });
t("modal_remotebasedir_shortdesc")
.split("\n")
.forEach((val, idx) => {
contentEl.createEl("p", {
text: val,
});
});
if (
this.newRemoteBaseDir === "" ||
this.newRemoteBaseDir === this.app.vault.getName()
) {
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(
t("modal_remotebasedir_secondconfirm_vaultname")
);
button.onClick(async () => {
// in the settings, the value is reset to the special case ""
this.plugin.settings[this.service].remoteBaseDir = "";
await this.plugin.saveSettings();
new Notice(t("modal_remotebasedir_notice"));
this.close();
});
button.setClass("remotebasedir-second-confirm");
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
} else if (checkHasSpecialCharForDir(this.newRemoteBaseDir)) {
contentEl.createEl("p", {
text: t("modal_remotebasedir_invaliddirhint"),
});
new Setting(contentEl).addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
} else {
new Setting(contentEl)
.addButton((button) => {
button.setButtonText(t("modal_remotebasedir_secondconfirm_change"));
button.onClick(async () => {
this.plugin.settings[this.service].remoteBaseDir =
this.newRemoteBaseDir;
await this.plugin.saveSettings();
new Notice(t("modal_remotebasedir_notice"));
this.close();
});
button.setClass("remotebasedir-second-confirm");
})
.addButton((button) => {
button.setButtonText(t("goback"));
button.onClick(() => {
this.close();
});
});
}
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
@@ -651,7 +748,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
}
if (!requireApiVersion(API_VER_REQURL)) {
if (!VALID_REQURL) {
s3Div.createEl("p", {
text: t("settings_s3_cors"),
});
@@ -673,7 +770,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
text: t("settings_s3_prod2"),
});
if (!requireApiVersion(API_VER_REQURL)) {
if (!VALID_REQURL) {
s3LinksUl.createEl("li").createEl("a", {
href: "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html",
text: t("settings_s3_prod3"),
@@ -766,7 +863,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
if (requireApiVersion(API_VER_REQURL)) {
if (VALID_REQURL) {
new Setting(s3Div)
.setName(t("settings_s3_bypasscorslocally"))
.setDesc(
@@ -861,7 +958,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropboxDiv.createEl("p", {
text: t("settings_dropbox_folder", {
pluginID: this.plugin.manifest.id,
vaultName: this.app.vault.getName(),
remoteBaseDir:
this.plugin.settings.dropbox.remoteBaseDir ||
this.app.vault.getName(),
}),
});
@@ -915,6 +1014,28 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
});
});
new Setting(dropboxRevokeAuthDiv)
.setName(t("settings_dropbox_clearlocal"))
.setDesc(t("settings_dropbox_clearlocal_desc"))
.addButton(async (button) => {
button.setButtonText(t("settings_dropbox_clearlocal_button"));
button.onClick(async () => {
this.plugin.settings.dropbox = JSON.parse(
JSON.stringify(DEFAULT_DROPBOX_CONFIG)
);
await this.plugin.saveSettings();
dropboxAuthDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
dropboxRevokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
new Notice(t("settings_dropbox_clearlocal_notice"));
});
});
new Setting(dropboxAuthDiv)
.setName(t("settings_dropbox_auth"))
.setDesc(t("settings_dropbox_auth_desc"))
@@ -945,6 +1066,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.dropbox.username === ""
);
let newDropboxRemoteBaseDir =
this.plugin.settings.dropbox.remoteBaseDir || "";
new Setting(dropboxDiv)
.setName(t("settings_remotebasedir"))
.setDesc(t("settings_remotebasedir_desc"))
.addText((text) =>
text
.setPlaceholder(this.app.vault.getName())
.setValue(newDropboxRemoteBaseDir)
.onChange((value) => {
newDropboxRemoteBaseDir = value.trim();
})
)
.addButton((button) => {
button.setButtonText(t("confirm"));
button.onClick(() => {
new ChangeRemoteBaseDirModal(
this.app,
this.plugin,
newDropboxRemoteBaseDir,
"dropbox"
).open();
});
});
new Setting(dropboxDiv)
.setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc"))
@@ -999,7 +1145,9 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
onedriveDiv.createEl("p", {
text: t("settings_onedrive_folder", {
pluginID: this.plugin.manifest.id,
vaultName: this.app.vault.getName(),
remoteBaseDir:
this.plugin.settings.onedrive.remoteBaseDir ||
this.app.vault.getName(),
}),
});
@@ -1064,6 +1212,31 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.onedrive.username === ""
);
let newOnedriveRemoteBaseDir =
this.plugin.settings.onedrive.remoteBaseDir || "";
new Setting(onedriveDiv)
.setName(t("settings_remotebasedir"))
.setDesc(t("settings_remotebasedir_desc"))
.addText((text) =>
text
.setPlaceholder(this.app.vault.getName())
.setValue(newOnedriveRemoteBaseDir)
.onChange((value) => {
newOnedriveRemoteBaseDir = value.trim();
})
)
.addButton((button) => {
button.setButtonText(t("confirm"));
button.onClick(() => {
new ChangeRemoteBaseDirModal(
this.app,
this.plugin,
newOnedriveRemoteBaseDir,
"onedrive"
).open();
});
});
new Setting(onedriveDiv)
.setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc"))
@@ -1105,9 +1278,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
this.plugin.settings.serviceType !== "webdav"
);
const webdavReq =
requireApiVersion(API_VER_REQURL) && !Platform.isAndroidApp;
webdavDiv.createEl("h2", { text: t("settings_webdav") });
webdavDiv.createEl("p", {
@@ -1115,7 +1285,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
cls: "webdav-disclaimer",
});
if (!webdavReq) {
if (!VALID_REQURL) {
if (Platform.isAndroidApp) {
webdavDiv.createEl("p", {
text: t("settings_webdav_cors_android"),
@@ -1133,7 +1303,8 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
webdavDiv.createEl("p", {
text: t("settings_webdav_folder", {
vaultName: this.app.vault.getName(),
remoteBaseDir:
this.plugin.settings.webdav.remoteBaseDir || this.app.vault.getName(),
}),
});
@@ -1146,6 +1317,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.address)
.onChange(async (value) => {
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();
})
);
@@ -1159,6 +1336,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.username)
.onChange(async (value) => {
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();
})
);
@@ -1172,6 +1355,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.webdav.password)
.onChange(async (value) => {
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();
})
);
@@ -1181,12 +1370,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.setDesc(t("settings_webdav_auth_desc"))
.addDropdown(async (dropdown) => {
dropdown.addOption("basic", "basic");
if (webdavReq) {
if (VALID_REQURL) {
dropdown.addOption("digest", "digest");
}
// new version config, copied to old version, we need to reset it
if (!webdavReq && this.plugin.settings.webdav.authType !== "basic") {
if (!VALID_REQURL && this.plugin.settings.webdav.authType !== "basic") {
this.plugin.settings.webdav.authType = "basic";
await this.plugin.saveSettings();
}
@@ -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)
.setName(t("settings_checkonnectivity"))
.setDesc(t("settings_checkonnectivity_desc"))
@@ -1259,7 +1473,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
if (res) {
new Notice(t("settings_webdav_connect_succ"));
} else {
if (webdavReq) {
if (VALID_REQURL) {
new Notice(t("settings_webdav_connect_fail"));
} else {
new Notice(t("settings_webdav_connect_fail_withcors"));
@@ -1376,15 +1590,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
.onChange(async (val) => {
if (val === "enable" && !bridge.secondConfirm) {
dropdown.setValue("disable");
const modal = new SyncConfigDirModal(
this.app,
this.plugin,
() => {
new SyncConfigDirModal(this.app, this.plugin, () => {
bridge.secondConfirm = true;
dropdown.setValue("enable");
}
);
modal.open();
}).open();
} else {
bridge.secondConfirm = false;
this.plugin.settings.syncConfigDir = false;
@@ -1466,7 +1675,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
await exportVaultSyncPlansToFiles(
this.plugin.db,
this.app.vault,
this.plugin.settings.vaultRandomID
this.plugin.vaultRandomID
);
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");
new Setting(dbsResetDiv)
.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,
sizeRemote: backwardMapping.localSize || entry.size,
remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: true,
changeRemoteMtimeUsingMapping: true,
};
} else {
r = {
@@ -227,7 +227,7 @@ export const parseRemoteItems = async (
mtimeRemote: entry.lastModified,
sizeRemote: entry.size,
remoteEncryptedKey: remoteEncryptedKey,
changeMtimeUsingMapping: false,
changeRemoteMtimeUsingMapping: false,
};
}
@@ -295,7 +295,7 @@ const ensembleMixedStates = async (
local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[],
localFileHistory: FileFolderHistoryRecord[],
syncConfigDir: boolean,
configDir: string,
syncUnderscoreItems: boolean
@@ -397,7 +397,7 @@ const ensembleMixedStates = async (
}
}
for (const entry of localDeleteHistory) {
for (const entry of localFileHistory) {
let key = entry.key;
if (entry.keyType === "folder") {
if (!entry.key.endsWith("/")) {
@@ -409,23 +409,45 @@ const ensembleMixedStates = async (
throw Error(`unexpected ${entry}`);
}
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue;
}
if (entry.actionType === "delete" || entry.actionType === "rename") {
const r = {
key: key,
deltimeLocal: entry.actionWhen,
} as FileOrFolderMixedState;
if (isSkipItem(key, syncConfigDir, syncUnderscoreItems, configDir)) {
continue;
}
if (results.hasOwnProperty(key)) {
results[key].key = r.key;
results[key].deltimeLocal = r.deltimeLocal;
} else {
results[key] = r;
results[key].existLocal = false;
results[key].existRemote = false;
results[key].existLocal = false; // we have already checked local
results[key].existRemote = false; // we have already checked remote
}
} 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
if (r.existLocal) {
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;
if (
r.mtimeLocal >= mtimeRemote &&
r.mtimeLocal >= deltimeLocal &&
r.mtimeLocal >= deltime_remote
r.mtimeLocal >= deltimeRemote
) {
if (r.mtimeLocal === r.mtimeRemote) {
// mtime the same
@@ -516,12 +538,12 @@ const assignOperationToFileInplace = (
// 2. mtimeRemote
if (r.existRemote) {
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;
if (
r.mtimeRemote > mtimeLocal &&
r.mtimeRemote >= deltimeLocal &&
r.mtimeRemote >= deltime_remote
r.mtimeRemote >= deltimeRemote
) {
r.decision = "downloadRemoteToLocal";
r.decisionBranch = 5;
@@ -534,11 +556,11 @@ const assignOperationToFileInplace = (
if (r.deltimeLocal !== undefined && r.deltimeLocal !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -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 (
r.deltimeLocal >= mtimeLocal &&
r.deltimeLocal >= mtimeRemote &&
r.deltimeLocal >= deltime_remote
r.deltimeLocal >= deltimeRemote
) {
r.decision = "uploadLocalDelHistToRemote";
r.decisionBranch = 6;
@@ -549,7 +571,7 @@ const assignOperationToFileInplace = (
}
}
// 4. deltime_remote
// 4. deltimeRemote
if (r.deltimeRemote !== undefined && r.deltimeRemote !== 0) {
const mtimeLocal = r.existLocal ? r.mtimeLocal : -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) {
// not yet decided by the above reason
if (deltimeLocal > 0 && deltimeLocal > deltimeRemote) {
@@ -678,7 +725,7 @@ export const getSyncPlan = async (
local: TAbstractFile[],
localConfigDirContents: ObsConfigDirFileType[] | undefined,
remoteDeleteHistory: DeletionOnRemote[],
localDeleteHistory: FileFolderHistoryRecord[],
localFileHistory: FileFolderHistoryRecord[],
remoteType: SUPPORTED_SERVICES_TYPE,
vault: Vault,
syncConfigDir: boolean,
@@ -691,7 +738,7 @@ export const getSyncPlan = async (
local,
localConfigDirContents,
remoteDeleteHistory,
localDeleteHistory,
localFileHistory,
syncConfigDir,
configDir,
syncUnderscoreItems
+19
View File
@@ -266,3 +266,22 @@ describe("Misc: at which level", () => {
expect(misc.atWhichLevel("x/y/z.md")).to.be.equal(3);
});
});
describe("Misc: special char for dir", () => {
it("should return false for normal string", () => {
expect(misc.checkHasSpecialCharForDir("")).to.be.false;
expect(misc.checkHasSpecialCharForDir("xxx")).to.be.false;
expect(misc.checkHasSpecialCharForDir("yyy_xxx")).to.be.false;
expect(misc.checkHasSpecialCharForDir("yyy.xxx")).to.be.false;
expect(misc.checkHasSpecialCharForDir("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.13": "0.13.21"
"0.3.16": "0.13.21"
}