fix auth freezing requests, fix ext extraction ?, fix upload not taking sanatized name, add file query and thumbnails

This commit is contained in:
creations 2025-03-12 12:54:25 -04:00
parent 6a55f9f5a9
commit b24b1484cb
Signed by: creations
GPG key ID: 8F553AA4320FC711
6 changed files with 264 additions and 19 deletions

View file

@ -39,7 +39,7 @@ export async function createTable(reservation?: ReservedSQL): Promise<void> {
// * Validation functions
// ? should support non english characters but wont mess up the url
// ? should support non english characters but won't mess up the url
export const userNameRestrictions: {
length: { min: number; max: number };
regex: RegExp;

View file

@ -8,11 +8,6 @@ export async function authByToken(
): Promise<ApiUserSession | null> {
let selfReservation: boolean = false;
if (!reservation) {
reservation = await sql.reserve();
selfReservation = true;
}
const authorizationHeader: string | null =
request.headers.get("Authorization");
@ -22,6 +17,11 @@ export async function authByToken(
const authorizationToken: string = authorizationHeader.slice(7).trim();
if (!authorizationToken || !isUUID(authorizationToken)) return null;
if (!reservation) {
reservation = await sql.reserve();
selfReservation = true;
}
try {
const result: User[] =
await reservation`SELECT * FROM users WHERE authorization_token = ${authorizationToken};`;

View file

@ -109,16 +109,72 @@ export function getBaseUrl(request: Request): string {
return `${protocol}://${url.hostname}${portSegment}`;
}
// * File Specific Helpers
const knownExtensions: Set<string> = new Set([
"mp4",
"txt",
"pdf",
"gz",
"tar",
"zip",
"7z",
"rar",
"png",
"jpg",
"jpeg",
"webp",
"gif",
"js",
"ts",
"tsx",
"json",
"html",
"css",
"md",
"log",
"db",
"db3",
"sqlite",
"csv",
"xml",
"yaml",
"yml",
"toml",
"ini",
"cfg",
"conf",
"env",
"sh",
"bash",
"zsh",
"fish",
"ps1",
"bat",
"cmd",
"py",
"pyc",
"pyo",
"pyd",
"pyw",
"pyz",
"pyzw",
]);
export function getExtension(fileName: string): string | null {
return fileName.split(".").length > 1 && fileName.split(".").pop() !== ""
? (fileName.split(".").pop() ?? null)
: null;
const lastDotIndex: number = fileName.lastIndexOf(".");
if (lastDotIndex <= 0) return null;
const ext: string = fileName.slice(lastDotIndex + 1).toLowerCase();
return knownExtensions.has(ext) ? ext : null;
}
export function nameWithoutExtension(fileName: string): string {
const extension: string | null = getExtension(fileName);
return extension ? fileName.slice(0, -extension.length - 1) : fileName;
const lastDotIndex: number = fileName.lastIndexOf(".");
if (lastDotIndex <= 0) return fileName;
const ext: string = fileName.slice(lastDotIndex + 1).toLowerCase();
return knownExtensions.has(ext)
? fileName.slice(0, lastDotIndex)
: fileName;
}
export function supportsExif(mimeType: string, extension: string): boolean {

View file

@ -224,17 +224,20 @@ async function processFile(
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9._-]/g, "_")
.toLowerCase();
if (sanitizedFileName.length > 255)
uploadEntry.name = sanitizedFileName.substring(0, 255);
uploadEntry.name =
sanitizedFileName.length > 255
? sanitizedFileName.substring(0, 255)
: sanitizedFileName;
try {
const existingFile: FileEntry[] = await sql`
SELECT * FROM files WHERE owner = ${session.id} AND name = ${uploadEntry.name};
`;
SELECT * FROM files WHERE owner = ${session.id} AND name = ${uploadEntry.name};
`;
if (existingFile.length > 0) {
const maxBaseLength: number = 255 - 6;
uploadEntry.name = `${uploadEntry.name?.substring(0, maxBaseLength)}_${generateRandomString(5)}`;
uploadEntry.name = `${uploadEntry.name.substring(0, maxBaseLength)}_${generateRandomString(5)}`;
}
} catch (error) {
logger.error(["Error checking for existing file:", error as Error]);
@ -322,8 +325,10 @@ async function processFile(
return;
}
uploadEntry.url = `${userHeaderOptions.domain}/f/${uploadEntry.name}`;
successfulFiles.push(uploadEntry); // ? should i remove the password from the response
if (uploadEntry.password) delete uploadEntry.password;
uploadEntry.url = `${userHeaderOptions.domain}/raw/${uploadEntry.name}`;
successfulFiles.push(uploadEntry);
}
async function handler(request: ExtendedRequest): Promise<Response> {

183
src/routes/raw/[query].ts Normal file
View file

@ -0,0 +1,183 @@
import { dataType } from "@config/environment";
import { type BunFile, type ReservedSQL, sql } from "bun";
import { resolve } from "path";
import { isUUID, nameWithoutExtension } from "@/helpers/char";
import { logger } from "@/helpers/logger";
const routeDef: RouteDef = {
method: "GET",
accepts: "*/*",
returns: "*/*",
};
async function handler(request: ExtendedRequest): Promise<Response> {
const { query: file } = request.params as { query: string };
const {
password,
download: downloadFile,
json,
thumbnail,
} = request.query as {
password: string;
download: string;
json: string;
thumbnail: string;
};
const isAdmin: boolean = request.session
? request.session.roles.includes("admin")
: false;
if (!file) {
return Response.json(
{
success: false,
code: 400,
error: "No file specified",
},
{ status: 400 },
);
}
const reservation: ReservedSQL = await sql.reserve();
const rawName: string = nameWithoutExtension(file);
const isID: boolean = isUUID(rawName);
let fileData: FileEntry | null = null;
try {
let result: FileEntry[] = [];
if (isID) {
result = await reservation`
SELECT * FROM files WHERE id = ${rawName}
`;
} else {
result = await reservation`
SELECT * FROM files WHERE name = ${rawName}
`;
}
if (result.length === 0) {
return Response.json(
{
success: false,
code: 404,
error: "File not found",
},
{ status: 404 },
);
}
fileData = result[0];
} catch (error) {
logger.error(["Failed to fetch file data", error as Error]);
return Response.json(
{
success: false,
code: 500,
error: "Failed to fetch file data",
},
{ status: 500 },
);
} finally {
reservation.release();
}
if (
!isAdmin &&
fileData.owner !== request.session?.id &&
fileData.password &&
!password
) {
return Response.json(
{
success: false,
code: 403,
error: "Password required",
},
{ status: 403 },
);
}
if (
fileData.password &&
(await Bun.password.verify(password, fileData.password)) !== true
) {
return Response.json(
{
success: false,
code: 403,
error: "Invalid password",
},
{ status: 403 },
);
}
if (json === "true" || json === "1") {
delete fileData.password;
fileData.tags = fileData.tags = fileData.tags[0]?.trim()
? fileData.tags[0].split(",").filter((tag: string) => tag.trim())
: [];
return Response.json(
{
success: true,
code: 200,
file: fileData,
},
{ status: 200 },
);
}
const shouldShowThumbnail: boolean =
thumbnail === "true" || thumbnail === "1";
let path: string;
if (dataType.type === "local" && dataType.path) {
if (shouldShowThumbnail) {
path = resolve(dataType.path, "thumbnails", `${fileData.id}.jpg`);
} else {
path = resolve(
dataType.path,
`${fileData.id}${
fileData.extension ? `.${fileData.extension}` : ""
}`,
);
}
} else {
if (shouldShowThumbnail) {
path = `thumbnails/${fileData.id}.jpg`;
} else {
path = `uploads/${fileData.id}${fileData.extension ? `.${fileData.extension}` : ""}`;
}
}
try {
const bunStream: BunFile = Bun.file(path);
return new Response(bunStream, {
headers: {
"Content-Type": shouldShowThumbnail
? "image/jpeg"
: fileData.mime_type,
"Content-Disposition":
downloadFile === "true" || downloadFile === "1"
? `attachment; filename="${fileData.original_name || fileData.name}"`
: `inline; filename="${fileData.original_name || fileData.name}"`,
},
status: 200,
});
} catch (error) {
logger.error(["Failed to fetch file", error as Error]);
return Response.json(
{
success: false,
code: 500,
error: "Failed to fetch file",
},
{ status: 500 },
);
}
}
export { handler, routeDef };

View file

@ -38,6 +38,7 @@ class ServerHandler {
message: webSocketHandler.handleMessage.bind(webSocketHandler),
close: webSocketHandler.handleClose.bind(webSocketHandler),
},
maxRequestBodySize: 10 * 1024 * 1024 * 1024, // 10GB ? will be changed to env var soon
});
logger.info(