2026-09-14 00:11:42 +08:00
|
|
|
|
#!/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
|
2026-09-14 00:11:42 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-09-14 00:11:42 +08:00
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(main())
|