From d45d54951e6512ccbb77776422d8d7eac8b8a67e Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Sun, 9 Aug 2026 22:41:17 -0400 Subject: [PATCH] feat: listen to user life cycle events Signed-off-by: Sebastian Krupinski --- lib/Listeners/UserEventListener.php | 34 +++++++++++++++++++++ lib/Module.php | 10 +++++++ lib/Providers/Personal/PersonalService.php | 35 ++++++++++++++++++++++ lib/Store/BlobStore.php | 28 +++++++++++++++++ lib/Store/MetaStore.php | 10 +++++++ 5 files changed, 117 insertions(+) create mode 100644 lib/Listeners/UserEventListener.php diff --git a/lib/Listeners/UserEventListener.php b/lib/Listeners/UserEventListener.php new file mode 100644 index 0000000..0ba10a9 --- /dev/null +++ b/lib/Listeners/UserEventListener.php @@ -0,0 +1,34 @@ +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(); + } +} diff --git a/lib/Module.php b/lib/Module.php index 62bdccf..fb5c071 100644 --- a/lib/Module.php +++ b/lib/Module.php @@ -3,9 +3,14 @@ namespace KTXM\ProviderLocalDocuments; 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\ModuleInstanceAbstract; use KTXF\Resource\Provider\ProviderInterface; +use KTXM\ProviderLocalDocuments\Listeners\UserEventListener; use KTXM\ProviderLocalDocuments\Providers\Provider; class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface @@ -19,6 +24,7 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface public function __construct( private readonly ProviderManager $providerManager, + private readonly EventListenerRegistrarInterface $events, ) {} public function handle(): string @@ -59,6 +65,10 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface 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); } diff --git a/lib/Providers/Personal/PersonalService.php b/lib/Providers/Personal/PersonalService.php index ddfe9c1..26c6255 100644 --- a/lib/Providers/Personal/PersonalService.php +++ b/lib/Providers/Personal/PersonalService.php @@ -335,6 +335,41 @@ class PersonalService implements ServiceBaseInterface, ServiceCollectionMutableI 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 { $identifier = $source->collection(); // Protect root collection diff --git a/lib/Store/BlobStore.php b/lib/Store/BlobStore.php index ff66355..57a9d0f 100644 --- a/lib/Store/BlobStore.php +++ b/lib/Store/BlobStore.php @@ -268,6 +268,34 @@ class BlobStore { 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 ========== /** diff --git a/lib/Store/MetaStore.php b/lib/Store/MetaStore.php index 9717ea8..29073b0 100644 --- a/lib/Store/MetaStore.php +++ b/lib/Store/MetaStore.php @@ -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, + ]); + } + }