850 lines
24 KiB
PHP
850 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace KTXM\FileProviderLocal\Providers\Personal;
|
|
|
|
use KTXF\Files\Node\INodeBase;
|
|
use KTXF\Files\Node\INodeCollectionBase;
|
|
use KTXF\Files\Node\INodeCollectionMutable;
|
|
use KTXF\Files\Node\INodeEntityBase;
|
|
use KTXF\Files\Node\INodeEntityMutable;
|
|
use KTXF\Files\Node\NodeType;
|
|
use KTXF\Files\Service\IServiceBase;
|
|
use KTXF\Files\Service\IServiceCollectionMutable;
|
|
use KTXF\Files\Service\IServiceEntityMutable;
|
|
use KTXF\Resource\Exceptions\InvalidParameterException;
|
|
use KTXF\Resource\Filter\Filter;
|
|
use KTXF\Resource\Filter\IFilter;
|
|
use KTXF\Resource\Range\IRange;
|
|
use KTXF\Resource\Range\RangeTally;
|
|
use KTXF\Resource\Range\RangeType;
|
|
use KTXF\Resource\Sort\ISort;
|
|
use KTXF\Resource\Sort\Sort;
|
|
use KTXM\FileProviderLocal\Store\BlobStore;
|
|
use KTXM\FileProviderLocal\Store\MetaStore;
|
|
use KTXF\Blob\Signature;
|
|
|
|
class PersonalService implements IServiceBase, IServiceCollectionMutable, IServiceEntityMutable {
|
|
|
|
public const ROOT_ID = '00000000-0000-0000-0000-000000000000';
|
|
public const ROOT_LABEL = 'Personal Files';
|
|
|
|
protected const SERVICE_ID = 'personal';
|
|
protected const SERVICE_LABEL = 'Personal Files Service';
|
|
protected const SERVICE_PROVIDER = 'default';
|
|
|
|
protected array $serviceCollectionCache = [];
|
|
protected array $serviceEntityCache = [];
|
|
protected ?string $serviceTenantId = null;
|
|
protected ?string $serviceUserId = null;
|
|
protected ?bool $serviceEnabled = true;
|
|
protected ?string $serviceRoot = null;
|
|
|
|
protected array $serviceAbilities = [
|
|
// Collection capabilities
|
|
self::CAPABILITY_COLLECTION_LIST => true,
|
|
self::CAPABILITY_COLLECTION_LIST_FILTER => [
|
|
'id' => 'a:10:64:192',
|
|
'label' => 's:100:256:771',
|
|
],
|
|
self::CAPABILITY_COLLECTION_LIST_SORT => [
|
|
'label',
|
|
'created',
|
|
'modified'
|
|
],
|
|
self::CAPABILITY_COLLECTION_EXTANT => true,
|
|
self::CAPABILITY_COLLECTION_FETCH => true,
|
|
self::CAPABILITY_COLLECTION_CREATE => true,
|
|
self::CAPABILITY_COLLECTION_MODIFY => true,
|
|
self::CAPABILITY_COLLECTION_DESTROY => true,
|
|
self::CAPABILITY_COLLECTION_COPY => true,
|
|
self::CAPABILITY_COLLECTION_MOVE => true,
|
|
// Entity capabilities
|
|
self::CAPABILITY_ENTITY_LIST => true,
|
|
self::CAPABILITY_ENTITY_LIST_FILTER => [
|
|
'id' => 'a:10:64:192',
|
|
'label' => 's:100:256:771',
|
|
'mimeType' => 's:100:256:771',
|
|
],
|
|
self::CAPABILITY_ENTITY_LIST_SORT => [
|
|
'label',
|
|
'size',
|
|
'created',
|
|
'modified'
|
|
],
|
|
self::CAPABILITY_ENTITY_LIST_RANGE => [
|
|
'tally' => ['absolute', 'relative']
|
|
],
|
|
self::CAPABILITY_ENTITY_DELTA => true,
|
|
self::CAPABILITY_ENTITY_EXTANT => true,
|
|
self::CAPABILITY_ENTITY_FETCH => true,
|
|
self::CAPABILITY_ENTITY_READ => true,
|
|
self::CAPABILITY_ENTITY_READ_STREAM => true,
|
|
self::CAPABILITY_ENTITY_READ_CHUNK => true,
|
|
self::CAPABILITY_ENTITY_CREATE => true,
|
|
self::CAPABILITY_ENTITY_MODIFY => true,
|
|
self::CAPABILITY_ENTITY_DESTROY => true,
|
|
self::CAPABILITY_ENTITY_COPY => true,
|
|
self::CAPABILITY_ENTITY_MOVE => true,
|
|
self::CAPABILITY_ENTITY_WRITE => true,
|
|
self::CAPABILITY_ENTITY_WRITE_STREAM => true,
|
|
self::CAPABILITY_ENTITY_WRITE_CHUNK => true,
|
|
// Node capabilities (recursive/unified)
|
|
self::CAPABILITY_NODE_LIST => true,
|
|
self::CAPABILITY_NODE_LIST_FILTER => [
|
|
'id' => 'a:10:64:192',
|
|
'label' => 's:100:256:771',
|
|
'nodeType' => 's:10:20:192',
|
|
'mimeType' => 's:100:256:771',
|
|
],
|
|
self::CAPABILITY_NODE_LIST_SORT => [
|
|
'label',
|
|
'nodeType',
|
|
'size',
|
|
'modified'
|
|
],
|
|
self::CAPABILITY_NODE_LIST_RANGE => [
|
|
'tally' => ['absolute', 'relative']
|
|
],
|
|
self::CAPABILITY_NODE_DELTA => true,
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly MetaStore $metaStore,
|
|
private readonly BlobStore $blobStore,
|
|
) {}
|
|
|
|
public function jsonSerialize(): mixed {
|
|
return [
|
|
self::JSON_PROPERTY_TYPE => self::JSON_TYPE,
|
|
self::JSON_PROPERTY_PROVIDER => self::SERVICE_PROVIDER,
|
|
self::JSON_PROPERTY_ID => self::SERVICE_ID,
|
|
self::JSON_PROPERTY_LABEL => self::SERVICE_LABEL,
|
|
self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities,
|
|
self::JSON_PROPERTY_ENABLED => $this->serviceEnabled,
|
|
];
|
|
}
|
|
|
|
public function init(string $tenantId, string $userId, string $root): self {
|
|
$this->serviceTenantId = $tenantId;
|
|
$this->serviceUserId = $userId;
|
|
$this->serviceRoot = $root;
|
|
|
|
// configure blob store with root path
|
|
$this->blobStore->configureRoot($root);
|
|
|
|
$this->serviceCollectionCache = [];
|
|
$root = new NodeCollection();
|
|
$root->fromStore([
|
|
'tid' => $tenantId,
|
|
'uid' => $userId,
|
|
'nid' => self::ROOT_ID,
|
|
'pid' => null,
|
|
'createdBy' => $userId,
|
|
'createdOn' => (new \DateTimeImmutable())->format('c'),
|
|
'owner' => $userId,
|
|
'label' => self::ROOT_LABEL,
|
|
]);
|
|
$this->serviceCollectionCache[self::ROOT_ID] = $root;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function capable(string $value): bool {
|
|
if (isset($this->serviceAbilities[$value])) {
|
|
return (bool)$this->serviceAbilities[$value];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function capabilities(): array {
|
|
return $this->serviceAbilities;
|
|
}
|
|
|
|
public function in(): string {
|
|
return self::SERVICE_PROVIDER;
|
|
}
|
|
|
|
public function id(): string {
|
|
return self::SERVICE_ID;
|
|
}
|
|
|
|
public function getLabel(): string {
|
|
return (string)self::SERVICE_LABEL;
|
|
}
|
|
|
|
public function getEnabled(): bool {
|
|
return (bool)$this->serviceEnabled;
|
|
}
|
|
|
|
// ==================== Collection Methods (IServiceBase) ====================
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionList(string|int|null $location = null, ?IFilter $filter = null, ?ISort $sort = null): array {
|
|
$entries = $this->metaStore->collectionList($this->serviceTenantId, $this->serviceUserId, $location, $filter, $sort);
|
|
// cache collections
|
|
foreach ($entries as $id => $collection) {
|
|
$this->serviceCollectionCache[$id] = $collection;
|
|
}
|
|
return $entries ?? [];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionListFilter(): IFilter {
|
|
return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionListSort(): ISort {
|
|
return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionExtant(string|int|null $identifier): bool {
|
|
// null is root
|
|
if ($identifier === null) {
|
|
return true;
|
|
}
|
|
// check cache first
|
|
if (isset($this->serviceCollectionCache[$identifier])) {
|
|
return true;
|
|
}
|
|
// check store
|
|
return $this->metaStore->collectionExtant($this->serviceTenantId, $this->serviceUserId, $identifier);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionFetch(string|int|null $identifier): ?INodeCollectionBase {
|
|
// null is root
|
|
if ($identifier === null) {
|
|
$identifier = self::ROOT_ID;
|
|
}
|
|
// check cache first
|
|
if (isset($this->serviceCollectionCache[$identifier])) {
|
|
return $this->serviceCollectionCache[$identifier];
|
|
}
|
|
// fetch from store
|
|
$collections = $this->metaStore->collectionFetch($this->serviceTenantId, $this->serviceUserId, $identifier);
|
|
if (isset($collections[$identifier])) {
|
|
$this->serviceCollectionCache[$identifier] = $collections[$identifier];
|
|
return $collections[$identifier];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ==================== Collection Methods (IServiceCollectionMutable) ====================
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionFresh(): INodeCollectionMutable {
|
|
$collection = new NodeCollection();
|
|
$collection->fromStore([
|
|
'tid' => $this->serviceTenantId,
|
|
'uid' => $this->serviceUserId,
|
|
'nid' => null,
|
|
'pid' => null,
|
|
'createdBy' => $this->serviceUserId,
|
|
'createdOn' => (new \DateTimeImmutable())->format('c'),
|
|
'owner' => $this->serviceUserId,
|
|
'label' => '',
|
|
]);
|
|
return $collection;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionCreate(string|int|null $location, INodeCollectionMutable $collection, array $options = []): INodeCollectionBase {
|
|
// null is root
|
|
if ($location === null) {
|
|
$location = self::ROOT_ID;
|
|
}
|
|
// Create in meta store
|
|
$node = $this->metaStore->collectionCreate($this->serviceTenantId, $this->serviceUserId, $location, $collection, $options);
|
|
// cache collection
|
|
$this->serviceCollectionCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionModify(string|int $identifier, INodeCollectionMutable $collection): INodeCollectionBase {
|
|
// Modify in meta store
|
|
$node = $this->metaStore->collectionModify($this->serviceTenantId, $this->serviceUserId, $identifier, $collection);
|
|
|
|
// update cache
|
|
$this->serviceCollectionCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionDestroy(string|int $identifier): bool {
|
|
// Protect root collection
|
|
if ($identifier === self::ROOT_ID) {
|
|
throw new InvalidParameterException("Cannot destroy root collection");
|
|
}
|
|
|
|
// Delete from meta store
|
|
$result = $this->metaStore->collectionDestroy($this->serviceTenantId, $this->serviceUserId, $identifier);
|
|
|
|
// remove from cache
|
|
unset($this->serviceCollectionCache[$identifier]);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionCopy(string|int $identifier, string|int|null $location): INodeCollectionBase {
|
|
// Protect root collection
|
|
if ($identifier === self::ROOT_ID) {
|
|
throw new InvalidParameterException("Cannot copy root collection");
|
|
}
|
|
// Verify collection exists
|
|
if (!$this->collectionExtant($identifier)) {
|
|
throw new InvalidParameterException("Collection not found: $identifier");
|
|
}
|
|
|
|
// null location is root
|
|
if ($location === null) {
|
|
$location = self::ROOT_ID;
|
|
}
|
|
// Copy in meta store
|
|
$node = $this->metaStore->collectionCopy($this->serviceTenantId, $this->serviceUserId, $identifier, $location);
|
|
|
|
// cache collection
|
|
$this->serviceCollectionCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function collectionMove(string|int $identifier, string|int|null $location): INodeCollectionBase {
|
|
// Protect root collection
|
|
if ($identifier === self::ROOT_ID) {
|
|
throw new InvalidParameterException("Cannot move root collection");
|
|
}
|
|
// Verify collection exists
|
|
if (!$this->collectionExtant($identifier)) {
|
|
throw new InvalidParameterException("Collection not found: $identifier");
|
|
}
|
|
|
|
// null location is root
|
|
if ($location === null) {
|
|
$location = self::ROOT_ID;
|
|
}
|
|
// Move in meta store
|
|
$node = $this->metaStore->collectionMove($this->serviceTenantId, $this->serviceUserId, $identifier, $location);
|
|
|
|
// update cache
|
|
$this->serviceCollectionCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
// ==================== Entity Methods (IServiceBase) ====================
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityList(string|int|null $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$entries = $this->metaStore->entityList($this->serviceTenantId, $this->serviceUserId, $collection, $filter, $sort, $range);
|
|
// cache entities
|
|
foreach ($entries as $id => $entity) {
|
|
$this->serviceEntityCache[$id] = $entity;
|
|
}
|
|
return $entries ?? [];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityListFilter(): IFilter {
|
|
return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityListSort(): ISort {
|
|
return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityListRange(RangeType $type): IRange {
|
|
if ($type !== RangeType::TALLY) {
|
|
throw new InvalidParameterException("Invalid: Entity range of type '{$type->value}' is not supported");
|
|
}
|
|
return new RangeTally();
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityDelta(string|int|null $collection, string $signature, string $detail = 'ids'): array {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
return $this->metaStore->entityDelta($this->serviceTenantId, $this->serviceUserId, $collection, $signature, $detail);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityExtant(string|int|null $collection, string|int ...$identifiers): array {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($identifiers as $id) {
|
|
// check cache first
|
|
if (isset($this->serviceEntityCache[$id])) {
|
|
$result[$id] = true;
|
|
continue;
|
|
}
|
|
// check store
|
|
$result[$id] = $this->metaStore->entityExtant($this->serviceTenantId, $this->serviceUserId, $collection, $id);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityFetch(string|int|null $collection, string|int ...$identifiers): array {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$result = [];
|
|
$toFetch = [];
|
|
|
|
// check cache first
|
|
foreach ($identifiers as $id) {
|
|
if (isset($this->serviceEntityCache[$id])) {
|
|
$result[$id] = $this->serviceEntityCache[$id];
|
|
} else {
|
|
$toFetch[] = $id;
|
|
}
|
|
}
|
|
|
|
// fetch remaining from store
|
|
if (!empty($toFetch)) {
|
|
$fetched = $this->metaStore->entityFetch($this->serviceTenantId, $this->serviceUserId, $collection, ...$toFetch);
|
|
foreach ($fetched as $id => $entity) {
|
|
$this->serviceEntityCache[$id] = $entity;
|
|
$result[$id] = $entity;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityRead(string|int|null $collection, string|int $identifier): ?string {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$entities = $this->entityFetch($collection, $identifier);
|
|
if (!isset($entities[$identifier])) {
|
|
return null;
|
|
}
|
|
return $this->blobStore->blobRead((string)$identifier);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityReadStream(string|int|null $collection, string|int $identifier) {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$entities = $this->entityFetch($collection, $identifier);
|
|
if (!isset($entities[$identifier])) {
|
|
return null;
|
|
}
|
|
|
|
return $this->blobStore->blobReadStream((string)$identifier);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityReadChunk(string|int|null $collection, string|int $identifier, int $offset, int $length): ?string {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
$entities = $this->entityFetch($collection, $identifier);
|
|
if (!isset($entities[$identifier])) {
|
|
return null;
|
|
}
|
|
|
|
return $this->blobStore->blobReadChunk((string)$identifier, $offset, $length);
|
|
}
|
|
|
|
// ==================== Entity Methods (IServiceEntityMutable) ====================
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityFresh(): INodeEntityMutable {
|
|
$entity = new NodeEntity();
|
|
$entity->fromStore([
|
|
'tid' => $this->serviceTenantId,
|
|
'uid' => $this->serviceUserId,
|
|
'nid' => null,
|
|
'pid' => null,
|
|
'createdBy' => $this->serviceUserId,
|
|
'createdOn' => (new \DateTimeImmutable())->format('c'),
|
|
'owner' => $this->serviceUserId,
|
|
'label' => '',
|
|
]);
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* Build metadata array for an entity to store in .meta file
|
|
*
|
|
* @param INodeEntityBase $node The entity node
|
|
* @return array Metadata array
|
|
*/
|
|
protected function buildEntityMeta(INodeEntityBase $node): array {
|
|
return [
|
|
'nid' => $node->id(),
|
|
'pid' => $node->in(),
|
|
'label' => $node->getLabel(),
|
|
'mime' => $node->getMime(),
|
|
'format' => $node->getFormat(),
|
|
'createdBy' => $node->createdBy(),
|
|
'createdOn' => $node->createdOn()?->format('c'),
|
|
'modifiedBy' => $node->modifiedBy(),
|
|
'modifiedOn' => $node->modifiedOn()?->format('c'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityCreate(string|int|null $collection, INodeEntityMutable $entity, array $options = []): INodeEntityBase {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Create in meta store
|
|
$node = $this->metaStore->entityCreate($this->serviceTenantId, $this->serviceUserId, $collection, $entity, $options);
|
|
|
|
// Write meta file for recovery
|
|
$this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node));
|
|
|
|
// cache entity
|
|
$this->serviceEntityCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityModify(string|int|null $collection, string|int $identifier, INodeEntityMutable $entity): INodeEntityBase {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Modify in meta store
|
|
$node = $this->metaStore->entityModify($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $entity);
|
|
|
|
// Update meta file for recovery
|
|
$this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node));
|
|
|
|
// update cache
|
|
$this->serviceEntityCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityDestroy(string|int|null $collection, string|int $identifier): bool {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Delete from meta store
|
|
$result = $this->metaStore->entityDestroy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier);
|
|
|
|
// Delete file from filesystem
|
|
if ($result) {
|
|
$this->blobStore->blobDelete((string)$identifier);
|
|
}
|
|
|
|
// remove from cache
|
|
unset($this->serviceEntityCache[$identifier]);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityCopy(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
// null destination is root
|
|
if ($destination === null) {
|
|
$destination = self::ROOT_ID;
|
|
}
|
|
|
|
// Verify entity exists
|
|
$extant = $this->entityExtant($collection, $identifier);
|
|
if (!($extant[$identifier] ?? false)) {
|
|
throw new InvalidParameterException("Entity not found: $identifier");
|
|
}
|
|
|
|
// Copy in meta store (creates new entity with new ID)
|
|
$node = $this->metaStore->entityCopy($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $destination);
|
|
|
|
// Copy file content (new entity has new UUID = new file)
|
|
$content = $this->blobStore->blobRead((string)$identifier);
|
|
if ($content !== null) {
|
|
$this->blobStore->blobWrite((string)$node->id(), $content);
|
|
}
|
|
|
|
// Write meta file for recovery
|
|
$this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node));
|
|
|
|
// cache entity
|
|
$this->serviceEntityCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityMove(string|int|null $collection, string|int $identifier, string|int|null $destination): INodeEntityBase {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
// null destination is root
|
|
if ($destination === null) {
|
|
$destination = self::ROOT_ID;
|
|
}
|
|
|
|
// Verify entity exists
|
|
$extant = $this->entityExtant($collection, $identifier);
|
|
if (!($extant[$identifier] ?? false)) {
|
|
throw new InvalidParameterException("Entity not found: $identifier");
|
|
}
|
|
|
|
// Move in meta store (file path never changes since it's based on entity ID)
|
|
$node = $this->metaStore->entityMove($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $destination);
|
|
|
|
// Update meta file for recovery (parent changed)
|
|
$this->blobStore->metaWrite((string)$node->id(), $this->buildEntityMeta($node));
|
|
|
|
// update cache
|
|
$this->serviceEntityCache[$node->id()] = $node;
|
|
return $node;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityWrite(string|int|null $collection, string|int $identifier, string $data): int {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Verify entity exists
|
|
$extant = $this->entityExtant($collection, $identifier);
|
|
if (!($extant[$identifier] ?? false)) {
|
|
throw new InvalidParameterException("Entity not found: $identifier");
|
|
}
|
|
|
|
// Detect MIME type and format from content header
|
|
$signature = Signature::detect(substr($data, 0, Signature::HEADER_SIZE));
|
|
|
|
$bytes = $this->blobStore->blobWrite((string)$identifier, $data);
|
|
if ($bytes === null) {
|
|
throw new \RuntimeException("Failed to write to entity: $identifier");
|
|
}
|
|
|
|
// Update metadata (size, mime, and format)
|
|
$this->metaStore->entityUpdate($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, [
|
|
'size' => $bytes,
|
|
'mime' => $signature['mime'],
|
|
'format' => $signature['format'],
|
|
]);
|
|
|
|
// Update meta file
|
|
$entities = $this->entityFetch($collection, $identifier);
|
|
if (isset($entities[$identifier])) {
|
|
$this->blobStore->metaWrite((string)$identifier, $this->buildEntityMeta($entities[$identifier]));
|
|
}
|
|
|
|
return $bytes;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityWriteStream(string|int|null $collection, string|int $identifier) {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Verify entity exists
|
|
$extant = $this->entityExtant($collection, $identifier);
|
|
if (!($extant[$identifier] ?? false)) {
|
|
throw new InvalidParameterException("Entity not found: $identifier");
|
|
}
|
|
|
|
return $this->blobStore->blobWriteStream((string)$identifier);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function entityWriteChunk(string|int|null $collection, string|int $identifier, int $offset, string $data): int {
|
|
// null collection is root
|
|
if ($collection === null) {
|
|
$collection = self::ROOT_ID;
|
|
}
|
|
|
|
// Verify entity exists
|
|
$extant = $this->entityExtant($collection, $identifier);
|
|
if (!($extant[$identifier] ?? false)) {
|
|
throw new InvalidParameterException("Entity not found: $identifier");
|
|
}
|
|
|
|
// Detect MIME type and format from first chunk (offset === 0)
|
|
$signature = null;
|
|
if ($offset === 0) {
|
|
$signature = Signature::detect(substr($data, 0, Signature::HEADER_SIZE));
|
|
}
|
|
|
|
$bytes = $this->blobStore->blobWriteChunk((string)$identifier, $offset, $data);
|
|
if ($bytes === null) {
|
|
throw new \RuntimeException("Failed to write chunk to entity: $identifier");
|
|
}
|
|
|
|
// Build update attributes
|
|
$updates = [];
|
|
|
|
// Update size metadata
|
|
$newSize = $this->blobStore->blobSize((string)$identifier);
|
|
if ($newSize !== null) {
|
|
$updates['size'] = $newSize;
|
|
}
|
|
|
|
// Update mime and format if detected (first chunk)
|
|
if ($signature !== null) {
|
|
$updates['mime'] = $signature['mime'];
|
|
$updates['format'] = $signature['format'];
|
|
}
|
|
|
|
// Perform single database update
|
|
if (!empty($updates)) {
|
|
$this->metaStore->entityUpdate($this->serviceTenantId, $this->serviceUserId, $collection, $identifier, $updates);
|
|
}
|
|
|
|
// Update meta file
|
|
$entities = $this->entityFetch($collection, $identifier);
|
|
if (isset($entities[$identifier])) {
|
|
$this->blobStore->metaWrite((string)$identifier, $this->buildEntityMeta($entities[$identifier]));
|
|
}
|
|
|
|
return $bytes;
|
|
}
|
|
|
|
// ==================== Node Methods (IServiceBase) ====================
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function nodeList(string|int|null $location = null, bool $recursive = false, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): array {
|
|
// null location is root
|
|
if ($location === null) {
|
|
$location = self::ROOT_ID;
|
|
}
|
|
return $this->metaStore->nodeList($this->serviceTenantId, $this->serviceUserId, $location, $recursive, $filter, $sort, $range);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function nodeListFilter(): IFilter {
|
|
return new Filter($this->serviceAbilities[self::CAPABILITY_NODE_LIST_FILTER] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function nodeListSort(): ISort {
|
|
return new Sort($this->serviceAbilities[self::CAPABILITY_NODE_LIST_SORT] ?? []);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function nodeListRange(RangeType $type): IRange {
|
|
if ($type !== RangeType::TALLY) {
|
|
throw new InvalidParameterException("Invalid: Node range of type '{$type->value}' is not supported");
|
|
}
|
|
return new RangeTally();
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function nodeDelta(string|int|null $location, string $signature, bool $recursive = false, string $detail = 'ids'): array {
|
|
return $this->metaStore->nodeDelta($this->serviceTenantId, $this->serviceUserId, $location, $signature, $recursive, $detail);
|
|
}
|
|
|
|
}
|