Skip to content

Commit 0bbbcde

Browse files
authored
Merge pull request #5717 from nextcloud/backport/5408/stable31
[stable31] feat: make preview conversion timeout and max file size configurable
2 parents b54d7e3 + d3c5c6d commit 0bbbcde

6 files changed

Lines changed: 210 additions & 6 deletions

File tree

docs/app_settings.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ token. These credentials then can be used by the 3rd party application to make c
2525
### Canonical webroot
2626
Canonical webroot, in case there are multiple, for Collabora Online to use. Provide the one with least restrictions. E.g.: Use non-shibbolized webroot if this instance is accessed by both shibbolized and non-shibbolized webroots. You can ignore this setting if only one webroot is used to access this instance.
2727

28+
### Theme
29+
30+
By default Nextcloud Office comes with Nextcloud theme (monochrome icons following Nextcloud style), to change to a more traditional office look:
31+
32+
occ config:app:set richdocuments theme --value="collabora"
33+
34+
To go back to default theme:
35+
36+
occ config:app:set richdocuments theme --value="nextcloud"
37+
38+
### Previews
39+
40+
By default Nextcloud will generate previews of Office files using the Collabora file conversion endpoint. This can be turned off through
41+
42+
occ config:app:set richdocuments preview_generation --type boolean --lazy --value false
43+
44+
The timeout for preview conversion requests (in seconds) can be configured. The default is 5 seconds:
45+
46+
occ config:app:set richdocuments preview_conversion_timeout --type integer --value 10
47+
48+
Files larger than the configured maximum file size will be skipped and no preview will be generated. The default limit is 100 MB (104857600 bytes):
49+
50+
occ config:app:set richdocuments preview_conversion_max_filesize --type integer --value 52428800
51+
2852
### Electronic signature
2953
From a shell running in the Nextcloud root directory, run the following `occ`
3054
command to configure a non-default base URL for eID Easy. For example:

