refactor: documents interfaces

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-09 19:32:27 -04:00
parent ac393843f1
commit 050467919b
23 changed files with 1731 additions and 1581 deletions
File diff suppressed because it is too large Load Diff
+62 -84
View File
@@ -15,6 +15,11 @@ use KTXC\Http\Response\StreamedResponse;
use KTXC\SessionIdentity;
use KTXC\SessionTenant;
use KTXF\Controller\ControllerAbstract;
use KTXF\Documents\Collection\CollectionBaseInterface;
use KTXF\Documents\Entity\EntityBaseInterface;
use KTXF\Resource\Identifier\CollectionIdentifier;
use KTXF\Resource\Identifier\EntityIdentifier;
use KTXF\Resource\Identifier\ResourceIdentifiers;
use KTXF\Routing\Attributes\AuthenticatedRoute;
use KTXM\DocumentsManager\Manager;
use KTXM\DocumentsManager\Transfer\StreamingZip;
@@ -23,7 +28,7 @@ use Throwable;
/**
* Controller for file transfers (downloads and uploads)
*
*
* Handles binary file transfers that don't fit the JSON API pattern:
* - Single file downloads (streamed)
* - Multi-file downloads as ZIP (streamed)
@@ -42,7 +47,7 @@ class TransferController extends ControllerAbstract
/**
* Download a single file
*
*
* GET /download/entity/{provider}/{service}/{collection}/{identifier}
*/
#[AuthenticatedRoute(
@@ -55,15 +60,11 @@ class TransferController extends ControllerAbstract
$userId = $this->userIdentity->identifier();
try {
$target = new EntityIdentifier($provider, $service, $collection, $identifier);
// Fetch entity metadata
$entities = $this->manager->entityFetch(
$tenantId,
$userId,
$provider,
$service,
$collection,
[$identifier]
);
/** @var EntityBaseInterface[] $entities */
$entities = $this->manager->entityFetchBulk($tenantId, $userId, $target);
if (empty($entities) || !isset($entities[$identifier])) {
return new JsonResponse([
@@ -73,16 +74,9 @@ class TransferController extends ControllerAbstract
}
$entity = $entities[$identifier];
// Get the stream
$stream = $this->manager->entityReadStream(
$tenantId,
$userId,
$provider,
$service,
$collection,
$identifier
);
$stream = $this->manager->entityReadStream($tenantId, $userId, $target);
if ($stream === null) {
return new JsonResponse([
@@ -114,7 +108,7 @@ class TransferController extends ControllerAbstract
if ($size > 0) {
$response->headers->set('Content-Length', (string) $size);
}
$response->headers->set('Content-Disposition',
$response->headers->set('Content-Disposition',
$response->headers->makeDisposition('attachment', $filename, $this->asciiFallback($filename))
);
$response->headers->set('Cache-Control', 'private, no-cache');
@@ -132,7 +126,7 @@ class TransferController extends ControllerAbstract
/**
* Download multiple files as a ZIP archive
*
*
* GET /download/archive?provider=...&service=...&ids[]=...&ids[]=...
*/
#[AuthenticatedRoute(
@@ -179,10 +173,7 @@ class TransferController extends ControllerAbstract
$stream = $this->manager->entityReadStream(
$tenantId,
$userId,
$provider,
$service,
$file['collection'],
$file['id']
new EntityIdentifier($provider, $service, (string)$file['collection'], (string)$file['id'])
);
if ($stream !== null) {
@@ -221,7 +212,7 @@ class TransferController extends ControllerAbstract
/**
* Download a collection (folder) as a ZIP archive with structure preserved
*
*
* GET /download/collection/{provider}/{service}/{identifier}
*/
#[AuthenticatedRoute(
@@ -239,9 +230,7 @@ class TransferController extends ControllerAbstract
$collection = $this->manager->collectionFetch(
$tenantId,
$userId,
$provider,
$service,
$identifier
new CollectionIdentifier($provider, $service, $identifier)
);
if ($collection === null) {
@@ -251,7 +240,7 @@ class TransferController extends ControllerAbstract
], Response::HTTP_NOT_FOUND);
}
$folderName = $collection->getLabel() ?? 'folder';
$folderName = $collection->getProperties()->getLabel() ?? 'folder';
$archiveName = $this->sanitizeFilename($folderName) . '.zip';
// Build recursive file list
@@ -275,10 +264,7 @@ class TransferController extends ControllerAbstract
$stream = $this->manager->entityReadStream(
$tenantId,
$userId,
$provider,
$service,
$file['collection'],
$file['id']
new EntityIdentifier($provider, $service, (string)$file['collection'], (string)$file['id'])
);
if ($stream !== null) {
@@ -325,13 +311,10 @@ class TransferController extends ControllerAbstract
// Try as entity first
if ($collection !== null) {
/** @var EntityBaseInterface[] $entities */
$entities = $this->manager->entityFetch(
$entities = $this->manager->entityFetchBulk(
$tenantId,
$userId,
$provider,
$service,
$collection,
[$id]
new EntityIdentifier($provider, $service, $collection, (string)$id)
);
if (!empty($entities) && isset($entities[$id])) {
@@ -340,8 +323,8 @@ class TransferController extends ControllerAbstract
'type' => 'file',
'id' => $id,
'collection' => $collection,
'path' => $entity->getLabel() ?? $id,
'modTime' => $entity->modifiedOn()?->getTimestamp(),
'path' => $entity->getProperties()->getLabel() ?? $id,
'modTime' => $entity->modified()?->getTimestamp(),
];
continue;
}
@@ -352,19 +335,17 @@ class TransferController extends ControllerAbstract
$collectionNode = $this->manager->collectionFetch(
$tenantId,
$userId,
$provider,
$service,
$id
new CollectionIdentifier($provider, $service, (string)$id)
);
if ($collectionNode !== null) {
$folderName = $collectionNode->getLabel() ?? $id;
$folderName = $collectionNode->getProperties()->getLabel() ?? $id;
$subFiles = $this->resolveCollectionContents(
$tenantId,
$userId,
$provider,
$service,
$id,
(string)$id,
$folderName
);
$files = array_merge($files, $subFiles);
@@ -407,17 +388,13 @@ class TransferController extends ControllerAbstract
];
}
// Get all nodes in this collection using nodeList with recursive=false
// We handle recursion ourselves to build proper paths
// List immediate child collections and entities of this collection;
// recursion is handled here to build proper archive paths
$sources = new ResourceIdentifiers();
$sources->add(new CollectionIdentifier($provider, $service, $collectionId));
try {
$nodes = $this->manager->nodeList(
$tenantId,
$userId,
$provider,
$service,
$collectionId,
false // Not recursive - we handle it ourselves
);
$collections = $this->manager->collectionList($tenantId, $userId, $sources)[$provider][$service] ?? [];
$entities = $this->manager->entityListBulk($tenantId, $userId, $sources)[$provider][$service][$collectionId] ?? [];
} catch (Throwable $e) {
$this->logger->warning('Failed to list collection contents', [
'collection' => $collectionId,
@@ -426,34 +403,35 @@ class TransferController extends ControllerAbstract
return $files;
}
foreach ($nodes as $node) {
$nodeName = $node->getLabel() ?? (string) $node->id();
/** @var CollectionBaseInterface $node */
foreach ($collections as $node) {
$nodeName = $node->getProperties()->getLabel() ?? (string) $node->identifier();
$nodePath = $basePath !== '' ? $basePath . '/' . $nodeName : $nodeName;
// Recursively get contents of sub-collection
$subFiles = $this->resolveCollectionContents(
$tenantId,
$userId,
$provider,
$service,
(string) $node->identifier(),
$nodePath,
$depth + 1,
$maxDepth
);
$files = array_merge($files, $subFiles);
}
if ($node->isCollection()) {
// Recursively get contents of sub-collection
$subFiles = $this->resolveCollectionContents(
$tenantId,
$userId,
$provider,
$service,
(string) $node->id(),
$nodePath,
$depth + 1,
$maxDepth
);
$files = array_merge($files, $subFiles);
} else {
// It's an entity (file)
/** @var INodeEntityBase $node */
$files[] = [
'type' => 'file',
'id' => (string) $node->id(),
'collection' => $collectionId,
'path' => $nodePath,
'modTime' => $node->modifiedOn()?->getTimestamp(),
];
}
/** @var EntityBaseInterface $node */
foreach ($entities as $node) {
$nodeName = $node->getProperties()->getLabel() ?? (string) $node->identifier();
$nodePath = $basePath !== '' ? $basePath . '/' . $nodeName : $nodeName;
$files[] = [
'type' => 'file',
'id' => (string) $node->identifier(),
'collection' => $collectionId,
'path' => $nodePath,
'modTime' => $node->modified()?->getTimestamp(),
];
}
return $files;
@@ -467,11 +445,11 @@ class TransferController extends ControllerAbstract
// Remove or replace problematic characters
$filename = preg_replace('/[<>:"\/\\|?*\x00-\x1F]/', '_', $filename);
$filename = trim($filename, '. ');
if ($filename === '') {
$filename = 'download';
}
return $filename;
}