Files
MeshVault/src/Repository/PdoCatalogRepository.php
T

123 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace MeshVault\Repository;
use DomainException;
use InvalidArgumentException;
use PDO;
final class PdoCatalogRepository extends PdoRepository implements CatalogRepositoryInterface
{
public function ensureModel(string $folderPath, string $modelName): int
{
if ($modelName === '') {
throw new InvalidArgumentException('Model name must not be empty.');
}
return $this->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;
}
}