Compare commits

...
19 Commits
Author SHA1 Message Date
fyears d42b31cea2 update node ci
Release A New Version / build (20.x) (push) Failing after 51s
2024-07-27 23:08:49 +08:00
fyears 69e0c77444 bump to 0.5.16 2024-07-27 22:58:36 +08:00
fyears ee4394873b upgrade check connections 2024-07-27 22:58:05 +08:00
fyears f2845cdcbf finally we can switch from ts-node to tsx 2024-07-27 22:55:04 +08:00
fyears bbee873b64 remove debug log 2024-07-27 22:08:22 +08:00
fyears f5fee16130 bump to 0.5.15 2024-07-27 19:38:25 +08:00
fyears f4b82204c1 update packages 2024-07-27 19:37:55 +08:00
fyears f1a06a3f52 add append mode for sabre 2024-07-27 19:22:57 +08:00
fyears ae4e67f3b4 fix smart conflict 2024-07-27 19:09:42 +08:00
fyears 254231e4bd add onedrive mtime error 2024-07-27 15:02:21 +08:00
fyears 871e8dc3cf encode username 2024-07-27 14:41:37 +08:00
fyears 85aed7cfb9 dufs 2024-07-27 14:41:26 +08:00
fyears 3f7a6dbf6a Merge branch 'master' of https://github.com/fyears/remotely-save 2024-07-27 12:37:31 +08:00
fyears 13cca9025e hard code famous website 2024-07-27 12:30:17 +08:00
Vincent Whizin a81af9ccec Update docs on "large" files sync issues (#753) 2024-07-18 13:14:06 +08:00
fyears db86a75534 Add vConsole tutorial 2024-07-14 22:31:08 +08:00
fyears 72e059d5cc description of config folder and bookmarks 2024-07-14 21:09:09 +08:00
fyears b083f006b1 bump to 0.5.14
Release A New Version / build (16.x) (push) Failing after 46s
2024-07-14 17:16:31 +08:00
fyears 12ed023cd1 add bookmarks sync 2024-07-14 17:04:05 +08:00
37 changed files with 616 additions and 112 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:
@@ -54,7 +54,7 @@ jobs:
- name: Git LFS Pull - name: Git LFS Pull
run: git lfs pull run: git lfs pull
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm install - run: npm install
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [20.x]
steps: steps:
- name: Checkout codes - name: Checkout codes
@@ -57,7 +57,7 @@ jobs:
- name: Git LFS Pull - name: Git LFS Pull
run: git lfs pull run: git lfs pull
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm install - run: npm install
+14 -3
View File
@@ -46,6 +46,8 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
- **You should protect your `data.json` file.** The file contains sensitive information. - **You should protect your `data.json` file.** The file contains sensitive information.
- It's strongly advised **NOT** to share your `data.json` file to anyone. - It's strongly advised **NOT** to share your `data.json` file to anyone.
- It's usually **NOT** a good idea to check the file into version control. By default, the plugin tries to create a `.gitignore` file inside the plugin directory if it doesn't exist, for ignoring `data.json` in the `git` version control. If you know exactly what it means and want to remove the setting, please modify the `.gitignore` file or set it to be empty. - It's usually **NOT** a good idea to check the file into version control. By default, the plugin tries to create a `.gitignore` file inside the plugin directory if it doesn't exist, for ignoring `data.json` in the `git` version control. If you know exactly what it means and want to remove the setting, please modify the `.gitignore` file or set it to be empty.
- **Obsidian API on Mobile has performance issues syncing large files (>= 50 MB).**
- Setting the "Skip Large Files" option can help resolve issues related to syncing large files.
## Questions, Suggestions, Or Bugs ## Questions, Suggestions, Or Bugs
@@ -112,6 +114,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
- [ownCloud](./docs/remote_services/webdav_owncloud/README.md) - [ownCloud](./docs/remote_services/webdav_owncloud/README.md)
- [InfiniCloud](./docs/remote_services/webdav_infinicloud_teracloud/README.md) - [InfiniCloud](./docs/remote_services/webdav_infinicloud_teracloud/README.md)
- [Synology webdav server](./docs/remote_services/webdav_synology_webdav_server/README.md) | [群晖 webdav server](./docs/remote_services/webdav_synology_webdav_server/README.zh-cn.md) - [Synology webdav server](./docs/remote_services/webdav_synology_webdav_server/README.md) | [群晖 webdav server](./docs/remote_services/webdav_synology_webdav_server/README.zh-cn.md)
- [dufs](./docs/remote_services/webdav_dufs/README.md)
- [AList(中文)](./docs/remote_services/webdav_alist/README.zh-cn.md) | [AList (English)](./docs/remote_services/webdav_alist/README.md) - [AList(中文)](./docs/remote_services/webdav_alist/README.zh-cn.md) | [AList (English)](./docs/remote_services/webdav_alist/README.md)
- [坚果云](./docs/remote_services/webdav_jianguoyun/README.zh-cn.md) | [JianGuoYun/NutStore](./docs/remote_services/webdav_jianguoyun/README.md) - [坚果云](./docs/remote_services/webdav_jianguoyun/README.zh-cn.md) | [JianGuoYun/NutStore](./docs/remote_services/webdav_jianguoyun/README.md)
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md) - [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
@@ -161,7 +164,7 @@ PRO (paid) feature "sync with Azure Blob Storage" allows users to to sync with A
Basic (free) version can detect conflicts, but users have to choose to keep newer version or larger version of the files. Basic (free) version can detect conflicts, but users have to choose to keep newer version or larger version of the files.
PRO (paid) feature "Smart Conflict" gives users one more option: merge small markdown files, or duplicate large markdown files or non-markdown files. PRO (paid) feature "Smart Conflict" gives users one more option: merge small markdown files, or duplicate large markdown files or any-size non-markdown files.
See documents [here](./docs/pro/README.md) See documents [here](./docs/pro/README.md)
@@ -176,11 +179,19 @@ See documents [here](./docs/pro/README.md)
- You can configure sync on save in settings. - You can configure sync on save in settings.
- In sync on save mode, if any error occurs, the plugin would **fail silently**. - In sync on save mode, if any error occurs, the plugin would **fail silently**.
## How To Deal With Hidden Files Or Folders ## Config Folder / Files and Bookmarks
By default, the plugin doesn't sync config folder / files of obsidian (normally `.obsidian` folder) because it's one of the hidden files.
However, in the latest version, you can enable syncing config folder in settings. Please be aware that it's an experimental feature. Notably some config files' modified times are changed everytime when Obsidian is opened, and this destroys the assumptions of the plugin, so some information may not be synced properly.
Moreover, the bookmarks of Obsidian are actually stored in `.obsidian/bookmarks.json` file. You can also set syncing this file (instead of the whole config folder) in settings. The plugin will keep the latest modified one across devices.
## Hidden Files Or Folders
**By default, all files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced. **By default, all files or folder starting with `.` (dot) or `_` (underscore) are treated as hidden files, and would NOT be synced.** It's useful if you have some files just staying locally. But this strategy also means that themes / other plugins / settings of this plugin would neither be synced.
In the latest version, you can change the settings to allow syncing `_` files or folders, as well as `.obsidian` special config folder (but not any other `.` files or folders). In the latest version, you can change the settings to allow syncing `_` files or folders, as well as `.obsidian` special config folder as described above (but not any other `.` files or folders).
## PRO Features ## PRO Features
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
}, },
@@ -24,7 +24,7 @@
"jsxQuoteStyle": "double", "jsxQuoteStyle": "double",
"quoteProperties": "asNeeded", "quoteProperties": "asNeeded",
"semicolons": "always", "semicolons": "always",
"trailingComma": "es5" "trailingCommas": "es5"
} }
}, },
"linter": { "linter": {
+7 -1
View File
@@ -6,12 +6,18 @@ Ideally, users do not need to debug anything. But if something bad happened, we
See [here](./export_sync_plans.md). See [here](./export_sync_plans.md).
## Advanced: Check Console Output Directly ## Medium: Desktop: Check Console Output Directly
...of desktop and Android. ...of desktop and Android.
See [here](./check_console_output.md). See [here](./check_console_output.md).
## Medium: Mobile: Use `Obsidian vConsole` to Check Console Ouput Directly
...of mobile (iOS and Android).
See [here](./check_vconsole_output.md).
## Advanced: Use `Logstravaganza` to export logs ## Advanced: Use `Logstravaganza` to export logs
This method works for desktop and mobile devices (iOS, Android), especially useful for iOS. This method works for desktop and mobile devices (iOS, Android), especially useful for iOS.
+3 -1
View File
@@ -18,6 +18,8 @@ Go to the plugin settings, scroll down to the section "Debug" -> "alter console
- If you are using Android - If you are using Android
You need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable) on your Android, then connect your Android to a computer using USB, then open the **desktop** Chrome browser and go to the special web page <chrome://inspect>. You shall be able to see the "inspect" link inside the special page, then click the link to open the console. After debugging, remember to turn off USB debugging. If you want to check the console **with the help of desktop Chrome**, you need to [enable USB debugging](https://developer.android.com/studio/debug/dev-options#enable) on your Android, then connect your Android to a computer using USB, then open the **desktop** Chrome browser and go to the special web page <chrome://inspect>. You shall be able to see the "inspect" link inside the special page, then click the link to open the console. After debugging, remember to turn off USB debugging.
If you want to check the console **directly on the mobile device**, check out [this doc](./check_vconsole_output.md).
Trigger the sync manually (by clicking the icon on the ribbon sidebar). Something (hopefully) helpful should show up in the console. You could understand what happened and what goes wrong more explictly by checking the output. Trigger the sync manually (by clicking the icon on the ribbon sidebar). Something (hopefully) helpful should show up in the console. You could understand what happened and what goes wrong more explictly by checking the output.
@@ -0,0 +1,21 @@
# Check "Obsidian vConsole" Output
It's quite hard to debug on mobile. But fortunately you can use another plugin to rescue!
This applies to both iOS (iPhone / iPad) and Android.
## Disable Auto Sync Firstly
You should disable auto sync to avoid any unexpected running.
## Set The Output Level To Debug
Go to the plugin settings, scroll down to the section "Debug" -> "alter console log level", and change it from "info" to "debug".
## Use "Obsidian vConsole"
1. Install the third-party plugin ["Obsidian vConsole"](https://github.com/zhouhua/obsidian-vconsole).
2. Enable "Obsidian vConsole". You shall see a green button in the right buttom corner of the interface of your mobile Obsidian. You can drag this green button to where you like.
3. Sync! And quickly click the vConsole green button, you will see the console logs in its panel! Moreover, you can check browser network and Storage (LocalStorage) in the panel!
4. You can copy (by clicking the small "disk" icon beside each line of the log) or take a screenshot of the logs, and report bugs in Remotely Save GitHub. You can click "Hide" in the panel to hide the panel.
5. If you don't need to debug any more, you can disable Obsidian vConsole plugin, then the green button should disappear.
@@ -0,0 +1,17 @@
# dufs webdav
## Link
<https://github.com/sigoden/dufs>
## Steps
1. Download the latest version: <https://github.com/sigoden/dufs/releases> and save it to a folder
2. Add the path environment variable.
3. Start the server in terminal, you can change the address (`127.0.0.1`) and username (`user1`) and password (`pass1`) accordingly:
```bash
dufs -A --enable-cors --bind 127.0.0.1 --port 8080 --auth user1:pass1@/:rw
```
4. In remotely-save setting page, select webdav type, then input the address/account/**webdav password**(not your account password).
5. In remotely-save setting page, click "Check Connectivity".
6. Sync!
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.13", "version": "0.5.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",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.13", "version": "0.5.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",
+34 -34
View File
@@ -1,15 +1,15 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.5.13", "version": "0.5.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 --watch", "dev2": "node esbuild.config.mjs --watch",
"build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "build2": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"build": "webpack --mode production", "build": "webpack --mode production",
"dev": "webpack --mode development --watch", "dev": "webpack --mode development --watch",
"format": "npx @biomejs/biome check --apply .", "format": "npx @biomejs/biome check --write .",
"clean": "npx rimraf main.js", "clean": "npx rimraf main.js",
"test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha -r ts-node/register 'tests/**/*.ts' 'pro/tests/**/*.ts'" "test": "mocha --import=tsx 'tests/**/*.ts' 'pro/tests/**/*.ts'"
}, },
"browser": { "browser": {
"path": "path-browserify", "path": "path-browserify",
@@ -25,55 +25,55 @@
"author": "", "author": "",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.7.3", "@biomejs/biome": "1.8.3",
"@microsoft/microsoft-graph-types": "^2.40.0", "@microsoft/microsoft-graph-types": "^2.40.0",
"@types/chai": "^4.3.14", "@types/chai": "^4.3.16",
"@types/chai-as-promised": "^7.1.8", "@types/chai-as-promised": "^7.1.8",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.7",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.7",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.7",
"@types/mustache": "^4.2.5", "@types/mustache": "^4.2.5",
"@types/node": "^20.12.7", "@types/node": "^20.14.12",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"builtin-modules": "^3.3.0", "builtin-modules": "^4.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"esbuild": "^0.20.2", "esbuild": "^0.23.0",
"esbuild-plugin-inline-worker": "^0.1.1", "esbuild-plugin-inline-worker": "^0.1.1",
"jsdom": "^24.0.0", "jsdom": "^24.1.1",
"mocha": "^10.4.0", "mocha": "^10.7.0",
"npm-check-updates": "^16.14.20", "npm-check-updates": "^16.14.20",
"obsidian": "^1.5.7", "obsidian": "^1.5.7",
"openapi-typescript": "^7.0.0-rc.0", "openapi-typescript": "^7.1.0",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"ts-node": "^10.9.2", "tslib": "^2.6.3",
"tslib": "^2.6.2", "tsx": "^4.16.2",
"typescript": "^5.4.5", "typescript": "^5.5.4",
"webdav-server": "^2.6.2", "webdav-server": "^2.6.2",
"webpack": "^5.91.0", "webpack": "^5.93.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"worker-loader": "^3.0.8" "worker-loader": "^3.0.8"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.563.0", "@aws-sdk/client-s3": "^3.620.0",
"@aws-sdk/lib-storage": "^3.563.0", "@aws-sdk/lib-storage": "^3.620.0",
"@aws-sdk/signature-v4-crt": "^3.556.0", "@aws-sdk/signature-v4-crt": "^3.620.0",
"@aws-sdk/types": "^3.535.0", "@aws-sdk/types": "^3.609.0",
"@azure/msal-node": "^2.7.0", "@azure/msal-node": "^2.12.0",
"@azure/storage-blob": "^12.23.0", "@azure/storage-blob": "^12.24.0",
"@fyears/rclone-crypt": "^0.0.7", "@fyears/rclone-crypt": "^0.0.7",
"@fyears/tsqueue": "^1.0.1", "@fyears/tsqueue": "^1.0.1",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",
"@sanity/diff-match-patch": "^3.1.1", "@sanity/diff-match-patch": "^3.1.1",
"@smithy/fetch-http-handler": "^2.5.0", "@smithy/fetch-http-handler": "^3.2.4",
"@smithy/protocol-http": "^3.3.0", "@smithy/protocol-http": "^4.1.0",
"@smithy/querystring-builder": "^2.2.0", "@smithy/querystring-builder": "^3.0.3",
"acorn": "^8.11.3", "acorn": "^8.12.1",
"aggregate-error": "^5.0.0", "aggregate-error": "^5.0.0",
"assert": "^2.1.0", "assert": "^2.1.0",
"aws-crt": "^1.21.2", "aws-crt": "^1.21.3",
"box-typescript-sdk-gen": "^1.0.0", "box-typescript-sdk-gen": "^1.3.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"dropbox": "^10.34.0", "dropbox": "^10.34.0",
@@ -83,21 +83,21 @@
"localforage-getitems": "^1.4.2", "localforage-getitems": "^1.4.2",
"localforage-removeitems": "^1.4.0", "localforage-removeitems": "^1.4.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide": "^0.376.1", "lucide": "^0.416.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"node-diff3": "^3.1.2", "node-diff3": "^3.1.2",
"openapi-fetch": "^0.9.7", "openapi-fetch": "^0.10.2",
"p-queue": "^8.0.1", "p-queue": "^8.0.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"pcloud-sdk-js": "^2.0.0", "pcloud-sdk-js": "^2.0.0",
"process": "^0.11.10", "process": "^0.11.10",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"rfc4648": "^1.5.3", "rfc4648": "^1.5.3",
"rimraf": "^5.0.5", "rimraf": "^6.0.1",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"url": "^0.11.3", "url": "^0.11.4",
"util": "^0.12.5", "util": "^0.12.5",
"webdav": "^5.6.0", "webdav": "^5.6.0",
"xregexp": "^5.1.1" "xregexp": "^5.1.1"
+10 -5
View File
@@ -38,9 +38,11 @@ export function isMergable(a: Entity, b?: Entity) {
* @param b * @param b
*/ */
function mergeDigInModified(a: string, o: string, b: string) { function mergeDigInModified(a: string, o: string, b: string) {
const { conflict, result } = mergeDigIn(a, o, b); const { conflict, result } = mergeDigIn(a, o, b, {
stringSeparator: /\n/,
});
for (let index = 0; index < result.length; ++index) { for (let index = 0; index < result.length; ++index) {
if (["<<<<<<<", "=======", ">>>>>>>"].contains(result[index])) { if (["<<<<<<<", "=======", ">>>>>>>"].includes(result[index])) {
result[index] = "`" + result[index] + "`"; result[index] = "`" + result[index] + "`";
} }
} }
@@ -72,7 +74,7 @@ function getLCSText(a: string, b: string) {
* @param b * @param b
* @returns * @returns
*/ */
function twoWayMerge(a: string, b: string): string { export function twoWayMerge(a: string, b: string): string {
const aa = a.trim(); const aa = a.trim();
const bb = b.trim(); const bb = b.trim();
if (aa === "" && bb === "") { if (aa === "" && bb === "") {
@@ -88,7 +90,10 @@ function twoWayMerge(a: string, b: string): string {
// const c = getLCSText(a, b); // const c = getLCSText(a, b);
// const patches = makePatches(c, a); // const patches = makePatches(c, a);
// const [d] = applyPatches(patches, b); // const [d] = applyPatches(patches, b);
const c = getLCSText(a, b); const c = getLCSText(a, b); //.trim();
// console.debug(`(start) LCS Text:`);
// console.debug(c);
// console.debug(`(end) LCS Text.`);
const d = mergeDigInModified(a, c, b).result.join("\n"); const d = mergeDigInModified(a, c, b).result.join("\n");
return d; return d;
} }
@@ -100,7 +105,7 @@ function twoWayMerge(a: string, b: string): string {
* @param orig * @param orig
* @returns * @returns
*/ */
function threeWayMerge(a: string, b: string, orig: string) { export function threeWayMerge(a: string, b: string, orig: string) {
return mergeDigInModified(a, orig, b).result.join("\n"); return mergeDigInModified(a, orig, b).result.join("\n");
} }
+1 -1
View File
@@ -320,12 +320,12 @@ export class FakeFsAzureBlobStorage extends FakeFs {
// if we can walk, we can connect // if we can walk, we can connect
try { try {
await this.walkPartial(); await this.walkPartial();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+1 -1
View File
@@ -892,12 +892,12 @@ export class FakeFsBox extends FakeFs {
// if we can init, we can connect // if we can init, we can connect
try { try {
await this._init(); await this._init();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
+1 -1
View File
@@ -728,12 +728,12 @@ export class FakeFsGoogleDrive extends FakeFs {
// if we can init, we can connect // if we can init, we can connect
try { try {
await this._init(); await this._init();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+4 -4
View File
@@ -263,7 +263,7 @@ const getAuthMiddleware = (
saveUpdatedConfigFunc: any saveUpdatedConfigFunc: any
) => { ) => {
const authMiddleware: Middleware = { const authMiddleware: Middleware = {
async onRequest(req) { async onRequest({ request }) {
const getAccessToken = async () => { const getAccessToken = async () => {
if (koofrConfig.refreshToken === "") { if (koofrConfig.refreshToken === "") {
throw Error("The user has not manually auth yet."); throw Error("The user has not manually auth yet.");
@@ -291,8 +291,8 @@ const getAuthMiddleware = (
}; };
const access = await getAccessToken(); const access = await getAccessToken();
req.headers.set("Authorization", `Bearer ${access}`); request.headers.set("Authorization", `Bearer ${access}`);
return req; return request;
}, },
}; };
return authMiddleware; return authMiddleware;
@@ -555,12 +555,12 @@ export class FakeFsKoofr extends FakeFs {
// if we can init, we can connect // if we can init, we can connect
try { try {
await this._init(); await this._init();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+10 -3
View File
@@ -290,8 +290,12 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
key = `${key}/`; key = `${key}/`;
} }
const mtimeSvr = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!); const mtimeTry = x?.fileSystemInfo?.lastModifiedDateTime;
const mtimeCli = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!); if (mtimeTry === undefined || mtimeTry === null) {
throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`);
}
const mtimeSvr = Date.parse(mtimeTry);
const mtimeCli = Date.parse(mtimeTry);
return { return {
key: key, key: key,
keyRaw: key, keyRaw: key,
@@ -912,12 +916,15 @@ export class FakeFsOnedriveFull extends FakeFs {
async checkConnect(callbackFunc?: any): Promise<boolean> { async checkConnect(callbackFunc?: any): Promise<boolean> {
try { try {
const k = await this.getUserDisplayName(); const k = await this.getUserDisplayName();
return k !== "<unknown display name>"; if (k === "<unknown display name>") {
throw Error(`unknown display name!`);
}
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName() { async getUserDisplayName() {
+1 -1
View File
@@ -604,12 +604,12 @@ export class FakeFsPCloud extends FakeFs {
// if we can init, we can connect // if we can init, we can connect
try { try {
await this._init(); await this._init();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+1 -1
View File
@@ -480,12 +480,12 @@ export class FakeFsYandexDisk extends FakeFs {
// if we can init, we can connect // if we can init, we can connect
try { try {
await this._init(); await this._init();
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+1
View File
@@ -74,6 +74,7 @@ export const generateClearDupFilesSettingsPart = (
const fsLocal = new FakeFsLocal( const fsLocal = new FakeFsLocal(
app.vault, app.vault,
plugin.settings.syncConfigDir ?? false, plugin.settings.syncConfigDir ?? false,
plugin.settings.syncBookmarks ?? false,
app.vault.configDir, app.vault.configDir,
plugin.manifest.id, plugin.manifest.id,
undefined, undefined,
+24 -1
View File
@@ -116,9 +116,21 @@ const isInsideObsFolder = (x: string, configDir: string) => {
return x === configDir || x.startsWith(`${configDir}/`); return x === configDir || x.startsWith(`${configDir}/`);
}; };
const isBookmarksFile = (x: string, configDir: string) => {
if (!configDir.startsWith(".")) {
throw Error(`configDir should starts with . but we get ${configDir}`);
}
return (
x === configDir ||
x === `${configDir}/` ||
x === `${configDir}/bookmarks.json`
);
};
const isSkipItemByName = ( const isSkipItemByName = (
key: string, key: string,
syncConfigDir: boolean, syncConfigDir: boolean,
syncBookmarks: boolean,
syncUnderscoreItems: boolean, syncUnderscoreItems: boolean,
configDir: string, configDir: string,
ignorePaths: string[] ignorePaths: string[]
@@ -136,6 +148,11 @@ const isSkipItemByName = (
if (syncConfigDir && isInsideObsFolder(key, configDir)) { if (syncConfigDir && isInsideObsFolder(key, configDir)) {
return false; return false;
} }
if (syncBookmarks && isBookmarksFile(key, configDir)) {
return false;
}
if (isSpecialFolderNameToSkip(key, [])) { if (isSpecialFolderNameToSkip(key, [])) {
// some special dirs and files are always skipped // some special dirs and files are always skipped
return true; return true;
@@ -157,6 +174,7 @@ const ensembleMixedEnties = async (
remoteEntityList: Entity[], remoteEntityList: Entity[],
syncConfigDir: boolean, syncConfigDir: boolean,
syncBookmarks: boolean,
configDir: string, configDir: string,
syncUnderscoreItems: boolean, syncUnderscoreItems: boolean,
ignorePaths: string[], ignorePaths: string[],
@@ -184,6 +202,7 @@ const ensembleMixedEnties = async (
isSkipItemByName( isSkipItemByName(
key, key,
syncConfigDir, syncConfigDir,
syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths
@@ -216,6 +235,7 @@ const ensembleMixedEnties = async (
isSkipItemByName( isSkipItemByName(
key, key,
syncConfigDir, syncConfigDir,
syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths
@@ -251,6 +271,7 @@ const ensembleMixedEnties = async (
isSkipItemByName( isSkipItemByName(
key, key,
syncConfigDir, syncConfigDir,
syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths
@@ -911,13 +932,14 @@ const getSyncPlanInplace = async (
mixedEntityMappings["/$@meta"] = { mixedEntityMappings["/$@meta"] = {
key: "/$@meta", // don't mess up with the types key: "/$@meta", // don't mess up with the types
sideNotes: { sideNotes: {
version: "20240616 fs version", version: "20240714 fs version",
generateTime: currTime, generateTime: currTime,
generateTimeFmt: currTimeFmt, generateTimeFmt: currTimeFmt,
service: settings.serviceType, service: settings.serviceType,
concurrency: settings.concurrency, concurrency: settings.concurrency,
hasPassword: settings.password !== "", hasPassword: settings.password !== "",
syncConfigDir: settings.syncConfigDir, syncConfigDir: settings.syncConfigDir,
syncBookmarks: settings.syncBookmarks,
syncUnderscoreItems: settings.syncUnderscoreItems, syncUnderscoreItems: settings.syncUnderscoreItems,
skipSizeLargerThan: settings.skipSizeLargerThan, skipSizeLargerThan: settings.skipSizeLargerThan,
protectModifyPercentage: settings.protectModifyPercentage, protectModifyPercentage: settings.protectModifyPercentage,
@@ -1709,6 +1731,7 @@ export async function syncer(
prevSyncEntityList, prevSyncEntityList,
remoteEntityList, remoteEntityList,
settings.syncConfigDir ?? false, settings.syncConfigDir ?? false,
settings.syncBookmarks ?? false,
configDir, configDir,
settings.syncUnderscoreItems ?? false, settings.syncUnderscoreItems ?? false,
settings.ignorePaths ?? [], settings.ignorePaths ?? [],
+226 -1
View File
@@ -1,5 +1,9 @@
import { deepStrictEqual, rejects, throws } from "assert"; import { deepStrictEqual, rejects, throws } from "assert";
import { getFileRenameForDup } from "../src/conflictLogic"; import {
getFileRenameForDup,
threeWayMerge,
twoWayMerge,
} from "../src/conflictLogic";
describe("New name is generated", () => { describe("New name is generated", () => {
it("should throw for empty file", async () => { it("should throw for empty file", async () => {
@@ -69,3 +73,224 @@ describe("New name is generated", () => {
); );
}); });
}); });
describe("Two way merge", () => {
it("should correctly merge from zero files", async () => {
const a = "aaa";
const b = "bbb";
const res = twoWayMerge(a, b);
const expected = `\`<<<<<<<\`
aaa
\`=======\`
bbb
\`>>>>>>>\``;
deepStrictEqual(expected, res);
});
it("should correctly merge from common lines", async () => {
const a = `
Something is cool. 中文1
Other thing is cooler. 哈哈!
`;
const b = `
Anything is cool. 中文2
Other thing is cooler. 哈哈!
`;
const res = twoWayMerge(a, b);
// console.log(res);
const expected = `
\`<<<<<<<\`
Something is cool. 中文1
\`=======\`
Anything is cool. 中文2
\`>>>>>>>\`
Other thing is cooler. 哈哈!
`;
deepStrictEqual(expected, res);
});
it("should merge by lines", async () => {
const a = `
Something is cool. 中文1
`;
const b = `
Something is cooler. 中文2
`;
const res = twoWayMerge(a, b);
// console.log(res);
const expected = `
\`<<<<<<<\`
Something is cool. 中文1
\`=======\`
Something is cooler. 中文2
\`>>>>>>>\`
`;
deepStrictEqual(expected, res);
});
});
describe("Three way merge", () => {
it("should correctly merge from zero files", async () => {
const orig = "";
const a = "aaa";
const b = "bbb";
const res = threeWayMerge(a, b, orig);
const expected = `\`<<<<<<<\`
aaa
\`=======\`
bbb
\`>>>>>>>\``;
deepStrictEqual(expected, res);
});
it("should correctly merge after adding lines on both sides", async () => {
const orig = `
* [ ] A1
* [ ] A2
* [ ] A3
`;
const a = `
* [ ] A1
* [ ] new line after A1
* [ ] A2
* [ ] A3
`;
const b = `
* [ ] A1
* [ ] A2
* [ ] New line after A2
* [ ] A3
`;
const res = threeWayMerge(a, b, orig);
// console.log(res);
const expected = `
* [ ] A1
* [ ] new line after A1
* [ ] A2
* [ ] New line after A2
* [ ] A3
`;
deepStrictEqual(expected, res);
});
it("should correctly merge after adding lines on both sides (again)", async () => {
const orig = `
* [ ] 中文
* [ ] にほんご/にっぽんご
* [ ] A3
`;
const a = `
* [ ] 中文
* [ ] new line after 中文
* [ ] にほんご/にっぽんご
* [ ] A3
`;
const b = `
* [ ] 中文
* [ ] にほんご/にっぽんご
* [ ] New line after にほんご/にっぽんご
* [ ] A3
`;
const res = threeWayMerge(a, b, orig);
// console.log(res);
const expected = `
* [ ] 中文
* [ ] new line after 中文
* [ ] にほんご/にっぽんご
* [ ] New line after にほんご/にっぽんご
* [ ] A3
`;
deepStrictEqual(expected, res);
});
it("should correctly merge after deleting lines on both sides", async () => {
const orig = `
* [ ] 中文
* [ ] にほんご/にっぽんご
* [ ] A3
* [ ] A4
`;
const a = `
* [ ] にほんご/にっぽんご
* [ ] A3
* [ ] A4
`;
const b = `
* [ ] 中文
* [ ] A3
* [ ] A4
`;
const res = threeWayMerge(a, b, orig);
// console.log(res);
const expected = `
\`<<<<<<<\`
* [ ] にほんご/にっぽんご
\`=======\`
* [ ] 中文
\`>>>>>>>\`
* [ ] A3
* [ ] A4
`;
deepStrictEqual(expected, res);
});
it("should correctly merge after adding on one side and deleting on other side", async () => {
const orig = `
* [ ] 中文
* [ ] A3
* [ ] A4
`;
const a = `
* [ ] 中文
* [ ] にほんご/にっぽんご
* [ ] A3
* [ ] A4
`;
const b = `
* [ ] A3
* [ ] A4
`;
const res = threeWayMerge(a, b, orig);
// console.log(res);
const expected = `
\`<<<<<<<\`
* [ ] 中文
* [ ] にほんご/にっぽんご
\`=======\`
\`>>>>>>>\`
* [ ] A3
* [ ] A4
`;
deepStrictEqual(expected, res);
});
it("should correctly merge after adding on one side and deleting on other side (again)", async () => {
const orig = `
* [ ] 中文
* [ ] A3
* [ ] A4
`;
const a = `
* [ ] A3
* [ ] A4
`;
const b = `
* [ ] 中文
* [ ] にほんご/にっぽんご
* [ ] A3
* [ ] A4
`;
const res = threeWayMerge(a, b, orig);
// console.log(res);
const expected = `
\`<<<<<<<\`
\`=======\`
* [ ] 中文
* [ ] にほんご/にっぽんご
\`>>>>>>>\`
* [ ] A3
* [ ] A4
`;
deepStrictEqual(expected, res);
});
});
+1
View File
@@ -167,6 +167,7 @@ export interface RemotelySavePluginSettings {
concurrency?: number; concurrency?: number;
syncConfigDir?: boolean; syncConfigDir?: boolean;
syncBookmarks?: boolean;
syncUnderscoreItems?: boolean; syncUnderscoreItems?: boolean;
lang?: LangTypeAndAuto; lang?: LangTypeAndAuto;
agreeToUseSyncV3?: boolean; agreeToUseSyncV3?: boolean;
+45
View File
@@ -1,3 +1,5 @@
import isEqual from "lodash/isEqual";
import { nanoid } from "nanoid";
import type { Entity } from "./baseTypes"; import type { Entity } from "./baseTypes";
export abstract class FakeFs { export abstract class FakeFs {
@@ -16,6 +18,49 @@ export abstract class FakeFs {
abstract rename(key1: string, key2: string): Promise<void>; abstract rename(key1: string, key2: string): Promise<void>;
abstract rm(key: string): Promise<void>; abstract rm(key: string): Promise<void>;
abstract checkConnect(callbackFunc?: any): Promise<boolean>; abstract checkConnect(callbackFunc?: any): Promise<boolean>;
async checkConnectCommonOps(callbackFunc?: any) {
try {
console.info(`check connect: create folder`);
const folderName = `rs-test-folder-${nanoid()}/`;
await this.mkdir(folderName);
// await delay(3000);
console.info(`check connect: upload file`);
const filename = `${folderName}/rs-test-file-${nanoid()}`;
const ctime = Date.now();
const mtime1 = Date.now();
const content1 = new ArrayBuffer(100);
await this.writeFile(filename, content1, mtime1, ctime);
// await delay(3000);
console.info(`check connect: overwrite file`);
const mtime2 = Date.now();
const content2 = new ArrayBuffer(200);
await this.writeFile(filename, content2, mtime2, ctime);
// await delay(3000);
console.info(`check connect: download file`);
const content3 = await this.readFile(filename);
if (!isEqual(content2, content3)) {
throw Error(`downloaded file is not equal with uploaded file!`);
}
// await delay(3000);
console.info(`check connect: delete file`);
await this.rm(filename);
// await delay(3000);
console.info(`check connect: delete folder`);
await this.rm(folderName);
// await delay(3000);
return true;
} catch (err) {
console.error(err);
callbackFunc?.(err);
return false;
}
}
abstract getUserDisplayName(): Promise<string>; abstract getUserDisplayName(): Promise<string>;
abstract revokeAuth(): Promise<any>; abstract revokeAuth(): Promise<any>;
abstract allowEmptyFile(): boolean; abstract allowEmptyFile(): boolean;
+3 -2
View File
@@ -741,14 +741,15 @@ export class FakeFsDropbox extends FakeFs {
await this._init(); await this._init();
const results = await this._statFromRoot(`/${this.remoteBaseDir}`); const results = await this._statFromRoot(`/${this.remoteBaseDir}`);
if (results === undefined) { if (results === undefined) {
return false; throw Error(`cannot check root vault folder!`);
} }
return true;
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName() { async getUserDisplayName() {
+9 -3
View File
@@ -9,6 +9,7 @@ import type { Profiler } from "./profiler";
export class FakeFsLocal extends FakeFs { export class FakeFsLocal extends FakeFs {
vault: Vault; vault: Vault;
syncConfigDir: boolean; syncConfigDir: boolean;
syncBookmarks: boolean;
configDir: string; configDir: string;
pluginID: string; pluginID: string;
profiler: Profiler | undefined; profiler: Profiler | undefined;
@@ -17,6 +18,7 @@ export class FakeFsLocal extends FakeFs {
constructor( constructor(
vault: Vault, vault: Vault,
syncConfigDir: boolean, syncConfigDir: boolean,
syncBookmarks: boolean,
configDir: string, configDir: string,
pluginID: string, pluginID: string,
profiler: Profiler | undefined, profiler: Profiler | undefined,
@@ -26,6 +28,7 @@ export class FakeFsLocal extends FakeFs {
this.vault = vault; this.vault = vault;
this.syncConfigDir = syncConfigDir; this.syncConfigDir = syncConfigDir;
this.syncBookmarks = syncBookmarks;
this.configDir = configDir; this.configDir = configDir;
this.pluginID = pluginID; this.pluginID = pluginID;
this.profiler = profiler; this.profiler = profiler;
@@ -96,13 +99,16 @@ export class FakeFsLocal extends FakeFs {
this.profiler?.insert("finish transforming walk for local"); this.profiler?.insert("finish transforming walk for local");
if (this.syncConfigDir) { if (this.syncConfigDir || this.syncBookmarks) {
this.profiler?.insert("into syncConfigDir"); this.profiler?.insert("into syncConfigDir or syncBookmarks");
const bookmarksOnly = !this.syncConfigDir;
const syncFiles = await listFilesInObsFolder( const syncFiles = await listFilesInObsFolder(
this.configDir, this.configDir,
this.vault, this.vault,
this.pluginID this.pluginID,
bookmarksOnly
); );
// console.debug(`syncFiles in obs: ${JSON.stringify(syncFiles, null, 2)}`);
for (const f of syncFiles) { for (const f of syncFiles) {
local.push(f); local.push(f);
} }
+1 -1
View File
@@ -47,7 +47,7 @@ export class FakeFsMock extends FakeFs {
} }
async checkConnect(callbackFunc?: any): Promise<boolean> { async checkConnect(callbackFunc?: any): Promise<boolean> {
return true; return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+11 -3
View File
@@ -399,8 +399,12 @@ ${constructFromDriveItemToEntityError(x)}`
key = `${key}/`; key = `${key}/`;
} }
const mtimeSvr = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!); const mtimeTry = x?.fileSystemInfo?.lastModifiedDateTime;
const mtimeCli = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!); if (mtimeTry === undefined || mtimeTry === null) {
throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`);
}
const mtimeSvr = Date.parse(mtimeTry);
const mtimeCli = Date.parse(mtimeTry);
return { return {
key: key, key: key,
keyRaw: key, keyRaw: key,
@@ -1032,12 +1036,16 @@ export class FakeFsOnedrive extends FakeFs {
async checkConnect(callbackFunc?: any): Promise<boolean> { async checkConnect(callbackFunc?: any): Promise<boolean> {
try { try {
const k = await this.getUserDisplayName(); const k = await this.getUserDisplayName();
return k !== "<unknown display name>"; if (k === "<unknown display name>") {
throw Error(`unknown display name!`);
}
} catch (err) { } catch (err) {
console.debug(err); console.debug(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName() { async getUserDisplayName() {
+5 -7
View File
@@ -831,14 +831,11 @@ export class FakeFsS3 extends FakeFs {
results.$metadata === undefined || results.$metadata === undefined ||
results.$metadata.httpStatusCode === undefined results.$metadata.httpStatusCode === undefined
) { ) {
const err = "results or $metadata or httStatusCode is undefined"; throw Error("results or $metadata or httStatusCode is undefined");
console.debug(err);
if (callbackFunc !== undefined) {
callbackFunc(err);
} }
return false; if (results.$metadata.httpStatusCode !== 200) {
throw Error(`not 200 httpStatusCode`);
} }
return results.$metadata.httpStatusCode === 200;
} catch (err: any) { } catch (err: any) {
console.debug(err); console.debug(err);
if (callbackFunc !== undefined) { if (callbackFunc !== undefined) {
@@ -854,9 +851,10 @@ export class FakeFsS3 extends FakeFs {
callbackFunc(err); callbackFunc(err);
} }
} }
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+94 -26
View File
@@ -4,8 +4,6 @@ import { getReasonPhrase } from "http-status-codes/build/cjs/utils-functions";
import chunk from "lodash/chunk"; import chunk from "lodash/chunk";
import cloneDeep from "lodash/cloneDeep"; import cloneDeep from "lodash/cloneDeep";
import flatten from "lodash/flatten"; import flatten from "lodash/flatten";
import isString from "lodash/isString";
import { nanoid } from "nanoid";
import { Platform, type RequestUrlParam, requestUrl } from "obsidian"; import { Platform, type RequestUrlParam, requestUrl } from "obsidian";
import type { import type {
FileStat, FileStat,
@@ -140,6 +138,7 @@ if (VALID_REQURL) {
); );
} }
import isEqual from "lodash/isEqual";
// @ts-ignore // @ts-ignore
// biome-ignore lint: we want to ts-ignore the next line // biome-ignore lint: we want to ts-ignore the next line
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js"; import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
@@ -209,6 +208,13 @@ const tryEncodeURI = (x: string) => {
return encodeURI(x); return encodeURI(x);
}; };
const tryEncodeUsernamePassword = (x: string) => {
if (onlyAscii(x)) {
return x;
}
return unescape(encodeURIComponent(x));
};
const parseCustomHeaders = (x: string): Record<string, string> => { const parseCustomHeaders = (x: string): Record<string, string> => {
const y = x.trim(); const y = x.trim();
if (y === "") { if (y === "") {
@@ -241,7 +247,8 @@ export class FakeFsWebdav extends FakeFs {
vaultFolderExists: boolean; vaultFolderExists: boolean;
saveUpdatedConfigFunc: () => Promise<any>; saveUpdatedConfigFunc: () => Promise<any>;
supportNativePartial: boolean; supportApachePartial: boolean;
supportSabrePartial: boolean;
isNextcloud: boolean; isNextcloud: boolean;
nextcloudUploadServerAddress: string; nextcloudUploadServerAddress: string;
@@ -258,7 +265,8 @@ export class FakeFsWebdav extends FakeFs {
this.vaultFolderExists = false; this.vaultFolderExists = false;
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc; this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
this.supportNativePartial = false; this.supportApachePartial = false;
this.supportSabrePartial = false;
this.isNextcloud = false; this.isNextcloud = false;
this.nextcloudUploadServerAddress = ""; this.nextcloudUploadServerAddress = "";
} }
@@ -282,8 +290,8 @@ export class FakeFsWebdav extends FakeFs {
this.webdavConfig.password !== "" this.webdavConfig.password !== ""
) { ) {
this.client = createClient(this.webdavConfig.address, { this.client = createClient(this.webdavConfig.address, {
username: this.webdavConfig.username, username: tryEncodeUsernamePassword(this.webdavConfig.username),
password: this.webdavConfig.password, password: tryEncodeUsernamePassword(this.webdavConfig.password),
headers: headers, headers: headers,
authType: authType:
this.webdavConfig.authType === "digest" this.webdavConfig.authType === "digest"
@@ -392,15 +400,20 @@ export class FakeFsWebdav extends FakeFs {
// taken from https://github.com/perry-mitchell/webdav-client/blob/master/source/operations/partialUpdateFileContents.ts // taken from https://github.com/perry-mitchell/webdav-client/blob/master/source/operations/partialUpdateFileContents.ts
// which is under MIT license // which is under MIT license
if ( if (
(compliance.server.includes("Apache") && compliance.server.includes("Apache") &&
compliance.compliance.includes( compliance.compliance.includes("<http://apache.org/dav/propset/fs/1>")
"<http://apache.org/dav/propset/fs/1>"
)) ||
compliance.compliance.includes("sabredav-partialupdate")
) { ) {
this.supportNativePartial = true; this.supportApachePartial = true;
console.debug( console.debug(
`supportNativePartial=true, compliance=${JSON.stringify(compliance)}` `supportApachePartial=true, compliance=${JSON.stringify(compliance)}`
);
return true;
}
if (compliance.compliance.includes("sabredav-partialupdate")) {
this.supportSabrePartial = true;
console.debug(
`supportSabrePartial=true, compliance=${JSON.stringify(compliance)}`
); );
return true; return true;
} }
@@ -417,7 +430,9 @@ export class FakeFsWebdav extends FakeFs {
this.webdavConfig.depth === "auto_unknown" || this.webdavConfig.depth === "auto_unknown" ||
this.webdavConfig.depth === "auto_1" || this.webdavConfig.depth === "auto_1" ||
this.webdavConfig.depth === "auto_infinity" /* don't trust auto now */ || this.webdavConfig.depth === "auto_infinity" /* don't trust auto now */ ||
this.webdavConfig.depth === "manual_1" this.webdavConfig.depth === "manual_1" ||
this.webdavConfig.address.includes("jianguoyun.com") ||
this.webdavConfig.address.includes("teracloud.jp")
) { ) {
// 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
@@ -570,7 +585,11 @@ export class FakeFsWebdav extends FakeFs {
} }
// larger than 10 MB // larger than 10 MB
if (!this.isNextcloud && !this.supportNativePartial) { if (
!this.isNextcloud &&
!this.supportApachePartial &&
!this.supportSabrePartial
) {
// give up and upload by whole, and directly return // give up and upload by whole, and directly return
return await this._writeFileFromRootFull( return await this._writeFileFromRootFull(
key, key,
@@ -591,8 +610,16 @@ export class FakeFsWebdav extends FakeFs {
ctime, ctime,
origKey origKey
); );
} else if (this.supportNativePartial) { } else if (this.supportApachePartial) {
return await this._writeFileFromRootNativePartial( return await this._writeFileFromRootApachePartial(
key,
content,
mtime,
ctime,
origKey
);
} else if (this.supportSabrePartial) {
return await this._writeFileFromRootSabrePartial(
key, key,
content, content,
mtime, mtime,
@@ -608,7 +635,7 @@ export class FakeFsWebdav extends FakeFs {
console.error(e); console.error(e);
throw e; throw e;
// this.isNextcloud = false; // this.isNextcloud = false;
// this.supportNativePartial = false; // this.supportApachePartial = false;
// return await this._writeFileFromRootFull( // return await this._writeFileFromRootFull(
// key, // key,
// content, // content,
@@ -676,8 +703,8 @@ export class FakeFsWebdav extends FakeFs {
console.debug(`tmpFolderName=${tmpFolderName}`); console.debug(`tmpFolderName=${tmpFolderName}`);
const clientForUpload = createClient(uploadServerAddress, { const clientForUpload = createClient(uploadServerAddress, {
username: this.webdavConfig.username, username: tryEncodeUsernamePassword(this.webdavConfig.username),
password: this.webdavConfig.password, password: tryEncodeUsernamePassword(this.webdavConfig.password),
headers: { headers: {
"Cache-Control": "no-cache", "Cache-Control": "no-cache",
}, },
@@ -753,7 +780,7 @@ export class FakeFsWebdav extends FakeFs {
return k; return k;
} }
async _writeFileFromRootNativePartial( async _writeFileFromRootApachePartial(
key: string, key: string,
content: ArrayBuffer, content: ArrayBuffer,
mtime: number, mtime: number,
@@ -775,6 +802,8 @@ export class FakeFsWebdav extends FakeFs {
content.byteLength, content.byteLength,
sizePerChunk sizePerChunk
); );
// TODO: parallel
for (let i = 0; i < chunkRanges.length; ++i) { for (let i = 0; i < chunkRanges.length; ++i) {
const { start, end } = chunkRanges[i]; const { start, end } = chunkRanges[i];
await this.client.partialUpdateFileContents( await this.client.partialUpdateFileContents(
@@ -789,6 +818,47 @@ export class FakeFsWebdav extends FakeFs {
return await this.stat(origKey); return await this.stat(origKey);
} }
async _writeFileFromRootSabrePartial(
key: string,
content: ArrayBuffer,
mtime: number,
ctime: number,
origKey: string
): Promise<Entity> {
// firstly upload a 0-byte data
await this._writeFileFromRootFull(
key,
new ArrayBuffer(0),
mtime,
ctime,
origKey
);
// then "update" by chunks
const sizePerChunk = 5 * 1024 * 1024; // 5 mb
const chunkRanges = splitFileSizeToChunkRanges(
content.byteLength,
sizePerChunk
);
// diff from apachePartial: we use "append" header here for dufs...
// we cannot parallel here
for (let i = 0; i < chunkRanges.length; ++i) {
const { start, end } = chunkRanges[i];
await this.client.customRequest(key, {
method: "PATCH",
headers: {
"X-Update-Range": "append",
"Content-Type": "application/x-sabredav-partialupdate",
},
data: content.slice(start, end + 1),
});
}
// lastly return
return await this.stat(origKey);
}
async readFile(key: string): Promise<ArrayBuffer> { async readFile(key: string): Promise<ArrayBuffer> {
if (key.endsWith("/")) { if (key.endsWith("/")) {
throw Error(`you should not call readFile on ${key}`); throw Error(`you should not call readFile on ${key}`);
@@ -853,17 +923,15 @@ export class FakeFsWebdav extends FakeFs {
await this._init(); await this._init();
const results = await this._statFromRoot(`/${this.remoteBaseDir}/`); const results = await this._statFromRoot(`/${this.remoteBaseDir}/`);
if (results === undefined) { if (results === undefined) {
const err = "results is undefined"; throw Error("cannot stat root vault folder!");
console.error(err);
callbackFunc?.(err);
return false;
} }
return true;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+5 -1
View File
@@ -252,12 +252,16 @@ export class FakeFsWebdis extends FakeFs {
const k = await ( const k = await (
await this._fetchCommand("GET", "PING/helloworld") await this._fetchCommand("GET", "PING/helloworld")
).json(); ).json();
return isEqual(k, { PING: "helloworld" }); if (!isEqual(k, { PING: "helloworld" })) {
throw Error(`no correct ping response`);
}
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);
callbackFunc?.(err); callbackFunc?.(err);
return false; return false;
} }
return await this.checkConnectCommonOps(callbackFunc);
// return true;
} }
async getUserDisplayName(): Promise<string> { async getUserDisplayName(): Promise<string> {
+2
View File
@@ -282,6 +282,8 @@
"settings_syncunderscore_desc": "Sync files or folders starting with _ (\"underscore\") or not", "settings_syncunderscore_desc": "Sync files or folders starting with _ (\"underscore\") or not",
"settings_configdir": "Sync Config Dir (experimental)", "settings_configdir": "Sync Config Dir (experimental)",
"settings_configdir_desc": "Sync config dir {{configDir}} or not (inner folder .git and node_modules would be ignored). Please be aware that this may impact all your plugins' or Obsidian's settings, and may require you restart Obsidian after sync. Enable this at your own risk.", "settings_configdir_desc": "Sync config dir {{configDir}} or not (inner folder .git and node_modules would be ignored). Please be aware that this may impact all your plugins' or Obsidian's settings, and may require you restart Obsidian after sync. Enable this at your own risk.",
"settings_bookmarks": "Sync Bookmarks (experimental)",
"settings_bookmarks_desc": "Sync {{configDir}}/bookmarks.json or not. If you sync config dir, this option will be ignore and treated as \"enabled\".",
"settings_deletetowhere": "Deletion Destination", "settings_deletetowhere": "Deletion Destination",
"settings_deletetowhere_desc": "Which trash should the plugin put the files into while deleting?", "settings_deletetowhere_desc": "Which trash should the plugin put the files into while deleting?",
"settings_deletetowhere_system_trash": "system trash (default)", "settings_deletetowhere_system_trash": "system trash (default)",
+2
View File
@@ -281,6 +281,8 @@
"settings_syncunderscore_desc": "是否同步“_”(下划线)开始的文件或文件夹。", "settings_syncunderscore_desc": "是否同步“_”(下划线)开始的文件或文件夹。",
"settings_configdir": "同步配置文件夹(实验性质)", "settings_configdir": "同步配置文件夹(实验性质)",
"settings_configdir_desc": "是否同步 Obsidian 的配置文件夹 {{configDir}}(其中的 .git 和 node_modules 依然会被忽略)。请注意,此设置可能会影响到您 Obsidian 或者所有的插件设置,也可能需要您同步完毕后重启 Obsidian。如果您同意自行承受其风险,则可以开启本设置。", "settings_configdir_desc": "是否同步 Obsidian 的配置文件夹 {{configDir}}(其中的 .git 和 node_modules 依然会被忽略)。请注意,此设置可能会影响到您 Obsidian 或者所有的插件设置,也可能需要您同步完毕后重启 Obsidian。如果您同意自行承受其风险,则可以开启本设置。",
"settings_bookmarks": "同步书签(实验性质)",
"settings_bookmarks_desc": "是否同步书签 {{configDir}}/bookmarks.json 。如果您设置了同步配置文件夹,此选项会被忽略并设为“开启”。",
"settings_deletetowhere": "删除文件到哪里", "settings_deletetowhere": "删除文件到哪里",
"settings_deletetowhere_desc": "插件触发删除操作时候,删除到哪里?", "settings_deletetowhere_desc": "插件触发删除操作时候,删除到哪里?",
"settings_deletetowhere_system_trash": "系统回收站(默认)", "settings_deletetowhere_system_trash": "系统回收站(默认)",
+2
View File
@@ -280,6 +280,8 @@
"settings_syncunderscore_desc": "是否同步“_”(下劃線)開始的檔案或資料夾。", "settings_syncunderscore_desc": "是否同步“_”(下劃線)開始的檔案或資料夾。",
"settings_configdir": "同步配置資料夾(實驗性質)", "settings_configdir": "同步配置資料夾(實驗性質)",
"settings_configdir_desc": "是否同步 Obsidian 的配置資料夾 {{configDir}}(其中的 .git 和 node_modules 依然會被忽略)。請注意,此設定可能會影響到您 Obsidian 或者所有的外掛設定,也可能需要您同步完畢後重啟 Obsidian。如果您同意自行承受其風險,則可以開啟本設定。", "settings_configdir_desc": "是否同步 Obsidian 的配置資料夾 {{configDir}}(其中的 .git 和 node_modules 依然會被忽略)。請注意,此設定可能會影響到您 Obsidian 或者所有的外掛設定,也可能需要您同步完畢後重啟 Obsidian。如果您同意自行承受其風險,則可以開啟本設定。",
"settings_bookmarks": "同步書籤(實驗性質)",
"settings_bookmarks_desc": "是否同步書籤 {{configDir}}/bookmarks.json 。如果您設定了同步配置資料夾,此選項會被忽略並設為“開啟”。",
"settings_deletetowhere": "刪除檔案到哪裡", "settings_deletetowhere": "刪除檔案到哪裡",
"settings_deletetowhere_desc": "外掛觸發刪除操作時候,刪除到哪裡?", "settings_deletetowhere_desc": "外掛觸發刪除操作時候,刪除到哪裡?",
"settings_deletetowhere_system_trash": "系統回收站(預設)", "settings_deletetowhere_system_trash": "系統回收站(預設)",
+7
View File
@@ -135,6 +135,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
agreeToUploadExtraMetadata: true, // as of 20240106, it's safe to assume every new user agrees with this agreeToUploadExtraMetadata: true, // as of 20240106, it's safe to assume every new user agrees with this
concurrency: 5, concurrency: 5,
syncConfigDir: false, syncConfigDir: false,
syncBookmarks: false,
syncUnderscoreItems: false, syncUnderscoreItems: false,
lang: "auto", lang: "auto",
logToDB: false, logToDB: false,
@@ -239,6 +240,7 @@ export default class RemotelySavePlugin extends Plugin {
const fsLocal = new FakeFsLocal( const fsLocal = new FakeFsLocal(
this.app.vault, this.app.vault,
this.settings.syncConfigDir ?? false, this.settings.syncConfigDir ?? false,
this.settings.syncBookmarks ?? false,
this.app.vault.configDir, this.app.vault.configDir,
this.manifest.id, this.manifest.id,
profiler, profiler,
@@ -1337,6 +1339,10 @@ export default class RemotelySavePlugin extends Plugin {
messyConfigToNormal(await this.loadData()) messyConfigToNormal(await this.loadData())
); );
if (this.settings.syncBookmarks === undefined) {
this.settings.syncBookmarks = false;
}
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;
} }
@@ -1879,6 +1885,7 @@ export default class RemotelySavePlugin extends Plugin {
const fsLocal = new FakeFsLocal( const fsLocal = new FakeFsLocal(
this.app.vault, this.app.vault,
this.settings.syncConfigDir ?? false, this.settings.syncConfigDir ?? false,
this.settings.syncBookmarks ?? false,
this.app.vault.configDir, this.app.vault.configDir,
this.manifest.id, this.manifest.id,
undefined, undefined,
+23 -2
View File
@@ -34,11 +34,15 @@ const isLikelyPluginSubFiles = (x: string) => {
export const listFilesInObsFolder = async ( export const listFilesInObsFolder = async (
configDir: string, configDir: string,
vault: Vault, vault: Vault,
pluginId: string pluginId: string,
bookmarksOnly: boolean
): Promise<Entity[]> => { ): Promise<Entity[]> => {
const q = new Queue([configDir]); const q = new Queue([configDir]);
const CHUNK_SIZE = 10; const CHUNK_SIZE = 10;
const contents: Entity[] = []; let contents: Entity[] = [];
let iterRound = 0;
while (q.length > 0) { while (q.length > 0) {
const itemsToFetch: string[] = []; const itemsToFetch: string[] = [];
while (q.length > 0) { while (q.length > 0) {
@@ -115,6 +119,23 @@ export const listFilesInObsFolder = async (
} }
} }
} }
if (bookmarksOnly && iterRound > 1) {
// list until bookmarks.json is found or next level is arrived.
break;
} }
iterRound += 1;
}
// console.debug(`contents in obs config: ${JSON.stringify(contents)}`);
if (bookmarksOnly) {
contents = contents.filter(
(e) =>
e.key === `${configDir}/` || e.key === `${configDir}/bookmarks.json`
);
}
return contents; return contents;
}; };
+21
View File
@@ -2325,6 +2325,27 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
new Setting(advDiv)
.setName(t("settings_bookmarks"))
.setDesc(
t("settings_bookmarks_desc", {
configDir: this.app.vault.configDir,
})
)
.addDropdown((dropdown) => {
dropdown.addOption("disable", t("disable"));
dropdown.addOption("enable", t("enable"));
dropdown
.setValue(
`${this.plugin.settings.syncBookmarks ? "enable" : "disable"}`
)
.onChange(async (val) => {
this.plugin.settings.syncBookmarks = val === "enable";
await this.plugin.saveSettings();
});
});
new Setting(advDiv) new Setting(advDiv)
.setName(t("settings_deletetowhere")) .setName(t("settings_deletetowhere"))
.setDesc(t("settings_deletetowhere_desc")) .setDesc(t("settings_deletetowhere_desc"))