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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user