# 上传「跳过已存在 / 改名保存」功能改造记录

- 日期：2026-09-12
- 对象：本机下载中心 `download-server.js`（端口 8899，DL_ROOT=dl-hub）
- 起因：用户提问「重新上传的时候，如果已经上传成功，是否会自动跳过？」
- 改动前结论：**不会跳过** —— 同名文件一律全量重传并覆盖

---

## 一、改动前的行为（核查结论）

| 文件大小 | 上传路径 | 重传行为 |
|---|---|---|
| ≤ 64MB | 一次 multipart POST 到当前目录 | 写临时文件后 `renameSync` 覆盖同名文件，**无存在性检查** |
| > 64MB | 分块续传 `/api/upload/chunk` + `/api/upload/complete` | **全量重传 + 覆盖**：续传进度只存在于 `.upload-parts/<hash>/`，而上传成功后会 `fs.rmSync` 删掉会话，所以 `status` 恒返回 `exists:false` |

唯一存在的「跳过」是**块级断点续传**：仅当上次上传中途中断、会话目录仍在，且 `fileSize` 与 `totalChunks` 都一致时，才会跳过已收到的块；不一致则 `abort` 后从零传。

## 二、本次改动（3 项）

### 1. 新增「同名且大小一致 → 跳过」（秒传式）
- 服务端 `GET /api/upload/status` 新增可选参数 `size`：当目标文件**已存在且字节数一致**时返回
  `{ok:true, done:true, skipped:true, target:"相对路径", size:N}`，前端据此直接跳过，不重传、不覆盖。
- 前端上传区新增复选框 **「同名且大小一致时跳过（不重传）」——默认勾选**。
- 不带 `size` 调用时行为与旧版完全一致（向后兼容）。

### 2. 新增「同名但大小不同 → 改名保存（不覆盖）」
- 复选框 **「同名但大小不同时改名保存（不覆盖）」——默认不勾选**（默认仍是覆盖，保持旧习惯）。
- 小文件：multipart 上传带 `?rename=1`；大文件：`/api/upload/complete` 请求体带 `"rename":true`。
- 命中同名时复用既有 `uniqueDest()` 退避规则，另存为 `name (2).ext`、`name (3).ext`…（与回收站恢复的改名规则一致），原文件保留；响应带 `renamed:true` 并在页面上提示「已存在同名文件，另存为 …」。

### 3. 修复：子目录里上传大文件会落到「用户上传」根目录（旧版 bug）
- 旧前端 `uploadChunked()` 的会话 key 为 `rel + '/' + 文件名`，**缺少当前目录这一层前缀**，而服务端以 `用户上传` 为基准解析 —— 因此在 `用户上传/某子目录/` 页面上传 >64MB 文件，最终会落盘到 `用户上传/根`，而不是当前目录（小文件走 multipart 路径没有这个问题）。
- 修复：服务端向页面注入 `window.__UP_REL__`（当前目录相对 `用户上传` 根），前端拼出与服务端一致的目标路径。根目录下 `__UP_REL__` 为空串 → **会话 key 与旧版完全一致，不影响任何在传会话**。

## 三、测试结果（端到端，23/23 通过）

测试脚本 `/tmp/up-test.mjs`（对 127.0.0.1:8899 真实 HTTP 调用，测试目录用后即删）：

1. 小文件首次 multipart 上传 → 200 + 内容正确
2. 状态接口：同大小→`done:true`；大小不符→不跳过；不带 size→不跳过；目标不存在→不跳过
3. 小文件 `?rename=1` 重传 → `renamed:true`，原文件保留为 v1，新文件 `small (2).txt` 为 v2
4. 小文件不带 rename 重传 → 覆盖成功（`renamed:false`）
5. 大文件 67MB 分块（2 块）→ 分块 200、会话记录 2/2、合并成功且**落盘在当前子目录**、内容逐字节一致、完成后 `done:true`、会话已清理（再 complete 返回 404）
6. 大文件合并时 `rename:true` → `renamed:true`，另存 `big (2).bin` 且原 `big.bin` 保留
7. 页面注入：子目录 `__UP_REL__="zz-上传功能测试"`、根目录为空串、桌面页与移动页均含两个复选框；页面内联 JS 通过 `node --check`

服务端 `node --check` 通过；已备份原文件为 `~/Downloads/download-server.js.bak-20260912-214503`。

## 四、使用说明与注意事项

- **需刷新上传页面**才能拿到新前端（旧页面仍跑旧 JS）。
- 跳过判定只依据 **相对路径 + 文件字节数**，不计算内容哈希。
  - 若「同名 + 同大小但内容不同」（例如等长改稿、重导出的同尺寸文件），默认设置会**跳过**，需先取消勾选「同名且大小一致时跳过」再上传。
- 勾选改名保存 + 取消跳过 = 每次上传都另存一份副本；只想更新文件时，两个都按默认（跳过开、改名关）即可正常覆盖。

## 五、运维提醒（重要）

- **自 2026-09-12 起，8899 服务已由 systemd 单元 `dl-server.service` 托管**（Restart=always + 开机自启，cgroup 独立于工具会话）。此前 nohup 启动的进程仍留在瞬态工具 shell 的进程组里，会被静默杀死 —— 根因与修复详见同目录《2026-09-12-下载服务静默死亡根因与systemd托管.md》。
- **重启服务会中断正在进行的分块上传**：在传的 chunk 请求 socket 被关闭，对应文件行会显示「❌ 网络中断」，需在页面上重新选择文件。已收到的块仍在 `.upload-parts/` 中，重新选择后会**从断点继续**，不会从头传。
- 本次排查过程中重启了服务 2 次（18:41、21:45 左右），当时正有一批模型在上传，可能造成部分文件行中断。
- 当前（2026-09-12 21:47）该批次上传状态：**45 个分块会话、`.upload-parts` 占用 63GB**，目标为 `用户上传/models/Stable-diffusion/**`（已在传约 45 个模型，多数进行到 20~24/32~104 块）；`models` 目录下已完整落盘 13 个文件。
- 磁盘：`/home` 805G，已用 270G，**可用 495G**；dl-hub 共 72G，其中 `用户上传` 70G（含 63G 分块暂存）。
- 45 个文件并行上传受浏览器「同域 6 连接」限制，实际是排队串行，整体带宽被摊薄 —— 这是「上传只有 8MB/s」观感的主要原因之一，与 2026-09-10 的排查记录方向一致。

## 六、回滚方式

```bash
cp ~/Downloads/download-server.js.bak-20260912-214503 ~/Downloads/download-server.js
bash ~/Downloads/restart-dl-server.sh   # 注意：会中断在传分块
```

## 七、相关文件

- 服务端：`~/Downloads/download-server.js`（改动点：`handleUploadStatus` 跳过判定、`handleUploadComplete` 的 `rename`、`handleUpload` 的 `finalize()` 改名落盘、`upRelJs()` 注入、前端 `UPLOAD_JS`/`UPLOAD_ZONE_HTML`/`UPLOAD_CSS`）
- 备份：`~/Downloads/download-server.js.bak-20260912-214503`
- 测试脚本：`/tmp/up-test.mjs`（临时文件，重启后可能被清理）
