feat(files): wide file manager layout and basic previews (FM-2)
- Files page uses a wide layout (route handle layout:"wide"): no max width, full-height directory tree docked next to the sidebar, resizable (200-520px, remembered, double-click resets) and collapsible; mobile keeps the tree in a drawer. - GET /files/preview (admin console session only, same path guards as download): text/NFO with encoding detection (BOM/UTF-16/UTF-8/GB18030/Big5), subtitles first 200 cues (SRT/VTT/ASS/SSA, PGS timings), .torrent file list/trackers (passkey masked)/infohash, image dimensions. Reads at most 1 MiB of text; .torrent capped at 4 MiB. - Download also allows text files, .torrent and avif/gif. - Research notes in docs/design/file-preview-research.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3add4bab41
commit
289e8b297a
20 files changed
+1294
-159
No files matched your search
@@ -1540,10 +1540,20 @@ export const Api = {
|
||||
path: "/files/download",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary: "下载小文件(nfo/字幕/图片,≤20 MiB;流式;仅管理员控制台会话;审计)",
|
||||
summary: "下载小文件(nfo/字幕/图片/文本/.torrent,≤20 MiB;流式;仅管理员控制台会话;审计)",
|
||||
query: files.FilesDownloadQuery,
|
||||
response: files.FilesDownloadResponse,
|
||||
}),
|
||||
preview: ep({
|
||||
method: "get",
|
||||
path: "/files/preview",
|
||||
scope: "admin",
|
||||
auth: "session",
|
||||
summary:
|
||||
"结构化预览(文本/NFO 前 1 MiB 带编码探测、字幕前 200 条、.torrent 文件列表、图片尺寸;仅管理员控制台会话)",
|
||||
query: files.FilesPreviewQuery,
|
||||
response: files.FilePreview,
|
||||
}),
|
||||
records: ep({
|
||||
method: "get",
|
||||
path: "/files/records",
|
||||
|
||||
@@ -728,6 +728,70 @@ export type FilesDownloadQuery = z.infer<typeof FilesDownloadQuery>;
|
||||
/** OpenAPI 描述用:实际响应为二进制流 */
|
||||
export const FilesDownloadResponse = z.string().describe("文件内容(application/octet-stream 等)");
|
||||
|
||||
// 预览(FM-2):GET /files/preview 返回结构化预览,仅管理员会话,路径守卫同下载。
|
||||
// 文本/字幕最多读前 1 MiB 并探测编码(BOM / UTF-8 / UTF-16 / GB18030 / Big5);字幕解析前 200 条;
|
||||
// .torrent ≤ 4 MiB 解析文件列表、tracker 与 infohash;图片只回尺寸类型信息,内容走 /files/download?inline=true。
|
||||
export const FilesPreviewQuery = z.object({ root: FileRootId, path: FileRelPath });
|
||||
export type FilesPreviewQuery = z.infer<typeof FilesPreviewQuery>;
|
||||
|
||||
export const SubtitleCue = z.object({
|
||||
/** 毫秒 */
|
||||
start: NonNegInt,
|
||||
end: NonNegInt,
|
||||
/** 去掉样式标签后的纯文本;图形字幕(sup)为空串 */
|
||||
text: z.string(),
|
||||
});
|
||||
export type SubtitleCue = z.infer<typeof SubtitleCue>;
|
||||
|
||||
const PreviewBase = z.object({ size: NonNegInt });
|
||||
export const FilePreview = z.discriminatedUnion("type", [
|
||||
PreviewBase.extend({
|
||||
type: z.literal("text"),
|
||||
/** 小写扩展名(nfo/txt/log/md/json/xml/yaml…) */
|
||||
format: z.string(),
|
||||
/** 探测出的编码名(utf-8 / utf-16le / utf-16be / gb18030 / big5 / windows-1252) */
|
||||
encoding: z.string(),
|
||||
text: z.string(),
|
||||
truncated: z.boolean(),
|
||||
}),
|
||||
PreviewBase.extend({
|
||||
type: z.literal("subtitle"),
|
||||
format: z.enum(["srt", "vtt", "ass", "ssa", "sup"]),
|
||||
encoding: z.string().nullable(),
|
||||
cues: z.array(SubtitleCue),
|
||||
/** 读到的范围内共解析出多少条(可能大于 cues.length) */
|
||||
parsed: NonNegInt,
|
||||
/** 只读了文件前一部分 */
|
||||
truncated: z.boolean(),
|
||||
/** ASS/SSA 的 [Script Info] Title */
|
||||
title: z.string().nullable(),
|
||||
}),
|
||||
PreviewBase.extend({
|
||||
type: z.literal("torrent"),
|
||||
name: z.string(),
|
||||
hash: z.string(),
|
||||
infohashV1: z.string().nullable(),
|
||||
infohashV2: z.string().nullable(),
|
||||
private: z.boolean(),
|
||||
totalSize: NonNegInt,
|
||||
fileCount: NonNegInt,
|
||||
/** 最多 500 个 */
|
||||
files: z.array(z.object({ path: z.string(), length: NonNegInt })),
|
||||
trackers: z.array(z.string()),
|
||||
comment: z.string().nullable(),
|
||||
createdBy: z.string().nullable(),
|
||||
createdAt: Timestamp.nullable(),
|
||||
pieceLength: NonNegInt.nullable(),
|
||||
}),
|
||||
PreviewBase.extend({
|
||||
type: z.literal("image"),
|
||||
mime: z.string(),
|
||||
width: NonNegInt.nullable(),
|
||||
height: NonNegInt.nullable(),
|
||||
}),
|
||||
]);
|
||||
export type FilePreview = z.infer<typeof FilePreview>;
|
||||
|
||||
// 整理记录回查(W5-A2,只读):整理页 `/console/organize?ids=` 预填;planIds 为 /files/move|copy|organize 的 planId
|
||||
|
||||
const csvIds = z
|
||||
|
||||
Reference in new issue
Block a user