diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3242d589..d7510d88d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
- Hide files starting with `.` in the timeline
+- Prevent automatically retrying failed indexing jobs
## [v6.2.2] - 2024-01-10
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 294dc958f..6da9a71ee 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -96,6 +96,7 @@ function w($base, $param)
['name' => 'Admin#getSystemStatus', 'url' => '/api/system-status', 'verb' => 'GET'],
['name' => 'Admin#getSystemConfig', 'url' => '/api/system-config', 'verb' => 'GET'],
['name' => 'Admin#setSystemConfig', 'url' => '/api/system-config/{key}', 'verb' => 'PUT'],
+ ['name' => 'Admin#getFailureLogs', 'url' => '/api/failure-logs', 'verb' => 'GET'],
['name' => 'Admin#placesSetup', 'url' => '/api/occ/places-setup', 'verb' => 'POST'],
// Service worker and assets
diff --git a/lib/Command/Index.php b/lib/Command/Index.php
index 0b07455e6..39b884f1f 100644
--- a/lib/Command/Index.php
+++ b/lib/Command/Index.php
@@ -42,6 +42,7 @@ class IndexOpts
public ?string $user = null;
public ?string $folder = null;
public ?string $group = null;
+ public bool $retry = false;
public bool $skipCleanup = false;
public function __construct(InputInterface $input)
@@ -50,7 +51,8 @@ public function __construct(InputInterface $input)
$this->clear = (bool) $input->getOption('clear');
$this->user = $input->getOption('user');
$this->folder = $input->getOption('folder');
- $this->skipCleanup = $input->getOption('skip-cleanup');
+ $this->retry = (bool) $input->getOption('retry');
+ $this->skipCleanup = (bool) $input->getOption('skip-cleanup');
$this->group = $input->getOption('group');
}
}
@@ -82,6 +84,7 @@ protected function configure(): void
->addOption('folder', null, InputOption::VALUE_REQUIRED, 'Index only the specified folder (relative to the user\'s root)')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force refresh of existing index entries')
->addOption('clear', null, InputOption::VALUE_NONE, 'Clear all existing index entries')
+ ->addOption('retry', 'r', InputOption::VALUE_NONE, 'Retry indexing of failed files')
->addOption('skip-cleanup', null, InputOption::VALUE_NONE, 'Skip cleanup step (removing index entries with missing files)')
;
}
@@ -109,6 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// Perform steps based on opts
$this->checkClear();
$this->checkForce();
+ $this->checkRetry();
// Run the indexer
$this->runIndex();
@@ -118,6 +122,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->indexer->cleanupStale();
}
+ // Warn about skipped files
+ $this->warnRetry();
+
return 0;
} catch (\Exception $e) {
$this->output->writeln("{$e->getMessage()}".PHP_EOL);
@@ -161,10 +168,31 @@ protected function checkForce(): void
}
$this->output->writeln('Forcing refresh of existing index entries');
-
$this->tw->orphanAll();
}
+ /**
+ * Check and act on the retry option if set.
+ */
+ protected function checkRetry(): void
+ {
+ if (!$this->opts->retry) {
+ return;
+ }
+
+ $this->output->writeln("Retrying indexing of failed files");
+ $this->tw->clearAllFailures();
+ }
+
+ /**
+ * Warn about skipped files (called at the end of indexing).
+ */
+ protected function warnRetry(): void {
+ if ($count = $this->tw->countFailures()) {
+ $this->output->writeln("Indexing skipped for ${count} failed files, use --retry to try again");
+ }
+ }
+
/**
* Run the indexer.
*/
diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php
index ccaab81bf..14ab67e0b 100644
--- a/lib/Controller/AdminController.php
+++ b/lib/Controller/AdminController.php
@@ -85,6 +85,7 @@ public function getSystemStatus(): Http\Response
return Util::guardEx(function () {
$config = \OC::$server->get(\OCP\IConfig::class);
$index = \OC::$server->get(\OCA\Memories\Service\Index::class);
+ $tw = \OC::$server->get(\OCA\Memories\Db\TimelineWrite::class);
// Build status array
$status = [];
@@ -107,6 +108,7 @@ public function getSystemStatus(): Http\Response
// Check number of indexed files
$status['indexed_count'] = $index->getIndexedCount();
+ $status['failure_count'] = $tw->countFailures();
// Automatic indexing stats
$jobStart = (int) $config->getAppValue(Application::APPNAME, 'last_index_job_start', (string) 0);
@@ -178,6 +180,29 @@ public function getSystemStatus(): Http\Response
});
}
+ /**
+ * @AdminRequired
+ *
+ * @NoCSRFRequired
+ */
+ public function getFailureLogs(): Http\Response {
+ return Util::guardExDirect(static function (Http\IOutput $out) {
+ $tw = \OC::$server->get(\OCA\Memories\Db\TimelineWrite::class);
+
+ $out->setHeader('Content-Type: text/plain');
+ $out->setHeader('X-Accel-Buffering: no');
+ $out->setHeader('Cache-Control: no-cache');
+
+ foreach ($tw->listFailures() as $log) {
+ $fileid = str_pad((string) $log['fileid'], 12, ' ', STR_PAD_RIGHT); // size
+ $mtime = $log['mtime'];
+ $reason = $log['reason'];
+
+ $out->setOutput("{$fileid}[{$mtime}]\t{$reason}\n");
+ }
+ });
+ }
+
/**
* @AdminRequired
*
diff --git a/lib/Db/TimelineWrite.php b/lib/Db/TimelineWrite.php
index 666de699c..eb56dc19c 100644
--- a/lib/Db/TimelineWrite.php
+++ b/lib/Db/TimelineWrite.php
@@ -11,7 +11,7 @@
use OCP\IDBConnection;
use OCP\Lock\ILockingProvider;
-const DELETE_TABLES = ['memories', 'memories_livephoto', 'memories_places'];
+const DELETE_TABLES = ['memories', 'memories_livephoto', 'memories_places', 'memories_failures'];
const TRUNCATE_TABLES = ['memories_mapclusters'];
class TimelineWrite
@@ -19,6 +19,7 @@ class TimelineWrite
use TimelineWriteMap;
use TimelineWriteOrphans;
use TimelineWritePlaces;
+ use TimelineWriteFailures;
public function __construct(
protected IDBConnection $connection,
@@ -182,7 +183,15 @@ public function processFile(
$query->insert('memories')->values($params);
}
- return $query->executeStatement() > 0;
+ // Execute query
+ $updated = $query->executeStatement() > 0;
+
+ // Clear failures if successful
+ if ($updated) {
+ $this->clearFailures($file);
+ }
+
+ return $updated;
}
/**
diff --git a/lib/Db/TimelineWriteFailures.php b/lib/Db/TimelineWriteFailures.php
new file mode 100644
index 000000000..4ad8d7c98
--- /dev/null
+++ b/lib/Db/TimelineWriteFailures.php
@@ -0,0 +1,93 @@
+getPath()})";
+
+ // Remove all previous failures for this file
+ $this->connection->beginTransaction();
+ $this->clearFailures($file);
+
+ // Add the failure to the database
+ $query = $this->connection->getQueryBuilder();
+ $query->insert('memories_failures')
+ ->values([
+ 'fileid' => $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT),
+ 'mtime' => $query->createNamedParameter($file->getMtime(), IQueryBuilder::PARAM_INT),
+ 'reason' => $query->createNamedParameter($reason, IQueryBuilder::PARAM_STR),
+ ])
+ ->executeStatement()
+ ;
+ $this->connection->commit();
+ }
+
+ /**
+ * Mark a file as successfully indexed.
+ * The entry will be removed from the failures table.
+ *
+ * @param File $file The file that was successfully indexed
+ */
+ public function clearFailures(File $file): void
+ {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete('memories_failures')
+ ->where($query->expr()->eq('fileid', $query->createNamedParameter($file->getId(), IQueryBuilder::PARAM_INT)))
+ ->executeStatement()
+ ;
+ }
+
+ /**
+ * Get the count of failed files.
+ */
+ public function countFailures(): int
+ {
+ $query = $this->connection->getQueryBuilder();
+ $query->select($query->createFunction('COUNT(fileid)'))
+ ->from('memories_failures')
+ ;
+ return (int) $query->executeQuery()->fetchOne();
+ }
+
+ /**
+ * Get the list of failures.
+ */
+ public function listFailures(): array
+ {
+ return $this->connection->getQueryBuilder()
+ ->select('*')
+ ->from('memories_failures')
+ ->executeQuery()
+ ->fetchAll();
+ ;
+ }
+
+ /**
+ * Clear all failures from the database.
+ */
+ public function clearAllFailures(): void
+ {
+ // Delete all entries and reset autoincrement counter
+ $this->connection->executeStatement(
+ $this->connection->getDatabasePlatform()->getTruncateTableSQL('*PREFIX*memories_failures', false));
+ }
+}
\ No newline at end of file
diff --git a/lib/Migration/Version602003Date20240310203729.php b/lib/Migration/Version602003Date20240310203729.php
new file mode 100644
index 000000000..300f61bca
--- /dev/null
+++ b/lib/Migration/Version602003Date20240310203729.php
@@ -0,0 +1,75 @@
+
+ * @author Varun Patil
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace OCA\Memories\Migration;
+
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version602003Date20240310203729 extends SimpleMigrationStep
+{
+ /**
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ */
+ public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {}
+
+ /**
+ * @param \Closure(): ISchemaWrapper $schemaClosure
+ */
+ public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
+ {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if (!$schema->hasTable('memories_failures')) {
+ $table = $schema->createTable('memories_failures');
+ $table->addColumn('id', 'integer', [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ ]);
+ $table->addColumn('fileid', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('mtime', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('reason', 'text', [
+ 'notnull' => false,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addIndex(['fileid', 'mtime'], 'memories_fail_fid_mt_idx');
+ }
+
+ return $schema;
+ }
+
+ /**
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ */
+ public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {}
+}
diff --git a/lib/Service/Index.php b/lib/Service/Index.php
index 6c337c6b1..58f8702f4 100644
--- a/lib/Service/Index.php
+++ b/lib/Service/Index.php
@@ -150,17 +150,24 @@ public function indexFolder(Folder $folder): void
;
// Filter out files that are already indexed
- $addFilter = static function (string $table, string $alias) use (&$query): void {
+ $addFilter = static function (
+ string $table,
+ string $alias,
+ bool $orphan = true,
+ ) use (&$query): void {
$query->leftJoin('f', $table, $alias, $query->expr()->andX(
$query->expr()->eq('f.fileid', "{$alias}.fileid"),
$query->expr()->eq('f.mtime', "{$alias}.mtime"),
- $query->expr()->eq("{$alias}.orphan", $query->expr()->literal(0)),
+ $orphan
+ ? $query->expr()->eq("{$alias}.orphan", $query->expr()->literal(0))
+ : $query->expr()->literal(1),
));
$query->andWhere($query->expr()->isNull("{$alias}.fileid"));
};
$addFilter('memories', 'm');
$addFilter('memories_livephoto', 'lp');
+ $addFilter('memories_failures', 'fail', false);
// Get file IDs to actually index
$fileIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
@@ -201,6 +208,7 @@ public function indexFile(File $file): void
$this->log("Skipping file {$path} due to lock", true);
} catch (\Exception $e) {
$this->error("Failed to index file {$path}: {$e->getMessage()}");
+ $this->tw->markFailed($file, $e->getMessage());
} finally {
$this->tempManager->clean();
}
diff --git a/src/components/admin/AdminTypes.ts b/src/components/admin/AdminTypes.ts
index 4f54aeb63..b7e213eb5 100644
--- a/src/components/admin/AdminTypes.ts
+++ b/src/components/admin/AdminTypes.ts
@@ -48,6 +48,7 @@ export type ISystemStatus = {
bad_encryption: boolean;
indexed_count: number;
+ failure_count: number;
mimes: string[];
imagick: string | false;
gis_type: number;
diff --git a/src/components/admin/sections/Indexing.vue b/src/components/admin/sections/Indexing.vue
index 8e6c31ce9..58bdb7e08 100644
--- a/src/components/admin/sections/Indexing.vue
+++ b/src/components/admin/sections/Indexing.vue
@@ -4,30 +4,16 @@
- {{
- t('memories', '{n} media files have been indexed', {
- n: status.indexed_count,
- })
- }}
+ {{ t('memories', '{n} media files have been indexed', { n: status.indexed_count }) }}
- {{
- t('memories', 'Automatic Indexing status: {status}', {
- status: status.last_index_job_status,
- })
- }}
+ {{ t('memories', 'Automatic Indexing status: {status}', { status: status.last_index_job_status }) }}
- {{
- t('memories', 'Last index job was run {t} seconds ago.', {
- t: status.last_index_job_start,
- })
- }}
+ {{ t('memories', 'Last index job was run {t} seconds ago.', { t: status.last_index_job_start }) }}
{{
status.last_index_job_duration
- ? t('memories', 'It took {t} seconds to complete.', {
- t: status.last_index_job_duration,
- })
+ ? t('memories', 'It took {t} seconds to complete.', { t: status.last_index_job_duration })
: t('memories', 'It is still running or was interrupted.')
}}
@@ -99,25 +85,42 @@
/>
- {{ t('memories', 'For advanced usage, perform a run of indexing by running:') }}
-
- occ memories:index
-
- {{ t('memories', 'Run index in parallel with 4 threads:') }}
-
- bash -c 'for i in {1..4}; do (occ memories:index &); done'
-
- {{ t('memories', 'Force re-indexing of all files:') }}
-
- occ memories:index --force
-
- {{ t('memories', 'You can limit indexing by user and/or folder:') }}
-
- occ memories:index --user=admin --folder=/Photos/
-
- {{ t('memories', 'Clear all existing index tables:') }}
-
- occ memories:index --clear
+
+ {{ t('memories', 'For advanced usage, perform a run of indexing by running:') }}
+
+ occ memories:index
+
+ {{ t('memories', 'Run index in parallel with 4 threads:') }}
+
+ bash -c 'for i in {1..4}; do (occ memories:index &); done'
+
+ {{ t('memories', 'Force re-indexing of all files:') }}
+
+ occ memories:index --force
+
+ {{ t('memories', 'You can limit indexing by user and/or folder:') }}
+
+ occ memories:index --user=admin --folder=/Photos/
+
+ {{ t('memories', 'Clear all existing index tables:') }}
+
+ occ memories:index --clear
+
+
+
+
+ {{ t('memories', '{n} media files failed indexing and were skipped', { n: status.failure_count }) }}
+
+
+ {{ t('memories', 'Files that failed indexing will not be indexed again unless they change.') }}
+ {{ t('memories', 'You can manually retry files that failed indexing.') }}
+
+ occ memories:index --retry
+
+
+ {{ t('memories', 'View failure logs') }}
+
+
@@ -125,6 +128,7 @@
import { defineComponent } from 'vue';
import { translate as t } from '@services/l10n';
+import { API } from '@services/API';
import AdminMixin from '../AdminMixin';
@@ -132,5 +136,11 @@ export default defineComponent({
name: 'Indexing',
title: t('memories', 'Media Indexing'),
mixins: [AdminMixin],
+
+ methods: {
+ openFailureLogs() {
+ return window.open(API.FAILURE_LOGS());
+ },
+ },
});
diff --git a/src/components/admin/sections/Places.vue b/src/components/admin/sections/Places.vue
index ae071041c..d7ccb2a65 100644
--- a/src/components/admin/sections/Places.vue
+++ b/src/components/admin/sections/Places.vue
@@ -13,9 +13,7 @@
>
{{
status.gis_count > 0
- ? t('memories', 'Database is populated with {n} geometries.', {
- n: status.gis_count,
- })
+ ? t('memories', 'Database is populated with {n} geometries.', { n: status.gis_count })
: t('memories', 'Geometry table has not been created.')
}}
{{
diff --git a/src/services/API.ts b/src/services/API.ts
index 0ce33cf00..07fcd6605 100644
--- a/src/services/API.ts
+++ b/src/services/API.ts
@@ -198,6 +198,10 @@ export class API {
return gen(`${BASE}/system-status`);
}
+ static FAILURE_LOGS() {
+ return gen(`${BASE}/failure-logs`);
+ }
+
static OCC_PLACES_SETUP() {
return gen(`${BASE}/occ/places-setup`);
}