import { dataType } from "@config/environment"; import { getSetting } from "@config/sql/settings"; import { password as bunPassword, randomUUIDv7, s3, sql, type SQLQuery, } from "bun"; import { exiftool } from "exiftool-vendored"; import { DateTime } from "luxon"; import { resolve } from "path"; import { generateRandomString, getBaseUrl, getExtension, getNewTimeUTC, nameWithoutExtension, supportsExif, } from "@/helpers/char"; import { logger } from "@/helpers/logger"; const routeDef: RouteDef = { method: "POST", accepts: ["multipart/form-data", "text/plain", "application/json"], returns: "application/json", }; async function removeExifData( fileBuffer: ArrayBuffer, extension: string, ): Promise { const tempInputPath: string = resolve( "temp", `${generateRandomString(5)}.${extension}`, ); try { await Bun.write(tempInputPath, fileBuffer, { createPath: true }); const tagsToRemove: Record = { GPSAltitude: null, GPSAltitudeRef: null, GPSAreaInformation: null, GPSCoordinates: null, GPSDateStamp: null, GPSDestBearing: null, GPSDateTime: null, GPSDestBearingRef: null, GPSDestDistance: null, GPSDestDistanceRef: null, GPSDestLatitude: null, GPSDestLatitudeRef: null, GPSDestLongitude: null, GPSDestLongitudeRef: null, GPSDifferential: null, GPSDOP: null, GPSHPositioningError: null, GPSImgDirection: null, GPSImgDirectionRef: null, GPSLatitude: null, GPSLatitudeRef: null, GPSLongitude: null, GPSLongitudeRef: null, GPSMapDatum: null, GPSMeasureMode: null, GPSPosition: null, GPSProcessingMethod: null, GPSSatellites: null, GPSSpeed: null, GPSSpeedRef: null, GPSStatus: null, GPSTimeStamp: null, GPSTrack: null, GPSTrackRef: null, GPSValid: null, GPSVersionID: null, GeolocationBearing: null, GeolocationCity: null, GeolocationCountry: null, GeolocationCountryCode: null, GeolocationDistance: null, GeolocationFeatureCode: null, GeolocationFeatureType: null, GeolocationPopulation: null, GeolocationPosition: null, GeolocationRegion: null, GeolocationSubregion: null, GeolocationTimeZone: null, GeolocationWarning: null, Location: null, LocationAccuracyHorizontal: null, LocationAreaCode: null, LocationInfoVersion: null, LocationName: null, }; await exiftool.write(tempInputPath, tagsToRemove, [ "-overwrite_original", ]); const modifiedBuffer: ArrayBuffer = await Bun.file(tempInputPath).arrayBuffer(); return modifiedBuffer; } catch (error) { logger.error(["Error modifying EXIF data:", error as Error]); return fileBuffer; } finally { try { await Bun.file(tempInputPath).unlink(); } catch (cleanupError) { logger.error([ "Error cleaning up temp EXIF data file:", cleanupError as Error, ]); } } } async function processFile( file: File, key: string, failedFiles: { reason: string; file: string }[], successfulFiles: FileUpload[], request: ExtendedRequest, ): Promise { const session: UserSession | ApiUserSession = request.session as | UserSession | ApiUserSession; const { max_views: user_provided_max_views, password: user_provided_password, expires: delete_short_string, tags: user_provided_tags, format: name_format = "original", // Supports original,date,random,uuid, folder: folder_identifier, favorite: user_wants_favorite, } = request.query as { max_views: string; password: string; expires: string; tags: string; format: string; folder: string; favorite: string; }; const userHeaderOptions: { domain: string; clearExif: string } = { domain: ((): string => { const domainsList: string[] = request.headers .get("X-Override-Domains") ?.split(",") .map((domain: string): string => domain.trim()) ?? []; return domainsList.length > 0 ? domainsList[Math.floor(Math.random() * domainsList.length)] : getBaseUrl(request); })(), clearExif: request.headers.get("X-Clear-Exif") ?? "", }; const extension: string | null = getExtension(file.name); let rawName: string | null = nameWithoutExtension(file.name); const maxViews: number | null = parseInt(user_provided_max_views, 10) || null; if (!rawName) { failedFiles.push({ reason: "Invalid file name", file: key, }); return; } let hashedPassword: string | null = null; if (user_provided_password) { try { hashedPassword = await bunPassword.hash(user_provided_password, { algorithm: "argon2id", }); } catch (error) { throw error; } } const randomUUID: string = randomUUIDv7(); const tags: string[] = Array.isArray(user_provided_tags) ? user_provided_tags : (user_provided_tags?.split(/[, ]+/).filter(Boolean) ?? []); let uploadEntry: FileUpload = { id: randomUUID as UUID, owner: session.id as UUID, name: rawName, mime_type: file.type, extension: extension, size: file.size, max_views: maxViews, password: hashedPassword, favorite: user_wants_favorite === "true" || user_wants_favorite === "1", tags: tags, expires_at: delete_short_string ? getNewTimeUTC(delete_short_string) : null, }; if (name_format === "date") { const setTimezone: string = session.timezone || (await getSetting("default_timezone")) || "UTC"; const date: DateTime = DateTime.local().setZone(setTimezone); uploadEntry.name = `${date.toFormat((await getSetting("date_format")) || "yyyy-MM-dd_HH-mm-ss")}`; } else if (name_format === "random") { uploadEntry.name = generateRandomString( Number(await getSetting("random_name_length")) || 8, ); } else if (name_format === "uuid") { uploadEntry.name = randomUUID; } else { // ? Should work not sure about non-english characters const sanitizedFileName: string = rawName .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-zA-Z0-9._-]/g, "_") .toLowerCase(); if (sanitizedFileName.length > 255) uploadEntry.name = sanitizedFileName.substring(0, 255); try { const existingFile: FileEntry[] = await sql` 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)}`; } } catch (error) { logger.error(["Error checking for existing file:", error as Error]); failedFiles.push({ reason: "Failed to check for existing file", file: key, }); return; } } if (uploadEntry.name !== rawName) uploadEntry.original_name = rawName; let fileBuffer: ArrayBuffer = await file.arrayBuffer(); if ( supportsExif(file.type, extension ?? "") && (userHeaderOptions.clearExif === "true" || userHeaderOptions.clearExif === "1") ) { fileBuffer = await removeExifData(fileBuffer, extension ?? ""); } const uuidWithExtension: string = `${uploadEntry.id}${extension ? `.${extension}` : ""}`; let path: string; if (dataType.type === "local" && dataType.path) { path = resolve(dataType.path, uuidWithExtension); try { await Bun.write(path, fileBuffer, { createPath: true }); } catch (error) { logger.error(["Error writing file to disk:", error as Error]); failedFiles.push({ reason: "Failed to write file", file: key, }); return; } } else { path = "/uploads/" + uuidWithExtension; try { await s3.write(path, fileBuffer); } catch (error) { logger.error(["Error writing file to S3:", error as Error]); failedFiles.push({ reason: "Failed to write file", file: key, }); return; } } try { const result: FileUpload[] = await sql` INSERT INTO files ( id, owner, folder, name, original_name, mime_type, extension, size, max_views, password, favorite, tags, expires_at ) VALUES ( ${uploadEntry.id}, ${uploadEntry.owner}, ${folder_identifier}, ${uploadEntry.name}, ${uploadEntry.original_name}, ${uploadEntry.mime_type}, ${uploadEntry.extension}, ${uploadEntry.size}, ${uploadEntry.max_views}, ${uploadEntry.password}, ${uploadEntry.favorite}, ARRAY[${(uploadEntry.tags ?? []).map((tag: string): SQLQuery => sql`${tag}`)}]::TEXT[], ${uploadEntry.expires_at} ) RETURNING id; `; if (result.length === 0) { failedFiles.push({ reason: "Failed to create file entry", file: key, }); return; } } catch (error) { logger.error(["Error creating file entry:", error as Error]); failedFiles.push({ reason: "Failed to create file entry", file: key, }); return; } uploadEntry.url = `${userHeaderOptions.domain}/f/${uploadEntry.name}`; successfulFiles.push(uploadEntry); // ? should i remove the password from the response } async function handler(request: ExtendedRequest): Promise { if (!request.session) { return Response.json( { success: false, code: 401, error: "Unauthorized", }, { status: 401 }, ); } let requestBody: FormData | null; if (request.actualContentType === "multipart/form-data") { try { requestBody = await request.formData(); } catch { return Response.json( { success: false, code: 400, error: "Invalid form data", }, { status: 400 }, ); } } else if ( request.actualContentType === "text/plain" || request.actualContentType === "application/json" ) { const body: string = await request.text(); requestBody = new FormData(); requestBody.append( "file", new Blob([body], { type: request.actualContentType }), request.actualContentType === "text/plain" ? "file.txt" : "file.json", ); } else { return Response.json( { success: false, code: 400, error: "Invalid content type", }, { status: 400 }, ); } const formData: FormData | null = requestBody as FormData; if (!formData || !(requestBody instanceof FormData)) { return Response.json( { success: false, code: 400, error: "Missing form data", }, { status: 400 }, ); } const failedFiles: { reason: string; file: string }[] = []; const successfulFiles: FileUpload[] = []; const files: { key: string; file: File }[] = []; formData.forEach((value: FormDataEntryValue, key: string): void => { if (value instanceof File) { files.push({ key, file: value }); } }); for (const { key, file } of files) { if (!file.type || file.type === "") { failedFiles.push({ reason: "Cannot determine file type", file: key, }); continue; } if (!file.size || file.size === 0) { failedFiles.push({ reason: "Empty file", file: key, }); continue; } if (!file.name || file.name === "") { failedFiles.push({ reason: "Missing file name", file: key, }); continue; } try { await processFile(file, key, failedFiles, successfulFiles, request); } catch (error) { logger.error(["Error processing file:", error as Error]); failedFiles.push({ reason: "Unexpected error processing file", file: key, }); } } return Response.json({ success: true, code: 200, files: successfulFiles, failed: failedFiles, }); } export { handler, routeDef };