From 0d09198e579c99593a0f591ba67ff3f4f40e039d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20T=C3=A4kker?= Date: Tue, 29 Sep 2026 17:03:08 +0200 Subject: [PATCH] Store artist and album artwork in S3 --- server/api/admin/album.delete.ts | 10 +++++++--- server/api/admin/album.patch.ts | 17 ++++++++++++++--- server/api/admin/album.post.ts | 12 +++++++++--- server/api/admin/artist.delete.ts | 16 ++++++++++++---- server/api/admin/artist.patch.ts | 15 ++++++++++++--- server/api/admin/artist.post.ts | 12 +++++++++--- 6 files changed, 63 insertions(+), 19 deletions(-) diff --git a/server/api/admin/album.delete.ts b/server/api/admin/album.delete.ts index 92e70f1..11ddbe8 100644 --- a/server/api/admin/album.delete.ts +++ b/server/api/admin/album.delete.ts @@ -3,7 +3,7 @@ import { Album } from "../../models/album" import { Track } from "../../models/track" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { parseMongoId, validationResponse } from "../../utils/api-validation" -import { tryCleanUploadFileOrDirectory } from "../../utils/upload-files" +import { tryDeleteObject, tryDeleteObjectPrefix } from "../../utils/object-storage" export default defineAuthenticatedEventHandler(async (event) => { await requireAuthenticatedUser(event, true) @@ -18,13 +18,17 @@ export default defineAuthenticatedEventHandler(async (event) => { } const dependents = await Track.find({ album: album._id }).select("_id name").exec() - if (force) await Promise.all(dependents.map((dependent) => Track.findByIdAndDelete(dependent._id))) + if (force) { + const tracks = await Track.find({ album: album._id }).select("_id fileDir").exec() + await Promise.all(tracks.map((track) => Track.findByIdAndDelete(track._id))) + await Promise.all(tracks.map((track) => tryDeleteObjectPrefix(event, `${track.fileDir}/`))) + } else if (dependents.length) { setResponseStatus(event, 409) return { message: "Album has one or more dependents", dependentType: "Track", dependents } } await Album.findByIdAndDelete(parsedId.value) - void tryCleanUploadFileOrDirectory(event, album.file) + await tryDeleteObject(event, album.file) return {} }) diff --git a/server/api/admin/album.patch.ts b/server/api/admin/album.patch.ts index 37c2256..7bbacab 100644 --- a/server/api/admin/album.patch.ts +++ b/server/api/admin/album.patch.ts @@ -1,7 +1,7 @@ import { getQuery, setResponseStatus } from "h3" import mongoose from "mongoose" import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form" -import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files" +import { putObjects, tryDeleteObject } from "../../utils/object-storage" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { parseMongoId, validationResponse } from "../../utils/api-validation" import { Album } from "../../models/album" @@ -32,10 +32,21 @@ export default defineAuthenticatedEventHandler(async (event) => { return {} } if (file) { - void tryCleanUploadFileOrDirectory(event, album.file) const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}` - await saveUploadFile(event, filePath, file.data) + await putObjects(event, [{ key: filePath, body: file.data, contentType: file.type }]) + const previousPath = album.file album.file = filePath + if (name) album.name = name + if (artists) album.artists = artists.map((id) => new mongoose.Types.ObjectId(id)) + if (genre) album.genre = new mongoose.Types.ObjectId(genre) + try { + await album.save() + } catch (error) { + await tryDeleteObject(event, filePath) + throw error + } + await tryDeleteObject(event, previousPath) + return album } if (name) album.name = name if (artists) album.artists = artists.map((id) => new mongoose.Types.ObjectId(id)) diff --git a/server/api/admin/album.post.ts b/server/api/admin/album.post.ts index 5aaa0ce..2a253b2 100644 --- a/server/api/admin/album.post.ts +++ b/server/api/admin/album.post.ts @@ -1,5 +1,5 @@ import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form" -import { saveUploadFile } from "../../utils/upload-files" +import { putObjects, tryDeleteObject } from "../../utils/object-storage" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { Album } from "../../models/album" import mongoose from "mongoose" @@ -30,8 +30,14 @@ export default defineAuthenticatedEventHandler(async (event) => { } const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}` - await saveUploadFile(event, filePath, file.data) - const album = await Album.create({ name, artists, file: filePath, genre }) + await putObjects(event, [{ key: filePath, body: file.data, contentType: file.type }]) + let album + try { + album = await Album.create({ name, artists, file: filePath, genre }) + } catch (error) { + await tryDeleteObject(event, filePath) + throw error + } setResponseStatus(event, 201) return album }) diff --git a/server/api/admin/artist.delete.ts b/server/api/admin/artist.delete.ts index 152da68..5472933 100644 --- a/server/api/admin/artist.delete.ts +++ b/server/api/admin/artist.delete.ts @@ -4,7 +4,7 @@ import { Artist } from "../../models/artist" import { Track } from "../../models/track" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { parseMongoId, validationResponse } from "../../utils/api-validation" -import { tryCleanUploadFileOrDirectory } from "../../utils/upload-files" +import { tryDeleteObject, tryDeleteObjectPrefix } from "../../utils/object-storage" async function findDependents(model: typeof Track | typeof Album, field: string, value: unknown) { return model.find({ [field]: value }).select("_id name").exec() @@ -23,20 +23,28 @@ export default defineAuthenticatedEventHandler(async (event) => { } let dependents = await findDependents(Track, "artists", artist._id) - if (force) await Promise.all(dependents.map((dependent) => Track.findByIdAndDelete(dependent._id))) + if (force) { + const tracks = await Track.find({ artists: artist._id }).select("_id fileDir").exec() + await Promise.all(tracks.map((track) => Track.findByIdAndDelete(track._id))) + await Promise.all(tracks.map((track) => tryDeleteObjectPrefix(event, `${track.fileDir}/`))) + } else if (dependents.length) { setResponseStatus(event, 409) return { message: "Artist has one or more dependents", dependentType: "Track", dependents } } dependents = await findDependents(Album, "artists", artist._id) - if (force) await Promise.all(dependents.map((dependent) => Album.findByIdAndDelete(dependent._id))) + if (force) { + const albums = await Album.find({ artists: artist._id }).select("_id file").exec() + await Promise.all(albums.map((album) => Album.findByIdAndDelete(album._id))) + await Promise.all(albums.map((album) => tryDeleteObject(event, album.file))) + } else if (dependents.length) { setResponseStatus(event, 409) return { message: "Artist has one or more dependents", dependentType: "Artist", dependents } } await Artist.findByIdAndDelete(parsedId.value) - void tryCleanUploadFileOrDirectory(event, artist.file) + await tryDeleteObject(event, artist.file) return {} }) diff --git a/server/api/admin/artist.patch.ts b/server/api/admin/artist.patch.ts index 0532e8e..d5c9b6c 100644 --- a/server/api/admin/artist.patch.ts +++ b/server/api/admin/artist.patch.ts @@ -1,7 +1,7 @@ import { getQuery, setResponseStatus } from "h3" import mongoose from "mongoose" import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form" -import { saveUploadFile, tryCleanUploadFileOrDirectory } from "../../utils/upload-files" +import { putObjects, tryDeleteObject } from "../../utils/object-storage" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { parseMongoId, validationResponse } from "../../utils/api-validation" import { Artist } from "../../models/artist" @@ -24,10 +24,19 @@ export default defineAuthenticatedEventHandler(async (event) => { return {} } if (file) { - void tryCleanUploadFileOrDirectory(event, artist.file) const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}` - await saveUploadFile(event, filePath, file.data) + await putObjects(event, [{ key: filePath, body: file.data, contentType: file.type }]) + const previousPath = artist.file artist.file = filePath + if (name) artist.name = name + try { + await artist.save() + } catch (error) { + await tryDeleteObject(event, filePath) + throw error + } + await tryDeleteObject(event, previousPath) + return artist } if (name) artist.name = name await artist.save() diff --git a/server/api/admin/artist.post.ts b/server/api/admin/artist.post.ts index 64fd019..1a463bf 100644 --- a/server/api/admin/artist.post.ts +++ b/server/api/admin/artist.post.ts @@ -1,5 +1,5 @@ import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form" -import { saveUploadFile } from "../../utils/upload-files" +import { putObjects, tryDeleteObject } from "../../utils/object-storage" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { Artist } from "../../models/artist" @@ -18,8 +18,14 @@ export default defineAuthenticatedEventHandler(async (event) => { } const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}` - await saveUploadFile(event, filePath, file.data) - const artist = await Artist.create({ name, file: filePath }) + await putObjects(event, [{ key: filePath, body: file.data, contentType: file.type }]) + let artist + try { + artist = await Artist.create({ name, file: filePath }) + } catch (error) { + await tryDeleteObject(event, filePath) + throw error + } setResponseStatus(event, 201) return artist })