news 2026/9/26 7:37:00

Remotely-Save 代码设计解析:纯函数分层、依赖隔离与文件夹字符串约定的架构实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Remotely-Save 代码设计解析:纯函数分层、依赖隔离与文件夹字符串约定的架构实践
  • 数据同步

【免费下载链接】remotely-save

Sync notes between local and cloud with smart conflict: S3 (Amazon S3/Cloudflare R2/Backblaze B2/...), Dropbox, webdav (NextCloud/InfiniCLOUD/Synology/...), OneDrive, Google Drive (GDrive), Box, pCloud, Yandex Disk, Koofr, Azure Blob Storage.

项目地址:https://gitcode.com/gh_mirrors/re/remotely-save
点击查看免费下载

本篇指南以 docs/code_design.md 为骨架,系统拆解 Remotely-Save 插件的核心架构设计:为何只有main.ts允许携带状态、misc.ts为何必须零依赖、各存储后端为何严禁依赖同步引擎,以及全库统一"以/结尾字符串表示文件夹"的约定。结合仓库源码逐条印证这些规则背后的工程动机,帮助你在阅读、调试或二次开发该插件时快速建立正确的代码心智模型。

一、设计文档的骨架:三条铁律与一条约定

原文档虽然精炼,却浓缩了插件分层架构的全部精髓,可归纳为"三条铁律 + 一条约定":

规则原文表述本质目的
铁律 1除main.ts外,所有函数都应是纯函数;有状态的信息一律通过参数传入消除全局状态,让每个模块可独立测试、可独立复用
铁律 2misc.ts不得依赖任何其他自研代码将工具层彻底固化,形成无环依赖的基石
铁律 3每个存储后端代码不得依赖sync.ts存储层与同步逻辑解耦,便于横向扩展新服务
约定同步代码中,文件夹一律用以/结尾的字符串表示用统一、可复现的路径规范消除文件/文件夹歧义

这四条规则贯穿于src/与pro/src/的全部实现中。下面逐一结合源码验证它们如何落地。

二、铁律 1:main.ts——唯一的"有状态"入口

在 src/main.ts 中,RemotelySavePlugin extends Plugin是插件唯一的类实例,集中持有全部可变状态:

  • settings: RemotelySavePluginSettings:全部服务的配置与同步偏好;
  • db: InternalDBs:IndexedDB 封装(prepareDBs初始化);
  • isSyncing/hasPendingSyncOnSave:同步运行状态;
  • oauth2Info:OAuth 授权过程中的 verifier、Modal 等临时状态;
  • currLogLevel/currSyncMsg:日志与进度信息;
  • vaultRandomID:当前 vault 的随机标识,用于区分不同库的同步记录。

这些状态在syncRun()中并不直接散落使用,而是被"参数化"传递给下游。以 src/main.ts 对syncer的调用为例:

