dupeGuru 核心文件系统抽象层 `core.fs` 深度解析:File/Folder 模型、三级哈希与 SQLite 元数据缓存

发布时间:2026/10/3 17:29:16
dupeGuru 核心文件系统抽象层 `core.fs` 深度解析:File/Folder 模型、三级哈希与 SQLite 元数据缓存 桌面应用【免费下载链接】dupeguruFind duplicate files项目地址https://gitcode.com/gh_mirrors/du/dupeguru点击查看免费下载core.fs是 dupeGuru 各版本标准版 SE、音乐版 ME、图片版 PE共用的文件系统抽象层它把磁盘上的文件与文件夹统一封装成带懒加载元数据大小、修改时间、全文摘要、局部摘要、采样摘要的File/Folder对象并通过 SQLite 缓存哈希结果是后续重复匹配见 core/engine.py的输入基础。本文以 help/en/developer/core/fs.rst 所指向的core.fs模块文档为骨架结合 core/fs.py 源码与其测试用例 core/tests/fs_test.py完整梳理该模块的公开 API、内部机制与扩展方式读完你将对 dupeGuru 扫描管线的数据源头有源码级的清晰认识并掌握自行扩展文件包装类的路径。模块定位为 dupeGuru 定制的文件系统抽象core.fs的前身是hsfs一个最初为 musicGuru 设计的文件系统封装库。在 core/fs.py 的头部注释第 9-12 行中明确记录了这次 fork 的动机hsfs对 dupeGuru 而言过度设计带来了不必要的复杂度和内存占用。因此本模块只保留扫描重复文件所必需的能力用统一的File对象包装文件用Folder对象包装目录且Folder继承自File保证两者在后续匹配代码中可互换所有元数据大小、mtime、摘要都懒加载首次访问时才从磁盘或缓存读取提供三级摘要策略全文 digest、局部 digest_partial、采样 digest_samples在准确性与 I/O 开销之间取得平衡通过 SQLiteFilesDB缓存路径 大小 mtime对应的摘要避免重复扫描时重复读盘。模块顶部还定义了一个重要的运行时选择哈希算法。源码第 26-34 行优先尝试导入xxhash并使用xxh128否则回退到标准库hashlib.md5try: import xxhash hasher xxhash.xxh128 except ImportError: import hashlib hasher hashlib.md5这意味着在没有安装xxhash的纯净环境中dupeGuru 自动退化为 MD5两种算法对上层代码完全透明。这一算法选择也体现在FilesDB的 schema 版本描述中Changed from md5 to xxhash if availablecore/fs.py。公开 API 一览模块通过__all__显式导出以下符号core/fs.py符号类型职责File类单个文件的包装持有扫描所需的元数据Folder类目录包装聚合其子项的大小与摘要get_file(path, fileclasses)函数将路径包装为合适的File子类实例get_files(path, fileclasses)函数扫描目录返回其中所有文件的File实例列表FSError异常基类文件系统操作错误的统一异常AlreadyExistsError异常目标已存在重命名/复制冲突InvalidPath异常路径无效InvalidDestinationError异常复制/移动操作的目标非法OperationError异常复制/移动/删除操作后的校验未通过此外模块内还定义了模块级单例filesdb FilesDB()core/fs.py所有File实例共享同一个摘要缓存连接。异常体系统一错误报告所有文件系统错误都派生自FSErrorcore/fs.py错误消息模板统一为An error has occured on {name} in {parent}。构造时可传入字符串、File实例或空值File实例会取用其name属性parent用于补充出错位置。四个子类分别覆盖了典型场景AlreadyExistsError{name} already exists in {parent}例如File.rename()发现目标路径已存在时抛出InvalidPath{name} is invalid.例如get_files()中os.scandir抛OSError时抛出InvalidDestinationError复制/移动操作的目标非法OperationErrorOperation on {name} failed.操作执行后校验未通过时抛出。这一设计让上层GUI 与扫描引擎可以用统一的except FSError捕获所有文件操作问题同时保留具体错误类型的细分能力。File懒加载元数据的核心对象File类core/fs.py)代表一个待扫描的文件其设计有三个关键点。1.__slots__与内存优化源码注释第 206-208 行记录了实测数据仅凭__slots__一项在大量文件场景下未读取过信息的文件可节省约 35% 内存读取属性后的收益甚至可达 70%。因此File显式声明了__slots__ (path, unicode_path, is_ref, words) tuple(INITIAL_INFO.keys())第 209 行把所有元数据字段固定下来INITIAL_INFO {size: 0, mtime: 0, digest: b, digest_partial: b, digest_samples: b}is_ref由扫描器在匹配前统一打标见 core/scanner.py 中get_dupe_groups的f.is_ref Falsewords则是文件名分词的结果由core.engine.getwords()写入。2. NOT_SET 哨兵与懒加载构造时第 211-221 行所有元数据字段先被置为模块级哨兵对象NOT_SET而不是初值__getattribute__重写第 226-236 行保证任何字段首次被访问时自动触发_read_info(field)去真正读取读取失败则记日志并回退到INITIAL_INFO中的默认值。例如首次访问f.size时才执行path.stat()首次访问f.digest时才计算全文摘要。这意味着扫描器按需读取字段避免对所有文件做无谓的磁盘 I/O。3. 三级摘要策略File提供三种摘要对应不同粒度的内容比对core/fs.py字段计算方式用途digest以CHUNK_SIZE1 MiB第 53 行分块流式读取整个文件逐块喂给哈希器最终确认内容的确定性全文比对digest_partial只读取固定偏移与长度PARTIAL_OFFSET_SIZE (0x4000, 0x4000)即文件 16 KiB 处起读 16 KiB快速初筛避免大文件全文哈希digest_samples采样文件 25% 处、60% 处各 1 MiB 及末尾 1 MiB_calc_digest_samples第 260-277 行超过大文件阈值时的二次粗筛配套的阈值常量第 55-59 行CHUNK_SIZE 1024 * 1024 # 1 MiB 分块 MIN_FILE_SIZE 3 * CHUNK_SIZE # 3 MiB低于此大小不做采样 PARTIAL_OFFSET_SIZE (0x4000, 0x4000)_read_info中还有两个关键回退逻辑若文件小于 partial 的读取窗口小于0x4000 0x4000digest_partial直接取全文digest第 289-290 行若文件大小不超过MIN_FILE_SIZE不如直接整体哈希digest_samples直接等于digest第 302-303 行。_calc_digest_samples采样时使用floor(size * 25 / 100)与floor(size * 60 / 100)定位 25%、60% 位置末尾用fp.seek(-CHUNK_SIZE, 2)读取最后 1 MiB第 260-277 行。4. 属性与方法File暴露三个便捷属性第 352-363 行extension通过hscommon.util.get_file_ext取扩展名、namepath.name、folder_pathpath.parent。公开方法包括can_handle(path)类方法第 321-324 行判断某路径是否可被本类包装——必须是非符号链接的普通文件not path.is_symlink() and path.is_file()这是后续get_file分派的判据exists()第 326-332 行安全地检查文件是否存在OSError一律当作不存在处理并记警告日志rename(newname)第 334-346 行重命名文件目标已存在抛AlreadyExistsErrorOSError或重命名后目标不存在抛OperationErrorget_display_info(group, delta)返回用于 GUI 展示的字典基类中raise NotImplementedError()由各版本子类实现见下文版本子类扩展。Folder目录的聚合语义Folder继承自Filecore/fs.py)文档字符串指出它拥有与File相同的 size/digest 信息但其值是其子项之和。核心差异在_read_info的重写size/mtimesize是所有子项子文件夹 直接文件size之和mtime取目录自身stat().st_mtime第 384-390 行digest 系列将_all_items()中每个子项的同名摘要按字节拼接后整体哈希第 391-403 行。源码注释强调了一个重要约束拼接顺序必须稳定——如果文件被移动到不同的子目录中应该产生不同的摘要因此先按f.path排序再拼接第 397-398 行。subfolders属性第 405-411 行用os.scandir惰性枚举直接子目录同样排除符号链接_all_items()则返回subfolders get_files(self.path)。can_handle第 413-415 行要求路径是非符号链接的目录与File.can_handle正好互补二者共同构成get_file的完整分派规则。FilesDBSQLite 摘要缓存为避免重复扫描同一批文件时反复计算摘要core.fs内置了基于 SQLite 的缓存FilesDBcore/fs.py模块级单例为filesdb。表结构files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)第 104-105 行三种摘要分别存储查询键默认按path size mtime_ns精确命中缓存第 107 行select_query若ignore_mtime为True则退化为只按path size查询第 108 行、115 行get()每次都会现取path.stat()得到 size 与 mtime_ns第 152-155 行保证缓存与磁盘状态的一致性写入使用INSERT ... ON CONFLICT(path) DO UPDATE第 109-113 行实现 upsert同时更新 size、mtime_ns、entry_dt 与对应摘要版本升级schema_version 1_check_upgrade在连接时检测版本不匹配则重建表并记录描述第 129-145 行当前版本描述正是从 md5 切换到可用的 xxhash并发连接时传入check_same_threadFalse并配合Lock保护读写第 121-126 行commit()与close()也走同一把锁容错get/put的任何异常都只记logging.warning而不向上传播第 172-173 行、187-188 行缓存失败不阻塞扫描主流程。File._read_info的缓存流程很直接访问某摘要字段时先filesdb.get()未命中才计算并filesdb.put()例如第 285-308 行对digest_partial、digest、digest_samples的处理。工厂函数get_file与get_files两个工厂函数完成路径 → 包装对象的转换core/fs.pyget_file(path, fileclasses[File])遍历候选类返回第一个can_handle(path)为真的类的实例若全部不匹配则返回Noneget_files(path, fileclasses[File])os.scandir遍历目录对每个条目调用get_file聚合所有可包装对象遇到OSError抛InvalidPath并用assert前置校验所有fileclasses都是File的子类。这套类分派 can_handle判据的机制正是各版本扩展自定义文件类型图片、音频的挂载点。在扫描管线中的位置三级摘要如何被使用core.fs的摘要字段不是孤立存在而是与 core/scanner.py 和 core/engine.py 协同扫描器按大小阈值过滤文件后调用engine.getmatches_by_contents(files, bigsizeself.big_file_size_threshold)core/scanner.pygetmatches_by_contents先按size分组core/engine.py只对同尺寸组内两两比较比较顺序为从便宜到昂贵先比digest_partial只读了 16 KiB命中后再看是否超过bigsize——超过则进一步比digest_samples采样 3 处否则做全文digest比对core/engine.py。这样大文件在最坏情况下也只会在partial 命中后才付出全文哈希的成本。这一调用链证实了三级摘要各自扮演的初筛 → 二次粗筛 → 最终确认角色与core.fs中三者的实现一一对应。零字节文件会跳过哈希直接以 100% 匹配处理core/engine.py这与_calc_digest对空文件的处理是自洽的。版本子类扩展从基类到 SE/PE/MEcore.fs的设计目标是作为可扩展基类。dupeGuru 三个版本各自实现了get_display_info及补充字段标准版SEcore/se/fs.py 中的File/Folder只重写get_display_info调用模块函数get_display_info生成供 GUI 展示的字典name、folder_path、size、extension、mtime、percentage、words、dupe_count并可基于group.get_match_of(dupe)输出相对参考文件的 delta 值图片版PEcore/pe/photo.py 的Photo扩展了INITIAL_INFO新增dimensions与exif_timestampHANDLED_EXTS限定可处理扩展名png/jpg/jpeg/gif/bmp/tiff/tif/webpcan_handle在此基础上叠加扩展名判断并额外实现 EXIF 方向读取get_orientation方向 5-8 时交换宽高与get_blocks图片模糊匹配块平台相关音乐版MEcore/me/fs.py 的MusicFile继承fs.File同样扩展元数据与can_handle判据按文件扩展名与可读标签能力过滤。这些子类只需遵守can_handle决定分派、_read_info决定元数据、INITIAL_INFO声明新字段三个约定即可无缝接入get_file/get_files与扫描管线。测试验证行为被测试锁定的关键保证core/tests/fs_test.py 集中验证了本模块的若干核心不变量test_size_aggregates_subfilesFolder.size等于所有子文件大小之和test_digest_aggregate_subfiles_sorted与test_partial_digest_aggregate_subfile_sorted目录摘要 对按固定顺序dir1/dir2/dir3与根目录文件排列的各子项摘要再哈希验证了目录摘要有序拼接的实现约束测试还用urandom生成了 200 KiB、1 MiB、10 MiB 三种随机数据文件create_fake_fs_with_random_data覆盖了全文摘要、partial 摘要与采样摘要三种路径test_has_file_attrsFolder必须表现如文件拥有mtime属性且扩展名为空——即Folder继承自File的兼容性契约。小结core.fs是 dupeGuru 扫描管线的数据基石File/Folder以懒加载与__slots__控制内存开销三级摘要全文/局部/采样配合CHUNK_SIZE、MIN_FILE_SIZE、PARTIAL_OFFSET_SIZE等常量在准确性、CPU 与 I/O 间取得平衡FilesDB以路径 size mtime_ns为键的 SQLite 缓存避免重复计算get_file/get_files与can_handle构成可扩展的分派机制SE/PE/ME 三版本在此之上各自叠加领域字段。理解这一层也就理解了 dupeGuru 一切匹配逻辑文件名分词、内容比对、目录摘要背后的输入来源与性能设计。赞分享桌面应用【免费下载链接】dupeguruFind duplicate files项目地址https://gitcode.com/gh_mirrors/du/dupeguru点击查看免费下载相关推荐深度解析Graphcool数据库抽象层与数据建模核心技术深度解析Graphcool数据库抽象层与数据建模核心技术 引言传统数据库开发的痛点与Graphcool的解决方案 你是否还在为繁琐的数据库配置、复杂的ORM深入解析 AferoWandB 核心组件中的 Go 通用文件系统抽象层深入解析 AferoWandB 核心组件中的 Go 通用文件系统抽象层 Afero 是 Go 生态中一款强大且可扩展的文件系统抽象库为本地磁盘、内存、归档文机器学习深度学习数据可视化可观测性从行情到交易信号machine-learning-for-trading 的完整数据管道与最快上手路径从行情到交易信号machine learning for trading 的完整数据管道与最快上手路径 行情数据拿到手怎样才能变成能回测、敢上实盘的信号中音视频桌面应用上一篇终极Python Modbus解决方案pymodbus完整指南下一篇VersaViT社区贡献指南如何参与这个开源视觉编码器的开发与改进创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考