feat(storage): implement S3 file upload and temporary URL generation functionality

This commit is contained in:
ncoronel 2026-06-25 14:23:34 -03:00
parent 3360e81e96
commit 049484dee5
7 changed files with 272 additions and 0 deletions

View File

@ -0,0 +1,39 @@
<?php
namespace App\Domains\StorageTest\Controllers;
use App\Domains\StorageTest\Requests\GenerateS3TemporaryUrlRequest;
use App\Domains\StorageTest\Requests\StoreS3TestFileRequest;
use App\Domains\StorageTest\Services\S3TestService;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
class S3TestController extends Controller
{
public function __construct(
protected S3TestService $s3TestService,
) {
}
public function store(StoreS3TestFileRequest $request): JsonResponse
{
return response()->json(
$this->s3TestService->storeTestFile(
$request->file('file'),
$request->validated('directory'),
(int) $request->validated('expires_in_minutes', 10),
),
201,
);
}
public function temporaryUrl(GenerateS3TemporaryUrlRequest $request): JsonResponse
{
return response()->json(
$this->s3TestService->generateTemporaryUrl(
$request->validated('path'),
(int) $request->validated('expires_in_minutes', 10),
)
);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Domains\StorageTest\Requests;
use Illuminate\Foundation\Http\FormRequest;
class GenerateS3TemporaryUrlRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, array<int, string>>
*/
public function rules(): array
{
return [
'path' => ['required', 'string', 'max:2048'],
'expires_in_minutes' => ['nullable', 'integer', 'min:1', 'max:1440'],
];
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Domains\StorageTest\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreS3TestFileRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, array<int, string>>
*/
public function rules(): array
{
return [
'file' => ['required', 'file', 'max:10240'],
'directory' => ['nullable', 'string', 'max:255'],
'expires_in_minutes' => ['nullable', 'integer', 'min:1', 'max:1440'],
];
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Domains\StorageTest\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class S3TestService
{
/**
* @return array<string, int|string|null>
*/
public function storeTestFile(
UploadedFile $file,
?string $directory = null,
int $expiresInMinutes = 10,
): array {
$directory = $this->normalizeDirectory($directory);
$disk = Storage::disk('s3');
$path = $disk->putFile($directory, $file);
if (! is_string($path) || $path === '') {
Log::error('S3 upload returned an empty path.', [
'disk' => 's3',
'directory' => $directory,
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getClientMimeType(),
'size' => $file->getSize(),
]);
throw new RuntimeException('No se pudo subir el archivo al disco s3.');
}
return [
'disk' => 's3',
'directory' => $directory,
'path' => $path,
'filename' => basename($path),
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getClientMimeType(),
'extension' => $file->extension(),
'size' => $file->getSize(),
'temporary_url' => $disk->temporaryUrl($path, now()->addMinutes($expiresInMinutes)),
'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(),
];
}
/**
* @return array<string, string>
*/
public function generateTemporaryUrl(string $path, int $expiresInMinutes = 10): array
{
return [
'disk' => 's3',
'path' => $path,
'temporary_url' => $this->temporaryUrlForPath($path, $expiresInMinutes),
'temporary_url_expires_at' => now()->addMinutes($expiresInMinutes)->toIso8601String(),
];
}
protected function temporaryUrlForPath(string $path, int $expiresInMinutes): string
{
return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes($expiresInMinutes));
}
protected function normalizeDirectory(?string $directory): string
{
$directory = trim((string) $directory, '/');
if ($directory !== '') {
return $directory;
}
return 'testing/attachments/'.now()->format('Y/m/d').'/'.Str::uuid();
}
}

View File

@ -0,0 +1,9 @@
<?php
use App\Domains\StorageTest\Controllers\S3TestController;
use Illuminate\Support\Facades\Route;
Route::prefix('storage-test/s3')->group(function (): void {
Route::post('upload', [S3TestController::class, 'store']);
Route::get('temporary-url', [S3TestController::class, 'temporaryUrl']);
});

View File

@ -0,0 +1,95 @@
{
"info": {
"_postman_id": "8faefdb8-734a-4262-a3b6-4d49e56ea901",
"name": "Storage Test S3",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "base_url",
"value": "http://127.0.0.1:8000"
},
{
"key": "expires_in_minutes",
"value": "10"
},
{
"key": "path",
"value": ""
}
],
"item": [
{
"name": "Upload Test File",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "file",
"type": "file",
"src": []
},
{
"key": "directory",
"value": "testing/manual",
"type": "text"
},
{
"key": "expires_in_minutes",
"value": "{{expires_in_minutes}}",
"type": "text"
}
]
},
"url": {
"raw": "{{base_url}}/api/storage-test/s3/upload",
"host": [
"{{base_url}}"
],
"path": [
"api",
"storage-test",
"s3",
"upload"
]
},
"description": "Sube un archivo al disco s3 y devuelve el path junto con una temporary_url."
},
"response": []
},
{
"name": "Generate Temporary URL",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{base_url}}/api/storage-test/s3/temporary-url?path={{path}}&expires_in_minutes={{expires_in_minutes}}",
"host": [
"{{base_url}}"
],
"path": [
"api",
"storage-test",
"s3",
"temporary-url"
],
"query": [
{
"key": "path",
"value": "{{path}}"
},
{
"key": "expires_in_minutes",
"value": "{{expires_in_minutes}}"
}
]
},
"description": "Genera una URL temporal para un path ya existente en S3."
},
"response": []
}
]
}

View File

@ -9,4 +9,5 @@ Route::get('/user', function (Request $request) {
require __DIR__.'/../app/Domains/Catalog/routes/api.php';
require __DIR__.'/../app/Domains/Cart/routes/api.php';
require __DIR__.'/../app/Domains/StorageTest/routes/api.php';
require __DIR__.'/../app/Domains/Tenant/routes/api.php';