Skip to content

Commit 05c668d

Browse files
committed
feat(Db): Use SnowflakeId for previews
Allow to get an id for the storing the preview on disk before inserting the preview on the DB. Signed-off-by: Carl Schwan <[email protected]>
1 parent 4b1fce7 commit 05c668d

File tree

19 files changed

+260
-43
lines changed

19 files changed

+260
-43
lines changed

core/BackgroundJobs/MovePreviewJob.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OCP\IAppConfig;
2727
use OCP\IConfig;
2828
use OCP\IDBConnection;
29+
use OCP\Snowflake\IGenerator;
2930
use Override;
3031
use Psr\Log\LoggerInterface;
3132

@@ -44,6 +45,7 @@ public function __construct(
4445
private readonly IMimeTypeDetector $mimeTypeDetector,
4546
private readonly IMimeTypeLoader $mimeTypeLoader,
4647
private readonly LoggerInterface $logger,
48+
private readonly IGenerator $generator,
4749
IAppDataFactory $appDataFactory,
4850
) {
4951
parent::__construct($time);
@@ -136,6 +138,7 @@ private function processPreviews(int $fileId, bool $flatPath): void {
136138
$path = $fileId . '/' . $previewFile->getName();
137139
/** @var SimpleFile $previewFile */
138140
$preview = Preview::fromPath($path, $this->mimeTypeDetector);
141+
$preview->setId($this->generator->nextId());
139142
if (!$preview) {
140143
$this->logger->error('Unable to import old preview at path.');
141144
continue;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Migrations;
10+
11+
use Closure;
12+
use OCP\DB\ISchemaWrapper;
13+
use OCP\Migration\Attributes\ModifyColumn;
14+
use OCP\Migration\IOutput;
15+
use OCP\Migration\SimpleMigrationStep;
16+
17+
/**
18+
* Migrate away from auto-increment
19+
*/
20+
#[ModifyColumn(table: 'preview_locations', name: 'id', description: 'Remove auto-increment')]
21+
#[ModifyColumn(table: 'previews', name: 'id', description: 'Remove auto-increment')]
22+
#[ModifyColumn(table: 'preview_versions', name: 'id', description: 'Remove auto-increment')]
23+
class Version33000Date20251023110529 extends SimpleMigrationStep {
24+
/**
25+
* @param Closure(): ISchemaWrapper $schemaClosure The `\Closure` returns a `ISchemaWrapper`
26+
*/
27+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
28+
$schema = $schemaClosure();
29+
30+
if ($schema->hasTable('preview_locations')) {
31+
$schema->dropAutoincrementColumn('preview_locations', 'id');
32+
}
33+
34+
if ($schema->hasTable('preview_versions')) {
35+
$schema->dropAutoincrementColumn('preview_versions', 'id');
36+
}
37+
38+
if ($schema->hasTable('previews')) {
39+
$schema->dropAutoincrementColumn('previews', 'id');
40+
}
41+
42+
return $schema;
43+
}
44+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Migrations;
10+
11+
use Closure;
12+
use OCP\DB\ISchemaWrapper;
13+
use OCP\IDBConnection;
14+
use OCP\Migration\Attributes\AddIndex;
15+
use OCP\Migration\Attributes\IndexType;
16+
use OCP\Migration\IOutput;
17+
use OCP\Migration\SimpleMigrationStep;
18+
19+
/**
20+
* Use unique index for preview_locations
21+
*/
22+
#[AddIndex(table: 'preview_locations', type: IndexType::UNIQUE)]
23+
class Version33000Date20251023120529 extends SimpleMigrationStep {
24+
public function __construct(
25+
private readonly IDBConnection $connection,
26+
) {
27+
}
28+
29+
/**
30+
* @param Closure(): ISchemaWrapper $schemaClosure The `\Closure` returns a `ISchemaWrapper`
31+
*/
32+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
33+
/** @var ISchemaWrapper $schema */
34+
$schema = $schemaClosure();
35+
36+
if ($schema->hasTable('preview_locations')) {
37+
$table = $schema->getTable('preview_locations');
38+
$table->addUniqueIndex(['bucket_name', 'object_store_name'], 'unique_bucket_store');
39+
}
40+
41+
return $schema;
42+
}
43+
44+
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
45+
// This shouldn't run on a production instance, only daily
46+
$qb = $this->connection->getQueryBuilder();
47+
$qb->select('*')
48+
->from('preview_locations');
49+
$result = $qb->executeQuery();
50+
51+
$set = [];
52+
53+
while ($row = $result->fetch()) {
54+
// Iterate over all the rows with duplicated rows
55+
$id = $row['id'];
56+
57+
if (isset($set[$row['bucket_name'] . '_' . $row['object_store_name']])) {
58+
// duplicate
59+
$authoritativeId = $set[$row['bucket_name'] . '_' . $row['object_store_name']];
60+
$qb = $this->connection->getQueryBuilder();
61+
$qb->select('id')
62+
->from('preview_locations')
63+
->where($qb->expr()->eq('bucket_name', $qb->createNamedParameter($row['bucket_name'])))
64+
->andWhere($qb->expr()->eq('object_store_name', $qb->createNamedParameter($row['object_store_name'])))
65+
->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($authoritativeId)));
66+
67+
$result = $qb->executeQuery();
68+
while ($row = $result->fetch()) {
69+
// Update previews entries to the now de-duplicated id
70+
$qb = $this->connection->getQueryBuilder();
71+
$qb->update('previews')
72+
->set('location_id', $qb->createNamedParameter($id))
73+
->where($qb->expr()->eq('id', $qb->createNamedParameter($row['id'])));
74+
$qb->executeStatement();
75+
76+
$qb = $this->connection->getQueryBuilder();
77+
$qb->delete('preview_locations')
78+
->where($qb->expr()->eq('id', $qb->createNamedParameter($row['id'])));
79+
$qb->executeStatement();
80+
}
81+
break;
82+
}
83+
$set[$row['bucket_name'] . '_' . $row['object_store_name']] = $row['id'];
84+
}
85+
}
86+
}

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,8 @@
15301530
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
15311531
'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
15321532
'OC\\Core\\Migrations\\Version33000Date20250819110529' => $baseDir . '/core/Migrations/Version33000Date20250819110529.php',
1533+
'OC\\Core\\Migrations\\Version33000Date20251023110529' => $baseDir . '/core/Migrations/Version33000Date20251023110529.php',
1534+
'OC\\Core\\Migrations\\Version33000Date20251023120529' => $baseDir . '/core/Migrations/Version33000Date20251023120529.php',
15331535
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
15341536
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
15351537
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
15711571
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
15721572
'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
15731573
'OC\\Core\\Migrations\\Version33000Date20250819110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20250819110529.php',
1574+
'OC\\Core\\Migrations\\Version33000Date20251023110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023110529.php',
1575+
'OC\\Core\\Migrations\\Version33000Date20251023120529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023120529.php',
15741576
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
15751577
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
15761578
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',

