Compare commits
No commits in common. "master" and "0.5.19" have entirely different histories.
6
.github/workflows/auto-build.yml
vendored
6
.github/workflows/auto-build.yml
vendored
@ -39,13 +39,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout codes
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout LFS file list
|
||||
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
||||
- name: LFS Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .git/lfs/objects
|
||||
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: my-dist
|
||||
path: |
|
||||
|
||||
52
.github/workflows/release.yml
vendored
52
.github/workflows/release.yml
vendored
@ -42,13 +42,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout codes
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout LFS file list
|
||||
run: git lfs ls-files --long | cut -d ' ' -f1 | sort > .lfs-assets-id
|
||||
- name: LFS Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .git/lfs/objects
|
||||
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}
|
||||
@ -63,14 +63,44 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
- name: Create Release And Upload
|
||||
uses: softprops/action-gh-release@v2
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ github.ref }}
|
||||
with:
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
make_latest: true
|
||||
prerelease: true
|
||||
- name: Upload main.js
|
||||
id: upload-main
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./main.js
|
||||
asset_name: main.js
|
||||
asset_content_type: text/javascript
|
||||
- name: Upload manifest.json
|
||||
id: upload-manifest
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./manifest.json
|
||||
asset_name: manifest.json
|
||||
asset_content_type: application/json
|
||||
- name: Upload styles.css
|
||||
id: upload-styles
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./styles.css
|
||||
asset_name: styles.css
|
||||
asset_content_type: text/css
|
||||
|
||||
15
README.md
15
README.md
@ -122,7 +122,6 @@ Additionally, the plugin author may occasionally visit Obsidian official forum a
|
||||
- [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)
|
||||
- [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).
|
||||
- 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**.
|
||||
@ -202,20 +201,8 @@ See [PRO](./docs/pro/README.md) for more details.
|
||||
|
||||
## How To Debug
|
||||
|
||||
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.md).
|
||||
See [here](./docs/how_to_debug/README.md) for more details.
|
||||
|
||||
## Bonus: Import And Export Not-Oauth2 Plugin Settings By QR Code
|
||||
|
||||
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,7 +122,6 @@
|
||||
- [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)
|
||||
- [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)。
|
||||
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
|
||||
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
|
||||
@ -200,22 +199,11 @@ PRO(付费)功能“智能冲突”为用户提供了另一个选项:合
|
||||
|
||||
详见[PRO](./docs/pro/README.md)了解更多详情。
|
||||
|
||||
## 如何调试(debug)
|
||||
## 如何调试
|
||||
|
||||
如发生错误,查看[这里文档](./docs/how_to_debug/README.md)了解调试方式。
|
||||
|
||||
如果没有发生错误,但是运行起来很慢,你需要[开启“性能收集”](./docs/check_performance/README)来看看有没有哪一步特别慢。
|
||||
详见[这里](./docs/how_to_debug/README.md)了解更多详情。
|
||||
|
||||
## 额外功能:通过 QR 码导入和导出非 OAuth2 插件设置
|
||||
|
||||
详见[这里](./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)
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
# Check performance
|
||||
|
||||
1. Go to settings, scroll to the very end, and enable the "Enable Profiler" option.
|
||||
2. Also enable "Enable Profiler Printing".
|
||||
3. Check Console Output (directly or via `vConsole` plugin). More details are [here](../how_to_debug/README.md).
|
||||
4. Sync!
|
||||
5. You can also "Export Profiler Results" afterwards. A new file `_debug_remotely_save/profiler_results_exported_on_xxxx.md` will be generated.
|
||||
|
||||
In the console or exported files, you can see the time cost of each steps.
|
||||
|
||||

|
||||
|
||||

