Metadata-Version: 2.5
Name: module_bank
Version: 0.4.2
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 导入系统
- **完整包支持** - 支持包结构和子模块导入
- **内容去重与压缩** - 内容按 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"
```

### 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"

# 列出数据库中的模块
mbank list --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"
```

### 编程接口

#### 打包模块

```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

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

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

# 列出所有可用模块
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         # 主打包类
├── 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/ 下）

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

### 包内布局

`.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
├─ 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()
```

## 🔧 高级功能

### 排除模式

```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` 重新打包**——
  旧包被打开时会直接报错说明这一点，不会静默给出一个空包
- 字节码不跨 Python 版本兼容（3.12 编译的模块无法在 3.11 加载）
- 不支持 C 扩展模块
- PyArmor 运行时文件与操作系统相关（Windows 用 `.pyd`，Linux 用 `.so`）
- 运行时文件需在目标系统上重新打包
- 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", readonly=True)
packer.sign_version("共享密钥", key_id="release-1.0")   # 或先切到那一版再签
report = packer.verify_version("共享密钥")              # {"ok": bool, "errors": [...]}
packer.close()
```

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

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

```bash
mbank install --db modules.mbank --signature-key "$KEY" --require-signature
```

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

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

## 📚 应用场景

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

---

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

**许可证**: MIT License
