---
id: SVC-STORAGE
tipo: servico
onda: 2
status: concluido
unidades: [SVC-STORAGE]
fontes:
  - V2/Services/Storage/
depende_de: [CORE-CMS]
consumidores: [SVC-MEDIA, SVC-GALLERY, SVC-CONTENTRICHEDITOR, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, MOD-CONEXOES-ASSOCIADOS, MOD-LAYOUTS-BASICPAGE]
criticidade: alta
dono: SvcStorageMedia
---

# Serviço Storage

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Grava, lê, apaga e resolve URL de arquivos em um disco local ou em S3, escolhendo o adaptador no construtor a partir de `filesystems.media_disk`. |
| Entrada | `StorageService::put/get/delete/exists/url/temporaryUrl/download/deleteSafe/ensureLocalPath`, `path` relativo ao disco, opções (`visibility`) e `UploadedFile`/conteúdo bruto. |
| Saída | Booleano de operação, `string` de conteúdo, URL pública ou assinada, `BinaryFileResponse`/`RedirectResponse`, caminho absoluto local. |
| Efeitos colaterais | Escrita/leitura/remoção no disco `public` ou no bucket S3; `Log::error`/`Log::warning`; download para `/tmp/lotep_s3_downloads`; nenhuma linha em banco, cache, auditoria ou índice. |
| Particularidade central | O adaptador é escolhido por `media_disk === 's3'` **e** credenciais preenchidas; sem credenciais, `LocalAdapter('s3')` é usado — não há fallback automático para `public` na escolha. |
| Como se integra | `app(StorageService::class)` (autowire, sem singleton) ou helper global `v2_storage_url($path)`; `put()` recebe caminho completo relativo, não gera nome. |
| Se removido, quebra | `v2_storage_url()` (`V2/Core/Helpers/helpers.php:259-267`), `UploadService` (`V2/Core/Services/UploadService.php:22-25`), `MediaInputer` (`V2/Services/Media/Services/MediaInputer.php:235-274`), `GalleryService` e todos os otimizadores de mídia. |
| Superfície pública | `V2\Services\Storage\StorageService`; `V2\Services\Storage\Contracts\StorageInterface`; `Adapters\LocalAdapter`; `Adapters\S3Adapter`; `Exceptions\StorageException`. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

Recebe uma chave relativa (ex.: `gallery/noticias/media_66c1_1712345678.jpg`) e um conteúdo, e grava exatamente nessa chave no disco resolvido pelo adaptador (`V2/Services/Storage/StorageService.php:69-80`; `V2/Services/Storage/Adapters/LocalAdapter.php:32-44`). Em leitura, devolve o conteúdo bruto ou `null`; em URL, concatena a base configurada do disco com a chave; em `ensureLocalPath`, devolve um caminho absoluto — no S3, baixando o objeto para `/tmp/lotep_s3_downloads` (`V2/Services/Storage/Adapters/S3Adapter.php:157-185`).

