Armazenamento de Arquivos e Uploads no Laravel
O sistema de arquivos é o "gerenciamento de repositório" do Laravel—seja os arquivos armazenados em um disco local ou na nuvem S3, o código permanece exatamente o mesmo.
1. O Que Você Vai Aprender
- Camada de abstração FileSystem: configuração de driver local/public/s3
- O processo completo de upload de arquivo: Validação → Armazenamento → Geração de URL → Resposta
- Link simbólico de armazenamento: php artisan storage:link
- Integração com Armazenamento em Nuvem S3 e URLs Pré-assinadas
- Operações de Arquivo: copy/move/delete/visibility e processamento de stream
2. Uma História Real do Mundo de Operações
(1) Problema: O disco do servidor está cheio, e todas as imagens foram perdidas
Todas as imagens de produtos do ShopMetrics estavam armazenadas localmente no servidor no diretório public/uploads/—500 GB de imagens encheram o disco, causando a queda do site. Para piorar, não havia backups após a falha de hardware do servidor, e Alice perdeu todas as 2.000 imagens de produtos. Bob queria migrar para o S3, mas os caminhos de arquivo estavam codificados em todo o código, e após três dias de trabalho, ainda não havia terminado as alterações.
(2) Soluções com a Camada de Abstração de Armazenamento
O facade Storage do Laravel usa uma API unificada para gerenciar arquivos—seja local, S3 ou qualquer outro driver, o código permanece o mesmo; para migrar basta alterar a configuração do .env.
// O mesmo código funciona para local, S3, ou qualquer driver
Storage::disk('public')->put('shops/logo.jpg', $file);
$url = Storage::disk('public')->url('shops/logo.jpg');
// Trocar para S3 alterando .env
// FILESYSTEM_DISK=s3
// Todo o resto permanece igual!
(3) Resultado
Bob trocou para S3 alterando apenas duas linhas do .env, sem modificações de código. As imagens de Alice no S3 têm 11 noves de durabilidade, então falta de espaço em disco e falhas de hardware não são mais um problema.
3. Camada de Abstração de Armazenamento
(1) Configuração de Driver
// config/filesystems.php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL') . '/storage',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
],
],
'default' => env('FILESYSTEM_DISK', 'local'),
(2) Comparação de Drivers
| Drive | Local de Armazenamento | Acesso Público | Persistência | Custo |
|---|---|---|---|---|
local |
storage/app/ | ❌ Requer roteamento | Servidor | Gratuito |
public |
storage/app/public/ | ✅ Link simbólico | Servidor | Gratuito |
s3 |
AWS S3 | ✅ URL | 11 noves | Pagamento por uso |
s3+CDN |
S3 + CloudFront | ✅ CDN | 11 noves | Pagamento por uso |
(1) ▶ Exemplo: Configuração de Armazenamento do ShopMetrics
# .env — Desenvolvimento: usar disco public
FILESYSTEM_DISK=public
# .env — Produção: usar S3
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=shopmetrics-uploads
# Criar link simbólico para disco público
php artisan storage:link
# [OK] Link criado: public/storage -> storage/app/public
Saída:
# Comando executado com sucesso
4. O Processo Completo de Upload de Arquivo
(1) Validação → Armazenamento → Geração de URL
// Passo 1: Validar
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Passo 2: Armazenar
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123def456.jpg"
// Passo 3: Gerar URL
$url = Storage::disk('public')->url($path);
// => "http://shopmetrics.test/storage/shops/logos/abc123def456.jpg"
// Passo 4: Salvar caminho no banco de dados
$shop->update(['logo_path' => $path]);
(2) Comparação de Métodos de Upload
| Método | Nome do Arquivo | Caminho de Exemplo |
|---|---|---|
store('dir', 'disk') |
Nome Hash Aleatório | shops/logos/abc123.jpg |
storeAs('dir', name, 'disk') |
Nome Personalizado | shops/logos/alice-store.jpg |
storePublicly('dir', 'disk') |
Nome Aleatório + Público | Mesmo que store |
storePubliclyAs(...) |
Nome Personalizado + Público | Mesmo que storeAs |
(1) ▶ Exemplo: Upload de Imagem de Produto do ShopMetrics
// app/Http/Controllers/ProductImageController.php
class ProductImageController extends Controller
{
public function store(Request $request, Product $product): JsonResponse
{
$validated = $request->validate([
'image' => 'required|image|mimes:jpeg,png,webp|max:5120',
'is_primary' => 'sometimes|boolean',
]);
$path = $request->file('image')->store(
"products/{$product->id}/images",
's3',
);
$image = $product->images()->create([
'path' => $path,
'is_primary' => $validated['is_primary'] ?? false,
'mime_type' => $request->file('image')->getMimeType(),
'size' => $request->file('image')->getSize(),
]);
return response()->json([
'message' => 'Image uploaded.',
'data' => [
'id' => $image->id,
'url' => Storage::disk('s3')->url($path),
],
], 201);
}
public function destroy(Product $product, Image $image): Response
{
Storage::disk('s3')->delete($image->path);
$image->delete();
return response()->noContent();
}
}
Saída:
// Execução bem-sucedida
5. Links Simbólicos e Disco Público
(1) Criar um link simbólico
php artisan storage:link
# Cria: public/storage → storage/app/public
(2) Como os Links Simbólicos Funcionam
public/
├── index.php
├── storage/ → ../../storage/app/public/ (link simbólico)
│ └── shops/logos/abc123.jpg (acessível via web)
storage/
└── app/
└── public/ (localização real do arquivo)
└── shops/logos/abc123.jpg
| Caminho | Propósito | Acesso Web |
|---|---|---|
storage/app/public/ |
Armazenamento de Arquivos Públicos | ✅ Via Links Simbólicos |
storage/app/ |
Armazenamento de Arquivos Privados | ❌ Não via Web |
public/ |
Diretório Raiz Web | ✅ Acesso Direto |
(1) ▶ Exemplo: Download de Arquivos Privados
// Arquivo privado — não acessível via web, deve passar pelo controller
Route::get('/reports/{report}/download', [ReportController::class, 'download'])
->middleware('auth');
class ReportController extends Controller
{
public function download(Report $report): StreamedResponse
{
$this->authorize('view', $report);
if (!Storage::disk('local')->exists($report->file_path)) {
abort(404, 'Report file not found.');
}
return Storage::disk('local')->download(
$report->file_path,
"report-{$report->id}.pdf",
);
}
}
Saída:
// Execução bem-sucedida
6. Armazenamento em Nuvem S3 e URLs Pré-assinadas
(1) Integração S3
composer require league/flysystem-aws-s3-v3:"^3.0"
(2) URL Pré-assinada
URLs pré-assinadas permitem que clientes façam upload e download de arquivos S3 diretamente, sem passar pelo servidor—economizando banda e reduzindo a carga do servidor.
// Gerar uma URL temporária de upload (cliente faz upload diretamente para o S3)
$uploadUrl = Storage::disk('s3')->temporaryUploadUrl(
"products/{$product->id}/images/" . $request->filename,
now()->addMinutes(30),
['ContentType' => $request->mime_type],
);
// Gerar uma URL temporária de download
$downloadUrl = Storage::disk('s3')->temporaryUrl(
$image->path,
now()->addMinutes(15),
);
(3) S3 vs. On-Premises
| Dimensão | Disco Público | S3 |
|---|---|---|
| Armazenamento de Arquivos | Disco do Servidor | AWS S3 |
| Acesso Web | Links Simbólicos | URL/CDN |
| Expansão | Limitado ao Disco | Ilimitado |
| Disponibilidade | Dependência do Servidor | 99.999999999% |
| Integração CDN | Requer Configuração | CloudFront |
| URL Pré-assinada | ❌ | ✅ |
| Custo | Gratuito | Pagamento por uso |
(1) ▶ Exemplo: Upload Pré-assinado no S3 do ShopMetrics
// app/Http/Controllers/Api/FileUploadController.php
class FileUploadController extends Controller
{
public function presign(Request $request): JsonResponse
{
$validated = $request->validate([
'filename' => 'required|string',
'mime_type' => 'required|string|in:image/jpeg,image/png,image/webp',
'size' => 'required|integer|max:5120',
]);
$path = 'uploads/' . auth()->id() . '/' . Str::uuid() . '/' . $validated['filename'];
$uploadUrl = Storage::disk('s3')->temporaryUploadUrl(
$path,
now()->addMinutes(30),
['ContentType' => $validated['mime_type']],
);
return response()->json([
'upload_url' => $uploadUrl,
'path' => $path,
'expires_in' => 1800,
]);
}
public function confirm(Request $request): JsonResponse
{
$validated = $request->validate([
'path' => 'required|string',
'attachable_type' => 'required|string',
'attachable_id' => 'required|integer',
]);
$url = Storage::disk('s3')->url($validated['path']);
return response()->json([
'url' => $url,
'path' => $validated['path'],
]);
}
}
Saída:
// Execução bem-sucedida
7. Operações de Arquivo
(1) Operações Comuns
// Ler
$content = Storage::disk('s3')->get('shops/logos/abc.jpg');
$exists = Storage::disk('s3')->exists('shops/logos/abc.jpg');
$missing = Storage::disk('s3')->missing('shops/logos/abc.jpg');
// Escrever
Storage::disk('s3')->put('reports/summary.csv', $csvContent);
Storage::disk('s3')->putFileAs('avatars', $uploadedFile, 'profile.jpg');
// Copiar / Mover
Storage::disk('s3')->copy('old/path.jpg', 'new/path.jpg');
Storage::disk('s3')->move('temp/file.jpg', 'permanent/file.jpg');
// Excluir
Storage::disk('s3')->delete('shops/logos/abc.jpg');
Storage::disk('s3')->delete(['file1.jpg', 'file2.jpg']);
// Visibilidade
Storage::disk('s3')->setVisibility('file.jpg', 'public');
Storage::disk('s3')->setVisibility('file.jpg', 'private');
$visibility = Storage::disk('s3')->getVisibility('file.jpg');
// Diretório
$files = Storage::disk('s3')->files('shops/logos');
$allFiles = Storage::disk('s3')->allFiles('shops');
$directories = Storage::disk('s3')->directories('shops');
Storage::disk('s3')->makeDirectory('shops/new-dir');
Storage::disk('s3')->deleteDirectory('shops/old-dir');
// Metadados do arquivo
$size = Storage::disk('s3')->size('file.jpg');
$modified = Storage::disk('s3')->lastModified('file.jpg');
$path = Storage::disk('s3')->path('file.jpg');
(1) ▶ Exemplo: Geração e Armazenamento de Relatórios do ShopMetrics
// app/Services/ReportService.php
class ReportService
{
public function generateOrderReport(Tenant $tenant, string $format = 'csv'): string
{
$orders = Order::where('tenant_id', $tenant->id)
->with(['shop', 'items.product'])
->completed()
->latest()
->get();
$csv = "Order Number,Shop,Customer,Total,Status,Date\n";
foreach ($orders as $order) {
$csv .= implode(',', [
$order->order_number,
$order->shop->name,
$order->user->name,
$order->total,
$order->status,
$order->created_at->format('Y-m-d'),
]) . "\n";
}
$path = "reports/{$tenant->slug}/orders-" . now()->format('Y-m-d-His') . ".csv";
Storage::disk('s3')->put($path, $csv);
return $path;
}
public function getDownloadUrl(string $path, int $expiresMinutes = 15): string
{
return Storage::disk('s3')->temporaryUrl($path, now()->addMinutes($expiresMinutes));
}
public function cleanupOldReports(Tenant $tenant): int
{
$cutoff = now()->subDays(90)->format('Y-m-d');
$files = Storage::disk('s3')->allFiles("reports/{$tenant->slug}");
$deleted = 0;
foreach ($files as $file) {
if (str_contains($file, $cutoff) || Storage::disk('s3')->lastModified($file) < now()->subDays(90)->timestamp) {
Storage::disk('s3')->delete($file);
$deleted++;
}
}
return $deleted;
}
}
Saída:
// Execução bem-sucedida
8. Exemplo Abrangente: Sistema de Upload de Arquivos do ShopMetrics
// ============================================
// Abrangente: Sistema de Upload de Arquivos do ShopMetrics
// Cobre: upload, S3, URLs pré-assinadas, limpeza, streaming
// ============================================
// app/Http/Controllers/Api/MediaController.php
class MediaController extends Controller
{
public function upload(Request $request): JsonResponse
{
$validated = $request->validate([
'file' => 'required|file|max:10240',
'collection' => 'required|in:logos,products,reports',
'attachable_type' => 'sometimes|string',
'attachable_id' => 'sometimes|integer',
]);
$file = $request->file('file');
$tenantId = tenant()->id;
$path = $validated['collection'] . "/{$tenantId}/" . Str::uuid() . '.' . $file->extension();
$stored = Storage::disk('s3')->put($path, $file->getContent(), 'public');
if (!$stored) {
return response()->json(['message' => 'Upload failed.'], 500);
}
$media = Media::create([
'tenant_id' => $tenantId,
'path' => $path,
'filename' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'collection' => $validated['collection'],
]);
return response()->json([
'message' => 'File uploaded.',
'data' => [
'id' => $media->id,
'url' => Storage::disk('s3')->url($path),
'filename' => $media->filename,
'size' => $media->size,
],
], 201);
}
public function download(Media $media): StreamedResponse
{
$this->authorize('view', $media);
if ($media->isPublic()) {
return redirect(Storage::disk('s3')->url($media->path));
}
return Storage::disk('s3')->download($media->path, $media->filename);
}
public function temporaryUrl(Media $media): JsonResponse
{
$this->authorize('view', $media);
$url = Storage::disk('s3')->temporaryUrl(
$media->path,
now()->addMinutes(15),
);
return response()->json(['url' => $url, 'expires_in' => 900]);
}
public function destroy(Media $media): Response
{
$this->authorize('delete', $media);
Storage::disk('s3')->delete($media->path);
$media->delete();
return response()->noContent();
}
}
❓ Perguntas Frequentes
storage:link no Windows?php artisan storage:link do Laravel cria automaticamente links simbólicos no Windows, mas são necessários privilégios de administrador. Se falhar, crie manualmente: mklink /D public\storage storage\app\public.upload_max_filesize e post_max_size.AWS_URL pelo seu nome de domínio CloudFront. Todas as URLs retornadas por Storage::url() serão automaticamente roteadas pelo CDN, acelerando o acesso de todo o mundo.deleting no boot() do modelo para excluir arquivos associados; execute comandos Artisan periodicamente para limpar arquivos órfãos; use políticas de ciclo de vida do S3 para expirar automaticamente arquivos antigos.FILESYSTEM_DISK do .env sem modificar o código.📖 Resumo
- A camada de abstração Storage fornece uma API unificada; para trocar drivers, basta modificar o arquivo .env
- Discos públicos requerem um link simbólico
storage:linkpara serem acessíveis via web - Upload de arquivo: Validação → store() → Salvar caminho no BD → Gerar URL
- S3 é adequado para ambientes de produção: alta durabilidade, escalabilidade ilimitada e integração com CDN
- URLs pré-assinadas permitem que clientes façam upload diretamente para o S3, economizando banda do servidor
- Arquivos privados são baixados via controller, enquanto arquivos públicos podem ser acessados diretamente via URL
📝 Exercícios
-
Exercício Básico (⭐): Configure o ShopMetrics para usar armazenamento em disco público, implemente uploads de imagens de produtos (validação, armazenamento e exibição), e exiba as imagens carregadas em uma página Blade.
-
Exercício Avançado (⭐⭐): Troque para armazenamento S3 e implemente uploads via URLs pré-assinadas—o front-end obtém a URL pré-assinada e faz upload diretamente para o S3, e o back-end cria um registro Media após a verificação.
-
Desafio (⭐⭐⭐): Implemente o gerenciamento de ciclo de vida de arquivos S3—defina uma tag (tenant_id) no upload, escreva um comando Artisan para limpar arquivos de relatório com mais de 90 dias por tenant, e use a API de exclusão em lote do S3 para otimizar o desempenho.



