Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f82ea00
feat(preview): honor MIME priority, cache headers, and record failures
rayvincent2 Aug 23, 2026
b764d61
feat(settings): add Previews administration page
rayvincent2 Aug 23, 2026
714478a
feat(settings): expose remaining preview settings with detection
rayvincent2 Aug 23, 2026
d32b86b
feat(settings): show inactive preview settings with unlock states
rayvincent2 Aug 23, 2026
f601416
feat(settings): treat enable previews as a page-level master switch
rayvincent2 Aug 25, 2026
e127de1
feat(settings): warn on unsaved Previews changes and duplicate Save
rayvincent2 Aug 25, 2026
7167059
feat(settings): drop MIME priority and finish Previews admin UX
rayvincent2 Aug 26, 2026
dfd838f
feat(settings): refine Previews admin defaults, filters, and failures
rayvincent2 Aug 26, 2026
f6119cb
chore(settings): fix Previews admin lint and Playwright specs
rayvincent2 Aug 26, 2026
e6ad2da
feat(settings): lock preview providers that fail requirements
rayvincent2 Aug 26, 2026
70c705c
feat(preview): honor enabledPreviewProviders as generation order
rayvincent2 Aug 26, 2026
6f5478f
chore(settings): fix Previews admin watch eslint
rayvincent2 Aug 26, 2026
725f56b
fix(settings): show Previews icon in admin navigation
rayvincent2 Aug 29, 2026
fda7774
test(settings): fix Previews admin Playwright locators
rayvincent2 Aug 29, 2026
a109b4b
feat(settings): drop failures UI and align Previews admin with review…
rayvincent2 Aug 30, 2026
3933746
fix(settings): wire Previews admin hints to their inputs
rayvincent2 Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REUSE.toml

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions apps/files_sharing/lib/Controller/PublicPreviewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\Files_Sharing\Controller;

use OC\Preview\Failure\PreviewFailureService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\NoSameSiteCookieRequired;
Expand Down Expand Up @@ -40,6 +41,7 @@ public function __construct(
ISession $session,
private IPreview $previewManager,
private IMimeIconProvider $mimeIconProvider,
private ?PreviewFailureService $failureService = null,
) {
parent::__construct($appName, $request, $session);
}
Expand Down Expand Up @@ -152,13 +154,19 @@ public function getPreview(

$response->cacheFor($cacheForSeconds);
return $response;
} catch (NotFoundException) {
} catch (NotFoundException $e) {
// If a preview could not be generated for a resolved file, we can redirect to the mime icon if any
if ($mimeFallback && $previewFile instanceof File) {
if ($url = $this->mimeIconProvider->getMimeIconUrl($previewFile->getMimeType())) {
return new RedirectResponse($url);
}
}
if ($previewFile instanceof File) {
$this->failureService?->recordFromFailedRequest(
$previewFile,
$e->getMessage() !== '' ? $e->getMessage() : 'Preview not found',
);
}
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (NotPermittedException) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
Expand Down Expand Up @@ -209,6 +217,7 @@ public function directLink(string $token) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}

$node = null;
try {
$node = $share->getNode();
if ($node instanceof Folder) {
Expand All @@ -220,12 +229,19 @@ public function directLink(string $token) {
$response = new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]);
$response->cacheFor(3600 * 24);
return $response;
} catch (NotFoundException) {
} catch (NotFoundException $e) {
if ($node instanceof File) {
$this->failureService?->recordFromFailedRequest(
$node,
$e->getMessage() !== '' ? $e->getMessage() : 'Preview not found',
);
}
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (NotPermittedException) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
} catch (\InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
}

}
9 changes: 9 additions & 0 deletions apps/files_trashbin/lib/Controller/PreviewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Files_Trashbin\Controller;

