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 <cursoragent@cursor.com>
This commit is contained in:
parent
08ce75d2ec
commit
2ba5ce2b3f
3 changed files with 155 additions and 126 deletions
|
|
@ -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
|
||||
|
|
|
|||
43
app/Services/Media/ChunkReceipt.php
Normal file
43
app/Services/Media/ChunkReceipt.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Media;
|
||||
|
||||
use App\Http\Resources\App\MediaResource;
|
||||
use App\Models\Media;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
final readonly class ChunkReceipt
|
||||
{
|
||||
private function __construct(
|
||||
public bool $done,
|
||||
public ?int $progress = null,
|
||||
public ?Media $media = null,
|
||||
) {}
|
||||
|
||||
public static function inProgress(int $progress): self
|
||||
{
|
||||
return new self(done: false, progress: $progress);
|
||||
}
|
||||
|
||||
public static function completed(Media $media): self
|
||||
{
|
||||
return new self(done: true, media: $media);
|
||||
}
|
||||
|
||||
public function toResponse(): JsonResponse
|
||||
{
|
||||
if (! $this->done) {
|
||||
return response()->json([
|
||||
'done' => false,
|
||||
'progress' => $this->progress,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'done' => true,
|
||||
...(new MediaResource($this->media))->resolve(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
102
app/Services/Media/ChunkedAssetReceiver.php
Normal file
102
app/Services/Media/ChunkedAssetReceiver.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Media;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Throwable;
|
||||
|
||||
final class ChunkedAssetReceiver
|
||||
{
|
||||
public function __construct(private readonly ChunkedCloudUploader $cloud) {}
|
||||
|
||||
public function receive(
|
||||
Workspace $workspace,
|
||||
User $user,
|
||||
string $fileName,
|
||||
string $chunk,
|
||||
int $rangeStart,
|
||||
int $rangeEnd,
|
||||
int $totalSize,
|
||||
): ChunkReceipt {
|
||||
$identifier = md5($user->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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue