ensureRoot($target); $disk = $this->disk($target); // Grouped by year, because seven years of invoices in one directory is // a directory nobody opens twice. $year = $invoice->issued_on?->format('Y') ?? 'unbekannt'; $relative = $year.'/'.$invoice->number.'.pdf'; $pdf = $this->renderer->forInvoice($invoice); // Written beside the final name and moved into place. A move within one // filesystem is atomic, so whatever watches that folder never sees a // truncated PDF and takes it for a finished invoice. $temporary = $relative.'.part'; if (! $disk->put($temporary, $pdf)) { throw new RuntimeException("Could not write to {$target->name}: {$temporary}"); } $disk->delete($relative); if (! $disk->move($temporary, $relative)) { $disk->delete($temporary); throw new RuntimeException("Could not move the finished file into place on {$target->name}: {$relative}"); } // Read the size back rather than trusting the write. Over a network // mount a short write can report success, and an archive of truncated // files looks exactly like one that worked. $written = $disk->size($relative); if ($written < 1000) { throw new RuntimeException("The archived file on {$target->name} is too small to be an invoice: {$relative}"); } return $relative; } /** * The root is used, never created. * * This is the whole difference between an archive and a hole in the ground. * mkdir -p on a path whose mount is not there SUCCEEDS: it creates the empty * directory beneath the mountpoint, writes the invoice onto the container's * own disk and reports success — so an unmounted NAS would quietly collect * invoices locally while every check said it worked. A missing root means a * missing mount, and that is a failure to report, not a directory to make. * * Only for 'local'. On sftp the root is a remote directory that the server * genuinely may need to create, and there is no mountpoint to be confused * with. */ private function ensureRoot(ExportTarget $target): void { if ($target->driver !== ExportTarget::LOCAL) { return; } if (! is_dir($target->path)) { throw new RuntimeException( "The path for {$target->name} does not exist — is the mount there? {$target->path}" ); } } private function disk(ExportTarget $target): FilesystemAdapter { if ($target->driver === ExportTarget::SFTP) { $stored = trim((string) $target->secret_key); if ($stored === '') { throw new RuntimeException("No credential stored for {$target->name}."); } $password = $this->cipher->decrypt($stored); if ($password === '') { // A rotated SECRETS_KEY makes every stored credential // unreadable. Saying so beats an authentication failure against // a host that is perfectly fine. throw new RuntimeException("The credential for {$target->name} cannot be decrypted — has SECRETS_KEY changed?"); } return Storage::build([ 'driver' => 'sftp', 'host' => (string) $target->host, 'port' => (int) ($target->port ?: 22), 'username' => (string) $target->username, 'password' => $password, 'root' => $target->path, 'timeout' => 30, ]); } return Storage::build([ 'driver' => 'local', 'root' => $target->path, 'throw' => false, ]); } }