数据同步【免费下载链接】remotely-saveSync 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/的全部实现中。下面逐一结合源码验证它们如何落地。二、铁律 1main.ts——唯一的有状态入口在 src/main.ts 中RemotelySavePlugin extends Plugin是插件唯一的类实例集中持有全部可变状态settings: RemotelySavePluginSettings全部服务的配置与同步偏好db: InternalDBsIndexedDB 封装prepareDBs初始化isSyncing/hasPendingSyncOnSave同步运行状态oauth2InfoOAuth 授权过程中的 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之外的模块之所以能保持纯净是因为插件生命周期所需的脏活读取设置、注册命令、处理协议回调、更新状态栏被全部收拢在这一处。三、铁律 2misc.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等支撑加密层与远端元数据计算。其他getSha1Web 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.tsexport abstract class FakeFs { abstract kind: string; abstract walk(): PromiseEntity[]; abstract walkPartial(): PromiseEntity[]; abstract stat(key: string): PromiseEntity; abstract mkdir(key: string, mtime?: number, ctime?: number): PromiseEntity; abstract writeFile(key: string, content: ArrayBuffer, mtime: number, ctime: number): PromiseEntity; abstract readFile(key: string): PromiseArrayBuffer; abstract rename(key1: string, key2: string): Promisevoid; abstract rm(key: string): Promisevoid; abstract checkConnect(callbackFunc?: any): Promiseboolean; // ...checkConnectCommonOps 提供了通用的连通性测试流程 abstract getUserDisplayName(): Promisestring; abstract revokeAuth(): Promiseany; abstract allowEmptyFile(): boolean; }同步引擎只面对这一组统一接口walk列出全部实体、stat获取单个实体、mkdir/writeFile/readFile/rename/rm完成增删改查。各存储实现只需关心如何与自己的远端协议对话完全不知道也不关心同步决策逻辑。4.2 具体的存储实现src/fsLocal.ts基于 ObsidianVault的本地实现kind为localsrc/fsS3.tsS3 兼容实现甚至自定义了ObsHttpHandler将 AWS SDK 的网络层替换为 Obsidian 的requestUrl以适配浏览器/CSP 环境src/fsWebdav.tsWebDAV 实现通过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: () Promiseany ): 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-saveSync 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点击查看免费下载相关推荐notepad-- 代码折叠教程如何 3 分钟把 8000 行大文件折成一张目录notepad 代码折叠教程如何 3 分钟把 8000 行大文件折成一张目录 接手一个老项目打开一个 8000 行的 C 文件想找主函数得滚半天鼠标。桌面应用AdminJS后端架构设计分层架构与依赖注入实践AdminJS后端架构设计分层架构与依赖注入实践 一、架构设计痛点与解决方案 你是否在开发后台系统时遇到过业务逻辑与数据访问混杂、代码复用率低、测试困难等问题后端低代码Remotely Save插件国际化架构解析多语言支持的设计与实践Remotely Save插件国际化架构解析多语言支持的设计与实践 Remotely Save是一款强大的Obsidian同步插件其国际化架构设计为全球用户数据同步上一篇Dropwizard资源方法返回类型Response与实体类对比下一篇10个使用 Flutter Clean Architecture 提升代码质量的实用技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考