await syncer( fsLocal, // 本地文件系统封装 fsRemote, // 远端服务封装 fsEncrypt, // 加密层封装 profiler, // 性能分析器 this.db, // 数据库 triggerSource, // 触发来源 profileID, // 配置档案 ID this.vaultRandomID, this.app.vault.configDir, this.settings, this.manifest.version, configSaver, // 回调:保存配置 getProtectError, // 回调:保护比例错误消息 markIsSyncingFunc, notifyFunc, // 回调:通知 errNotifyFunc, ribboonFunc, statusBarFunc, callbackSyncProcess );

可见插件状态全部以"参数 + 回调"的方式注入同步引擎,syncer本身不依赖main.ts的任何实例成员——这正是"纯函数 + 参数传递"的直接体现。

同样的模式贯穿在 OAuth 回调处理中:syncRun()内部构造的fsLocal、fsRemote、fsEncrypt三个封装对象,正是下一节要讨论的分层核心。从源码结构可以推断,main.ts之外的模块之所以能保持纯净,是因为插件生命周期所需的"脏活"(读取设置、注册命令、处理协议回调、更新状态栏)被全部收拢在这一处。

三、铁律 2:misc.ts——零自研依赖的纯工具层

src/misc.ts 的 import 列表极短且全部为第三方或 Obsidian 类型:

import * as path from "path"; import type { Vault } from "obsidian"; import emojiRegex from "emoji-regex"; import { base32 } from "rfc4648"; import XRegExp from "xregexp";

注意其中只引入了 Obsidian 的Vault类型(import type),运行时完全不依赖任何自研代码。这保证了misc.ts处于依赖图的底层,任何模块都可以安全地引用它而不会产生环。

该文件提供的工具函数覆盖了同步所需的基础操作,按职责可分为几组:

路径与层级:

  • getFolderLevels:将"a/b/c/"展开为["a", "a/b", "a/b/c"],是"mkdir -p"的核心实现;
  • getPathFolder:输入为文件夹则原样返回,输入为文件则返回其所在目录,且保证以/结尾;
  • getParentFolder:返回父目录,根目录返回"/";
  • atWhichLevel:计算路径层级数,对文件夹会先去掉结尾/;
  • checkHasSpecialCharForDir:检测路径中的非法字符。

判断与校验:

  • isHiddenPath:路径任一组成部分以.或_开头即视为隐藏;
  • isSpecialFolderNameToSkip:内置.git、node_modules、thumbs.db、desktop.ini、Office 临时文件(~$前缀)等黑名单;
  • checkValidName:按 Windows/OneDrive 命名规则校验文件名合法性(保留字符、保留名、结尾空格等)。

编码转换:bufferToArrayBuffer/arrayBufferToBuffer/arrayBufferToBase64/base64ToArrayBuffer/base64ToBase32/hexStringToTypedArray等,支撑加密层与远端元数据计算。

其他:getSha1(Web Crypto SHA-1)、getSplitRanges/splitFileSizeToChunkRanges(分片上传区间)、compareVersion(版本比较)、unixTimeToStr(时间格式化)等。

正是因为misc.ts是零依赖的,它才能被 src/fsLocal.ts、src/fsS3.ts、src/fsWebdav.ts、pro/src/sync.ts 等几乎所有模块安全引用——这是整个依赖图无环的基石。

四、铁律 3:存储后端与sync.ts的依赖隔离

4.1 存储后端的"统一抽象":FakeFs

所有存储后端(S3、WebDAV、Dropbox、OneDrive、本地等)都实现同一个抽象基类 src/fsAll.ts:

export abstract class FakeFs { abstract kind: string; abstract walk(): Promise<Entity[]>; abstract walkPartial(): Promise<Entity[]>; abstract stat(key: string): Promise<Entity>; abstract mkdir(key: string, mtime?: number, ctime?: number): Promise<Entity>; abstract writeFile(key: string, content: ArrayBuffer, mtime: number, ctime: number): Promise<Entity>; abstract readFile(key: string): Promise<ArrayBuffer>; abstract rename(key1: string, key2: string): Promise<void>; abstract rm(key: string): Promise<void>; abstract checkConnect(callbackFunc?: any): Promise<boolean>; // ...checkConnectCommonOps 提供了通用的连通性测试流程 abstract getUserDisplayName(): Promise<string>; abstract revokeAuth(): Promise<any>; abstract allowEmptyFile(): boolean; }

同步引擎只面对这一组统一接口:walk列出全部实体、stat获取单个实体、mkdir/writeFile/readFile/rename/rm完成增删改查。各存储实现只需关心"如何与自己的远端协议对话",完全不知道也不关心同步决策逻辑。

4.2 具体的存储实现

  • src/fsLocal.ts:基于 ObsidianVault的本地实现,kind为"local";
  • src/fsS3.ts:S3 兼容实现,甚至自定义了ObsHttpHandler将 AWS SDK 的网络层替换为 Obsidian 的requestUrl,以适配浏览器/CSP 环境;
  • src/fsWebdav.ts:WebDAV 实现,通过getPatcher().patch("request", ...)将webdav库的请求也统一到requestUrl,并处理了 iOS 上 PROPFIND 对无斜杠目录返回 401 的怪癖;
  • src/fsDropbox.ts、src/fsOnedrive.ts、src/fsWebdis.ts 以及pro/下的 pro/src/fsBox.ts、pro/src/fsGoogleDrive.ts、pro/src/fsPCloud.ts、pro/src/fsYandexDisk.ts、pro/src/fsKoofr.ts、pro/src/fsAzureBlobStorage.ts 等。

4.3 工厂函数getClient如何避免循环依赖

存储后端不依赖sync.ts是一条显式规则,但存储模块之间、以及存储模块与配置类型之间仍可能形成环。项目用 src/fsGetter.ts 这个专门的工厂文件解决:

/** * To avoid circular dependency, we need a new file here. */ export function getClient( settings: RemotelySavePluginSettings, vaultName: string, saveUpdatedConfigFunc: () => Promise<any> ): FakeFs { switch (settings.serviceType) { case "s3": return new FakeFsS3(settings.s3); case "webdav": return new FakeFsWebdav(settings.webdav, vaultName, saveUpdatedConfigFunc); // ...其余服务同理 default: throw Error(`cannot init client for serviceType=${settings.serviceType}`); } }

getClient统一负责按settings.serviceType实例化正确的存储后端,任何调用方(主要是main.ts的syncRun())只需依赖这一个工厂,即可获得对应的FakeFs,无需逐个 import 各存储类,从根源上压缩了依赖环的规模。

4.4 加密层:另一个"纯装饰器"证明

src/fsEncrypt.ts 的FakeFsEncrypt同样继承FakeFs,它的构造参数是innerFs: FakeFs、password、method——即把任意存储后端"包一层"变成加密后端,kind甚至动态生成为encrypt(${innerFs.kind},...)。这进一步证明:存储层是完全可组合的纯对象,sync.ts只依赖接口而非任何具体实现。

而真正的同步决策逻辑位于 pro/src/sync.ts,其 import 仅涉及copyLogic、fsAll、fsEncrypt、localdb、metadataOnRemote、misc、profiler等,从未反向 import 任何存储后端——规则 3 在依赖方向上得到严格贯彻。

五、统一约定:文件夹 = 以/结尾的字符串

这是设计文档中最容易被忽视、却最影响全库可读性的约定。同步代码中没有"文件夹对象"这样的运行时实体,一切路径都是字符串,靠结尾是否有/区分类型。

5.1 约定的源头与落地

在 src/fsLocal.ts 的walk()中,Obsidian 的TFolder被转换成key + "/":

} else if (entry instanceof TFolder) { key = `${key}/`; r = { key: key, keyRaw: key, size: 0, sizeRaw: 0, }; }

而在 src/baseTypes.ts 的Entity接口中,keyRaw(真实路径)与key(加密后路径,未加密时等于keyRaw)都遵循这一约定,size为 0 且以/结尾即为文件夹。

5.2 配套的工具函数如何维护约定

misc.ts中一系列函数都围绕"文件夹结尾必须有/"设计:

  • getPathFolder("a/b/c.txt")→"a/b/",而getPathFolder("a/b/")→"a/b/"(原样返回);
  • getParentFolder对根目录返回"/";
  • atWhichLevel计算层级前先slice(0, -1)去掉结尾/;
  • isSpecialFolderNameToSkip同时匹配x === iterator与x ===${iterator}/`` 两种形态;
  • isHiddenPath对"."、".."、""空段直接跳过,避免被"a//b/"之类的异常路径干扰。

同步引擎 pro/src/sync.ts 中的ensureMTimeOfRemoteEntityValid也利用该约定判断文件/文件夹:!remote.key!.endsWith("/")时按文件处理并校验 mtime,否则跳过校验——可见约定不仅是表示习惯,更被同步决策直接依赖。

5.3 为什么坚持字符串而非对象

从Entity接口的注释可以读出设计意图:"uniform representation, everything should be flat and primitive, so that we can copy"。扁平、原始的数据结构意味着:

  • 实体可以在本地/远端/上次同步三方状态(MixedEntity的local、remote、prevSync)之间轻松复制与比对;
  • 可以序列化进 IndexedDB 与远端元数据文件(见 src/metadataOnRemote.ts);
  • 不同存储后端返回的实体天然同构,加密层只需在keyRaw与key(加密 key)之间做变换即可透明工作(见 src/fsEncrypt.ts 的_dealWithWalk)。

六、设计规则的实战价值

6.1 对调试者的帮助

当同步行为异常时,你可以按依赖方向逐层排查:先验证misc.ts的路径工具(如文件夹结尾/是否被破坏),再检查具体存储后端的walk/stat返回的Entity是否符合约定,最后才进入pro/src/sync.ts的决策逻辑。仓库还提供了 docs/how_to_debug 与 docs/check_performance 等配套资料,配合 src/profiler.ts 可定位性能瓶颈。

6.2 对扩展者的帮助

想接入一个新的云服务,只需三步:

  1. 在 src/baseTypes.ts 的SUPPORTED_SERVICES_TYPE中登记服务类型;
  2. 继承FakeFs实现 src/fsAll.ts 的全部抽象方法;
  3. 在 src/fsGetter.ts 的工厂中注册实例化分支。

全程不需要触碰sync.ts——这正是铁律 3 带来的可扩展性红利。pro/目录中 Box、Google Drive、pCloud、Yandex Disk、Koofr、Azure Blob Storage 等十余种后端的实现方式,均可作为接入范本。

6.3 对测试者的帮助

由于除main.ts外全是纯函数与纯对象,测试无需 mock Obsidian 插件实例:tests/misc.test.ts、tests/metadataOnRemote.test.ts、pro/tests/sync.test.ts 等测试直接构造输入验证输出,正是这套纯函数设计的直接收益。

七、结语

Remotely-Save 的架构并不复杂,但每一处克制都服务于同一个目标:让同步引擎、存储后端与工具函数三层各司其职、互不越界。main.ts承载全部状态、misc.ts保持零依赖、存储层统一实现FakeFs接口并隔离于sync.ts、路径一律以/结尾的字符串表示——理解这四条规则,你就掌握了阅读这个插件源码的正确打开方式。后续在排查同步问题或扩展新服务时,不妨先对照本文的依赖地图定位模块边界,再深入具体实现。

  • 数据同步

【免费下载链接】remotely-save

Sync notes between local and cloud with smart conflict: S3 (Amazon S3/Cloudflare R2/Backblaze B2/...), Dropbox, webdav (NextCloud/InfiniCLOUD/Synology/...), OneDrive, Google Drive (GDrive), Box, pCloud, Yandex Disk, Koofr, Azure Blob Storage.

项目地址:https://gitcode.com/gh_mirrors/re/remotely-save
点击查看免费下载

相关推荐

上一篇:Dropwizard资源方法返回类型:Response与实体类对比
下一篇:10个使用 Flutter Clean Architecture 提升代码质量的实用技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 7:36:57

网络安全证书选择全攻略:CEH/OSCP/CISP/CISSP对比分析

1. 先从证书焦虑说起&#xff1a;这行到底认什么&#xff0c;不认什么我是那种典型的"半路出家"安全人。大学学的不是网安&#xff0c;实习的时候被分到等保测评项目组&#xff0c;天天对着测评表单打勾勾&#xff0c;心里发虚——客户问一句"你们渗透怎么做的&…

作者头像 李华
网站建设 2026/9/26 7:36:55

WAF编码绕过深层原理:解码栈不一致与多层攻击实战

1. 为什么一个编码字符能让WAF瞬间“失明”&#xff1a;解析差异的根源1.1 在WAF眼里&#xff0c;请求不是一串字符串&#xff0c;而是“一串待猜的代码”很多刚开始接触WAF绕过的人会陷入一个误区&#xff1a;觉得WAF就是在请求里找敏感关键词&#xff0c;比如看到union、sele…

作者头像 李华
网站建设 2026/9/26 7:36:27

5分钟安装上手狗头军师:从0到1的AI恋爱军师快速入门教程

5分钟安装上手狗头军师&#xff1a;从0到1的AI恋爱军师快速入门教程 【免费下载链接】goutoujunshi 一个先接住情绪、再分析关系并给出可执行策略的 Codex 恋爱军师&#xff0c;内置心理、法律、社会、人文、哲学、婚姻家庭与性学知识库&#xff0c;支持多元关系。 项目地址:…

作者头像 李华
网站建设 2026/9/26 7:36:18

Flask构建就业信息管理系统:集成智能推荐、薪资预测与AI咨询

做就业信息管理系统的人不在少数&#xff0c;但一口气把智能推荐、薪资预测、AI问答这三件事都塞进同一个Flask项目里的&#xff0c;确实值得聊一聊。这个项目最初的定位就很清楚&#xff1a;用轻量级Web框架搭建一个大学生就业信息管理与推荐系统&#xff0c;学生能浏览岗位、…

作者头像 李华
网站建设 2026/9/26 7:36:03

大疆LRF文件解析指南:无人机高精度传感器日志的读取与应用

1. 这不是普通视频文件&#xff1a;LRF的本质与常见误操作陷阱 大疆无人机用户在导出飞行数据时&#xff0c;常会遇到一个看似普通却让人困惑的文件——LRF。它通常和MP4视频文件一起生成&#xff0c;命名规则类似“DJI_0001.LRF”&#xff0c;但双击打不开、拖进播放器报错、…

作者头像 李华