Fix uploads over 4 GB failing with a misleading size error
Livewire's temporary upload rule was hard-coded to max:4194304 (4 GB), so /livewire/upload-file rejected any larger file no matter how high PHP_UPLOAD_MAX_FILESIZE or the admin's max file size were set. _uploadErrored() then replaced Livewire's actual message with "file exceeds the maximum size of N MB", quoting the admin limit the file was under. The cap is removed: PHP's upload_max_filesize is the hard limit and the admin setting is still enforced in updatedFiles() and createShare(). A rejected upload now logs the real validation errors and tells the user the server could not accept the file. max_upload_time is configurable via LIVEWIRE_MAX_UPLOAD_TIME, and the README documents every limit large uploads depend on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017XYnWFt9pJEwvAmNFN38XD
This commit is contained in:
co-authored by
Claude Opus 5
parent
992f3da084
commit
126c0a5cdb
@@ -86,6 +86,19 @@ Migrations run automatically on startup. Open your configured domain — the Set
|
||||
| `caddy_data` | `/data` | TLS certificates |
|
||||
| `caddy_config` | `/config` | Caddy configuration |
|
||||
|
||||
**Large files:**
|
||||
|
||||
Uploads beyond the defaults need these limits raised together:
|
||||
|
||||
| Limit | Where | Default |
|
||||
|-------|-------|---------|
|
||||
| `PHP_UPLOAD_MAX_FILESIZE` / `PHP_POST_MAX_SIZE` | Environment | `4G` — hard cap per file / per upload batch |
|
||||
| Max file size / Max size per share | Admin → Settings | 100 MB / 2 GB |
|
||||
| `LIVEWIRE_MAX_UPLOAD_TIME` | Environment | 30 minutes per upload |
|
||||
| `OCTANE_MAX_EXECUTION_TIME` / `PHP_MAX_EXECUTION_TIME` | Environment | 300 seconds — encrypting a large file takes a while |
|
||||
|
||||
Behind a reverse proxy, raise its request body limit and read timeout as well (nginx: `client_max_body_size`, `proxy_read_timeout`).
|
||||
|
||||
### Manual (without Docker)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Services\ShareService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
@@ -36,30 +37,29 @@ class FileUploader extends Component
|
||||
$this->expiration = Setting::get('default_expiration', '7d') ?: '7d';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an upload the temporary upload endpoint did not accept.
|
||||
*
|
||||
* Validation errors (a 422) mean the whole file reached the server and was
|
||||
* rejected there, so the real reason is logged for the administrator rather
|
||||
* than guessed at in front of the user. Anything else is a transport failure.
|
||||
*/
|
||||
public function _uploadErrored($name, $errorsInJson, $isMultiple): void
|
||||
{
|
||||
$this->dispatch('upload:errored', name: $name)->self();
|
||||
|
||||
$maxFileSize = (int) Setting::get('max_file_size', 100 * 1024 * 1024);
|
||||
$maxFileSizeMb = (int) ($maxFileSize / (1024 * 1024));
|
||||
$errors = is_null($errorsInJson) ? null : (json_decode($errorsInJson, true)['errors'] ?? null);
|
||||
|
||||
if (! is_null($errorsInJson)) {
|
||||
$errors = json_decode($errorsInJson, true)['errors'] ?? null;
|
||||
if ($errors) {
|
||||
Log::warning('File upload rejected by the temporary upload endpoint.', ['errors' => $errors]);
|
||||
|
||||
if ($errors) {
|
||||
$messages = [];
|
||||
foreach ($errors as $messages_array) {
|
||||
foreach ((array) $messages_array as $msg) {
|
||||
$messages[] = $msg;
|
||||
}
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'files' => __('Upload failed: file exceeds the maximum size of :max MB.', ['max' => $maxFileSizeMb]),
|
||||
]);
|
||||
}
|
||||
throw ValidationException::withMessages([
|
||||
'files' => __('Upload failed: the server could not accept the file. Please try again or contact the administrator.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$maxFileSizeMb = (int) ((int) Setting::get('max_file_size', 100 * 1024 * 1024) / (1024 * 1024));
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'files' => __('Upload failed: file may be too large (max :max MB) or the connection was interrupted.', ['max' => $maxFileSizeMb]),
|
||||
]);
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ return [
|
||||
|
||||
'temporary_file_upload' => [
|
||||
'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default'
|
||||
'rules' => ['required', 'file', 'max:4194304'], // 4GB — fine-grained limits enforced per-component
|
||||
'rules' => ['required', 'file'], // No size cap: PHP's upload_max_filesize is the hard limit, the admin limit is enforced per-component
|
||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
@@ -138,7 +138,7 @@ return [
|
||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
],
|
||||
'max_upload_time' => 30, // Max duration (in minutes) before an upload is invalidated...
|
||||
'max_upload_time' => (int) env('LIVEWIRE_MAX_UPLOAD_TIME', 30), // Max duration (in minutes) before an upload is invalidated...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
],
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ services:
|
||||
# PHP_MAX_EXECUTION_TIME: "300" # Upload timeout in seconds
|
||||
# PHP_MAX_INPUT_TIME: "300" # Input processing timeout
|
||||
# PHP_MEMORY_LIMIT: "512M" # PHP memory limit
|
||||
# LIVEWIRE_MAX_UPLOAD_TIME: "30" # Minutes a single upload may take (raise for large files on slow links)
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--silent", "--fail", "http://localhost/up"]
|
||||
interval: 30s
|
||||
|
||||
@@ -38,6 +38,7 @@ services:
|
||||
PHP_MAX_EXECUTION_TIME: ${PHP_MAX_EXECUTION_TIME:-300}
|
||||
PHP_MAX_INPUT_TIME: ${PHP_MAX_INPUT_TIME:-300}
|
||||
PHP_MEMORY_LIMIT: ${PHP_MEMORY_LIMIT:-512M}
|
||||
LIVEWIRE_MAX_UPLOAD_TIME: ${LIVEWIRE_MAX_UPLOAD_TIME:-30}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "--silent", "--fail", "http://localhost/up"]
|
||||
interval: 30s
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Livewire\SystemPasswordPrompt;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Share;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@@ -119,6 +120,39 @@ test('files added in multiple batches end up in the same share', function () {
|
||||
expect($share->files->pluck('original_name')->all())->toBe(['first.txt', 'second.txt']);
|
||||
});
|
||||
|
||||
test('files larger than 4 GB can be shared when within the admin file size limit', function () {
|
||||
Storage::fake('shares');
|
||||
Setting::set('max_file_size', 15000 * 1024 * 1024);
|
||||
Setting::set('max_size_per_share', 20 * 1024 * 1024 * 1024);
|
||||
|
||||
Livewire::test(FileUploader::class)
|
||||
->set('files', [UploadedFile::fake()->create('backup.dump', 6 * 1024 * 1024)])
|
||||
->assertHasNoErrors('files')
|
||||
->call('createShare')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirectContains('/share/');
|
||||
|
||||
expect(Share::query()->first()->total_size)->toBe(6 * 1024 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test('a rejected upload logs the real reason instead of blaming the file size limit', function () {
|
||||
Log::spy();
|
||||
Setting::set('max_file_size', 15000 * 1024 * 1024);
|
||||
|
||||
$errors = ['files.0' => ['The files.0 failed to upload.']];
|
||||
|
||||
$component = Livewire::test(FileUploader::class)
|
||||
->call('_uploadErrored', 'files', json_encode(['errors' => $errors]), true)
|
||||
->assertDispatched('upload:errored');
|
||||
|
||||
expect($component->errors()->first('files'))
|
||||
->toBe('Upload failed: the server could not accept the file. Please try again or contact the administrator.');
|
||||
|
||||
Log::shouldHaveReceived('warning')
|
||||
->withArgs(fn (string $message, array $context): bool => $context['errors'] === $errors)
|
||||
->once();
|
||||
});
|
||||
|
||||
test('file upload requires at least one file', function () {
|
||||
Livewire::test(FileUploader::class)
|
||||
->set('files', [])
|
||||
|
||||
Reference in New Issue
Block a user