lib/private/AppFramework/Http/Dispatcher.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ private function executeController(Controller $controller, string $methodName):
204204
try {
205205
$response = \call_user_func_array([$controller, $methodName], $arguments);
206206
} catch (\TypeError $e) {
207-
// Only intercept TypeErrors occuring on the first line, meaning that the invocation of the controller method failed.
207+
// Only intercept TypeErrors occurring on the first line, meaning that the invocation of the controller method failed.
208208
// Any other TypeError happens inside the controller method logic and should be logged as normal.
209209
if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
210210
$this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);

lib/private/DB/SchemaWrapper.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88

99
use Doctrine\DBAL\Exception;
1010
use Doctrine\DBAL\Platforms\AbstractPlatform;
11+
use Doctrine\DBAL\Platforms\OraclePlatform;
1112
use Doctrine\DBAL\Schema\Schema;
1213
use OCP\DB\ISchemaWrapper;
14+
use OCP\Server;
15+
use Psr\Log\LoggerInterface;
1316

1417
class SchemaWrapper implements ISchemaWrapper {
1518
/** @var Connection */
@@ -131,4 +134,18 @@ public function getTables() {
131134
public function getDatabasePlatform() {
132135
return $this->connection->getDatabasePlatform();
133136
}
137+
138+
public function dropAutoincrementColumn(string $table, string $column): void {
139+
$tableObj = $this->schema->getTable($this->connection->getPrefix() . $table);
140+
$tableObj->modifyColumn('id', ['autoincrement' => false]);
141+
$platform = $this->getDatabasePlatform();
142+
if ($platform instanceof OraclePlatform) {
143+
try {
144+
$this->connection->executeStatement('DROP TRIGGER "' . $this->connection->getPrefix() . $table . '_AI_PK"');
145+
$this->connection->executeStatement('DROP SEQUENCE "' . $this->connection->getPrefix() . $table . '_SEQ"');
146+
} catch (Exception $e) {
147+
Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
148+
}
149+
}
150+
}
134151
}

lib/private/Preview/Db/Preview.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
/**
1818
* Preview entity mapped to the oc_previews and oc_preview_locations table.
1919
*
20+
* @method string getId()
21+
* @method void setId(string $id)
2022
* @method int getFileId() Get the file id of the original file.
2123
* @method void setFileId(int $fileId)
2224
* @method int getStorageId() Get the storage id of the original file.
2325
* @method void setStorageId(int $fileId)
2426
* @method int getOldFileId() Get the old location in the file-cache table, for legacy compatibility.
2527
* @method void setOldFileId(int $oldFileId)
26-
* @method int getLocationId() Get the location id in the preview_locations table. Only set when using an object store as primary storage.
27-
* @method void setLocationId(int $locationId)
28+
* @method string getLocationId() Get the location id in the preview_locations table. Only set when using an object store as primary storage.
29+
* @method void setLocationId(string $locationId)
2830
* @method string|null getBucketName() Get the bucket name where the preview is stored. This is stored in the preview_locations table.
2931
* @method string|null getObjectStoreName() Get the object store name where the preview is stored. This is stored in the preview_locations table.
3032
* @method int getWidth() Get the width of the preview.
@@ -46,7 +48,7 @@
4648
* @method string getEtag() Get the etag of the preview.
4749
* @method void setEtag(string $etag)
4850
* @method string|null getVersion() Get the version for files_versions_s3
49-
* @method void setVersionId(int $versionId)
51+
* @method void setVersionId(string $versionId)
5052
* @method bool|null getIs() Get the version for files_versions_s3
5153
* @method bool isEncrypted() Get whether the preview is encrypted. At the moment every preview is unencrypted.
5254
* @method void setEncrypted(bool $encrypted)
@@ -57,7 +59,7 @@ class Preview extends Entity {
5759
protected ?int $fileId = null;
5860
protected ?int $oldFileId = null;
5961
protected ?int $storageId = null;
60-
protected ?int $locationId = null;
62+
protected ?string $locationId = null;
6163
protected ?string $bucketName = null;
6264
protected ?string $objectStoreName = null;
6365
protected ?int $width = null;
@@ -72,14 +74,15 @@ class Preview extends Entity {
7274
protected ?bool $cropped = null;
7375
protected ?string $etag = null;
7476
protected ?string $version = null;
75-
protected ?int $versionId = null;
77+
protected ?string $versionId = null;
7678
protected ?bool $encrypted = null;
7779

7880
public function __construct() {
81+
$this->addType('id', Types::STRING);
7982
$this->addType('fileId', Types::BIGINT);
8083
$this->addType('storageId', Types::BIGINT);
8184
$this->addType('oldFileId', Types::BIGINT);
82-
$this->addType('locationId', Types::BIGINT);
85+
$this->addType('locationId', Types::STRING);
8386
$this->addType('width', Types::INTEGER);
8487
$this->addType('height', Types::INTEGER);
8588
$this->addType('mimetypeId', Types::INTEGER);

lib/private/Preview/Db/PreviewMapper.php

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCP\DB\QueryBuilder\IQueryBuilder;
1616
use OCP\Files\IMimeTypeLoader;
1717
use OCP\IDBConnection;
18+
use OCP\Snowflake\IGenerator;
1819
use Override;
1920

2021
/**
@@ -29,6 +30,7 @@ class PreviewMapper extends QBMapper {
2930
public function __construct(
3031
IDBConnection $db,
3132
private readonly IMimeTypeLoader $mimeTypeLoader,
33+
private readonly IGenerator $snowflake,
3234
) {
3335
parent::__construct($db, self::TABLE_NAME, Preview::class);
3436
}
@@ -50,13 +52,15 @@ public function insert(Entity $entity): Entity {
5052

5153
if ($preview->getVersion() !== null && $preview->getVersion() !== '') {
5254
$qb = $this->db->getQueryBuilder();
55+
$id = $this->snowflake->nextId();
5356
$qb->insert(self::VERSION_TABLE_NAME)
5457
->values([
58+
'id' => $id,
5559
'version' => $preview->getVersion(),
5660
'file_id' => $preview->getFileId(),
5761
])
5862
->executeStatement();
59-
$entity->setVersionId($qb->getLastInsertId());
63+
$entity->setVersionId($id);
6064
}
6165
return parent::insert($preview);
6266
}
@@ -148,7 +152,13 @@ protected function joinLocation(IQueryBuilder $qb): IQueryBuilder {
148152
));
149153
}
150154

151-
public function getLocationId(string $bucket, string $objectStore): int {
155+
/**
156+
* Get the location id corresponding to the $bucket and $objectStore. Create one
157+
* if not existing yet.
158+
*
159+
* @throws Exception
160+
*/
161+
public function getLocationId(string $bucket, string $objectStore): string {
152162
$qb = $this->db->getQueryBuilder();
153163
$result = $qb->select('id')
154164
->from(self::LOCATION_TABLE_NAME)
@@ -157,14 +167,33 @@ public function getLocationId(string $bucket, string $objectStore): int {
157167
->executeQuery();
158168
$data = $result->fetchOne();
159169
if ($data) {
160-
return $data;
170+
return (string)$data;
161171
} else {
162-
$qb->insert(self::LOCATION_TABLE_NAME)
163-
->values([
164-
'bucket_name' => $qb->createNamedParameter($bucket),
165-
'object_store_name' => $qb->createNamedParameter($objectStore),
166-
])->executeStatement();
167-
return $qb->getLastInsertId();
172+
try {
173+
$id = $this->snowflake->nextId();
174+
$qb->insert(self::LOCATION_TABLE_NAME)
175+
->values([
176+
'id' => $qb->createNamedParameter($id),
177+
'bucket_name' => $qb->createNamedParameter($bucket),
178+
'object_store_name' => $qb->createNamedParameter($objectStore),
179+
])->executeStatement();
180+
return $id;
181+
} catch (Exception $e) {
182+
if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
183+
// Fetch again as there seems to be another entry added meanwhile
184+
$result = $qb->select('id')
185+
->from(self::LOCATION_TABLE_NAME)
186+
->where($qb->expr()->eq('bucket_name', $qb->createNamedParameter($bucket)))
187+
->andWhere($qb->expr()->eq('object_store_name', $qb->createNamedParameter($objectStore)))
188+
->executeQuery();
189+
$data = $result->fetchOne();
190+
if ($data) {
191+
return (string)$data;
192+
}
193+
}
194+
195+
throw $e;
196+
}
168197
}
169198
}
170199

0 commit comments

Comments
 (0)