issued_on?->format('Y') ?? 'unbekannt'); if (! is_dir($directory) && ! @mkdir($directory, 0o775, true) && ! is_dir($directory)) { throw new RuntimeException("Cannot create the archive directory: {$directory}"); } if (! is_writable($directory)) { // Said plainly. On a mount that has gone away this is the first // thing that fails, and "permission denied" on a path nobody // recognises is a worse message than the path itself. throw new RuntimeException("The archive directory is not writable: {$directory}"); } $target = $directory.'/'.$invoice->number.'.pdf'; $pdf = $this->renderer->forInvoice($invoice); // Same directory, so the rename below stays within one filesystem and // is therefore atomic. A temp file in /tmp would make it a copy. $temporary = $target.'.part'; if (@file_put_contents($temporary, $pdf) === false) { throw new RuntimeException("Could not write {$temporary}"); } if (! @rename($temporary, $target)) { @unlink($temporary); throw new RuntimeException("Could not move the finished file into place: {$target}"); } // Read back the size rather than trusting the write. Over a network // mount a short write can report success, and an archive of truncated // files is worse than an empty one: it looks like it worked. clearstatcache(true, $target); $written = @filesize($target); if ($written === false || $written < 1000) { throw new RuntimeException("The archived file is too small to be an invoice: {$target}"); } return $target; } }