后端:附件读取/下载端点 + 删除端点 + addAttachments 落库去重 (tsk_tttdU3VLCy50)
- store.addAttachments:按 path 去重(later-wins),消除重传产生的悬空元数据 - store.removeAttachment:按磁盘文件名(basename(path))移除元数据,返回 removed - GET /api/tasks/:id/attachments/:name:inline 预览,?download=1 触发下载(RFC5987 文件名) - DELETE /api/tasks/:id/attachments/:name:先删元数据再尽力删磁盘文件 - 只服务元数据登记过的附件 + resolve/startsWith 前缀校验,杜绝目录穿越 - 新增 test/attachments.test.ts(去重/读取/下载/删除/穿越/丢失/端到端上传去重) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -16,6 +16,7 @@ import { computeCost, addUsage, EMPTY_USAGE, type UsageTokens } from '../model/p
|
||||
import {
|
||||
type TaskStatus, type GateKind, canTransition, initialNextStatus, gateOf, STATUS_LABEL,
|
||||
} from '../model/status.js';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
const now = (): string => new Date().toISOString();
|
||||
|
||||
@@ -419,12 +420,28 @@ export class Store {
|
||||
const t = this.getTaskRow(taskId);
|
||||
if (!t) throw new StoreError(`任务不存在: ${taskId}`);
|
||||
const cur: Attachment[] = t.attachments ? JSON.parse(t.attachments) as Attachment[] : [];
|
||||
const next = [...cur, ...items];
|
||||
// 按 path 去重(later-wins):path 唯一对应一份磁盘文件,重传同名文件只保留最新一条元数据。
|
||||
const byPath = new Map<string, Attachment>();
|
||||
for (const a of [...cur, ...items]) byPath.set(a.path, a);
|
||||
const next = [...byPath.values()];
|
||||
this.db.prepare(`UPDATE tasks SET attachments = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(next), now(), taskId);
|
||||
this.emit(t.project_id, taskId, 'task.updated', { field: 'attachments', count: next.length });
|
||||
return next;
|
||||
}
|
||||
|
||||
/** 按磁盘文件名(basename(path))移除一条附件元数据。返回剩余列表与被移除项(不存在则 removed=null)。 */
|
||||
removeAttachment(taskId: string, name: string): { attachments: Attachment[]; removed: Attachment | null } {
|
||||
const t = this.getTaskRow(taskId);
|
||||
if (!t) throw new StoreError(`任务不存在: ${taskId}`);
|
||||
const cur: Attachment[] = t.attachments ? JSON.parse(t.attachments) as Attachment[] : [];
|
||||
const removed = cur.find((a) => basename(a.path) === name) ?? null;
|
||||
if (!removed) return { attachments: cur, removed: null };
|
||||
const next = cur.filter((a) => a !== removed);
|
||||
this.db.prepare(`UPDATE tasks SET attachments = ?, updated_at = ? WHERE id = ?`).run(JSON.stringify(next), now(), taskId);
|
||||
this.emit(t.project_id, taskId, 'task.updated', { field: 'attachments', count: next.length });
|
||||
return { attachments: next, removed };
|
||||
}
|
||||
|
||||
private getTaskRow(taskId: string): TaskRow | undefined {
|
||||
return this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId) as TaskRow | undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user