抽象層 `core.fs` 深度解析:File/Folder 模型、三級(jí)哈希與 SQLite 元數(shù)據(jù)緩存)
桌面應(yīng)用【免費(fèi)下載鏈接】dupeguruFind duplicate files項(xiàng)目地址https://gitcode.com/gh_mirrors/du/dupeguru點(diǎn)擊查看免費(fèi)下載core.fs是 dupeGuru 各版本標(biāo)準(zhǔn)版 SE、音樂(lè)版 ME、圖片版 PE共用的文件系統(tǒng)抽象層它把磁盤上的文件與文件夾統(tǒng)一封裝成帶懶加載元數(shù)據(jù)大小、修改時(shí)間、全文摘要、局部摘要、采樣摘要的File/Folder對(duì)象并通過(guò) SQLite 緩存哈希結(jié)果是后續(xù)重復(fù)匹配見(jiàn) core/engine.py的輸入基礎(chǔ)。本文以 help/en/developer/core/fs.rst 所指向的core.fs模塊文檔為骨架結(jié)合 core/fs.py 源碼與其測(cè)試用例 core/tests/fs_test.py完整梳理該模塊的公開(kāi) API、內(nèi)部機(jī)制與擴(kuò)展方式讀完你將對(duì) dupeGuru 掃描管線的數(shù)據(jù)源頭有源碼級(jí)的清晰認(rèn)識(shí)并掌握自行擴(kuò)展文件包裝類的路徑。模塊定位為 dupeGuru 定制的文件系統(tǒng)抽象core.fs的前身是hsfs一個(gè)最初為 musicGuru 設(shè)計(jì)的文件系統(tǒng)封裝庫(kù)。在 core/fs.py 的頭部注釋第 9-12 行中明確記錄了這次 fork 的動(dòng)機(jī)hsfs對(duì) dupeGuru 而言過(guò)度設(shè)計(jì)帶來(lái)了不必要的復(fù)雜度和內(nèi)存占用。因此本模塊只保留掃描重復(fù)文件所必需的能力用統(tǒng)一的File對(duì)象包裝文件用Folder對(duì)象包裝目錄且Folder繼承自File保證兩者在后續(xù)匹配代碼中可互換所有元數(shù)據(jù)大小、mtime、摘要都懶加載首次訪問(wèn)時(shí)才從磁盤或緩存讀取提供三級(jí)摘要策略全文 digest、局部 digest_partial、采樣 digest_samples在準(zhǔn)確性與 I/O 開(kāi)銷之間取得平衡通過(guò) SQLiteFilesDB緩存路徑 大小 mtime對(duì)應(yīng)的摘要避免重復(fù)掃描時(shí)重復(fù)讀盤。模塊頂部還定義了一個(gè)重要的運(yùn)行時(shí)選擇哈希算法。源碼第 26-34 行優(yōu)先嘗試導(dǎo)入xxhash并使用xxh128否則回退到標(biāo)準(zhǔn)庫(kù)hashlib.md5try: import xxhash hasher xxhash.xxh128 except ImportError: import hashlib hasher hashlib.md5這意味著在沒(méi)有安裝xxhash的純凈環(huán)境中dupeGuru 自動(dòng)退化為 MD5兩種算法對(duì)上層代碼完全透明。這一算法選擇也體現(xiàn)在FilesDB的 schema 版本描述中Changed from md5 to xxhash if availablecore/fs.py。公開(kāi) API 一覽模塊通過(guò)__all__顯式導(dǎo)出以下符號(hào)core/fs.py符號(hào)類型職責(zé)File類單個(gè)文件的包裝持有掃描所需的元數(shù)據(jù)Folder類目錄包裝聚合其子項(xiàng)的大小與摘要get_file(path, fileclasses)函數(shù)將路徑包裝為合適的File子類實(shí)例get_files(path, fileclasses)函數(shù)掃描目錄返回其中所有文件的File實(shí)例列表FSError異常基類文件系統(tǒng)操作錯(cuò)誤的統(tǒng)一異常AlreadyExistsError異常目標(biāo)已存在重命名/復(fù)制沖突InvalidPath異常路徑無(wú)效InvalidDestinationError異常復(fù)制/移動(dòng)操作的目標(biāo)非法OperationError異常復(fù)制/移動(dòng)/刪除操作后的校驗(yàn)未通過(guò)此外模塊內(nèi)還定義了模塊級(jí)單例filesdb FilesDB()core/fs.py所有File實(shí)例共享同一個(gè)摘要緩存連接。異常體系統(tǒng)一錯(cuò)誤報(bào)告所有文件系統(tǒng)錯(cuò)誤都派生自FSErrorcore/fs.py錯(cuò)誤消息模板統(tǒng)一為An error has occured on {name} in {parent}。構(gòu)造時(shí)可傳入字符串、File實(shí)例或空值File實(shí)例會(huì)取用其name屬性parent用于補(bǔ)充出錯(cuò)位置。四個(gè)子類分別覆蓋了典型場(chǎng)景AlreadyExistsError{name} already exists in {parent}例如File.rename()發(fā)現(xiàn)目標(biāo)路徑已存在時(shí)拋出InvalidPath{name} is invalid.例如get_files()中os.scandir拋OSError時(shí)拋出InvalidDestinationError復(fù)制/移動(dòng)操作的目標(biāo)非法OperationErrorOperation on {name} failed.操作執(zhí)行后校驗(yàn)未通過(guò)時(shí)拋出。這一設(shè)計(jì)讓上層GUI 與掃描引擎可以用統(tǒng)一的except FSError捕獲所有文件操作問(wèn)題同時(shí)保留具體錯(cuò)誤類型的細(xì)分能力。File懶加載元數(shù)據(jù)的核心對(duì)象File類core/fs.py)代表一個(gè)待掃描的文件其設(shè)計(jì)有三個(gè)關(guān)鍵點(diǎn)。1.__slots__與內(nèi)存優(yōu)化源碼注釋第 206-208 行記錄了實(shí)測(cè)數(shù)據(jù)僅憑__slots__一項(xiàng)在大量文件場(chǎng)景下未讀取過(guò)信息的文件可節(jié)省約 35% 內(nèi)存讀取屬性后的收益甚至可達(dá) 70%。因此File顯式聲明了__slots__ (path, unicode_path, is_ref, words) tuple(INITIAL_INFO.keys())第 209 行把所有元數(shù)據(jù)字段固定下來(lái)INITIAL_INFO {size: 0, mtime: 0, digest: b, digest_partial: b, digest_samples: b}is_ref由掃描器在匹配前統(tǒng)一打標(biāo)見(jiàn) core/scanner.py 中g(shù)et_dupe_groups的f.is_ref Falsewords則是文件名分詞的結(jié)果由core.engine.getwords()寫入。2. NOT_SET 哨兵與懶加載構(gòu)造時(shí)第 211-221 行所有元數(shù)據(jù)字段先被置為模塊級(jí)哨兵對(duì)象NOT_SET而不是初值__getattribute__重寫第 226-236 行保證任何字段首次被訪問(wèn)時(shí)自動(dòng)觸發(fā)_read_info(field)去真正讀取讀取失敗則記日志并回退到INITIAL_INFO中的默認(rèn)值。例如首次訪問(wèn)f.size時(shí)才執(zhí)行path.stat()首次訪問(wèn)f.digest時(shí)才計(jì)算全文摘要。這意味著掃描器按需讀取字段避免對(duì)所有文件做無(wú)謂的磁盤 I/O。3. 三級(jí)摘要策略File提供三種摘要對(duì)應(yīng)不同粒度的內(nèi)容比對(duì)core/fs.py字段計(jì)算方式用途digest以CHUNK_SIZE1 MiB第 53 行分塊流式讀取整個(gè)文件逐塊喂給哈希器最終確認(rèn)內(nèi)容的確定性全文比對(duì)digest_partial只讀取固定偏移與長(zhǎng)度PARTIAL_OFFSET_SIZE (0x4000, 0x4000)即文件 16 KiB 處起讀 16 KiB快速初篩避免大文件全文哈希digest_samples采樣文件 25% 處、60% 處各 1 MiB 及末尾 1 MiB_calc_digest_samples第 260-277 行超過(guò)大文件閾值時(shí)的二次粗篩配套的閾值常量第 55-59 行CHUNK_SIZE 1024 * 1024 # 1 MiB 分塊 MIN_FILE_SIZE 3 * CHUNK_SIZE # 3 MiB低于此大小不做采樣 PARTIAL_OFFSET_SIZE (0x4000, 0x4000)_read_info中還有兩個(gè)關(guān)鍵回退邏輯若文件小于 partial 的讀取窗口小于0x4000 0x4000digest_partial直接取全文digest第 289-290 行若文件大小不超過(guò)MIN_FILE_SIZE不如直接整體哈希digest_samples直接等于digest第 302-303 行。_calc_digest_samples采樣時(shí)使用floor(size * 25 / 100)與floor(size * 60 / 100)定位 25%、60% 位置末尾用fp.seek(-CHUNK_SIZE, 2)讀取最后 1 MiB第 260-277 行。4. 屬性與方法File暴露三個(gè)便捷屬性第 352-363 行extension通過(guò)hscommon.util.get_file_ext取擴(kuò)展名、namepath.name、folder_pathpath.parent。公開(kāi)方法包括can_handle(path)類方法第 321-324 行判斷某路徑是否可被本類包裝——必須是非符號(hào)鏈接的普通文件not path.is_symlink() and path.is_file()這是后續(xù)get_file分派的判據(jù)exists()第 326-332 行安全地檢查文件是否存在OSError一律當(dāng)作不存在處理并記警告日志rename(newname)第 334-346 行重命名文件目標(biāo)已存在拋AlreadyExistsErrorOSError或重命名后目標(biāo)不存在拋OperationErrorget_display_info(group, delta)返回用于 GUI 展示的字典基類中raise NotImplementedError()由各版本子類實(shí)現(xiàn)見(jiàn)下文版本子類擴(kuò)展。Folder目錄的聚合語(yǔ)義Folder繼承自Filecore/fs.py)文檔字符串指出它擁有與File相同的 size/digest 信息但其值是其子項(xiàng)之和。核心差異在_read_info的重寫size/mtimesize是所有子項(xiàng)子文件夾 直接文件size之和mtime取目錄自身stat().st_mtime第 384-390 行digest 系列將_all_items()中每個(gè)子項(xiàng)的同名摘要按字節(jié)拼接后整體哈希第 391-403 行。源碼注釋強(qiáng)調(diào)了一個(gè)重要約束拼接順序必須穩(wěn)定——如果文件被移動(dòng)到不同的子目錄中應(yīng)該產(chǎn)生不同的摘要因此先按f.path排序再拼接第 397-398 行。subfolders屬性第 405-411 行用os.scandir惰性枚舉直接子目錄同樣排除符號(hào)鏈接_all_items()則返回subfolders get_files(self.path)。can_handle第 413-415 行要求路徑是非符號(hào)鏈接的目錄與File.can_handle正好互補(bǔ)二者共同構(gòu)成get_file的完整分派規(guī)則。FilesDBSQLite 摘要緩存為避免重復(fù)掃描同一批文件時(shí)反復(fù)計(jì)算摘要core.fs內(nèi)置了基于 SQLite 的緩存FilesDBcore/fs.py模塊級(jí)單例為filesdb。表結(jié)構(gòu)files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER, entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)第 104-105 行三種摘要分別存儲(chǔ)查詢鍵默認(rèn)按path size mtime_ns精確命中緩存第 107 行select_query若ignore_mtime為True則退化為只按path size查詢第 108 行、115 行g(shù)et()每次都會(huì)現(xiàn)取path.stat()得到 size 與 mtime_ns第 152-155 行保證緩存與磁盤狀態(tài)的一致性寫入使用INSERT ... ON CONFLICT(path) DO UPDATE第 109-113 行實(shí)現(xiàn) upsert同時(shí)更新 size、mtime_ns、entry_dt 與對(duì)應(yīng)摘要版本升級(jí)schema_version 1_check_upgrade在連接時(shí)檢測(cè)版本不匹配則重建表并記錄描述第 129-145 行當(dāng)前版本描述正是從 md5 切換到可用的 xxhash并發(fā)連接時(shí)傳入check_same_threadFalse并配合Lock保護(hù)讀寫第 121-126 行commit()與close()也走同一把鎖容錯(cuò)get/put的任何異常都只記logging.warning而不向上傳播第 172-173 行、187-188 行緩存失敗不阻塞掃描主流程。File._read_info的緩存流程很直接訪問(wèn)某摘要字段時(shí)先f(wàn)ilesdb.get()未命中才計(jì)算并filesdb.put()例如第 285-308 行對(duì)digest_partial、digest、digest_samples的處理。工廠函數(shù)get_file與get_files兩個(gè)工廠函數(shù)完成路徑 → 包裝對(duì)象的轉(zhuǎn)換core/fs.pyget_file(path, fileclasses[File])遍歷候選類返回第一個(gè)can_handle(path)為真的類的實(shí)例若全部不匹配則返回Noneget_files(path, fileclasses[File])os.scandir遍歷目錄對(duì)每個(gè)條目調(diào)用get_file聚合所有可包裝對(duì)象遇到OSError拋InvalidPath并用assert前置校驗(yàn)所有fileclasses都是File的子類。這套類分派 can_handle判據(jù)的機(jī)制正是各版本擴(kuò)展自定義文件類型圖片、音頻的掛載點(diǎn)。在掃描管線中的位置三級(jí)摘要如何被使用core.fs的摘要字段不是孤立存在而是與 core/scanner.py 和 core/engine.py 協(xié)同掃描器按大小閾值過(guò)濾文件后調(diào)用engine.getmatches_by_contents(files, bigsizeself.big_file_size_threshold)core/scanner.pygetmatches_by_contents先按size分組core/engine.py只對(duì)同尺寸組內(nèi)兩兩比較比較順序?yàn)閺谋阋说桨嘿F先比digest_partial只讀了 16 KiB命中后再看是否超過(guò)bigsize——超過(guò)則進(jìn)一步比digest_samples采樣 3 處否則做全文digest比對(duì)core/engine.py。這樣大文件在最壞情況下也只會(huì)在partial 命中后才付出全文哈希的成本。這一調(diào)用鏈證實(shí)了三級(jí)摘要各自扮演的初篩 → 二次粗篩 → 最終確認(rèn)角色與core.fs中三者的實(shí)現(xiàn)一一對(duì)應(yīng)。零字節(jié)文件會(huì)跳過(guò)哈希直接以 100% 匹配處理core/engine.py這與_calc_digest對(duì)空文件的處理是自洽的。版本子類擴(kuò)展從基類到 SE/PE/MEcore.fs的設(shè)計(jì)目標(biāo)是作為可擴(kuò)展基類。dupeGuru 三個(gè)版本各自實(shí)現(xiàn)了get_display_info及補(bǔ)充字段標(biāo)準(zhǔn)版SEcore/se/fs.py 中的File/Folder只重寫get_display_info調(diào)用模塊函數(shù)get_display_info生成供 GUI 展示的字典name、folder_path、size、extension、mtime、percentage、words、dupe_count并可基于group.get_match_of(dupe)輸出相對(duì)參考文件的 delta 值圖片版PEcore/pe/photo.py 的Photo擴(kuò)展了INITIAL_INFO新增dimensions與exif_timestampHANDLED_EXTS限定可處理擴(kuò)展名png/jpg/jpeg/gif/bmp/tiff/tif/webpcan_handle在此基礎(chǔ)上疊加擴(kuò)展名判斷并額外實(shí)現(xiàn) EXIF 方向讀取get_orientation方向 5-8 時(shí)交換寬高與get_blocks圖片模糊匹配塊平臺(tái)相關(guān)音樂(lè)版MEcore/me/fs.py 的MusicFile繼承fs.File同樣擴(kuò)展元數(shù)據(jù)與can_handle判據(jù)按文件擴(kuò)展名與可讀標(biāo)簽?zāi)芰^(guò)濾。這些子類只需遵守can_handle決定分派、_read_info決定元數(shù)據(jù)、INITIAL_INFO聲明新字段三個(gè)約定即可無(wú)縫接入get_file/get_files與掃描管線。測(cè)試驗(yàn)證行為被測(cè)試鎖定的關(guān)鍵保證core/tests/fs_test.py 集中驗(yàn)證了本模塊的若干核心不變量test_size_aggregates_subfilesFolder.size等于所有子文件大小之和test_digest_aggregate_subfiles_sorted與test_partial_digest_aggregate_subfile_sorted目錄摘要 對(duì)按固定順序dir1/dir2/dir3與根目錄文件排列的各子項(xiàng)摘要再哈希驗(yàn)證了目錄摘要有序拼接的實(shí)現(xiàn)約束測(cè)試還用urandom生成了 200 KiB、1 MiB、10 MiB 三種隨機(jī)數(shù)據(jù)文件create_fake_fs_with_random_data覆蓋了全文摘要、partial 摘要與采樣摘要三種路徑test_has_file_attrsFolder必須表現(xiàn)如文件擁有mtime屬性且擴(kuò)展名為空——即Folder繼承自File的兼容性契約。小結(jié)core.fs是 dupeGuru 掃描管線的數(shù)據(jù)基石File/Folder以懶加載與__slots__控制內(nèi)存開(kāi)銷三級(jí)摘要全文/局部/采樣配合CHUNK_SIZE、MIN_FILE_SIZE、PARTIAL_OFFSET_SIZE等常量在準(zhǔn)確性、CPU 與 I/O 間取得平衡FilesDB以路徑 size mtime_ns為鍵的 SQLite 緩存避免重復(fù)計(jì)算get_file/get_files與can_handle構(gòu)成可擴(kuò)展的分派機(jī)制SE/PE/ME 三版本在此之上各自疊加領(lǐng)域字段。理解這一層也就理解了 dupeGuru 一切匹配邏輯文件名分詞、內(nèi)容比對(duì)、目錄摘要背后的輸入來(lái)源與性能設(shè)計(jì)。贊分享桌面應(yīng)用【免費(fèi)下載鏈接】dupeguruFind duplicate files項(xiàng)目地址https://gitcode.com/gh_mirrors/du/dupeguru點(diǎn)擊查看免費(fèi)下載相關(guān)推薦深度解析Graphcool數(shù)據(jù)庫(kù)抽象層與數(shù)據(jù)建模核心技術(shù)深度解析Graphcool數(shù)據(jù)庫(kù)抽象層與數(shù)據(jù)建模核心技術(shù) 引言傳統(tǒng)數(shù)據(jù)庫(kù)開(kāi)發(fā)的痛點(diǎn)與Graphcool的解決方案 你是否還在為繁瑣的數(shù)據(jù)庫(kù)配置、復(fù)雜的ORM深入解析 AferoWandB 核心組件中的 Go 通用文件系統(tǒng)抽象層深入解析 AferoWandB 核心組件中的 Go 通用文件系統(tǒng)抽象層 Afero 是 Go 生態(tài)中一款強(qiáng)大且可擴(kuò)展的文件系統(tǒng)抽象庫(kù)為本地磁盤、內(nèi)存、歸檔文機(jī)器學(xué)習(xí)深度學(xué)習(xí)數(shù)據(jù)可視化可觀測(cè)性從行情到交易信號(hào)machine-learning-for-trading 的完整數(shù)據(jù)管道與最快上手路徑從行情到交易信號(hào)machine learning for trading 的完整數(shù)據(jù)管道與最快上手路徑 行情數(shù)據(jù)拿到手怎樣才能變成能回測(cè)、敢上實(shí)盤的信號(hào)中音視頻桌面應(yīng)用上一篇終極Python Modbus解決方案pymodbus完整指南下一篇VersaViT社區(qū)貢獻(xiàn)指南如何參與這個(gè)開(kāi)源視覺(jué)編碼器的開(kāi)發(fā)與改進(jìn)創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考