← Files EW_knowledge_forgeARCHIVED FILE
skills/obsidian-knowledge-forge/scripts/build_vault_zip.py
2.89 KB · Oct 2, 2026 · 00:32 UTC
#!/usr/bin/env python3
"""Build a standard, plugin-free Obsidian Vault ZIP."""
from __future__ import annotations
import argparse
import shutil
import tempfile
import zipfile
from pathlib import Path, PurePosixPath
BLOCKED_PARTS = {".obsidian", "__MACOSX"}
BLOCKED_SUFFIXES = {".exe", ".dll", ".dylib", ".so", ".bat", ".cmd"}
def safe_path(path: Path, root: Path) -> PurePosixPath:
rel = PurePosixPath(path.relative_to(root).as_posix())
if rel.is_absolute() or ".." in rel.parts or any(part in BLOCKED_PARTS for part in rel.parts):
raise ValueError(f"Unsafe Vault path: {rel}")
if path.suffix.lower() in BLOCKED_SUFFIXES:
raise ValueError(f"Executable files are not allowed: {rel}")
return rel
def render_guide(template: Path, vault_name: str) -> str:
return template.read_text(encoding="utf-8").replace("{{VAULT_NAME}}", vault_name)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("staging", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--vault-name", required=True)
parser.add_argument("--assets-dir", type=Path, default=Path(__file__).resolve().parent.parent / "assets")
args = parser.parse_args()
source, output = args.staging.resolve(), args.output.resolve()
vault_name = args.vault_name.strip().strip("/\\")
if not source.is_dir() or not vault_name or output.suffix.lower() != ".zip":
parser.error("Provide a staging directory, a Vault name, and a .zip output")
with tempfile.TemporaryDirectory(prefix="ew-vault-") as temporary:
vault = Path(temporary) / vault_name
shutil.copytree(source, vault)
guides = {
"README-START-HERE.md": args.assets_dir / "README-START-HERE.md",
"使用說明-請先閱讀.md": args.assets_dir / "使用說明-請先閱讀.md",
"分卷ZIP安裝與合併說明.md": args.assets_dir / "分卷ZIP安裝與合併說明-template.md",
}
for filename, template in guides.items():
(vault / filename).write_text(render_guide(template, vault_name), encoding="utf-8")
output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive:
for path in sorted(vault.rglob("*")):
if path.is_file():
archive.write(path, PurePosixPath(vault_name) / safe_path(path, vault))
with zipfile.ZipFile(output) as archive:
bad, names = archive.testzip(), archive.namelist()
if bad:
raise ValueError(f"ZIP integrity check failed: {bad}")
for guide in guides:
if not any(name.endswith(guide) for name in names):
raise ValueError(f"Required ZIP guide is missing: {guide}")
print(f"Built {output} with {len(names)} files")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: ee19f468c511ef1437ad86f6319cebdcd23ece3343d2e228fd6fe0dc7702a012