#!/usr/bin/env python3 """端到端冒烟示例:把"打包 → 校验 → 快照 → 发布 → 取回"跑一遍。 直接运行即可(全部在临时目录里操作,不碰仓库里的任何文件): python main.py 原来的版本拿仓库自带的 appcation.svfs 做演示,但那个库是**旧格式**(schema 1), 当前版本会直接拒绝并提示用 `vfsmigrate` 迁移——所以示例改成自给自足的一棵树, 走到哪儿都能跑通。 要了解完整能力看 README.md;要了解踩过的坑与取舍看 BUG_REPORT.md。 """ import contextlib import io import os import shutil import sys import tempfile from sqlite_vfs import publish from sqlite_vfs.core import SQLiteVFS from sqlite_vfs.folder_packer import FolderPacker def step(text): print(f"\n=== {text} ===") @contextlib.contextmanager def quiet(): """屏蔽打包过程的逐文件输出,让演示保持可读。""" with contextlib.redirect_stdout(io.StringIO()): yield def write_file(root, rel_path, data): target = os.path.join(root, *rel_path.split("/")) os.makedirs(os.path.dirname(target), exist_ok=True) with open(target, "wb") as f: f.write(data) return target def read_tree(root): out = {} for base, _dirs, files in os.walk(root): for name in files: path = os.path.join(base, name) with open(path, "rb") as f: out[os.path.relpath(path, root).replace(os.sep, "/")] = f.read() return out def main(): work = tempfile.mkdtemp(prefix="svfs_demo_") try: # ---- 1. 造一棵源目录:含两份内容完全相同的文件,用来展示去重 ---- src = os.path.join(work, "dist") write_file(src, "index.html", b"demo" * 8) write_file(src, "app.js", b"console.log('hello');\n" * 40) write_file(src, "assets/logo.svg", b"" * 50) write_file(src, "assets/vendor.js", b"export const V = 1;\n" * 60) write_file(src, "admin/vendor.js", b"export const V = 1;\n" * 60) os.makedirs(os.path.join(src, "empty_dir"), exist_ok=True) # ---- 2. 打包(zlib 压缩 + 内容寻址去重)---- step("打包:整个文件夹 → 单个 .svfs") db = os.path.join(work, "app.svfs") vfs = SQLiteVFS(db, compress=True) with quiet(): FolderPacker(vfs, fs_name="demo").pack_folder(src) vfs.close() vfs = SQLiteVFS(db, readonly=True) stats = vfs.get_stats() blobs = vfs.list_blobs() unique_bytes = sum(b["size"] for b in blobs) stored_bytes = sum(b["stored_size"] for b in blobs) print(f" 包大小: {os.path.getsize(db) / 1024:.1f} KB") print(f" 文件数: {stats['total_files']} 原始总大小: {stats['total_size']} 字节") print( f" 去重后唯一内容: {len(blobs)} 份 / {unique_bytes} 字节" f"(两份 vendor.js 共用一份,省 {stats['total_size'] - unique_bytes} 字节)" ) print(f" 压缩后库内实际占用: {stored_bytes} 字节") vfs.close() # ---- 3. 完整性校验 + 签名(只读即可验证)---- step("完整性校验与签名") vfs = SQLiteVFS(db) report = vfs.verify(check_content=True) print( f" 校验通过: {report['ok']}" f"(逐文件重算 sha256:{report['content_checked']} 个)" ) print(f" 快照摘要: {report['actual_digest'][:32]}…") vfs.sign("demo-key", key_id="demo") vfs.close() readonly = SQLiteVFS(db, readonly=True) print(f" 只读连接上验签: {readonly.verify_signature('demo-key')['ok']}") readonly.close() # ---- 4. 快照:同一路径存两个版本 ---- step("版本历史(快照)") vfs = SQLiteVFS(db, compress=True) parent = vfs._active_snapshot() vfs.create_snapshot("v2", parent_id=parent, copy_entries=True) vfs.remove("app.js") new_app = write_file(work, "app-v2.js", b"console.log('v2');\n" * 40) with quiet(): vfs.add_file(new_app, "app.js") vfs.recount() print(f" v2 里的 app.js: {vfs.read_file('app.js')[:24]!r}") vfs.use_snapshot(parent) print(f" v1 里的 app.js: {vfs.read_file('app.js')[:24]!r}") for snapshot in vfs.list_snapshots(): print( f" 快照 {snapshot['id']} {snapshot['name']}: " f"{snapshot['total_files']} 个文件,摘要 {snapshot['digest'][:12]}…" ) vfs.close() # ---- 5. 发布成"按 hash 取对象 + 清单" ---- step("发布到对象存储(对象名 = 内容 sha256)") objects = os.path.join(work, "public") # 注意默认发布**最新**快照(此时是 v2,app.js 已被替换)。这里显式指定 v1, # 因为第 7 步要拿它和源目录比对;要发布 v2 就去掉 snapshot_id。 result = publish.export_objects(db, objects, snapshot_id=parent) print(f" 发布快照: {result['snapshot_id']}(默认会发布最新的那个)") print(f" 对象: {result['objects']} 个,共 {result['objects_bytes']} 字节") print( f" 清单: {os.path.basename(result['manifest'])}" f"({result['manifest_bytes']} 字节,含全部路径、大小与 sha256)" ) # ---- 6. 从"对象 + 清单"取回,并逐个校验 sha256 ---- step("取回并逐个校验 sha256") manifest = publish.load_manifest(result["manifest"]) restored = os.path.join(work, "restored") fetch_stats = publish.ObjectFetcher( manifest, local_root=objects, cache_dir=os.path.join(work, "cache") ).fetch(restored) print( f" 取回 {fetch_stats.files} 个文件," f"校验 {fetch_stats.verified} 个对象" ) # ---- 7. 逐字节比对 ---- step("与源目录逐字节比对") if read_tree(src) == read_tree(restored): print(" 一致 ✓") else: print(" 不一致 ✗") return 1 print(f"\n全部演示完成(文件都在临时目录里,退出时删除)") return 0 finally: shutil.rmtree(work, ignore_errors=True) if __name__ == "__main__": sys.exit(main())