Skip to content

Commit d002d04

Browse files
juliusknorrbackportbot[bot]
authored andcommitted
feat: make preview conversion timeout and max file size configurable
Signed-off-by: Julius Knorr <jus@bitgrid.net>
1 parent b08cb22 commit d002d04

6 files changed

Lines changed: 188 additions & 6 deletions

File tree

docs/app_settings.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ By default Nextcloud will generate previews of Office files using the Collabora
4141

4242
occ config:app:set richdocuments preview_generation --type boolean --lazy --value false
4343

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+
4452
### Electronic signature
4553
From a shell running in the Nextcloud root directory, run the following `occ`
4654
command to configure a non-default base URL for eID Easy. For example:

lib/AppConfig.php

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

34+
public const PREVIEW_CONVERSION_TIMEOUT = 'preview_conversion_timeout';
35+
public const PREVIEW_CONVERSION_MAX_FILESIZE = 'preview_conversion_max_filesize';
36+
3437
private array $defaults = [
3538
'wopi_url' => '',
3639
'timeout' => 15,
40+
'preview_conversion_timeout' => 5,
41+
'preview_conversion_max_filesize' => 104857600, // 100 MB
3742
'watermark_text' => '{userId}',
3843
'watermark_allGroupsList' => [],
3944
'watermark_allTagsList' => [],
@@ -247,6 +252,21 @@ public function isPreviewGenerationEnabled(): bool {
247252
return $this->appConfig->getAppValueBool('preview_generation', true);
248253
}
249254

255+
/**
256+
* Returns the timeout in seconds for preview conversion requests to Collabora.
257+
*/
258+
public function getPreviewConversionTimeout(): int {
259+
return (int)$this->getAppValue(self::PREVIEW_CONVERSION_TIMEOUT);
260+
}
261+
262+
/**
263+
* Returns the maximum file size in bytes for which preview conversion is attempted.
264+
* Files larger than this limit will be skipped and return no preview.
265+
*/
266+
public function getPreviewConversionMaxFileSize(): int {
267+
return (int)$this->getAppValue(self::PREVIEW_CONVERSION_MAX_FILESIZE);
268+
}
269+
250270
private function getGSDomains(): array {
251271
if (!$this->globalScaleConfig->isGlobalScaleEnabled()) {
252272
return [];

lib/Preview/Office.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,22 @@ public function isAvailable(FileInfo $file): bool {
3737

3838
#[\Override]
3939
public function getThumbnail(File $file, int $maxX, int $maxY): ?IImage {
40-
if ($file->getSize() === 0) {
40+
$fileSize = $file->getSize();
41+
if ($fileSize === 0) {
42+
return null;
43+
}
44+
45+
$maxFileSize = $this->appConfig->getPreviewConversionMaxFileSize();
46+
if ($fileSize > $maxFileSize) {
47+
$this->logger->debug('Skipping preview conversion: file size {size} exceeds limit {limit}', [
48+
'size' => $fileSize,
49+
'limit' => $maxFileSize,
50+
]);
4151
return null;
4252
}
4353

4454
try {
45-
$response = $this->remoteService->convertFileTo($file, 'png');
55+
$response = $this->remoteService->convertFileTo($file, 'png', $this->appConfig->getPreviewConversionTimeout());
4656
$image = new Image();
4757
$image->loadFromData($response);
4858

lib/Service/RemoteService.php

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

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

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

tests/lib/AppConfigTest.php

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

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

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)