From 2ba5ce2b3f42f92d535f24f39a265136e0f16961 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 23 Jul 2026 11:18:10 -0300 Subject: [PATCH] refactor: pull chunked upload orchestration out of AssetController Controller only authorizes and returns ChunkReceipt. Multipart vs local assemble lives in ChunkedAssetReceiver; JSON shape comes from MediaResource. Co-authored-by: Cursor --- app/Http/Controllers/App/AssetController.php | 136 ++----------------- app/Services/Media/ChunkReceipt.php | 43 ++++++ app/Services/Media/ChunkedAssetReceiver.php | 102 ++++++++++++++ 3 files changed, 155 insertions(+), 126 deletions(-) create mode 100644 app/Services/Media/ChunkReceipt.php create mode 100644 app/Services/Media/ChunkedAssetReceiver.php diff --git a/app/Http/Controllers/App/AssetController.php b/app/Http/Controllers/App/AssetController.php index 62e2ca17..e94576b3 100644 --- a/app/Http/Controllers/App/AssetController.php +++ b/app/Http/Controllers/App/AssetController.php @@ -9,9 +9,8 @@ use App\Http\Requests\App\Asset\StoreChunkedAssetRequest; use App\Http\Resources\App\MediaResource; use App\Models\Media; -use App\Models\Workspace; use App\Services\Brand\SafeHttpFetcher; -use App\Services\Media\ChunkedCloudUploader; +use App\Services\Media\ChunkedAssetReceiver; use App\Services\UnsplashService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -23,7 +22,6 @@ use Inertia\Response; use RuntimeException; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; -use Throwable; class AssetController extends Controller { @@ -71,135 +69,21 @@ public function store(StoreAssetRequest $request): MediaResource return new MediaResource($media); } - public function storeChunked(StoreChunkedAssetRequest $request, ChunkedCloudUploader $cloudUploader): JsonResponse + public function storeChunked(StoreChunkedAssetRequest $request, ChunkedAssetReceiver $receiver): JsonResponse { $workspace = $request->user()->currentWorkspace; $this->authorize('createPost', $workspace); - $rangeStart = (int) $request->validated('range_start'); - $rangeEnd = (int) $request->validated('range_end'); - $totalSize = (int) $request->validated('total_size'); - $fileName = (string) $request->validated('file_name'); - $chunk = $request->getContent(); - $identifier = md5($request->user()->id.$fileName.$totalSize); - - if ($cloudUploader->shouldUseMultipart($fileName)) { - return $this->storeChunkedViaMultipart( - $cloudUploader, - $workspace, - $identifier, - $fileName, - $chunk, - $rangeStart, - $rangeEnd, - $totalSize, - ); - } - - return $this->storeChunkedViaLocalAssemble( + return $receiver->receive( $workspace, - $identifier, - $fileName, - $chunk, - $rangeStart, - $rangeEnd, - $totalSize, - ); - } - - private function storeChunkedViaMultipart( - ChunkedCloudUploader $cloudUploader, - Workspace $workspace, - string $identifier, - string $fileName, - string $chunk, - int $rangeStart, - int $rangeEnd, - int $totalSize, - ): JsonResponse { - $result = $cloudUploader->receiveChunk( - $identifier, - $fileName, - $chunk, - $rangeStart, - $rangeEnd, - $totalSize, - ); - - if (! data_get($result, 'done')) { - return response()->json([ - 'done' => false, - 'progress' => data_get($result, 'progress'), - ]); - } - - $path = (string) data_get($result, 'path'); - - try { - $media = $workspace->addMediaFromStoredPath( - $path, - $fileName, - (string) data_get($result, 'mime_type'), - (int) data_get($result, 'size'), - 'assets', - ); - } catch (Throwable $exception) { - Storage::delete($path); - - throw $exception; - } - - return $this->chunkedMediaResponse($media); - } - - private function storeChunkedViaLocalAssemble( - Workspace $workspace, - string $identifier, - string $fileName, - string $chunk, - int $rangeStart, - int $rangeEnd, - int $totalSize, - ): JsonResponse { - $tempFile = storage_path("app/private/chunks/{$identifier}"); - - if (! is_dir(dirname($tempFile))) { - mkdir(dirname($tempFile), 0755, true); - } - - file_put_contents($tempFile, $chunk, $rangeStart === 0 ? 0 : FILE_APPEND); - - if (($rangeEnd + 1) < $totalSize) { - return response()->json([ - 'done' => false, - 'progress' => (int) round(($rangeEnd + 1) / $totalSize * 100), - ]); - } - - try { - $media = $workspace->addMediaFromPath($tempFile, $fileName, 'assets'); - } finally { - @unlink($tempFile); - } - - return $this->chunkedMediaResponse($media); - } - - private function chunkedMediaResponse(Media $media): JsonResponse - { - return response()->json([ - 'done' => true, - 'id' => $media->id, - 'path' => $media->path, - 'url' => $media->url, - 'type' => $media->type->value, - 'mime_type' => $media->mime_type, - 'original_filename' => $media->original_filename, - 'size' => $media->size, - 'meta' => $media->meta, - 'created_at' => $media->created_at->toISOString(), - ]); + $request->user(), + (string) $request->validated('file_name'), + $request->getContent(), + (int) $request->validated('range_start'), + (int) $request->validated('range_end'), + (int) $request->validated('total_size'), + )->toResponse(); } public function storeFromUrl(StoreAssetFromUrlRequest $request, UnsplashService $unsplash, SafeHttpFetcher $safeHttp): MediaResource diff --git a/app/Services/Media/ChunkReceipt.php b/app/Services/Media/ChunkReceipt.php new file mode 100644 index 00000000..e5f576c7 --- /dev/null +++ b/app/Services/Media/ChunkReceipt.php @@ -0,0 +1,43 @@ +done) { + return response()->json([ + 'done' => false, + 'progress' => $this->progress, + ]); + } + + return response()->json([ + 'done' => true, + ...(new MediaResource($this->media))->resolve(), + ]); + } +} diff --git a/app/Services/Media/ChunkedAssetReceiver.php b/app/Services/Media/ChunkedAssetReceiver.php new file mode 100644 index 00000000..fa09a595 --- /dev/null +++ b/app/Services/Media/ChunkedAssetReceiver.php @@ -0,0 +1,102 @@ +id.$fileName.$totalSize); + + return $this->cloud->shouldUseMultipart($fileName) + ? $this->receiveViaMultipart($workspace, $identifier, $fileName, $chunk, $rangeStart, $rangeEnd, $totalSize) + : $this->receiveViaLocalAssemble($workspace, $identifier, $fileName, $chunk, $rangeStart, $rangeEnd, $totalSize); + } + + private function receiveViaMultipart( + Workspace $workspace, + string $identifier, + string $fileName, + string $chunk, + int $rangeStart, + int $rangeEnd, + int $totalSize, + ): ChunkReceipt { + $result = $this->cloud->receiveChunk( + $identifier, + $fileName, + $chunk, + $rangeStart, + $rangeEnd, + $totalSize, + ); + + if (! data_get($result, 'done')) { + return ChunkReceipt::inProgress((int) data_get($result, 'progress')); + } + + $path = (string) data_get($result, 'path'); + + try { + $media = $workspace->addMediaFromStoredPath( + $path, + $fileName, + (string) data_get($result, 'mime_type'), + (int) data_get($result, 'size'), + 'assets', + ); + } catch (Throwable $exception) { + Storage::delete($path); + + throw $exception; + } + + return ChunkReceipt::completed($media); + } + + private function receiveViaLocalAssemble( + Workspace $workspace, + string $identifier, + string $fileName, + string $chunk, + int $rangeStart, + int $rangeEnd, + int $totalSize, + ): ChunkReceipt { + $tempFile = storage_path("app/private/chunks/{$identifier}"); + + if (! is_dir(dirname($tempFile))) { + mkdir(dirname($tempFile), 0755, true); + } + + file_put_contents($tempFile, $chunk, $rangeStart === 0 ? 0 : FILE_APPEND); + + if (($rangeEnd + 1) < $totalSize) { + return ChunkReceipt::inProgress((int) round(($rangeEnd + 1) / $totalSize * 100)); + } + + try { + $media = $workspace->addMediaFromPath($tempFile, $fileName, 'assets'); + } finally { + @unlink($tempFile); + } + + return ChunkReceipt::completed($media); + } +}