library, playlists and playlists/[id] read locals.user!.sub with no check, so an anonymous request threw a TypeError and returned 500 instead of redirecting to login. Adds requireUser(), which redirects to /login?returnTo=... and returns a non-null SessionUser, and drops the five non-null assertions. /upload had no load at all: the page rendered for anonymous users and only failed on submit. It now redirects up front.
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
import type { Actions, PageServerLoad } from './$types';
|
|
import { fail } from '@sveltejs/kit';
|
|
import {
|
|
createApiClient,
|
|
requireAccessTokenAction,
|
|
requireUser,
|
|
unwrapOrFail
|
|
} from '$lib/server/api';
|
|
import { putPresigned, extOf, contentTypeOf, AUDIO_TYPES, IMAGE_TYPES } from '$lib/server/upload';
|
|
|
|
const MAX_TITLE = 100;
|
|
|
|
export const load: PageServerLoad = ({ locals, url }) => {
|
|
requireUser(locals, url);
|
|
};
|
|
|
|
export const actions: Actions = {
|
|
default: async ({ request, locals, fetch }) => {
|
|
const accessToken = requireAccessTokenAction(locals, 'Inicia sesión para subir música.');
|
|
if (typeof accessToken !== 'string') return accessToken;
|
|
|
|
const form = await request.formData();
|
|
const title = String(form.get('title') ?? '').trim();
|
|
const audio = form.get('audio');
|
|
const cover = form.get('cover');
|
|
|
|
if (title === '' || title.length > MAX_TITLE) {
|
|
return fail(400, { message: 'El título es obligatorio (máx. 100 caracteres).' });
|
|
}
|
|
if (!(audio instanceof File) || audio.size === 0) {
|
|
return fail(400, { message: 'Selecciona un archivo de audio.' });
|
|
}
|
|
if (!(cover instanceof File) || cover.size === 0) {
|
|
return fail(400, { message: 'Selecciona una portada.' });
|
|
}
|
|
|
|
const audioContentType = contentTypeOf(audio, AUDIO_TYPES);
|
|
const pictureContentType = contentTypeOf(cover, IMAGE_TYPES);
|
|
const api = createApiClient({ fetch, accessToken });
|
|
|
|
const urlsResult = await api.POST('/tracks/upload-urls', {
|
|
body: {
|
|
pictureFileType: extOf(cover.name) || 'jpg',
|
|
pictureContentType,
|
|
audioFileType: extOf(audio.name) || 'mp3',
|
|
audioContentType,
|
|
expectedPictureSizeBytes: cover.size,
|
|
expectedAudioSizeBytes: audio.size
|
|
}
|
|
});
|
|
|
|
const urlsFailure = unwrapOrFail(urlsResult, 'No se pudieron reservar las URLs de subida.');
|
|
if (urlsFailure) return urlsFailure;
|
|
const urls = urlsResult.data!;
|
|
|
|
try {
|
|
const [audioBuffer, coverBuffer] = await Promise.all([
|
|
audio.arrayBuffer(),
|
|
cover.arrayBuffer()
|
|
]);
|
|
await Promise.all([
|
|
putPresigned(urls.audioUploadUrl, Buffer.from(audioBuffer), urls.audioContentType),
|
|
putPresigned(urls.pictureUploadUrl, Buffer.from(coverBuffer), urls.pictureContentType)
|
|
]);
|
|
} catch (err) {
|
|
return fail(502, {
|
|
message: 'Falló la subida del archivo al almacenamiento.',
|
|
detail: err instanceof Error ? err.message : undefined
|
|
});
|
|
}
|
|
|
|
const trackResult = await api.POST('/tracks', {
|
|
body: {
|
|
title,
|
|
pictureIntentId: urls.pictureIntentId,
|
|
audioIntentId: urls.audioIntentId
|
|
}
|
|
});
|
|
|
|
const trackFailure = unwrapOrFail(trackResult, 'No se pudo crear la pista tras la subida.');
|
|
if (trackFailure) return trackFailure;
|
|
const track = trackResult.data!;
|
|
|
|
return { success: true, trackId: track.id, title: track.title };
|
|
}
|
|
};
|