feat(preview): Expire previews

Signed-off-by: provokateurin <kate@provokateurin.de>
This commit is contained in:
provokateurin 2025-10-08 18:22:10 +02:00
parent 1a5679b176
commit fe9e43c165
No known key found for this signature in database
9 changed files with 96 additions and 4 deletions

View file

@ -10,7 +10,7 @@ $nextcloudDir = dirname(__DIR__);
return (require __DIR__ . '/rector-shared.php')
->withPaths([
$nextcloudDir . '/build/rector-strict.php',
// TODO: Add more files. The entry above is just there to stop rector from complaining about the fact that it ran without checking any files.
$nextcloudDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
])
->withPreparedSets(
deadCode: true,

View file

@ -2917,4 +2917,12 @@ $CONFIG = [
'fe80::/10',
'10.0.0.1',
],
/**
* Delete previews older than a certain number of days to reduce storage usage.
* Less than one day is not allowed, so set it to 0 to disable the deletion.
*
* Defaults to ``0``.
*/
'preview_expiration_days' => 0,
];

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\BackgroundJobs;
use OC\Preview\PreviewService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
class ExpirePreviewsJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private readonly IConfig $config,
private readonly PreviewService $service,
) {
parent::__construct($time);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
$this->setInterval(60 * 60 * 24);
}
protected function run(mixed $argument): void {
$days = $this->config->getSystemValueInt('preview_expiration_days');
if ($days <= 0) {
return;
}
$this->service->deleteExpiredPreviews($days);
}
}

View file

@ -1309,6 +1309,7 @@ return array(
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\ExpirePreviewsJob' => $baseDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => $baseDir . '/core/BackgroundJobs/GenerateMetadataJob.php',
'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => $baseDir . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php',
'OC\\Core\\BackgroundJobs\\PreviewMigrationJob' => $baseDir . '/core/BackgroundJobs/PreviewMigrationJob.php',

View file

@ -1350,6 +1350,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php',
'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php',
'OC\\Core\\BackgroundJobs\\ExpirePreviewsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/ExpirePreviewsJob.php',
'OC\\Core\\BackgroundJobs\\GenerateMetadataJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/GenerateMetadataJob.php',
'OC\\Core\\BackgroundJobs\\LookupServerSendCheckBackgroundJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/LookupServerSendCheckBackgroundJob.php',
'OC\\Core\\BackgroundJobs\\PreviewMigrationJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/PreviewMigrationJob.php',

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OC\Preview\Db;
use DateInterval;
use DateTimeImmutable;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
@ -26,6 +28,7 @@ class PreviewMapper extends QBMapper {
private const TABLE_NAME = 'previews';
private const LOCATION_TABLE_NAME = 'preview_locations';
private const VERSION_TABLE_NAME = 'preview_versions';
public const MAX_CHUNK_SIZE = 1000;
public function __construct(
IDBConnection $db,
@ -204,11 +207,16 @@ class PreviewMapper extends QBMapper {
/**
* @return \Generator<Preview>
*/
public function getPreviews(string $lastId, int $limit = 1000): \Generator {
public function getPreviews(string $lastId, int $limit = self::MAX_CHUNK_SIZE, ?int $maxAgeDays = null): \Generator {
$qb = $this->db->getQueryBuilder();
$this->joinLocation($qb)
->where($qb->expr()->gt('p.id', $qb->createNamedParameter($lastId)))
->setMaxResults($limit);
if ($maxAgeDays !== null) {
$qb->andWhere($qb->expr()->lt('mtime', $qb->createNamedParameter((new DateTimeImmutable())->sub(new DateInterval('P' . $maxAgeDays . 'D'))->getTimestamp(), IQueryBuilder::PARAM_INT)));
}
return $this->yieldEntities($qb);
}

View file

@ -112,7 +112,7 @@ class PreviewService {
public function deleteAll(): void {
$lastId = '0';
while (true) {
$previews = $this->previewMapper->getPreviews($lastId, 1000);
$previews = $this->previewMapper->getPreviews($lastId, PreviewMapper::MAX_CHUNK_SIZE);
$i = 0;
// FIXME: Should we use transaction here? Du to the I/O created when
@ -124,7 +124,7 @@ class PreviewService {
$lastId = $preview->getId();
}
if ($i !== 1000) {
if ($i !== PreviewMapper::MAX_CHUNK_SIZE) {
break;
}
}
@ -137,4 +137,37 @@ class PreviewService {
public function getAvailablePreviews(array $fileIds): array {
return $this->previewMapper->getAvailablePreviews($fileIds);
}
public function deleteExpiredPreviews(int $maxAgeDays): void {
$lastId = '0';
$startTime = time();
while (true) {
try {
$this->connection->beginTransaction();
$previews = $this->previewMapper->getPreviews($lastId, PreviewMapper::MAX_CHUNK_SIZE, $maxAgeDays);
$i = 0;
foreach ($previews as $preview) {
$this->deletePreview($preview);
$i++;
$lastId = $preview->getId();
}
$this->connection->commit();
if ($i !== PreviewMapper::MAX_CHUNK_SIZE) {
break;
}
} catch (Exception $e) {
$this->connection->commit();
throw $e;
}
// Stop if execution time is more than one hour.
if (time() - $startTime > 3600) {
return;
}
}
}
}

View file

@ -15,6 +15,7 @@ use InvalidArgumentException;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Authentication\Token\PublicKeyTokenProvider;
use OC\Authentication\Token\TokenCleanupJob;
use OC\Core\BackgroundJobs\ExpirePreviewsJob;
use OC\Core\BackgroundJobs\GenerateMetadataJob;
use OC\Core\BackgroundJobs\PreviewMigrationJob;
use OC\Log\Rotate;
@ -526,6 +527,7 @@ class Setup {
$jobList->add(CleanupDeletedUsers::class);
$jobList->add(GenerateMetadataJob::class);
$jobList->add(PreviewMigrationJob::class);
$jobList->add(ExpirePreviewsJob::class);
}
/**

View file

@ -16,6 +16,7 @@
phpVersion="8.2"
>
<projectFiles>
<file name="core/BackgroundJobs/ExpirePreviewsJob.php"/>
<ignoreFiles>
<directory name="apps/**/composer"/>
<directory name="apps/**/tests"/>