Store artist and album artwork in S3

This commit is contained in:
lucast committed 2026-09-29 17:03:08 +02:00
1 parent c2dcec2c31
commit 0d09198e57
6 files changed
+63 -19

No files matched your search

+7 -3
View File
@@ -3,7 +3,7 @@ import { Album } from "../../models/album"
import { Track } from "../../models/track" import { Track } from "../../models/track"
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation" 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) => { export default defineAuthenticatedEventHandler(async (event) => {
await requireAuthenticatedUser(event, true) 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() 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) { else if (dependents.length) {
setResponseStatus(event, 409) setResponseStatus(event, 409)
return { message: "Album has one or more dependents", dependentType: "Track", dependents } return { message: "Album has one or more dependents", dependentType: "Track", dependents }
} }
await Album.findByIdAndDelete(parsedId.value) await Album.findByIdAndDelete(parsedId.value)
void tryCleanUploadFileOrDirectory(event, album.file) await tryDeleteObject(event, album.file)
return {} return {}
}) })
+14 -3
View File
@@ -1,7 +1,7 @@
import { getQuery, setResponseStatus } from "h3" import { getQuery, setResponseStatus } from "h3"
import mongoose from "mongoose" import mongoose from "mongoose"
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form" 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 { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation" import { parseMongoId, validationResponse } from "../../utils/api-validation"
import { Album } from "../../models/album" import { Album } from "../../models/album"
@@ -32,10 +32,21 @@ export default defineAuthenticatedEventHandler(async (event) => {
return {} return {}
} }
if (file) { if (file) {
void tryCleanUploadFileOrDirectory(event, album.file)
const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}` 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 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 (name) album.name = name
if (artists) album.artists = artists.map((id) => new mongoose.Types.ObjectId(id)) if (artists) album.artists = artists.map((id) => new mongoose.Types.ObjectId(id))
+9 -3
View File
@@ -1,5 +1,5 @@
import { readAdminForm, adminFormText, adminFormTexts, adminFormFile, safeFileExtension, splitIds } from "../../utils/admin-form" 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 { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { Album } from "../../models/album" import { Album } from "../../models/album"
import mongoose from "mongoose" import mongoose from "mongoose"
@@ -30,8 +30,14 @@ export default defineAuthenticatedEventHandler(async (event) => {
} }
const filePath = `album/${crypto.randomUUID()}.${safeFileExtension(file.name)}` 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 album = await Album.create({ name, artists, file: filePath, genre }) let album
try {
album = await Album.create({ name, artists, file: filePath, genre })
} catch (error) {
await tryDeleteObject(event, filePath)
throw error
}
setResponseStatus(event, 201) setResponseStatus(event, 201)
return album return album
}) })
+12 -4
View File
@@ -4,7 +4,7 @@ import { Artist } from "../../models/artist"
import { Track } from "../../models/track" import { Track } from "../../models/track"
import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth" import { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation" 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) { async function findDependents(model: typeof Track | typeof Album, field: string, value: unknown) {
return model.find({ [field]: value }).select("_id name").exec() 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) 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) { else if (dependents.length) {
setResponseStatus(event, 409) setResponseStatus(event, 409)
return { message: "Artist has one or more dependents", dependentType: "Track", dependents } return { message: "Artist has one or more dependents", dependentType: "Track", dependents }
} }
dependents = await findDependents(Album, "artists", artist._id) 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) { else if (dependents.length) {
setResponseStatus(event, 409) setResponseStatus(event, 409)
return { message: "Artist has one or more dependents", dependentType: "Artist", dependents } return { message: "Artist has one or more dependents", dependentType: "Artist", dependents }
} }
await Artist.findByIdAndDelete(parsedId.value) await Artist.findByIdAndDelete(parsedId.value)
void tryCleanUploadFileOrDirectory(event, artist.file) await tryDeleteObject(event, artist.file)
return {} return {}
}) })
+12 -3
View File
@@ -1,7 +1,7 @@
import { getQuery, setResponseStatus } from "h3" import { getQuery, setResponseStatus } from "h3"
import mongoose from "mongoose" import mongoose from "mongoose"
import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form" 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 { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { parseMongoId, validationResponse } from "../../utils/api-validation" import { parseMongoId, validationResponse } from "../../utils/api-validation"
import { Artist } from "../../models/artist" import { Artist } from "../../models/artist"
@@ -24,10 +24,19 @@ export default defineAuthenticatedEventHandler(async (event) => {
return {} return {}
} }
if (file) { if (file) {
void tryCleanUploadFileOrDirectory(event, artist.file)
const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}` 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 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 if (name) artist.name = name
await artist.save() await artist.save()
+9 -3
View File
@@ -1,5 +1,5 @@
import { readAdminForm, adminFormText, adminFormFile, safeFileExtension } from "../../utils/admin-form" 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 { defineAuthenticatedEventHandler, requireAuthenticatedUser } from "../../utils/auth"
import { Artist } from "../../models/artist" import { Artist } from "../../models/artist"
@@ -18,8 +18,14 @@ export default defineAuthenticatedEventHandler(async (event) => {
} }
const filePath = `artists/${crypto.randomUUID()}.${safeFileExtension(file.name)}` 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 artist = await Artist.create({ name, file: filePath }) let artist
try {
artist = await Artist.create({ name, file: filePath })
} catch (error) {
await tryDeleteObject(event, filePath)
throw error
}
setResponseStatus(event, 201) setResponseStatus(event, 201)
return artist return artist
}) })