forked from atums.world/atums.world
fix auth freezing requests, fix ext extraction ?, fix upload not taking sanatized name, add file query and thumbnails
This commit is contained in:
parent
6a55f9f5a9
commit
b24b1484cb
6 changed files with 264 additions and 19 deletions
|
@ -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;
|
||||
|
|
|
@ -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};`;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
183
src/routes/raw/[query].ts
Normal 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 };
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue