mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-06 05:38:44 +08:00
Add new route /api/file/[id]
This commit is contained in:
7
db.ts
7
db.ts
@@ -540,6 +540,13 @@ export class EhDb {
|
||||
if (!this.#dblock) return;
|
||||
eval(`Deno.funlockSync(${this.#dblock.rid});`);
|
||||
}
|
||||
get_file(id: number) {
|
||||
const d = this.convert_file(this.db.queryEntries<EhFileRaw>(
|
||||
"SELECT * FROM file WHERE id = ?;",
|
||||
[id],
|
||||
));
|
||||
return d.length ? d[0] : null;
|
||||
}
|
||||
get_files(token: string) {
|
||||
return this.convert_file(this.db.queryEntries<EhFileRaw>(
|
||||
"SELECT * FROM file WHERE token = ?;",
|
||||
|
||||
2
deno.lock
generated
2
deno.lock
generated
@@ -208,6 +208,7 @@
|
||||
"https://esm.sh/[email protected]": "07fb48e0e08189de5f370ab8a1fa9d4dfd1cdb31a1843fba7e6f30d1c28575cd",
|
||||
"https://esm.sh/[email protected]/isEqual": "d94a1bba01b5a2061d42ee5c7d27cce9e4c42781b724c48fb9aeb58240272a1b",
|
||||
"https://esm.sh/[email protected]": "f6a40767720d9f1a65e2b9731ec129296b2e974cb6bdb33f0776818dbf777ac1",
|
||||
"https://esm.sh/[email protected]": "db399b5a45370e2ca58ca7f8484c5fced635ff50d31e805653afd2ad0952317c",
|
||||
"https://esm.sh/[email protected]/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8",
|
||||
"https://esm.sh/[email protected]/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c",
|
||||
"https://esm.sh/[email protected]/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb",
|
||||
@@ -449,6 +450,7 @@
|
||||
"https://esm.sh/v125/[email protected]/dist/types/token.d.ts": "160ef96d5065bc36cd99ce8584f2612ca9fe28ac72d7bf09a37768919d4def18",
|
||||
"https://esm.sh/v125/[email protected]/dist/types/types/index.d.ts": "a0e396a938a5e86b0a838c1f17311c703ff20e21095983a7523549fdb6a2a48f",
|
||||
"https://esm.sh/v125/[email protected]/dist/types/types/types.d.ts": "eae72c3f246fb8aa28089798ca8570d1877c0480d3db311f3cc458ddc0786978",
|
||||
"https://esm.sh/v125/[email protected]/denonext/mime.mjs": "7fe83d762ec6d1bac50893acc45af4ff52d5ad0176959c4bc48ae0cde77ab50f",
|
||||
"https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/deps.ts": "b7248e5b750be62613a9417f407e65ed43726d83b11f9631d6dbb58634bbd7d1",
|
||||
"https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/mod.ts": "3e507379372361162f93325a216b86f6098defb5bb60144555b507bca26d061f",
|
||||
"https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/src/deno.ts": "71bee6b14e72ca193c0686d8b4f1f47d639a64745b6f5c7576f7a3616f436f57",
|
||||
|
||||
14
fresh.gen.ts
14
fresh.gen.ts
@@ -8,9 +8,10 @@ import * as $1 from "./routes/api/config.ts";
|
||||
import * as $2 from "./routes/api/deploy_id.ts";
|
||||
import * as $3 from "./routes/api/exit.ts";
|
||||
import * as $4 from "./routes/api/export/gallery/zip/[gid].ts";
|
||||
import * as $5 from "./routes/api/gallery/[gid].ts";
|
||||
import * as $6 from "./routes/api/task.ts";
|
||||
import * as $7 from "./routes/index.tsx";
|
||||
import * as $5 from "./routes/api/file/[id].ts";
|
||||
import * as $6 from "./routes/api/gallery/[gid].ts";
|
||||
import * as $7 from "./routes/api/task.ts";
|
||||
import * as $8 from "./routes/index.tsx";
|
||||
import * as $$0 from "./islands/Container.tsx";
|
||||
import * as $$1 from "./islands/Settings.tsx";
|
||||
import * as $$2 from "./islands/TaskManager.tsx";
|
||||
@@ -22,9 +23,10 @@ const manifest = {
|
||||
"./routes/api/deploy_id.ts": $2,
|
||||
"./routes/api/exit.ts": $3,
|
||||
"./routes/api/export/gallery/zip/[gid].ts": $4,
|
||||
"./routes/api/gallery/[gid].ts": $5,
|
||||
"./routes/api/task.ts": $6,
|
||||
"./routes/index.tsx": $7,
|
||||
"./routes/api/file/[id].ts": $5,
|
||||
"./routes/api/gallery/[gid].ts": $6,
|
||||
"./routes/api/task.ts": $7,
|
||||
"./routes/index.tsx": $8,
|
||||
},
|
||||
islands: {
|
||||
"./islands/Container.tsx": $$0,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"esbuild/": "https://deno.land/x/[email protected]/",
|
||||
"lifegpc-md5": "https://esm.sh/[email protected]",
|
||||
"meilisearch": "https://esm.sh/[email protected]",
|
||||
"lodash/": "https://esm.sh/[email protected]/"
|
||||
"lodash/": "https://esm.sh/[email protected]/",
|
||||
"mime": "https://esm.sh/[email protected]"
|
||||
}
|
||||
}
|
||||
|
||||
24
routes/api/file/[id].ts
Normal file
24
routes/api/file/[id].ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { get_task_manager } from "../../../server.ts";
|
||||
import {
|
||||
get_file_response,
|
||||
GetFileResponseOptions,
|
||||
} from "../../../server/get_file_response.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, ctx) {
|
||||
const id = parseInt(ctx.params.id);
|
||||
if (isNaN(id)) {
|
||||
return new Response("Bad Request", { status: 400 });
|
||||
}
|
||||
const m = get_task_manager();
|
||||
const f = m.db.get_file(id);
|
||||
if (!f) {
|
||||
return new Response("File not found.", { status: 404 });
|
||||
}
|
||||
const opts: GetFileResponseOptions = {};
|
||||
const range = req.headers.get("range");
|
||||
if (range) opts.range = range;
|
||||
return await get_file_response(f.path, opts);
|
||||
},
|
||||
};
|
||||
241
server/get_file_response.ts
Normal file
241
server/get_file_response.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { exists } from "std/fs/exists.ts";
|
||||
import mime from "mime";
|
||||
import { parse_range } from "./range_parser.ts";
|
||||
|
||||
export type GetFileResponseOptions = {
|
||||
boundary?: string;
|
||||
/**@default {4096} */
|
||||
chunk?: number;
|
||||
/**@default {false} */
|
||||
combineRange?: boolean;
|
||||
mimetype?: string;
|
||||
range?: string;
|
||||
};
|
||||
|
||||
export async function get_file_response(
|
||||
path: string,
|
||||
opts?: GetFileResponseOptions,
|
||||
) {
|
||||
if (!(await exists(path, { isFile: true, isReadable: true }))) {
|
||||
return new Response("404 not found.", { status: 404 });
|
||||
}
|
||||
const i = await Deno.stat(path);
|
||||
const mimetype = opts && opts.mimetype ? opts.mimetype : mime.getType(path);
|
||||
const f = await Deno.open(path);
|
||||
const close_f = () => {
|
||||
try {
|
||||
f.close();
|
||||
} catch (_) {
|
||||
null;
|
||||
}
|
||||
};
|
||||
const chunk = opts && opts.chunk && opts.chunk > 0 ? opts.chunk : 4096;
|
||||
try {
|
||||
if (opts && opts.range) {
|
||||
const combine = opts.combineRange || false;
|
||||
const ranges = parse_range(i.size, opts.range, combine);
|
||||
if (ranges === -1) {
|
||||
return new Response("Malformed header: range.", {
|
||||
status: 400,
|
||||
});
|
||||
} else if (ranges === -2 || ranges.type !== "bytes") {
|
||||
return new Response("Range Not Satisfiable", { status: 416 });
|
||||
}
|
||||
if (ranges.length === 1) {
|
||||
const start = ranges[0].start;
|
||||
const end = ranges[0].end + 1;
|
||||
let now = start;
|
||||
await f.seek(now, Deno.SeekMode.Start);
|
||||
const readInto = async (s: Uint8Array) => {
|
||||
if (now === end) return null;
|
||||
try {
|
||||
const n = await f.read(s);
|
||||
if (n === null) return null;
|
||||
const readed = Math.min(n, end - now);
|
||||
now += readed;
|
||||
return readed;
|
||||
} catch (e) {
|
||||
close_f();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const readable = new ReadableStream({
|
||||
pull: async (c) => {
|
||||
if (c.byobRequest) {
|
||||
const r = c.byobRequest;
|
||||
if (r.view) {
|
||||
const v = new Uint8Array(
|
||||
r.view.buffer,
|
||||
r.view.byteOffset,
|
||||
);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
r.respond(f);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const v = new Uint8Array(chunk);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
r.respondWithNewView(v.slice(0, f));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const v = new Uint8Array(chunk);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
c.enqueue(v.slice(0, f));
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel: close_f,
|
||||
type: "bytes",
|
||||
});
|
||||
const headers: HeadersInit = {
|
||||
"Content-Length": (end - start).toString(),
|
||||
"Content-Range": `bytes ${start}-${end - 1}/${i.size}`,
|
||||
};
|
||||
if (mimetype) headers["Content-Type"] = mimetype;
|
||||
return new Response(readable, { status: 206, headers });
|
||||
} else {
|
||||
let ri = 0;
|
||||
const boundary = opts.boundary
|
||||
? encodeURIComponent(opts.boundary)
|
||||
: new Date().getTime().toString();
|
||||
let current = ranges[ri];
|
||||
let now = current.start;
|
||||
await f.seek(now, Deno.SeekMode.Start);
|
||||
const encoder = new TextEncoder();
|
||||
const boundaries = ranges.map((r) => {
|
||||
const d = [""];
|
||||
const h = new Headers({
|
||||
"Content-Range": `bytes ${r.start}-${r.end}/${i.size}`,
|
||||
});
|
||||
if (mimetype) h.append("Content-Type", mimetype);
|
||||
d.push(`--${boundary}`);
|
||||
h.forEach((v, k) => {
|
||||
d.push(`${k}: ${v}`);
|
||||
});
|
||||
d.push("");
|
||||
d.push("");
|
||||
return encoder.encode(d.join("\r\n"));
|
||||
});
|
||||
boundaries.push(encoder.encode(`\r\n--${boundary}--`));
|
||||
const len = boundaries.reduce((p, c) => p + c.length, 0) +
|
||||
ranges.reduce((p, r) => p + r.end + 1 - r.start, 0);
|
||||
let current_boundary = boundaries[ri];
|
||||
let cb = current_boundary.length;
|
||||
const readInto = async (
|
||||
s: Uint8Array,
|
||||
): Promise<number | null> => {
|
||||
try {
|
||||
if (cb > 0) {
|
||||
const readed = Math.min(s.length, cb);
|
||||
s.set(current_boundary.slice(0, readed));
|
||||
current_boundary.set(
|
||||
current_boundary.slice(readed),
|
||||
0,
|
||||
);
|
||||
cb -= readed;
|
||||
return readed;
|
||||
}
|
||||
if (now === current.end + 1) {
|
||||
ri++;
|
||||
if (ri < ranges.length) {
|
||||
current = ranges[ri];
|
||||
now = current.start;
|
||||
current_boundary = boundaries[ri];
|
||||
cb = current_boundary.length;
|
||||
await f.seek(now, Deno.SeekMode.Start);
|
||||
return await readInto(s);
|
||||
} else {
|
||||
if (ri === ranges.length) {
|
||||
current_boundary = boundaries[ri];
|
||||
cb = current_boundary.length;
|
||||
return await readInto(s);
|
||||
} else return null;
|
||||
}
|
||||
}
|
||||
const n = await f.read(s);
|
||||
if (n === null) return 0;
|
||||
const readed = Math.min(n, current.end + 1 - now);
|
||||
now += readed;
|
||||
return readed;
|
||||
} catch (e) {
|
||||
close_f();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const readable = new ReadableStream({
|
||||
pull: async (c) => {
|
||||
if (c.byobRequest) {
|
||||
const r = c.byobRequest;
|
||||
if (r.view) {
|
||||
const v = new Uint8Array(
|
||||
r.view.buffer,
|
||||
r.view.byteOffset,
|
||||
);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
r.respond(f);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const v = new Uint8Array(chunk);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
r.respondWithNewView(v.slice(0, f));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const v = new Uint8Array(chunk);
|
||||
const f = await readInto(v);
|
||||
if (f === null) {
|
||||
c.close();
|
||||
return;
|
||||
} else if (f !== 0) {
|
||||
c.enqueue(v.slice(0, f));
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
cancel: close_f,
|
||||
type: "bytes",
|
||||
});
|
||||
const headers: HeadersInit = {
|
||||
"Content-Length": len.toString(),
|
||||
"Content-Type":
|
||||
`multipart/byteranges; boundary=${boundary}`,
|
||||
};
|
||||
return new Response(readable, { status: 206, headers });
|
||||
}
|
||||
} else {
|
||||
const headers: HeadersInit = {
|
||||
"Content-Length": i.size.toString(),
|
||||
};
|
||||
if (mimetype) headers["Content-Type"] = mimetype;
|
||||
return new Response(f.readable, { headers });
|
||||
}
|
||||
} catch (e) {
|
||||
close_f();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
66
server/range_parser.ts
Normal file
66
server/range_parser.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export type Range = {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
export type Ranges = Range[] & { type: string };
|
||||
|
||||
export function parse_range(size: number, str: string, combine = false) {
|
||||
const index = str.indexOf("=");
|
||||
if (index === -1) {
|
||||
return -2;
|
||||
}
|
||||
const arr = str.slice(index + 1).split(",");
|
||||
/**@ts-ignore */
|
||||
const ranges = [] as Ranges;
|
||||
ranges.type = str.slice(0, index);
|
||||
for (const r of arr) {
|
||||
const range = r.split("-");
|
||||
let start = parseInt(range[0], 10);
|
||||
let end = parseInt(range[1], 10);
|
||||
if (isNaN(start)) {
|
||||
start = size - end;
|
||||
end = size - 1;
|
||||
} else if (isNaN(end)) {
|
||||
end = size - 1;
|
||||
}
|
||||
if (end > size - 1) {
|
||||
end = size - 1;
|
||||
}
|
||||
if (isNaN(start) || isNaN(end) || start > end || start < 0) {
|
||||
continue;
|
||||
}
|
||||
ranges.push({
|
||||
start: start,
|
||||
end: end,
|
||||
});
|
||||
}
|
||||
if (ranges.length < 1) return -1;
|
||||
return combine ? combineRanges(ranges) : ranges;
|
||||
}
|
||||
|
||||
function combineRanges(ranges: Ranges) {
|
||||
const ordered = ranges.map((r, index) => {
|
||||
return { start: r.start, end: r.end, index };
|
||||
}).sort((a, b) => a.start - b.start);
|
||||
let j = 0;
|
||||
for (let i = 1; i < ordered.length; i++) {
|
||||
const range = ordered[i];
|
||||
const current = ordered[j];
|
||||
if (range.start > current.end + 1) {
|
||||
ordered[++j] = range;
|
||||
} else if (range.end > current.end) {
|
||||
current.end = range.end;
|
||||
current.index = Math.min(current.index, range.index);
|
||||
}
|
||||
}
|
||||
ordered.length = j + 1;
|
||||
const combined: Range[] = ordered.sort((a, b) => a.index - b.index).map(
|
||||
(d) => {
|
||||
return { start: d.start, end: d.end };
|
||||
},
|
||||
);
|
||||
/**@ts-ignore */
|
||||
const r = combined as Ranges;
|
||||
r.type = ranges.type;
|
||||
return r;
|
||||
}
|
||||
29
server/range_parser_test.ts
Normal file
29
server/range_parser_test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { assert, assertEquals } from "std/testing/asserts.ts";
|
||||
import { parse_range } from "./range_parser.ts";
|
||||
|
||||
Deno.test("parse_range_test", () => {
|
||||
let r = parse_range(200, "bytes=1-30, 50-70");
|
||||
assert(typeof r !== "number");
|
||||
assertEquals(r.type, "bytes");
|
||||
assertEquals(r.slice(0), [{ start: 1, end: 30 }, { start: 50, end: 70 }]);
|
||||
r = parse_range(100, "bytes=20-,50-");
|
||||
assert(typeof r !== "number");
|
||||
assertEquals(r.type, "bytes");
|
||||
assertEquals(r.slice(0), [{ start: 20, end: 99 }, { start: 50, end: 99 }]);
|
||||
r = parse_range(100, "bytes=-3");
|
||||
assert(typeof r !== "number");
|
||||
assertEquals(r.type, "bytes");
|
||||
assertEquals(r.slice(0), [{ start: 97, end: 99 }]);
|
||||
r = parse_range(100, "bytes=-10,1-30, 70-96");
|
||||
assert(typeof r !== "number");
|
||||
assertEquals(r.type, "bytes");
|
||||
assertEquals(r.slice(0), [
|
||||
{ start: 90, end: 99 },
|
||||
{ start: 1, end: 30 },
|
||||
{ start: 70, end: 96 },
|
||||
]);
|
||||
r = parse_range(100, "bytes=-10,1-30,70-96", true);
|
||||
assert(typeof r !== "number");
|
||||
assertEquals(r.type, "bytes");
|
||||
assertEquals(r.slice(0), [{ start: 70, end: 99 }, { start: 1, end: 30 }]);
|
||||
});
|
||||
Reference in New Issue
Block a user