Compare commits

..
7 Commits
Author SHA1 Message Date
fyears 245d529821 bump to 0.5.19
Release A New Version / build (20.x) (push) Failing after 43s
2024-09-01 17:29:48 +08:00
fyears 74e5231861 not transfer data to worker for safety 2024-09-01 17:26:32 +08:00
fyears ac9c2a9248 mini support to ctimeCli 2024-09-01 17:17:08 +08:00
fyears b2dea5a351 add allow list mode 2024-09-01 16:48:35 +08:00
fyears e9b9cd97ac check if folder/file name is valid 2024-09-01 16:15:42 +08:00
fyears fccebb0594 fix error notice 2024-09-01 16:15:17 +08:00
fyears dda304c857 add cn for readme 2024-08-04 00:25:21 +08:00
27 changed files with 552 additions and 28 deletions
+8 -6
View File
@@ -1,5 +1,7 @@
# Remotely Save # Remotely Save
English | [中文](./README.zh-cn.md)
This is yet another unofficial sync plugin for Obsidian. If you like it or find it useful, please consider give it a [star ![GitHub Repo stars](https://img.shields.io/github/stars/fyears/remotely-save?style=social)](https://github.com/fyears/remotely-save) on Github. This is yet another unofficial sync plugin for Obsidian. If you like it or find it useful, please consider give it a [star ![GitHub Repo stars](https://img.shields.io/github/stars/fyears/remotely-save?style=social)](https://github.com/fyears/remotely-save) on Github.
[![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) [![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
@@ -31,7 +33,7 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
- Azure Blob Storage (PRO feature) - Azure Blob Storage (PRO feature)
- [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details. - [Here](./docs/services_connectable_or_not.md) shows more connectable (or not-connectable) services in details.
- **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker". - **Obsidian Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
- **[End-to-end encryption](./docs/encryption/README.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password. - **[End-to-end encryption](./docs/encryption/README.md) supported.** Files would be encrypted using openssl / rclone crypt format before being sent to the cloud **if** user specify a password.
- **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination). - **Scheduled auto sync supported.** You can also manually trigger the sync using sidebar ribbon, or using the command from the command palette (or even bind the hot key combination to the command then press the hot key combination).
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).** - **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
- **Skip Large files** and **skip paths** by custom regex conditions! - **Skip Large files** and **skip paths** by custom regex conditions!
@@ -53,16 +55,16 @@ This is yet another unofficial sync plugin for Obsidian. If you like it or find
You are greatly welcome to ask questions, post any suggestions, or report any bugs! The project is mainly maintained on GitHub: You are greatly welcome to ask questions, post any suggestions, or report any bugs! The project is mainly maintained on GitHub:
- Questions: [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions) - Questions: [GitHub repo Discussions](https://github.com/remotely-save/remotely-save/discussions)
- Suggestions: also in [GitHub repo Discussions](https://github.com/fyears/remotely-save/discussions) - Suggestions: also in [GitHub repo Discussions](https://github.com/remotely-save/remotely-save/discussions)
- Bugs: [GitHub repo Issues](https://github.com/fyears/remotely-save/issues) (NOT Discussion) - Bugs: [GitHub repo Issues](https://github.com/remotely-save/remotely-save/issues) (NOT Discussion)
Additionally, the plugin author may occasionally visit Obsidian official forum and official Discord server, and pay attention to this-plugin-related information there. Additionally, the plugin author may occasionally visit Obsidian official forum and official Discord server, and pay attention to this-plugin-related information there.
## Download and Install ## Download and Install
- Option #1: Search in the official "community plugin list", or visit this: [https://obsidian.md/plugins?id=remotely-save](https://obsidian.md/plugins?id=remotely-save) (which should redirect you into Obsidian app), then install the plugin. - Option #1: Search in the official "community plugin list", or visit this: [https://obsidian.md/plugins?id=remotely-save](https://obsidian.md/plugins?id=remotely-save) (which should redirect you into Obsidian app), then install the plugin.
- Option #2: You can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. Input `fyears/remotely-save` in the configuration of BRAT. - Option #2: You can also use [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) to install this plugin. Input `remotely-save/remotely-save` in the configuration of BRAT.
- Option #3: [![GitHub release (latest by SemVer and asset including pre-releases)](https://img.shields.io/github/downloads-pre/fyears/remotely-save/latest/main.js?sort=semver)](https://github.com/fyears/remotely-save/releases) Manually download assets (`main.js`, `manifest.json`, `styles.css`) from the latest release. - Option #3: [![GitHub release (latest by SemVer and asset including pre-releases)](https://img.shields.io/github/downloads-pre/fyears/remotely-save/latest/main.js?sort=semver)](https://github.com/fyears/remotely-save/releases) Manually download assets (`main.js`, `manifest.json`, `styles.css`) from the latest release.
- Option #4: [![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. It's automatically generated by every commit, may break something. - Option #4: [![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) Every artifacts are placed in the "Summary" under every successful builds. It's automatically generated by every commit, may break something.
@@ -193,7 +195,7 @@ Moreover, the bookmarks of Obsidian are actually stored in `.obsidian/bookmarks.
In the latest version, you can change the settings to allow syncing `_` files or folders, as well as `.obsidian` special config folder as described above (but not any other `.` files or folders). In the latest version, you can change the settings to allow syncing `_` files or folders, as well as `.obsidian` special config folder as described above (but not any other `.` files or folders).
## PRO Features ## PRO (paid) Features
See [PRO](./docs/pro/README.md) for more details. See [PRO](./docs/pro/README.md) for more details.
+209
View File
@@ -0,0 +1,209 @@
# Remotely Save
[English](./README.md) | 中文
这是Obsidian的一个非官方同步插件。如果你喜欢它或觉得它帮到了你,请考虑在Github上给它一颗[星星 ![GitHub Repo stars](https://img.shields.io/github/stars/fyears/remotely-save?style=social)](https://github.com/fyears/remotely-save)。
[![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml)
[![最新版本的下载量](https://img.shields.io/github/downloads-pre/remotely-save/remotely-save/latest/main.js?sort=semver)](https://github.com/fyears/remotely-save/releases)
## 免责声明
- **这不是 Obsidian 提供的[官方同步服务](https://obsidian.md/sync)。**
## !!!警告!!!
**在使用此插件之前,一定,一定要记得备份你的 vault。**
## 功能
- 支持:
- Amazon S3 或兼容 S3 的服务(Cloudflare R2 / BackBlaze B2 / MinIO / ...
- Dropbox
- 个人版本 OneDrive(应用文件夹)
- 个人版本 OneDrive(根目录)(PRO 功能)
- WebdavNextCloud / InfiniCloud / Synology webdav 服务器 / ...
- Webdis
- Google DriveGDrive)(PRO 功能)
- BoxPRO 功能)
- pCloudPRO 功能)
- Yandex DiskPRO 功能)
- KoofrPRO 功能)
- Azure Blob StoragePRO 功能)
- [这里](./docs/services_connectable_or_not.md)详细展示了更多可连接(或不可连接)的服务。
- **支持 Obsidian 移动版。** vault 可以通过云服务作为“中介”在移动和桌面设备之间同步。
- **支持[端到端加密](./docs/encryption/README.md)。** 如果用户指定密码,文件在发送到云之前会使用 openssl / rclone crypt 格式加密。
- **支持计划自动同步。** 你也可以使用侧边栏按钮,或者或命令面板中的命令,来手动触发同步(又或者绑定热键组合然后触发)。
- **[最小侵入性](./docs/minimal_intrusive_design.md)。**
- 通过自定义正则表达式条件**跳过大文件和路径!**
- **[同步算法](./docs/sync_algorithm/v3/intro.md)文档公开。**
- 免费版本支持 **[基本冲突检测和处理](./docs/sync_algorithm/v3/intro.md)**。PRO 版本支持 **[高级智能冲突处理](./pro/README.md)**。
- 源代码可阅。详见[许可证](./LICENSE)。
## 限制
- **云服务会产生费用。** 始终记得注意成本和定价。具体来说,所有操作,包括但不限于下载、上传、列出所有文件、调用任何 api、存储大小,可能会或可能不会产生费用。
- **来自浏览器环境的一些限制。** 更多技术细节在[文档](./docs/browser_env.md)中。
- **记得保护你的 `data.json` 文件。** 该文件包含敏感信息。
- 强烈建议**不要**与任何人共享你的 `data.json` 文件。
- 通常**不要**将此文件检入版本控制。默认情况下,插件会尝试在插件目录中创建一个 `.gitignore` 文件(如果不存在),以忽略 `git` 版本控制中的 `data.json`。如果你确切知道这意味着什么并想移除设置,请修改 `.gitignore` 文件或将其设置为空。
- **Obsidian 移动版 API 在同步大文件(>= 50 MB)时存在性能问题。**
- 设置“跳过大文件”选项可以帮助解决同步大文件的问题。
## 问题、建议或错误
非常欢迎你提出问题、发布任何建议或报告任何错误!该项目主要在GitHub上维护:
- 问题:[GitHub 仓库讨论](https://github.com/remotely-save/remotely-save/discussions)
- 建议:也在[GitHub 仓库讨论](https://github.com/remotely-save/remotely-save/discussions)
- 错误:[GitHub 仓库 Issue](https://github.com/remotely-save/remotely-save/issues)(注意这里是 bug 反馈,不是讨论)
此外,插件作者可能会偶尔访问 Obsidian 官方论坛和官方 Discord 服务器,并关注与该插件相关的信息。
## 下载和安装
- 选项 #1:在官方“社区插件列表”中搜索,或访问此链接:[https://obsidian.md/plugins?id=remotely-save](https://obsidian.md/plugins?id=remotely-save)(应该会重定向到 Obsidian app),然后安装插件。
- 选项 #2:你也可以使用 [Obsidian42 - BRAT](https://github.com/TfTHacker/obsidian42-brat) 来安装此插件。在 BRAT 的配置中输入 `remotely-save/remotely-save`
- 选项 #3[![GitHub release (latest by SemVer and asset including pre-releases)](https://img.shields.io/github/downloads-pre/fyears/remotely-save/latest/main.js?sort=semver)](https://github.com/fyears/remotely-save/releases) 从最新发布中手动下载文件(`main.js``manifest.json``styles.css`)。
- 选项 #4[![BuildCI](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml/badge.svg)](https://github.com/fyears/remotely-save/actions/workflows/auto-build.yml) 每个成功的构建的“摘要”下都有构建文件。它是由每个提交自动生成的,可能不会正常工作。
## 使用方法
### S3
- 教程/示例:
- [Cloudflare R2](./docs/remote_services/s3_cloudflare_r2/README.md)
- [BackBlaze B2](./docs/remote_services/s3_backblaze_b2/README.md)
- [Storj](./docs/remote_services/s3_storj_io/README.md)
- [腾讯云 COS](./docs/remote_services/s3_tencent_cloud_cos/README.zh-cn.md) | [Tencent Cloud COS](./docs/remote_services/s3_tencent_cloud_cos/README.md)
- [MinIO](./docs/remote_services/s3_minio/README.md)
- [又拍云](./docs/remote_services/s3_upyun/README.zh-cn.md)
- 准备你的 S3(兼容)服务信息:[端点,区域](https://docs.aws.amazon.com/general/latest/gr/s3.html)[访问密钥 ID ,密钥](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html),存储桶名称。
- 如果你使用 AWS S3,还要创建[策略和用户](./docs/remote_services/s3_general/s3_user_policy.md)。
- 非常旧版本的 Obsidian 需要[配置 CORS](./docs/remote_services/s3_general/s3_cors_configure.md)。
- 下载并启用此插件。
- 在插件的设置中输入你的信息。
- 如果你没有在设置中设置前缀,存储桶应该是空的,并且仅用于同步一个 vault。你可以在设置中设置前缀,以便同一个存储桶可以存储多个 vault。
- 如果你想启用端到端加密,也要在设置中设置一个密码。如果你没有指定密码,文件和文件夹将以明文、原始内容同步到云。
- **每次**你想在本地和远程之间同步你的 vault 的时候,点击侧边栏上的新“圆形箭头”图标。(或者,你可以在设置面板中配置自动同步(见下一章)。)在同步过程中,图标变成“两个半圆形箭头”。除了点击侧边栏上的图标,你还可以在命令面板中激活相应的命令。
- **同步时要有耐心。** 尤其是在第一次同步时。
- 如果你想在多个设备之间同步文件,**在使用默认设置时,你的 vault 名称应该相同**。
### Dropbox
- **此插件不是官方Dropbox产品。** 插件只是使用 Dropbox 的公共API。
- 授权后,插件可以读取你的姓名和电子邮件(在 Dropbox api 上无法取消选择),并读取和写入你的 Dropbox 的 `/Apps/remotely-save` 文件夹中的文件。
- 如果你决定授权此插件连接到 Dropbox,请访问插件的设置页,选择Dropbox 然后按照说明操作。[更多带截图的信息在这里](./docs/dropbox_review_material/README.md)。
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
- 如果你想在多个设备之间同步文件,**在使用默认设置时,你的 vault 名称应该相同**。
### 个人 OneDrive(应用文件夹)
- **此插件不是官方 Microsoft / OneDrive 产品。** 插件只是使用 Microsoft 的 [OneDrive 公共 API](https://docs.microsoft.com/en-us/onedrive/developer/rest-api) 而已。
- 此插件仅适用于“个人 OneDrive”,不适用于“OneDrive for Business。详见 [#11](https://github.com/fyears/remotely-save/issues/11)。
- 授权后,插件可以读取你的姓名和电子邮件,并读取和写入你的OneDrive的 `/Apps/remotely-save` 文件夹中的文件。**Remotely Save 的免费版本仅连接到应用文件夹,而 PRO 版本可以连接到 Onedrive 的根文件夹。见下面的 PRO 部分。**
- 如果你决定授权此插件连接到 OneDrive,请访问插件的设置页,选择OneDrive 然后按照说明操作。
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
- 如果你想在多个设备之间同步文件,**在使用默认设置时,你的 vault 名称应该相同**。
- 你可能还想查看 [OneDrive 的常见问题](./docs/remote_services/onedrive/README.md)。
### webdav
- 教程/示例:
- [Nextcloud](./docs/remote_services/webdav_nextcloud/README.md)
- [The Good Cloud](./docs/remote_services/webdav_thegoodcloud/README.md)
- [ownCloud](./docs/remote_services/webdav_owncloud/README.md)
- [InfiniCloud](./docs/remote_services/webdav_infinicloud_teracloud/README.md)
- [Synology webdav 服务器](./docs/remote_services/webdav_synology_webdav_server/README.md) | [群晖 webdav 服务器](./docs/remote_services/webdav_synology_webdav_server/README.zh-cn.md)
- [dufs](./docs/remote_services/webdav_dufs/README.md)
- [AList(中文)](./docs/remote_services/webdav_alist/README.zh-cn.md) | [AList (English)](./docs/remote_services/webdav_alist/README.md)
- [坚果云](./docs/remote_services/webdav_jianguoyun/README.zh-cn.md) | [JianGuoYun/NutStore](./docs/remote_services/webdav_jianguoyun/README.md)
- [Open Media Vault](./docs/remote_services/webdav_openmediavault/README.md)
- [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)
- 非常旧版本的Obsidian需要[配置 CORS](./docs/remote_services/webdav_general/webav_cors.md)。
- 你的数据会同步到你的webdav服务器上的 `${vaultName}` 子文件夹。
- 基于密码的端到端加密也是可以的。但请注意,**vault 名称本身未加密**。
- 如果你想在多个设备之间同步文件,**在使用默认设置时,你的 vault 名称应该相同**。
### Webdis
- 教程:
- [Webdis](./docs/remote_services/webdis/README.md)
- 实验性质。
- 你必须自己设置和保护你的 web 服务器。
### Onedrive(完整访问)(PRO 功能)
PRO(付费)功能“与 Onedrive(完整)同步”允许用户与 Onedrive 根文件夹进行同步。教程和限制在[这里](./docs/remote_services/onedrivefull/README.md)。
### Google DriveGDrive)(PRO 功能)
PRO(付费)功能“与 Google Drive 同步”允许用户与 Google Drive 进行同步。教程和限制在[这里](./docs/remote_services/googledrive/README.md)。
### BoxPRO 功能)
PRO(付费)功能“与 Box 同步”允许用户与 Box 同步。教程和限制在[这里](./docs/remote_services/box/README.md)。
### pCloudPRO 功能)
PRO(付费)功能“与 pCloud 同步”允许用户与 pCloud 同步(使用其原生 API 而不是 webdav)。教程和限制在[这里](./docs/remote_services/pcloud/README.md)。
### Yandex DiskPRO 功能)
PRO(付费)功能“与 Yandex Disk 同步”允许用户与 Yandex Disk 同步(使用其原生 API 而不是 webdav)。教程和限制在[这里](./docs/remote_services/yandexdisk/README.md)。
### KoofrPRO 功能)
PRO(付费)功能“与 Koofr 同步”允许用户与 Koofr 同步(使用其原生 API 而不是 webdav)。教程和限制在[这里](./docs/remote_services/koofr/README.md)。
### Azure Blob StoragePRO 功能)
PRO(付费)功能“与 Azure Blob Storage 同步”允许用户与 Azure Blob Storage 同步。教程和限制在[这里](./docs/remote_services/azureblobstorage/README.md)。
## 智能冲突(PRO功能)
基本(免费)版本可以检测冲突,但用户必须选择保留较新版本或较大版本的文件两种选项之一。
PRO(付费)功能“智能冲突”为用户提供了另一个选项:合并小的 markdown 文件,或复制大的 markdown 文件或任何大小的非 markdown 文件。
文档见[这里](./docs/pro/README.md)。
## 定时自动同步
- 你可以在设置中配置每 N 分钟自动同步。
- 在自动同步模式下,如果发生任何错误,插件会**静默失败**。
- 自动同步仅在 Obsidian 打开时有效。由于插件仅在 Obsidian 提供的浏览器环境中工作,因此**技术上不可能**在 Obsidian 后台自动同步。
## 保存时同步
- 你可以在设置中配置保存时同步。
- 在保存时同步模式下,如果发生任何错误,插件会**静默失败**。
## 配置文件夹/文件和书签
默认情况下,插件不同步 obsidian 的配置文件夹/文件(通常是 `.obsidian` 文件夹),因为它是隐藏文件之一。
然而,在最新版本中,你可以在设置中启用同步配置文件夹。请注意,这是一个实验性功能。特别是一些配置文件的修改时间每次 Obsidian 打开时都会改变,这破坏了插件的假设,因此某些信息可能无法正确同步。
此外,Obsidian的书签实际上存储在 `.obsidian/bookmarks.json` 文件中的。你也可以在设置中设置同步这个文件(而不是整个配置文件夹)。插件将保持跨设备最新修改的文件。
## 隐藏文件或文件夹
**默认情况下,所有以 `.`(点)或 `_`(下划线)开头的文件或文件夹都被视为隐藏文件,不会被同步。** 如果你有一些文件只是本地保留,这很有用。但这种策略也意味着主题/其他插件/此插件的设置也不会被同步。
在最新版本中,你可以更改设置以允许同步 `_` 文件或文件夹,以及如上所述的 `.obsidian` 特殊配置文件夹(但不包括其他任何 `.` 文件或文件夹)。
## PRO(付费)功能
详见[PRO](./docs/pro/README.md)了解更多详情。
## 如何调试
详见[这里](./docs/how_to_debug/README.md)了解更多详情。
## 额外功能:通过 QR 码导入和导出非 OAuth2 插件设置
详见[这里](./docs/import_export_some_settings.md)了解更多详情。
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.18", "version": "0.5.19",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "remotely-save", "id": "remotely-save",
"name": "Remotely Save", "name": "Remotely Save",
"version": "0.5.18", "version": "0.5.19",
"minAppVersion": "0.13.21", "minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.", "description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears", "author": "fyears",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "remotely-save", "name": "remotely-save",
"version": "0.5.18", "version": "0.5.19",
"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",
+3 -1
View File
@@ -112,7 +112,9 @@ export const setConfigBySuccessfullAuthInplace = async (
saveUpdatedConfigFunc: () => Promise<any> | undefined saveUpdatedConfigFunc: () => Promise<any> | undefined
) => { ) => {
if (authRes.error !== undefined) { if (authRes.error !== undefined) {
throw Error(`you should not save the setting for ${authRes.error}`); throw Error(
`remotely save account auth failed, please auth again: ${authRes.error}`
);
} }
config.accessToken = authRes.access_token; config.accessToken = authRes.access_token;
+1 -1
View File
@@ -167,7 +167,7 @@ export async function mergeFile(
key, key,
newArrayBuffer, newArrayBuffer,
rightEntity.mtimeCli ?? mtime, rightEntity.mtimeCli ?? mtime,
rightEntity.mtimeCli ?? mtime rightEntity.ctimeCli ?? rightEntity.mtimeCli ?? mtime
); );
return { return {
+1 -1
View File
@@ -112,7 +112,7 @@ export const setConfigBySuccessfullAuthInplace = async (
) => { ) => {
if (authRes.access_token === undefined || authRes.access_token === "") { if (authRes.access_token === undefined || authRes.access_token === "") {
throw Error( throw Error(
`you should not save the setting for ${JSON.stringify(authRes)}` `remotely save account auth failed, please auth again: ${JSON.stringify(authRes)}`
); );
} }
+1 -1
View File
@@ -140,7 +140,7 @@ export const setConfigBySuccessfullAuthInplace = async (
) => { ) => {
if (authRes.access_token === undefined || authRes.access_token === "") { if (authRes.access_token === undefined || authRes.access_token === "") {
throw Error( throw Error(
`you should not save the setting for ${JSON.stringify(authRes)}` `remotely save account auth failed, please auth again: ${JSON.stringify(authRes)}`
); );
} }
+9
View File
@@ -294,13 +294,21 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
if (mtimeTry === undefined || mtimeTry === null) { if (mtimeTry === undefined || mtimeTry === null) {
throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`); throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`);
} }
let ctimeTry = x?.fileSystemInfo?.createdDateTime;
if (ctimeTry === undefined || ctimeTry === null) {
// throw Error(`onedrive cannot parse ctime: ${JSON.stringify(x, null, 2)}`);
// it doesn't hurt, we just use mtimeTry to fullfill
ctimeTry = mtimeTry;
}
const mtimeSvr = Date.parse(mtimeTry); const mtimeSvr = Date.parse(mtimeTry);
const mtimeCli = Date.parse(mtimeTry); const mtimeCli = Date.parse(mtimeTry);
const ctimeCli = Date.parse(ctimeTry);
return { return {
key: key, key: key,
keyRaw: key, keyRaw: key,
mtimeSvr: mtimeSvr, mtimeSvr: mtimeSvr,
mtimeCli: mtimeCli, mtimeCli: mtimeCli,
ctimeCli: ctimeCli,
size: isFolder ? 0 : x.size!, size: isFolder ? 0 : x.size!,
sizeRaw: isFolder ? 0 : x.size!, sizeRaw: isFolder ? 0 : x.size!,
synthesizedFile: false, synthesizedFile: false,
@@ -771,6 +779,7 @@ export class FakeFsOnedriveFull extends FakeFs {
keyRaw: origKey, keyRaw: origKey,
mtimeSvr: mtime, mtimeSvr: mtime,
mtimeCli: mtime, mtimeCli: mtime,
ctimeCli: ctime,
size: 0, size: 0,
sizeRaw: 0, sizeRaw: 0,
synthesizedFile: true, synthesizedFile: true,
+3 -1
View File
@@ -99,7 +99,9 @@ export const setConfigBySuccessfullAuthInplace = async (
saveUpdatedConfigFunc: () => Promise<any> | undefined saveUpdatedConfigFunc: () => Promise<any> | undefined
) => { ) => {
if (authRes === undefined) { if (authRes === undefined) {
throw Error(`you should not save the setting for undefined result`); throw Error(
`remotely save account auth failed, please auth again: undefined result`
);
} }
config.accessToken = authRes.access_token; config.accessToken = authRes.access_token;
+1 -1
View File
@@ -127,7 +127,7 @@ export const setConfigBySuccessfullAuthInplace = async (
) => { ) => {
if (authRes.access_token === undefined || authRes.access_token === "") { if (authRes.access_token === undefined || authRes.access_token === "") {
throw Error( throw Error(
`you should not save the setting for ${JSON.stringify(authRes)}` `remotely save account auth failed, please auth again: ${JSON.stringify(authRes)}`
); );
} }
+68 -4
View File
@@ -28,6 +28,7 @@ import {
} from "../../src/metadataOnRemote"; } from "../../src/metadataOnRemote";
import { import {
atWhichLevel, atWhichLevel,
checkValidName,
getParentFolder, getParentFolder,
isHiddenPath, isHiddenPath,
isSpecialFolderNameToSkip, isSpecialFolderNameToSkip,
@@ -59,6 +60,17 @@ const copyEntityAndFixTimeFormat = (
result.mtimeCliFmt = unixTimeToStr(result.mtimeCli); result.mtimeCliFmt = unixTimeToStr(result.mtimeCli);
} }
} }
if (result.ctimeCli !== undefined) {
if (result.ctimeCli === 0) {
result.ctimeCli = undefined;
} else {
if (serviceType === "s3" || serviceType === "dropbox") {
// round to second instead of millisecond
result.ctimeCli = Math.floor(result.ctimeCli / 1000.0) * 1000;
}
result.ctimeCliFmt = unixTimeToStr(result.ctimeCli);
}
}
if (result.mtimeSvr !== undefined) { if (result.mtimeSvr !== undefined) {
if (result.mtimeSvr === 0) { if (result.mtimeSvr === 0) {
result.mtimeSvr = undefined; result.mtimeSvr = undefined;
@@ -133,13 +145,44 @@ const isSkipItemByName = (
syncBookmarks: boolean, syncBookmarks: boolean,
syncUnderscoreItems: boolean, syncUnderscoreItems: boolean,
configDir: string, configDir: string,
ignorePaths: string[] ignorePaths: string[],
onlyAllowPaths: string[]
) => { ) => {
if (key === undefined) { if (key === undefined) {
throw Error(`isSkipItemByName meets undefinded key!`); throw Error(`isSkipItemByName meets undefinded key!`);
} }
let enableAllowMode = false;
let isInAllowList = false;
if (onlyAllowPaths !== undefined && onlyAllowPaths.length > 0) {
for (const r of onlyAllowPaths) {
if (r.trim() === "") {
// ignore empty lines
continue;
}
enableAllowMode = true; // we really want to check the allow list
if (XRegExp(r, "A").test(key)) {
isInAllowList = true;
}
}
}
// we want to determine "is skippable or not".
// if the key meets the allow list,
// it may or may not be skippable,
// and is deferred to next checking steps
// if the key doesn't meet the allow list,
// it must be skippable.
if (enableAllowMode && !isInAllowList) {
return true; // must be skippable
}
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() === "") {
// ignore empty lines
continue;
}
if (XRegExp(r, "A").test(key)) { if (XRegExp(r, "A").test(key)) {
return true; return true;
} }
@@ -178,6 +221,7 @@ const ensembleMixedEnties = async (
configDir: string, configDir: string,
syncUnderscoreItems: boolean, syncUnderscoreItems: boolean,
ignorePaths: string[], ignorePaths: string[],
onlyAllowPaths: string[],
fsEncrypt: FakeFsEncrypt, fsEncrypt: FakeFsEncrypt,
serviceType: SUPPORTED_SERVICES_TYPE, serviceType: SUPPORTED_SERVICES_TYPE,
@@ -205,12 +249,20 @@ const ensembleMixedEnties = async (
syncBookmarks, syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths,
onlyAllowPaths
) )
) { ) {
continue; continue;
} }
const checkValidNameResult = checkValidName(key);
if (!checkValidNameResult.result) {
throw Error(
`your remote folder/file name is invalid: ${checkValidNameResult.reason}`
);
}
finalMappings[key] = { finalMappings[key] = {
key: key, key: key,
remote: remoteCopied, remote: remoteCopied,
@@ -238,7 +290,8 @@ const ensembleMixedEnties = async (
syncBookmarks, syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths,
onlyAllowPaths
) )
) { ) {
continue; continue;
@@ -274,12 +327,20 @@ const ensembleMixedEnties = async (
syncBookmarks, syncBookmarks,
syncUnderscoreItems, syncUnderscoreItems,
configDir, configDir,
ignorePaths ignorePaths,
onlyAllowPaths
) )
) { ) {
continue; continue;
} }
const checkValidNameResult = checkValidName(key);
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(
copyEntityAndFixTimeFormat(local, serviceType) copyEntityAndFixTimeFormat(local, serviceType)
@@ -920,6 +981,8 @@ const getSyncPlanInplace = async (
keptFolder.delete("/"); keptFolder.delete("/");
keptFolder.delete(""); keptFolder.delete("");
if (keptFolder.size > 0) { if (keptFolder.size > 0) {
console.error(sortedKeys);
console.error(mixedEntityMappings);
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`); throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
} }
@@ -1735,6 +1798,7 @@ export async function syncer(
configDir, configDir,
settings.syncUnderscoreItems ?? false, settings.syncUnderscoreItems ?? false,
settings.ignorePaths ?? [], settings.ignorePaths ?? [],
settings.onlyAllowPaths ?? [],
fsEncrypt, fsEncrypt,
settings.serviceType, settings.serviceType,
profiler profiler
+3
View File
@@ -173,6 +173,7 @@ export interface RemotelySavePluginSettings {
agreeToUseSyncV3?: boolean; agreeToUseSyncV3?: boolean;
skipSizeLargerThan?: number; skipSizeLargerThan?: number;
ignorePaths?: string[]; ignorePaths?: string[];
onlyAllowPaths?: string[];
enableStatusBarInfo?: boolean; enableStatusBarInfo?: boolean;
deleteToWhere?: "system" | "obsidian"; deleteToWhere?: "system" | "obsidian";
conflictAction?: ConflictActionType; conflictAction?: ConflictActionType;
@@ -270,6 +271,8 @@ export interface Entity {
keyRaw: string; keyRaw: string;
mtimeCli?: number; mtimeCli?: number;
mtimeCliFmt?: string; mtimeCliFmt?: string;
ctimeCli?: number;
ctimeCliFmt?: string;
mtimeSvr?: number; mtimeSvr?: number;
mtimeSvrFmt?: string; mtimeSvrFmt?: string;
prevSyncTime?: number; prevSyncTime?: number;
+1 -1
View File
@@ -41,7 +41,7 @@ export async function copyFile(key: string, left: FakeFs, right: FakeFs) {
key, key,
content, content,
statsLeft.mtimeCli, statsLeft.mtimeCli,
statsLeft.mtimeCli /* TODO */ statsLeft.ctimeCli ?? statsLeft.mtimeCli
), ),
content: content, content: content,
}; };
+5 -3
View File
@@ -202,7 +202,7 @@ export class CipherRclone {
}, },
[ [
channel.port1, channel.port1,
// input // the array buffer might be re-used later, so we CANNOT transform here // input // the array buffer might be re-used later, so we CANNOT transfer here
] ]
); );
}); });
@@ -247,8 +247,10 @@ export class CipherRclone {
action: "decryptContent", action: "decryptContent",
inputContent: input, inputContent: input,
}, },
// the decrypted result is not used later in worker, so it's save to transfer [
[channel.port1, input] channel.port1,
input, // not transfer for safety
]
); );
}); });
} }
+2 -2
View File
@@ -141,7 +141,7 @@ ctx.addEventListener("message", async (event: any) => {
status: "ok", status: "ok",
outputContent: outputContent, outputContent: outputContent,
}, },
[outputContent] [outputContent] // on worker side, we don't reuse anything so it's safe to transfer
); );
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -166,7 +166,7 @@ ctx.addEventListener("message", async (event: any) => {
status: "ok", status: "ok",
outputContent: outputContent, outputContent: outputContent,
}, },
[outputContent] [outputContent] // on worker side, we don't reuse anything so it's safe to transfer
); );
} catch (error) { } catch (error) {
console.error(error); console.error(error);
+2
View File
@@ -133,8 +133,10 @@ export class FakeFsLocal extends FakeFs {
return { return {
key: isFolder ? `${key}/` : key, // local always unencrypted key: isFolder ? `${key}/` : key, // local always unencrypted
keyRaw: isFolder ? `${key}/` : key, keyRaw: isFolder ? `${key}/` : key,
ctimeCli: statRes.ctime,
mtimeCli: statRes.mtime, mtimeCli: statRes.mtime,
mtimeSvr: statRes.mtime, mtimeSvr: statRes.mtime,
ctimeCliFmt: unixTimeToStr(statRes.ctime),
mtimeCliFmt: unixTimeToStr(statRes.mtime), mtimeCliFmt: unixTimeToStr(statRes.mtime),
mtimeSvrFmt: unixTimeToStr(statRes.mtime), mtimeSvrFmt: unixTimeToStr(statRes.mtime),
size: statRes.size, // local always unencrypted size: statRes.size, // local always unencrypted
+11
View File
@@ -403,13 +403,23 @@ ${constructFromDriveItemToEntityError(x)}`
if (mtimeTry === undefined || mtimeTry === null) { if (mtimeTry === undefined || mtimeTry === null) {
throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`); throw Error(`onedrive cannot parse mtime: ${JSON.stringify(x, null, 2)}`);
} }
let ctimeTry = x?.fileSystemInfo?.createdDateTime;
if (ctimeTry === undefined || ctimeTry === null) {
// throw Error(`onedrive cannot parse ctime: ${JSON.stringify(x, null, 2)}`);
// it doesn't hurt, we just use mtimeTry to fullfill
ctimeTry = mtimeTry;
}
const mtimeSvr = Date.parse(mtimeTry); const mtimeSvr = Date.parse(mtimeTry);
const mtimeCli = Date.parse(mtimeTry); const mtimeCli = Date.parse(mtimeTry);
const ctimeCli = Date.parse(ctimeTry);
return { return {
key: key, key: key,
keyRaw: key, keyRaw: key,
mtimeSvr: mtimeSvr, mtimeSvr: mtimeSvr,
mtimeCli: mtimeCli, mtimeCli: mtimeCli,
ctimeCli: ctimeCli,
size: isFolder ? 0 : x.size!, size: isFolder ? 0 : x.size!,
sizeRaw: isFolder ? 0 : x.size!, sizeRaw: isFolder ? 0 : x.size!,
synthesizedFile: false, synthesizedFile: false,
@@ -878,6 +888,7 @@ export class FakeFsOnedrive extends FakeFs {
keyRaw: origKey, keyRaw: origKey,
mtimeSvr: mtime, mtimeSvr: mtime,
mtimeCli: mtime, mtimeCli: mtime,
ctimeCli: ctime,
size: 0, size: 0,
sizeRaw: 0, sizeRaw: 0,
synthesizedFile: true, synthesizedFile: true,
+3 -1
View File
@@ -149,7 +149,9 @@
"settings_skiplargefiles_desc": "Skip files with sizes larger than the threshold. Here 1 MB = 10^6 bytes.", "settings_skiplargefiles_desc": "Skip files with sizes larger than the threshold. Here 1 MB = 10^6 bytes.",
"settings_skiplargefiles_notset": "(not set)", "settings_skiplargefiles_notset": "(not set)",
"settings_ignorepaths": "Regex Of Paths To Ignore", "settings_ignorepaths": "Regex Of Paths To Ignore",
"settings_ignorepaths_desc": "Regex of paths of folders or files to ignore. One regex per line. The path is relative to the vault root without leading slash.", "settings_ignorepaths_desc": "Regex of paths of folders or files to ignore. One regex per line. The path is relative to the vault root without leading slash. Non-empty allow paths and non-empty ignore paths are effective together.",
"settings_onlyallowpaths": "Regex Of Paths To Allow",
"settings_onlyallowpaths_desc": "Regex of paths of folders or files to allow to sync. One regex per line. The path is relative to the vault root without leading slash. If this setting is not empty, all paths you want to sync including config dirs must be explicitly set here. Non-empty allow paths and non-empty ignore paths are effective together.",
"settings_enablestatusbar_info": "Show Last Successful Sync In Status Bar", "settings_enablestatusbar_info": "Show Last Successful Sync In Status Bar",
"settings_enablestatusbar_info_desc": "Show the time of the last successful sync in the status bar.", "settings_enablestatusbar_info_desc": "Show the time of the last successful sync in the status bar.",
"settings_enablestatusbar_reloadrequired_notice": "Reload the plugin for the changes to take effect.", "settings_enablestatusbar_reloadrequired_notice": "Reload the plugin for the changes to take effect.",
+3 -1
View File
@@ -148,7 +148,9 @@
"settings_skiplargefiles_desc": "跳过大于某一个阈值的文件。这里 1 MB = 10^6 bytes。", "settings_skiplargefiles_desc": "跳过大于某一个阈值的文件。这里 1 MB = 10^6 bytes。",
"settings_skiplargefiles_notset": "(不设置)", "settings_skiplargefiles_notset": "(不设置)",
"settings_ignorepaths": "忽略的文件或文件夹的正则表达式", "settings_ignorepaths": "忽略的文件或文件夹的正则表达式",
"settings_ignorepaths_desc": "忽略的文件或文件夹的正则表达式。每行一条。路径是相对于库(Vault)根目录的,没有前置 / 符号。", "settings_ignorepaths_desc": "忽略的文件或文件夹的正则表达式。每行一条。路径是相对于库(Vault)根目录的,没有前置 / 符号。非空的忽略列表和非空的允许列表会一起生效。",
"settings_onlyallowpaths": "允许同步的文件或文件夹的正则表达式",
"settings_onlyallowpaths_desc": "允许同步的文件或文件夹的正则表达式。每行一条。路径是相对于库(Vault)根目录的,没有前置 / 符号。如果这个设置非空,那么所有您想同步的路径(包括隐藏的设置文件夹)都要明确地列出到这里。非空的忽略列表和非空的允许列表会一起生效。",
"settings_enablestatusbar_info": "在状态栏显示上一次成功的同步", "settings_enablestatusbar_info": "在状态栏显示上一次成功的同步",
"settings_enablestatusbar_info_desc": "在状态栏显示上一次成功的同步的时间。", "settings_enablestatusbar_info_desc": "在状态栏显示上一次成功的同步的时间。",
"settings_enablestatusbar_reloadrequired_notice": "需要重新载入此插件才生效。", "settings_enablestatusbar_reloadrequired_notice": "需要重新载入此插件才生效。",
+3 -1
View File
@@ -147,7 +147,9 @@
"settings_skiplargefiles_desc": "跳過大於某一個閾值的檔案。這裡 1 MB = 10^6 bytes。", "settings_skiplargefiles_desc": "跳過大於某一個閾值的檔案。這裡 1 MB = 10^6 bytes。",
"settings_skiplargefiles_notset": "(不設定)", "settings_skiplargefiles_notset": "(不設定)",
"settings_ignorepaths": "忽略的檔案或資料夾的正則表示式", "settings_ignorepaths": "忽略的檔案或資料夾的正則表示式",
"settings_ignorepaths_desc": "忽略的檔案或資料夾的正則表示式。每行一條。路徑是相對於庫(Vault)根目錄的,沒有前置 / 符號。", "settings_ignorepaths_desc": "忽略的檔案或資料夾的正則表示式。每行一條。路徑是相對於庫(Vault)根目錄的,沒有前置 / 符號。非空的忽略列表和非空的允許列表會一起生效。",
"settings_onlyallowpaths": "允許同步的檔案或資料夾的正則表示式",
"settings_onlyallowpaths_desc": "允許同步的檔案或資料夾的正則表示式。每行一條。路徑是相對於庫(Vault)根目錄的,沒有前置 / 符號。如果這個設定非空,那麼所有您想同步的路徑(包括隱藏的設定資料夾)都要明確地列出到這裡。非空的忽略列表和非空的允許列表會一起生效。",
"settings_enablestatusbar_info": "在狀態列顯示上一次成功的同步", "settings_enablestatusbar_info": "在狀態列顯示上一次成功的同步",
"settings_enablestatusbar_info_desc": "在狀態列顯示上一次成功的同步的時間。", "settings_enablestatusbar_info_desc": "在狀態列顯示上一次成功的同步的時間。",
"settings_enablestatusbar_reloadrequired_notice": "需要重新載入此外掛才生效。", "settings_enablestatusbar_reloadrequired_notice": "需要重新載入此外掛才生效。",
+4
View File
@@ -141,6 +141,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
logToDB: false, logToDB: false,
skipSizeLargerThan: -1, skipSizeLargerThan: -1,
ignorePaths: [], ignorePaths: [],
onlyAllowPaths: [],
enableStatusBarInfo: true, enableStatusBarInfo: true,
deleteToWhere: "system", deleteToWhere: "system",
agreeToUseSyncV3: false, agreeToUseSyncV3: false,
@@ -1409,6 +1410,9 @@ export default class RemotelySavePlugin extends Plugin {
if (this.settings.ignorePaths === undefined) { if (this.settings.ignorePaths === undefined) {
this.settings.ignorePaths = []; this.settings.ignorePaths = [];
} }
if (this.settings.onlyAllowPaths === undefined) {
this.settings.onlyAllowPaths = [];
}
if (this.settings.enableStatusBarInfo === undefined) { if (this.settings.enableStatusBarInfo === undefined) {
this.settings.enableStatusBarInfo = true; this.settings.enableStatusBarInfo = true;
} }
+107
View File
@@ -742,3 +742,110 @@ export const getSha1 = async (x: ArrayBuffer, stringify: "base64" | "hex") => {
} }
throw Error(`not supported stringify option = ${stringify}`); throw Error(`not supported stringify option = ${stringify}`);
}; };
/**
* https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
* https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa#invalidcharacters
*/
export const checkValidName = (x: string) => {
if (x === undefined || x === "") {
// what??
return {
reason: "empty",
result: false,
};
}
// The following reserved characters:
const invalidChars = '*"<>:|?'.split("");
for (const c of invalidChars) {
if (x.includes(c)) {
return {
reason: `reserved character: ${c}`,
result: false,
};
}
}
// directory component
for (const c of [".", ".."]) {
if (
x === c ||
x.endsWith(`/${c}`) ||
x.startsWith(`${c}/`) ||
x.includes(`/${c}/`)
) {
return {
reason: `directory being ${c}`,
result: false,
};
}
}
// reserved file names
const reservedNames = [
"CON",
"PRN",
"AUX",
"NUL",
"COM0",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"COM¹",
"COM²",
"COM³",
"LPT0",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
"LPT¹",
"LPT²",
"LPT³",
];
for (const f of reservedNames) {
if (
x === f ||
x.startsWith(`${f}.`) ||
x.startsWith(`${f}/`) ||
x.includes(`/${f}/`) ||
x.endsWith(`/${f}`) ||
x.includes(`/${f}.`)
) {
return {
reason: `reserved folder/file name: ${f}`,
result: false,
};
}
}
// Do not end a file or directory name with a space or a period.
if (
x.endsWith(" ") ||
x.endsWith(".") ||
x.includes(" /") ||
x.includes("./")
) {
return {
reason: `folder/file name ending with a space or a period`,
result: false,
};
}
return {
reason: "ok",
result: true,
};
};
+21
View File
@@ -2247,6 +2247,27 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
textArea.inputEl.addClass("ignorepaths-textarea"); textArea.inputEl.addClass("ignorepaths-textarea");
}); });
new Setting(basicDiv)
.setName(t("settings_onlyallowpaths"))
.setDesc(t("settings_onlyallowpaths_desc"))
.setClass("onlyallowpaths-settings")
.addTextArea((textArea) => {
textArea
.setValue(`${(this.plugin.settings.onlyAllowPaths ?? []).join("\n")}`)
.onChange(async (value) => {
this.plugin.settings.onlyAllowPaths = value
.trim()
.split("\n")
.filter((x) => x.trim() !== "");
await this.plugin.saveSettings();
});
textArea.inputEl.rows = 10;
textArea.inputEl.cols = 30;
textArea.inputEl.addClass("onlyallowpaths-textarea");
});
////////////////////////////////////////////////// //////////////////////////////////////////////////
// below for advanced settings // below for advanced settings
////////////////////////////////////////////////// //////////////////////////////////////////////////
+4
View File
@@ -214,6 +214,10 @@
font-family: monospace; font-family: monospace;
} }
.onlyallowpaths-textarea {
font-family: monospace;
}
.logtohttpserver-warning { .logtohttpserver-warning {
color: red; color: red;
font-weight: bolder; font-weight: bolder;
+76
View File
@@ -363,6 +363,82 @@ describe("Misc: split chunk ranges", () => {
}); });
}); });
describe("Misc: check valid file names", () => {
it("should be ok for normal file nmes", () => {
let item = "what/.hidden/what/what/what";
assert.ok(misc.checkValidName(item).result);
item = "ssss/%%%^^^$xxxx.md";
assert.ok(misc.checkValidName(item).result);
});
it("should be not ok for reserved characters", () => {
let item = "a**";
assert.ok(!misc.checkValidName(item).result);
item = "a?*";
assert.ok(!misc.checkValidName(item).result);
item = "<>:";
assert.ok(!misc.checkValidName(item).result);
item = "<>:/ssss";
assert.ok(!misc.checkValidName(item).result);
});
it("should be not ok for reserved names", () => {
let item = "CON";
assert.ok(!misc.checkValidName(item).result);
item = "CON.txt";
assert.ok(!misc.checkValidName(item).result);
item = "CON.md";
assert.ok(!misc.checkValidName(item).result);
item = "con"; // lower case is ok
assert.ok(misc.checkValidName(item).result);
item = "CON/";
assert.ok(!misc.checkValidName(item).result);
item = "CON.dir/";
assert.ok(!misc.checkValidName(item).result);
item = "CON.dir.folder/";
assert.ok(!misc.checkValidName(item).result);
item = "xxx/CON";
assert.ok(!misc.checkValidName(item).result);
item = "xxx/CON.txt";
assert.ok(!misc.checkValidName(item).result);
item = "xxx/CON.txt.md";
assert.ok(!misc.checkValidName(item).result);
});
it("should be not ok for invalid endings", () => {
let item = "xxx ";
assert.ok(!misc.checkValidName(item).result);
item = "/xxx ";
assert.ok(!misc.checkValidName(item).result);
item = "xxx.yyy.";
assert.ok(!misc.checkValidName(item).result);
item = "xxx.yyy.";
assert.ok(!misc.checkValidName(item).result);
item = "xxx /";
assert.ok(!misc.checkValidName(item).result);
item = "xxx.yyy./";
assert.ok(!misc.checkValidName(item).result);
});
});
describe("Misc: Dropbox: should fix the folder name cases", () => { describe("Misc: Dropbox: should fix the folder name cases", () => {
it("should do nothing on empty folders", () => { it("should do nothing on empty folders", () => {
const input: any[] = []; const input: any[] = [];