O serviço **não** cria registro de mídia: apenas move bytes. A catalogação é do `SVC-GALLERY`, que insere em `v2_gallery_media` depois de chamar `MediaInputer` (`V2/Services/Gallery/Services/GalleryService.php:58-75`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Escolhe `S3Adapter` somente com `media_disk='s3'` e key/secret/bucket não vazios | Container/instância, sem I/O | `resolveAdapter` | `V2/Services/Storage/StorageService.php:33-43,50-57` |
| Grava conteúdo no disco local configurado | `storage/app/<root>/<path>` | `LocalAdapter::put` | `V2/Services/Storage/Adapters/LocalAdapter.php:32-44` |
| Grava no bucket S3; em falha tenta o disco `public` | S3 + `storage/app/public` | `S3Adapter::put` | `V2/Services/Storage/Adapters/S3Adapter.php:22-37` |
| Remove o arquivo; falha vira `false` + warning | disco | `StorageService::delete` | `V2/Services/Storage/StorageService.php:100-113` |
| `deleteSafe` não falha se o arquivo já não existe | disco | `deleteSafe` | `V2/Services/Storage/StorageService.php:164-179` |
| Gera URL concatenando `filesystems.disks.<disk>.url` | string, sem I/O | `LocalAdapter::url` | `V2/Services/Storage/Adapters/LocalAdapter.php:85-96` |
| Gera URL temporária apenas no S3; no local lança | S3 / exceção | `temporaryUrl` | `V2/Services/Storage/Adapters/S3Adapter.php:114-126`; `V2/Services/Storage/Adapters/LocalAdapter.php:103-106` |
| Download redireciona para URL S3 de 1 h ou serve arquivo local | HTTP response | `download` | `V2/Services/Storage/Adapters/S3Adapter.php:130-153`; `V2/Services/Storage/Adapters/LocalAdapter.php:111-124` |
| Baixa objeto S3 para arquivo temporário local | `/tmp/lotep_s3_downloads/<path com / trocado por _>` | `S3Adapter::ensureLocalPath` | `V2/Services/Storage/Adapters/S3Adapter.php:157-185` |

### 1.3 Contrato observável

| Operação | Entrada válida | Resultado | Estado inválido |
|---|---|---|---|
| `put($file, string $path, array $options = [])` | conteúdo string/stream/`UploadedFile` (local) e chave | `true`/resultado do driver; `UploadedFile` no local é convertido em conteúdo antes de gravar | falha lança `StorageException` no local; no S3 tenta `public` e, se também falhar, lança |
| `get(string $path)` | chave | `string` ou `null` se `exists()` falso | se `exists()` lançar exceção, tenta a cópia no `public`; se o S3 responder `false` (objeto ausente), devolve `null` sem consultar o `public` |
| `delete(string $path)` | chave | `true` se ausente ou removido; `false` em exceção | nunca lança no `StorageService` |
| `deleteSafe(string $path)` | chave | `true` se ausente; senão resultado de `delete` | falha loga e devolve `false` |
| `exists(string $path)` | chave | `bool` | falha S3 (exceção) tenta `public`; `false` do S3 não consulta o `public` |
| `url(string $path)` | chave | URL pública montada; local parte de `filesystems.disks.<disk>.url` | não valida existência; URL vazia do disco produz `/<path>` |
| `temporaryUrl(string $path, $expiration)` | chave + `DateTimeInterface`/int | URL assinada S3 | local lança `"Disco '<disk>' nao disponivel."` |
| `download(string $path)` | chave | `RedirectResponse` (S3) ou `BinaryFileResponse` (local) | local ausente lança `StorageException::uploadFailed(...)` com motivo `"Arquivo nao encontrado para download."` |
| `ensureLocalPath($path)` | chave | caminho absoluto local | S3 retorna o caminho mesmo se o download falhar (só loga `error`) |
| `getAdapter()` / `setAdapter(StorageInterface $adapter)` | — / instância | adaptador ativo / troca em runtime | `setAdapter` não valida compatibilidade com a config |

Mensagens literais de exceção (`V2/Services/Storage/Exceptions/StorageException.php:13-38`): `"Falha ao fazer upload para: {path}. Motivo: {reason}"`, `"Falha ao deletar: {path}. Motivo: {reason}"`, `"Disco '{disk}' nao disponivel."`.

### 1.4 O que o serviço deliberadamente NÃO faz

- Não valida MIME, extensão, tamanho ou conteúdo (`V2/Services/Storage/StorageService.php:69-80`); validação é do chamador, como `MediaInputer::validateMimeType` (`V2/Services/Media/Services/MediaInputer.php:100-122`).
- Não gera nome de arquivo, pasta por data, hash ou slug; o `path` é responsabilidade do chamador.
- Não cria, atualiza ou apaga linhas em `v2_gallery_media` nem em qualquer tabela.
- Não audita (`ActivityLogger`) nem invalida cache/CDN.
- Não garante atomicidade entre original e variantes; cada `put` é uma operação independente.
- Não aplica `visibility` no disco local: `LocalAdapter::put` ignora `$options` (`V2/Services/Storage/Adapters/LocalAdapter.php:32-44`).
- Não impede path traversal nem normaliza `..`/barras duplicadas no `path`.
- Não remove arquivos antigos ao sobrescrever a mesma chave.

## 2. Abordagem

### 2.1 Estratégia e por quê

Adapter pattern com um contrato único (`StorageInterface`) e duas implementações. O objetivo observável é permitir trocar local por S3 sem alterar chamadores de mídia; o `StorageService` funciona como fachada e centraliza log/relançamento (`V2/Services/Storage/StorageService.php:12-25`). O `disk` recebido por `MediaInputer`/`UploadService` é ignorado na prática: quem decide o disco é `resolveAdapter()` no construtor.

```mermaid
flowchart TD
    A[new StorageService] --> B{media_disk == 's3'<br/>e key+secret+bucket?}
    B -- sim --> C[S3Adapter<br/>disk = 's3']
    B -- não --> D[LocalAdapter<br/>disk = media_disk]
    C --> E[put/get/exists/url/temporaryUrl/download]
    D --> E
    E --> F[Storage::disk(driver)]
```

### 2.2 Mecânica passo a passo

1. `new StorageService()` chama `resolveAdapter()` uma única vez (`V2/Services/Storage/StorageService.php:23-26`); não há binding singleton, então cada injeção resolve de novo.
2. `$default = config('filesystems.default', 'public')` e `$mediaDisk = config('filesystems.media_disk', $default)` (`:35-36`).
3. Se `$mediaDisk === 's3'` e `hasAwsCredentials()` (key, secret e bucket não vazios) → `S3Adapter` (`:38-40,50-57`).
4. Caso contrário → `LocalAdapter($mediaDisk)` (`:42`), inclusive se `$mediaDisk === 's3'` sem credenciais.
5. `put()` delega ao adaptador; exceções são logadas como `[StorageService] Falha no upload: ...` e relançadas (`:69-80`).
6. `delete()` engole a exceção, loga `[StorageService] Falha ao deletar (nao critico): ...` e devolve `false` (`:100-113`).
7. `deleteSafe()` verifica `exists()` antes; se ausente devolve `true`; falha loga `deleteSafe falhou (arquivo pode ficar orfao)` e devolve `false` (`:164-179`).
8. `ensureLocalPath()` usa `S3Adapter::ensureLocalPath` ou `LocalAdapter::path`; qualquer outro adaptador devolve o `path` cru (`:187-198`).

### 2.3 Trechos de código pivô

```php
// V2/Services/Storage/StorageService.php:33-43 — a escolha do adaptador é feita uma vez,
// no construtor. Sem credenciais, 's3' vira LocalAdapter('s3'), não LocalAdapter('public').
protected function resolveAdapter()
{
    $default = config('filesystems.default', 'public');
    $mediaDisk = config('filesystems.media_disk', $default);

    if ($mediaDisk === 's3' && $this->hasAwsCredentials()) {
        return new S3Adapter();
    }

    return new LocalAdapter($mediaDisk);
}
```

O trecho prova que a configuração "S3" só é efetiva com as três credenciais; caso contrário o nome do disco é repassado ao adaptador local, que usa `Storage::disk($this->disk)`. Não existe fallback silencioso para `public` na escolha.

```php
// V2/Services/Storage/StorageService.php:50-57 — credenciais lidas de filesystems.disks.s3.*
protected function hasAwsCredentials()
{
    $key = config('filesystems.disks.s3.key', '');
    $secret = config('filesystems.disks.s3.secret', '');
    $bucket = config('filesystems.disks.s3.bucket', '');

    return !empty($key) && !empty($secret) && !empty($bucket);
}
```

A checagem é puramente de presença; não testa conectividade, permissão do bucket, região nem validade da chave. Credencial errada mantém `S3Adapter` ativo e só falha na primeira operação.

```php
// V2/Services/Storage/Adapters/S3Adapter.php:22-37 — S3 com fallback para o disco 'public'
public function put($file, string $path, array $options = [])
{
    try {
        return Storage::disk($this->disk)->put($path, $file, $options);
    } catch (\Exception $e) {
        Log::warning('[S3Adapter] Falha no upload S3, tentando fallback local: ' . $e->getMessage(), [
            'path' => $path
        ]);

        try {
            return Storage::disk('public')->put($path, $file);
        } catch (\Exception $localException) {
            throw StorageException::uploadFailed($path, 'S3 e fallback local falharam: ' . $localException->getMessage());
        }
    }
}
```

O fallback grava o mesmo `path` no disco `public`, mas **não** altera o adaptador ativo: `url()` continua gerando URL S3, e `getDisk()` continua devolvendo `s3`. O chamador que persiste o `disk` retornado grava um valor que não corresponde ao local onde os bytes estão.

```php
// V2/Services/Storage/Adapters/LocalAdapter.php:32-44 — UploadedFile é convertido em
// conteúdo para evitar que o Laravel trate $path como diretório (putFile/hashName).
public function put($file, string $path, array $options = [])
{
    try {
        if ($file instanceof UploadedFile) {
            $file = file_get_contents($file->getRealPath());
        }
        return Storage::disk($this->disk)->put($path, $file);
    } catch (\Exception $e) {
        throw StorageException::uploadFailed($path, $e->getMessage());
    }
}
```

No Laravel 5.5, `FilesystemAdapter::put()` com `UploadedFile` chama `putFile($path, $file)`, que grava em `$path/<hashName>` (`vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php:164-194`). O `LocalAdapter` contorna isso; o `S3Adapter` **não** tem a mesma guarda e, se receber `UploadedFile`, grava em subpasta com nome hash (`V2/Services/Storage/Adapters/S3Adapter.php:22-25`).

```php
// V2/Services/Storage/Adapters/LocalAdapter.php:85-96 — URL local montada da config,
// com colapso de barras duplicadas no path e sem validar existência.
public function url(string $path)
{
    $rawUrl = config("filesystems.disks.{$this->disk}.url", '');
    $parts = explode('://', $rawUrl, 2);
    if (count($parts) === 2) {
        $rawUrl = $parts[0] . '://' . preg_replace('#/{2,}#', '/', $parts[1]);
    }
    $diskUrl = rtrim($rawUrl, '/');
    return $diskUrl . '/' . ltrim($path, '/');
}
```

Com `'url' => '/storage'` (`config/filesystems.php:59-66`), `url('gallery/a.png')` devolve `/storage/gallery/a.png`: URL relativa, same-origin, independente de `APP_URL`. Se o disco não tiver `url`, a base é `''` e o resultado vira `/gallery/a.png` (não há erro).

```php
// V2/Services/Storage/Adapters/S3Adapter.php:157-185 — materializa o objeto S3 em /tmp
// e devolve o caminho mesmo quando o download falha.
public function ensureLocalPath($path)
{
    $tempDir = sys_get_temp_dir() . '/lotep_s3_downloads';
    // ... cria o diretório e monta $localPath trocando '/' por '_'
    if (file_exists($localPath)) {
        return $localPath;
    }
    try {
        $content = Storage::disk($this->disk)->get($path);
        file_put_contents($localPath, $content);
        return $localPath;
    } catch (\Exception $e) {
        Log::error('[S3Adapter] Falha ao baixar arquivo para path local: ' . $e->getMessage(), [
            'path' => $path
        ]);
        return $localPath;
    }
}
```

O cache é permanente (não há TTL/limpeza) e o nome achata a chave trocando `/` por `_`: `a/b_c.png` e `a_b/c.png` colidem. Em erro, devolve um caminho inexistente; o chamador só descobre ao abrir o arquivo (ex.: `new Imagick($path)` em `ImageOptimizer`).

### 2.4 Algoritmos e custos

- `get()` faz `exists()` e depois `get()`: no S3 são duas chamadas (HEAD + GET); no local são duas verificação/leitura.
- `delete()` também faz `exists()` antes; `deleteSafe()` faz `exists()` + `delete()` (que repete `exists()` no `LocalAdapter`).
- `put()` materializa o conteúdo inteiro em memória quando chamado pelos serviços (`file_get_contents` em `UploadService.php:41`, `MediaInputer.php:245,261`). Não há streaming.
- `ensureLocalPath()` baixa o objeto inteiro para disco; sem invalidação, um arquivo atualizado no S3 continua sendo servido pela cópia temporária antiga.
- Não há concorrência coordenada: dois `put` no mesmo `path` sobrescrevem sem lock; dois `ensureLocalPath` simultâneos podem escrever o mesmo temporário.

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

É a única camada que traduz uma chave relativa em URL e em caminho físico para **todo** o V2. `v2_storage_url()` é o único helper sancionado (`V2/Core/Helpers/helpers.php:251-267`), e `MediaInputer`, `GalleryService`, `UploadService` e os seeders dependem dele. Nenhum outro serviço resolve `filesystems.disks.*` para URL de mídia.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-STORAGE-01` | O adaptador é resolvido **uma vez**, no construtor; `config()` alterado depois não troca a instância. | Testes que mudam `filesystems.media_disk` precisam nova instância/`setAdapter`. | `V2/Services/Storage/StorageService.php:23-26` |
| `RN-STORAGE-02` | `S3Adapter` exige `media_disk === 's3'` **e** key+secret+bucket. | `media_disk='s3'` sem credenciais usa `LocalAdapter('s3')`, não `public`. | `V2/Services/Storage/StorageService.php:33-43,50-57` |
| `RN-STORAGE-03` | `path` é sempre relativo ao disco; não inclui `/storage` nem `s3://`. | URL = base do disco + `/` + path. | `V2/Services/Storage/Adapters/LocalAdapter.php:85-96`; `V2/Services/Storage/Adapters/S3Adapter.php:106-109` |
| `RN-STORAGE-04` | `put` local ignora `$options`; `visibility` só vale no S3. | `['visibility'=>'private']` não torna o arquivo privado no disco local. | `V2/Services/Storage/Adapters/LocalAdapter.php:32-44` |
| `RN-STORAGE-05` | `temporaryUrl` no local sempre falha. | Não há URL assinada local; acesso privado local exige `download()` via controller. | `V2/Services/Storage/Adapters/LocalAdapter.php:103-106` |
| `RN-STORAGE-06` | `deleteSafe` é idempotente para ausente. | Pode ser chamado após falha parcial sem erro. | `V2/Services/Storage/StorageService.php:164-179` |
| `RN-STORAGE-07` | `url()` não checa existência nem adaptador efetivo de gravação. | Após fallback S3→local, a URL ainda aponta para S3. | `V2/Services/Storage/Adapters/S3Adapter.php:22-37,106-109` |
| `RN-STORAGE-08` | `UploadedFile` é suportado no local, mas não é normalizado no S3. | Mesma chamada pode gerar chave exata no local e `path/<hash>` no S3. | `V2/Services/Storage/Adapters/LocalAdapter.php:32-44`; `V2/Services/Storage/Adapters/S3Adapter.php:22-25` |
| `RN-STORAGE-09` | `download` S3 usa `temporaryUrl(now()->addHour())`; não há TTL configurável. | Link de download expira em 1 h. | `V2/Services/Storage/Adapters/S3Adapter.php:130-135` |

### 3.3 Casos-limite

- **Disco indisponível:** `temporaryUrl` local lança `StorageException`; `url` não lança e devolve uma URL possivelmente inválida.
- **Arquivo ausente:** `get` devolve `null`; `download` local lança; `delete` devolve `true`; `deleteSafe` devolve `true`.
- **S3 fora do ar:** `put`, `get`, `exists` caem para `public` **quando a chamada ao S3 lança exceção**; se o S3 responde normalmente que o objeto não existe, o fallback local não é consultado (`V2/Services/Storage/Adapters/S3Adapter.php:42-63,86-103`). `delete` devolve `false` em exceção; `url` não cai; `download` tenta baixar e pode lançar.
- **S3 sem credenciais:** `LocalAdapter('s3')` chama `Storage::disk('s3')`; sem driver configurado corretamente, a operação falha em vez de usar `public`.
- **Path vazio:** `v2_storage_url('')` devolve `''` (`V2/Core/Helpers/helpers.php:261-263`); chamar `LocalAdapter::url('')` devolve a base do disco com `/` final.
- **Path com `..` ou barras iniciais:** `ltrim` remove barras iniciais na URL, mas `put/get` repassam o valor cru ao driver.
- **Objeto S3 grande:** `ensureLocalPath` e `get` carregam tudo em memória/disco; não há streaming.
- **Sobrescrita:** não há versionamento; a chave antiga é substituída, variantes antigas permanecem.

### 3.4 Armadilhas de reimplementação

- **Tratar `getDisk()` como "onde o arquivo está".** No fallback interno do `S3Adapter`, o retorno continua `s3` (`V2/Services/Storage/Adapters/S3Adapter.php:22-37,190-193`). Use `metadata.fallback_local` do `MediaInputer` apenas quando o `put` lança, não quando o adaptador faz fallback.
- **Passar `UploadedFile` direto ao S3.** O `S3Adapter` não converte; o Laravel grava em subpasta com `hashName`. Converta para conteúdo/stream antes, como fazem `UploadService` e `MediaInputer`.
- **Assumir que `visibility` é aplicada localmente.** O disco `public` é servido por `/storage`; um arquivo "private" continua acessível se estiver sob `storage/app/public`.
- **Usar `Storage::url()` em Blade.** O helper `v2_storage_url()` resolve o adaptador e a base correta; `Storage::url()` ignora o `StorageService` e o fallback (`V2/Core/Helpers/helpers.php:251-267`).
- **Trocar `media_disk` em runtime e reutilizar instância.** `resolveAdapter` já rodou; use `setAdapter(new LocalAdapter('public'))` (`V2/Services/Storage/StorageService.php:226-229`).
- **Esquecer que o `path` é a chave final.** `put($file, 'pasta')` grava na chave `pasta` (ou `pasta/<hash>` se `UploadedFile` e S3), não em `pasta/<nome original>`.
- **Apagar depois de um fallback S3→local.** `S3Adapter::delete()` só remove do S3; se o `put` caiu para `public`, a cópia local permanece órfã. `exists()` só encontra a cópia local se a consulta ao S3 lançar exceção; com S3 saudável e objeto ausente, devolve `false` e a cópia fica invisível.
- **Assumir que o fallback cobre leitura.** O fallback de `get`/`exists` é acionado por **exceção**, não por `false`; um objeto gravado apenas no `public` após falha de `put` não é lido enquanto o S3 responder normalmente.

### 3.5 Dívidas e divergências

- **README divergente da config real.** O README instrui `FILESYSTEMS_MEDIA_DISK` e `AWS_KEY`/`AWS_SECRET` (`V2/Services/Storage/README.md:107-133`), mas o código lê `MEDIA_DISK` (`config/filesystems.php:24`) e `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` com fallback para `AWS_KEY`/`AWS_SECRET` (`config/filesystems.php:68-74`). `FILESYSTEMS_MEDIA_DISK` não é lido por ninguém.
- **Fallback S3 "mentiroso".** O README promete fallback local (`V2/Services/Storage/README.md:135-142`), mas o adaptador ativo, `getDisk()` e `url()` continuam S3.
- **`get()` duplica checagem de existência.** `StorageService::get` delega e o adaptador repete `exists`; no S3 é HEAD+GET.
- **`ensureLocalPath` engole erro e nunca limpa `/tmp/lotep_s3_downloads`.**
- **`setAdapter` público sem uso no código de produção**, apenas testes/fallback de mídia.
- **Sem testes no diretório da unidade**; os testes de Storage existem fora do manifest (`tests/Unit/V2/Services/Storage/StorageServiceTest.php`), portanto não fazem parte desta unidade.

### 3.6 Público vs privado

| Disco/uso | Onde fica | URL | Acesso privado |
|---|---|---|---|
| `public` (default de mídia) | `storage/app/public/<path>` (`config/filesystems.php:59-66`) | `/storage/<path>`, relativa e same-origin | impossível pelo serviço: `temporaryUrl` lança; `visibility` é ignorada |
| `local` | `storage/app/<path>` (`config/filesystems.php:54-57`) | sem `url` configurada: `LocalAdapter::url` devolve `/<path>` | `download()` serve via response, desde que o controller autorize |
| `s3` | bucket/objeto `<key>` | `Storage::disk('s3')->url($path)` (default do driver) | `temporaryUrl($path, $exp)` gera URL assinada; `download()` redireciona com 1 h |
| `s3` com fallback local | objeto pode estar em `public` após falha de `put` | continua a URL S3 (`url` não cai) | o link assinado S3 falha; a cópia local não é considerada |

O serviço não tem noção de "dono" ou permissão: `url()` é público por construção; arquivos privados dependem de `download()`/`temporaryUrl()` e de autorização no chamador. `StorageInterface` só distingue os dois modos por esses métodos (`V2/Services/Storage/Contracts/StorageInterface.php:52-62`).



## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `StorageService::__construct()` | `()` | — | — | autowire |
| `StorageService::put` | `($file, string $path, array $options = [])` | `bool ou string` | `StorageException` | `UploadService`, `MediaInputer`, otimizadores, seeders |
| `StorageService::get` | `(string $path)` | `string` ou `null` | propaga falha do driver local | uso avançado |
| `StorageService::delete` | `(string $path)` | `bool` | nunca (catch) | limpeza |
| `StorageService::deleteSafe` | `(string $path)` | `bool` | nunca (catch) | `PdfVisualOptimizer` |
| `StorageService::exists` | `(string $path)` | `bool` | pode propagar no local | otimizadores, chamadores |
| `StorageService::url` | `(string $path)` | `string` | pode propagar no S3 (`Storage::url`) | `v2_storage_url` |
| `StorageService::temporaryUrl` | `(string $path, $expiration)` | `string` | `StorageException` | acesso privado S3 |
| `StorageService::download` | `(string $path)` | response | `StorageException` | controllers de download |
| `StorageService::ensureLocalPath` | `($path)` | `string` | não lança | `ImageOptimizer`, `PdfVisualOptimizer` |
| `StorageService::getAdapter` | `()` | `StorageInterface` | — | uso avançado/testes |
| `StorageService::getDisk` | `()` | `string` | — | `MediaInputer` |
| `StorageService::setAdapter` | `(StorageInterface $adapter)` | `void` | — | `MediaInputer` fallback, testes |
| `LocalAdapter::path` | `($path)` | `string` | — | `ensureLocalPath` |
| `S3Adapter::ensureLocalPath` | `($path)` | `string` | não lança | `StorageService` |

`StorageInterface` define `put`, `get`, `delete`, `exists`, `url`, `temporaryUrl`, `download` e `getDisk` (`V2/Services/Storage/Contracts/StorageInterface.php:9-72`). `LocalAdapter::path` e `S3Adapter::ensureLocalPath` são métodos extra, fora do contrato.

### 4.2 Componentes Blade e contrato de UI

Não se aplica: o serviço não possui views nem componentes. As views de mídia pertencem ao `SVC-MEDIA` e consomem `v2_storage_url()`.

### 4.3 JS e ciclo de vida

Não se aplica: sem JavaScript.

### 4.4 Ordem de inicialização e registro

1. `config/filesystems.php` precisa estar carregado (`media_disk`, discos `public`/`s3`).
2. `V2ServiceProvider::register()` carrega `V2/Core/Helpers/helpers.php` com `require_once`, habilitando `v2_storage_url` (`V2/Core/Providers/V2ServiceProvider.php:342-351`).
3. `StorageService` é autowire: não há `singleton`/`bind` no provider; cada injeção cria uma instância e resolve o adaptador no construtor.
4. O caminho `/storage` precisa ser servido pelo webserver (symlink `public/storage → storage/app/public` via `storage:link` ou alias equivalente); o código do serviço não cria esse mapeamento `[I]` — a config só declara `'url' => '/storage'`.

### 4.5 Receitas de uso

```php
// 1. Injeção e gravação em caminho explícito (padrão do MediaInputer).
use V2\Services\Storage\StorageService;

public function __construct(StorageService $storage) { $this->storage = $storage; }

$contents = file_get_contents($uploaded->getRealPath());
$this->storage->put($contents, 'noticias/arquivo.pdf', ['visibility' => 'public']);
// efeito: grava a chave exata no disco resolvido; NÃO cria registro em banco.
```

```php
// 2. URL em Blade: sempre pelo helper, nunca Storage::url().
<img src="{{ v2_storage_url($media->file_path) }}" alt="{{ $media->alt_text }}">
// com disco public e url '/storage' => /storage/<file_path>
```

```php
// 3. Download/URL privada.
return $this->storage->download('contratos/2026/arquivo.pdf');       // local: BinaryFileResponse
return $this->storage->temporaryUrl('contratos/2026/arquivo.pdf', now()->addMinutes(10)); // S3
```

```php
// 4. Forçar disco local em um fluxo específico (usado no fallback do MediaInputer).
$local = new \V2\Services\Storage\StorageService();
$local->setAdapter(new \V2\Services\Storage\Adapters\LocalAdapter('public'));
$local->put($contents, 'gallery/modulo/foto.jpg', ['visibility' => 'public']);
```

### 4.6 Anti-uso

- **`Storage::url($path)` em views.** Ignora o `StorageService` e o adaptador resolvido; em S3 com fallback local gera URL que não existe. Use `v2_storage_url()` (`V2/Core/Helpers/helpers.php:251-267`).
- **Passar `UploadedFile` para `StorageService::put()` esperando o nome original.** No S3 o Laravel grava em `path/<hashName>`; no local o adaptador converte e grava na chave. Normalize para conteúdo/stream.
- **Usar `temporaryUrl()` no disco local.** Sempre lança `"Disco 'public' nao disponivel."`; para acesso controlado local use `download()` em um controller autorizado.
- **Assumir que `put(..., ['visibility' => 'private'])` protege o arquivo local.** O disco `public` continua servido por `/storage`.

### 4.7 Custos e limites

- Sem limite de tamanho, timeout ou quota no serviço; o limite efetivo é do PHP/webserver e dos `validate()` dos chamadores (`max:10240` no editor e na galeria, `V2/Services/Gallery/Controllers/GalleryController.php:88-93`).
- `get`/`put`/`ensureLocalPath` carregam o arquivo inteiro em memória; arquivos grandes pressionam `memory_limit`.
- Cada operação S3 é uma chamada de rede; `exists`+`get`/`delete` dobram o custo.
- `ensureLocalPath` ocupa disco em `/tmp` sem expurgo.

### 4.8 StorageService × UploadService (Core)

`UploadService` é um wrapper fino sobre o `StorageService`, não um concorrente: recebe `UploadedFile`, gera nome (`upload_<uniqid>_<time>.<ext>` ou `img_...`) e chama `StorageService::put(file_get_contents(...), $fullPath)`; não valida MIME/extensão/tamanho e não cria registro (`V2/Core/Services/UploadService.php:35-50,63-93`).

| Necessidade | Usar | Por quê |
|---|---|---|
| Gravar/ler/apagar uma chave explícita, resolver URL, URL assinada ou download | `StorageService` | controla `path`, `options`, adaptador e exceções |
| Upload administrativo simples, sem catálogo e sem otimização | `UploadService::handle()` | gera nome e grava bytes; **hoje sem chamadores** no repositório |
| Imagem redimensionada pontualmente | `UploadService::processAndUploadImage()` | resize via Intervention; dependência `Intervention\Image` **ausente** do `composer.json`/`vendor`, então falha com class not found |
| Mídia de conteúdo (imagem/PDF/áudio/planilha) com validação e variantes | `MediaInputer` (`SVC-MEDIA`) | único pipeline que valida e otimiza |
| Mídia catalogada e reutilizável em seletores | `GalleryService` (`SVC-GALLERY`) | persiste `v2_gallery_media` e aplica visibilidade por módulo |

Regra prática: `UploadService` só deve ser usado quando o chamador já validou tudo e quer apenas um nome/gravação; para qualquer mídia pública de conteúdo, use `Media`/`Gallery`. O `$disk` passado ao `UploadService` é ignorado e o retorno reporta `config('filesystems.media_disk')`, não o adaptador efetivo (`V2/Core/Services/UploadService.php:45,88`).



## 5. Rotas e endpoints

Não se aplica: `V2/Services/Storage` não registra rotas (`V2/Core/Providers/V2ServiceProvider.php:168-192` só carrega `Routes/cms.php`, `Routes/web.php` e `Routes/api.php` se existirem; o diretório não tem `Routes/`).

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|
| — | — | — | — | — | — | sem rotas próprias | `V2/Services/Storage/` |

## 6. Dados e migrations

Não se aplica: a unidade não possui `Migrations/`, models ou tabelas próprias. As tabelas consumidas indiretamente são de outros donos:

| Tabela | Dono | Uso pelo Storage |
|---|---|---|
| `v2_gallery_media` | `SVC-GALLERY` | nenhum direto; `file_path` é a chave usada por `MediaInputer`/`v2_storage_url` (`V2/Services/Gallery/Migrations/2026_06_28_000000_create_v2_gallery_media_table.php:23-40`) |
| `v2_basic_page_assets` | `MOD-LAYOUTS-BASICPAGE` | `storage_path` é chave de storage |
| `v2_sle_assets` | `MOD-SUPPORT-STRUCTURALLANDINGEDITOR` | `storage_path` é chave de storage |

## 7. Configuração e permissões

| Chave | Default no código | Env | Efeito | Fonte |
|---|---|---|---|---|
| `filesystems.default` | `public` | `FILESYSTEM_DRIVER` | fallback de `media_disk` | `config/filesystems.php:16`; `V2/Services/Storage/StorageService.php:35` |
| `filesystems.media_disk` | `public` | `MEDIA_DISK` (fallback `FILESYSTEM_DRIVER`) | escolhe `s3` vs disco local | `config/filesystems.php:24` |
| `filesystems.disks.public.url` | `/storage` | — | base de URL local | `config/filesystems.php:59-66` |
| `filesystems.disks.s3.key` | `null` | `AWS_ACCESS_KEY_ID`/`AWS_KEY` | habilita S3 | `config/filesystems.php:68-74`; `StorageService.php:52` |
| `filesystems.disks.s3.secret` | `null` | `AWS_SECRET_ACCESS_KEY`/`AWS_SECRET` | habilita S3 | idem |
| `filesystems.disks.s3.bucket` | `null` | `AWS_BUCKET` | habilita S3 | idem |
| `filesystems.disks.s3.region` | `us-east-1` | `AWS_DEFAULT_REGION`/`AWS_REGION` | endpoint S3 | `config/filesystems.php:72` |

Permissões: não se aplica. O serviço não verifica usuário, papel ou posse do arquivo; autorização é do chamador (controllers/middlewares).

## 8. Observabilidade e erros

| Situação | Log/Exceção | Nível | Fonte |
|---|---|---|---|
| Falha de upload | `[StorageService] Falha no upload: {msg}` + relança | `error` | `V2/Services/Storage/StorageService.php:74-79` |
| Falha ao deletar | `[StorageService] Falha ao deletar (nao critico): {msg}` e `false` | `warning` | `:105-111` |
| Falha em `deleteSafe` | `[StorageService] deleteSafe falhou (arquivo pode ficar orfao): {msg}` | `warning` | `:173-177` |
| Fallback de upload S3 | `[S3Adapter] Falha no upload S3, tentando fallback local: {msg}` | `warning` | `V2/Services/Storage/Adapters/S3Adapter.php:27-29` |
| Fallback de leitura S3 | `[S3Adapter] Falha ao obter arquivo do S3, tentando fallback local: {msg}` | `warning` | `:46-49` |
| Falha de delete S3 | `[S3Adapter] Falha ao deletar do S3 (nao critico): {msg}` e `false` | `warning` | `:70-73` |
| Falha de `exists` S3 | `[S3Adapter] Falha ao verificar existencia no S3, tentando fallback local: {msg}` | `warning` | `:90-93` |
| Falha de URL assinada | `[S3Adapter] Falha ao gerar temporaryUrl: {msg}` + `StorageException` | `warning` | `:119-123` |
| Falha de download temporário | `[S3Adapter] Falha ao gerar temporaryUrl para download, baixando localmente: {msg}` | `warning` | `:137-140` |
| Falha ao baixar para `/tmp` | `[S3Adapter] Falha ao baixar arquivo para path local: {msg}` e devolve caminho | `error` | `:178-184` |

Não há auditoria (`ActivityLogger`) nem métricas. `StorageException` estende `RuntimeException` (`V2/Services/Storage/Exceptions/StorageException.php:5`).

## 9. Consumidores (mapa de integração)

| Consumidor | O que usa | Como usa | Risco de quebra |
|---|---|---|---|
| `v2_storage_url()` | `StorageService::url` | helper global chamado por ~56 arquivos | alto: toda mídia renderizada perde URL |
| `V2\Core\Services\UploadService` | `put` | conteúdo bruto; retorna `media_disk` (não o adaptador efetivo) | médio: `disk` pode mentir em fallback S3 |
| `V2\Services\Media\Services\MediaInputer` | `put`, `getDisk`, `setAdapter` | grava original e fallback local | alto: ingestão de toda mídia |
| `ImageOptimizer`/`PdfVisualOptimizer` | `exists`, `ensureLocalPath`, `put`, `deleteSafe` | lê original, grava variantes/capa | alto: sem variantes WebP |
| `GalleryService` | via `MediaInputer` | persiste `file_path` em `v2_gallery_media` | alto: mídia sem registro |
| `SystemMaintenanceService` | `UploadService`/`StorageService` | logo/favicon do CMS | médio |
| Seeders (`SleHeaderSeeder`, `SleFooterSeeder`, `ProcessesGalleryImages`) | `put` via `GalleryService` | nomes previsíveis | médio |
| `LayoutBaseCmsController` | `MediaInputer` | logos/banners do LSE | médio |

## 10. Lacunas

- `LG-STORAGE-01` — O ambiente de produção (S3 real, bucket, região, CDN) não está versionado; `AWS_*` só aparece em `config/filesystems.php`. Onde confirmar: `.env` de deploy (fora do repositório).
- `LG-STORAGE-02` — Não há configuração de `AWS_URL`/CDN para o disco `s3`; a URL final depende do default do driver Flysystem. Onde confirmar: `vendor/league/flysystem-aws-s3-v3` e variáveis de ambiente do servidor.
- `LG-STORAGE-03` — Não há política de retenção/limpeza de `/tmp/lotep_s3_downloads`; confirmar cron/limpeza externa no servidor.
- `LG-STORAGE-04` — Não há tratamento de path traversal no serviço; confirmar se o driver local do Laravel rejeita `..` na versão instalada.
- `LG-STORAGE-05` — A existência do symlink `public/storage` não é garantida por código; confirmar no deploy.

## 11. Checklist de paridade

- [ ] `PC-STORAGE-01` — Com `filesystems.media_disk='public'`, `put('<path>', $contents)` grava em `storage/app/public/<path>` e `url('<path>')` devolve `/storage/<path>` (`LocalAdapter.php:32-44,85-96`).
- [ ] `PC-STORAGE-02` — Com `media_disk='s3'` e key/secret/bucket preenchidos, o adaptador é `S3Adapter`; sem qualquer uma das três, é `LocalAdapter('s3')` (`StorageService.php:33-43,50-57`).
- [ ] `PC-STORAGE-03` — `put` no S3 com falha tenta `Storage::disk('public')->put` e loga warning; se ambos falham, lança `StorageException` com `"S3 e fallback local falharam"` (`S3Adapter.php:22-37`).
- [ ] `PC-STORAGE-04` — `temporaryUrl` no `LocalAdapter` lança `"Disco '<disk>' nao disponivel."` (`LocalAdapter.php:103-106`).
- [ ] `PC-STORAGE-05` — `deleteSafe` devolve `true` para chave ausente e `false` (com warning) em falha de remoção (`StorageService.php:164-179`).
- [ ] `PC-STORAGE-06` — `ensureLocalPath` no S3 grava em `/tmp/lotep_s3_downloads/<path com '/'→'_'>` e devolve o caminho mesmo em erro (`S3Adapter.php:157-185`).
- [ ] `PC-STORAGE-07` — `v2_storage_url('')` devolve `''`; `v2_storage_url('a/b.png')` devolve `{url_do_disco}/a/b.png` (`helpers.php:259-267`).
- [ ] `PC-STORAGE-08` — `LocalAdapter::put` converte `UploadedFile` em conteúdo e grava na chave exata, sem criar subpasta (`LocalAdapter.php:32-44`).

### 11.1 Checklist de extração (A–M)

| Item | Resposta |
|---|---|
| A1–A7 | Efeito e contrato na §1; entradas/saídas/erros na §1.3; "não faz" na §1.4; perfis: não há distinção por perfil neste serviço. |
| B1–B7 | Fluxo e decisão na §2.1–2.2; trechos na §2.3; custos na §2.4; falhas na §1.3/§8. |
| C1–C7 | Fonte única de URL/chave na §3.1; invariantes na §3.2; casos-limite na §3.3; armadilhas na §3.4; divergências na §3.5. |
| D1–D9 | API na §4.1; sem UI/JS (§4.2–4.3); init na §4.4; receitas/anti-uso na §4.5–4.6; limites na §4.7; consumidores na §9; extensão: novo adaptador implementando `StorageInterface` e `setAdapter`. |
| E | Não se aplica: sem rotas (§5). |
| F | Não se aplica: sem tabelas/migrations próprias; tabelas consumidas na §6. |
| G/H | Não se aplica: serviço sem tela. |
| I | Não se aplica: sem componentes/JS; contrato de URL coberto na §1.3. |
| J | Config na §7; sem permissões. |
| K | Logs/exceções na §8; limites na §4.7. |
| L | Não se aplica: sem seeds. |
| M | PCs na §11; lacunas na §10; evidências na §12. |

## 12. Evidências consultadas

- `V2/Services/Storage/StorageService.php:23-229`.
- `V2/Services/Storage/Contracts/StorageInterface.php:9-72`.
- `V2/Services/Storage/Adapters/LocalAdapter.php:11-139`.
- `V2/Services/Storage/Adapters/S3Adapter.php:11-193`.
- `V2/Services/Storage/Exceptions/StorageException.php:5-40`.
- `V2/Services/Storage/README.md:1-155`.
- `config/filesystems.php:16-78`.
- `V2/Core/Helpers/helpers.php:251-300`.
- `V2/Core/Services/UploadService.php:22-93`.
- `V2/Core/Providers/V2ServiceProvider.php:168-192,342-351`.
- `V2/Services/Media/Services/MediaInputer.php:235-274`.
- `V2/Services/Gallery/Services/GalleryService.php:58-75`.
- `vendor/laravel/framework/src/Illuminate/Filesystem/FilesystemAdapter.php:164-194` (semântica de `put` com `UploadedFile`).
