feat: heic support

This commit is contained in:
Raul Lugo
2026-09-12 23:50:10 +02:00
parent b78d1ee094
commit 5aa35e7d18
18 changed files with 336 additions and 153 deletions
+6 -1
View File
@@ -75,6 +75,11 @@ export const de = {
success: "Foto erfolgreich hochgeladen.",
error: "Das Foto konnte nicht hochgeladen werden. Bitte versuche es erneut.",
disabled: "Foto-Uploads sind geschlossen, da die RSVP-Frist abgelaufen ist.",
requirements: "JPG, PNG oder WebP. Maximal 10 MB.",
requirements: "JPG, JPEG, PNG, WebP, HEIC oder HEIF. Maximal 10 MB.",
saved: "Aktuell hochgeladenes Foto",
selected: "Ausgewähltes Foto — noch nicht hochgeladen",
invalid: "Bitte wähle ein JPG-, JPEG-, PNG-, WebP-, HEIC- oder HEIF-Bild bis 10 MB.",
refresh: "Das gespeicherte Foto konnte nicht geladen werden. Bitte aktualisiere die Seite.",
previewUnavailable: "Dieses Format kann hier nicht angezeigt werden, wird aber nach dem Hochladen in JPEG umgewandelt.",
},
} as const
+6 -1
View File
@@ -75,6 +75,11 @@ export const en = {
success: "Photo uploaded successfully.",
error: "We could not upload the photo. Please try again.",
disabled: "Photo uploads are closed because the RSVP deadline has passed.",
requirements: "JPG, PNG, or WebP. Maximum 10 MB.",
requirements: "JPG, JPEG, PNG, WebP, HEIC, or HEIF. Maximum 10 MB.",
saved: "Currently uploaded photo",
selected: "Selected photo — not uploaded yet",
invalid: "Please choose a JPG, JPEG, PNG, WebP, HEIC, or HEIF image up to 10 MB.",
refresh: "The saved photo could not be loaded. Try refreshing the page.",
previewUnavailable: "This format can't be previewed here, but it will be converted to a JPEG once uploaded.",
},
} as const
+6 -1
View File
@@ -75,6 +75,11 @@ export const es = {
success: "Foto subida correctamente.",
error: "No pudimos subir la foto. Inténtalo de nuevo.",
disabled: "La subida de fotos está cerrada porque ya pasó la fecha límite.",
requirements: "JPG, PNG o WebP. Máximo 10 MB.",
requirements: "JPG, JPEG, PNG, WebP, HEIC o HEIF. Máximo 10 MB.",
saved: "Foto subida actualmente",
selected: "Foto seleccionada — todavía no subida",
invalid: "Elige una imagen JPG, JPEG, PNG, WebP, HEIC o HEIF de hasta 10 MB.",
refresh: "No se pudo cargar la foto guardada. Intenta actualizar la página.",
previewUnavailable: "Este formato no se puede previsualizar aquí, pero se convertirá a JPEG al subirlo.",
},
} as const
+6 -1
View File
@@ -75,6 +75,11 @@ export const pl = {
success: "Zdjęcie zostało przesłane.",
error: "Nie udało się przesłać zdjęcia. Spróbuj ponownie.",
disabled: "Przesyłanie zdjęć jest zamknięte, ponieważ termin odpowiedzi już minął.",
requirements: "JPG, PNG lub WebP. Maksymalnie 10 MB.",
requirements: "JPG, JPEG, PNG, WebP, HEIC lub HEIF. Maksymalnie 10 MB.",
saved: "Aktualnie przesłane zdjęcie",
selected: "Wybrane zdjęcie — jeszcze nieprzesłane",
invalid: "Wybierz zdjęcie JPG, JPEG, PNG, WebP, HEIC lub HEIF o rozmiarze do 10 MB.",
refresh: "Nie udało się załadować zapisanego zdjęcia. Odśwież stronę.",
previewUnavailable: "Tego formatu nie można tu wyświetlić, ale po przesłaniu zostanie przekonwertowany na JPEG.",
},
} as const
+53 -1
View File
@@ -1,4 +1,4 @@
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { nanoid } from "nanoid"
@@ -61,6 +61,58 @@ export async function createPresignedDownloadUrl(objectKey: string) {
)
}
export async function downloadUploadObject(objectKey: string, maxSizeBytes: number) {
const response = await getS3Client().send(new GetObjectCommand({
Bucket: getUploadBucket(),
Key: objectKey,
}))
const body = response.Body
if (!body) throw new Error("Uploaded object has no body")
if ("transformToByteArray" in body && typeof body.transformToByteArray === "function") {
const bytes = await body.transformToByteArray()
if (bytes.byteLength > maxSizeBytes) throw new Error("Uploaded object is too large")
return Buffer.from(bytes)
}
const chunks: Buffer[] = []
let size = 0
for await (const chunk of body as AsyncIterable<Uint8Array | string>) {
const buffer = Buffer.from(chunk)
size += buffer.length
if (size > maxSizeBytes) throw new Error("Uploaded object is too large")
chunks.push(buffer)
}
return Buffer.concat(chunks, size)
}
export async function putUploadObject({
objectKey,
contentType,
body,
}: {
objectKey: string
contentType: string
body: Buffer
}) {
await getS3Client().send(new PutObjectCommand({
Bucket: getUploadBucket(),
Key: objectKey,
Body: body,
ContentType: contentType,
ContentLength: body.length,
}))
}
export async function getUploadedObjectMetadata(objectKey: string) {
const response = await getS3Client().send(new HeadObjectCommand({
Bucket: getUploadBucket(),
Key: objectKey,
}))
return { contentType: response.ContentType, sizeBytes: response.ContentLength }
}
export async function deleteUploadObject(objectKey: string) {
const client = getS3Client()
const bucket = getUploadBucket()
+26 -1
View File
@@ -1,8 +1,33 @@
import { z } from "zod"
export const allowedImageContentTypes = ["image/jpeg", "image/png", "image/webp"] as const
export const allowedImageContentTypes = [
"image/jpeg",
"image/png",
"image/webp",
"image/heic",
"image/heif",
] as const
export const allowedImageExtensions = [".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif"] as const
export const maxUploadSizeBytes = 10 * 1024 * 1024
export type AllowedImageContentType = (typeof allowedImageContentTypes)[number]
export function getUploadContentType(file: File): AllowedImageContentType | null {
const type = file.type.toLowerCase()
if (type === "image/jpg") return "image/jpeg"
if (allowedImageContentTypes.includes(type as AllowedImageContentType)) {
return type as AllowedImageContentType
}
const extension = `.${file.name.split(".").pop()?.toLowerCase() ?? ""}`
if ([".jpg", ".jpeg"].includes(extension)) return "image/jpeg"
if (extension === ".png") return "image/png"
if (extension === ".webp") return "image/webp"
if (extension === ".heic") return "image/heic"
if (extension === ".heif") return "image/heif"
return null
}
export const presignUploadSchema = z.object({
token: z.string().min(1),
filename: z.string().trim().min(1).max(255),