fix(backend): add recording upload diagnostics

This commit is contained in:
2026-04-15 13:50:00 +01:00
parent 9dc202ce03
commit 8cad36deb3
2 changed files with 197 additions and 120 deletions

View File

@@ -43,7 +43,7 @@ const buildObjectKey = (userId: string, fileName: string, prefix?: string): stri
router.use(requireAuth);
router.post('/upload-url', async (req, res) => {
const parsed = uploadUrlSchema.safeParse(req.body);
const parsed = uploadUrlSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
@@ -57,94 +57,120 @@ router.post('/upload-url', async (req, res) => {
return;
}
await ensureMinioBucket();
try {
await ensureMinioBucket();
const device = await db.query.devices.findFirst({
where: and(eq(devices.id, parsed.data.deviceId), eq(devices.userId, authSession.user.id)),
columns: { id: true },
});
if (!device) {
res.status(400).json({ message: 'Invalid deviceId for this user' });
return;
}
if (parsed.data.eventId) {
const event = await db.query.events.findFirst({
where: and(eq(events.id, parsed.data.eventId), eq(events.userId, authSession.user.id)),
const device = await db.query.devices.findFirst({
where: and(eq(devices.id, parsed.data.deviceId), eq(devices.userId, authSession.user.id)),
columns: { id: true },
});
if (!event) {
res.status(400).json({ message: 'Invalid eventId for this user' });
if (!device) {
res.status(400).json({ message: 'Invalid deviceId for this user' });
return;
}
}
const objectKey = buildObjectKey(authSession.user.id, parsed.data.fileName, parsed.data.prefix);
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
const now = new Date();
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
if (parsed.data.eventId) {
const event = await db.query.events.findFirst({
where: and(eq(events.id, parsed.data.eventId), eq(events.userId, authSession.user.id)),
columns: { id: true },
});
let persistedRecording;
if (!event) {
res.status(400).json({ message: 'Invalid eventId for this user' });
return;
}
}
if (parsed.data.recordingId) {
const existingRecording = await db.query.recordings.findFirst({
where: and(eq(recordings.id, parsed.data.recordingId), eq(recordings.ownerUserId, authSession.user.id)),
const objectKey = buildObjectKey(authSession.user.id, parsed.data.fileName, parsed.data.prefix);
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
const now = new Date();
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
console.info('[recording.upload-url]', {
ownerUserId: authSession.user.id,
deviceId: parsed.data.deviceId,
recordingId: parsed.data.recordingId ?? null,
eventId: parsed.data.eventId ?? null,
objectKey,
bucket: minioBucket,
expiresInSeconds: minioPresignedExpirySeconds,
minioEndpoint: process.env.MINIO_ENDPOINT ?? 'localhost',
minioPort: Number(process.env.MINIO_PORT ?? 9000),
minioUseSSL: (process.env.MINIO_USE_SSL ?? 'false').toLowerCase() === 'true',
});
if (!existingRecording) {
res.status(404).json({ message: 'Recording not found' });
let persistedRecording;
if (parsed.data.recordingId) {
const existingRecording = await db.query.recordings.findFirst({
where: and(eq(recordings.id, parsed.data.recordingId), eq(recordings.ownerUserId, authSession.user.id)),
});
if (!existingRecording) {
res.status(404).json({ message: 'Recording not found' });
return;
}
[persistedRecording] = await db
.update(recordings)
.set({
objectKey,
bucket: minioBucket,
status: 'awaiting_upload',
updatedAt: now,
error: null,
})
.where(eq(recordings.id, existingRecording.id))
.returning();
} else {
[persistedRecording] = await db
.insert(recordings)
.values({
ownerUserId: authSession.user.id,
cameraDeviceId: parsed.data.deviceId,
requesterDeviceId: null,
eventId: parsed.data.eventId ?? null,
objectKey,
bucket: minioBucket,
status: 'awaiting_upload',
updatedAt: now,
})
.returning();
}
if (!persistedRecording) {
res.status(500).json({ message: 'Unable to persist recording metadata' });
return;
}
[persistedRecording] = await db
.update(recordings)
.set({
objectKey,
bucket: minioBucket,
status: 'awaiting_upload',
updatedAt: now,
error: null,
})
.where(eq(recordings.id, existingRecording.id))
.returning();
} else {
[persistedRecording] = await db
.insert(recordings)
.values({
ownerUserId: authSession.user.id,
cameraDeviceId: parsed.data.deviceId,
requesterDeviceId: null,
eventId: parsed.data.eventId ?? null,
objectKey,
bucket: minioBucket,
status: 'awaiting_upload',
updatedAt: now,
})
.returning();
res.status(201).json({
message: 'Upload URL generated',
bucket: minioBucket,
objectKey,
uploadUrl,
expiresInSeconds: minioPresignedExpirySeconds,
video: {
id: persistedRecording.id,
objectKey: persistedRecording.objectKey,
bucket: persistedRecording.bucket,
status: persistedRecording.status,
createdAt: persistedRecording.createdAt,
expiresAt,
},
});
} catch (error) {
console.error('[recording.upload-url] failed', {
ownerUserId: authSession.user.id,
deviceId: parsed.data.deviceId,
recordingId: parsed.data.recordingId ?? null,
eventId: parsed.data.eventId ?? null,
fileName: parsed.data.fileName,
prefix: parsed.data.prefix ?? null,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
if (!persistedRecording) {
res.status(500).json({ message: 'Unable to persist recording metadata' });
return;
}
res.status(201).json({
message: 'Upload URL generated',
bucket: minioBucket,
objectKey,
uploadUrl,
expiresInSeconds: minioPresignedExpirySeconds,
video: {
id: persistedRecording.id,
objectKey: persistedRecording.objectKey,
bucket: persistedRecording.bucket,
status: persistedRecording.status,
createdAt: persistedRecording.createdAt,
expiresAt,
},
});
});
router.get('/download-url', async (req, res) => {