Add new route /api/file/[id]

This commit is contained in:
2023-06-15 18:05:09 +08:00
parent 8b2607988d
commit 0f28eda10d
8 changed files with 379 additions and 7 deletions

7
db.ts
View File

@@ -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
View File

@@ -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",

View File

@@ -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,

View File

@@ -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
View 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
View 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
View 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;
}

View 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 }]);
});