Reject empty recording downloads and store WebM with correct MIME.
Deploy Ladill Meet / deploy (push) Successful in 31s

Mark legacy zero-byte placeholders as failed, validate files on show/download, and surface upload errors in the meeting room.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 17:59:18 +00:00
co-authored by Cursor
parent 827d36ddc1
commit ceb9ca192e
7 changed files with 108 additions and 9 deletions
@@ -40,6 +40,7 @@ class RecordingController extends Controller
$this->authorizeAbility($request, 'meetings.view');
$this->authorizeOwner($request, $recording);
$recording->load(['session.room', 'session.aiSummaries', 'session.participants']);
$recording = $this->recordings->ensureStorageValid($recording);
$canManage = app(\App\Services\Meet\MeetPermissions::class)
->can($this->member($request), 'meetings.manage');
@@ -51,11 +52,23 @@ class RecordingController extends Controller
{
$this->authorizeAbility($request, 'meetings.view');
$this->authorizeOwner($request, $recording);
abort_unless($recording->isReady() && $recording->storage_path, 404);
$recording = $this->recordings->ensureStorageValid($recording);
abort_unless($recording->hasPlayableFile(), 404, 'Recording file is not available.');
abort_unless(app(\App\Services\Meet\MeetBillingService::class)->storageIsAccessible($recording), 402, 'Recording storage billing is overdue. Top up your Ladill wallet to download this recording.');
return Storage::disk(config('meet.recordings.disk', 'local'))
->download($recording->storage_path, $recording->session->room->title.'.'.pathinfo($recording->storage_path, PATHINFO_EXTENSION));
$disk = Storage::disk(config('meet.recordings.disk', 'local'));
$extension = strtolower(pathinfo($recording->storage_path, PATHINFO_EXTENSION) ?: 'webm');
$mime = match ($extension) {
'webm' => 'video/webm',
'mp4' => 'video/mp4',
'mkv' => 'video/x-matroska',
default => 'application/octet-stream',
};
$filename = \Illuminate\Support\Str::slug($recording->session->room->title).'.'.$extension;
return response()->streamDownload(function () use ($disk, $recording) {
echo $disk->get($recording->storage_path);
}, $filename, ['Content-Type' => $mime]);
}
public function destroy(Request $request, Recording $recording): RedirectResponse
+7
View File
@@ -55,4 +55,11 @@ class Recording extends Model
{
return $this->status === 'ready';
}
public function hasPlayableFile(): bool
{
return $this->isReady()
&& filled($this->storage_path)
&& (int) ($this->file_size ?? 0) > 0;
}
}
+32
View File
@@ -121,6 +121,38 @@ class RecordingService
return $recording->fresh();
}
/** Mark ready recordings with missing/empty files as failed. */
public function ensureStorageValid(Recording $recording): Recording
{
if ($recording->status !== 'ready' || ! $recording->storage_path) {
return $recording;
}
$disk = Storage::disk(config('meet.recordings.disk', 'local'));
$size = $disk->exists($recording->storage_path) ? (int) $disk->size($recording->storage_path) : 0;
if ($size > 0) {
if ((int) ($recording->file_size ?? 0) !== $size) {
$recording->update(['file_size' => $size]);
}
return $recording->fresh();
}
if ($recording->storage_path) {
$disk->delete($recording->storage_path);
}
$recording->update([
'status' => 'failed',
'storage_path' => null,
'file_size' => null,
'failure_reason' => 'Recording file is missing or empty. Please record the meeting again.',
]);
return $recording->fresh();
}
public function delete(Recording $recording, string $actorRef): void
{
if ($recording->storage_path) {