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,4 +1,3 @@
|
||||
import { lstat, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type {
|
||||
FileRootId,
|
||||
@@ -8,6 +7,7 @@ import type {
|
||||
OrganizationStatus,
|
||||
} from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import { isRemotePath, vlstat as lstat, vreaddirents as readdirents } from "../../storage/vfs.ts";
|
||||
import { FilesError, type FmDeps, isTempName, loadRootDefs, resolveIn, within } from "./common.ts";
|
||||
import { kindOfName } from "./list.ts";
|
||||
|
||||
@@ -20,7 +20,8 @@ const hiddenName = (n: string) => n.startsWith(".") || isTempName(n);
|
||||
export async function treeOf(d: FmDeps, q: FilesTreeQuery): Promise<FilesTreeResult> {
|
||||
const r = await resolveIn(d, q.root, q.path);
|
||||
if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录:${r.rel || "/"}`);
|
||||
const names = await readdir(r.abs, { withFileTypes: true });
|
||||
const remote = isRemotePath(r.abs);
|
||||
const names = await readdirents(r.abs);
|
||||
const subdirs = names
|
||||
.filter((n) => n.isDirectory() && !hiddenName(n.name))
|
||||
.map((n) => n.name)
|
||||
@@ -30,7 +31,9 @@ export async function treeOf(d: FmDeps, q: FilesTreeQuery): Promise<FilesTreeRes
|
||||
const dirs = await Promise.all(
|
||||
subdirs.slice(0, max).map(async (name) => {
|
||||
const path = r.rel ? `${r.rel}/${name}` : name;
|
||||
const kids = await readdir(join(r.abs, name), { withFileTypes: true }).catch(() => []);
|
||||
// Alist:不逐个子目录再列一遍(避免 N 次网盘请求),按「可能有下级」展示,展开时再列
|
||||
if (remote) return { name, path, hasChildren: true };
|
||||
const kids = await readdirents(join(r.abs, name)).catch(() => []);
|
||||
return { name, path, hasChildren: kids.some((k) => k.isDirectory() && !hiddenName(k.name)) };
|
||||
}),
|
||||
);
|
||||
@@ -39,10 +42,15 @@ export async function treeOf(d: FmDeps, q: FilesTreeQuery): Promise<FilesTreeRes
|
||||
let seen = 0;
|
||||
let videoCount = 0;
|
||||
let partial = false;
|
||||
const stack = [r.abs];
|
||||
// Alist:只数当前目录这一层(不递归打网盘接口),结果标为不完整
|
||||
if (remote) {
|
||||
videoCount = names.filter((n) => n.isFile() && kindOfName(n.name) === "video").length;
|
||||
partial = names.some((n) => n.isDirectory());
|
||||
}
|
||||
const stack = remote ? [] : [r.abs];
|
||||
while (stack.length > 0 && !partial) {
|
||||
const dir = stack.pop()!;
|
||||
for (const n of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
||||
for (const n of await readdirents(dir).catch(() => [])) {
|
||||
if (++seen > d.options.dirSummaryMax) {
|
||||
partial = true;
|
||||
break;
|
||||
|
||||
Reference in new issue
Block a user