diff --git a/config/system.php b/config/system.php index a8777f14064..90a0c2b1615 100644 --- a/config/system.php +++ b/config/system.php @@ -286,4 +286,31 @@ // ], + /* + |-------------------------------------------------------------------------- + | File Uploads Disk + |-------------------------------------------------------------------------- + | + | Temporary file uploads are stored here before being moved to their + | final destination. You may configure this to use a shared filesystem + | in multiserver environments. This disk may be shared by other kinds + | of temporary file uploads (e.g. forms) that use their own path below. + | + */ + + 'file_uploads_disk' => env('STATAMIC_FILE_UPLOADS_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | File Uploads Path + |-------------------------------------------------------------------------- + | + | The path (on the file uploads disk above) where temporary file uploads + | from the Files fieldtype are stored before being moved to their final + | destination. These files are automatically cleaned up over time. + | + */ + + 'file_uploads_path' => env('STATAMIC_FILE_UPLOADS_PATH', 'statamic/file-uploads'), + ]; diff --git a/src/Actions/ReuploadAsset.php b/src/Actions/ReuploadAsset.php index 8f1cce20bf9..bb3f6ad3de0 100644 --- a/src/Actions/ReuploadAsset.php +++ b/src/Actions/ReuploadAsset.php @@ -56,7 +56,8 @@ public function run($assets, $values) { $asset = $assets->first(); - $file = new ReplacementFile('statamic/file-uploads/'.$values['file']); + $basePath = config('statamic.system.file_uploads_path', 'statamic/file-uploads'); + $file = new ReplacementFile($basePath.'/'.$values['file']); try { $asset->reupload($file); diff --git a/src/Assets/FileUploader.php b/src/Assets/FileUploader.php index 97461cb9d64..e37cf5f2ab7 100644 --- a/src/Assets/FileUploader.php +++ b/src/Assets/FileUploader.php @@ -30,7 +30,7 @@ protected function uploadPath(UploadedFile $file) protected function uploadPathPrefix() { - return 'statamic/file-uploads/'; + return config('statamic.system.file_uploads_path', 'statamic/file-uploads').'/'; } protected function preset() @@ -40,6 +40,6 @@ protected function preset() protected function disk() { - return Storage::disk('local'); + return Storage::disk(config('statamic.system.file_uploads_disk', 'local')); } } diff --git a/src/Assets/ReplacementFile.php b/src/Assets/ReplacementFile.php index aa6fc3804c1..93e80e17237 100644 --- a/src/Assets/ReplacementFile.php +++ b/src/Assets/ReplacementFile.php @@ -31,9 +31,8 @@ public function basename() public function writeTo(Filesystem $disk, $path) { - $disk->put( - $path, - Storage::disk('local')->readStream($this->path) - ); + $sourceDisk = Storage::disk(config('statamic.system.file_uploads_disk', 'local')); + + $disk->put($path, $sourceDisk->readStream($this->path)); } } diff --git a/src/Forms/DeleteTemporaryAttachments.php b/src/Forms/DeleteTemporaryAttachments.php index 2905f270cd0..5d53ee938bb 100644 --- a/src/Forms/DeleteTemporaryAttachments.php +++ b/src/Forms/DeleteTemporaryAttachments.php @@ -22,11 +22,14 @@ public function __construct(public Submission $submission) public function handle() { + $disk = Storage::disk(config('statamic.system.file_uploads_disk', 'local')); + $basePath = config('statamic.system.file_uploads_path', 'statamic/file-uploads'); + $this->submission->form()->blueprint()->fields()->all() ->filter(fn (Field $field) => $field->type() === 'files') - ->each(function (Field $field) { + ->each(function (Field $field) use ($disk, $basePath) { Collection::wrap($this->submission->get($field->handle(), [])) - ->each(fn ($path) => Storage::disk('local')->delete('statamic/file-uploads/'.$path)); + ->each(fn ($path) => $disk->delete($basePath.'/'.$path)); $this->submission->remove($field->handle()); }); diff --git a/src/Forms/Email.php b/src/Forms/Email.php index 013266d1d52..41adbc0566e 100644 --- a/src/Forms/Email.php +++ b/src/Forms/Email.php @@ -150,8 +150,11 @@ private function attachFiles($field) return; } + $disk = config('statamic.system.file_uploads_disk', 'local'); + $basePath = config('statamic.system.file_uploads_path', 'statamic/file-uploads'); + foreach ($value as $file) { - $this->attachFromStorageDisk('local', 'statamic/file-uploads/'.$file); + $this->attachFromStorageDisk($disk, $basePath.'/'.$file); } } diff --git a/src/Http/Middleware/DeleteTemporaryFileUploads.php b/src/Http/Middleware/DeleteTemporaryFileUploads.php index 50e18b07347..ef13ab19634 100644 --- a/src/Http/Middleware/DeleteTemporaryFileUploads.php +++ b/src/Http/Middleware/DeleteTemporaryFileUploads.php @@ -20,10 +20,11 @@ public function handle($request, Closure $next) private function deleteFilesOverAnHourOld() { - $disk = File::disk('local'); + $disk = File::disk(config('statamic.system.file_uploads_disk', 'local')); + $directory = config('statamic.system.file_uploads_path', 'statamic/file-uploads'); $disk - ->getFilesRecursively($dir = 'statamic/file-uploads') + ->getFilesRecursively($directory) ->filter(function ($path) { $bits = explode('/', $path); $timestamp = $bits[count($bits) - 2]; @@ -32,6 +33,6 @@ private function deleteFilesOverAnHourOld() }) ->each(fn ($path) => $disk->delete($path)); - $disk->deleteEmptySubfolders($dir); + $disk->deleteEmptySubfolders($directory); } } diff --git a/tests/Assets/ReplacementFileTest.php b/tests/Assets/ReplacementFileTest.php index 3db4a5b00b6..477ed7058b1 100644 --- a/tests/Assets/ReplacementFileTest.php +++ b/tests/Assets/ReplacementFileTest.php @@ -30,4 +30,19 @@ public function it_writes_the_file_to_another_disk() $targetDisk->assertExists('the/new/path.jpg'); } + + #[Test] + public function it_reads_from_configured_disk() + { + config(['statamic.system.file_uploads_disk' => 'uploads']); + + $originDisk = Storage::fake('uploads'); + $targetDisk = Storage::fake('target'); + $originDisk->write('foo/bar/baz.jpg', 'contents'); + + (new ReplacementFile('foo/bar/baz.jpg'))->writeTo($targetDisk, 'the/new/path.jpg'); + + $targetDisk->assertExists('the/new/path.jpg'); + $this->assertEquals('contents', $targetDisk->get('the/new/path.jpg')); + } } diff --git a/tests/Feature/Fieldtypes/FilesTest.php b/tests/Feature/Fieldtypes/FilesTest.php index fb408f4f7b3..5b23a7069c0 100644 --- a/tests/Feature/Fieldtypes/FilesTest.php +++ b/tests/Feature/Fieldtypes/FilesTest.php @@ -96,6 +96,35 @@ public static function uploadProvider() ]; } + #[Test] + public function it_uploads_a_file_to_the_configured_disk_and_path() + { + config([ + 'statamic.system.file_uploads_disk' => 'uploads', + 'statamic.system.file_uploads_path' => 'temp-uploads', + ]); + + $localDisk = Storage::fake('local'); + $uploadsDisk = Storage::fake('uploads'); + + $file = UploadedFile::fake()->create('test.txt'); + + Date::setTestNow(Date::createFromTimestamp(1671484636, config('app.timezone'))); + + $this + ->actingAs(tap(User::make()->makeSuper())->save()) + ->post('/cp/fieldtypes/files/upload', ['file' => $file]) + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => '1671484636/test.txt', + ], + ]); + + $uploadsDisk->assertExists('temp-uploads/1671484636/test.txt'); + $localDisk->assertMissing('statamic/file-uploads/1671484636/test.txt'); + } + #[Test] public function it_replaces_dimensions_rule() { diff --git a/tests/Forms/SendEmailsTest.php b/tests/Forms/SendEmailsTest.php index 0360d85f685..aafcf9f9b9c 100644 --- a/tests/Forms/SendEmailsTest.php +++ b/tests/Forms/SendEmailsTest.php @@ -3,6 +3,7 @@ namespace Tests\Forms; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Storage; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Forms\SubmissionRepository; @@ -139,6 +140,34 @@ public function delete_attachments_job_only_saves_submission_when_enabled() $this->assertEmpty($submissions); } + #[Test] + public function delete_attachments_job_deletes_files_from_the_configured_disk_and_path() + { + config([ + 'statamic.system.file_uploads_disk' => 'uploads', + 'statamic.system.file_uploads_path' => 'temp-uploads', + ]); + + $localDisk = Storage::fake('local'); + $uploadsDisk = Storage::fake('uploads'); + $uploadsDisk->put('temp-uploads/1234567/file.txt', 'contents'); + $localDisk->put('statamic/file-uploads/1234567/file.txt', 'contents'); + + $form = tap(FacadesForm::make('attachments_test')->email([ + 'from' => 'first@sender.com', + 'to' => 'first@recipient.com', + ]))->save(); + + $form->blueprint()->ensureField('attachments', ['type' => 'files'])->save(); + + $submission = $form->makeSubmission()->data(['attachments' => ['1234567/file.txt']]); + + (new DeleteTemporaryAttachments($submission))->handle(); + + $uploadsDisk->assertMissing('temp-uploads/1234567/file.txt'); + $localDisk->assertExists('statamic/file-uploads/1234567/file.txt'); + } + #[Test] #[DataProvider('noEmailsProvider')] public function no_email_jobs_are_queued_if_none_are_configured($emailConfig) diff --git a/tests/Http/Middleware/DeleteTemporaryFileUploadsTest.php b/tests/Http/Middleware/DeleteTemporaryFileUploadsTest.php new file mode 100644 index 00000000000..1784473b09f --- /dev/null +++ b/tests/Http/Middleware/DeleteTemporaryFileUploadsTest.php @@ -0,0 +1,40 @@ + 'uploads', + 'statamic.system.file_uploads_path' => 'temp-uploads', + ]); + + $localDisk = Storage::fake('local'); + $uploadsDisk = Storage::fake('uploads'); + + Date::setTestNow(now()); + + $oldTimestamp = now()->subHours(2)->timestamp; + $newTimestamp = now()->timestamp; + + $uploadsDisk->put("temp-uploads/{$oldTimestamp}/old.txt", 'old'); + $uploadsDisk->put("temp-uploads/{$newTimestamp}/new.txt", 'new'); + $localDisk->put("statamic/file-uploads/{$oldTimestamp}/old.txt", 'old'); + + (new \ReflectionMethod(DeleteTemporaryFileUploads::class, 'deleteFilesOverAnHourOld')) + ->invoke(new DeleteTemporaryFileUploads); + + $uploadsDisk->assertMissing("temp-uploads/{$oldTimestamp}/old.txt"); + $uploadsDisk->assertExists("temp-uploads/{$newTimestamp}/new.txt"); + $localDisk->assertExists("statamic/file-uploads/{$oldTimestamp}/old.txt"); + } +}