diff --git a/CHANGELOG.md b/CHANGELOG.md index 9606e8ce..d014e831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Note: this is a major release and may introduce breaking changes to your workflo Make sure your temp directory is writable by the web server. - **Breaking**: The `--cleanup` flag to `memories:index` has been removed and is no longer necessary. Folders having a `.nomedia` file will automatically be excluded from the timeline. +- **Feature**: Indexing will now build and check indices automatically in the backgroud. + Make sure Nextcloud cron is configured correctly. You can disable automatic indexing in the admin panel. + Note that files are still indexed immediately on upload. - **Feature**: You can now choose which folders to index by default. This can be configured from the admin panel. The available options are: - All media files (excluding folders with `.nomedia` files, default and recommended) diff --git a/appinfo/info.xml b/appinfo/info.xml index 30288933..fbaec0e7 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -64,4 +64,7 @@ Memories is a *batteries-included* photo management solution for Nextcloud with OCA\Memories\Migration\Repair + + OCA\Memories\Cron\IndexJob + diff --git a/lib/Cron/IndexJob.php b/lib/Cron/IndexJob.php new file mode 100644 index 00000000..bf3a16b5 --- /dev/null +++ b/lib/Cron/IndexJob.php @@ -0,0 +1,79 @@ +service = $service; + $this->userManager = $userManager; + $this->logger = $logger; + + $this->setInterval(INTERVAL); + } + + protected function run($arguments) + { + // Check if indexing is enabled + if ('0' === Util::getSystemConfig('memories.index.mode')) { + return; + } + + // Run for a maximum of 5 minutes + $startTime = microtime(true); + $this->service->continueCheck = function () use ($startTime) { + return (microtime(true) - $startTime) < MAX_RUN_TIME; + }; + + // Index with static exiftool process + // This is sub-optimal: the process may not be required at all. + try { + \OCA\Memories\Exif::ensureStaticExiftoolProc(); + $this->indexAllUsers(); + } catch (Service\ProcessClosedException $e) { + $this->logger->warning('Memories: Indexing process closed before completion, will continue on next run.'); + } finally { + \OCA\Memories\Exif::closeStaticExiftoolProc(); + } + } + + /** + * Index all users. + * + * @throws Service\ProcessClosedException if the process was closed before completion + */ + private function indexAllUsers(): void + { + $this->userManager->callForSeenUsers(function ($user) { + try { + $this->service->indexUser($user->getUID()); + } catch (Service\ProcessClosedException $e) { + throw $e; + } catch (\Exception $e) { + $this->logger->error('Indexing failed for user '.$user->getUID().': '.$e->getMessage()); + } catch (\Throwable $e) { + $this->logger->error('[BUG] uncaught exception in memories: '.$e->getMessage()); + } + }); + } +} diff --git a/lib/Service/Index.php b/lib/Service/Index.php index 337c7198..9b223000 100644 --- a/lib/Service/Index.php +++ b/lib/Service/Index.php @@ -40,8 +40,14 @@ use Symfony\Component\Console\Output\OutputInterface; class Index { - public ?OutputInterface $output; - public ?ConsoleSectionOutput $section; + public ?OutputInterface $output = null; + public ?ConsoleSectionOutput $section = null; + + /** + * Callback to check if the process should continue. + * This is called before every file is indexed. + */ + public ?\Closure $continueCheck = null; protected IRootFolder $rootFolder; protected TimelineWrite $timelineWrite; @@ -162,6 +168,7 @@ class Index // Index files foreach ($fileIds as $fileId) { + $this->ensureContinueOk(); $this->indexFile($chunk[$fileId]); } } @@ -169,8 +176,12 @@ class Index // All folders $folders = array_filter($nodes, fn ($n) => $n instanceof Folder); foreach ($folders as $folder) { + $this->ensureContinueOk(); + try { $this->indexFolder($folder); + } catch (ProcessClosedException $e) { + throw $e; } catch (\Exception $e) { $this->logger->error('Failed to index folder {folder}: {error}', [ 'folder' => $folder->getPath(), @@ -280,4 +291,14 @@ class Index $this->section->write($message); } } + + /** + * Ensure that the process should go on. + */ + private function ensureContinueOk(): void + { + if (null !== $this->continueCheck && !($this->continueCheck)()) { + throw new ProcessClosedException(); + } + } } diff --git a/lib/Service/ProcessClosedException.php b/lib/Service/ProcessClosedException.php new file mode 100644 index 00000000..97a2f21a --- /dev/null +++ b/lib/Service/ProcessClosedException.php @@ -0,0 +1,9 @@ +