Gestão de Documentos (GED)

Lixeira

Lixeira e arquivo morto via status (GedTrashService) — distinto do soft delete do framework. Retenção configurável.

Lixeira

O GED tem seu próprio fluxo de lixeira/arquivo morto, independente do soft delete genérico do framework. É uma máquina de estados sobre a coluna Document.status (active ↔ trash ↔ archived), implementada em App\Service\Ged\GedTrashService.

Por que não é o HasMadSoftDeletes do framework? Todo model do GED usa o trait, mas ele só vira soft delete quando o model mapeia uma coluna deleted_at (via $madAudit ou a propriedade legada) — nenhum model do GED faz esse mapeamento. Na prática, Document::find($id)->delete() é uma exclusão física real nesta instalação, não uma marcação. A "lixeira" do GED é deliberadamente um conceito de negócio à parte (documento continua visível/recuperável via status), não o mecanismo de soft delete do Eloquent.

Os estados

active ──trash()────────► trash ──restore()──────► active
active ──archive()──────► archived ──restoreFromArchive()──► active
trash ──permanentDelete()──► (removido de verdade)
use App\Service\Ged\GedTrashService;

GedTrashService::trash($documentId, $userId);              // active → trash
GedTrashService::restore($documentId, $userId);             // trash → active
GedTrashService::archive($documentId, $userId);              // active → archived
GedTrashService::restoreFromArchive($documentId, $userId);   // archived → active
GedTrashService::permanentDelete($documentId, $userId);      // remove de vez

trash()

$doc->original_folder_id = $doc->folder_id;  // lembra de onde veio
$doc->folder_id  = null;                      // sai da árvore de pastas
$doc->status     = 'trash';
$doc->trash_date = date('Y-m-d H:i:s');
$doc->trashed_by = $userId;
$doc->save();

O documento some da pasta onde estava (folder_id = null) enquanto está na lixeira — restore() usa original_folder_id para devolvê-lo ao lugar certo.

permanentDelete() — o que de fato é apagado

// objetos no disco de uploads — todo IO via Storage/Flysystem
foreach ($versions as $v) {
    GedStorage::delete((string) ($v->storage_path ?? ''));   // chave inválida = no-op
}
// e o prefixo inteiro do documento, INCLUSIVE sobras não registradas no banco
\Mad\Service\MadUploadStorage::deleteDirectory("files/ged/{$documentId}");

// linhas relacionadas (todas exclusões reais — sem soft delete ativo)
DocumentVersion::where('document_id', $documentId)->delete();
DocumentTag::where('document_id', $documentId)->delete();
DocumentPermission::where('document_id', $documentId)->delete();
SharedLink::where('document_id', $documentId)->delete();
Favorite::where('document_id', $documentId)->delete();
ActivityLog::where('document_id', $documentId)->delete();
$doc->delete();

Objetos no storage, todas as linhas filhas (versões, tags, permissões, links, favoritos, atividade) e a linha do documento em si — tudo removido, dentro de uma transação para as linhas do banco. Não há como recuperar um documento depois de permanentDelete().

O deleteDirectory() existe além do loop por versão de propósito: ele varre o prefixo inteiro e leva embora arquivos órfãos (um upload que gravou no storage e cujo commit da transação falhou, por exemplo). Como todo o IO passa pelo disco de uploads (mad_uploads por default, ou o de MAD_UPLOAD_DISK), o mesmo código funciona em S3 — ver Upload e versionamento.

Retenção automática

public static function purgeExpired(): void

Lê ged_trash_retention_days (preferência do sistema, default 30), encontra todo documento com status = 'trash' e trash_date além do prazo, e chama permanentDelete() em cada um (com userId = 0, o que silencia o log de atividade — propositalmente: é uma rotina automática, não uma ação de usuário).

purgeExpired() não está agendada em routes/console.php nesta instalação — para que a retenção funcione automaticamente, registre-a você mesmo:

// routes/console.php
use Illuminate\Support\Facades\Schedule;
use App\Service\Ged\GedTrashService;

Schedule::call(fn () => GedTrashService::purgeExpired())->daily();

Ver Scheduler para mais sobre Schedule::*.

Configuração

Mesmas preferências do sistema usadas por upload/versionamento (ver Upload e versionamento) — mad_sys_preference com prefixo ged_, editadas na aba "GED" do PreferenceForm e lidas por App\Service\Ged\GedSettingsService:

Preferência Default Efeito
ged_trash_retention_days 30 Dias na lixeira antes de purgeExpired() considerar o documento elegível para remoção definitiva.
$days = (int) GedSettingsService::get('trash_retention_days', '30');

Na tela

DocumentList tem modos de visualização trash e archived (prop $viewMode, filtrando Document.status na query do grid) — é de lá que o usuário restaura ou apaga em definitivo pelas ações da linha. O painel DocumentDetail também expõe "Mover para lixeira" e "Arquivar" no rodapé, delegando para GedTrashService::trash()/archive().

Cuidado ao estender: as ações de linha do próprio DocumentList (onDelete, onRestore, onPermanentDelete, onRestoreFromArchive) reimplementam a lógica inline em vez de chamar GedTrashService — e não são equivalentes em todos os pontos. O onPermanentDelete da lista apaga só linhas (DocumentTag, Favorite, DocumentVersion, Document): não remove os objetos do storage nem as DocumentPermission/SharedLink/ ActivityLog, e não registra atividade. GedTrashService::permanentDelete() faz tudo isso. Se você for estender o fluxo, centralize no service e chame-o dos dois lugares.

Ver também