feat: implement node list
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -23,24 +23,16 @@ class EntityProperties extends EntityPropertiesMutableAbstract {
|
|||||||
*/
|
*/
|
||||||
public function fromJmap(array $parameters): static {
|
public function fromJmap(array $parameters): static {
|
||||||
|
|
||||||
if (isset($data['size'])) {
|
if (isset($parameters['size'])) {
|
||||||
$this->data[self::PROPERTY_SIZE] = (int) $data['size'];
|
$this->data[self::PROPERTY_SIZE] = (int) $parameters['size'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['label'])) {
|
if (isset($parameters['name'])) {
|
||||||
$this->data[self::PROPERTY_LABEL] = (string) $data['name'];
|
$this->data[self::PROPERTY_LABEL] = (string) $parameters['name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($data['mime'])) {
|
if (isset($parameters['type'])) {
|
||||||
$this->data[self::PROPERTY_MIME] = (string) $data['type'];
|
$this->data[self::PROPERTY_MIME] = (string) $parameters['type'];
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($data['format'])) {
|
|
||||||
$this->data[self::PROPERTY_FORMAT] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($data['encoding'])) {
|
|
||||||
$this->data[self::PROPERTY_ENCODING] = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ class EntityResource extends EntityMutableAbstract {
|
|||||||
if (isset($parameters['modified'])) {
|
if (isset($parameters['modified'])) {
|
||||||
$this->data['modified'] = $parameters['modified'];
|
$this->data['modified'] = $parameters['modified'];
|
||||||
}
|
}
|
||||||
if (isset($parameters['accessed']))
|
|
||||||
|
|
||||||
$this->getProperties()->fromJmap($parameters);
|
$this->getProperties()->fromJmap($parameters);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ use KTXF\Documents\Service\ServiceBaseInterface;
|
|||||||
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
|
use KTXF\Documents\Service\ServiceCollectionMutableInterface;
|
||||||
use KTXF\Documents\Service\ServiceConfigurableInterface;
|
use KTXF\Documents\Service\ServiceConfigurableInterface;
|
||||||
use KTXF\Documents\Service\ServiceMutableInterface;
|
use KTXF\Documents\Service\ServiceMutableInterface;
|
||||||
|
use KTXF\Documents\Service\ServiceNodeListInterface;
|
||||||
use KTXF\Resource\Delta\Delta;
|
use KTXF\Resource\Delta\Delta;
|
||||||
|
use KTXF\Resource\Exceptions\InvalidParameterException;
|
||||||
use KTXF\Resource\Filter\Filter;
|
use KTXF\Resource\Filter\Filter;
|
||||||
use KTXF\Resource\Filter\IFilter;
|
use KTXF\Resource\Filter\IFilter;
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
|
use KTXF\Resource\Identifier\CollectionIdentifierInterface;
|
||||||
@@ -26,6 +28,8 @@ use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
|||||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||||
use KTXF\Resource\Range\IRange;
|
use KTXF\Resource\Range\IRange;
|
||||||
use KTXF\Resource\Range\Range;
|
use KTXF\Resource\Range\Range;
|
||||||
|
use KTXF\Resource\Range\RangeAnchorType;
|
||||||
|
use KTXF\Resource\Range\RangeTally;
|
||||||
use KTXF\Resource\Range\RangeType;
|
use KTXF\Resource\Range\RangeType;
|
||||||
use KTXF\Resource\Sort\ISort;
|
use KTXF\Resource\Sort\ISort;
|
||||||
use KTXF\Resource\Sort\Sort;
|
use KTXF\Resource\Sort\Sort;
|
||||||
@@ -34,7 +38,7 @@ use KTXM\ProviderJmapc\Providers\ServiceLocation;
|
|||||||
use KTXM\ProviderJmapc\Service\Remote\RemoteFilesService;
|
use KTXM\ProviderJmapc\Service\Remote\RemoteFilesService;
|
||||||
use KTXM\ProviderJmapc\Service\Remote\RemoteService;
|
use KTXM\ProviderJmapc\Service\Remote\RemoteService;
|
||||||
|
|
||||||
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface
|
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceNodeListInterface
|
||||||
{
|
{
|
||||||
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
|
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
|
||||||
|
|
||||||
@@ -76,6 +80,22 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
|||||||
self::CAPABILITY_ENTITY_DELTA => true,
|
self::CAPABILITY_ENTITY_DELTA => true,
|
||||||
self::CAPABILITY_ENTITY_EXTANT => true,
|
self::CAPABILITY_ENTITY_EXTANT => true,
|
||||||
self::CAPABILITY_ENTITY_FETCH => true,
|
self::CAPABILITY_ENTITY_FETCH => true,
|
||||||
|
self::CAPABILITY_NODE_LIST => true,
|
||||||
|
self::CAPABILITY_NODE_LIST_FILTER => [
|
||||||
|
self::CAPABILITY_NODE_FILTER_LABEL => 's:100:256:256',
|
||||||
|
],
|
||||||
|
self::CAPABILITY_NODE_LIST_SORT => [
|
||||||
|
self::CAPABILITY_NODE_SORT_LABEL,
|
||||||
|
self::CAPABILITY_NODE_SORT_SIZE,
|
||||||
|
self::CAPABILITY_NODE_SORT_CREATED_ON,
|
||||||
|
self::CAPABILITY_NODE_SORT_MODIFIED_ON,
|
||||||
|
],
|
||||||
|
self::CAPABILITY_NODE_LIST_RANGE => [
|
||||||
|
self::CAPABILITY_NODE_RANGE_TALLY => [
|
||||||
|
self::CAPABILITY_NODE_RANGE_TALLY_ABSOLUTE,
|
||||||
|
self::CAPABILITY_NODE_RANGE_TALLY_RELATIVE,
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
private readonly RemoteFilesService $remoteService;
|
private readonly RemoteFilesService $remoteService;
|
||||||
@@ -499,4 +519,111 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Node operations (unified collections + entities)
|
||||||
|
|
||||||
|
public function nodeList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
|
||||||
|
{
|
||||||
|
$this->initialize();
|
||||||
|
|
||||||
|
$result = $this->remoteService->nodeList($location !== null ? (string)$location : null);
|
||||||
|
|
||||||
|
// convert and filter, keeping folders and files apart so folders always come first
|
||||||
|
$label = null;
|
||||||
|
if ($filter !== null && isset($filter->conditions()[self::CAPABILITY_NODE_FILTER_LABEL])) {
|
||||||
|
$label = (string)$filter->conditions()[self::CAPABILITY_NODE_FILTER_LABEL]['value'];
|
||||||
|
}
|
||||||
|
$collections = [];
|
||||||
|
$entities = [];
|
||||||
|
foreach ($result['list'] as $index => $entry) {
|
||||||
|
unset($result['list'][$index]);
|
||||||
|
if (!is_array($entry) || !isset($entry['id'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// folders carry no blob
|
||||||
|
$object = empty($entry['blobId'])
|
||||||
|
? new CollectionResource(provider: $this->provider(), service: $this->identifier())
|
||||||
|
: new EntityResource(provider: $this->provider(), service: $this->identifier());
|
||||||
|
$object->fromJmap($entry);
|
||||||
|
if ($label !== null && $label !== '' && mb_stripos($object->getProperties()->getLabel(), $label) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ($object instanceof CollectionResource) {
|
||||||
|
$collections[] = $object;
|
||||||
|
} else {
|
||||||
|
$entities[] = $object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort each group, defaulting to label ascending
|
||||||
|
$conditions = $sort?->conditions() ?? [];
|
||||||
|
if ($conditions === []) {
|
||||||
|
$conditions = [['attribute' => self::CAPABILITY_NODE_SORT_LABEL, 'direction' => true]];
|
||||||
|
}
|
||||||
|
$comparator = function (CollectionResource|EntityResource $a, CollectionResource|EntityResource $b) use ($conditions): int {
|
||||||
|
foreach ($conditions as $condition) {
|
||||||
|
$order = match ($condition['attribute']) {
|
||||||
|
self::CAPABILITY_NODE_SORT_LABEL => strnatcasecmp($a->getProperties()->getLabel(), $b->getProperties()->getLabel()),
|
||||||
|
self::CAPABILITY_NODE_SORT_SIZE => $this->nodeSize($a) <=> $this->nodeSize($b),
|
||||||
|
self::CAPABILITY_NODE_SORT_CREATED_ON => ($a->created()?->getTimestamp() ?? 0) <=> ($b->created()?->getTimestamp() ?? 0),
|
||||||
|
self::CAPABILITY_NODE_SORT_MODIFIED_ON => ($a->modified()?->getTimestamp() ?? 0) <=> ($b->modified()?->getTimestamp() ?? 0),
|
||||||
|
default => 0,
|
||||||
|
};
|
||||||
|
if ($order !== 0) {
|
||||||
|
return $condition['direction'] ? $order : -$order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strnatcasecmp($a->getProperties()->getLabel(), $b->getProperties()->getLabel());
|
||||||
|
};
|
||||||
|
usort($collections, $comparator);
|
||||||
|
usort($entities, $comparator);
|
||||||
|
$nodes = array_merge($collections, $entities);
|
||||||
|
unset($collections, $entities);
|
||||||
|
|
||||||
|
// apply range
|
||||||
|
if ($range instanceof RangeTally) {
|
||||||
|
$offset = 0;
|
||||||
|
if ($range->getAnchor() === RangeAnchorType::RELATIVE) {
|
||||||
|
// relative ranges start after the anchor node
|
||||||
|
foreach ($nodes as $index => $node) {
|
||||||
|
if ((string)$node->identifier() === (string)$range->getPosition()) {
|
||||||
|
$offset = $index + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$offset = max(0, (int)$range->getPosition());
|
||||||
|
}
|
||||||
|
$nodes = array_slice($nodes, $offset, $range->getTally());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($nodes as $index => $node) {
|
||||||
|
unset($nodes[$index]);
|
||||||
|
yield $node->identifier() => $node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nodeListFilter(): IFilter
|
||||||
|
{
|
||||||
|
return new Filter($this->serviceAbilities[self::CAPABILITY_NODE_LIST_FILTER] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nodeListSort(): ISort
|
||||||
|
{
|
||||||
|
// sort attributes are matched by key
|
||||||
|
return new Sort(array_fill_keys($this->serviceAbilities[self::CAPABILITY_NODE_LIST_SORT] ?? [], true));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nodeSize(CollectionResource|EntityResource $node): int
|
||||||
|
{
|
||||||
|
return $node instanceof EntityResource ? $node->getProperties()->size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,54 @@ class RemoteFilesService {
|
|||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list of nodes (folders and files) directly within a location in remote storage
|
||||||
|
*
|
||||||
|
* FileNode/query support is incomplete on some servers (e.g. empty fileNodeQuerySortOptions),
|
||||||
|
* so all nodes are retrieved and the children of the location are selected locally.
|
||||||
|
*
|
||||||
|
* @since 2026.10.01
|
||||||
|
*
|
||||||
|
* @return array{list: array<string,array>, state: string|null}
|
||||||
|
*/
|
||||||
|
public function nodeList(?string $location = null): array {
|
||||||
|
$location = (empty($location) || $location === self::ROOT_ID) ? null : $location;
|
||||||
|
// construct request
|
||||||
|
$r0 = new NodeGet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel);
|
||||||
|
// transceive
|
||||||
|
$bundle = $this->dataStore->perform([$r0]);
|
||||||
|
// extract response
|
||||||
|
$response = $bundle->response(0);
|
||||||
|
// check for command error
|
||||||
|
if ($response instanceof ResponseException) {
|
||||||
|
if ($response->type() === 'unknownMethod') {
|
||||||
|
throw new JmapUnknownMethod($response->description(), 1);
|
||||||
|
} else {
|
||||||
|
throw new Exception($response->type() . ': ' . $response->description(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// select direct children of location
|
||||||
|
$state = $response->state();
|
||||||
|
$list = [];
|
||||||
|
foreach ($response->objects() as $so) {
|
||||||
|
if (!$so instanceof NodeParametersResponse) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parameters = $so->parametersRaw();
|
||||||
|
$parent = $parameters['parentId'] ?? null;
|
||||||
|
if ($parent === self::ROOT_ID) {
|
||||||
|
$parent = null;
|
||||||
|
}
|
||||||
|
if ($parent !== $location) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parameters['signature'] = $state;
|
||||||
|
$list[$so->id()] = $parameters;
|
||||||
|
}
|
||||||
|
// return node collection
|
||||||
|
return ['list' => $list, 'state' => $state];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fresh instance of collection filter object
|
* fresh instance of collection filter object
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user