Raise audio upload limit to 2 GB per file.

Update validation, dropzone checks, Docker PHP ini, and docs so multi-hour recordings can be uploaded.
This commit is contained in:
ben
2026-08-12 17:42:55 +02:00
parent 771ee8db5a
commit e81a27cb8f
8 changed files with 63 additions and 7 deletions
+2
View File
@@ -36,6 +36,8 @@ RUN install-php-extensions \
intl \
opcache
COPY docker/php.ini /usr/local/etc/php/conf.d/99-uploads.ini
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
+1 -1
View File
@@ -6,7 +6,7 @@ Built with Laravel 13, Blade, Alpine.js, Tailwind CSS 4, [Laravel Reverb](https:
## Features
- Upload common audio formats (MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, AIFF — up to 100 MB)
- Upload common audio formats (MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, AIFF — up to 2 GB)
- Automatic metadata extraction when tags are present (title, artist, album, duration, recorded date)
- Search recordings by title, artist, or transcript
- Queued local transcription (faster-whisper in Docker)
+7 -2
View File
@@ -30,6 +30,11 @@ class StoreRecordingRequest extends FormRequest
'aif',
];
/**
* Maximum upload size accepted by validation (2 GiB).
*/
public const MAX_AUDIO_KILOBYTES = 2 * 1024 * 1024;
public function authorize(): bool
{
return true;
@@ -54,7 +59,7 @@ class StoreRecordingRequest extends FormRequest
'audio' => ['required', 'array', 'min:1', 'max:50'],
'audio.*' => [
'required',
File::types(self::AUDIO_EXTENSIONS)->max(102400),
File::types(self::AUDIO_EXTENSIONS)->max('2gb'),
],
'title' => ['nullable', 'string', 'max:255'],
];
@@ -71,7 +76,7 @@ class StoreRecordingRequest extends FormRequest
'audio.max' => 'You can upload at most 50 files at once.',
'audio.*.required' => 'Please choose an audio file to upload.',
'audio.*' => 'Unsupported audio type. Use MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF.',
'audio.*.max' => 'Each audio file may not be larger than 100 MB.',
'audio.*.max' => 'Each audio file may not be larger than 2 GB.',
];
}
}
+6
View File
@@ -0,0 +1,6 @@
; PHP limits for large pocket-recorder uploads (up to 2 GB per file).
upload_max_filesize = 2G
post_max_size = 10G
memory_limit = 512M
max_execution_time = 3600
max_input_time = 3600
+9 -3
View File
@@ -3,6 +3,8 @@
* before submitting the form.
*/
const MAX_FILE_BYTES = 2 * 1024 * 1024 * 1024;
export function uploadDropzone({ existingFingerprints = [] } = {}) {
const acceptExt = ['.mp3', '.wav', '.ogg', '.oga', '.flac', '.m4a', '.mp4', '.aac', '.webm', '.wma', '.aiff', '.aif'];
const known = new Set(existingFingerprints);
@@ -50,7 +52,7 @@ export function uploadDropzone({ existingFingerprints = [] } = {}) {
continue;
}
if (file.size > 100 * 1024 * 1024) {
if (file.size > MAX_FILE_BYTES) {
skippedTooLarge++;
continue;
}
@@ -80,7 +82,7 @@ export function uploadDropzone({ existingFingerprints = [] } = {}) {
if (skippedUnsupported > 0) {
this.error = 'Skipped unsupported file type. Use common audio formats only.';
} else if (skippedTooLarge > 0) {
this.error = 'Skipped a file larger than 100 MB.';
this.error = 'Skipped a file larger than 2 GB.';
}
if (skippedDuplicates > 0) {
@@ -153,7 +155,11 @@ export function uploadDropzone({ existingFingerprints = [] } = {}) {
return (bytes / 1024).toFixed(1) + ' KB';
}
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
},
};
}
+1 -1
View File
@@ -35,7 +35,7 @@
>
<p class="text-sm font-medium text-stone-800">Drop audio files here</p>
<p class="mt-1 text-sm text-stone-600">or click to browse</p>
<p class="mt-3 text-xs text-stone-500">MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF · max 100 MB each · up to 50 files</p>
<p class="mt-3 text-xs text-stone-500">MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF · max 2 GB each · up to 50 files</p>
</div>
<input
@@ -99,4 +99,20 @@ class RecordingDuplicateUploadTest extends TestCase
$this->assertSame(1, Recording::query()->count());
Bus::assertNothingDispatched();
}
public function test_stored_recording_persists_content_hash(): void
{
Storage::fake('local');
Bus::fake();
$file = UploadedFile::fake()->createWithContent('hash-me.mp3', 'payload-for-hash');
$this->post(route('recordings.store'), [
'audio' => [$file],
])->assertRedirect();
$recording = Recording::query()->first();
$this->assertNotNull($recording);
$this->assertSame(hash('sha256', 'payload-for-hash'), $recording->content_hash);
}
}
+21
View File
@@ -2,6 +2,7 @@
namespace Tests\Feature;
use App\Http\Requests\StoreRecordingRequest;
use App\Jobs\TranscribeRecording;
use App\Models\Recording;
use App\Services\TranscriptionService;
@@ -87,9 +88,29 @@ class RecordingUploadTest extends TestCase
$this->get(route('recordings.create'))
->assertOk()
->assertSee('Drop audio files here')
->assertSee('max 2 GB each', false)
->assertSee('name="audio[]"', false);
}
public function test_files_larger_than_two_gigabytes_are_rejected(): void
{
Storage::fake('local');
Bus::fake();
$file = UploadedFile::fake()
->create('huge.mp3', 10, 'audio/mpeg')
->size(StoreRecordingRequest::MAX_AUDIO_KILOBYTES + 1);
$this->from(route('recordings.create'))
->post(route('recordings.store'), [
'audio' => [$file],
])
->assertSessionHasErrors(['audio.0']);
$this->assertSame(0, Recording::query()->count());
Bus::assertNothingDispatched();
}
public function test_user_can_upload_wav_and_ogg(): void
{
Storage::fake('local');