e137de1183
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
168 lines
5.7 KiB
PHP
168 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXC\SystemStore;
|
|
|
|
use KTXC\Resource\ProviderManager;
|
|
use KTXC\Service\TenantService;
|
|
use KTXF\Resource\BinaryResource;
|
|
use KTXF\Resource\Provider\ProviderInterface;
|
|
use KTXF\Resource\SystemIdentity;
|
|
use KTXF\SystemStore\BlobInfo;
|
|
use KTXF\SystemStore\InvalidKeyException;
|
|
use KTXF\SystemStore\Provider\ProviderBaseInterface;
|
|
use KTXF\SystemStore\Service\SystemStoreServiceInterface;
|
|
use KTXF\SystemStore\StoreReference;
|
|
use KTXF\SystemStore\SystemStoreException;
|
|
use KTXF\SystemStore\SystemStoreManagerInterface;
|
|
use KTXF\SystemStore\WriteCondition;
|
|
|
|
final readonly class SystemStoreManager implements SystemStoreManagerInterface
|
|
{
|
|
public function __construct(
|
|
private TenantService $tenants,
|
|
private ProviderManager $providers,
|
|
) {
|
|
}
|
|
|
|
public function stat(string $tenantId, string $store, string $key): ?BlobInfo
|
|
{
|
|
[$service, $reference] = $this->resolve($tenantId, $store);
|
|
$info = $service->stat($this->qualify($reference, $key));
|
|
|
|
return $info === null ? null : $this->logicalInfo($info, $reference);
|
|
}
|
|
|
|
public function read(string $tenantId, string $store, string $key): ?BinaryResource
|
|
{
|
|
[$service, $reference] = $this->resolve($tenantId, $store);
|
|
return $service->read($this->qualify($reference, $key));
|
|
}
|
|
|
|
public function write(
|
|
string $tenantId,
|
|
string $store,
|
|
string $key,
|
|
BinaryResource $content,
|
|
array $metadata = [],
|
|
?WriteCondition $condition = null,
|
|
): BlobInfo {
|
|
[$service, $reference] = $this->resolve($tenantId, $store);
|
|
$info = $service->write(
|
|
$this->qualify($reference, $key),
|
|
$content,
|
|
$metadata,
|
|
$condition,
|
|
);
|
|
|
|
return $this->logicalInfo($info, $reference);
|
|
}
|
|
|
|
public function delete(
|
|
string $tenantId,
|
|
string $store,
|
|
string $key,
|
|
?WriteCondition $condition = null,
|
|
): bool {
|
|
[$service, $reference] = $this->resolve($tenantId, $store);
|
|
return $service->delete($this->qualify($reference, $key), $condition);
|
|
}
|
|
|
|
public function list(string $tenantId, string $store, string $prefix = ''): iterable
|
|
{
|
|
[$service, $reference] = $this->resolve($tenantId, $store);
|
|
$physicalPrefix = $reference->namespace . '/';
|
|
if ($prefix !== '') {
|
|
$physicalPrefix .= $this->normalizeKey($prefix, true);
|
|
}
|
|
|
|
return $this->logicalItems($service->list($physicalPrefix), $reference);
|
|
}
|
|
|
|
/** @return array{SystemStoreServiceInterface, StoreReference} */
|
|
private function resolve(string $tenantId, string $store): array
|
|
{
|
|
$tenant = $this->tenants->fetchById($tenantId);
|
|
if ($tenant === null) {
|
|
throw new SystemStoreException("Tenant '{$tenantId}' was not found");
|
|
}
|
|
|
|
$reference = $tenant->getConfiguration()->stores()->store($store);
|
|
if ($reference === null) {
|
|
throw new SystemStoreException("System store '{$store}' is not configured for tenant '{$tenantId}'");
|
|
}
|
|
|
|
$provider = $this->providers->resolve(ProviderInterface::TYPE_SYSTEM_STORE, $reference->provider);
|
|
if (!$provider instanceof ProviderBaseInterface) {
|
|
throw new SystemStoreException("System-store provider '{$reference->provider}' is unavailable or incompatible");
|
|
}
|
|
|
|
$service = $provider->serviceFetch($tenantId, SystemIdentity::USER, $reference->service);
|
|
if (!$service instanceof SystemStoreServiceInterface) {
|
|
throw new SystemStoreException("System-store service '{$reference->service}' is unavailable or incompatible");
|
|
}
|
|
|
|
return [$service, $reference];
|
|
}
|
|
|
|
private function qualify(StoreReference $reference, string $key): string
|
|
{
|
|
return $reference->namespace . '/' . $this->normalizeKey($key);
|
|
}
|
|
|
|
private function normalizeKey(string $key, bool $allowTrailingSlash = false): string
|
|
{
|
|
if (
|
|
$key === ''
|
|
|| str_starts_with($key, '/')
|
|
|| (!$allowTrailingSlash && str_ends_with($key, '/'))
|
|
|| str_contains($key, '\\')
|
|
|| str_contains($key, "\0")
|
|
) {
|
|
throw new InvalidKeyException('System-store keys must be non-empty normalized relative keys');
|
|
}
|
|
|
|
$segments = explode('/', $key);
|
|
if ($allowTrailingSlash && end($segments) === '') {
|
|
array_pop($segments);
|
|
}
|
|
if (in_array('', $segments, true) || in_array('.', $segments, true) || in_array('..', $segments, true)) {
|
|
throw new InvalidKeyException('System-store keys cannot contain empty or traversal segments');
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
private function logicalInfo(BlobInfo $info, StoreReference $reference): BlobInfo
|
|
{
|
|
$prefix = $reference->namespace . '/';
|
|
if (!str_starts_with($info->key, $prefix)) {
|
|
throw new SystemStoreException('System-store provider returned a blob outside the configured namespace');
|
|
}
|
|
|
|
return new BlobInfo(
|
|
key: substr($info->key, strlen($prefix)),
|
|
mimeType: $info->mimeType,
|
|
size: $info->size,
|
|
etag: $info->etag,
|
|
modifiedAt: $info->modifiedAt,
|
|
attributes: $info->attributes,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param iterable<BlobInfo> $items
|
|
* @return iterable<BlobInfo>
|
|
*/
|
|
private function logicalItems(iterable $items, StoreReference $reference): iterable
|
|
{
|
|
foreach ($items as $item) {
|
|
if (!$item instanceof BlobInfo) {
|
|
throw new SystemStoreException('System-store provider returned invalid listing metadata');
|
|
}
|
|
yield $this->logicalInfo($item, $reference);
|
|
}
|
|
}
|
|
}
|