Files
bknd/app/src/core/utils/reqres.ts

355 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomString } from "core/utils/strings";
import type { Context } from "hono";
import { extension, guess, isMimeType } from "media/storage/mime-types-tiny";
export function headersToObject(headers: Headers): Record<string, string> {
if (!headers) return {};
return { ...Object.fromEntries(headers.entries()) };
}
export function pickHeaders(headers: Headers, keys: string[]): Record<string, string> {
const obj = headersToObject(headers);
const res = {};
for (const key of keys) {
if (obj[key]) {
res[key] = obj[key];
}
}
return res;
}
export function pickHeaders2(headers: Headers, keys: string[]): Headers {
const newHeaders = new Headers();
for (const key of keys) {
if (headers.has(key)) {
newHeaders.set(key, headers.get(key)!);
}
}
return newHeaders;
}
export const replaceUrlParam = (urlString: string, params: Record<string, string>) => {
let newString = urlString;
for (const [k, v] of Object.entries(params)) {
const reg = new RegExp(`/:${k}(?:{[^/]+})?`);
newString = newString.replace(reg, `/${v}`);
}
return newString;
};
export function encodeSearch(obj, options?: { prefix?: string; encode?: boolean }) {
let str = "";
function _encode(str) {
return options?.encode ? encodeURIComponent(str) : str;
}
for (const k in obj) {
let tmp = obj[k];
if (tmp !== void 0) {
if (Array.isArray(tmp)) {
for (let i = 0; i < tmp.length; i++) {
if (str.length > 0) str += "&";
str += `${_encode(k)}=${_encode(tmp[i])}`;
}
} else {
if (typeof tmp === "object") {
tmp = JSON.stringify(tmp);
}
if (str.length > 0) str += "&";
str += `${_encode(k)}=${_encode(tmp)}`;
}
}
}
return (options?.prefix || "") + str;
}
export function decodeSearch(str) {
function toValue(mix) {
if (!mix) return "";
const str = decodeURIComponent(mix);
if (str === "false") return false;
if (str === "true") return true;
try {
return JSON.parse(str);
} catch (e) {
return +str * 0 === 0 ? +str : str;
}
}
let tmp: any;
let k: string;
const out = {};
const arr = str.split("&");
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
while ((tmp = arr.shift())) {
tmp = tmp.split("=");
k = tmp.shift();
if (out[k] !== void 0) {
out[k] = [].concat(out[k], toValue(tmp.shift()));
} else {
out[k] = toValue(tmp.shift());
}
}
return out;
}
export function isReadableStream(value: unknown): value is ReadableStream {
return (
typeof value === "object" &&
value !== null &&
typeof (value as ReadableStream).getReader === "function"
);
}
export function isBlob(value: unknown): value is Blob {
return (
typeof value === "object" &&
value !== null &&
typeof (value as Blob).arrayBuffer === "function" &&
typeof (value as Blob).type === "string"
);
}
export function isFile(value: unknown): value is File {
return (
isBlob(value) &&
typeof (value as File).name === "string" &&
typeof (value as File).lastModified === "number"
);
}
export function isArrayBuffer(value: unknown): value is ArrayBuffer {
return (
typeof value === "object" &&
value !== null &&
Object.prototype.toString.call(value) === "[object ArrayBuffer]"
);
}
export function isArrayBufferView(value: unknown): value is ArrayBufferView {
return typeof value === "object" && value !== null && ArrayBuffer.isView(value);
}
export function getContentName(request: Request): string | undefined;
export function getContentName(contentDisposition: string): string | undefined;
export function getContentName(headers: Headers): string | undefined;
export function getContentName(ctx: Headers | Request | string): string | undefined {
let c: string = "";
if (typeof ctx === "string") {
c = ctx;
} else if (ctx instanceof Headers) {
c = ctx.get("Content-Disposition") || "";
} else if (ctx instanceof Request) {
c = ctx.headers.get("Content-Disposition") || "";
}
const match = c.match(/filename\*?=(?:UTF-8'')?("?)([^";]+)\1/);
return match ? match[2] : undefined;
}
const FILE_SIGNATURES: Record<string, string> = {
"89504E47": "image/png",
FFD8FF: "image/jpeg",
"47494638": "image/gif",
"49492A00": "image/tiff", // Little Endian TIFF
"4D4D002A": "image/tiff", // Big Endian TIFF
"52494646????57454250": "image/webp", // WEBP (RIFF....WEBP)
"504B0304": "application/zip",
"25504446": "application/pdf",
"00000020667479706D70": "video/mp4",
"000001BA": "video/mpeg",
"000001B3": "video/mpeg",
"1A45DFA3": "video/webm",
"4F676753": "audio/ogg",
"494433": "audio/mpeg", // MP3 with ID3 header
FFF1: "audio/aac",
FFF9: "audio/aac",
"52494646????41564920": "audio/wav",
"52494646????57415645": "audio/wave",
"52494646????415550": "audio/aiff",
};
async function detectMimeType(
input: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | File | null,
): Promise<string | undefined> {
if (!input) return;
let buffer: Uint8Array;
if (isReadableStream(input)) {
const reader = input.getReader();
const { value } = await reader.read();
if (!value) return;
buffer = new Uint8Array(value);
} else if (isBlob(input) || isFile(input)) {
buffer = new Uint8Array(await input.slice(0, 12).arrayBuffer());
} else if (isArrayBuffer(input)) {
buffer = new Uint8Array(input);
} else if (isArrayBufferView(input)) {
buffer = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
} else if (typeof input === "string") {
buffer = new TextEncoder().encode(input);
} else {
return;
}
const hex = Array.from(buffer.slice(0, 12))
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
.join("");
for (const [signature, mime] of Object.entries(FILE_SIGNATURES)) {
const regex = new RegExp("^" + signature.replace(/\?\?/g, ".."));
if (regex.test(hex)) return mime;
}
return;
}
export async function blobToFile(
blob: Blob | File | unknown,
overrides: FilePropertyBag & { name?: string } = {},
): Promise<File> {
if (isFile(blob)) return blob;
if (!isBlob(blob)) throw new Error("Not a Blob");
const type = isMimeType(overrides.type, ["application/octet-stream"])
? overrides.type
: await detectMimeType(blob);
const ext = type ? extension(type) : "";
const name = overrides.name || [randomString(16), ext].filter(Boolean).join(".");
return new File([blob], name, {
type: type || guess(name),
lastModified: Date.now(),
});
}
export async function getFileFromContext(c: Context<any>): Promise<File> {
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
if (
contentType?.startsWith("multipart/form-data") ||
contentType?.startsWith("application/x-www-form-urlencoded")
) {
try {
const f = await c.req.formData();
if ([...f.values()].length > 0) {
const v = [...f.values()][0];
return await blobToFile(v);
}
} catch (e) {
console.warn("Error parsing form data", e);
}
} else {
try {
const blob = await c.req.blob();
if (isFile(blob)) {
return blob;
} else if (isBlob(blob)) {
return await blobToFile(blob, { name: getContentName(c.req.raw), type: contentType });
}
} catch (e) {
console.warn("Error parsing blob", e);
}
}
throw new Error("No file found in request");
}
export async function getBodyFromContext(c: Context<any>): Promise<ReadableStream | File> {
const contentType = c.req.header("Content-Type") ?? "application/octet-stream";
if (
!contentType?.startsWith("multipart/form-data") &&
!contentType?.startsWith("application/x-www-form-urlencoded")
) {
const body = c.req.raw.body;
if (body) {
return body;
}
}
return getFileFromContext(c);
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
// biome-ignore lint/suspicious/noConstEnum: <explanation>
export const enum HttpStatus {
// Informational responses (100199)
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
// Successful responses (200299)
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
ALREADY_REPORTED = 208,
IM_USED = 226,
// Redirection messages (300399)
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
// Client error responses (400499)
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
PAYLOAD_TOO_LARGE = 413,
URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
IM_A_TEAPOT = 418,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
TOO_EARLY = 425,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
// Server error responses (500599)
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
VARIANT_ALSO_NEGOTIATES = 506,
INSUFFICIENT_STORAGE = 507,
LOOP_DETECTED = 508,
NOT_EXTENDED = 510,
NETWORK_AUTHENTICATION_REQUIRED = 511,
}