feat(files): 文件管理接入 Alist 存储(浏览、增删改、移动复制、上传下载、回收站)
- 文件管理根多出 st:<存储 id>(对照 MP 存储切换),Alist 媒体库目录照常出现在媒体库组 - 列目录一次 list,refresh 只在用户刷新时带;连不上返回 503 不当成空目录 - 新建目录 / 改名(同名不覆盖)/ 删除进 Alist 上的 .umt-trash 并可还原 - 存储内移动复制走 Alist API,本地 ↔ Alist 走整理同一套执行与账本 - 新增 PUT /files/upload(同名不覆盖,Alist 经服务端转传);下载 / 预览流式转发直链 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
c2bf8a071a
commit
9d95076a0e
24 files changed
+901
-72
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { basename, extname } from "node:path";
|
||||
import type { FilesDownloadQuery } from "@umt/contracts";
|
||||
import type { Principal } from "../../http/auth.ts";
|
||||
import { remote, remoteError } from "../../storage/vfs.ts";
|
||||
import { FilesError, type FmDeps, isAdminSession, recheck, resolveIn } from "./common.ts";
|
||||
import { audit } from "./ledger.ts";
|
||||
import { kindOfName } from "./list.ts";
|
||||
@@ -55,6 +56,7 @@ export async function downloadFile(
|
||||
if (!ALLOWED.has(kind) && !pk)
|
||||
throw new FilesError("forbidden", `只能下载 NFO、字幕、图片、文本和 .torrent,此文件类型为 ${kind}`);
|
||||
const real = await recheck(r.abs, r.rootReal);
|
||||
if (r.def.storageId) return downloadRemote(d, principal, q, r.rel, real, name, kind, pk);
|
||||
const file = await Deno.open(real, { read: true });
|
||||
let size: number;
|
||||
try {
|
||||
@@ -86,3 +88,47 @@ export async function downloadFile(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Alist 文件:经服务端流式转发(不落本地盘,同一下载上限与类型限制;MP 是先下到临时目录再回传) */
|
||||
async function downloadRemote(
|
||||
d: FmDeps,
|
||||
principal: Principal | undefined,
|
||||
q: FilesDownloadQuery,
|
||||
rel: string,
|
||||
abs: string,
|
||||
name: string,
|
||||
kind: string,
|
||||
pk: ReturnType<typeof previewKindOf>,
|
||||
): Promise<Response> {
|
||||
const { v, client } = await remote(abs);
|
||||
let size: number;
|
||||
let res: Response;
|
||||
try {
|
||||
const o = await client.get(v.remote);
|
||||
if (o.isDir) throw new FilesError("bad_request", "只能下载文件,不能下载目录");
|
||||
size = o.size;
|
||||
if (size > d.options.downloadMaxBytes)
|
||||
throw new FilesError(
|
||||
"too_large",
|
||||
`文件 ${size} 字节,超过下载上限 ${d.options.downloadMaxBytes}(FILES_DOWNLOAD_MAX_BYTES)`,
|
||||
);
|
||||
res = await client.openRead(v.remote);
|
||||
} catch (e) {
|
||||
if (e instanceof FilesError) throw e;
|
||||
throw remoteError(e, abs);
|
||||
}
|
||||
const ext = extname(name).replace(/^\./, "").toLowerCase();
|
||||
const inline = q.inline && (kind === "image" || pk === "image") && ext !== "svg";
|
||||
await audit(d, principal, "download", { root: q.root, path: rel, size, inline });
|
||||
return new Response(res.body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": (ext !== "svg" && MIME[ext]) || "application/octet-stream",
|
||||
...(size ? { "content-length": String(size) } : {}),
|
||||
"content-disposition": disposition(inline ? "inline" : "attachment", name),
|
||||
"x-content-type-options": "nosniff",
|
||||
"content-security-policy": "default-src 'none'; sandbox",
|
||||
"cache-control": "private, no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user