Skip to content

Commit 04aa4e8

Browse files
authored
Merge pull request #63376 from nextcloud/fix/preview-keep-local-file-on-race
fix(preview): keep the local file when losing the insert race
2 parents f255146 + b235ac7 commit 04aa4e8

6 files changed

Lines changed: 74 additions & 1 deletion

File tree

lib/private/Preview/Generator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ public function savePreview(Preview $previewEntry, IImage $preview): Preview {
594594
throw $e;
595595
}
596596

597-
$this->storageFactory->deletePreview($previewEntry);
597+
$this->storageFactory->deleteUnreferencedPreview($previewEntry);
598598

599599
$this->logger->debug('Generating a preview but one already exists.', ['exception' => $e]);
600600

lib/private/Preview/Storage/IPreviewStorage.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ public function readPreview(Preview $preview): mixed;
3535
*/
3636
public function deletePreview(Preview $preview): void;
3737

38+
/**
39+
* Delete the stored data of a preview that never got a database row, for
40+
* example after losing the insert race on the unique constraint.
41+
*
42+
* Only backends that key their storage by the preview id hold data that
43+
* belongs to this entity alone. The local storage derives its path from
44+
* the preview specification, so the file is the very same one the winner
45+
* of the race is now referencing and has to be kept.
46+
*
47+
* @throws NotPermittedException
48+
*/
49+
public function deleteUnreferencedPreview(Preview $preview): void;
50+
3851
/**
3952
* Migration helper
4053
*

lib/private/Preview/Storage/LocalPreviewStorage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ public function deletePreview(Preview $preview): void {
6969
}
7070
}
7171

72+
#[Override]
73+
public function deleteUnreferencedPreview(Preview $preview): void {
74+
// constructPath() keys on the file id and the preview specification, not
75+
// on the preview id, so this file is shared with the preview that won the
76+
// race. Deleting it would leave that one with a row but no file.
77+
}
78+
7279
public function getRootFolder(): string {
7380
return $this->config->getSystemValueString('datadirectory', OC::$SERVERROOT . '/data');
7481
}

lib/private/Preview/Storage/ObjectStorePreviewStorage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,13 @@ private function getObjectStore(string $objectStoreName, array $config): IObject
167167
return $this->objectStoreCache[$objectStoreName][$bucketName];
168168
}
169169

170+
#[Override]
171+
public function deleteUnreferencedPreview(Preview $preview): void {
172+
// The urn embeds the preview id, so this object belongs to this entity
173+
// alone and can be removed safely.
174+
$this->deletePreview($preview);
175+
}
176+
170177
public function getUrn(Preview $preview, array $config): string {
171178
if ($preview->getOldFileId()) {
172179
return ($config['arguments']['objectPrefix'] ?? 'urn:oid:') . $preview->getOldFileId();

lib/private/Preview/Storage/StorageFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public function deletePreview(Preview $preview): void {
3737
$this->getBackend()->deletePreview($preview);
3838
}
3939

40+
#[Override]
41+
public function deleteUnreferencedPreview(Preview $preview): void {
42+
$this->getBackend()->deleteUnreferencedPreview($preview);
43+
}
44+
4045
private function getBackend(): IPreviewStorage {
4146
if ($this->backend) {
4247
return $this->backend;

tests/lib/Preview/Storage/LocalPreviewStorageTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace Test\Preview\Storage;
1111

12+
use OC\Preview\Db\Preview;
1213
use OC\Preview\Db\PreviewMapper;
1314
use OC\Preview\Storage\LocalPreviewStorage;
1415
use OCP\DB\Exception as DBException;
@@ -19,6 +20,7 @@
1920
use OCP\Files\IMimeTypeDetector;
2021
use OCP\Files\IMimeTypeLoader;
2122
use OCP\Files\IRootFolder;
23+
use OCP\Files\NotFoundException;
2224
use OCP\IAppConfig;
2325
use OCP\IConfig;
2426
use OCP\IDBConnection;
@@ -287,4 +289,43 @@ public function testScanFetchesAllFilecacheRows(): void {
287289

288290
$this->assertSame(3, $count);
289291
}
292+
293+
private function makePreview(int $fileId = self::FILE_ID): Preview {
294+
$preview = new Preview();
295+
$preview->setFileId($fileId);
296+
$preview->setWidth(1024);
297+
$preview->setHeight(768);
298+
$preview->setCropped(false);
299+
$preview->setMax(true);
300+
$preview->setMimetype('image/jpeg');
301+
302+
return $preview;
303+
}
304+
305+
/**
306+
* Losing the insert race must not remove the local file: the path is derived
307+
* from the preview specification, so it is the same file the preview that
308+
* won the race now points at.
309+
*/
310+
public function testDeleteUnreferencedPreviewKeepsTheSharedFile(): void {
311+
$preview = $this->makePreview();
312+
$this->storage->writePreview($preview, 'preview data');
313+
314+
$this->storage->deleteUnreferencedPreview($preview);
315+
316+
$this->assertSame('preview data', stream_get_contents($this->storage->readPreview($preview)));
317+
}
318+
319+
/**
320+
* Deleting a preview for real still has to remove the file.
321+
*/
322+
public function testDeletePreviewRemovesTheFile(): void {
323+
$preview = $this->makePreview();
324+
$this->storage->writePreview($preview, 'preview data');
325+
326+
$this->storage->deletePreview($preview);
327+
328+
$this->expectException(NotFoundException::class);
329+
$this->storage->readPreview($preview);
330+
}
290331
}

0 commit comments

Comments
 (0)