feat(recordings): enhance recording management with improved error handling, finalize recording logic, and add motion notification support
This commit is contained in:
@@ -6,7 +6,7 @@ import { db } from '../db/client';
|
||||
import { recordings, streamSessions } from '../db/schema';
|
||||
import { requireDeviceAuth } from '../middleware/device-auth';
|
||||
import { writeAuditLog } from '../services/audit';
|
||||
import { minioBucket, minioClient, minioPresignedExpirySeconds } from '../utils/minio';
|
||||
import { ensureMinioBucket, minioBucket, minioClient, minioPresignedExpirySeconds } from '../utils/minio';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -26,6 +26,15 @@ const recordingParamSchema = z.object({
|
||||
recordingId: z.string().uuid(),
|
||||
});
|
||||
|
||||
const isMissingStorageObjectError = (error: unknown): boolean => {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const code = 'code' in error ? String((error as { code?: unknown }).code) : '';
|
||||
return code === 'NoSuchKey' || code === 'NotFound';
|
||||
};
|
||||
|
||||
router.get('/me/list', requireDeviceAuth, async (req, res) => {
|
||||
const parsed = listSchema.safeParse(req.query);
|
||||
|
||||
@@ -89,12 +98,41 @@ router.post('/:recordingId/finalize', requireDeviceAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
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',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(recordings)
|
||||
.set({
|
||||
objectKey: parsed.data.objectKey,
|
||||
bucket: parsed.data.bucket,
|
||||
objectKey,
|
||||
bucket,
|
||||
durationSeconds: parsed.data.durationSeconds,
|
||||
sizeBytes: parsed.data.sizeBytes,
|
||||
status: 'ready',
|
||||
@@ -154,6 +192,17 @@ router.get('/:recordingId/download-url', requireDeviceAuth, async (req, res) =>
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await minioClient.statObject(recording.bucket, recording.objectKey);
|
||||
} catch (error) {
|
||||
if (isMissingStorageObjectError(error)) {
|
||||
res.status(409).json({ message: 'Recording file is missing from storage' });
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const downloadUrl = await minioClient.presignedGetObject(
|
||||
recording.bucket,
|
||||
recording.objectKey,
|
||||
|
||||
Reference in New Issue
Block a user