Add image upload and edit functions
This commit is contained in:
67
src/actions/artworks/deleteArtwork.ts
Normal file
67
src/actions/artworks/deleteArtwork.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
export async function deleteArtwork(artworkId: string) {
|
||||
const artwork = await prisma.artwork.findUnique({
|
||||
where: { id: artworkId },
|
||||
include: {
|
||||
variants: true,
|
||||
colors: true,
|
||||
metadata: true,
|
||||
tags: true,
|
||||
categories: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!artwork) throw new Error("Artwork not found");
|
||||
|
||||
// Delete S3 objects
|
||||
for (const variant of artwork.variants) {
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: variant.s3Key,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("Failed to delete S3 object: " + variant.s3Key + ". " + err);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Delete join entries
|
||||
await prisma.artworkColor.deleteMany({ where: { artworkId } });
|
||||
|
||||
// Colors
|
||||
for (const color of artwork.colors) {
|
||||
const count = await prisma.artworkColor.count({
|
||||
where: { colorId: color.colorId },
|
||||
});
|
||||
if (count === 0) {
|
||||
await prisma.color.delete({ where: { id: color.colorId } });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete variants
|
||||
await prisma.fileVariant.deleteMany({ where: { artworkId } });
|
||||
|
||||
// Delete metadata
|
||||
await prisma.artworkMetadata.deleteMany({ where: { artworkId } });
|
||||
|
||||
// Clean many-to-many tag/category joins
|
||||
await prisma.artwork.update({
|
||||
where: { id: artworkId },
|
||||
data: {
|
||||
tags: { set: [] },
|
||||
categories: { set: [] },
|
||||
},
|
||||
});
|
||||
|
||||
// Finally delete the image
|
||||
await prisma.artwork.delete({ where: { id: artworkId } });
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
149
src/actions/artworks/generateArtworkColors.ts
Normal file
149
src/actions/artworks/generateArtworkColors.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
"use server"
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { VibrantSwatch } from "@/types/VibrantSwatch";
|
||||
import { getImageBufferFromS3 } from "@/utils/getImageBufferFromS3";
|
||||
import { generateColorName, rgbToHex } from "@/utils/uploadHelper";
|
||||
import { converter, parse } from "culori";
|
||||
import { Vibrant } from "node-vibrant/node";
|
||||
|
||||
const toOklab = converter("oklab");
|
||||
const A_MIN = -0.5, A_MAX = 0.5;
|
||||
const B_MIN = -0.5, B_MAX = 0.5;
|
||||
|
||||
function clamp01(x: number) {
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
|
||||
function norm(x: number, lo: number, hi: number) {
|
||||
return clamp01((x - lo) / (hi - lo));
|
||||
}
|
||||
|
||||
function hilbertIndex15(x01: number, y01: number): number {
|
||||
let x = Math.floor(clamp01(x01) * 32767);
|
||||
let y = Math.floor(clamp01(y01) * 32767);
|
||||
let index = 0;
|
||||
for (let s = 1 << 14; s > 0; s >>= 1) { // start at bit 14
|
||||
const rx = (x & s) ? 1 : 0;
|
||||
const ry = (y & s) ? 1 : 0;
|
||||
index += s * s * ((3 * rx) ^ ry);
|
||||
if (ry === 0) {
|
||||
if (rx === 1) { x = 32767 - x; y = 32767 - y; }
|
||||
const t = x; x = y; y = t;
|
||||
}
|
||||
}
|
||||
return index >>> 0;
|
||||
}
|
||||
|
||||
function centroidFromPaletteHexes(hexByType: Record<string, string | undefined>) {
|
||||
// Tweak weights as you like. Biasing toward Vibrant keeps things “readable”.
|
||||
const weights: Record<string, number> = {
|
||||
Vibrant: 0.7,
|
||||
Muted: 0.15,
|
||||
DarkVibrant: 0.07,
|
||||
DarkMuted: 0.05,
|
||||
LightVibrant: 0.02,
|
||||
LightMuted: 0.01,
|
||||
};
|
||||
|
||||
// Ensure we have at least a vibrant color to anchor on
|
||||
const fallbackHex =
|
||||
hexByType["Vibrant"] ||
|
||||
hexByType["Muted"] ||
|
||||
hexByType["DarkVibrant"] ||
|
||||
hexByType["DarkMuted"] ||
|
||||
hexByType["LightVibrant"] ||
|
||||
hexByType["LightMuted"];
|
||||
|
||||
let L = 0, A = 0, B = 0, W = 0;
|
||||
|
||||
const entries = Object.entries(weights);
|
||||
for (const [type, w] of entries) {
|
||||
const hex = hexByType[type] ?? fallbackHex;
|
||||
if (!hex || w <= 0) continue;
|
||||
const c = toOklab(parse(hex));
|
||||
if (!c) continue;
|
||||
L += c.l * w; A += c.a * w; B += c.b * w; W += w;
|
||||
}
|
||||
|
||||
if (W === 0) {
|
||||
// Should be rare; default to mid-gray
|
||||
return { l: 0.5, a: 0, b: 0 };
|
||||
}
|
||||
return { l: L / W, a: A / W, b: B / W };
|
||||
}
|
||||
|
||||
export async function generateArtworkColors(artworkId: string, fileKey: string, fileType?: string) {
|
||||
const buffer = await getImageBufferFromS3(fileKey, fileType);
|
||||
const palette = await Vibrant.from(buffer).getPalette();
|
||||
|
||||
const vibrantHexes = Object.entries(palette).map(([key, swatch]) => {
|
||||
const castSwatch = swatch as VibrantSwatch | null;
|
||||
const rgb = castSwatch?._rgb;
|
||||
const hex = castSwatch?.hex || (rgb ? rgbToHex(rgb) : undefined);
|
||||
return { type: key, hex };
|
||||
});
|
||||
|
||||
for (const { type, hex } of vibrantHexes) {
|
||||
if (!hex) continue;
|
||||
|
||||
const [r, g, b] = hex.match(/\w\w/g)!.map((h) => parseInt(h, 16));
|
||||
const name = generateColorName(hex);
|
||||
|
||||
const color = await prisma.color.upsert({
|
||||
where: { name },
|
||||
create: {
|
||||
name,
|
||||
type,
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
},
|
||||
update: {
|
||||
hex,
|
||||
red: r,
|
||||
green: g,
|
||||
blue: b,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.artworkColor.upsert({
|
||||
where: {
|
||||
artworkId_type: {
|
||||
artworkId,
|
||||
type,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
artworkId,
|
||||
colorId: color.id,
|
||||
type,
|
||||
},
|
||||
update: {
|
||||
colorId: color.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Compute OKLab centroid → Hilbert sortKey (incremental-safe)
|
||||
const hexByType: Record<string, string | undefined> = Object.fromEntries(
|
||||
vibrantHexes.map(({ type, hex }) => [type, hex])
|
||||
);
|
||||
|
||||
const { l, a, b } = centroidFromPaletteHexes(hexByType);
|
||||
const ax = norm(a, A_MIN, A_MAX);
|
||||
const bx = norm(b, B_MIN, B_MAX);
|
||||
const sortKey = hilbertIndex15(ax, bx);
|
||||
|
||||
// 3) Store on the Image (plus optional OKLab fields)
|
||||
await prisma.artwork.update({
|
||||
where: { id: artworkId },
|
||||
data: { sortKey, okLabL: l, okLabA: a, okLabB: b },
|
||||
});
|
||||
|
||||
return await prisma.artworkColor.findMany({
|
||||
where: { artworkId },
|
||||
include: { color: true },
|
||||
});
|
||||
}
|
||||
81
src/actions/artworks/updateArtwork.ts
Normal file
81
src/actions/artworks/updateArtwork.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
"use server"
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { artworkSchema } from "@/schemas/artworks/imageSchema";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export async function updateArtwork(
|
||||
values: z.infer<typeof artworkSchema>,
|
||||
id: string
|
||||
) {
|
||||
const validated = artworkSchema.safeParse(values);
|
||||
// console.log(validated)
|
||||
if (!validated.success) {
|
||||
throw new Error("Invalid image data");
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
needsWork,
|
||||
nsfw,
|
||||
published,
|
||||
setAsHeader,
|
||||
altText,
|
||||
description,
|
||||
notes,
|
||||
month,
|
||||
year,
|
||||
creationDate,
|
||||
tagIds,
|
||||
categoryIds
|
||||
} = validated.data;
|
||||
|
||||
if(setAsHeader) {
|
||||
await prisma.artwork.updateMany({
|
||||
where: { setAsHeader: true },
|
||||
data: { setAsHeader: false },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const updatedArtwork = await prisma.artwork.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
name,
|
||||
needsWork,
|
||||
nsfw,
|
||||
published,
|
||||
setAsHeader,
|
||||
altText,
|
||||
description,
|
||||
notes,
|
||||
month,
|
||||
year,
|
||||
creationDate
|
||||
}
|
||||
});
|
||||
|
||||
if (tagIds) {
|
||||
await prisma.artwork.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
tags: {
|
||||
set: tagIds.map(id => ({ id }))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (categoryIds) {
|
||||
await prisma.artwork.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
categories: {
|
||||
set: categoryIds.map(id => ({ id }))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return updatedArtwork
|
||||
}
|
||||
41
src/actions/uploads/createBulkImages.ts
Normal file
41
src/actions/uploads/createBulkImages.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createImageFromFile } from "./createImageFromFile";
|
||||
|
||||
type BulkResult =
|
||||
| { ok: true; artworkId: string; name: string }
|
||||
| { ok: false; name: string; error: string };
|
||||
|
||||
export async function createImagesBulk(formData: FormData): Promise<BulkResult[]> {
|
||||
const entries = formData.getAll("file");
|
||||
const files = entries.filter((x): x is File => x instanceof File);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error("No files received. Ensure you send FormData with key 'file'.");
|
||||
}
|
||||
|
||||
const results: BulkResult[] = [];
|
||||
|
||||
for (const f of files) {
|
||||
try {
|
||||
if (!f.type.startsWith("image/")) {
|
||||
results.push({ ok: false, name: f.name, error: "Unsupported file type" });
|
||||
continue;
|
||||
}
|
||||
|
||||
const artwork = await createImageFromFile(f);
|
||||
if (!artwork) {
|
||||
results.push({ ok: false, name: f.name, error: "Upload failed" });
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({ ok: true, artworkId: artwork.id, name: f.name });
|
||||
} catch (err) {
|
||||
results.push({
|
||||
ok: false,
|
||||
name: f.name,
|
||||
error: err instanceof Error ? err.message : "Upload failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
203
src/actions/uploads/createImage.ts
Normal file
203
src/actions/uploads/createImage.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
"use server"
|
||||
|
||||
import { fileUploadSchema } from "@/schemas/artworks/imageSchema";
|
||||
import "dotenv/config";
|
||||
import { z } from "zod/v4";
|
||||
import { createImageFromFile } from "./createImageFromFile";
|
||||
|
||||
export async function createImage(values: z.infer<typeof fileUploadSchema>) {
|
||||
const imageFile = values.file[0];
|
||||
return createImageFromFile(imageFile);
|
||||
}
|
||||
|
||||
/*
|
||||
export async function createImage(values: z.infer<typeof fileUploadSchema>) {
|
||||
const imageFile = values.file[0];
|
||||
|
||||
if (!(imageFile instanceof File)) {
|
||||
console.log("No image or invalid type");
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileName = 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 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;
|
||||
if (width && height) {
|
||||
if (height < width) {
|
||||
resizeOptions = { height: targetSize };
|
||||
} else {
|
||||
resizeOptions = { 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;
|
||||
if (width && height) {
|
||||
if (height < width) {
|
||||
thumbnailOptions = { height: thumbnailTargetSize };
|
||||
} else {
|
||||
thumbnailOptions = { 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,
|
||||
})
|
||||
);
|
||||
|
||||
const fileRecord = await prisma.fileData.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
fileKey,
|
||||
originalFile: fileName,
|
||||
uploadDate: lastModified,
|
||||
fileType: realFileType,
|
||||
fileSize: fileSize,
|
||||
},
|
||||
});
|
||||
|
||||
const artworkSlug = fileName.toLowerCase().replace(/\s+/g, "-");
|
||||
const artworkRecord = await prisma.artwork.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
slug: artworkSlug,
|
||||
creationDate: lastModified,
|
||||
fileId: fileRecord.id,
|
||||
},
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
return artworkRecord
|
||||
}
|
||||
*/
|
||||
198
src/actions/uploads/createImageFromFile.ts
Normal file
198
src/actions/uploads/createImageFromFile.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
"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";
|
||||
|
||||
export async function createImageFromFile(imageFile: File, opts?: { originalName?: string }) {
|
||||
if (!(imageFile instanceof File)) {
|
||||
console.log("No image or invalid type");
|
||||
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 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,
|
||||
})
|
||||
);
|
||||
|
||||
const fileRecord = await prisma.fileData.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
fileKey,
|
||||
originalFile: fileName,
|
||||
uploadDate: lastModified,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return artworkRecord;
|
||||
}
|
||||
Reference in New Issue
Block a user