use OC\Preview\Failure\PreviewFailureService;
use OCA\Files_Trashbin\Trash\ITrashManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
Expand Down Expand Up @@ -37,6 +38,7 @@ public function __construct(
private IMimeTypeDetector $mimeTypeDetector,
private IPreview $previewManager,
private ITimeFactory $time,
private ?PreviewFailureService $failureService = null,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -67,6 +69,7 @@ public function getPreview(
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

$file = null;
try {
$file = $this->trashManager->getTrashNodeById($this->userSession->getUser(), $fileId);
if ($file === null) {
Expand Down Expand Up @@ -96,6 +99,12 @@ public function getPreview(
$response->cacheFor(3600 * 24);
return $response;
} catch (NotFoundException $e) {
if ($file instanceof \OCP\Files\File) {
$this->failureService?->recordFromFailedRequest(
$file,
$e->getMessage() !== '' ? $e->getMessage() : 'Preview not found',
);
}
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (\InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
Expand Down
8 changes: 8 additions & 0 deletions apps/files_versions/lib/Controller/PreviewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\Files_Versions\Controller;

use OC\Preview\Failure\PreviewFailureService;
use OCA\Files_Versions\Versions\IVersionManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
Expand Down Expand Up @@ -34,6 +35,7 @@ public function __construct(
private IVersionManager $versionManager,
private IPreview $previewManager,
private IMimeIconProvider $mimeIconProvider,
private ?PreviewFailureService $failureService = null,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -85,6 +87,12 @@ public function getPreview(
}
}

if ($versionFile instanceof \OCP\Files\File) {
$this->failureService?->recordFromFailedRequest(
$versionFile,
$e->getMessage() !== '' ? $e->getMessage() : 'Preview not found',
);
}
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (\InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
Expand Down
2 changes: 2 additions & 0 deletions apps/settings/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<admin>OCA\Settings\Settings\Admin\ArtificialIntelligence</admin>
<admin>OCA\Settings\Settings\Admin\Server</admin>
<admin>OCA\Settings\Settings\Admin\Sharing</admin>
<admin>OCA\Settings\Settings\Admin\Previews</admin>
<admin>OCA\Settings\Settings\Admin\Security</admin>
<admin>OCA\Settings\Settings\Admin\Delegation</admin>
<admin-section>OCA\Settings\Sections\Admin\Additional</admin-section>
Expand All @@ -45,6 +46,7 @@
<admin-section>OCA\Settings\Sections\Admin\Security</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Server</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Sharing</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Previews</admin-section>
<admin-delegation>OCA\Settings\Settings\Admin\Users</admin-delegation>
<admin-delegation-section>OCA\Settings\Sections\Admin\Users</admin-delegation-section>
<personal>OCA\Settings\Settings\Personal\Additional</personal>
Expand Down
6 changes: 6 additions & 0 deletions apps/settings/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
['name' => 'TwoFactorSettings#index', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'GET' , 'root' => ''],
['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT' , 'root' => ''],
['name' => 'AISettings#update', 'url' => '/settings/api/admin/ai', 'verb' => 'PUT' , 'root' => ''],
['name' => 'PreviewAdmin#update', 'url' => '/settings/api/admin/previews', 'verb' => 'PUT' , 'root' => ''],
['name' => 'PreviewAdmin#testImaginary', 'url' => '/settings/api/admin/previews/imaginary/test', 'verb' => 'POST' , 'root' => ''],
['name' => 'PreviewAdmin#listFailures', 'url' => '/settings/api/admin/previews/failures', 'verb' => 'GET' , 'root' => ''],
['name' => 'PreviewAdmin#retryFailure', 'url' => '/settings/api/admin/previews/failures/{id}/retry', 'verb' => 'POST' , 'root' => ''],
['name' => 'PreviewAdmin#deleteFailure', 'url' => '/settings/api/admin/previews/failures/{id}', 'verb' => 'DELETE' , 'root' => ''],
['name' => 'PreviewAdmin#clearFailures', 'url' => '/settings/api/admin/previews/failures', 'verb' => 'DELETE' , 'root' => ''],
Comment thread
rayvincent2 marked this conversation as resolved.
Outdated

['name' => 'Preset#getPreset', 'url' => '/settings/preset', 'verb' => 'GET' , 'root' => ''],
['name' => 'Preset#getCurrentPreset', 'url' => '/settings/preset/current', 'verb' => 'GET' , 'root' => ''],
Expand Down
3 changes: 3 additions & 0 deletions apps/settings/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'OCA\\Settings\\Controller\\MailSettingsController' => $baseDir . '/../lib/Controller/MailSettingsController.php',
'OCA\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/../lib/Controller/PersonalSettingsController.php',
'OCA\\Settings\\Controller\\PresetController' => $baseDir . '/../lib/Controller/PresetController.php',
'OCA\\Settings\\Controller\\PreviewAdminController' => $baseDir . '/../lib/Controller/PreviewAdminController.php',
'OCA\\Settings\\Controller\\ReasonsController' => $baseDir . '/../lib/Controller/ReasonsController.php',
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php',
'OCA\\Settings\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php',
Expand All @@ -57,6 +58,7 @@
'OCA\\Settings\\Sections\\Admin\\Office' => $baseDir . '/../lib/Sections/Admin/Office.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => $baseDir . '/../lib/Sections/Admin/Overview.php',
'OCA\\Settings\\Sections\\Admin\\Presets' => $baseDir . '/../lib/Sections/Admin/Presets.php',
'OCA\\Settings\\Sections\\Admin\\Previews' => $baseDir . '/../lib/Sections/Admin/Previews.php',
'OCA\\Settings\\Sections\\Admin\\Security' => $baseDir . '/../lib/Sections/Admin/Security.php',
'OCA\\Settings\\Sections\\Admin\\Server' => $baseDir . '/../lib/Sections/Admin/Server.php',
'OCA\\Settings\\Sections\\Admin\\Sharing' => $baseDir . '/../lib/Sections/Admin/Sharing.php',
Expand All @@ -77,6 +79,7 @@
'OCA\\Settings\\Settings\\Admin\\MailProvider' => $baseDir . '/../lib/Settings/Admin/MailProvider.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => $baseDir . '/../lib/Settings/Admin/Overview.php',
'OCA\\Settings\\Settings\\Admin\\Presets' => $baseDir . '/../lib/Settings/Admin/Presets.php',
'OCA\\Settings\\Settings\\Admin\\Previews' => $baseDir . '/../lib/Settings/Admin/Previews.php',
'OCA\\Settings\\Settings\\Admin\\Security' => $baseDir . '/../lib/Settings/Admin/Security.php',
'OCA\\Settings\\Settings\\Admin\\Server' => $baseDir . '/../lib/Settings/Admin/Server.php',
'OCA\\Settings\\Settings\\Admin\\Sharing' => $baseDir . '/../lib/Settings/Admin/Sharing.php',
Expand Down
3 changes: 3 additions & 0 deletions apps/settings/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/..' . '/../lib/Controller/MailSettingsController.php',
'OCA\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/..' . '/../lib/Controller/PersonalSettingsController.php',
'OCA\\Settings\\Controller\\PresetController' => __DIR__ . '/..' . '/../lib/Controller/PresetController.php',
'OCA\\Settings\\Controller\\PreviewAdminController' => __DIR__ . '/..' . '/../lib/Controller/PreviewAdminController.php',
'OCA\\Settings\\Controller\\ReasonsController' => __DIR__ . '/..' . '/../lib/Controller/ReasonsController.php',
'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php',
'OCA\\Settings\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php',
Expand All @@ -72,6 +73,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Sections\\Admin\\Office' => __DIR__ . '/..' . '/../lib/Sections/Admin/Office.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Sections/Admin/Overview.php',
'OCA\\Settings\\Sections\\Admin\\Presets' => __DIR__ . '/..' . '/../lib/Sections/Admin/Presets.php',
'OCA\\Settings\\Sections\\Admin\\Previews' => __DIR__ . '/..' . '/../lib/Sections/Admin/Previews.php',
'OCA\\Settings\\Sections\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Sections/Admin/Security.php',
'OCA\\Settings\\Sections\\Admin\\Server' => __DIR__ . '/..' . '/../lib/Sections/Admin/Server.php',
'OCA\\Settings\\Sections\\Admin\\Sharing' => __DIR__ . '/..' . '/../lib/Sections/Admin/Sharing.php',
Expand All @@ -92,6 +94,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Settings\\Admin\\MailProvider' => __DIR__ . '/..' . '/../lib/Settings/Admin/MailProvider.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Settings/Admin/Overview.php',
'OCA\\Settings\\Settings\\Admin\\Presets' => __DIR__ . '/..' . '/../lib/Settings/Admin/Presets.php',
'OCA\\Settings\\Settings\\Admin\\Previews' => __DIR__ . '/..' . '/../lib/Settings/Admin/Previews.php',
'OCA\\Settings\\Settings\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Settings/Admin/Security.php',
'OCA\\Settings\\Settings\\Admin\\Server' => __DIR__ . '/..' . '/../lib/Settings/Admin/Server.php',
'OCA\\Settings\\Settings\\Admin\\Sharing' => __DIR__ . '/..' . '/../lib/Settings/Admin/Sharing.php',
Expand Down
1 change: 1 addition & 0 deletions apps/settings/img/previews.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
167 changes: 167 additions & 0 deletions apps/settings/lib/Controller/PreviewAdminController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Settings\Controller;

use OC\Preview\Failure\PreviewFailureService;
use OC\Preview\PreviewAdminConfig;
use OCA\Settings\Settings\Admin\Previews;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Http\Client\IClientService;
use OCP\IPreview;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

class PreviewAdminController extends Controller {
Comment thread
rayvincent2 marked this conversation as resolved.
Outdated
public function __construct(
string $appName,
IRequest $request,
private PreviewAdminConfig $previewAdminConfig,
private PreviewFailureService $failureService,
private IClientService $clientService,
private IPreview $preview,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}

/**
* @param array $settings
*/
#[AuthorizedAdminSetting(settings: Previews::class)]
#[PasswordConfirmationRequired]
public function update(array $settings): DataResponse {
try {
$this->previewAdminConfig->setSettings($settings);
} catch (\InvalidArgumentException $e) {
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}

return new DataResponse($this->previewAdminConfig->getSettings());
}

#[AuthorizedAdminSetting(settings: Previews::class)]
public function testImaginary(?string $url = null, ?string $key = null): DataResponse {
try {
$target = $this->previewAdminConfig->validateImaginaryUrl($url ?? '');
} catch (\InvalidArgumentException $e) {
return new DataResponse([
'status' => 'unreachable',
'error' => $e->getMessage(),
], Http::STATUS_BAD_REQUEST);
}

if ($target === '') {
return new DataResponse([
'status' => 'unconfigured',
]);
}

try {
$client = $this->clientService->newClient();
$options = [
'timeout' => 3,
'connect_timeout' => 3,
'nextcloud' => ['allow_local_address' => true],
];
if (is_string($key) && $key !== '') {
$options['query'] = ['key' => $key];
}
$response = $client->get($target, $options);
$statusCode = $response->getStatusCode();
$reachable = $statusCode >= 200 && $statusCode < 500;
return new DataResponse([
'status' => $reachable ? 'reachable' : 'unreachable',
'httpCode' => $statusCode,
]);
} catch (\Throwable $e) {
$this->logger->info('Imaginary connection test failed', [
'exception' => $e,
]);
return new DataResponse([
'status' => 'unreachable',
'error' => $e->getMessage(),
]);
}
}

#[AuthorizedAdminSetting(settings: Previews::class)]
public function listFailures(?string $mime = null, ?string $provider = null, ?string $range = null): DataResponse {
$since = $this->rangeToSince($range);
return new DataResponse([
'failures' => $this->failureService->listFailures($mime, $provider, $since),
]);
}

#[AuthorizedAdminSetting(settings: Previews::class)]
public function retryFailure(int $id): DataResponse {
try {
$failure = $this->failureService->get($id);
} catch (DoesNotExistException) {
return new DataResponse(['error' => 'Unknown failure'], Http::STATUS_NOT_FOUND);
}

$nodes = $this->rootFolder->getById($failure->getFileId());
$file = null;
foreach ($nodes as $node) {
if ($node instanceof File) {
$file = $node;
break;
}
}
if ($file === null) {
return new DataResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND);
}

try {
$this->preview->getPreview($file);
$this->failureService->clearForFile($failure->getFileId());
return new DataResponse(['status' => 'ok']);
} catch (NotFoundException|\InvalidArgumentException $e) {
return new DataResponse([
'status' => 'failed',
'error' => $e->getMessage(),
], Http::STATUS_BAD_REQUEST);
}
}

#[AuthorizedAdminSetting(settings: Previews::class)]
public function deleteFailure(int $id): DataResponse {
try {
$this->failureService->delete($id);
} catch (DoesNotExistException) {
return new DataResponse(['error' => 'Unknown failure'], Http::STATUS_NOT_FOUND);
}
return new DataResponse(['status' => 'ok']);
}

#[AuthorizedAdminSetting(settings: Previews::class)]
public function clearFailures(): DataResponse {
$this->failureService->clearAll();
return new DataResponse(['status' => 'ok']);
}

private function rangeToSince(?string $range): ?int {
return match ($range) {
'24h' => time() - 86400,
'7d' => time() - 7 * 86400,
'30d' => time() - 30 * 86400,
default => null,
};
}
}
Loading