mirror of
https://github.com/VueFileManager/vuefilemanager.git
synced 2026-05-30 15:44:41 +00:00
remote upload backend functionality
This commit is contained in:
@@ -947,5 +947,6 @@ return [
|
|||||||
'remote_links' => 'Remote Links',
|
'remote_links' => 'Remote Links',
|
||||||
'remote_links_help' => 'For every line paste one link',
|
'remote_links_help' => 'For every line paste one link',
|
||||||
'paste_remote_links_here' => 'Paste your remote links here...',
|
'paste_remote_links_here' => 'Paste your remote links here...',
|
||||||
|
'remote_download_submitted' => 'Your links will be downloaded as soon as possible',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"/chunks/environment.js": "/chunks/environment.js?id=784c2442268b36dc",
|
"/chunks/environment.js": "/chunks/environment.js?id=784c2442268b36dc",
|
||||||
"/chunks/app-setup.js": "/chunks/app-setup.js?id=cbe7bfed06400736",
|
"/chunks/app-setup.js": "/chunks/app-setup.js?id=cbe7bfed06400736",
|
||||||
"/chunks/admin-account.js": "/chunks/admin-account.js?id=78d257775f5fc485",
|
"/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/browser.js": "/chunks/shared/browser.js?id=d2fff07a2bc7af3f",
|
||||||
"/chunks/shared/single-file.js": "/chunks/shared/single-file.js?id=a6063bed9be75a09",
|
"/chunks/shared/single-file.js": "/chunks/shared/single-file.js?id=a6063bed9be75a09",
|
||||||
"/chunks/shared/authenticate.js": "/chunks/shared/authenticate.js?id=b5519d193bce2339",
|
"/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-password.js": "/chunks/settings-password.js?id=d00bf503d8126dc4",
|
||||||
"/chunks/settings-storage.js": "/chunks/settings-storage.js?id=092e324aad54656b",
|
"/chunks/settings-storage.js": "/chunks/settings-storage.js?id=092e324aad54656b",
|
||||||
"/chunks/billing.js": "/chunks/billing.js?id=115c25478cee576d",
|
"/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/files.js": "/chunks/files.js?id=aaea9173f7697d6e",
|
||||||
"/chunks/recent-uploads.js": "/chunks/recent-uploads.js?id=4bab41df721a6fc6",
|
"/chunks/recent-uploads.js": "/chunks/recent-uploads.js?id=4bab41df721a6fc6",
|
||||||
"/chunks/my-shared-items.js": "/chunks/my-shared-items.js?id=c62bc3eb07de20df",
|
"/chunks/my-shared-items.js": "/chunks/my-shared-items.js?id=c62bc3eb07de20df",
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export default {
|
|||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
let route = this.$store.getters.sharedDetail
|
let route = this.$store.getters.sharedDetail
|
||||||
? `/api/editor/remote-upload/${this.$router.currentRoute.params.token}`
|
? `/api/editor/upload/remote/${this.$router.currentRoute.params.token}`
|
||||||
: '/api/remote-upload'
|
: '/api/upload/remote'
|
||||||
|
|
||||||
let parentId = this.$store.getters.currentFolder
|
let parentId = this.$store.getters.currentFolder
|
||||||
? this.$store.getters.currentFolder.data.id
|
? this.$store.getters.currentFolder.data.id
|
||||||
@@ -85,7 +85,9 @@ export default {
|
|||||||
|
|
||||||
axios
|
axios
|
||||||
.post(route, {
|
.post(route, {
|
||||||
url: this.links.split(/\r?\n/),
|
urls: this.links
|
||||||
|
.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "")
|
||||||
|
.split(/\r?\n/),
|
||||||
parent_id: parentId,
|
parent_id: parentId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -96,7 +98,13 @@ export default {
|
|||||||
|
|
||||||
events.$emit('popup:close')
|
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', {
|
events.$emit('toaster', {
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
message: this.$t('popup_error.title'),
|
message: this.$t('popup_error.title'),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Domain\SetupWizard\Controllers\PingAPIController;
|
|||||||
use Domain\Folders\Controllers\CreateFolderController;
|
use Domain\Folders\Controllers\CreateFolderController;
|
||||||
use Domain\Browsing\Controllers\BrowseFolderController;
|
use Domain\Browsing\Controllers\BrowseFolderController;
|
||||||
use Domain\Sharing\Controllers\ShareViaEmailController;
|
use Domain\Sharing\Controllers\ShareViaEmailController;
|
||||||
|
use Domain\Files\Controllers\RemoteUploadFileController;
|
||||||
use Domain\Folders\Controllers\NavigationTreeController;
|
use Domain\Folders\Controllers\NavigationTreeController;
|
||||||
use Domain\Items\Controllers\MoveFileOrFolderController;
|
use Domain\Items\Controllers\MoveFileOrFolderController;
|
||||||
use App\Socialite\Controllers\SocialiteRedirectController;
|
use App\Socialite\Controllers\SocialiteRedirectController;
|
||||||
@@ -78,6 +79,7 @@ Route::group(['middleware' => ['auth:sanctum']], function () {
|
|||||||
|
|
||||||
// User master,editor routes
|
// User master,editor routes
|
||||||
Route::group(['middleware' => ['auth:sanctum']], function () {
|
Route::group(['middleware' => ['auth:sanctum']], function () {
|
||||||
|
Route::post('/upload/remote', RemoteUploadFileController::class);
|
||||||
Route::post('/create-folder', CreateFolderController::class);
|
Route::post('/create-folder', CreateFolderController::class);
|
||||||
Route::post('/upload', UploadFileController::class);
|
Route::post('/upload', UploadFileController::class);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\Files\Actions;
|
||||||
|
|
||||||
|
use App\Users\Models\User;
|
||||||
|
use Domain\Files\Models\File;
|
||||||
|
use Error;
|
||||||
|
use ErrorException;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Domain\Sharing\Models\Share;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Log;
|
||||||
|
use Spatie\QueueableAction\QueueableAction;
|
||||||
|
|
||||||
|
class GetContentFromExternalSource
|
||||||
|
{
|
||||||
|
use QueueableAction;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public ProcessFileAction $processFile,
|
||||||
|
public StoreFileExifMetadataAction $storeExifMetadata,
|
||||||
|
public MoveFileToFTPStorageAction $moveFileToFTPStorage,
|
||||||
|
public ProcessImageThumbnailAction $createImageThumbnail,
|
||||||
|
public MoveFileToExternalStorageAction $moveFileToExternalStorage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(
|
||||||
|
array $payload,
|
||||||
|
User $user,
|
||||||
|
) {
|
||||||
|
foreach ($payload['urls'] as $url) {
|
||||||
|
try {
|
||||||
|
// Get local disk instance
|
||||||
|
$localDisk = Storage::disk('local');
|
||||||
|
|
||||||
|
// Get file from external source
|
||||||
|
$response = Http::get($url);
|
||||||
|
|
||||||
|
// Get extension from response
|
||||||
|
$extension = extractExtensionFromUrl($url, $response);
|
||||||
|
|
||||||
|
// Get blacklisted mimetypes
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Domain\Files\Controllers;
|
||||||
|
|
||||||
|
use Domain\Folders\Models\Folder;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Domain\Sharing\Models\Share;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Domain\Files\Requests\RemoteUploadRequest;
|
||||||
|
use Domain\Files\Actions\GetContentFromExternalSource;
|
||||||
|
|
||||||
|
class RemoteUploadFileController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public GetContentFromExternalSource $getContentFromExternalSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(RemoteUploadRequest $request, ?Share $shared = null): Response|array
|
||||||
|
{
|
||||||
|
if (is_demo_account()) {
|
||||||
|
return response('Files were successfully added to the upload queue', 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
$user = $request->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\Files\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RemoteUploadRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'urls.*' => 'required|url',
|
||||||
|
'parent_id' => 'nullable|uuid',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1157,4 +1157,36 @@ if (! function_exists('replace_occurrence')) {
|
|||||||
return "{$degrees}°$minutes'$seconds\"$ref";
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Tests\Domain\Files;
|
namespace Tests\Domain\Files;
|
||||||
|
|
||||||
use Storage;
|
use Storage;
|
||||||
@@ -9,6 +10,7 @@ use Domain\Files\Models\File;
|
|||||||
use Domain\Folders\Models\Folder;
|
use Domain\Folders\Models\Folder;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Domain\Settings\Models\Setting;
|
use Domain\Settings\Models\Setting;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
class FileTest extends TestCase
|
class FileTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -59,7 +61,7 @@ class FileTest extends TestCase
|
|||||||
])
|
])
|
||||||
->collapse()
|
->collapse()
|
||||||
->each(
|
->each(
|
||||||
fn ($item) => Storage::assertExists(
|
fn($item) => Storage::assertExists(
|
||||||
"files/{$user->id}/{$item['name']}-{$file->basename}"
|
"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
|
* @test
|
||||||
*/
|
*/
|
||||||
@@ -233,7 +283,7 @@ class FileTest extends TestCase
|
|||||||
'parent_id' => $folder->id,
|
'parent_id' => $folder->id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
@@ -288,7 +338,7 @@ class FileTest extends TestCase
|
|||||||
|
|
||||||
// Assert thumbnail was deleted
|
// Assert thumbnail was deleted
|
||||||
getThumbnailFileList('fake-image.jpeg')
|
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()
|
public function it_store_file_exif_data_after_file_upload()
|
||||||
{
|
{
|
||||||
$file = UploadedFile::fake()
|
$file = UploadedFile::fake()
|
||||||
|
|||||||
Reference in New Issue
Block a user