404 Not Found

404 Not Found


nginx

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


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.

PHP
// 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

PHP
// 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

BASH
# .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:

TEXT
# Comando executado com sucesso

4. O Processo Completo de Upload de Arquivo

(1) Validação → Armazenamento → Geração de URL

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

BASH
php artisan storage:link
# Cria: public/storage → storage/app/public
TEXT
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

PHP
// 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:

TEXT
// Execução bem-sucedida

6. Armazenamento em Nuvem S3 e URLs Pré-assinadas

(1) Integração S3

BASH
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.

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

7. Operações de Arquivo

(1) Operações Comuns

PHP
// 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

PHP
// 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:

TEXT
// Execução bem-sucedida

8. Exemplo Abrangente: Sistema de Upload de Arquivos do ShopMetrics

PHP
// ============================================
// 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

P Como criar storage:link no Windows?
R O 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.
P Quando devo usar uma URL pré-assinada?
R Use uma URL pré-assinada quando um cliente (navegador/dispositivo móvel) precisa fazer upload de um arquivo diretamente para o S3 para evitar rotear o arquivo pelo servidor. Arquivos grandes (>10 MB) devem ser carregados usando uma URL pré-assinada.
P Como lidar com o upload de arquivos grandes?
R Use uma URL pré-assinada para permitir que clientes façam upload diretamente para o S3; ou use uploads multipart, que suportam arquivos na faixa de gigabytes. No lado do PHP, você precisará modificar upload_max_filesize e post_max_size.
P Como o S3 e o CloudFront CDN funcionam juntos?
R Na AWS, configure uma distribuição CloudFront para apontar para um bucket S3, e substitua 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.
P Como limpar arquivos não utilizados?
R Ao excluir um modelo, escute o evento 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.
P Posso usar o disco público e o S3 ao mesmo tempo?
R Sim. Use o disco público para desenvolvimento (gratuito e rápido) e S3 para produção (confiável e escalável). Você pode trocar o disco padrão usando FILESYSTEM_DISK do .env sem modificar o código.

📖 Resumo


📝 Exercícios

  1. 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.

  2. 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.

  3. 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%