Manual-commit notebook snapshot extension (JupyterLab 4):
Backend
- Storage adapter layer (Local + S3 via boto3, pip extra)
- Dumb put/get/list/delete contract; key validation
- S3Storage: lazy boto3 import, S3ConnectionError with friendly
messages + connectivity check at startup
- SnapshotStore
- Strip code outputs/execution_count, MD5 on cleaned JSON
- Gzipped envelope (id/timestamp/name/description/hash/size/notebook)
- Append-only manifest.json (rebuildable from version files)
- SnapshotUnchangedError when new hash matches most recent version
- REST API (commit / list / content) under /snapshot/ namespace
- traitlets config (storage_type, local_root, s3_* + S3_* env fallback)
Frontend
- snapshot:commit command (toolbar button + command palette)
- Dialog for name + description
- SnapshotPanel (left sidebar timeline) + DiffWidget (main area)
- Cell diff: id-match first, LCS fallback; jsdiff for line diff
- Restore via model.fromJSON() + context.save() (no refresh)
- All AGENTS.md hard conventions enforced
Tests: 33 backend pytest passing (storage + store + routes)
Docs: AGENTS.md, design.md, README.md synced with implementation.
Co-Authored-By: Claude <noreply@anthropic.com>
85 lines
6.7 KiB
Markdown
85 lines
6.7 KiB
Markdown
### `notebook-snapshot` 架构总结
|
||
|
||
本方案摒弃了对宿主机 Git CLI 的依赖,采用**轻量级本地文件 / 对象存储快照模式**。通过后端存储网关实现版本的压缩持久化,前端专注监听与 Cell 级的 UI 比对,整体具有**零环境依赖、轻量极速、天然兼容云存储**的特点。
|
||
|
||
#### 1. 前端职责 (TypeScript + React)
|
||
|
||
* **手动快照提交**:注册命令 `snapshot:commit`(notebook 工具栏按钮 + 命令面板入口),弹出对话框填写 **name(必填)+ description(可选)** 后 POST 提交。**不做** contentChanged 防抖自动提交,**不绑定** Ctrl+S(避免与 `docmanager:save` 冲突)。
|
||
* **可视化侧边栏**:左侧栏渲染历史版本时间轴(名称 / 描述 / 时间戳 / 大小,来源于 manifest 查询 API),提供「对比 (Diff)」与「恢复 (Restore)」入口。
|
||
* **Cell 级 Diff 引擎**:先按 nbformat 4.5 cell id 匹配(modified / unchanged / added / deleted),LCS 仅作无 id 时的对齐兜底;行级 diff 用 jsdiff 渲染红绿行。**禁用 Monaco**。
|
||
* **无刷新还原**:确认对话框(含「未保存修改将丢失」警告)→ GET 旧版本 JSON → `model.fromJSON()` 内存覆盖 → `context.save()` 落盘。
|
||
|
||
#### 2. 后端职责 (Jupyter Server Extension / Python)
|
||
|
||
* **统一存储适配器 (Storage Adapter)**:抹平本地隐藏目录(如 `.notebook_versions/`)与云端对象存储(S3 / MinIO / 阿里云 OSS 等 S3-API 兼容服务)的接口差异。**已实现** Local File + S3(boto3),哑接口 `put/get/list_keys/delete`。
|
||
* **内容清洗 (Sanitizer)**:剥离全部 `outputs` 与 `execution_count`,基于清洗后 JSON 计算 MD5 并记录;**新内容 hash 与最近版本相同则跳过保存**(弹 warning,append-only 历史不变)。
|
||
* **压缩持久化**:快照文件为 gzip 压缩的 JSON **envelope**(`{id, timestamp, name, description, hash, size, notebook}`),文件名 `v_<ts>_<hash8>.ipynb.gz`,KB 级。
|
||
* **索引维护 (Manifest)**:`manifest.json` 是纯索引(原子写)。真相在快照文件的 envelope 内,manifest 损坏 / 丢失时可扫描版本文件重建。
|
||
|
||
#### 3. 可配置项
|
||
|
||
* **存储类型**:`local`(默认;路径 `c.SnapshotConfig.local_root`,缺省 `<root_dir>/.notebook_versions`) / `s3`(boto3;六件套 traitlet + `S3_ENDPOINT` / `S3_ACCESS_KEY` / `S3_SECRET_KEY` 环境变量兜底;同一配置可指向 AWS S3 或自建 MinIO)
|
||
|
||
#### 4. API 契约
|
||
|
||
* `POST /snapshot/notebook-version/commit` — body `{path, content, name, description}`;`content` 为前端显式提交的 notebook JSON 本体(后端**不**从磁盘读)。若新内容 hash 与最近版本相同,响应 `{"skipped": true, "reason": "unchanged", "message": "..."}`(HTTP 200,前端弹 warning)。
|
||
* `GET /snapshot/notebook-version/list?path=...` — 返回 manifest 版本列表(name / description / 时间戳 / hash / 大小)。
|
||
* `GET /snapshot/notebook-version/content?path=...&id=...` — 解压并返回旧版本 notebook JSON(已剥离 outputs)。
|
||
* `POST /snapshot/notebook-version/restore` — **暂缓**。前端 restore 走 `fromJSON()` + `context.save()` 闭环,不依赖此端点。
|
||
|
||
---
|
||
|
||
### 系统交互流程图
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────────────┐
|
||
│ JupyterLab FrontEnd │
|
||
└──────────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
├─► [ 1. 用户点击工具栏「保存快照」按钮 / 命令面板执行 snapshot:commit ]
|
||
│ │
|
||
│ ▼
|
||
├─► [ 2. 弹出对话框:填 name (必填) + description (可选) ]
|
||
│ │
|
||
│ ▼
|
||
├─► [ 3. (POST /snapshot/notebook-version/commit) body={path, content, name, description} ]
|
||
│ │
|
||
┌──┴─────────────────────────────────────────────────────────────────────────────────┴──┐
|
||
│ Jupyter Server Extension │
|
||
└──┴────────────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
├─► [ 4. Outputs / execution_count 清洗 & MD5 哈希计算 (基于清洗后 JSON) ]
|
||
│ │
|
||
│ ▼
|
||
├─► [ 5. 构建 envelope {id, timestamp, name, description, hash, size, notebook} ]
|
||
│ │
|
||
│ ▼
|
||
├─► [ 6. Gzip 压缩 envelope → 写入 v_<ts>_<hash8>.ipynb.gz (Storage Adapter) ]
|
||
│
|
||
└─► [ 7. append manifest.json (Storage.put 原子写) ]
|
||
│
|
||
│ (200 entry 响应)
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────────┐
|
||
│ JupyterLab FrontEnd │
|
||
└──────────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
├─► [ 8. 侧边栏 refresh (GET /notebook-version/list?path=...) → 渲染带 name 的条目 ]
|
||
|
||
|
||
├──► [ 9. 用户点击某条 "Diff" ]
|
||
│ │
|
||
│ ├──► (GET /notebook-version/content?path=&id=) ──► 后端返回清洗后旧 JSON
|
||
│ │
|
||
│ └──► 前端按 cell id 匹配 + LCS 兜底 + jsdiff 行级 ──► 主区 DiffWidget 红绿高亮
|
||
│
|
||
│
|
||
└───► [ 10. 用户点击 "Restore" ]
|
||
│
|
||
├──► 确认对话框(警告未保存修改将丢失,提示先建快照)
|
||
│
|
||
├──► (GET /notebook-version/content?path=&id=) ──► 旧 JSON
|
||
│
|
||
└──► model.fromJSON() 内存覆盖 + context.save() 落盘
|
||
```
|