123 lines
4.8 KiB
PHP
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;
|
|
}
|
|
}
|