|
||||
@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5a71d0746a75c93aa4cb5be3f7964bfdf74722a10505f5a1965790738eaebdc4
|
||||
size 265289
|
||||
@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ffd1557c2f714c56bfacd8166b85749d6bfa15c2f9c7363942b443d51288083
|
||||
size 81259
|
||||
@ -1,33 +0,0 @@
|
||||
# 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,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.5.25",
|
||||
"version": "0.5.19",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.5.25",
|
||||
"version": "0.5.19",
|
||||
"minAppVersion": "0.13.21",
|
||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "remotely-save",
|
||||
"version": "0.5.25",
|
||||
"version": "0.5.19",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev2": "node esbuild.config.mjs --watch",
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
"settings_azureblobstorage_parts": "Parts Concurrency",
|
||||
"settings_azureblobstorage_parts_desc": "Large files are split into small parts to upload. How many parts do you want to upload in parallel at most?",
|
||||
"settings_azureblobstorage_generatefolderobject": "Generate Folder Object Or Not",
|
||||
"settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage doesn't have \"real\" folder. If you set \"Generate\" here, the plugin will upload a zero-byte object ending with \"/\" to represent the folder. By default, the plugin skips generating folder object.",
|
||||
"settings_azureblobstorage_generatefolderobject_desc": "Azure Blob Storage doesn't have \"real\" folder. If you set \"Generate\" here, the plugin will upload a zero-byte object endding with \"/\" to represent the folder. By default, the plugin skips generating folder object.",
|
||||
"settings_azureblobstorage_generatefolderobject_notgenerate": "Not generate (default)",
|
||||
"settings_azureblobstorage_generatefolderobject_generate": "Generate",
|
||||
"settings_azureblobstorage_connect_succ": "Great! We can connect to Azure Blob Storage!",
|
||||
|
||||
@ -95,10 +95,6 @@ class GoogleDriveAuthModal extends Modal {
|
||||
k.expires_in * 1000;
|
||||
this.plugin.settings.googledrive.accessTokenExpiresAtTimeMs =
|
||||
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();
|
||||
|
||||
// try to remove data in clipboard
|
||||
|
||||
347
pro/src/sync.ts
347
pro/src/sync.ts
@ -29,7 +29,6 @@ import {
|
||||
import {
|
||||
atWhichLevel,
|
||||
checkValidName,
|
||||
getFolderLevels,
|
||||
getParentFolder,
|
||||
isHiddenPath,
|
||||
isSpecialFolderNameToSkip,
|
||||
@ -140,14 +139,7 @@ const isBookmarksFile = (x: string, configDir: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface IsSkipResult {
|
||||
enableAllowMode: boolean;
|
||||
isExplictlyAllowed: boolean;
|
||||
isExplictlyIgnored: boolean;
|
||||
finalIsIgnored: boolean;
|
||||
}
|
||||
|
||||
export const checkIsSkipItemOrNotByName = (
|
||||
const isSkipItemByName = (
|
||||
key: string,
|
||||
syncConfigDir: boolean,
|
||||
syncBookmarks: boolean,
|
||||
@ -155,15 +147,13 @@ export const checkIsSkipItemOrNotByName = (
|
||||
configDir: string,
|
||||
ignorePaths: string[],
|
||||
onlyAllowPaths: string[]
|
||||
): IsSkipResult => {
|
||||
) => {
|
||||
if (key === undefined) {
|
||||
throw Error(`checkIsSkipItemOrNotByName meets undefinded key!`);
|
||||
throw Error(`isSkipItemByName meets undefinded key!`);
|
||||
}
|
||||
|
||||
let finalIsIgnored: boolean | undefined = undefined;
|
||||
|
||||
let enableAllowMode = false;
|
||||
let isExplictlyAllowed = false;
|
||||
let isInAllowList = false;
|
||||
if (onlyAllowPaths !== undefined && onlyAllowPaths.length > 0) {
|
||||
for (const r of onlyAllowPaths) {
|
||||
if (r.trim() === "") {
|
||||
@ -173,7 +163,7 @@ export const checkIsSkipItemOrNotByName = (
|
||||
enableAllowMode = true; // we really want to check the allow list
|
||||
|
||||
if (XRegExp(r, "A").test(key)) {
|
||||
isExplictlyAllowed = true;
|
||||
isInAllowList = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,11 +173,10 @@ export const checkIsSkipItemOrNotByName = (
|
||||
// and is deferred to next checking steps
|
||||
// if the key doesn't meet the allow list,
|
||||
// it must be skippable.
|
||||
if (enableAllowMode && !isExplictlyAllowed) {
|
||||
finalIsIgnored = true; // must be skippable
|
||||
if (enableAllowMode && !isInAllowList) {
|
||||
return true; // must be skippable
|
||||
}
|
||||
|
||||
let isExplictlyIgnored = false;
|
||||
if (ignorePaths !== undefined && ignorePaths.length > 0) {
|
||||
for (const r of ignorePaths) {
|
||||
if (r.trim() === "") {
|
||||
@ -195,163 +184,29 @@ export const checkIsSkipItemOrNotByName = (
|
||||
continue;
|
||||
}
|
||||
if (XRegExp(r, "A").test(key)) {
|
||||
if (finalIsIgnored === undefined) {
|
||||
isExplictlyIgnored = true;
|
||||
finalIsIgnored = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncConfigDir && isInsideObsFolder(key, configDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// sync config, not sync bookmarks: sync config and **force syncing bookmarks as well**
|
||||
// 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 (syncBookmarks && isBookmarksFile(key, configDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSpecialFolderNameToSkip(key, [])) {
|
||||
// some special dirs and files are always skipped
|
||||
if (finalIsIgnored === undefined) {
|
||||
isExplictlyIgnored = true;
|
||||
finalIsIgnored = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const checkIsHidden =
|
||||
return (
|
||||
isHiddenPath(key, true, false) ||
|
||||
(!syncUnderscoreItems && isHiddenPath(key, false, true)) ||
|
||||
key === "/" ||
|
||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE ||
|
||||
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;
|
||||
key === DEFAULT_FILE_NAME_FOR_METADATAONREMOTE2
|
||||
);
|
||||
};
|
||||
|
||||
export type SyncPlanType = Record<string, MixedEntity>;
|
||||
@ -380,39 +235,33 @@ const ensembleMixedEnties = async (
|
||||
|
||||
const finalMappings: SyncPlanType = {};
|
||||
|
||||
const skipOrNotResults: Record<string, IsSkipResult> = {};
|
||||
|
||||
// remote has to be first
|
||||
let remoteMaySkipCountAndNotConfig = 0;
|
||||
for (const remote of remoteEntityList) {
|
||||
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
||||
copyEntityAndFixTimeFormat(remote, serviceType)
|
||||
);
|
||||
|
||||
const key = remoteCopied.key!;
|
||||
|
||||
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||
key,
|
||||
syncConfigDir,
|
||||
syncBookmarks,
|
||||
syncUnderscoreItems,
|
||||
configDir,
|
||||
ignorePaths,
|
||||
onlyAllowPaths
|
||||
);
|
||||
skipOrNotResults[key] = skipOrNot;
|
||||
if (skipOrNot.finalIsIgnored && !key.startsWith(configDir)) {
|
||||
remoteMaySkipCountAndNotConfig += 1;
|
||||
if (
|
||||
isSkipItemByName(
|
||||
key,
|
||||
syncConfigDir,
|
||||
syncBookmarks,
|
||||
syncUnderscoreItems,
|
||||
configDir,
|
||||
ignorePaths,
|
||||
onlyAllowPaths
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 20240907: users (not on windows) doesn't like it. revert back now.
|
||||
// TODO: platform specific but not introducing obsidian dependency into sync.ts
|
||||
// const checkValidNameResult = checkValidName(key);
|
||||
// if (!checkValidNameResult.result) {
|
||||
// throw Error(
|
||||
// `your remote folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||
// );
|
||||
// }
|
||||
const checkValidNameResult = checkValidName(key);
|
||||
if (!checkValidNameResult.result) {
|
||||
throw Error(
|
||||
`your remote folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||
);
|
||||
}
|
||||
|
||||
finalMappings[key] = {
|
||||
key: key,
|
||||
@ -423,25 +272,19 @@ const ensembleMixedEnties = async (
|
||||
profiler?.insert("ensembleMixedEnties: finish remote");
|
||||
profiler?.insertSize("sizeof finalMappings", finalMappings);
|
||||
|
||||
if (
|
||||
Object.keys(finalMappings).filter((k) => !k.startsWith(configDir)).length -
|
||||
remoteMaySkipCountAndNotConfig ===
|
||||
0 ||
|
||||
localEntityList.filter((e) => !e.key?.startsWith(configDir)).length === 0
|
||||
) {
|
||||
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
||||
// Special checking:
|
||||
// if one side is totally empty,
|
||||
// usually that's a hard rest.
|
||||
// So we need to ignore everything of prevSyncEntityList to avoid deletions!
|
||||
// TODO: acutally erase everything of prevSyncEntityList?
|
||||
// TODO: local should also go through a checkIsSkipItemOrNotByName checking beforehand
|
||||
// TODO: local should also go through a isSkipItemByName checking beforehand
|
||||
} else {
|
||||
// normally go through the prevSyncEntityList
|
||||
for (const prevSync of prevSyncEntityList) {
|
||||
const key = prevSync.key!;
|
||||
|
||||
if (!(key in skipOrNotResults)) {
|
||||
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||
if (
|
||||
isSkipItemByName(
|
||||
key,
|
||||
syncConfigDir,
|
||||
syncBookmarks,
|
||||
@ -449,8 +292,9 @@ const ensembleMixedEnties = async (
|
||||
configDir,
|
||||
ignorePaths,
|
||||
onlyAllowPaths
|
||||
);
|
||||
skipOrNotResults[key] = skipOrNot;
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: abstraction leaking?
|
||||
@ -476,9 +320,8 @@ const ensembleMixedEnties = async (
|
||||
// (we don't consume prevSync here because it gains no benefit)
|
||||
for (const local of localEntityList) {
|
||||
const key = local.key!;
|
||||
|
||||
if (!(key in skipOrNotResults)) {
|
||||
const skipOrNot = checkIsSkipItemOrNotByName(
|
||||
if (
|
||||
isSkipItemByName(
|
||||
key,
|
||||
syncConfigDir,
|
||||
syncBookmarks,
|
||||
@ -486,18 +329,17 @@ const ensembleMixedEnties = async (
|
||||
configDir,
|
||||
ignorePaths,
|
||||
onlyAllowPaths
|
||||
);
|
||||
skipOrNotResults[key] = skipOrNot;
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 20240907: users (not on windows) doesn't like it. revert back now.
|
||||
// TODO: platform specific but not introducing obsidian dependency into sync.ts
|
||||
// const checkValidNameResult = checkValidName(key);
|
||||
// if (!checkValidNameResult.result) {
|
||||
// throw Error(
|
||||
// `your local folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||
// );
|
||||
// }
|
||||
const checkValidNameResult = checkValidName(key);
|
||||
if (!checkValidNameResult.result) {
|
||||
throw Error(
|
||||
`your local folder/file name is invalid: ${checkValidNameResult.reason}`
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: abstraction leaking?
|
||||
const localCopied = await fsEncrypt.encryptEntity(
|
||||
@ -516,15 +358,6 @@ const ensembleMixedEnties = async (
|
||||
profiler?.insert("ensembleMixedEnties: finish local");
|
||||
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(finalMappings);
|
||||
|
||||
@ -550,7 +383,7 @@ const getSyncPlanInplace = async (
|
||||
) => {
|
||||
profiler?.addIndent();
|
||||
profiler?.insert("getSyncPlanInplace: enter");
|
||||
// from long(deep) to short(shadow), descending
|
||||
// from long(deep) to short(shadow)
|
||||
const sortedKeys = Object.keys(mixedEntityMappings).sort(
|
||||
(k1, k2) => k2.length - k1.length
|
||||
);
|
||||
@ -643,36 +476,14 @@ const getSyncPlanInplace = async (
|
||||
mixedEntry.change = false;
|
||||
keptFolder.add(getParentFolder(key));
|
||||
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
||||
if (
|
||||
key === `${configDir}/` ||
|
||||
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;
|
||||
}
|
||||
mixedEntry.decisionBranch = 135;
|
||||
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
||||
mixedEntry.change = true;
|
||||
} else {
|
||||
// bidirectional
|
||||
if (
|
||||
key === `${configDir}/` ||
|
||||
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;
|
||||
}
|
||||
mixedEntry.decisionBranch = 124;
|
||||
mixedEntry.decision = "folder_to_be_deleted_on_local";
|
||||
mixedEntry.change = true;
|
||||
}
|
||||
} else {
|
||||
// then the folder is created on local
|
||||
@ -1114,35 +925,13 @@ const getSyncPlanInplace = async (
|
||||
mixedEntry.decision = "conflict_created_then_do_nothing";
|
||||
mixedEntry.change = false;
|
||||
} else if (syncDirection === "incremental_pull_and_delete_only") {
|
||||
if (
|
||||
key === `${configDir}/` ||
|
||||
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;
|
||||
}
|
||||
mixedEntry.decisionBranch = 39;
|
||||
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
||||
mixedEntry.change = true;
|
||||
} else {
|
||||
if (
|
||||
key === `${configDir}/` ||
|
||||
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;
|
||||
}
|
||||
mixedEntry.decisionBranch = 7;
|
||||
mixedEntry.decision = "remote_is_deleted_thus_also_delete_local";
|
||||
mixedEntry.change = true;
|
||||
}
|
||||
} else {
|
||||
// if A is in the previous list and MODIFIED, A has been deleted by B but modified by A
|
||||
@ -1930,7 +1719,7 @@ export async function syncer(
|
||||
) => any,
|
||||
callbackSyncProcess?: any
|
||||
) {
|
||||
console.info(`starting sync.`);
|
||||
console.info(`startting sync.`);
|
||||
markIsSyncingFunc(true);
|
||||
|
||||
let everythingOk = true;
|
||||
@ -2089,6 +1878,6 @@ export async function syncer(
|
||||
await ribboonFunc?.(triggerSource, step);
|
||||
await statusBarFunc?.(triggerSource, step, everythingOk);
|
||||
|
||||
console.info(`ending sync.`);
|
||||
console.info(`endding sync.`);
|
||||
markIsSyncingFunc(false);
|
||||
}
|
||||
|
||||
@ -1,154 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -140,7 +140,6 @@ export type CipherMethodType = "rclone-base64" | "openssl-base64" | "unknown";
|
||||
export type QRExportType = "basic_and_advanced" | SUPPORTED_SERVICES_TYPE;
|
||||
|
||||
export interface ProfilerConfig {
|
||||
enable?: boolean;
|
||||
enablePrinting?: boolean;
|
||||
recordSize?: boolean;
|
||||
}
|
||||
|
||||
@ -252,10 +252,6 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
// why?? /drive/root:/Apps/Graph
|
||||
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,
|
||||
// e.g.: /Livefolders/应用/remotely-save/${remoteBaseDir}
|
||||
const SECOND_COMMON_PREFIX_REGEX = /^\/Livefolders\/[^\/]+\/remotely-save\//g;
|
||||
@ -282,11 +278,6 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
const fullPathOriginal = `${x.parentReference.path}/${x.name}`;
|
||||
const matchFirstPrefixRes = fullPathOriginal.match(FIRST_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(
|
||||
SECOND_COMMON_PREFIX_REGEX
|
||||
);
|
||||
@ -326,40 +317,6 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
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
|
||||
else if (
|
||||
matchSecondPrefixRes !== null &&
|
||||
@ -417,8 +374,6 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
fullPathOriginal=${fullPathOriginal}
|
||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||
${constructFromDriveItemToEntityError(x)}`
|
||||
@ -433,8 +388,6 @@ ${constructFromDriveItemToEntityError(x)}`
|
||||
fullPathOriginal=${fullPathOriginal}
|
||||
matchFirstPrefixRes=${matchFirstPrefixRes}
|
||||
matchFifthPrefixRes=${matchFifthPrefixRes}
|
||||
matchSixthPrefixRes=${matchSixthPrefixRes}
|
||||
matchSixthV2PrefixRes=${matchSixthV2PrefixRes}
|
||||
matchSecondPrefixRes=${matchSecondPrefixRes}
|
||||
matchThirdPrefixRes=${matchThirdPrefixRes}
|
||||
${constructFromDriveItemToEntityError(x)}`
|
||||
|
||||
@ -138,6 +138,7 @@ if (VALID_REQURL) {
|
||||
);
|
||||
}
|
||||
|
||||
import isEqual from "lodash/isEqual";
|
||||
// @ts-ignore
|
||||
// biome-ignore lint: we want to ts-ignore the next line
|
||||
import { AuthType, BufferLike, createClient } from "webdav/dist/web/index.js";
|
||||
@ -168,37 +169,23 @@ const getWebdavPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||
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 strippedFileOrFolderPath = stripLeadingPath(fileOrFolderPath);
|
||||
if (
|
||||
!(
|
||||
strippedFileOrFolderPath === `/${remoteBaseDir}` ||
|
||||
strippedFileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||
fileOrFolderPath === `/${remoteBaseDir}` ||
|
||||
fileOrFolderPath.startsWith(`/${remoteBaseDir}/`)
|
||||
)
|
||||
) {
|
||||
throw Error(
|
||||
`"${fileOrFolderPath}" after stripping doesn't starts with "/${remoteBaseDir}/"`
|
||||
`"${fileOrFolderPath}" doesn't starts with "/${remoteBaseDir}/"`
|
||||
);
|
||||
}
|
||||
const result = strippedFileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||
return result;
|
||||
|
||||
return fileOrFolderPath.slice(`/${remoteBaseDir}/`.length);
|
||||
};
|
||||
|
||||
const fromWebdavItemToEntity = (x: FileStat, remoteBaseDir: string): Entity => {
|
||||
let key = getNormPath(x.filename, remoteBaseDir);
|
||||
|
||||
if (x.type === "directory" && !key.endsWith("/")) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
@ -460,17 +447,15 @@ export class FakeFsWebdav extends FakeFs {
|
||||
// console.debug(itemsToFetchChunks);
|
||||
const subContents = [] as FileStat[];
|
||||
for (const singleChunk of itemsToFetchChunks) {
|
||||
const r = singleChunk.map(async (x) => {
|
||||
let k = (await this.client.getDirectoryContents(x, {
|
||||
const r = singleChunk.map((x) => {
|
||||
return this.client.getDirectoryContents(x, {
|
||||
deep: false,
|
||||
details: false /* no need for verbose details here */,
|
||||
// TODO: to support .obsidian,
|
||||
// we need to load all files including dot,
|
||||
// anyway to reduce the resources?
|
||||
// glob: "/**" /* avoid dot files by using glob */,
|
||||
})) as FileStat[];
|
||||
k = k.filter((sub) => stripLeadingPath(sub.filename) !== x);
|
||||
return k;
|
||||
}) as Promise<FileStat[]>;
|
||||
});
|
||||
const r3 = await Promise.all(r);
|
||||
for (const r4 of r3) {
|
||||
@ -492,7 +477,7 @@ export class FakeFsWebdav extends FakeFs {
|
||||
const f = subContents[i];
|
||||
contents.push(f);
|
||||
if (f.type === "directory") {
|
||||
q.push(stripLeadingPath(f.filename));
|
||||
q.push(f.filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -510,11 +495,7 @@ export class FakeFsWebdav extends FakeFs {
|
||||
}
|
||||
)) as FileStat[];
|
||||
}
|
||||
|
||||
const result = contents
|
||||
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.keyRaw !== "/");
|
||||
return result;
|
||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
||||
}
|
||||
|
||||
async walkPartial(): Promise<Entity[]> {
|
||||
@ -527,9 +508,7 @@ export class FakeFsWebdav extends FakeFs {
|
||||
details: false /* no need for verbose details here */,
|
||||
}
|
||||
)) as FileStat[];
|
||||
return contents
|
||||
.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.keyRaw !== "/");
|
||||
return contents.map((x) => fromWebdavItemToEntity(x, this.remoteBaseDir));
|
||||
}
|
||||
|
||||
async stat(key: string): Promise<Entity> {
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
"protocol_dropbox_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_dropbox_connect_fail": "Something went wrong from response from Dropbox. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
"protocol_dropbox_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||
"protocol_dropbox_no_modal": "You are not starting Dropbox connection from the settings page. Abort.",
|
||||
"protocol_dropbox_no_modal": "You are not startting Dropbox connection from the settings page. Abort.",
|
||||
"protocol_onedrive_connecting": "Connecting to OneDrive...\nPlease DO NOT close this modal.",
|
||||
"protocol_onedrive_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_onedrive_connect_fail": "Something went wrong from response from OneDrive. Maybe you rejected the auth?",
|
||||
@ -110,7 +110,7 @@
|
||||
"modal_onedriverevokeauth_clean_button": "Clean",
|
||||
"modal_onedriverevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_onedriverevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other starting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||
"modal_syncconfig_attn": "Attention 1/2: This only syncs (copies) the whole Obsidian config dir, not other startting-with-dot folders or files. Except for ignoring folders .git and node_modules, it also doesn't understand the meaning of sub-files and sub-folders inside the config dir.\nAttention 2/2: After the config dir is synced, plugins settings might be corrupted, and Obsidian might need to be restarted to load the new settings.\nIf you are agreed to take your own risk, please click the following second confirm button.",
|
||||
"modal_syncconfig_secondconfirm": "The Second Confirm To Enable.",
|
||||
"modal_syncconfig_notice": "You've enabled syncing config folder!",
|
||||
"modal_qr_shortdesc": "This exports (partial) settings.\nYou can use another device to scan this qrcode.\nOr, you can click the button to copy the special uri and paste it into another device's web browser or Remotely Save Import Setting.",
|
||||
@ -194,7 +194,7 @@
|
||||
"settings_s3_reverse_proxy_no_sign_url": "S3 Reverse Proxy (No Sign) Url (experimental)",
|
||||
"settings_s3_reverse_proxy_no_sign_url_desc": "S3 reverse proxy url without signature. This is useful if you use a revers proxy but do not change the original credential signature. No http(s):// prefix. Leave it blank if you don't know what it is.",
|
||||
"settings_s3_generatefolderobject": "Generate Folder Object Or Not",
|
||||
"settings_s3_generatefolderobject_desc": "S3 doesn't have \"real\" folder. If you set \"Generate\" here (or use old version), the plugin will upload a zero-byte object ending with \"/\" to represent the folder. In the new version, the plugin skips generating folder object by default.",
|
||||
"settings_s3_generatefolderobject_desc": "S3 doesn't have \"real\" folder. If you set \"Generate\" here (or use old version), the plugin will upload a zero-byte object endding with \"/\" to represent the folder. In the new version, the plugin skips generating folder object by default.",
|
||||
"settings_s3_generatefolderobject_notgenerate": "Not generate (default)",
|
||||
"settings_s3_generatefolderobject_generate": "Generate",
|
||||
"settings_s3_connect_succ": "Great! The bucket can be accessed.",
|
||||
@ -340,7 +340,7 @@
|
||||
"settings_obfuscatesettingfile": "Obfuscate The Setting File Or Not",
|
||||
"settings_obfuscatesettingfile_desc": "The setting file (data.json) has some sensitive information. It's strongly recommended to obfuscate it to avoid unexpected read and modification. If you are sure to view and edit it manually, you can disable the obfuscation.",
|
||||
"settings_viewconsolelog": "View Console Log",
|
||||
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+option+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
||||
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
||||
"settings_syncplans": "Export Sync Plans",
|
||||
"settings_syncplans_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
|
||||
"settings_syncplans_button_1_only_change": "Export latest 1 (change part)",
|
||||
@ -361,8 +361,6 @@
|
||||
"settings_profiler_results_desc": "The plugin records the time cost of each steps. Here you can export them to know which step is slow.",
|
||||
"settings_profiler_results_notice": "Profiler results exported.",
|
||||
"settings_profiler_results_button_all": "Export All",
|
||||
"settings_profiler_enableprofiler": "Enable Profiler",
|
||||
"settings_profiler_enableprofiler_desc": "Collect performance data or not?",
|
||||
"settings_profiler_enabledebugprint": "Enable Profiler Printing",
|
||||
"settings_profiler_enabledebugprint_desc": "Print profiler result in each insertion to console or not?",
|
||||
"settings_profiler_recordsize": "Enable Profiler Recording Size",
|
||||
|
||||
@ -360,12 +360,6 @@
|
||||
"settings_profiler_results_desc": "插件记录了每次同步每一步的耗时。这里可以导出记录得知哪一步最慢。",
|
||||
"settings_profiler_results_notice": "性能数据已导出",
|
||||
"settings_profiler_results_button_all": "导出所有",
|
||||
"settings_profiler_enableprofiler": "性能收集",
|
||||
"settings_profiler_enableprofiler_desc": "是否开启性能收集功能?",
|
||||
"settings_profiler_enabledebugprint": "性能收集输出",
|
||||
"settings_profiler_enabledebugprint_desc": "是否直接输出性能收集结果到终端里?",
|
||||
"settings_profiler_recordsize": "性能收集统计对象大小",
|
||||
"settings_profiler_recordsize_desc": "是否收集对象的大小?",
|
||||
"settings_outputbasepathvaultid": "输出资料库对应的位置和随机分配的 ID",
|
||||
"settings_outputbasepathvaultid_desc": "用于调试。",
|
||||
"settings_outputbasepathvaultid_button": "输出",
|
||||
|
||||
@ -359,12 +359,6 @@
|
||||
"settings_profiler_results_desc": "外掛記錄了每次同步每一步的耗時。這裡可以匯出記錄得知哪一步最慢。",
|
||||
"settings_profiler_results_notice": "效能資料已匯出",
|
||||
"settings_profiler_results_button_all": "匯出所有",
|
||||
"settings_profiler_enableprofiler": "效能收集",
|
||||
"settings_profiler_enableprofiler_desc": "是否開啟效能收集功能?",
|
||||
"settings_profiler_enabledebugprint": "效能收集輸出",
|
||||
"settings_profiler_enabledebugprint_desc": "是否直接輸出效能收集結果到終端裡?",
|
||||
"settings_profiler_recordsize": "效能收集統計物件大小",
|
||||
"settings_profiler_recordsize_desc": "是否收集物件的大小?",
|
||||
"settings_outputbasepathvaultid": "輸出資料庫對應的位置和隨機分配的 ID",
|
||||
"settings_outputbasepathvaultid_desc": "用於除錯。",
|
||||
"settings_outputbasepathvaultid_button": "輸出",
|
||||
|
||||
27
src/main.ts
27
src/main.ts
@ -108,7 +108,7 @@ import {
|
||||
upsertPluginVersionByVault,
|
||||
} from "./localdb";
|
||||
import { changeMobileStatusBar } from "./misc";
|
||||
import { DEFAULT_PROFILER_CONFIG, Profiler } from "./profiler";
|
||||
import { DEFAULT_PROFILER_CONFIG, type Profiler } from "./profiler";
|
||||
import { RemotelySaveSettingTab } from "./settings";
|
||||
import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
|
||||
|
||||
@ -232,14 +232,12 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
appContainerObserver?: MutationObserver;
|
||||
|
||||
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
||||
let profiler: Profiler | undefined = undefined;
|
||||
if (this.settings.profiler?.enable ?? false) {
|
||||
profiler = new Profiler(
|
||||
undefined,
|
||||
this.settings.profiler?.enablePrinting ?? false,
|
||||
this.settings.profiler?.recordSize ?? false
|
||||
);
|
||||
}
|
||||
// const profiler = new Profiler(
|
||||
// undefined,
|
||||
// this.settings.profiler?.enablePrinting ?? false,
|
||||
// this.settings.profiler?.recordSize ?? false
|
||||
// );
|
||||
const profiler: Profiler | undefined = undefined;
|
||||
const fsLocal = new FakeFsLocal(
|
||||
this.app.vault,
|
||||
this.settings.syncConfigDir ?? false,
|
||||
@ -1473,9 +1471,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
if (this.settings.profiler === undefined) {
|
||||
this.settings.profiler = DEFAULT_PROFILER_CONFIG;
|
||||
}
|
||||
if (this.settings.profiler.enable === undefined) {
|
||||
this.settings.profiler.enable = false;
|
||||
}
|
||||
if (this.settings.profiler.enablePrinting === undefined) {
|
||||
this.settings.profiler.enablePrinting = false;
|
||||
}
|
||||
@ -1560,7 +1555,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.dropbox.refreshToken !== "" &&
|
||||
current >= this.settings!.dropbox!.credentialsShouldBeDeletedAtTime!
|
||||
) {
|
||||
console.warn(`dropbox expired`);
|
||||
dropboxExpired = true;
|
||||
this.settings.dropbox = cloneDeep(DEFAULT_DROPBOX_CONFIG);
|
||||
needSave = true;
|
||||
@ -1571,7 +1565,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.onedrive.refreshToken !== "" &&
|
||||
current >= this.settings!.onedrive!.credentialsShouldBeDeletedAtTime!
|
||||
) {
|
||||
console.warn(`onedrive expired`);
|
||||
onedriveExpired = true;
|
||||
this.settings.onedrive = cloneDeep(DEFAULT_ONEDRIVE_CONFIG);
|
||||
needSave = true;
|
||||
@ -1582,7 +1575,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.onedrivefull.refreshToken !== "" &&
|
||||
current >= this.settings!.onedrivefull!.credentialsShouldBeDeletedAtTime!
|
||||
) {
|
||||
console.warn(`onedrive full expired`);
|
||||
onedriveFullExpired = true;
|
||||
this.settings.onedrivefull = cloneDeep(DEFAULT_ONEDRIVEFULL_CONFIG);
|
||||
needSave = true;
|
||||
@ -1593,7 +1585,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.googledrive.refreshToken !== "" &&
|
||||
current >= this.settings!.googledrive!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
console.warn(`google drive expired`);
|
||||
googleDriveExpired = true;
|
||||
this.settings.googledrive = cloneDeep(DEFAULT_GOOGLEDRIVE_CONFIG);
|
||||
needSave = true;
|
||||
@ -1604,7 +1595,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.box.refreshToken !== "" &&
|
||||
current >= this.settings!.box!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
console.warn(`box expired`);
|
||||
boxExpired = true;
|
||||
this.settings.box = cloneDeep(DEFAULT_BOX_CONFIG);
|
||||
needSave = true;
|
||||
@ -1615,7 +1605,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.pcloud.accessToken !== "" &&
|
||||
current >= this.settings!.pcloud!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
console.warn(`pcloud expired`);
|
||||
pCloudExpired = true;
|
||||
this.settings.pcloud = cloneDeep(DEFAULT_PCLOUD_CONFIG);
|
||||
needSave = true;
|
||||
@ -1626,7 +1615,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.yandexdisk.refreshToken !== "" &&
|
||||
current >= this.settings!.yandexdisk!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
console.warn(`yandex disk expired`);
|
||||
yandexDiskExpired = true;
|
||||
this.settings.yandexdisk = cloneDeep(DEFAULT_YANDEXDISK_CONFIG);
|
||||
needSave = true;
|
||||
@ -1637,7 +1625,6 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.koofr.refreshToken !== "" &&
|
||||
current >= this.settings!.koofr!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
console.warn(`koofr expired`);
|
||||
koofrExpired = true;
|
||||
this.settings.koofr = cloneDeep(DEFAULT_KOOFR_CONFIG);
|
||||
needSave = true;
|
||||
|
||||
@ -279,7 +279,7 @@ export const reverseString = (x: string) => {
|
||||
};
|
||||
|
||||
export interface SplitRange {
|
||||
partNum: number; // starting from 1
|
||||
partNum: number; // startting from 1
|
||||
start: number;
|
||||
end: number; // exclusive
|
||||
}
|
||||
@ -316,7 +316,7 @@ export const getTypeName = (obj: any) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* starting from 1
|
||||
* Startting from 1
|
||||
* @param x
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@ -10,7 +10,6 @@ interface BreakPoint {
|
||||
}
|
||||
|
||||
export const DEFAULT_PROFILER_CONFIG: ProfilerConfig = {
|
||||
enable: false,
|
||||
enablePrinting: false,
|
||||
recordSize: false,
|
||||
};
|
||||
|
||||
@ -2940,25 +2940,6 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(debugDiv)
|
||||
.setName(t("settings_profiler_enableprofiler"))
|
||||
.setDesc(t("settings_profiler_enableprofiler_desc"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("enable", t("enable"));
|
||||
dropdown.addOption("disable", t("disable"));
|
||||
dropdown
|
||||
.setValue(
|
||||
this.plugin.settings.profiler?.enable ? "enable" : "disable"
|
||||
)
|
||||
.onChange(async (val: string) => {
|
||||
if (this.plugin.settings.profiler === undefined) {
|
||||
this.plugin.settings.profiler = DEFAULT_PROFILER_CONFIG;
|
||||
}
|
||||
this.plugin.settings.profiler.enable = val === "enable";
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(debugDiv)
|
||||
.setName(t("settings_profiler_enabledebugprint"))
|
||||
.setDesc(t("settings_profiler_enabledebugprint_desc"))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user