Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e39edcea70 | ||
|
|
9ac7ecf828 | ||
|
|
50c08400ff | ||
|
|
7bc2fad88b | ||
|
|
6dc1c28361 | ||
|
|
2220f511e3 | ||
|
|
06de1a4ab0 | ||
|
|
df184ff9d8 | ||
|
|
f50ba953f1 | ||
|
|
55cb26bd22 | ||
|
|
36d3018efb | ||
|
|
421b43d937 | ||
|
|
17708359a1 | ||
|
|
9acae1b077 | ||
|
|
4ade975585 | ||
|
|
e988fc7ae9 | ||
|
|
5c56f2146f | ||
|
|
74125f8dff | ||
|
|
ace16de20c | ||
|
|
716e5aa6c5 | ||
|
|
fcdbb71493 | ||
|
|
0040e18b49 |
@@ -39,13 +39,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout codes
|
- name: Checkout codes
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout LFS file list
|
- name: Checkout LFS file list
|
||||||
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
||||||
- name: LFS Cache
|
- name: LFS Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .git/lfs/objects
|
path: .git/lfs/objects
|
||||||
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: my-dist
|
name: my-dist
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout codes
|
- name: Checkout codes
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Checkout LFS file list
|
- name: Checkout LFS file list
|
||||||
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
||||||
- name: LFS Cache
|
- name: LFS Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: .git/lfs/objects
|
path: .git/lfs/objects
|
||||||
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
||||||
@@ -63,44 +63,14 @@ jobs:
|
|||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: Create Release
|
- name: Create Release And Upload
|
||||||
id: create_release
|
uses: softprops/action-gh-release@v2
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
VERSION: ${{ github.ref }}
|
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
files: |
|
||||||
release_name: ${{ github.ref }}
|
main.js
|
||||||
|
manifest.json
|
||||||
|
styles.css
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: true
|
prerelease: false
|
||||||
- name: Upload main.js
|
make_latest: true
|
||||||
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
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
|||||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||||
|
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||||
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
- Very old version of Obsidian needs [configuring CORS](./docs/remote_services/webdav_general/webav_cors.md).
|
||||||
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
- Your data would be synced to a `${vaultName}` sub folder on your webdav server.
|
||||||
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
- Password-based end-to-end encryption is also supported. But please be aware that **the vault name itself is not encrypted**.
|
||||||
@@ -203,8 +204,18 @@ See [PRO](./docs/pro/README.md) for more details.
|
|||||||
|
|
||||||
If you see any errors, please check the doc [here](./docs/how_to_debug/README.md) for more details.
|
If you see any errors, please check the doc [here](./docs/how_to_debug/README.md) for more details.
|
||||||
|
|
||||||
Moreover, sometimes the program runs but slowly, you want to check the performance by [enabling the profiler](./docs/check_performance/README).
|
Moreover, sometimes the program runs but slowly, you want to check the performance by [enabling the profiler](./docs/check_performance/README.md).
|
||||||
|
|
||||||
## Bonus: Import And Export Not-Oauth2 Plugin Settings By QR Code
|
## Bonus: Import And Export Not-Oauth2 Plugin Settings By QR Code
|
||||||
|
|
||||||
See [here](./docs/import_export_some_settings.md) for more details.
|
See [here](./docs/import_export_some_settings.md) for more details.
|
||||||
|
|
||||||
|
## Download History
|
||||||
|
|
||||||
|
Download history can be viewed on the unofficial [Obsidian Stats](https://www.moritzjung.dev/obsidian-stats/plugins/remotely-save/#downloads) (NOT affiliated with official Obsidian and GitHub and Remotely Save).
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
(NOT affiliated with official Obsidian and GitHub and Remotely Save.)
|
||||||
|
|
||||||
|
[](https://star-history.com/#remotely-save/remotely-save&Date)
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
|
||||||
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
- [Nginx (`ngx_http_dav_module`, `nginx-dav-ext-module`, with Docker)](./docs/remote_services/webdav_nginx/README.md)
|
||||||
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
- [Apache (with Docker)](./docs/remote_services/webdav_apache/README.md)
|
||||||
|
- [Caddy with `http.handlers.webdav` module](./docs/remote_services/webdav_caddy/README.md)
|
||||||
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
|
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
|
||||||
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
||||||
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
||||||
@@ -209,3 +210,12 @@ PRO(付费)功能“智能冲突”为用户提供了另一个选项:合
|
|||||||
|
|
||||||
详见[这里](./docs/import_export_some_settings.md)了解更多详情。
|
详见[这里](./docs/import_export_some_settings.md)了解更多详情。
|
||||||
|
|
||||||
|
## 下载历史
|
||||||
|
|
||||||
|
下载历史可以从非官方的 [Obsidian Stats](https://www.moritzjung.dev/obsidian-stats/plugins/remotely-save/#downloads) 查阅。(和官方 Obsidian,GitHub,Remotely Save 均无利益关系。)
|
||||||
|
|
||||||
|
## 星星历史
|
||||||
|
|
||||||
|
(和官方 Obsidian,GitHub,Remotely Save 均无利益关系。)
|
||||||
|
|
||||||
|
[](https://star-history.com/#remotely-save/remotely-save&Date)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Caddy with `http.handlers.webdav` module
|
||||||
|
|
||||||
|
> modified from the instruction from @cyruz-git in https://github.com/remotely-save/remotely-save/issues/825
|
||||||
|
|
||||||
|
## Link
|
||||||
|
|
||||||
|
<https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Download caddy with webdav module from <https://caddyserver.com/download?package=github.com%2Fmholt%2Fcaddy-webdav>. Or you can install Caddy then install the plugins.
|
||||||
|
2. Create a folder for storing webdav server files. Like `/usr/local/mywebdav`.
|
||||||
|
3. Create a `Caddyfile` (yeah the file name itself is `Caddyfile`.) like this:
|
||||||
|
```caddy
|
||||||
|
:8080 {
|
||||||
|
route /dav/* {
|
||||||
|
root /usr/local/mywebdav
|
||||||
|
basicauth {
|
||||||
|
# Username "Bob", password "hiccup"
|
||||||
|
Bob $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
|
||||||
|
}
|
||||||
|
uri strip_prefix /dav
|
||||||
|
webdav
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The password hash is generated like [this](https://caddyserver.com/docs/caddyfile/directives/basic_auth).
|
||||||
|
4. In Remotely Save, setup:
|
||||||
|
* address `http://localhost:8080/dav/`
|
||||||
|
* username `Bob`
|
||||||
|
* password `hiccup`
|
||||||
|
* auth type: `basic`
|
||||||
|
5. Check the connection and sync!
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.5.20",
|
"version": "0.5.25",
|
||||||
"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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.5.20",
|
"version": "0.5.25",
|
||||||
"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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.5.20",
|
"version": "0.5.25",
|
||||||
"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",
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ class GoogleDriveAuthModal extends Modal {
|
|||||||
k.expires_in * 1000;
|
k.expires_in * 1000;
|
||||||
this.plugin.settings.googledrive.accessTokenExpiresAtTimeMs =
|
this.plugin.settings.googledrive.accessTokenExpiresAtTimeMs =
|
||||||
ts + k.expires_in * 1000 - 60 * 2 * 1000;
|
ts + k.expires_in * 1000 - 60 * 2 * 1000;
|
||||||
|
|
||||||
|
// manually set it expired after 60 days;
|
||||||
|
this.plugin.settings.googledrive.credentialsShouldBeDeletedAtTimeMs =
|
||||||
|
Date.now() + 1000 * 60 * 60 * 24 * 59;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
|
||||||
// try to remove data in clipboard
|
// try to remove data in clipboard
|
||||||
|
|||||||
+277
-66
@@ -29,6 +29,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
atWhichLevel,
|
atWhichLevel,
|
||||||
checkValidName,
|
checkValidName,
|
||||||
|
getFolderLevels,
|
||||||
getParentFolder,
|
getParentFolder,
|
||||||
isHiddenPath,
|
isHiddenPath,
|
||||||
isSpecialFolderNameToSkip,
|
isSpecialFolderNameToSkip,
|
||||||
@@ -139,7 +140,14 @@ const isBookmarksFile = (x: string, configDir: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSkipItemByName = (
|
interface IsSkipResult {
|
||||||
|
enableAllowMode: boolean;
|
||||||
|
isExplictlyAllowed: boolean;
|
||||||
|
isExplictlyIgnored: boolean;
|
||||||
|
finalIsIgnored: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkIsSkipItemOrNotByName = (
|
||||||
key: string,
|
key: string,
|
||||||
syncConfigDir: boolean,
|
syncConfigDir: boolean,
|
||||||
syncBookmarks: boolean,
|
syncBookmarks: boolean,
|
||||||
@@ -147,13 +155,15 @@ const isSkipItemByName = (
|
|||||||
configDir: string,
|
configDir: string,
|
||||||
ignorePaths: string[],
|
ignorePaths: string[],
|
||||||
onlyAllowPaths: string[]
|
onlyAllowPaths: string[]
|
||||||
) => {
|
): IsSkipResult => {
|
||||||
if (key === undefined) {
|
if (key === undefined) {
|
||||||
throw Error(`isSkipItemByName meets undefinded key!`);
|
throw Error(`checkIsSkipItemOrNotByName meets undefinded key!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finalIsIgnored: boolean | undefined = undefined;
|
||||||
|
|
||||||
let enableAllowMode = false;
|
let enableAllowMode = false;
|
||||||
let isInAllowList = false;
|
let isExplictlyAllowed = false;
|
||||||
if (onlyAllowPaths !== undefined && onlyAllowPaths.length > 0) {
|
if (onlyAllowPaths !== undefined && onlyAllowPaths.length > 0) {
|
||||||
for (const r of onlyAllowPaths) {
|
for (const r of onlyAllowPaths) {
|
||||||
if (r.trim() === "") {
|
if (r.trim() === "") {
|
||||||
@@ -163,7 +173,7 @@ const isSkipItemByName = (
|
|||||||
enableAllowMode = true; // we really want to check the allow list
|
enableAllowMode = true; // we really want to check the allow list
|
||||||
|
|
||||||
if (XRegExp(r, "A").test(key)) {
|
if (XRegExp(r, "A").test(key)) {
|
||||||
isInAllowList = true;
|
isExplictlyAllowed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,10 +183,11 @@ const isSkipItemByName = (
|
|||||||
// and is deferred to next checking steps
|
// and is deferred to next checking steps
|
||||||
// if the key doesn't meet the allow list,
|
// if the key doesn't meet the allow list,
|
||||||
// it must be skippable.
|
// it must be skippable.
|
||||||
if (enableAllowMode && !isInAllowList) {
|
if (enableAllowMode && !isExplictlyAllowed) {
|
||||||
return true; // must be skippable
|
finalIsIgnored = true; // must be skippable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isExplictlyIgnored = false;
|
||||||
if (ignorePaths !== undefined && ignorePaths.length > 0) {
|
if (ignorePaths !== undefined && ignorePaths.length > 0) {
|
||||||
for (const r of ignorePaths) {
|
for (const r of ignorePaths) {
|
||||||
if (r.trim() === "") {
|
if (r.trim() === "") {
|
||||||
@@ -184,29 +195,163 @@ const isSkipItemByName = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (XRegExp(r, "A").test(key)) {
|
if (XRegExp(r, "A").test(key)) {
|
||||||
return true;
|
if (finalIsIgnored === undefined) {
|
||||||
|
isExplictlyIgnored = true;
|
||||||
|
finalIsIgnored = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (syncConfigDir && isInsideObsFolder(key, configDir)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncBookmarks && isBookmarksFile(key, configDir)) {
|
// sync config, not sync bookmarks: sync config and **force syncing bookmarks as well**
|
||||||
return false;
|
// not sync config, sync bookmarks: sync bookmars, not other config
|
||||||
|
// not sync config, not sync bookmarks: not sync config
|
||||||
|
if (finalIsIgnored === undefined) {
|
||||||
|
if (syncConfigDir) {
|
||||||
|
if (isInsideObsFolder(key, configDir)) {
|
||||||
|
// force sync everything
|
||||||
|
finalIsIgnored = false;
|
||||||
|
} else {
|
||||||
|
// not config files, do not judge now, do nothing
|
||||||
|
}
|
||||||
|
} else if (syncBookmarks) {
|
||||||
|
// not sync config, sync bookmarks
|
||||||
|
if (isBookmarksFile(key, configDir)) {
|
||||||
|
// sync everything of bookmarks
|
||||||
|
finalIsIgnored = false;
|
||||||
|
} else if (isInsideObsFolder(key, configDir)) {
|
||||||
|
// not sync any other thing in config
|
||||||
|
finalIsIgnored = true;
|
||||||
|
} else {
|
||||||
|
// not config files, do not judge now, do nothing
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not sync config, and not sync bookmarks
|
||||||
|
if (isInsideObsFolder(key, configDir)) {
|
||||||
|
// not sync any thing in config
|
||||||
|
finalIsIgnored = true;
|
||||||
|
} else {
|
||||||
|
// not config files, do not judge now, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSpecialFolderNameToSkip(key, [])) {
|
if (isSpecialFolderNameToSkip(key, [])) {
|
||||||
// some special dirs and files are always skipped
|
// some special dirs and files are always skipped
|
||||||
return true;
|
if (finalIsIgnored === undefined) {
|
||||||
|
isExplictlyIgnored = true;
|
||||||
|
finalIsIgnored = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
|
const checkIsHidden =
|
||||||
isHiddenPath(key, true, false) ||
|
isHiddenPath(key, true, false) ||
|
||||||
(!syncUnderscoreItems && isHiddenPath(key, false, true)) ||
|
(!syncUnderscoreItems && isHiddenPath(key, false, true)) ||
|
||||||
key === "/" ||
|
key === "/" ||
|
||||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE ||
|
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE ||
|
||||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2
|
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2;
|
||||||
);
|
if (finalIsIgnored === undefined) {
|
||||||
|
isExplictlyIgnored = checkIsHidden;
|
||||||
|
finalIsIgnored = checkIsHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalIsIgnored === undefined) {
|
||||||
|
throw Error(`no finalIsIgnored in checkIsSkipItemOrNotByName for ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enableAllowMode: enableAllowMode,
|
||||||
|
isExplictlyAllowed: isExplictlyAllowed,
|
||||||
|
isExplictlyIgnored: isExplictlyIgnored,
|
||||||
|
finalIsIgnored: finalIsIgnored,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* | finalIgnored | reason | explictlyIgnored | allowMode | explictlyAllowed |
|
||||||
|
* | ------------------------------- | -------------------------------------------- | ---------------- | --------- | ---------------- |
|
||||||
|
* | no | nothing blocking | no | no | no |
|
||||||
|
* | yes, MAY be changed by children | allow mode not allowed, inexplicitly ignored | no | yes | no |
|
||||||
|
* | no, MAY apply to parents | allow mode allowed | no | yes | yes |
|
||||||
|
* | yes, also apply to children | explictly ignored | yes | no | no |
|
||||||
|
* | yes, also apply to children | explictly ignored | yes | yes | no |
|
||||||
|
* | yes, also apply to children | explictly ignored | yes | yes | yes |
|
||||||
|
*/
|
||||||
|
export const getSkipItemsByList = (
|
||||||
|
skipOrNotResults: Record<string, IsSkipResult>
|
||||||
|
): string[] => {
|
||||||
|
const allPotentialKeys = Object.keys(skipOrNotResults);
|
||||||
|
|
||||||
|
// from short(shadow) to long(deep) , ascending
|
||||||
|
const sortedKeys = allPotentialKeys.sort((k1, k2) => k1.length - k2.length);
|
||||||
|
|
||||||
|
// we deal with explicty ignored list firstly, apply them to children
|
||||||
|
const explictlyIgnoredSet = new Set<string>();
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
if (skipOrNotResults[key].isExplictlyIgnored) {
|
||||||
|
skipOrNotResults[key].finalIsIgnored = true;
|
||||||
|
explictlyIgnoredSet.add(key);
|
||||||
|
} else {
|
||||||
|
const parents = getFolderLevels(key, true).reverse();
|
||||||
|
for (const key2 of parents) {
|
||||||
|
if (explictlyIgnoredSet.has(key2)) {
|
||||||
|
skipOrNotResults[key].isExplictlyIgnored = true;
|
||||||
|
skipOrNotResults[key].finalIsIgnored = true;
|
||||||
|
explictlyIgnoredSet.add(key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we deal with explictly allow list secondly, apply them to PARENTS if possible
|
||||||
|
let enableAllowMode = false;
|
||||||
|
if (
|
||||||
|
allPotentialKeys.length > 0 &&
|
||||||
|
allPotentialKeys[0] !== undefined &&
|
||||||
|
skipOrNotResults[allPotentialKeys[0]] !== undefined
|
||||||
|
) {
|
||||||
|
enableAllowMode = skipOrNotResults[allPotentialKeys[0]].enableAllowMode;
|
||||||
|
}
|
||||||
|
if (enableAllowMode) {
|
||||||
|
for (let index = 0; index < sortedKeys.length; index++) {
|
||||||
|
// reverse order, long(deep) to short(shadow), ascending
|
||||||
|
const key = sortedKeys[sortedKeys.length - index - 1];
|
||||||
|
if (
|
||||||
|
!skipOrNotResults[key].isExplictlyIgnored &&
|
||||||
|
skipOrNotResults[key].isExplictlyAllowed
|
||||||
|
) {
|
||||||
|
// the file is explictly allowed, and not explictly ignored by anywhere
|
||||||
|
// we allow all its parents!
|
||||||
|
const parents = getFolderLevels(key, true).reverse();
|
||||||
|
|
||||||
|
for (const key2 of parents) {
|
||||||
|
if (
|
||||||
|
key2 in skipOrNotResults &&
|
||||||
|
!skipOrNotResults[key2].isExplictlyIgnored &&
|
||||||
|
!explictlyIgnoredSet.has(key2)
|
||||||
|
) {
|
||||||
|
skipOrNotResults[key2].isExplictlyAllowed = true;
|
||||||
|
skipOrNotResults[key2].finalIsIgnored = false; // from ignored to allowed
|
||||||
|
} else {
|
||||||
|
throw Error(
|
||||||
|
`${key}'s parent ${key2} in abnormal state: ${JSON.stringify(skipOrNotResults[key2])}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all finalIsIgnored
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
if (skipOrNotResults[key].finalIsIgnored) {
|
||||||
|
result.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(`finalIsIgnored list= ${JSON.stringify(result)}`);
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SyncPlanType = Record<string, MixedEntity>;
|
export type SyncPlanType = Record<string, MixedEntity>;
|
||||||
@@ -235,33 +380,39 @@ const ensembleMixedEnties = async (
|
|||||||
|
|
||||||
const finalMappings: SyncPlanType = {};
|
const finalMappings: SyncPlanType = {};
|
||||||
|
|
||||||
|
const skipOrNotResults: Record<string, IsSkipResult> = {};
|
||||||
|
|
||||||
// remote has to be first
|
// remote has to be first
|
||||||
|
let remoteMaySkipCountAndNotConfig = 0;
|
||||||
for (const remote of remoteEntityList) {
|
for (const remote of remoteEntityList) {
|
||||||
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
||||||
copyEntityAndFixTimeFormat(remote, serviceType)
|
copyEntityAndFixTimeFormat(remote, serviceType)
|
||||||
);
|
);
|
||||||
|
|
||||||
const key = remoteCopied.key!;
|
const key = remoteCopied.key!;
|
||||||
if (
|
|
||||||
isSkipItemByName(
|
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||||
key,
|
key,
|
||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
syncBookmarks,
|
syncBookmarks,
|
||||||
syncUnderscoreItems,
|
syncUnderscoreItems,
|
||||||
configDir,
|
configDir,
|
||||||
ignorePaths,
|
ignorePaths,
|
||||||
onlyAllowPaths
|
onlyAllowPaths
|
||||||
)
|
);
|
||||||
) {
|
skipOrNotResults[key] = skipOrNot;
|
||||||
continue;
|
if (skipOrNot.finalIsIgnored && !key.startsWith(configDir)) {
|
||||||
|
remoteMaySkipCountAndNotConfig += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkValidNameResult = checkValidName(key);
|
// 20240907: users (not on windows) doesn't like it. revert back now.
|
||||||
if (!checkValidNameResult.result) {
|
// TODO: platform specific but not introducing obsidian dependency into sync.ts
|
||||||
throw Error(
|
// const checkValidNameResult = checkValidName(key);
|
||||||
`your remote folder/file name is invalid: ${checkValidNameResult.reason}`
|
// if (!checkValidNameResult.result) {
|
||||||
);
|
// throw Error(
|
||||||
}
|
// `your remote folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
finalMappings[key] = {
|
finalMappings[key] = {
|
||||||
key: key,
|
key: key,
|
||||||
@@ -272,19 +423,25 @@ const ensembleMixedEnties = async (
|
|||||||
profiler?.insert("ensembleMixedEnties: finish remote");
|
profiler?.insert("ensembleMixedEnties: finish remote");
|
||||||
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
||||||
|
|
||||||
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
if (
|
||||||
|
Object.keys(finalMappings).filter((k) => !k.startsWith(configDir)).length -
|
||||||
|
remoteMaySkipCountAndNotConfig ===
|
||||||
|
0 ||
|
||||||
|
localEntityList.filter((e) => !e.key?.startsWith(configDir)).length === 0
|
||||||
|
) {
|
||||||
// Special checking:
|
// Special checking:
|
||||||
// if one side is totally empty,
|
// if one side is totally empty,
|
||||||
// usually that's a hard rest.
|
// usually that's a hard rest.
|
||||||
// So we need to ignore everything of prevSyncEntityList to avoid deletions!
|
// So we need to ignore everything of prevSyncEntityList to avoid deletions!
|
||||||
// TODO: acutally erase everything of prevSyncEntityList?
|
// TODO: acutally erase everything of prevSyncEntityList?
|
||||||
// TODO: local should also go through a isSkipItemByName checking beforehand
|
// TODO: local should also go through a checkIsSkipItemOrNotByName checking beforehand
|
||||||
} else {
|
} else {
|
||||||
// normally go through the prevSyncEntityList
|
// normally go through the prevSyncEntityList
|
||||||
for (const prevSync of prevSyncEntityList) {
|
for (const prevSync of prevSyncEntityList) {
|
||||||
const key = prevSync.key!;
|
const key = prevSync.key!;
|
||||||
if (
|
|
||||||
isSkipItemByName(
|
if (!(key in skipOrNotResults)) {
|
||||||
|
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||||
key,
|
key,
|
||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
syncBookmarks,
|
syncBookmarks,
|
||||||
@@ -292,9 +449,8 @@ const ensembleMixedEnties = async (
|
|||||||
configDir,
|
configDir,
|
||||||
ignorePaths,
|
ignorePaths,
|
||||||
onlyAllowPaths
|
onlyAllowPaths
|
||||||
)
|
);
|
||||||
) {
|
skipOrNotResults[key] = skipOrNot;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: abstraction leaking?
|
// TODO: abstraction leaking?
|
||||||
@@ -320,8 +476,9 @@ const ensembleMixedEnties = async (
|
|||||||
// (we don't consume prevSync here because it gains no benefit)
|
// (we don't consume prevSync here because it gains no benefit)
|
||||||
for (const local of localEntityList) {
|
for (const local of localEntityList) {
|
||||||
const key = local.key!;
|
const key = local.key!;
|
||||||
if (
|
|
||||||
isSkipItemByName(
|
if (!(key in skipOrNotResults)) {
|
||||||
|
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||||
key,
|
key,
|
||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
syncBookmarks,
|
syncBookmarks,
|
||||||
@@ -329,17 +486,18 @@ const ensembleMixedEnties = async (
|
|||||||
configDir,
|
configDir,
|
||||||
ignorePaths,
|
ignorePaths,
|
||||||
onlyAllowPaths
|
onlyAllowPaths
|
||||||
)
|
);
|
||||||
) {
|
skipOrNotResults[key] = skipOrNot;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkValidNameResult = checkValidName(key);
|
// 20240907: users (not on windows) doesn't like it. revert back now.
|
||||||
if (!checkValidNameResult.result) {
|
// TODO: platform specific but not introducing obsidian dependency into sync.ts
|
||||||
throw Error(
|
// const checkValidNameResult = checkValidName(key);
|
||||||
`your local folder/file name is invalid: ${checkValidNameResult.reason}`
|
// if (!checkValidNameResult.result) {
|
||||||
);
|
// throw Error(
|
||||||
}
|
// `your local folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
// TODO: abstraction leaking?
|
// TODO: abstraction leaking?
|
||||||
const localCopied = await fsEncrypt.encryptEntity(
|
const localCopied = await fsEncrypt.encryptEntity(
|
||||||
@@ -358,6 +516,15 @@ const ensembleMixedEnties = async (
|
|||||||
profiler?.insert("ensembleMixedEnties: finish local");
|
profiler?.insert("ensembleMixedEnties: finish local");
|
||||||
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
||||||
|
|
||||||
|
// we check the skipOrNotResults again! in case we adjust some paths!
|
||||||
|
const allReallySkipKeys = getSkipItemsByList(skipOrNotResults);
|
||||||
|
for (const key of allReallySkipKeys) {
|
||||||
|
delete finalMappings[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
profiler?.insert("ensembleMixedEnties: finish parsing all skip items");
|
||||||
|
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
||||||
|
|
||||||
// console.debug("in the end of ensembleMixedEnties, finalMappings is:");
|
// console.debug("in the end of ensembleMixedEnties, finalMappings is:");
|
||||||
// console.debug(finalMappings);
|
// console.debug(finalMappings);
|
||||||
|
|
||||||
@@ -383,7 +550,7 @@ const getSyncPlanInplace = async (
|
|||||||
) => {
|
) => {
|
||||||
profiler?.addIndent();
|
profiler?.addIndent();
|
||||||
profiler?.insert("getSyncPlanInplace: enter");
|
profiler?.insert("getSyncPlanInplace: enter");
|
||||||
// from long(deep) to short(shadow)
|
// from long(deep) to short(shadow), descending
|
||||||
const sortedKeys = Object.keys(mixedEntityMappings).sort(
|
const sortedKeys = Object.keys(mixedEntityMappings).sort(
|
||||||
(k1, k2) => k2.length - k1.length
|
(k1, k2) => k2.length - k1.length
|
||||||
);
|
);
|
||||||
@@ -476,14 +643,36 @@ const getSyncPlanInplace = async (
|
|||||||
mixedEntry.change = false;
|
mixedEntry.change = false;
|
||||||
keptFolder.add(getParentFolder(key));
|
keptFolder.add(getParentFolder(key));
|
||||||
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
||||||
mixedEntry.decisionBranch = 135;
|
if (
|
||||||
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
key === `${configDir}/` ||
|
||||||
mixedEntry.change = true;
|
key === `${configDir}/bookmarks.json`
|
||||||
|
) {
|
||||||
|
// special: never delete .obsidian folder!
|
||||||
|
mixedEntry.decisionBranch = 137;
|
||||||
|
mixedEntry.decision = "folder_existed_both_then_do_nothing";
|
||||||
|
mixedEntry.change = false;
|
||||||
|
keptFolder.add(getParentFolder(key));
|
||||||
|
} else {
|
||||||
|
mixedEntry.decisionBranch = 135;
|
||||||
|
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// bidirectional
|
// bidirectional
|
||||||
mixedEntry.decisionBranch = 124;
|
if (
|
||||||
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
key === `${configDir}/` ||
|
||||||
mixedEntry.change = true;
|
key === `${configDir}/bookmarks.json`
|
||||||
|
) {
|
||||||
|
// special: never delete .obsidian folder!
|
||||||
|
mixedEntry.decisionBranch = 138;
|
||||||
|
mixedEntry.decision = "folder_existed_both_then_do_nothing";
|
||||||
|
mixedEntry.change = false;
|
||||||
|
keptFolder.add(getParentFolder(key));
|
||||||
|
} else {
|
||||||
|
mixedEntry.decisionBranch = 124;
|
||||||
|
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// then the folder is created on local
|
// then the folder is created on local
|
||||||
@@ -925,13 +1114,35 @@ const getSyncPlanInplace = async (
|
|||||||
mixedEntry.decision = "conflict_created_then_do_nothing";
|
mixedEntry.decision = "conflict_created_then_do_nothing";
|
||||||
mixedEntry.change = false;
|
mixedEntry.change = false;
|
||||||
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
||||||
mixedEntry.decisionBranch = 39;
|
if (
|
||||||
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
key === `${configDir}/` ||
|
||||||
mixedEntry.change = true;
|
key === `${configDir}/bookmarks.json`
|
||||||
|
) {
|
||||||
|
// special: never delete .obsidian/bookmarks.json file!
|
||||||
|
mixedEntry.decisionBranch = 139;
|
||||||
|
mixedEntry.decision = "conflict_created_then_keep_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
keptFolder.add(getParentFolder(key));
|
||||||
|
} else {
|
||||||
|
mixedEntry.decisionBranch = 39;
|
||||||
|
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mixedEntry.decisionBranch = 7;
|
if (
|
||||||
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
key === `${configDir}/` ||
|
||||||
mixedEntry.change = true;
|
key === `${configDir}/bookmarks.json`
|
||||||
|
) {
|
||||||
|
// special: never delete .obsidian/bookmarks.json file!
|
||||||
|
mixedEntry.decisionBranch = 140;
|
||||||
|
mixedEntry.decision = "conflict_created_then_keep_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
keptFolder.add(getParentFolder(key));
|
||||||
|
} else {
|
||||||
|
mixedEntry.decisionBranch = 7;
|
||||||
|
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
||||||
|
mixedEntry.change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if A is in the previous list and MODIFIED, A has been deleted by B but modified by A
|
// if A is in the previous list and MODIFIED, A has been deleted by B but modified by A
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { strict as assert } from "assert";
|
||||||
|
import { checkIsSkipItemOrNotByName } from "../src/sync";
|
||||||
|
|
||||||
|
describe("Sync: checkIsSkipItemOrNotByName", () => {
|
||||||
|
it("should be ok everywhere for empty config", async () => {
|
||||||
|
let isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ [],
|
||||||
|
/* onlyAllowPaths */ []
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(!isSkip);
|
||||||
|
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ [""],
|
||||||
|
/* onlyAllowPaths */ ["", "\n"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(!isSkip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be ok for deny list", async () => {
|
||||||
|
let isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ ["xxx"],
|
||||||
|
/* onlyAllowPaths */ []
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(isSkip);
|
||||||
|
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"yyy.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ ["xxx"],
|
||||||
|
/* onlyAllowPaths */ []
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(!isSkip);
|
||||||
|
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ ["xxx$"],
|
||||||
|
/* onlyAllowPaths */ []
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(!isSkip);
|
||||||
|
|
||||||
|
// if we deny a folder, we have to deny all the sub files
|
||||||
|
// TODO: it's soooo hard to do the path resolution in this func with regex,
|
||||||
|
// so we defer the detection to later steps now.
|
||||||
|
// the test here doesn't work.
|
||||||
|
// isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
// 'xxx/yyy.md',
|
||||||
|
// false,
|
||||||
|
// false,
|
||||||
|
// false,
|
||||||
|
// '.obsidian',
|
||||||
|
// /* ignorePaths */ ['xxx/$'],
|
||||||
|
// /* onlyAllowPaths */ []
|
||||||
|
// ).finalIsIgnored;
|
||||||
|
// assert.ok(isSkip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be ok for allow list", async () => {
|
||||||
|
let isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ [],
|
||||||
|
/* onlyAllowPaths */ ["xxx"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(!isSkip);
|
||||||
|
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"yyy.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ [""],
|
||||||
|
/* onlyAllowPaths */ ["xxx"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(isSkip);
|
||||||
|
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ [],
|
||||||
|
/* onlyAllowPaths */ ["xxx$"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(isSkip);
|
||||||
|
|
||||||
|
// should NOT skip because we allow the sub file AND not deny the folder
|
||||||
|
// TODO: it's soooo hard to do the path resolution in this func with regex,
|
||||||
|
// so we defer the detection to later steps now.
|
||||||
|
// the test here doesn't work.
|
||||||
|
// isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
// 'xxx/',
|
||||||
|
// false,
|
||||||
|
// false,
|
||||||
|
// false,
|
||||||
|
// '.obsidian',
|
||||||
|
// /* ignorePaths */ [],
|
||||||
|
// /* onlyAllowPaths */ ['xxx/yyy.md']
|
||||||
|
// ).finalIsIgnored;
|
||||||
|
// assert.ok(!isSkip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should detect the name by two lists together", async () => {
|
||||||
|
// should skip because we ignore the path
|
||||||
|
let isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ ["xxx"],
|
||||||
|
/* onlyAllowPaths */ ["yyy"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(isSkip);
|
||||||
|
|
||||||
|
// should skip because we disallow the whole folder
|
||||||
|
isSkip = checkIsSkipItemOrNotByName(
|
||||||
|
"xxx/yyy.md",
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
".obsidian",
|
||||||
|
/* ignorePaths */ ["xxx"],
|
||||||
|
/* onlyAllowPaths */ ["xxx/yyy.md"]
|
||||||
|
).finalIsIgnored;
|
||||||
|
assert.ok(isSkip);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -252,6 +252,10 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
// why?? /drive/root:/Apps/Graph
|
// why?? /drive/root:/Apps/Graph
|
||||||
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
const FIFTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph\//g;
|
||||||
|
|
||||||
|
// why again?? /drive/root:/Apps/Graph 1
|
||||||
|
const SIXTH_COMMON_PREFIX_REGEX = /^\/drive\/root:\/[^\/]+\/Graph 1\//g;
|
||||||
|
const SIXTH_COMMON_PREFIX_REGEX_V2 = /^\/drive\/root:\/[^\/]+\/Graph%201\//g;
|
||||||
|
|
||||||
// or the root is absolute path /Livefolders,
|
// or the root is absolute path /Livefolders,
|
||||||
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
||||||
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||||
@@ -278,6 +282,11 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_COMMON_PREFIX_REGEX);
|
||||||
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
const matchFifthPrefixRes = fullPathOriginal.match(FIFTH_COMMON_PREFIX_REGEX);
|
||||||
|
const matchSixthPrefixRes = fullPathOriginal.match(SIXTH_COMMON_PREFIX_REGEX);
|
||||||
|
const matchSixthV2PrefixRes = fullPathOriginal.match(
|
||||||
|
SIXTH_COMMON_PREFIX_REGEX_V2
|
||||||
|
);
|
||||||
|
|
||||||
const matchSecondPrefixRes = fullPathOriginal.match(
|
const matchSecondPrefixRes = fullPathOriginal.match(
|
||||||
SECOND_COMMON_PREFIX_REGEX
|
SECOND_COMMON_PREFIX_REGEX
|
||||||
);
|
);
|
||||||
@@ -317,6 +326,40 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sixth
|
||||||
|
else if (
|
||||||
|
matchSixthPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchSixthPrefixRes[0]}${remoteBaseDir}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDir}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (
|
||||||
|
matchSixthPrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(
|
||||||
|
`${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthPrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sixth v2
|
||||||
|
else if (
|
||||||
|
matchSixthV2PrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(`${matchSixthV2PrefixRes[0]}${remoteBaseDir}`)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDir}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
} else if (
|
||||||
|
matchSixthV2PrefixRes !== null &&
|
||||||
|
fullPathOriginal.startsWith(
|
||||||
|
`${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const foundPrefix = `${matchSixthV2PrefixRes[0]}${remoteBaseDirEncoded}`;
|
||||||
|
key = fullPathOriginal.substring(foundPrefix.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// second
|
// second
|
||||||
else if (
|
else if (
|
||||||
matchSecondPrefixRes !== null &&
|
matchSecondPrefixRes !== null &&
|
||||||
@@ -374,6 +417,8 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
|||||||
fullPathOriginal=${fullPathOriginal}
|
fullPathOriginal=${fullPathOriginal}
|
||||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||||
|
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||||
|
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||||
${constructFromDriveItemToEntityError(x)}`
|
${constructFromDriveItemToEntityError(x)}`
|
||||||
@@ -388,6 +433,8 @@ ${constructFromDriveItemToEntityError(x)}`
|
|||||||
fullPathOriginal=${fullPathOriginal}
|
fullPathOriginal=${fullPathOriginal}
|
||||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||||
|
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||||
|
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||||
${constructFromDriveItemToEntityError(x)}`
|
${constructFromDriveItemToEntityError(x)}`
|
||||||
|
|||||||
+33
-12
@@ -138,7 +138,6 @@ 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";
|
||||||
@@ -169,23 +168,37 @@ const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
|||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sometimes the path startswith /../../......
|
||||||
|
* we want to make sure the path is compatible
|
||||||
|
*/
|
||||||
|
const stripLeadingPath = (x: string) => {
|
||||||
|
let y = x;
|
||||||
|
while (y.startsWith("/..")) {
|
||||||
|
y = y.slice("/..".length);
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
const getNormPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||||
|
const strippedFileOrFolderPath = stripLeadingPath(fileOrFolderPath);
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
fileOrFolderPath === `/${remoteBaseDir}` ||
|
strippedFileOrFolderPath === `/${remoteBaseDir}` ||
|
||||||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
strippedFileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
|
`"${fileOrFolderPath}" after stripping doesn't starts with "/${remoteBaseDir}/"`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const result = strippedFileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||||
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
||||||
let key = getNormPath(x.filename, remoteBaseDir);
|
let key = getNormPath(x.filename, remoteBaseDir);
|
||||||
|
|
||||||
if (x.type === "directory" && !key.endsWith("/")) {
|
if (x.type === "directory" && !key.endsWith("/")) {
|
||||||
key = `${key}/`;
|
key = `${key}/`;
|
||||||
}
|
}
|
||||||
@@ -447,15 +460,17 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
// console.debug(itemsToFetchChunks);
|
// console.debug(itemsToFetchChunks);
|
||||||
const subContents = [] as FileStat[];
|
const subContents = [] as FileStat[];
|
||||||
for (const singleChunk of itemsToFetchChunks) {
|
for (const singleChunk of itemsToFetchChunks) {
|
||||||
const r = singleChunk.map((x) => {
|
const r = singleChunk.map(async (x) => {
|
||||||
return this.client.getDirectoryContents(x, {
|
let k = (await this.client.getDirectoryContents(x, {
|
||||||
deep: false,
|
deep: false,
|
||||||
details: false /* no need for verbose details here */,
|
details: false /* no need for verbose details here */,
|
||||||
// TODO: to support .obsidian,
|
// TODO: to support .obsidian,
|
||||||
// we need to load all files including dot,
|
// we need to load all files including dot,
|
||||||
// anyway to reduce the resources?
|
// anyway to reduce the resources?
|
||||||
// glob: "/**" /* avoid dot files by using glob */,
|
// glob: "/**" /* avoid dot files by using glob */,
|
||||||
}) as Promise<FileStat[]>;
|
})) as FileStat[];
|
||||||
|
k = k.filter((sub) => stripLeadingPath(sub.filename) !== x);
|
||||||
|
return k;
|
||||||
});
|
});
|
||||||
const r3 = await Promise.all(r);
|
const r3 = await Promise.all(r);
|
||||||
for (const r4 of r3) {
|
for (const r4 of r3) {
|
||||||
@@ -477,7 +492,7 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
const f = subContents[i];
|
const f = subContents[i];
|
||||||
contents.push(f);
|
contents.push(f);
|
||||||
if (f.type === "directory") {
|
if (f.type === "directory") {
|
||||||
q.push(f.filename);
|
q.push(stripLeadingPath(f.filename));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +510,11 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
}
|
}
|
||||||
)) as FileStat[];
|
)) as FileStat[];
|
||||||
}
|
}
|
||||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
|
||||||
|
const result = contents
|
||||||
|
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||||
|
.filter((x) => x.keyRaw !== "/");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async walkPartial(): Promise<Entity[]> {
|
async walkPartial(): Promise<Entity[]> {
|
||||||
@@ -508,7 +527,9 @@ export class FakeFsWebdav extends FakeFs {
|
|||||||
details: false /* no need for verbose details here */,
|
details: false /* no need for verbose details here */,
|
||||||
}
|
}
|
||||||
)) as FileStat[];
|
)) as FileStat[];
|
||||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
return contents
|
||||||
|
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||||
|
.filter((x) => x.keyRaw !== "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
async stat(key: string): Promise<Entity> {
|
async stat(key: string): Promise<Entity> {
|
||||||
|
|||||||
@@ -1560,6 +1560,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.dropbox.refreshToken !== "" &&
|
this.settings.dropbox.refreshToken !== "" &&
|
||||||
current >= this.settings!.dropbox!.credentialsShouldBeDeletedAtTime!
|
current >= this.settings!.dropbox!.credentialsShouldBeDeletedAtTime!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`dropbox expired`);
|
||||||
dropboxExpired = true;
|
dropboxExpired = true;
|
||||||
this.settings.dropbox = cloneDeep(DEFAULT_DROPBOX_CONFIG);
|
this.settings.dropbox = cloneDeep(DEFAULT_DROPBOX_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1570,6 +1571,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.onedrive.refreshToken !== "" &&
|
this.settings.onedrive.refreshToken !== "" &&
|
||||||
current >= this.settings!.onedrive!.credentialsShouldBeDeletedAtTime!
|
current >= this.settings!.onedrive!.credentialsShouldBeDeletedAtTime!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`onedrive expired`);
|
||||||
onedriveExpired = true;
|
onedriveExpired = true;
|
||||||
this.settings.onedrive = cloneDeep(DEFAULT_ONEDRIVE_CONFIG);
|
this.settings.onedrive = cloneDeep(DEFAULT_ONEDRIVE_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1580,6 +1582,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.onedrivefull.refreshToken !== "" &&
|
this.settings.onedrivefull.refreshToken !== "" &&
|
||||||
current >= this.settings!.onedrivefull!.credentialsShouldBeDeletedAtTime!
|
current >= this.settings!.onedrivefull!.credentialsShouldBeDeletedAtTime!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`onedrive full expired`);
|
||||||
onedriveFullExpired = true;
|
onedriveFullExpired = true;
|
||||||
this.settings.onedrivefull = cloneDeep(DEFAULT_ONEDRIVEFULL_CONFIG);
|
this.settings.onedrivefull = cloneDeep(DEFAULT_ONEDRIVEFULL_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1590,6 +1593,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.googledrive.refreshToken !== "" &&
|
this.settings.googledrive.refreshToken !== "" &&
|
||||||
current >= this.settings!.googledrive!.credentialsShouldBeDeletedAtTimeMs!
|
current >= this.settings!.googledrive!.credentialsShouldBeDeletedAtTimeMs!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`google drive expired`);
|
||||||
googleDriveExpired = true;
|
googleDriveExpired = true;
|
||||||
this.settings.googledrive = cloneDeep(DEFAULT_GOOGLEDRIVE_CONFIG);
|
this.settings.googledrive = cloneDeep(DEFAULT_GOOGLEDRIVE_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1600,6 +1604,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.box.refreshToken !== "" &&
|
this.settings.box.refreshToken !== "" &&
|
||||||
current >= this.settings!.box!.credentialsShouldBeDeletedAtTimeMs!
|
current >= this.settings!.box!.credentialsShouldBeDeletedAtTimeMs!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`box expired`);
|
||||||
boxExpired = true;
|
boxExpired = true;
|
||||||
this.settings.box = cloneDeep(DEFAULT_BOX_CONFIG);
|
this.settings.box = cloneDeep(DEFAULT_BOX_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1610,6 +1615,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.pcloud.accessToken !== "" &&
|
this.settings.pcloud.accessToken !== "" &&
|
||||||
current >= this.settings!.pcloud!.credentialsShouldBeDeletedAtTimeMs!
|
current >= this.settings!.pcloud!.credentialsShouldBeDeletedAtTimeMs!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`pcloud expired`);
|
||||||
pCloudExpired = true;
|
pCloudExpired = true;
|
||||||
this.settings.pcloud = cloneDeep(DEFAULT_PCLOUD_CONFIG);
|
this.settings.pcloud = cloneDeep(DEFAULT_PCLOUD_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1620,6 +1626,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.yandexdisk.refreshToken !== "" &&
|
this.settings.yandexdisk.refreshToken !== "" &&
|
||||||
current >= this.settings!.yandexdisk!.credentialsShouldBeDeletedAtTimeMs!
|
current >= this.settings!.yandexdisk!.credentialsShouldBeDeletedAtTimeMs!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`yandex disk expired`);
|
||||||
yandexDiskExpired = true;
|
yandexDiskExpired = true;
|
||||||
this.settings.yandexdisk = cloneDeep(DEFAULT_YANDEXDISK_CONFIG);
|
this.settings.yandexdisk = cloneDeep(DEFAULT_YANDEXDISK_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
@@ -1630,6 +1637,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.settings.koofr.refreshToken !== "" &&
|
this.settings.koofr.refreshToken !== "" &&
|
||||||
current >= this.settings!.koofr!.credentialsShouldBeDeletedAtTimeMs!
|
current >= this.settings!.koofr!.credentialsShouldBeDeletedAtTimeMs!
|
||||||
) {
|
) {
|
||||||
|
console.warn(`koofr expired`);
|
||||||
koofrExpired = true;
|
koofrExpired = true;
|
||||||
this.settings.koofr = cloneDeep(DEFAULT_KOOFR_CONFIG);
|
this.settings.koofr = cloneDeep(DEFAULT_KOOFR_CONFIG);
|
||||||
needSave = true;
|
needSave = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user