- 数据同步
【免费下载链接】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.
本篇指南以 docs/code_design.md 为骨架,系统拆解 Remotely-Save 插件的核心架构设计:为何只有
main.ts允许携带状态、misc.ts为何必须零依赖、各存储后端为何严禁依赖同步引擎,以及全库统一"以/结尾字符串表示文件夹"的约定。结合仓库源码逐条印证这些规则背后的工程动机,帮助你在阅读、调试或二次开发该插件时快速建立正确的代码心智模型。
一、设计文档的骨架:三条铁律与一条约定
原文档虽然精炼,却浓缩了插件分层架构的全部精髓,可归纳为"三条铁律 + 一条约定":
| 规则 | 原文表述 | 本质目的 |
|---|---|---|
| 铁律 1 | 除main.ts外,所有函数都应是纯函数;有状态的信息一律通过参数传入 | 消除全局状态,让每个模块可独立测试、可独立复用 |
| 铁律 2 | misc.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:基于 Obsidian
Vault的本地实现,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 对扩展者的帮助
想接入一个新的云服务,只需三步:
- 在 src/baseTypes.ts 的
SUPPORTED_SERVICES_TYPE中登记服务类型; - 继承
FakeFs实现 src/fsAll.ts 的全部抽象方法; - 在 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.
相关推荐
notepad-- 代码折叠教程:如何 3 分钟把 8000 行大文件折成一张目录
notepad 代码折叠教程:如何 3 分钟把 8000 行大文件折成一张目录 接手一个老项目,打开一个 8000 行的 C++ 文件,想找主函数得滚半天鼠标。
桌面应用AdminJS后端架构设计:分层架构与依赖注入实践
AdminJS后端架构设计:分层架构与依赖注入实践 一、架构设计痛点与解决方案 你是否在开发后台系统时遇到过业务逻辑与数据访问混杂、代码复用率低、测试困难等问题
后端低代码Remotely Save插件国际化架构解析:多语言支持的设计与实践
Remotely Save插件国际化架构解析:多语言支持的设计与实践 Remotely Save是一款强大的Obsidian同步插件,其国际化架构设计为全球用户
数据同步
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考