diff --git a/config/language-translations.php b/config/language-translations.php index 5620662b..3a0c4d0b 100644 --- a/config/language-translations.php +++ b/config/language-translations.php @@ -947,5 +947,6 @@ return [ 'remote_links' => 'Remote Links', 'remote_links_help' => 'For every line paste one link', 'paste_remote_links_here' => 'Paste your remote links here...', + 'remote_download_submitted' => 'Your links will be downloaded as soon as possible', ], ]; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 963f6a04..93002981 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -9,7 +9,7 @@ "/chunks/environment.js": "/chunks/environment.js?id=784c2442268b36dc", "/chunks/app-setup.js": "/chunks/app-setup.js?id=cbe7bfed06400736", "/chunks/admin-account.js": "/chunks/admin-account.js?id=78d257775f5fc485", - "/chunks/shared.js": "/chunks/shared.js?id=df78268616502614", + "/chunks/shared.js": "/chunks/shared.js?id=76d00e2402745b07", "/chunks/shared/browser.js": "/chunks/shared/browser.js?id=d2fff07a2bc7af3f", "/chunks/shared/single-file.js": "/chunks/shared/single-file.js?id=a6063bed9be75a09", "/chunks/shared/authenticate.js": "/chunks/shared/authenticate.js?id=b5519d193bce2339", @@ -62,7 +62,7 @@ "/chunks/settings-password.js": "/chunks/settings-password.js?id=d00bf503d8126dc4", "/chunks/settings-storage.js": "/chunks/settings-storage.js?id=092e324aad54656b", "/chunks/billing.js": "/chunks/billing.js?id=115c25478cee576d", - "/chunks/platform.js": "/chunks/platform.js?id=b2be2d8a25d580e0", + "/chunks/platform.js": "/chunks/platform.js?id=780d762236b079cf", "/chunks/files.js": "/chunks/files.js?id=aaea9173f7697d6e", "/chunks/recent-uploads.js": "/chunks/recent-uploads.js?id=4bab41df721a6fc6", "/chunks/my-shared-items.js": "/chunks/my-shared-items.js?id=c62bc3eb07de20df", diff --git a/resources/js/components/RemoteUpload/RemoteUploadPopup.vue b/resources/js/components/RemoteUpload/RemoteUploadPopup.vue index eb82b13d..43067241 100644 --- a/resources/js/components/RemoteUpload/RemoteUploadPopup.vue +++ b/resources/js/components/RemoteUpload/RemoteUploadPopup.vue @@ -76,8 +76,8 @@ export default { this.loading = true let route = this.$store.getters.sharedDetail - ? `/api/editor/remote-upload/${this.$router.currentRoute.params.token}` - : '/api/remote-upload' + ? `/api/editor/upload/remote/${this.$router.currentRoute.params.token}` + : '/api/upload/remote' let parentId = this.$store.getters.currentFolder ? this.$store.getters.currentFolder.data.id @@ -85,7 +85,9 @@ export default { axios .post(route, { - url: this.links.split(/\r?\n/), + urls: this.links + .replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "") + .split(/\r?\n/), parent_id: parentId, }) .then(() => { @@ -96,7 +98,13 @@ export default { events.$emit('popup:close') }) - .catch(() => { + .catch((error) => { + if (error.response.status === 422) { + this.$refs.createForm.setErrors({ + 'Remote Links': error.response.data.message, + }) + } + events.$emit('toaster', { type: 'danger', message: this.$t('popup_error.title'), diff --git a/routes/api.php b/routes/api.php index 761b9e9f..21dbd1e8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,7 @@ use Domain\SetupWizard\Controllers\PingAPIController; use Domain\Folders\Controllers\CreateFolderController; use Domain\Browsing\Controllers\BrowseFolderController; use Domain\Sharing\Controllers\ShareViaEmailController; +use Domain\Files\Controllers\RemoteUploadFileController; use Domain\Folders\Controllers\NavigationTreeController; use Domain\Items\Controllers\MoveFileOrFolderController; use App\Socialite\Controllers\SocialiteRedirectController; @@ -78,6 +79,7 @@ Route::group(['middleware' => ['auth:sanctum']], function () { // User master,editor routes Route::group(['middleware' => ['auth:sanctum']], function () { + Route::post('/upload/remote', RemoteUploadFileController::class); Route::post('/create-folder', CreateFolderController::class); Route::post('/upload', UploadFileController::class); diff --git a/src/Domain/Files/Actions/GetContentFromExternalSource.php b/src/Domain/Files/Actions/GetContentFromExternalSource.php new file mode 100644 index 00000000..88145f45 --- /dev/null +++ b/src/Domain/Files/Actions/GetContentFromExternalSource.php @@ -0,0 +1,102 @@ +checkDisabledMimetypes($extension); + + // Get file basename + $basename = Str::uuid() . ".$extension"; + + // Get file name + $name = array_key_exists('filename', pathinfo($url)) + ? explode('?', pathinfo($url)['filename'])[0] + : Str::uuid(); + + // Get file path + $path = "files/$user->id/$basename"; + + // Store file to main storage disk + $localDisk->put($path, $response->getBody()); + + // Create multiple image thumbnails + ($this->createImageThumbnail)($basename, $user->id); + + // Create new file + $file = File::create([ + 'mimetype' => $extension, + 'type' => getFileType($localDisk->mimeType($path)), + 'parent_id' => $payload['parent_id'] ?? null, + 'name' => $name ?? $basename, + 'basename' => $basename, + 'filesize' => $localDisk->size($path), + 'user_id' => $user->id, + 'creator_id' => auth()->id(), + ]); + + // Store file exif information + ($this->storeExifMetadata)($file); + + // Move file to external storage + match (config('filesystems.default')) { + 's3' => ($this->moveFileToExternalStorage)($basename, $user->id), + 'ftp', 'azure' => ($this->moveFileToFTPStorage)($basename, $user->id), + default => null + }; + } catch ( ErrorException | Error $e) { + Log::error("Remote upload failed as {$e->getMessage()}"); + Log::error($e->getTraceAsString()); + } + } + } + + /** + * @param string|null $extension + */ + protected function checkDisabledMimetypes(?string $extension): void + { + $mimetypeBlacklist = explode(',', get_settings('mimetypes_blacklist')) ?? null; + + // If is extension in mimetype blacklist, Abort! + if ($extension && array_intersect([str_replace('.', '', ".$extension")], $mimetypeBlacklist)) { + abort(422); + } + } +} diff --git a/src/Domain/Files/Controllers/RemoteUploadFileController.php b/src/Domain/Files/Controllers/RemoteUploadFileController.php new file mode 100644 index 00000000..8ac53a33 --- /dev/null +++ b/src/Domain/Files/Controllers/RemoteUploadFileController.php @@ -0,0 +1,36 @@ +filled('parent_id') + ? Folder::find($request->input('parent_id'))->getLatestParent()->user + : auth()->user(); + + // Execute job for get content from url and save + ($this->getContentFromExternalSource) + ->onQueue() + ->execute($request->all(), $user); + + return response('Files were successfully added to the upload queue', 201); + } +} diff --git a/src/Domain/Files/Requests/RemoteUploadRequest.php b/src/Domain/Files/Requests/RemoteUploadRequest.php new file mode 100644 index 00000000..60bf9d4b --- /dev/null +++ b/src/Domain/Files/Requests/RemoteUploadRequest.php @@ -0,0 +1,30 @@ + 'required|url', + 'parent_id' => 'nullable|uuid', + ]; + } +} diff --git a/src/Support/helpers.php b/src/Support/helpers.php index fa6c5402..e80dea7f 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -1157,4 +1157,36 @@ if (! function_exists('replace_occurrence')) { return "{$degrees}°$minutes'$seconds\"$ref"; } } + + if (! function_exists('extractExtensionFromUrl')) { + /** + * Extract extension from the url + * + * TODO: make unit test + */ + function extractExtensionFromUrl($url, $response): string|null + { + $string = str_replace(['&'], '?', pathinfo($url)['extension']); + + // Get extension from url path + $extension = array_key_exists('extension', pathinfo($url)) + ? explode('?', $string)[0] + : null; + + // Return pure extension + if ($extension) { + return $extension; + } + + // Prepare header for extracting content-type line + $header = array_change_key_case($response->headers(), CASE_LOWER); + + // Get extension + if (array_key_exists('content-type', $header)) { + return '.' . explode('/', $header['content-type'][0])[1]; + } + + return null; + } + } } diff --git a/tests/Domain/Files/FileTest.php b/tests/Domain/Files/FileTest.php index b5c9c557..0751841b 100644 --- a/tests/Domain/Files/FileTest.php +++ b/tests/Domain/Files/FileTest.php @@ -1,4 +1,5 @@ collapse() ->each( - fn ($item) => Storage::assertExists( + fn($item) => Storage::assertExists( "files/{$user->id}/{$item['name']}-{$file->basename}" ) ); @@ -100,6 +102,54 @@ class FileTest extends TestCase ); } + /** + * @test + */ + public function it_remotely_upload_new_file() + { + $user = User::factory() + ->hasSettings() + ->create(); + + $folder = Folder::factory() + ->create([ + 'user_id' => $user->id, + ]); + + $fakeFile = UploadedFile::fake() + ->create('top-secret-document.pdf', 12000000, 'application/pdf'); + + Http::fake([ + 'https://fake.com/top-secret-document.pdf' => Http::response($fakeFile->getContent()), + 'https://fake.com/another-secret-document.pdf' => Http::response($fakeFile->getContent()), + ]); + + $this + ->actingAs($user) + ->postJson('/api/upload/remote', [ + 'urls' => [ + 'https://fake.com/top-secret-document.pdf', + 'https://fake.com/another-secret-document.pdf', + ], + 'parent_id' => $folder->id, + ])->assertStatus(201); + + $this + ->assertDatabaseHas('files', [ + 'user_id' => $user->id, + 'name' => 'top-secret-document', + 'parent_id' => $folder->id, + ]) + ->assertDatabaseHas('files', [ + 'name' => 'another-secret-document', + ]); + + File::all() + ->each(function ($file) { + Storage::assertExists("files/$file->user_id/$file->basename"); + }); + } + /** * @test */ @@ -233,7 +283,7 @@ class FileTest extends TestCase 'parent_id' => $folder->id, ]); } - + /** * @test */ @@ -288,7 +338,7 @@ class FileTest extends TestCase // Assert thumbnail was deleted getThumbnailFileList('fake-image.jpeg') - ->each(fn ($thumbnail) => Storage::assertMissing("files/$user->id/$thumbnail")); + ->each(fn($thumbnail) => Storage::assertMissing("files/$user->id/$thumbnail")); } /** @@ -387,8 +437,8 @@ class FileTest extends TestCase } /** - * @test - */ + * @test + */ public function it_store_file_exif_data_after_file_upload() { $file = UploadedFile::fake()