lib/AppConfig.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ class AppConfig {
3030
// Default: 'no', set to 'yes' to enable
3131
public const USE_SECURE_VIEW_ADDITIONAL_MIMES = 'use_secure_view_additional_mimes';
3232

33+
public const PREVIEW_CONVERSION_TIMEOUT = 'preview_conversion_timeout';
34+
public const PREVIEW_CONVERSION_MAX_FILESIZE = 'preview_conversion_max_filesize';
35+
3336
private array $defaults = [
3437
'wopi_url' => '',
3538
'timeout' => 15,
39+
'preview_conversion_timeout' => 5,
40+
'preview_conversion_max_filesize' => 104857600, // 100 MB
3641
'watermark_text' => '{userId}',
3742
'watermark_allGroupsList' => [],
3843
'watermark_allTagsList' => [],
@@ -240,6 +245,25 @@ private function getFederationDomains(): array {
240245
return array_map(fn ($url) => $this->domainOnly($url), array_merge($trustedNextcloudDomains, $trustedCollaboraDomains));
241246
}
242247

248+
public function isPreviewGenerationEnabled(): bool {
249+
return $this->config->getAppValue(Application::APPNAME, 'preview_generation', 'true') !== 'false';
250+
}
251+
252+
/**
253+
* Returns the timeout in seconds for preview conversion requests to Collabora.
254+
*/
255+
public function getPreviewConversionTimeout(): int {
256+
return (int)$this->getAppValue(self::PREVIEW_CONVERSION_TIMEOUT);
257+
}
258+
259+
/**
260+
* Returns the maximum file size in bytes for which preview conversion is attempted.
261+
* Files larger than this limit will be skipped and return no preview.
262+
*/
263+
public function getPreviewConversionMaxFileSize(): int {
264+
return (int)$this->getAppValue(self::PREVIEW_CONVERSION_MAX_FILESIZE);
265+
}
266+
243267
private function getGSDomains(): array {
244268
if (!$this->globalScaleConfig->isGlobalScaleEnabled()) {
245269
return [];

lib/Preview/Office.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
namespace OCA\Richdocuments\Preview;
77

8+
use OCA\Richdocuments\AppConfig;
89
use OCA\Richdocuments\Capabilities;
910
use OCA\Richdocuments\Service\RemoteService;
1011
use OCP\Files\File;
@@ -20,6 +21,7 @@ abstract class Office implements IProviderV2 {
2021
public function __construct(
2122
private RemoteService $remoteService,
2223
private LoggerInterface $logger,
24+
private AppConfig $appConfig,
2325
Capabilities $capabilities,
2426
) {
2527
$this->capabilities = $capabilities->getCapabilities()['richdocuments'] ?? [];
@@ -33,12 +35,22 @@ public function isAvailable(FileInfo $file): bool {
3335
}
3436

3537
public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
36-
if ($file->getSize() === 0) {
38+
$fileSize = $file->getSize();
39+
if ($fileSize === 0) {
40+
return null;
41+
}
42+
43+
$maxFileSize = $this->appConfig->getPreviewConversionMaxFileSize();
44+
if ($fileSize > $maxFileSize) {
45+
$this->logger->debug('Skipping preview conversion: file size {size} exceeds limit {limit}', [
46+
'size' => $fileSize,
47+
'limit' => $maxFileSize,
48+
]);
3749
return null;
3850
}
3951

4052
try {
41-
$response = $this->remoteService->convertFileTo($file, 'png');
53+
$response = $this->remoteService->convertFileTo($file, 'png', $this->appConfig->getPreviewConversionTimeout());
4254
$image = new Image();
4355
$image->loadFromData($response);
4456

lib/Service/RemoteService.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,28 @@ public function fetchTargetThumbnail(File $file, string $target): ?string {
5757
/**
5858
* @return resource|string
5959
*/
60-
public function convertFileTo(File $file, string $format) {
60+
public function convertFileTo(File $file, string $format, int $timeout = RemoteOptionsService::REMOTE_TIMEOUT_DEFAULT) {
6161
$fileName = $file->getStorage()->getLocalFile($file->getInternalPath());
6262
$stream = fopen($fileName, 'rb');
6363

6464
if ($stream === false) {
6565
throw new Exception('Failed to open stream');
6666
}
67-
return $this->convertTo($file->getName(), $stream, $format);
67+
68+
try {
69+
return $this->convertTo($file->getName(), $stream, $format, [], $timeout);
70+
} finally {
71+
fclose($stream);
72+
}
6873
}
6974

7075
/**
7176
* @param resource $stream
7277
* @return resource|string
7378
*/
74-
public function convertTo(string $filename, $stream, string $format) {
79+
public function convertTo(string $filename, $stream, string $format, ?array $conversionOptions = [], int $timeout = RemoteOptionsService::REMOTE_TIMEOUT_DEFAULT) {
7580
$client = $this->clientService->newClient();
76-
$options = RemoteOptionsService::getDefaultOptions();
81+
$options = RemoteOptionsService::getDefaultOptions($timeout);
7782
// FIXME: can be removed once https://github.com/CollaboraOnline/online/issues/6983 is fixed upstream
7883
$options['expect'] = false;
7984

tests/lib/AppConfigTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,40 @@ public function testGetAppValueArrayWithNoneValue() {
5858

5959
$this->assertSame([], $result);
6060
}
61+
62+
public function testGetPreviewConversionTimeoutReturnsDefault(): void {
63+
$this->config->expects($this->once())
64+
->method('getAppValue')
65+
->with('richdocuments', 'preview_conversion_timeout', 5)
66+
->willReturn(5);
67+
68+
$this->assertSame(5, $this->appConfig->getPreviewConversionTimeout());
69+
}
70+
71+
public function testGetPreviewConversionTimeoutReturnsConfiguredValue(): void {
72+
$this->config->expects($this->once())
73+
->method('getAppValue')
74+
->with('richdocuments', 'preview_conversion_timeout', 5)
75+
->willReturn(30);
76+
77+
$this->assertSame(30, $this->appConfig->getPreviewConversionTimeout());
78+
}
79+
80+
public function testGetPreviewConversionMaxFileSizeReturnsDefault(): void {
81+
$this->config->expects($this->once())
82+
->method('getAppValue')
83+
->with('richdocuments', 'preview_conversion_max_filesize', 104857600)
84+
->willReturn(104857600);
85+
86+
$this->assertSame(104857600, $this->appConfig->getPreviewConversionMaxFileSize());
87+
}
88+
89+
public function testGetPreviewConversionMaxFileSizeReturnsConfiguredValue(): void {
90+
$this->config->expects($this->once())
91+
->method('getAppValue')
92+
->with('richdocuments', 'preview_conversion_max_filesize', 104857600)
93+
->willReturn(52428800);
94+
95+
$this->assertSame(52428800, $this->appConfig->getPreviewConversionMaxFileSize());
96+
}
6197
}

tests/lib/Preview/OfficeTest.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace Tests\Richdocuments\Preview;
9+
10+
use OCA\Richdocuments\AppConfig;
11+
use OCA\Richdocuments\Capabilities;
12+
use OCA\Richdocuments\Preview\Office;
13+
use OCA\Richdocuments\Service\RemoteService;
14+
use OCP\Files\File;
15+
use PHPUnit\Framework\MockObject\MockObject;
16+
use PHPUnit\Framework\TestCase;
17+
use Psr\Log\LoggerInterface;
18+
19+
class OfficeTest extends TestCase {
20+
private RemoteService&MockObject $remoteService;
21+
private LoggerInterface&MockObject $logger;
22+
private AppConfig&MockObject $appConfig;
23+
private Capabilities&MockObject $capabilities;
24+
private Office $provider;
25+
26+
protected function setUp(): void {
27+
parent::setUp();
28+
29+
$this->remoteService = $this->createMock(RemoteService::class);
30+
$this->logger = $this->createMock(LoggerInterface::class);
31+
$this->appConfig = $this->createMock(AppConfig::class);
32+
$this->capabilities = $this->createMock(Capabilities::class);
33+
$this->capabilities->method('getCapabilities')->willReturn(['richdocuments' => []]);
34+
35+
$this->provider = new class($this->remoteService, $this->logger, $this->appConfig, $this->capabilities) extends Office {
36+
#[\Override]
37+
public function getMimeType(): string {
38+
return '/application\\/test/';
39+
}
40+
};
41+
}
42+
43+
public function testGetThumbnailSkipsConversionWhenFileIsTooLarge(): void {
44+
$file = $this->createMock(File::class);
45+
$file->expects($this->once())->method('getSize')->willReturn(101 * 1024 * 1024);
46+
47+
$this->appConfig->expects($this->once())
48+
->method('getPreviewConversionMaxFileSize')
49+
->willReturn(100 * 1024 * 1024);
50+
$this->remoteService->expects($this->never())->method('convertFileTo');
51+
52+
$result = $this->provider->getThumbnail($file, 64, 64);
53+
54+
$this->assertNull($result);
55+
}
56+
57+
public function testGetThumbnailReturnsNullForEmptyFile(): void {
58+
$file = $this->createMock(File::class);
59+
$file->expects($this->once())->method('getSize')->willReturn(0);
60+
61+
$this->appConfig->expects($this->never())->method('getPreviewConversionMaxFileSize');
62+
$this->remoteService->expects($this->never())->method('convertFileTo');
63+
64+
$result = $this->provider->getThumbnail($file, 64, 64);
65+
66+
$this->assertNull($result);
67+
}
68+
69+
public function testGetThumbnailAttemptsConversionWhenFileSizeIsExactlyAtLimit(): void {
70+
$file = $this->createMock(File::class);
71+
$file->expects($this->once())->method('getSize')->willReturn(100 * 1024 * 1024);
72+
73+
$this->appConfig->expects($this->once())
74+
->method('getPreviewConversionMaxFileSize')
75+
->willReturn(100 * 1024 * 1024);
76+
// Conversion is attempted; throw to keep the test simple (image loading is not unit-tested here)
77+
$this->remoteService->expects($this->once())
78+
->method('convertFileTo')
79+
->with($file, 'png')
80+
->willThrowException(new \Exception('conversion failed'));
81+
82+
$result = $this->provider->getThumbnail($file, 64, 64);
83+
84+
$this->assertNull($result);
85+
}
86+
87+
public function testGetThumbnailReturnsNullWhenConversionFails(): void {
88+
$file = $this->createMock(File::class);
89+
$file->expects($this->once())->method('getSize')->willReturn(1024);
90+
91+
$this->appConfig->expects($this->once())
92+
->method('getPreviewConversionMaxFileSize')
93+
->willReturn(100 * 1024 * 1024);
94+
$this->remoteService->expects($this->once())
95+
->method('convertFileTo')
96+
->with($file, 'png')
97+
->willThrowException(new \Exception('conversion failed'));
98+
99+
$result = $this->provider->getThumbnail($file, 64, 64);
100+
101+
$this->assertNull($result);
102+
}
103+
}

0 commit comments

Comments
 (0)