Files

167 lines
6.3 KiB
Python
Raw Permalink Normal View History

#!/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
2026-01-23 17:50:27 +08:00
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"<!doctype html><title>demo</title>" * 8)
write_file(src, "app.js", b"console.log('hello');\n" * 40)
write_file(src, "assets/logo.svg", b"<svg/>" * 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)
2026-01-23 17:50:27 +08:00
if __name__ == "__main__":
sys.exit(main())