transaction(function () use ($folderPath, $modelName): int { $folder = $this->ensureFolder($folderPath); $this->execute( 'INSERT INTO models (folder_id, name) VALUES (?, ?) ON CONFLICT (folder_id, name) DO NOTHING', [$folder, $modelName] ); return (int) $this->row( 'SELECT id FROM models WHERE folder_id = ? AND name = ?', [$folder, $modelName] )['id']; }); } public function recordFile( int $modelId, string $managedPath, string $hash, int $size, ?string $mime = null, string $status = 'Present' ): int { $this->validateStatus($status); if ($managedPath === '' || $hash === '' || $size < 0) { throw new InvalidArgumentException('A file requires a path, hash and non-negative size.'); } return $this->transaction(function () use ($modelId, $managedPath, $hash, $size, $mime, $status): int { // Serialize catalog writers on the root row, including paths assigned to different models. $this->ensureFolder('/'); $this->execute("UPDATE folders SET name = name WHERE path = '/'"); $existing = $this->row('SELECT * FROM files WHERE managed_path = ?', [$managedPath]); if ($existing !== null) { if ( (int) $existing['model_id'] !== $modelId || $existing['hash'] !== $hash || (int) $existing['size'] !== $size ) { throw new DomainException('Managed path already records a different file.'); } return (int) $existing['id']; } $folder = $this->ensureFolder(dirname(str_replace('\\', '/', $managedPath))); $file = $this->row( 'INSERT INTO files (model_id, folder_id, name, managed_path, hash, size, mime_type, status)' . ' VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id', [$modelId, $folder, basename($managedPath), $managedPath, $hash, $size, $mime, $status] ); return (int) $file['id']; }); } public function aggregate(int $modelId): ?array { return $this->transaction(function () use ($modelId): ?array { $model = $this->row('SELECT * FROM models WHERE id = ?', [$modelId]); if ($model === null) { return null; } return [ 'model' => $model, 'folder' => $this->row('SELECT * FROM folders WHERE id = ?', [$model['folder_id']]), 'files' => $this->execute('SELECT * FROM files WHERE model_id = ? ORDER BY id', [$modelId]) ->fetchAll(PDO::FETCH_ASSOC), ]; }); } public function setFileStatus(int $fileId, string $status): void { $this->validateStatus($status); if ( $this->execute( 'UPDATE files SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [$status, $fileId] )->rowCount() === 0 ) { throw new DomainException('Unknown file.'); } } private function validateStatus(string $status): void { if (!in_array($status, ['Present', 'Missing', 'Updated', 'Duplicate'], true)) { throw new InvalidArgumentException('Invalid file status.'); } } private function ensureFolder(string $path): int { $segments = array_values(array_filter(explode('/', str_replace('\\', '/', $path)), 'strlen')); $segments = array_values(array_filter($segments, static fn (string $part): bool => $part !== '.')); if (in_array('..', $segments, true)) { throw new InvalidArgumentException('Folder traversal is not allowed.'); } $parent = null; $current = ''; foreach (array_merge([''], $segments) as $segment) { $current = $segment === '' ? '/' : rtrim($current, '/') . '/' . $segment; $this->execute( 'INSERT INTO folders (parent_id, name, path) VALUES (?, ?, ?) ON CONFLICT (path) DO NOTHING', [$parent, $segment === '' ? '/' : $segment, $current] ); $parent = (int) $this->row('SELECT id FROM folders WHERE path = ?', [$current])['id']; } return $parent; } }