Add timelapse upload and display to image edit page
This commit is contained in:
@ -9,6 +9,7 @@ export async function deleteArtwork(artworkId: string) {
|
||||
where: { id: artworkId },
|
||||
include: {
|
||||
variants: true,
|
||||
timelapse: true,
|
||||
colors: true,
|
||||
metadata: true,
|
||||
tags: true,
|
||||
@ -32,6 +33,20 @@ export async function deleteArtwork(artworkId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete timelapse S3 object (if present)
|
||||
if (artwork.timelapse?.s3Key) {
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: artwork.timelapse.s3Key,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to delete timelapse S3 object: ${artwork.timelapse.s3Key}. ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Delete join entries
|
||||
await prisma.artworkColor.deleteMany({ where: { artworkId } });
|
||||
|
||||
@ -48,6 +63,9 @@ export async function deleteArtwork(artworkId: string) {
|
||||
// Delete variants
|
||||
await prisma.fileVariant.deleteMany({ where: { artworkId } });
|
||||
|
||||
// Delete timelapse DB row (relation also cascades, but be explicit)
|
||||
await prisma.artworkTimelapse.deleteMany({ where: { artworkId } });
|
||||
|
||||
// Delete metadata
|
||||
await prisma.artworkMetadata.deleteMany({ where: { artworkId } });
|
||||
|
||||
|
||||
@ -16,7 +16,8 @@ export async function getSingleArtwork(id: string) {
|
||||
colors: { include: { color: true } },
|
||||
// sortContexts: true,
|
||||
tags: true,
|
||||
variants: true
|
||||
variants: true,
|
||||
timelapse: true
|
||||
}
|
||||
})
|
||||
}
|
||||
136
src/actions/artworks/timelapse.ts
Normal file
136
src/actions/artworks/timelapse.ts
Normal file
@ -0,0 +1,136 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { s3 } from "@/lib/s3";
|
||||
import { PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod/v4";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const createUploadSchema = z.object({
|
||||
artworkId: z.string().min(1),
|
||||
fileName: z.string().min(1),
|
||||
mimeType: z.string().min(1),
|
||||
sizeBytes: z.number().int().positive(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a presigned PUT url so the client can upload large timelapse videos directly to S3
|
||||
* (avoids Next.js body-size/proxy limits).
|
||||
*/
|
||||
export async function createArtworkTimelapseUpload(input: z.infer<typeof createUploadSchema>) {
|
||||
const { artworkId, fileName, mimeType, sizeBytes } = createUploadSchema.parse(input);
|
||||
|
||||
const ext = fileName.includes(".") ? fileName.split(".").pop() : undefined;
|
||||
const suffix = ext ? `.${ext}` : "";
|
||||
|
||||
// Keep previous uploads unique so caching/CDNs won't bite you.
|
||||
const fileId = uuidv4();
|
||||
const s3Key = `timelapse/${artworkId}/${fileId}${suffix}`;
|
||||
|
||||
const cmd = new PutObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: s3Key,
|
||||
ContentType: mimeType,
|
||||
// If you want size enforcement at S3 level, you'd do that via policy; presigned PUT doesn't strictly enforce.
|
||||
});
|
||||
|
||||
const uploadUrl = await getSignedUrl(s3, cmd, { expiresIn: 60 * 5 });
|
||||
|
||||
return { uploadUrl, s3Key, fileName, mimeType, sizeBytes };
|
||||
}
|
||||
|
||||
const confirmSchema = z.object({
|
||||
artworkId: z.string().min(1),
|
||||
s3Key: z.string().min(1),
|
||||
fileName: z.string().min(1),
|
||||
mimeType: z.string().min(1),
|
||||
sizeBytes: z.number().int().positive(),
|
||||
});
|
||||
|
||||
/** Persist uploaded timelapse metadata in DB (upsert by artworkId). */
|
||||
export async function confirmArtworkTimelapseUpload(input: z.infer<typeof confirmSchema>) {
|
||||
const { artworkId, s3Key, fileName, mimeType, sizeBytes } = confirmSchema.parse(input);
|
||||
|
||||
// If an old timelapse exists, delete the old object so you don't leak storage.
|
||||
const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } });
|
||||
if (existing?.s3Key && existing.s3Key !== s3Key) {
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: existing.s3Key,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
// don't fail the request if cleanup fails
|
||||
console.warn("Failed to delete previous timelapse object", existing.s3Key, err);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.artworkTimelapse.upsert({
|
||||
where: { artworkId },
|
||||
create: {
|
||||
artworkId,
|
||||
s3Key,
|
||||
fileName,
|
||||
mimeType,
|
||||
sizeBytes,
|
||||
enabled: true,
|
||||
},
|
||||
update: {
|
||||
s3Key,
|
||||
fileName,
|
||||
mimeType,
|
||||
sizeBytes,
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath(`/artworks/${artworkId}`);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const enabledSchema = z.object({
|
||||
artworkId: z.string().min(1),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
|
||||
export async function setArtworkTimelapseEnabled(input: z.infer<typeof enabledSchema>) {
|
||||
const { artworkId, enabled } = enabledSchema.parse(input);
|
||||
|
||||
await prisma.artworkTimelapse.update({
|
||||
where: { artworkId },
|
||||
data: { enabled },
|
||||
});
|
||||
|
||||
revalidatePath(`/artworks/${artworkId}`);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const deleteSchema = z.object({
|
||||
artworkId: z.string().min(1),
|
||||
});
|
||||
|
||||
export async function deleteArtworkTimelapse(input: z.infer<typeof deleteSchema>) {
|
||||
const { artworkId } = deleteSchema.parse(input);
|
||||
|
||||
const existing = await prisma.artworkTimelapse.findUnique({ where: { artworkId } });
|
||||
if (!existing) return { ok: true };
|
||||
|
||||
try {
|
||||
await s3.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: `${process.env.BUCKET_NAME}`,
|
||||
Key: existing.s3Key,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("Failed to delete timelapse object", existing.s3Key, err);
|
||||
}
|
||||
|
||||
await prisma.artworkTimelapse.delete({ where: { artworkId } });
|
||||
|
||||
revalidatePath(`/artworks/${artworkId}`);
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user