"use server"; import { prisma } from "@/lib/prisma"; import { s3 } from "@/lib/s3"; import { PutObjectCommand } from "@aws-sdk/client-s3"; import "dotenv/config"; import sharp from "sharp"; import { v4 as uuidv4 } from "uuid"; import { generateArtworkColorsForArtwork } from "../artworks/generateArtworkColors"; // Upload pipeline that generates variants and metadata, then creates artwork records. export async function createImageFromFile( imageFile: File, opts?: { originalName?: string; colorMode?: "inline" | "defer" | "off" }, ) { if (!(imageFile instanceof File)) { return null; } const fileName = opts?.originalName ?? imageFile.name; const fileType = imageFile.type; const fileSize = imageFile.size; const lastModified = new Date(imageFile.lastModified); const fileKey = uuidv4(); const arrayBuffer = await imageFile.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const realFileType = fileType.split("/")[1]; const originalKey = `original/${fileKey}.${realFileType}`; const modifiedKey = `modified/${fileKey}.webp`; const resizedKey = `resized/${fileKey}.webp`; const thumbnailKey = `thumbnail/${fileKey}.webp`; const galleryKey = `gallery/${fileKey}.webp`; const sharpData = sharp(buffer); const metadata = await sharpData.metadata(); //--- Original file await s3.send( new PutObjectCommand({ Bucket: `${process.env.BUCKET_NAME}`, Key: originalKey, Body: buffer, ContentType: "image/" + metadata.format, }), ); //--- Modified file const modifiedBuffer = await sharp(buffer).toFormat("webp").toBuffer(); const modifiedMetadata = await sharp(modifiedBuffer).metadata(); await s3.send( new PutObjectCommand({ Bucket: `${process.env.BUCKET_NAME}`, Key: modifiedKey, Body: modifiedBuffer, ContentType: "image/" + modifiedMetadata.format, }), ); //--- Resized file const { width, height } = modifiedMetadata; const targetSize = 400; let resizeOptions: { width?: number; height?: number }; if (width && height) { resizeOptions = height < width ? { height: targetSize } : { width: targetSize }; } else { resizeOptions = { height: targetSize }; } const resizedBuffer = await sharp(modifiedBuffer) .resize({ ...resizeOptions, withoutEnlargement: true }) .toFormat("webp") .toBuffer(); const resizedMetadata = await sharp(resizedBuffer).metadata(); await s3.send( new PutObjectCommand({ Bucket: `${process.env.BUCKET_NAME}`, Key: resizedKey, Body: resizedBuffer, ContentType: "image/" + resizedMetadata.format, }), ); //--- Thumbnail file const thumbnailTargetSize = 160; let thumbnailOptions: { width?: number; height?: number }; if (width && height) { thumbnailOptions = height < width ? { height: thumbnailTargetSize } : { width: thumbnailTargetSize }; } else { thumbnailOptions = { height: thumbnailTargetSize }; } const thumbnailBuffer = await sharp(modifiedBuffer) .resize({ ...thumbnailOptions, withoutEnlargement: true }) .toFormat("webp") .toBuffer(); const thumbnailMetadata = await sharp(thumbnailBuffer).metadata(); await s3.send( new PutObjectCommand({ Bucket: `${process.env.BUCKET_NAME}`, Key: thumbnailKey, Body: thumbnailBuffer, ContentType: "image/" + thumbnailMetadata.format, }), ); //--- Gallery file const galleryTargetSize = 300; let galleryOptions: { width?: number; height?: number }; if (width && height) { galleryOptions = height < width ? { height: galleryTargetSize } : { width: galleryTargetSize }; } else { galleryOptions = { height: galleryTargetSize }; } const galleryBuffer = await sharp(modifiedBuffer) .resize({ ...galleryOptions, withoutEnlargement: true }) .toFormat("webp") .toBuffer(); const galleryMetadata = await sharp(galleryBuffer).metadata(); await s3.send( new PutObjectCommand({ Bucket: `${process.env.BUCKET_NAME}`, Key: galleryKey, Body: galleryBuffer, ContentType: "image/" + galleryMetadata.format, }), ); const fileRecord = await prisma.fileData.create({ data: { name: fileName, fileKey, originalFile: fileName, uploadDate: new Date(), fileType: realFileType, fileSize, }, }); const artworkSlug = fileName.toLowerCase().replace(/\s+/g, "-"); const artworkRecord = await prisma.artwork.create({ data: { name: fileName, slug: artworkSlug, creationDate: lastModified, fileId: fileRecord.id, colorStatus: "PENDING", }, }); await prisma.artworkMetadata.create({ data: { artworkId: artworkRecord.id, format: metadata.format || "unknown", width: metadata.width || 0, height: metadata.height || 0, space: metadata.space || "unknown", channels: metadata.channels || 0, depth: metadata.depth || "unknown", density: metadata.density ?? undefined, bitsPerSample: metadata.bitsPerSample ?? undefined, isProgressive: metadata.isProgressive ?? undefined, isPalette: metadata.isPalette ?? undefined, hasProfile: metadata.hasProfile ?? undefined, hasAlpha: metadata.hasAlpha ?? undefined, autoOrientW: metadata.autoOrient?.width ?? undefined, autoOrientH: metadata.autoOrient?.height ?? undefined, }, }); await prisma.fileVariant.createMany({ data: [ { s3Key: originalKey, type: "original", height: metadata.height, width: metadata.width, fileExtension: metadata.format, mimeType: "image/" + metadata.format, sizeBytes: metadata.size, artworkId: artworkRecord.id, }, { s3Key: modifiedKey, type: "modified", height: modifiedMetadata.height, width: modifiedMetadata.width, fileExtension: modifiedMetadata.format, mimeType: "image/" + modifiedMetadata.format, sizeBytes: modifiedMetadata.size, artworkId: artworkRecord.id, }, { s3Key: resizedKey, type: "resized", height: resizedMetadata.height, width: resizedMetadata.width, fileExtension: resizedMetadata.format, mimeType: "image/" + resizedMetadata.format, sizeBytes: resizedMetadata.size, artworkId: artworkRecord.id, }, { s3Key: thumbnailKey, type: "thumbnail", height: thumbnailMetadata.height, width: thumbnailMetadata.width, fileExtension: thumbnailMetadata.format, mimeType: "image/" + thumbnailMetadata.format, sizeBytes: thumbnailMetadata.size, artworkId: artworkRecord.id, }, { s3Key: galleryKey, type: "gallery", height: galleryMetadata.height ?? 0, width: galleryMetadata.width ?? 0, fileExtension: galleryMetadata.format, mimeType: "image/" + galleryMetadata.format, sizeBytes: galleryMetadata.size, artworkId: artworkRecord.id, }, ], }); const mode = opts?.colorMode ?? "defer"; if (mode === "inline") { // blocks, but guarantees sortKey is ready immediately await generateArtworkColorsForArtwork(artworkRecord.id); } else if (mode === "defer") { // mark pending; a separate job will process these // (nothing else to do here) } return artworkRecord; }