Files
v2.admin.gaertan.art/src/actions/uploads/createImageFromFile.ts
2026-02-03 12:17:47 +01:00

258 lines
7.3 KiB
TypeScript

"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;
}