feat(core): implement tenant-scoped system store manager
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user