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

@@ -101,59 +101,110 @@ router.post('/:recordingId/finalize', requireDeviceAuth, async (req, res) => {
const bucket = parsed.data.bucket;
const objectKey = parsed.data.objectKey;
await ensureMinioBucket();
try {
await minioClient.statObject(bucket, objectKey);
} catch (error) {
if (objectKey.startsWith('sim/')) {
const placeholder = Buffer.from(
JSON.stringify({
message: 'simulated recording placeholder',
await ensureMinioBucket();
console.info('[recording.finalize] checking storage object', {
recordingId: recording.id,
streamSessionId: recording.streamSessionId,
bucket,
objectKey,
durationSeconds: parsed.data.durationSeconds ?? null,
sizeBytes: parsed.data.sizeBytes ?? null,
});
try {
await minioClient.statObject(bucket, objectKey);
console.info('[recording.finalize] storage object found', {
recordingId: recording.id,
bucket,
objectKey,
});
} catch (error) {
if (objectKey.startsWith('sim/')) {
console.warn('[recording.finalize] creating simulator fallback object', {
recordingId: recording.id,
bucket,
objectKey,
error: error instanceof Error ? error.message : String(error),
});
const placeholder = Buffer.from(
JSON.stringify({
message: 'simulated recording placeholder',
recordingId: recording.id,
streamSessionId: recording.streamSessionId,
createdAt: now.toISOString(),
}),
'utf8',
);
await minioClient.putObject(bucket, objectKey, placeholder, placeholder.byteLength, {
'Content-Type': 'application/json',
});
} else if (isMissingStorageObjectError(error)) {
console.warn('[recording.finalize] storage object missing', {
recordingId: recording.id,
streamSessionId: recording.streamSessionId,
createdAt: now.toISOString(),
}),
'utf8',
);
await minioClient.putObject(bucket, objectKey, placeholder, placeholder.byteLength, {
'Content-Type': 'application/json',
});
} else if (isMissingStorageObjectError(error)) {
res.status(409).json({ message: 'Recording object does not exist in storage yet' });
return;
} else {
throw error;
bucket,
objectKey,
error: error instanceof Error ? error.message : String(error),
});
res.status(409).json({ message: 'Recording object does not exist in storage yet' });
return;
} else {
console.error('[recording.finalize] storage verification failed', {
recordingId: recording.id,
bucket,
objectKey,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
}
const [updated] = await db
.update(recordings)
.set({
objectKey,
const [updated] = await db
.update(recordings)
.set({
objectKey,
bucket,
durationSeconds: parsed.data.durationSeconds,
sizeBytes: parsed.data.sizeBytes,
status: 'ready',
availableAt: now,
updatedAt: now,
error: null,
})
.where(eq(recordings.id, recording.id))
.returning();
console.info('[recording.finalize] recording marked ready', {
recordingId: recording.id,
streamSessionId: recording.streamSessionId,
bucket,
durationSeconds: parsed.data.durationSeconds,
sizeBytes: parsed.data.sizeBytes,
status: 'ready',
availableAt: now,
updatedAt: now,
error: null,
})
.where(eq(recordings.id, recording.id))
.returning();
objectKey,
durationSeconds: parsed.data.durationSeconds ?? null,
sizeBytes: parsed.data.sizeBytes ?? null,
});
res.json({ message: 'Recording finalized', recording: updated });
res.json({ message: 'Recording finalized', recording: updated });
await writeAuditLog({
ownerUserId: recording.ownerUserId,
actorDeviceId: recording.cameraDeviceId,
action: 'recording.finalized',
targetType: 'recording',
targetId: recording.id,
metadata: { objectKey: parsed.data.objectKey, bucket: parsed.data.bucket },
ipAddress: req.ip,
});
await writeAuditLog({
ownerUserId: recording.ownerUserId,
actorDeviceId: recording.cameraDeviceId,
action: 'recording.finalized',
targetType: 'recording',
targetId: recording.id,
metadata: { objectKey: parsed.data.objectKey, bucket: parsed.data.bucket },
ipAddress: req.ip,
});
} catch (error) {
console.error('[recording.finalize] failed', {
recordingId: recording.id,
streamSessionId: recording.streamSessionId,
bucket,
objectKey,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
});
router.get('/:recordingId/download-url', requireDeviceAuth, async (req, res) => {