feat: listen to user life cycle events

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-08-09 22:41:17 -04:00
parent e48c6c9bb6
commit d45d54951e
5 changed files with 117 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderLocalDocuments\Listeners;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXM\ProviderLocalDocuments\Providers\Personal\PersonalService;
use KTXM\ProviderLocalDocuments\Providers\Provider;
final class UserEventListener
{
public function __construct(
private readonly Provider $provider,
) {
}
public function onUserCreated(UserCreatedEvent $event): void
{
// Fetching initializes the virtual root and creates its disk location.
$this->provider->serviceFetch($event->tenantIdentifier(), $event->userIdentifier(), 'personal');
}
public function onUserDeleting(UserDeletingEvent $event): void
{
$service = $this->provider->serviceFetch($event->tenantIdentifier(), $event->userIdentifier(), 'personal');
if (!($service instanceof PersonalService)) {
return;
}
$service->destroy();
}
}
+10
View File
@@ -3,9 +3,14 @@
namespace KTXM\ProviderLocalDocuments; namespace KTXM\ProviderLocalDocuments;
use KTXC\Resource\ProviderManager; use KTXC\Resource\ProviderManager;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface;
use KTXF\Module\ModuleBrowserInterface; use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleInstanceAbstract; use KTXF\Module\ModuleInstanceAbstract;
use KTXF\Resource\Provider\ProviderInterface; use KTXF\Resource\Provider\ProviderInterface;
use KTXM\ProviderLocalDocuments\Listeners\UserEventListener;
use KTXM\ProviderLocalDocuments\Providers\Provider; use KTXM\ProviderLocalDocuments\Providers\Provider;
class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
@@ -19,6 +24,7 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
public function __construct( public function __construct(
private readonly ProviderManager $providerManager, private readonly ProviderManager $providerManager,
private readonly EventListenerRegistrarInterface $events,
) {} ) {}
public function handle(): string public function handle(): string
@@ -59,6 +65,10 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
public function boot(): void public function boot(): void
{ {
// Register listeners
$this->events->listen($this->handle(), UserCreatedEvent::class, UserEventListener::class, 'onUserCreated', DeliveryMode::Deferred);
$this->events->listen($this->handle(), UserDeletingEvent::class, UserEventListener::class, 'onUserDeleting', DeliveryMode::Deferred);
// Register providers
$this->providerManager->register(ProviderInterface::TYPE_DOCUMENT, 'default', Provider::class); $this->providerManager->register(ProviderInterface::TYPE_DOCUMENT, 'default', Provider::class);
} }
@@ -335,6 +335,41 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI
return true; return true;
} }
/**
* Permanently remove all document data owned by the initialized user.
*/
public function destroy(): void {
foreach ($this->entityListBulk(null) as $entity) {
$identifier = $entity->identifier();
if ($identifier === null) {
continue;
}
$this->entityDelete(new EntityIdentifier(
$entity->provider(),
(string) $entity->service(),
(string) $entity->collection(),
(string) $identifier,
));
}
foreach ($this->collectionList(null) as $collection) {
$identifier = $collection->identifier();
if ($identifier === null) {
continue;
}
$this->collectionDelete(new CollectionIdentifier(
$collection->provider(),
(string) $collection->service(),
(string) $identifier,
), true);
}
$this->metaStore->chronicleExpungeByUser($this->serviceTenantId, $this->serviceUserId);
$this->blobStore->rootDestroy();
}
public function collectionCopy(CollectionIdentifierInterface|null $target, CollectionIdentifierInterface $source): CollectionResource { public function collectionCopy(CollectionIdentifierInterface|null $target, CollectionIdentifierInterface $source): CollectionResource {
$identifier = $source->collection(); $identifier = $source->collection();
// Protect root collection // Protect root collection
+28
View File
@@ -268,6 +268,34 @@ class BlobStore {
return $blobDeleted && $metaDeleted; return $blobDeleted && $metaDeleted;
} }
/**
* Permanently remove the configured user storage root.
*/
public function rootDestroy(): void {
if ($this->rootPath === null || !is_dir($this->rootPath)) {
return;
}
$root = realpath($this->rootPath);
if ($root === false || $root === DIRECTORY_SEPARATOR) {
throw new RuntimeException('Refusing to destroy an invalid blob root');
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $entry) {
if ($entry->isDir() && !$entry->isLink()) {
rmdir($entry->getPathname());
} else {
unlink($entry->getPathname());
}
}
rmdir($root);
$this->rootPath = null;
}
// ========== Blob Existence Check ========== // ========== Blob Existence Check ==========
/** /**
+10
View File
@@ -796,4 +796,14 @@ class MetaStore {
} }
} }
/**
* Delete every chronicle entry owned by a user.
*/
public function chronicleExpungeByUser(string $tenantId, string $userId): void {
$this->_store->selectCollection($this->_ChronicleTable)->deleteMany([
'tid' => $tenantId,
'uid' => $userId,
]);
}
} }