dir) && !mkdir($this->dir, 0775, true) && !is_dir($this->dir)) { throw new RuntimeException("Cannot create directory {$this->dir}"); } } /** A stored file name, as it appears in a project document. */ abstract public static function isValidName(string $name): bool; /** * Every stored file, newest first – what the picker of its kind offers. * * @return list> */ public function list(): array { $items = []; foreach (glob($this->dir . '/*.*') ?: [] as $path) { $file = basename($path); // Sidecars share the directory and are skipped: `{hash}.png.json` is not a valid name. if (!static::isValidName($file)) { continue; } $items[] = [ 'file' => $file, 'name' => OriginalName::read($this->dir, $file), ...$this->details($path), 'bytes' => (int) filesize($path), 'updatedAt' => gmdate('c', (int) filemtime($path)), ]; } usort($items, static fn(array $a, array $b): int => strcmp($b['updatedAt'], $a['updatedAt'])); return $items; } public function delete(string $file): bool { if (!static::isValidName($file)) { return false; } $path = $this->dir . '/' . $file; if (!is_file($path) || !unlink($path)) { return false; } OriginalName::delete($this->dir, $file); return true; } /** * Keeps bytes that passed the store's checks under their hash, with `$originalName` beside them * as a caption (see OriginalName), and returns the record. Bytes stored before are not written * again; a new name renames them. * * @param array $details what the record says between the name and the size * @return array */ protected function keep(string $bytes, string $extension, string $originalName, array $details = []): array { $file = hash('sha256', $bytes) . '.' . $extension; $path = $this->dir . '/' . $file; if (!is_file($path)) { AtomicFile::write($path, $bytes, AtomicFile::SERVED, 'stored file'); } OriginalName::write($this->dir, $file, $originalName); return ['file' => $file, 'name' => OriginalName::read($this->dir, $file), ...$details, 'bytes' => strlen($bytes)]; } /** * What a record of a stored file says between its name and its size. An image reads its pixel * size here; a font or a sound has nothing to read without decoding it. * * @return array */ protected function details(string $path): array { return []; } }