dirname = rtrim($dirname, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function save(string $id, array $data): void { if (!file_exists($this->dirname)) { mkdir($this->dirname, 0o755, true); file_put_contents($this->dirname . '.gitignore', "*\n!.gitignore\n"); } file_put_contents($this->makeFilename($id), json_encode($data)); $this->autoPrune(); } /** * {@inheritdoc} */ public function get(string $id): array { $fileName = $this->makeFilename($id); if (!file_exists($fileName)) { return []; } $content = file_get_contents($fileName); if ($content === false) { throw new \RuntimeException("Unable to read file $fileName"); } return json_decode($content, true) ?: []; } /** * {@inheritdoc} */ public function find(array $filters = [], int $max = 20, int $offset = 0): array { //Load the metadata and filter the results. $results = []; $ids = []; $i = 0; $files = new \FilesystemIterator($this->dirname, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME); foreach ($files as $path) { if (str_ends_with($path, '.json')) { $ids[] = basename($path, '.json'); } } // Sort by id sort($ids, SORT_STRING); $ids = array_reverse($ids); $filterUtime = null; if ($filters['utime'] ?? false) { $filterUtime = (float) $filters['utime']; unset($filters['utime']); } foreach ($ids as $id) { //When filter is empty, skip loading the offset if ($i++ < $offset && !$filters) { $results[] = null; continue; } $data = $this->get($id); if (!isset($data['__meta'])) { continue; } $meta = $data['__meta']; unset($data); if ($filterUtime !== null && $meta['utime'] <= $filterUtime) { break; } if ($this->filter($meta, $filters)) { $results[] = $meta; } if (count($results) >= ($max + $offset)) { break; } } return array_slice($results, $offset, $max); } /** * Filter the metadata for matches. * * */ protected function filter(array $meta, array $filters): bool { foreach ($filters as $key => $value) { if (!isset($meta[$key]) || fnmatch($value, $meta[$key]) === false) { return false; } } return true; } /** * {@inheritdoc} */ public function clear(): void { foreach (new \FilesystemIterator($this->dirname, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS) as $path) { if (is_file($path) && str_ends_with($path, '.json')) { unlink($path); } } } public function makeFilename(string $id): string { return $this->dirname . basename($id) . ".json"; } /** * {@inheritdoc} */ public function prune(int $hours = 24): void { $cutoffTime = time() - $hours * 3600; if (!is_dir($this->dirname)) { return; } /** @var \DirectoryIterator $file */ foreach (new \FilesystemIterator($this->dirname, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS) as $file) { if ($file->isFile() && str_ends_with($file->getFilename(), '.json') && $file->getMTime() < $cutoffTime) { unlink($file->getPathname()); } } } }