258 lines
7.3 KiB
TypeScript
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;
|
|
}
|