fix(backend): add recording upload diagnostics
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user