refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -12,16 +12,16 @@ import { type AlistAuth, AlistClient } from "./client.ts";
|
||||
// UMT_ALIST_REAL_ROOT2 可选:另一个挂载(驱动)下的可写目录,测跨驱动复制 / 移动(异步任务)
|
||||
// 跑法:docs/runbook/storage-alist.md「真机验收」。
|
||||
|
||||
const URL_ = Deno.env.get("UMT_ALIST_REAL_URL");
|
||||
const ROOT = Deno.env.get("UMT_ALIST_REAL_ROOT");
|
||||
const ROOT2 = Deno.env.get("UMT_ALIST_REAL_ROOT2");
|
||||
const auth: AlistAuth | null = Deno.env.get("UMT_ALIST_REAL_TOKEN")
|
||||
? { kind: "token", token: Deno.env.get("UMT_ALIST_REAL_TOKEN")! }
|
||||
: Deno.env.get("UMT_ALIST_REAL_USER")
|
||||
const URL_ = process.env.UMT_ALIST_REAL_URL;
|
||||
const ROOT = process.env.UMT_ALIST_REAL_ROOT;
|
||||
const ROOT2 = process.env.UMT_ALIST_REAL_ROOT2;
|
||||
const auth: AlistAuth | null = process.env.UMT_ALIST_REAL_TOKEN
|
||||
? { kind: "token", token: process.env.UMT_ALIST_REAL_TOKEN! }
|
||||
: process.env.UMT_ALIST_REAL_USER
|
||||
? {
|
||||
kind: "password",
|
||||
username: Deno.env.get("UMT_ALIST_REAL_USER")!,
|
||||
password: Deno.env.get("UMT_ALIST_REAL_PASSWORD") ?? "",
|
||||
username: process.env.UMT_ALIST_REAL_USER!,
|
||||
password: process.env.UMT_ALIST_REAL_PASSWORD ?? "",
|
||||
}
|
||||
: null;
|
||||
const enabled = !!URL_ && !!ROOT && !!auth;
|
||||
|
||||
@@ -388,14 +388,17 @@ describe("上传", () => {
|
||||
|
||||
it("空闲超时:服务端不读数据时中止", async () => {
|
||||
const mock = createMockAlist({ token: "t" });
|
||||
const stuck: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
|
||||
const stuck: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
|
||||
input: Request | URL | string,
|
||||
init?: RequestInit,
|
||||
) => {
|
||||
const req = input instanceof Request ? input : new Request(String(input), init);
|
||||
if (new URL(req.url).pathname === "/api/fs/put")
|
||||
return new Promise<Response>((_, reject) =>
|
||||
init?.signal?.addEventListener("abort", () => reject(init.signal!.reason)),
|
||||
);
|
||||
return mock.fetch(input, init);
|
||||
}) as typeof fetch;
|
||||
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
const client = new AlistClient({
|
||||
baseUrl: mock.base,
|
||||
auth: { kind: "token", token: "t" },
|
||||
@@ -453,14 +456,17 @@ describe("下载", () => {
|
||||
mock.put("/s1/a.mkv", bytes(1000));
|
||||
const realFetch = mock.fetch;
|
||||
let expired = false;
|
||||
const f: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
|
||||
const f: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
|
||||
input: Request | URL | string,
|
||||
init?: RequestInit,
|
||||
) => {
|
||||
const u = String(input instanceof Request ? input.url : input);
|
||||
if (u.includes("/d/") && !expired) {
|
||||
expired = true;
|
||||
mock.expireSigns();
|
||||
}
|
||||
return realFetch(input, init);
|
||||
}) as typeof fetch;
|
||||
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
const c2 = new AlistClient({ baseUrl: mock.base, auth: { kind: "token", token: "perm-token" }, fetch: f, qps: 0 });
|
||||
await c2.download("/s1/a.mkv", join(dir, "a.mkv"));
|
||||
expect((await stat(join(dir, "a.mkv"))).size).toBe(1000);
|
||||
@@ -478,7 +484,10 @@ describe("下载", () => {
|
||||
const mock = createMockAlist({ token: "t" });
|
||||
mock.put("/s1/a.mkv", "x");
|
||||
const calls: { url: string; auth: string | null }[] = [];
|
||||
const f: typeof fetch = (async (input: Request | URL | string, init?: RequestInit) => {
|
||||
const f: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = (async (
|
||||
input: Request | URL | string,
|
||||
init?: RequestInit,
|
||||
) => {
|
||||
const req = input instanceof Request ? input : new Request(String(input), init);
|
||||
calls.push({ url: req.url, auth: req.headers.get("authorization") });
|
||||
if (req.url.startsWith("https://cdn.example")) return new Response("x");
|
||||
@@ -489,7 +498,7 @@ describe("下载", () => {
|
||||
return Response.json(j);
|
||||
}
|
||||
return res;
|
||||
}) as typeof fetch;
|
||||
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
const c = new AlistClient({ baseUrl: mock.base, auth: { kind: "token", token: "t" }, fetch: f, qps: 0 });
|
||||
await c.download("/s1/a.mkv", join(dir, "a.mkv"));
|
||||
expect(calls.find((x) => x.url.startsWith("https://cdn.example"))!.auth).toBeNull();
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface AlistClientOptions {
|
||||
auth: AlistAuth;
|
||||
/** 路径密码(Alist「元信息」里给目录设的密码),list / get 时带上 */
|
||||
pathPassword?: string | null;
|
||||
fetch?: typeof fetch;
|
||||
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
/** 普通请求超时(ms),缺省 30 s */
|
||||
timeoutMs?: number;
|
||||
/** 数据流无进度空闲超时(ms),缺省 120 s */
|
||||
@@ -203,7 +203,7 @@ function idleWatch(ms: number, outer?: AbortSignal) {
|
||||
|
||||
export class AlistClient {
|
||||
readonly baseUrl: string;
|
||||
private readonly fetchFn: typeof fetch;
|
||||
private readonly fetchFn: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
private readonly limiter: AlistLimiter;
|
||||
private readonly o: Required<
|
||||
Pick<
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// 令牌无效 code 401;copy 一律异步任务;同挂载 move 立即完成、跨挂载 move 是任务;put 覆盖同名;/d 支持 Range 与 sign。
|
||||
// 顶层目录视为「挂载」(驱动),用于区分同驱动 / 跨驱动。
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { type NetAddr, serve } from "@umt/runtime";
|
||||
|
||||
export interface MockFile {
|
||||
data: Uint8Array;
|
||||
@@ -43,7 +44,7 @@ export interface MockAlist {
|
||||
readonly base: string;
|
||||
setOrigin(origin: string): void;
|
||||
handler: (req: Request) => Promise<Response>;
|
||||
fetch: typeof fetch;
|
||||
fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
files: Map<string, MockFile>;
|
||||
dirs: Map<string, number>;
|
||||
tasks: Map<string, Task>;
|
||||
@@ -432,7 +433,7 @@ export function createMockAlist(opts: MockAlistOptions = {}): MockAlist {
|
||||
// 老版本(noTaskIds):没有任务 id 的复制 / 移动,在之后的任意 get 上推进
|
||||
if (opts.noTaskIds) for (const t of tasks.values()) advanceTask(t);
|
||||
return handler(req);
|
||||
}) as typeof fetch,
|
||||
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
|
||||
files,
|
||||
dirs,
|
||||
tasks,
|
||||
@@ -471,13 +472,13 @@ export function createMockAlist(opts: MockAlistOptions = {}): MockAlist {
|
||||
|
||||
/** 起一个真 HTTP 监听(ffprobe / 下载代理等需要真实 URL 的测试用);返回 base 与关闭函数 */
|
||||
export function serveMockAlist(mock: MockAlist): { base: string; close: () => Promise<void> } {
|
||||
const server = Deno.serve({ port: 0, hostname: "127.0.0.1", onListen: () => {} }, (req) => {
|
||||
const server = serve({ port: 0, hostname: "127.0.0.1", onListen: () => {} }, (req) => {
|
||||
const u = new URL(req.url);
|
||||
return mock
|
||||
.handler(new Request(`${mock.base}${u.pathname}${u.search}`, req))
|
||||
.catch(() => new Response("connection reset", { status: 502 }));
|
||||
});
|
||||
const addr = server.addr as Deno.NetAddr;
|
||||
const addr = server.addr as NetAddr;
|
||||
const prev = mock.base;
|
||||
const base = `http://127.0.0.1:${addr.port}`;
|
||||
mock.setOrigin(base);
|
||||
|
||||
Reference in new issue
Block a user