Metadata-Version: 2.5
Name: module_bank
Version: 0.5.1
Summary: 把 Python 模块打包成单文件（.mbank / svfs 包）并按需导入
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: cryptography>=45.0.7
Requires-Dist: pyarmor>=9.2.4
Requires-Dist: sqlite-vfs>=0.4.0
Description-Content-Type: text/markdown

# Python Module Bank - 单文件模块仓库

把 Python 模块打包进一个文件（`.mbank`），运行时直接从包里导入，支持 **AES 字节码加密**
和 **PyArmor 代码混淆** 双重保护。

存储层是 [`sqlite_vfs`](http://124.71.68.6:3000/chakcy_code_repository/svfs)：
`.mbank` 就是一个 **标准 svfs 包**——一个用 SQLite 实现的虚拟文件系统，模块、源码、
PyArmor 运行时、第三方依赖都以文件形式躺在里面。

## 🌟 特性

- **单文件分发** - 所有模块打包到单个 `.mbank`；它同时是标准 `.svfs` 包，`svfs` /
  `vfsverify` / `vfsunpack` / `svfs publish` 这些工具都能直接处理它
- **AES 字节码加密** - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储
- **PyArmor 代码混淆** - 支持使用 PyArmor 对 Python 源代码进行混淆保护
- **双层保护** - 可同时使用 PyArmor 混淆 + AES 字节码加密，提供双重安全保障
- **动态导入** - 运行时直接从包加载模块，无缝集成 Python 导入系统
- **完整包支持** - 支持包结构和子模块导入
- **第三方依赖随包分发** - 自动扫描 import 或显式指定，把打包机上已安装的第三方
  包（含 C 扩展、数据文件、`.dist-info`）一起收进包里的 `packages/`；运行时首次
  解包到用户缓存目录，之后直接复用，目标机不需要再安装这些依赖
- **内容去重与压缩** - 内容按 sha256 寻址（同一份运行时/文件只存一次），写入时
  zlib 压缩（压不动的内容原样存，不浪费）
- **可校验** - 快照带摘要，`vfsverify` 能验证包内容是否被改过
- **CLI 工具** - 提供完整的命令行接口

## 📦 安装

### 从源码安装

```bash
git clone http://124.71.68.6:3000/chakcy/module_bank.git
cd module-bank
pip install -e .
```

### 使用 uv（推荐）

```bash
git clone http://124.71.68.6:3000/chakcy/module_bank.git
cd module-bank
uv sync
```

### 可选依赖

- **PyArmor** (强烈推荐): `uv add pyarmor` 或 `pip install pyarmor`
  - 为模块提供 Python 源代码级别的混淆保护
  - 商业版可去除试用版水印

### 存储层依赖

`sqlite-vfs` 是必装依赖，在 `pyproject.toml` 里以 git 依赖声明。发布到 PyPI 之前，
**`v0.4.0` 那个 tag 必须先推到 Gitea**，否则 `uv sync` / `pip install` 装不上（那个
仓库是私有的话，使用者还要能访问它，比如配好凭据助手或 `.netrc`）。

本地开发期间还没推 tag 时，把 `pyproject.toml` 里注释掉的 `[tool.uv.sources]`
覆盖打开（指向本机 sqlite_vfs 目录），或者直接
`uv pip install -e <sqlite_vfs 路径>`。

## 🚀 快速开始

### 1. 创建示例模块

```python
# my_package/__init__.py
def hello():
    print("Hello from my_package!")
    return "success"

VERSION = "1.0.0"
```

```python
# my_package/utils.py
def add(a, b):
    return a + b
```

### 2. 打包模块到数据库

#### 使用 AES 加密（推荐）

```bash
# 生成密钥并打包
mbank pack my_package --db my_package.mbank --key "$(python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())")"
```

#### 使用 PyArmor 混淆

```bash
# 使用 PyArmor 保护源代码
mbank pack my_package --db my_package.mbank --pyarmor
```

#### 使用双层保护

```bash
# PyArmor 混淆 + AES 加密
mbank pack my_package --db my_package.mbank --pyarmor --key "your-encryption-key"
```

#### 连同第三方依赖一起打包

依赖需要先装在**打包环境**里（工具不联网、不执行 pip，只从已安装的包里收文件）：

```bash
# 自动扫描源码里的 import，把自己写的包用到、且已安装的第三方依赖一起收进来
mbank pack my_package --db my_package.mbank --with-deps

# 显式指定（发行版名或导入名都行；默认沿 Requires-Dist 收传递依赖）
mbank pack my_package --db my_package.mbank --dep requests --dep yaml

# 从 requirements.txt 读依赖名（同样要求已安装）
mbank pack my_package --db my_package.mbank --requirements requirements.txt
```

打包结果可以自检：

```bash
mbank deps --db my_package.mbank      # 列出随包分发的第三方依赖及版本
```

依赖只在**第一次运行**时解压到用户缓存目录，后续进程按内容指纹复用同一份目录，
不再重复写盘：

```bash
mbank install --db my_package.mbank --cache-dir /var/cache/myapp   # 自定义缓存位置
mbank install --db my_package.mbank --no-cache                     # 关闭缓存（临时目录）
mbank cache                                                        # 查看缓存占用
mbank cache --clear                                                # 清理缓存
```

### 3. 从数据库导入

```python
# import_example.py
from module_bank.python_to_sqlite import PythonToSQLite

packer = PythonToSQLite("my_package.mbank")
finder = packer.install_importer(key="your-encryption-key")  # 如果加密了需要提供密钥

from my_package import utils
from my_package import hello

print(hello())
print(utils.add(3, 4))
```

## 🛡️ 安全保护机制

### 第一层：PyArmor 代码混淆

PyArmor 对 Python 源代码进行混淆，将代码转换为难以阅读和逆向的形式：

- 混淆后的代码依赖运行时二进制文件（`.pyd`/`.so`），无法直接运行
- 支持混淆级别控制（代码混淆、模块混淆）
- 运行时文件会自动存储在包中（`runtime/`），加载时自动部署到临时目录
- **整个包只跑一次 PyArmor**，因此整包只存**一份**运行时：PyArmor 每次调用都会
  生成一份自己的运行时（同名、内含那一次的新密钥、约 639 KB），逐模块调一次就会
  让包体积按模块数线性膨胀（实测 6 个模块 2.3 倍、30 个模块约 6 倍）

```bash
# 高级混淆选项
mbank pack my_module.py --name my_module --db modules.mbank \
    --pyarmor \
    --pyarmor-obf-code 1 \
    --pyarmor-obf-mod 1
```

### 第二层：AES 字节码加密

编译后的字节码使用 Fernet (AES-128-CBC) 加密：

```bash
# 生成密钥
python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())" > my_key.key

# 使用密钥打包
mbank pack my_module.py --name my_module --db modules.mbank --key "$(cat my_key.key)"

# 使用时提供密钥
mbank install --db modules.mbank --key "$(cat my_key.key)"
```

### 把密钥存进包（可选，注意权衡）

`packer_key` / `get_key` 可以把打包密钥写进包里，省掉"分发时要另传密钥"的麻烦：

```python
packer = PythonToSQLite("modules.mbank", key=aes_key)
packer.pack_directory("src/")
packer.packer_key(aes_key)     # 存进包根部（是 packer_key 文件，不是模块）
packer.close()

# 使用方拿到包就能直接跑（不用再传 key）
packer = PythonToSQLite("modules.mbank", readonly=True)
key = packer.get_key()          # 没存过返回 None
packer.install_importer(key=key)
```

> **先把话说清楚**：密钥和密文躺在同一个文件里，AES 这一层对"包泄露"就不再是
> 保护——能读包的人就能解开。它的价值是省事，真正的保护来自 PyArmor 那一层。
> 要真保密，就别用这两个方法，让使用方自己持有 key。

### 安全建议

1. **密钥管理**：加密密钥不要硬编码在代码中，使用环境变量或密钥管理服务
2. **删除源代码**：打包后可从数据库删除源代码，仅保留混淆后的代码：

```bash
# 删除所有模块的源代码
mbank delete_source --db modules.mbank

# 删除指定模块的源代码
mbank delete_source --db modules.mbank --module my_module
```

## 📖 详细使用

### 命令行工具

```bash
# 打包单个模块
mbank pack my_module.py --name my_module --db modules.mbank

# 打包整个目录（自动识别包结构）
mbank pack my_package/ --db modules.mbank

# 使用 PyArmor 混淆打包
mbank pack my_module.py --name my_module --db modules.mbank --pyarmor

# 使用 AES 加密打包
mbank pack my_module.py --name my_module --db modules.mbank --key "your-key"

# 双层保护
mbank pack my_module.py --name my_module --db modules.mbank --pyarmor --key "your-key"

# 连同第三方依赖一起打包（自动扫描 import）
mbank pack my_package/ --db modules.mbank --with-deps

# 显式指定依赖（可重复），或从 requirements.txt 读
mbank pack my_package/ --db modules.mbank --dep requests --dep idna
mbank pack my_package/ --db modules.mbank --requirements requirements.txt

# 排除某个依赖（例如平台专属、运行环境自带的包）
mbank pack my_package/ --db modules.mbank --with-deps --dep-exclude pywin32

# 列出数据库中的模块
mbank list --db modules.mbank

# 列出随包分发的第三方依赖
mbank deps --db modules.mbank

# 删除源代码
mbank delete_source --db modules.mbank
mbank delete_source --db modules.mbank --module my_module

# 安装导入器并进入交互模式
mbank install --db modules.mbank
mbank install --db modules.mbank --key "your-key"

# 依赖解包缓存（默认用户缓存目录；--no-cache 关闭）
mbank install --db modules.mbank --cache-dir /var/cache/myapp
mbank install --db modules.mbank --no-cache

# 查看/清理依赖缓存
mbank cache
mbank cache --clear
```

### 编程接口

#### 打包模块

```python
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption

# 生成加密密钥
key = Encryption.generate_key()

# 使用 AES 加密打包
packer = PythonToSQLite("modules.mbank", key=key)

# 或使用 PyArmor 混淆打包
packer = PythonToSQLite(
    "modules.mbank",
    pyarmor_obfuscate=True,
    pyarmor_obf_code=1,
    pyarmor_obf_mod=1,
)

# 打包单个模块
packer.pack_module("module.py", "module_name")

# 打包整个目录（自动识别包结构）
packer.pack_directory("my_package/")

# 验证包结构
packer.verify_package_structure()
```

#### 打包第三方依赖

```python
from module_bank import PythonToSQLite
from module_bank.dependencies import find_third_party_imports

packer = PythonToSQLite("modules.mbank")

# 显式指定：发行版名、导入名、requirements.txt 路径，或它们的列表
packer.pack_dependencies(["requests", "yaml"], recursive=True)

# 自动扫描自己的源码，把已安装的第三方依赖一起收进来
detected = find_third_party_imports(
    ["my_package/"],                 # 目录或 .py 文件
    packed_top_levels=["my_package"],  # 自己的包不算第三方
)
packer.pack_dependencies(optional=detected)   # 找不到只提示，不报错

# 排除某些依赖（支持发行版名或导入名）
packer.pack_dependencies(["requests"], exclude=["pywin32"])

# 看包里到底带了什么
for dependency in packer.list_dependencies():
    print(dependency["distribution"], dependency["version"],
          dependency["top_level"], dependency["files"])
```

`requirements=` 里的名字找不到会直接抛 `DependencyError`（名字写错不该静默产出
一个缺依赖的包）；`optional=` 里找不到只打印提示（代码里的可选导入很常见）。

#### 导入模块

```python
from module_bank import PythonToSQLite

# 如果数据库中的模块被 AES 加密了，需要提供密钥
packer = PythonToSQLite("modules.mbank")

# 安装导入器到 sys.meta_path
finder = packer.install_importer(key="your-key")

# 依赖解包缓存：默认用户缓存目录（首次运行解压，之后直接复用）
finder = packer.install_importer(cache_dir="/var/cache/myapp")
# 关闭缓存：每次解到临时目录，finder.close()/进程退出时删除
finder = packer.install_importer(cache=False)

# 列出所有可用模块
modules = packer.list_modules()
for module in modules:
    package_flag = " [包]" if module["is_package"] else ""
    print(f"  - {module['module_name']}{package_flag}")

# 直接导入数据库中的模块（就像普通模块一样）
import my_package
import my_package.submodule

print(my_package)
my_package.hello()
```

## 🏗️ 架构设计

### 核心组件

```python
src/module_bank/
├── layout.py                   # 包内布局：模块名 ↔ svfs 路径的唯一映射处
├── python_to_sqlite.py         # 主打包类
├── dependencies.py             # 第三方依赖：发现、文件收集、依赖清单
├── dependency_cache.py         # 第三方依赖的落盘缓存（首次解压、后续复用）
├── sqlite_module_importer.py   # 存储管理器（写侧：模块、依赖与 PyArmor 运行时）
├── sqlite_meta_path_finder.py  # 元路径查找器（只读打开 + 模块清单缓存 + 依赖解包）
├── sqlite_module_loader.py     # 模块加载器（读包、解密、部署运行时）
├── encryption.py               # AES 密钥管理
├── pyarmor_encryption.py       # PyArmor 混淆封装
├── cli.py                      # 命令行接口
└── __init__.py                 # 模块导出
```

### 数据流

```text
1. 打包阶段:
   .py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密)
            → 作为文件写进 svfs 包（modules/ 下）

   第三方依赖（packages/）:
   已安装的发行版 → 按 RECORD/顶层目录收文件（.py / .pyd / 数据 / dist-info）
                 → 原样写进 svfs 包（packages/ 下）+ 依赖清单

2. 导入阶段:
   导入请求 → MetaPathFinder 查内存缓存 → ModuleLoader 读包
            → (AES 解密) → (PyArmor 运行时部署到临时目录) → 执行模块

   第一次 import 到某个第三方依赖:
   按依赖内容指纹查缓存 → 命中则直接复用；未命中解出 packages/<顶层名>/ + dist-info
   → 用户缓存目录（deps-<hash>）→ sys.path → 标准路径查找器加载
   （原生扩展、数据文件、importlib.metadata 都按真实文件工作）
```

### 包内布局

`.mbank` 是一个标准 svfs 包（schema v3），把它 unpack 出来就是下面这棵树——
所以"包里有什么"看一眼目录就知道：

```text
bank.mbank
├─ modules/a/b.pyc                      字节码（marshal，可选 AES 加密）← 模块存在的锚点
├─ modules/a/b.py                       源码（PyArmor 混淆后的原文；可删）
├─ modules/a/b.meta.json                标志位与用户元数据（全默认值时不写这个文件）
├─ modules/pkg/__init__.pyc             包（is_package）用 __init__
├─ modules/pkg/__init__.py
├─ modules/pkg/__init__.meta.json
├─ packages/requests/__init__.py        第三方依赖：按安装时的文件原样存
├─ packages/requests-2.34.2.dist-info/  依赖元数据（importlib.metadata 用）
├─ packages/.manifest.json              依赖清单（发行版/版本/文件列表）
├─ runtime/pyarmor_runtime_000000/pyarmor_runtime.pyd
└─ runtime/pyarmor_runtime_000000/__init__.py
```

几条约定：

- 点分模块名里的点换成目录分隔符：`a.b` → `modules/a/b`，所以包的字节码落在
  `modules/a/b/__init__.pyc`，与普通模块 `modules/a/b.pyc` 不冲突；
  `is_package` 完全由路径决定（不记在元数据里，免得两处说法打架）
- **`.pyc` 是锚点**：每个模块一定有字节码（打包时总是从源码编译），源码和元数据
  都是可选的——`delete_source` 删掉的就是 `.py` 那个文件
- 元数据文件里只有不可推导的信息：

  ```json
  {
    "format": 1,
    "encrypted": false,
    "pyarmor_protected": true,
    "pyarmor_runtime": "pyarmor_runtime_000000",
    "metadata": { "version": "1.0" }
  }
  ```

- 一个模块的源码 + 元数据 + 字节码写在**同一个事务**里，所以不会出现"新元数据配
  旧字节码"的中间态

### 用 svfs 的工具链直接操作包

因为 `.mbank` 就是 svfs 包，下列操作不需要 module_bank 参与：

```bash
svfs info bank.mbank              # 统计与快照信息
vfsverify bank.mbank              # 校验内容摘要
vfsunpack bank.mbank ./out        # 导出成上面那棵目录树
svfs sign bank.mbank --key "$K"   # 给快照做 HMAC 签名（防篡改）
```

```python
from sqlite_vfs.core import SQLiteVFS

vfs = SQLiteVFS("bank.mbank", readonly=True)
print(vfs.get_stats())
print([i["name"] for i in vfs.list_directory("modules")])
vfs.close()
```

## 🔧 高级功能

### 打包第三方依赖

#### 它做了什么

`packages/` 是打包机 site-packages 的一个裁剪镜像：每个被选中的发行版按
`RECORD` 收齐**它自己的**文件（`.py`、`.pyd`/`.so`、数据文件、`licenses`），
连同它的 `<name>-<version>.dist-info/` 一起写进包。运行时第一次 import 到某个
依赖的顶层包时，把这棵子树解到临时目录、挂到 `sys.path` 最前面，之后就是标准
Python 导入——C 扩展能加载、`importlib.resources` 能读数据、
`importlib.metadata.version()` 能取到版本号。

依赖清单（`packages/.manifest.json`）记录每个发行版的版本、顶层名与文件列表，
供 `mbank deps` 展示，也用于**增量替换**：同一个发行版重新打包时，先写新版本，
再按旧清单删掉新版本已经没有的文件，不会留下旧的 dist-info。

#### 怎么选依赖

- `--with-deps`：静态扫描源码里的 `import` / `from ... import` / 字面量
  `importlib.import_module("x")`，排掉标准库和自己的包，剩下的作为候选。只收
  **打包环境里已经安装**的包，不联网、不执行 pip。
- `--dep NAME`：显式指定，发行版名（`PyYAML`）或导入名（`yaml`）都行；找不到
  直接报错。
- `--requirements FILE`：读 requirements.txt 里的名字（`-r`、`--index-url` 等
  选项忽略）。
- 默认沿 `Requires-Dist` 递归收传递依赖（`--no-dep-recursive` 关掉）；
  `extra == "..."` 标记的可选依赖不收。
- `--dep-exclude NAME`：排除某个依赖，应对平台专属或运行环境自带的包。

#### 运行时行为与限制

- 依赖第一次被 import 时解到**用户缓存目录**，之后进程直接复用，不再重复解压：
  Windows 是 `%LOCALAPPDATA%\module_bank\cache`，macOS 是
  `~/Library/Caches/module_bank`，Linux 是 `$XDG_CACHE_HOME/module_bank`
  （默认 `~/.cache/module_bank`）。缓存目录按依赖内容指纹命名（`deps-<hash>`），
  包内容、Python 版本或平台一变就自动换新目录，不会误用旧缓存。
- 用 `--cache-dir` / `install_importer(cache_dir=...)` 改位置，`MBANK_CACHE_DIR`
  改默认根目录；`--no-cache` / `cache=False` 回到临时目录模式（关闭时删除）。
- 缓存目录是用户私有的（POSIX 0700）；不可写时会**自动回退**到临时目录并给出
  警告，导入本身不受影响。
- 并发冷启动是安全的：文件先写临时名再原子替换，完成标记用 `O_EXCL` 创建，两个
  进程最多重复解一遍，不会读到半份文件。
- 缓存不会自动删除；用 `mbank cache` 查看占用、`mbank cache --clear` 清理。
  缓存模式不删除已加载的 `.pyd`，Windows 上"运行时锁定文件"的问题也就不存在了。
- **依赖是明文**：第三方代码本来就是公开的，这一层不做 PyArmor/AES；保护只
  针对你自己放在 `modules/` 里的模块。
- 包里的依赖是**为打包平台与 Python 版本准备的**：带 C 扩展的依赖（numpy、
  cryptography…）不能跨系统/跨版本直接用，需要在目标平台重新打包。
- 自动扫描是静态的：动态拼接的 import（`importlib.import_module(name)` 的变量
  形式）发现不了，用 `--dep` 补。
- 不会安装 console script（依赖自带的命令行工具）；`entry_points` 元数据会随
  dist-info 一起带上，但入口脚本本身没有安装。
- `.pth` 文件、安装器写入 site-packages 之外的脚本/数据不在收集范围内。
- 子进程里 import 依赖时，需要子进程自己也调用 `install_importer()`——
  `sys.path` 的改动是进程内的。
- 同名冲突：`modules/` 里你自己的模块优先；依赖之间同名顶层包按 `sys.path`
  顺序（后解出来的包在前）。

### 排除模式

```python
# 打包时排除特定文件
packer.pack_directory(
    "my_project/",
    exclude_patterns=["*_test.py", "*.pyc", "__pycache__"],
)
```

### 元数据存储

```python
# 为模块添加元数据
packer.importer.add_module(
    "my_module",
    source_code,
    is_package=False,
    metadata={"version": "1.0", "author": "me"},
)
```

### 混合导入

```python
# 数据库导入器默认插入到 meta_path 开头，优先级高于文件系统
import sys
from module_bank import PythonToSQLite

packer = PythonToSQLite("modules.mbank")
finder = packer.install_importer()

# 如果需要文件系统优先，将查找器追加到末尾
sys.meta_path.remove(finder)
sys.meta_path.append(finder)
```

### 验证运行时完整性

```python
from module_bank.sqlite_module_importer import SQLiteModuleImporter

importer = SQLiteModuleImporter("modules.mbank")
runtimes = importer.list_runtimes()
for rt in runtimes:
    print(f"运行时: {rt['runtime_name']} (创建于: {rt['created_at']})")
```

## ⚠️ 注意事项

### 安全性

- 模块字节码直接执行，确保包来源可信
- AES 加密密钥必须安全保管，丢失后将无法解密模块
- PyArmor 混淆提供源代码级别的保护，但商业版才能去除试用版水印
- 生产环境建议 PyArmor 混淆 + AES 加密同时使用
- `delete_source` 会把源码文件及其内容从包里删掉（svfs 会回收不再被引用的内容），
  但和所有 SQLite 删除一样，**这不是安全擦除**：旧内容可能还留在数据库的空闲页里。
  真要彻底消除，打一个不含源码的新包，或者打完之后用 `VACUUM` 压一遍

### 兼容性

- **0.4.0 起存储层换成 sqlite_vfs**：`.mbank` 从"自建的表结构"变成 svfs 包
  （`modules/` + `runtime/` 文件树）。**旧包不兼容，需要用 `mbank pack` 重新打包**——
  旧包被打开时会直接报错说明这一点，不会静默给出一个空包
- **0.5.0 起支持第三方依赖**：依赖解析结果存在 `packages/` + 依赖清单里；没有
  清单的旧包行为不变，不需要重新打包
- **0.5.1 起依赖解包缓存**：首次运行解到用户缓存目录，之后按内容指纹复用；
  `cache=False` / `--no-cache` 可回到每次解临时目录的模式
- 字节码不跨 Python 版本兼容（3.12 编译的模块无法在 3.11 加载）
- **你自己的模块**不能是 C 扩展（打包时按源码编译为字节码）；第三方依赖可以
  连同 C 扩展一起打包
- 打包的第三方依赖与平台、Python 版本相关：带 C 扩展的依赖只能用于对应平台
  （如 Windows 的 `.pyd` 不能拿到 Linux 用），需要在目标平台重新打包
- PyArmor 运行时文件与操作系统相关（Windows 用 `.pyd`，Linux 用 `.so`）
- PyArmor 运行时与第三方依赖都需在目标系统上重新打包
- Python 3.10+（跟着 sqlite-vfs 与本项目实际用到的语法走）

### 性能

- **启动时**：一次遍历包内容建模块清单（纯内存查表，与包大小无关），外加字节码
  反序列化/运行时部署开销
- **运行时**：与传统导入性能相同（使用 sys.modules 缓存）
- **最佳适用**：长期运行的服务、桌面应用

### 更新模块

```python
# 重新打包会自动更新（同一批文件在一个事务里替换，不会出现新旧混搭）
packer.pack_module("updated_module.py", "module_name")
```

### 删除模块

没有单独的"删模块"命令：不再需要的模块用 `svfs` 直接删掉目录项即可。更常见的
做法是**删掉源码只留字节码**（见上面的 `delete_source`）——那是一份 `.mbank` 该
有的状态。

```python
from sqlite_vfs.core import SQLiteVFS

vfs = SQLiteVFS("modules.mbank")
vfs.remove("modules/module_to_remove.pyc")   # 锚点没了，模块即不存在
vfs.remove("modules/module_to_remove.py")
vfs.close()
```

### 备份与恢复

```bash
# 包是单个文件，易于备份
cp modules.mbank modules.backup.mbank

# 恢复
cp modules.backup.mbank modules.mbank
```

### 版本管理

包里的版本就是 svfs 的快照（版本之间共享内容 blob，多留一版几乎不占空间）。
分工是：**sqlite_vfs 只负责"把你要的那一版内容给你"，选哪一版、什么时候留版
是 module_bank 这一层的事**。

#### 定格一个版本

```python
from module_bank import PythonToSQLite

packer = PythonToSQLite("modules.mbank")
packer.pack_directory("src/")
packer.tag_version("release-1.0")   # 把当前内容定格成 release-1.0
packer.close()

# 之后继续改、继续打包，写的是自动派生的工作版本（名字叫 work），
# release-1.0 那份不再变动
packer = PythonToSQLite("modules.mbank")
packer.pack_directory("src/")
packer.tag_version("release-2.0")
for v in packer.list_versions():
    print(v["id"], v["name"], v["total_files"], "当前" if v["current"] else "")
packer.close()
```

> **为什么 `tag_version` 不是简单调一次 `create_snapshot(name)`**：svfs 的快照
> 语义是"当前那一版"（写入落进当前版本），所以 `create_snapshot("release-1.0",
> copy_entries=True)` 得到的名字落在**会继续变动**的那一版上——实测结果是打完
> v1、tag 成 release-1.0、再打 v2 之后，`release-1.0` 里是 **v2**。`tag_version`
> 因此是"先把当前快照改名（定格），再派生一个工作版本接手后续写入"。

#### 从指定版本加载

```python
# 加载 release-1.0 那一份（id 或名字都行）
packer = PythonToSQLite("modules.mbank", readonly=True, snapshot="release-1.0")
packer.install_importer()      # 之后 import 到的都是 v1.0 的代码
packer.close()
```

不指定 `snapshot` 就是**跟着最新（工作版本）走**；指定了就**钉死在那一版**，
包后续新增多少版本都不影响它——灰度、回滚、给老客户重发旧版都用这个。

`snapshot` 也可以传给 `install_importer(snapshot=...)`，或者在包里用
`use_version(...)` 切换当前版本。**可写句柄不能直接打开一个历史版本**（那是
"顺手改掉发出去的那份"）：确实要改，先显式 `use_version("release-1.0")`。

```bash
mbank list --db modules.mbank --snapshot release-1.0   # 看某一版里有什么
mbank install --db modules.mbank --snapshot release-1.0
```

#### 防篡改：签名与验签

签的是**快照摘要**（HMAC-SHA256，对称密钥，只挡"发出后被改过"，不挡伪造）：

```python
packer = PythonToSQLite("modules.mbank")   # 签名要写入签名记录 → 可写句柄
packer.sign_version("共享密钥", key_id="ci-2026", snapshot="release-1.0")
packer.close()
```

`key_id` 是"用哪把密钥签的"（同一版可以有多把密钥的签名，靠它区分），要签哪一版
用 `snapshot=`。验签（包括"加载前先验"）在**只读**句柄上就能做：

```python
packer = PythonToSQLite("modules.mbank", readonly=True)
report = packer.verify_version("共享密钥", snapshot="release-1.0", key_id="ci-2026")
print(report["ok"], report["errors"])            # {"ok": bool, "errors": [...]}
packer.close()
```

**验不验签是加载方的策略**，默认不验（不传就当没这回事）：

```python
# 加载前先验，验不过拒绝加载（抛 SignatureVerificationError）
finder = packer.install_importer(
    signature_key="共享密钥",
    signature_key_id="ci-2026",       # 可选：只认这个 key_id 的签名
    allow_unsigned=False,             # 那一版还没签过时是否放行（默认放行）
)
```

```bash
mbank install --db modules.mbank --signature-key "$KEY" \
    --signature-key-id ci-2026 --require-signature
```

`install_importer` 的结论落在返回的 finder 上（`finder.signature_report`）：报告里
`signature` 为 `None` 表示那一版**没有匹配的签名记录**（按了 `key_id` 就只认那个
key_id 的记录），默认策略下放行；这与"签名不匹配"是两回事——后者一律拒绝加载。
传了 `signature_key` 不等于验过了，脚本里要如实报，就看这个字段。

上面这些验签（`verify_version`、`install_importer`）都跑在只读连接上，不需要写权限。
更细的操作（快照清单、导出、发 CDN）直接用 svfs 的工具链：

```bash
svfs info modules.mbank               # 统计与当前快照
vfsverify modules.mbank               # 校验内容摘要
vfsunpack modules.mbank ./out         # 导出成目录树
svfs sign modules.mbank --key "$K" --key-id ci-2026   # 命令行签名（等价）
```

## 📚 应用场景

1. **商业软件分发** - PyArmor 混淆保护源代码知识产权
2. **安全敏感环境** - AES 加密确保即使数据库泄露也无法还原代码
3. **插件系统** - 动态加载数据库中的加密/混淆插件模块
4. **教育平台** - 安全分发练习代码，防止学生抄袭
5. **微服务** - 打包多个服务模块到单个加密文件
6. **嵌入式系统** - 减少文件系统依赖，提高安全性

---

**注意**: 本工具主要用于模块分发和部署场景，不适合开发阶段的频繁修改。

**许可证**: MIT License
