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 <carl.schwan@nextcloud.com>
pull/55728/head
Carl Schwan 2025-10-13 09:52:04 +07:00
parent c9b055a0d0
commit 336cc3fa35
19 changed files with 260 additions and 43 deletions

@ -26,6 +26,7 @@ use OCP\Files\IRootFolder;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use Override; use Override;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -44,6 +45,7 @@ class MovePreviewJob extends TimedJob {
private readonly IMimeTypeDetector $mimeTypeDetector, private readonly IMimeTypeDetector $mimeTypeDetector,
private readonly IMimeTypeLoader $mimeTypeLoader, private readonly IMimeTypeLoader $mimeTypeLoader,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly IGenerator $generator,
IAppDataFactory $appDataFactory, IAppDataFactory $appDataFactory,
) { ) {
parent::__construct($time); parent::__construct($time);
@ -136,6 +138,7 @@ class MovePreviewJob extends TimedJob {
$path = $fileId . '/' . $previewFile->getName(); $path = $fileId . '/' . $previewFile->getName();
/** @var SimpleFile $previewFile */ /** @var SimpleFile $previewFile */
$preview = Preview::fromPath($path, $this->mimeTypeDetector); $preview = Preview::fromPath($path, $this->mimeTypeDetector);
$preview->setId($this->generator->nextId());
if (!$preview) { if (!$preview) {
$this->logger->error('Unable to import old preview at path.'); $this->logger->error('Unable to import old preview at path.');
continue; continue;

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\Attributes\ModifyColumn;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Migrate away from auto-increment
*/
#[ModifyColumn(table: 'preview_locations', name: 'id', description: 'Remove auto-increment')]
#[ModifyColumn(table: 'previews', name: 'id', description: 'Remove auto-increment')]
#[ModifyColumn(table: 'preview_versions', name: 'id', description: 'Remove auto-increment')]
class Version33000Date20251023110529 extends SimpleMigrationStep {
/**
* @param Closure(): ISchemaWrapper $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
if ($schema->hasTable('preview_locations')) {
$schema->dropAutoincrementColumn('preview_locations', 'id');
}
if ($schema->hasTable('preview_versions')) {
$schema->dropAutoincrementColumn('preview_versions', 'id');
}
if ($schema->hasTable('previews')) {
$schema->dropAutoincrementColumn('previews', 'id');
}
return $schema;
}
}

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\Attributes\AddIndex;
use OCP\Migration\Attributes\IndexType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Use unique index for preview_locations
*/
#[AddIndex(table: 'preview_locations', type: IndexType::UNIQUE)]
class Version33000Date20251023120529 extends SimpleMigrationStep {
public function __construct(
private readonly IDBConnection $connection,
) {
}
/**
* @param Closure(): ISchemaWrapper $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if ($schema->hasTable('preview_locations')) {
$table = $schema->getTable('preview_locations');
$table->addUniqueIndex(['bucket_name', 'object_store_name'], 'unique_bucket_store');
}
return $schema;
}
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
// This shouldn't run on a production instance, only daily
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('preview_locations');
$result = $qb->executeQuery();
$set = [];
while ($row = $result->fetch()) {
// Iterate over all the rows with duplicated rows
$id = $row['id'];
if (isset($set[$row['bucket_name'] . '_' . $row['object_store_name']])) {
// duplicate
$authoritativeId = $set[$row['bucket_name'] . '_' . $row['object_store_name']];
$qb = $this->connection->getQueryBuilder();
$qb->select('id')
->from('preview_locations')
->where($qb->expr()->eq('bucket_name', $qb->createNamedParameter($row['bucket_name'])))
->andWhere($qb->expr()->eq('object_store_name', $qb->createNamedParameter($row['object_store_name'])))
->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($authoritativeId)));
$result = $qb->executeQuery();
while ($row = $result->fetch()) {
// Update previews entries to the now de-duplicated id
$qb = $this->connection->getQueryBuilder();
$qb->update('previews')
->set('location_id', $qb->createNamedParameter($id))
->where($qb->expr()->eq('id', $qb->createNamedParameter($row['id'])));
$qb->executeStatement();
$qb = $this->connection->getQueryBuilder();
$qb->delete('preview_locations')
->where($qb->expr()->eq('id', $qb->createNamedParameter($row['id'])));
$qb->executeStatement();
}
break;
}
$set[$row['bucket_name'] . '_' . $row['object_store_name']] = $row['id'];
}
}
}

@ -1530,6 +1530,8 @@ return array(
'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Migrations\\Version33000Date20250819110529' => $baseDir . '/core/Migrations/Version33000Date20250819110529.php', 'OC\\Core\\Migrations\\Version33000Date20250819110529' => $baseDir . '/core/Migrations/Version33000Date20250819110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023110529' => $baseDir . '/core/Migrations/Version33000Date20251023110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023120529' => $baseDir . '/core/Migrations/Version33000Date20251023120529.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php', 'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php',

@ -1571,6 +1571,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php',
'OC\\Core\\Migrations\\Version33000Date20250819110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20250819110529.php', 'OC\\Core\\Migrations\\Version33000Date20250819110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20250819110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023110529.php',
'OC\\Core\\Migrations\\Version33000Date20251023120529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251023120529.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php', 'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php',

@ -204,7 +204,7 @@ class Dispatcher {
try { try {
$response = \call_user_func_array([$controller, $methodName], $arguments); $response = \call_user_func_array([$controller, $methodName], $arguments);
} catch (\TypeError $e) { } catch (\TypeError $e) {
// Only intercept TypeErrors occuring on the first line, meaning that the invocation of the controller method failed. // Only intercept TypeErrors occurring on the first line, meaning that the invocation of the controller method failed.
// Any other TypeError happens inside the controller method logic and should be logged as normal. // Any other TypeError happens inside the controller method logic and should be logged as normal.
if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) { if ($e->getFile() === $this->reflector->getFile() && $e->getLine() === $this->reflector->getStartLine()) {
$this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]); $this->logger->debug('Failed to call controller method: ' . $e->getMessage(), ['exception' => $e]);

@ -8,8 +8,11 @@ namespace OC\DB;
use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use OCP\DB\ISchemaWrapper; use OCP\DB\ISchemaWrapper;
use OCP\Server;
use Psr\Log\LoggerInterface;
class SchemaWrapper implements ISchemaWrapper { class SchemaWrapper implements ISchemaWrapper {
/** @var Connection */ /** @var Connection */
@ -131,4 +134,18 @@ class SchemaWrapper implements ISchemaWrapper {
public function getDatabasePlatform() { public function getDatabasePlatform() {
return $this->connection->getDatabasePlatform(); return $this->connection->getDatabasePlatform();
} }
public function dropAutoincrementColumn(string $table, string $column): void {
$tableObj = $this->schema->getTable($this->connection->getPrefix() . $table);
$tableObj->modifyColumn('id', ['autoincrement' => false]);
$platform = $this->getDatabasePlatform();
if ($platform instanceof OraclePlatform) {
try {
$this->connection->executeStatement('DROP TRIGGER "' . $this->connection->getPrefix() . $table . '_AI_PK"');
$this->connection->executeStatement('DROP SEQUENCE "' . $this->connection->getPrefix() . $table . '_SEQ"');
} catch (Exception $e) {
Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
}
}
}
} }

@ -17,14 +17,16 @@ use OCP\Files\IMimeTypeDetector;
/** /**
* Preview entity mapped to the oc_previews and oc_preview_locations table. * Preview entity mapped to the oc_previews and oc_preview_locations table.
* *
* @method string getId()
* @method void setId(string $id)
* @method int getFileId() Get the file id of the original file. * @method int getFileId() Get the file id of the original file.
* @method void setFileId(int $fileId) * @method void setFileId(int $fileId)
* @method int getStorageId() Get the storage id of the original file. * @method int getStorageId() Get the storage id of the original file.
* @method void setStorageId(int $fileId) * @method void setStorageId(int $fileId)
* @method int getOldFileId() Get the old location in the file-cache table, for legacy compatibility. * @method int getOldFileId() Get the old location in the file-cache table, for legacy compatibility.
* @method void setOldFileId(int $oldFileId) * @method void setOldFileId(int $oldFileId)
* @method int getLocationId() Get the location id in the preview_locations table. Only set when using an object store as primary storage. * @method string getLocationId() Get the location id in the preview_locations table. Only set when using an object store as primary storage.
* @method void setLocationId(int $locationId) * @method void setLocationId(string $locationId)
* @method string|null getBucketName() Get the bucket name where the preview is stored. This is stored in the preview_locations table. * @method string|null getBucketName() Get the bucket name where the preview is stored. This is stored in the preview_locations table.
* @method string|null getObjectStoreName() Get the object store name where the preview is stored. This is stored in the preview_locations table. * @method string|null getObjectStoreName() Get the object store name where the preview is stored. This is stored in the preview_locations table.
* @method int getWidth() Get the width of the preview. * @method int getWidth() Get the width of the preview.
@ -46,7 +48,7 @@ use OCP\Files\IMimeTypeDetector;
* @method string getEtag() Get the etag of the preview. * @method string getEtag() Get the etag of the preview.
* @method void setEtag(string $etag) * @method void setEtag(string $etag)
* @method string|null getVersion() Get the version for files_versions_s3 * @method string|null getVersion() Get the version for files_versions_s3
* @method void setVersionId(int $versionId) * @method void setVersionId(string $versionId)
* @method bool|null getIs() Get the version for files_versions_s3 * @method bool|null getIs() Get the version for files_versions_s3
* @method bool isEncrypted() Get whether the preview is encrypted. At the moment every preview is unencrypted. * @method bool isEncrypted() Get whether the preview is encrypted. At the moment every preview is unencrypted.
* @method void setEncrypted(bool $encrypted) * @method void setEncrypted(bool $encrypted)
@ -57,7 +59,7 @@ class Preview extends Entity {
protected ?int $fileId = null; protected ?int $fileId = null;
protected ?int $oldFileId = null; protected ?int $oldFileId = null;
protected ?int $storageId = null; protected ?int $storageId = null;
protected ?int $locationId = null; protected ?string $locationId = null;
protected ?string $bucketName = null; protected ?string $bucketName = null;
protected ?string $objectStoreName = null; protected ?string $objectStoreName = null;
protected ?int $width = null; protected ?int $width = null;
@ -72,14 +74,15 @@ class Preview extends Entity {
protected ?bool $cropped = null; protected ?bool $cropped = null;
protected ?string $etag = null; protected ?string $etag = null;
protected ?string $version = null; protected ?string $version = null;
protected ?int $versionId = null; protected ?string $versionId = null;
protected ?bool $encrypted = null; protected ?bool $encrypted = null;
public function __construct() { public function __construct() {
$this->addType('id', Types::STRING);
$this->addType('fileId', Types::BIGINT); $this->addType('fileId', Types::BIGINT);
$this->addType('storageId', Types::BIGINT); $this->addType('storageId', Types::BIGINT);
$this->addType('oldFileId', Types::BIGINT); $this->addType('oldFileId', Types::BIGINT);
$this->addType('locationId', Types::BIGINT); $this->addType('locationId', Types::STRING);
$this->addType('width', Types::INTEGER); $this->addType('width', Types::INTEGER);
$this->addType('height', Types::INTEGER); $this->addType('height', Types::INTEGER);
$this->addType('mimetypeId', Types::INTEGER); $this->addType('mimetypeId', Types::INTEGER);

@ -15,6 +15,7 @@ use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\IMimeTypeLoader; use OCP\Files\IMimeTypeLoader;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use Override; use Override;
/** /**
@ -29,6 +30,7 @@ class PreviewMapper extends QBMapper {
public function __construct( public function __construct(
IDBConnection $db, IDBConnection $db,
private readonly IMimeTypeLoader $mimeTypeLoader, private readonly IMimeTypeLoader $mimeTypeLoader,
private readonly IGenerator $snowflake,
) { ) {
parent::__construct($db, self::TABLE_NAME, Preview::class); parent::__construct($db, self::TABLE_NAME, Preview::class);
} }
@ -50,13 +52,15 @@ class PreviewMapper extends QBMapper {
if ($preview->getVersion() !== null && $preview->getVersion() !== '') { if ($preview->getVersion() !== null && $preview->getVersion() !== '') {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$id = $this->snowflake->nextId();
$qb->insert(self::VERSION_TABLE_NAME) $qb->insert(self::VERSION_TABLE_NAME)
->values([ ->values([
'id' => $id,
'version' => $preview->getVersion(), 'version' => $preview->getVersion(),
'file_id' => $preview->getFileId(), 'file_id' => $preview->getFileId(),
]) ])
->executeStatement(); ->executeStatement();
$entity->setVersionId($qb->getLastInsertId()); $entity->setVersionId($id);
} }
return parent::insert($preview); return parent::insert($preview);
} }
@ -148,7 +152,13 @@ class PreviewMapper extends QBMapper {
)); ));
} }
public function getLocationId(string $bucket, string $objectStore): int { /**
* Get the location id corresponding to the $bucket and $objectStore. Create one
* if not existing yet.
*
* @throws Exception
*/
public function getLocationId(string $bucket, string $objectStore): string {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$result = $qb->select('id') $result = $qb->select('id')
->from(self::LOCATION_TABLE_NAME) ->from(self::LOCATION_TABLE_NAME)
@ -157,14 +167,33 @@ class PreviewMapper extends QBMapper {
->executeQuery(); ->executeQuery();
$data = $result->fetchOne(); $data = $result->fetchOne();
if ($data) { if ($data) {
return $data; return (string)$data;
} else { } else {
$qb->insert(self::LOCATION_TABLE_NAME) try {
->values([ $id = $this->snowflake->nextId();
'bucket_name' => $qb->createNamedParameter($bucket), $qb->insert(self::LOCATION_TABLE_NAME)
'object_store_name' => $qb->createNamedParameter($objectStore), ->values([
])->executeStatement(); 'id' => $qb->createNamedParameter($id),
return $qb->getLastInsertId(); 'bucket_name' => $qb->createNamedParameter($bucket),
'object_store_name' => $qb->createNamedParameter($objectStore),
])->executeStatement();
return $id;
} catch (Exception $e) {
if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
// Fetch again as there seems to be another entry added meanwhile
$result = $qb->select('id')
->from(self::LOCATION_TABLE_NAME)
->where($qb->expr()->eq('bucket_name', $qb->createNamedParameter($bucket)))
->andWhere($qb->expr()->eq('object_store_name', $qb->createNamedParameter($objectStore)))
->executeQuery();
$data = $result->fetchOne();
if ($data) {
return (string)$data;
}
}
throw $e;
}
} }
} }

@ -23,6 +23,7 @@ use OCP\IPreview;
use OCP\IStreamImage; use OCP\IStreamImage;
use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Preview\BeforePreviewFetchedEvent;
use OCP\Preview\IVersionedPreviewFile; use OCP\Preview\IVersionedPreviewFile;
use OCP\Snowflake\IGenerator;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class Generator { class Generator {
@ -37,6 +38,7 @@ class Generator {
private LoggerInterface $logger, private LoggerInterface $logger,
private PreviewMapper $previewMapper, private PreviewMapper $previewMapper,
private StorageFactory $storageFactory, private StorageFactory $storageFactory,
private IGenerator $snowflakeGenerator,
) { ) {
} }
@ -348,6 +350,7 @@ class Generator {
try { try {
$previewEntry = new Preview(); $previewEntry = new Preview();
$previewEntry->setId($this->snowflakeGenerator->nextId());
$previewEntry->setFileId($file->getId()); $previewEntry->setFileId($file->getId());
$previewEntry->setStorageId($file->getMountPoint()->getNumericStorageId()); $previewEntry->setStorageId($file->getMountPoint()->getNumericStorageId());
$previewEntry->setSourceMimeType($file->getMimeType()); $previewEntry->setSourceMimeType($file->getMimeType());
@ -360,7 +363,6 @@ class Generator {
$previewEntry->setMimetype($preview->dataMimeType()); $previewEntry->setMimetype($preview->dataMimeType());
$previewEntry->setEtag($file->getEtag()); $previewEntry->setEtag($file->getEtag());
$previewEntry->setMtime((new \DateTime())->getTimestamp()); $previewEntry->setMtime((new \DateTime())->getTimestamp());
$previewEntry->setSize(0);
return $this->savePreview($previewEntry, $preview); return $this->savePreview($previewEntry, $preview);
} catch (NotPermittedException) { } catch (NotPermittedException) {
throw new NotFoundException(); throw new NotFoundException();
@ -502,6 +504,7 @@ class Generator {
} }
$previewEntry = new Preview(); $previewEntry = new Preview();
$previewEntry->setId($this->snowflakeGenerator->nextId());
$previewEntry->setFileId($file->getId()); $previewEntry->setFileId($file->getId());
$previewEntry->setStorageId($file->getMountPoint()->getNumericStorageId()); $previewEntry->setStorageId($file->getMountPoint()->getNumericStorageId());
$previewEntry->setWidth($width); $previewEntry->setWidth($width);
@ -514,7 +517,6 @@ class Generator {
$previewEntry->setMimeType($preview->dataMimeType()); $previewEntry->setMimeType($preview->dataMimeType());
$previewEntry->setEtag($file->getEtag()); $previewEntry->setEtag($file->getEtag());
$previewEntry->setMtime((new \DateTime())->getTimestamp()); $previewEntry->setMtime((new \DateTime())->getTimestamp());
$previewEntry->setSize(0);
if ($cacheResult) { if ($cacheResult) {
$previewEntry = $this->savePreview($previewEntry, $preview); $previewEntry = $this->savePreview($previewEntry, $preview);
return new PreviewFile($previewEntry, $this->storageFactory, $this->previewMapper); return new PreviewFile($previewEntry, $this->storageFactory, $this->previewMapper);
@ -530,26 +532,20 @@ class Generator {
* @throws \OCP\DB\Exception * @throws \OCP\DB\Exception
*/ */
public function savePreview(Preview $previewEntry, IImage $preview): Preview { public function savePreview(Preview $previewEntry, IImage $preview): Preview {
$previewEntry = $this->previewMapper->insert($previewEntry);
// we need to save to DB first // we need to save to DB first
try { if ($preview instanceof IStreamImage) {
if ($preview instanceof IStreamImage) { $size = $this->storageFactory->writePreview($previewEntry, $preview->resource());
$size = $this->storageFactory->writePreview($previewEntry, $preview->resource()); } else {
} else { $stream = fopen('php://temp', 'w+');
$stream = fopen('php://temp', 'w+'); fwrite($stream, $preview->data());
fwrite($stream, $preview->data()); rewind($stream);
rewind($stream); $size = $this->storageFactory->writePreview($previewEntry, $stream);
$size = $this->storageFactory->writePreview($previewEntry, $stream); }
} if (!$size) {
if (!$size) { throw new \RuntimeException('Unable to write preview file');
throw new \RuntimeException('Unable to write preview file');
}
} catch (\Exception $e) {
$this->previewMapper->delete($previewEntry);
throw $e;
} }
$previewEntry->setSize($size); $previewEntry->setSize($size);
return $this->previewMapper->update($previewEntry);
return $this->previewMapper->insert($previewEntry);
} }
} }

@ -22,6 +22,7 @@ use OCP\Files\NotPermittedException;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use Override; use Override;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
@ -38,6 +39,7 @@ class LocalPreviewStorage implements IPreviewStorage {
private readonly IDBConnection $connection, private readonly IDBConnection $connection,
private readonly IMimeTypeDetector $mimeTypeDetector, private readonly IMimeTypeDetector $mimeTypeDetector,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly IGenerator $generator,
) { ) {
$this->instanceId = $this->config->getSystemValueString('instanceid'); $this->instanceId = $this->config->getSystemValueString('instanceid');
$this->rootFolder = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data'); $this->rootFolder = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data');
@ -118,6 +120,7 @@ class LocalPreviewStorage implements IPreviewStorage {
$this->logger->error('Unable to parse preview information for ' . $file->getRealPath()); $this->logger->error('Unable to parse preview information for ' . $file->getRealPath());
continue; continue;
} }
$preview->setId($this->generator->nextId());
try { try {
$preview->setSize($file->getSize()); $preview->setSize($file->getSize());
$preview->setMtime($file->getMtime()); $preview->setMtime($file->getMtime());

@ -23,6 +23,7 @@ use OCP\IBinaryFinder;
use OCP\IConfig; use OCP\IConfig;
use OCP\IPreview; use OCP\IPreview;
use OCP\Preview\IProviderV2; use OCP\Preview\IProviderV2;
use OCP\Snowflake\IGenerator;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -141,6 +142,7 @@ class PreviewManager implements IPreview {
$this->container->get(LoggerInterface::class), $this->container->get(LoggerInterface::class),
$this->container->get(PreviewMapper::class), $this->container->get(PreviewMapper::class),
$this->container->get(StorageFactory::class), $this->container->get(StorageFactory::class),
$this->container->get(IGenerator::class),
); );
} }
return $this->generator; return $this->generator;

@ -19,10 +19,8 @@ use function substr;
* @psalm-consistent-constructor * @psalm-consistent-constructor
*/ */
abstract class Entity { abstract class Entity {
/** /** @var int $id */
* @var int public $id = null;
*/
public $id;
private array $_updatedFields = []; private array $_updatedFields = [];
/** @var array<string, \OCP\DB\Types::*> */ /** @var array<string, \OCP\DB\Types::*> */

@ -90,4 +90,11 @@ interface ISchemaWrapper {
* @since 23.0.0 * @since 23.0.0
*/ */
public function getDatabasePlatform(); public function getDatabasePlatform();
/**
* Drop autoincrement from an existing table of the database.
*
* @since 33.0.0
*/
public function dropAutoincrementColumn(string $table, string $column): void;
} }

@ -22,6 +22,7 @@ use OCP\IPreview;
use OCP\Preview\BeforePreviewFetchedEvent; use OCP\Preview\BeforePreviewFetchedEvent;
use OCP\Preview\IProviderV2; use OCP\Preview\IProviderV2;
use OCP\Preview\IVersionedPreviewFile; use OCP\Preview\IVersionedPreviewFile;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -41,6 +42,7 @@ class GeneratorTest extends TestCase {
private LoggerInterface&MockObject $logger; private LoggerInterface&MockObject $logger;
private StorageFactory&MockObject $storageFactory; private StorageFactory&MockObject $storageFactory;
private PreviewMapper&MockObject $previewMapper; private PreviewMapper&MockObject $previewMapper;
private IGenerator&MockObject $snowflakeGenerator;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -52,6 +54,7 @@ class GeneratorTest extends TestCase {
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->previewMapper = $this->createMock(PreviewMapper::class); $this->previewMapper = $this->createMock(PreviewMapper::class);
$this->storageFactory = $this->createMock(StorageFactory::class); $this->storageFactory = $this->createMock(StorageFactory::class);
$this->snowflakeGenerator = $this->createMock(IGenerator::class);
$this->generator = new Generator( $this->generator = new Generator(
$this->config, $this->config,
@ -61,6 +64,7 @@ class GeneratorTest extends TestCase {
$this->logger, $this->logger,
$this->previewMapper, $this->previewMapper,
$this->storageFactory, $this->storageFactory,
$this->snowflakeGenerator,
); );
} }

@ -24,6 +24,7 @@ use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Server; use OCP\Server;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\Attributes\TestDox; use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -123,6 +124,7 @@ class MovePreviewJobTest extends TestCase {
$this->mimeTypeDetector, $this->mimeTypeDetector,
$this->mimeTypeLoader, $this->mimeTypeLoader,
$this->logger, $this->logger,
Server::get(IGenerator::class),
Server::get(IAppDataFactory::class), Server::get(IAppDataFactory::class),
); );
$this->invokePrivate($job, 'run', [[]]); $this->invokePrivate($job, 'run', [[]]);
@ -155,6 +157,7 @@ class MovePreviewJobTest extends TestCase {
$this->mimeTypeDetector, $this->mimeTypeDetector,
$this->mimeTypeLoader, $this->mimeTypeLoader,
$this->logger, $this->logger,
Server::get(IGenerator::class),
Server::get(IAppDataFactory::class) Server::get(IAppDataFactory::class)
); );
$this->invokePrivate($job, 'run', [[]]); $this->invokePrivate($job, 'run', [[]]);
@ -195,6 +198,7 @@ class MovePreviewJobTest extends TestCase {
$this->mimeTypeDetector, $this->mimeTypeDetector,
$this->mimeTypeLoader, $this->mimeTypeLoader,
$this->logger, $this->logger,
Server::get(IGenerator::class),
Server::get(IAppDataFactory::class) Server::get(IAppDataFactory::class)
); );
$this->invokePrivate($job, 'run', [[]]); $this->invokePrivate($job, 'run', [[]]);

@ -14,16 +14,28 @@ use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper; use OC\Preview\Db\PreviewMapper;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Server; use OCP\Server;
use OCP\Snowflake\IGenerator;
use Test\TestCase; use Test\TestCase;
#[\PHPUnit\Framework\Attributes\Group('DB')] #[\PHPUnit\Framework\Attributes\Group('DB')]
class PreviewMapperTest extends TestCase { class PreviewMapperTest extends TestCase {
private PreviewMapper $previewMapper; private PreviewMapper $previewMapper;
private IDBConnection $connection; private IDBConnection $connection;
private IGenerator $snowflake;
public function setUp(): void { public function setUp(): void {
$this->previewMapper = Server::get(PreviewMapper::class); $this->previewMapper = Server::get(PreviewMapper::class);
$this->connection = Server::get(IDBConnection::class); $this->connection = Server::get(IDBConnection::class);
$this->snowflake = Server::get(IGenerator::class);
$qb = $this->connection->getQueryBuilder();
$qb->delete('preview_locations')->executeStatement();
$qb = $this->connection->getQueryBuilder();
$qb->delete('preview_versions')->executeStatement();
$qb = $this->connection->getQueryBuilder();
$qb->delete('previews')->executeStatement();
} }
public function testGetAvailablePreviews(): void { public function testGetAvailablePreviews(): void {
@ -51,15 +63,17 @@ class PreviewMapperTest extends TestCase {
$locationId = null; $locationId = null;
if ($bucket) { if ($bucket) {
$qb = $this->connection->getQueryBuilder(); $qb = $this->connection->getQueryBuilder();
$locationId = $this->snowflake->nextId();
$qb->insert('preview_locations') $qb->insert('preview_locations')
->values([ ->values([
'id' => $locationId,
'bucket_name' => $qb->createNamedParameter('preview-' . $bucket), 'bucket_name' => $qb->createNamedParameter('preview-' . $bucket),
'object_store_name' => $qb->createNamedParameter('default'), 'object_store_name' => $qb->createNamedParameter('default'),
]); ]);
$qb->executeStatement(); $qb->executeStatement();
$locationId = $qb->getLastInsertId();
} }
$preview = new Preview(); $preview = new Preview();
$preview->setId($this->snowflake->nextId());
$preview->setFileId($fileId); $preview->setFileId($fileId);
$preview->setStorageId(1); $preview->setStorageId(1);
$preview->setCropped(true); $preview->setCropped(true);

@ -14,7 +14,7 @@ use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper; use OC\Preview\Db\PreviewMapper;
use OC\Preview\PreviewService; use OC\Preview\PreviewService;
use OCP\Server; use OCP\Server;
use PHPUnit\Framework\Attributes\CoversClass; use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
#[CoversClass(PreviewService::class)] #[CoversClass(PreviewService::class)]
@ -22,10 +22,12 @@ use PHPUnit\Framework\TestCase;
class PreviewServiceTest extends TestCase { class PreviewServiceTest extends TestCase {
private PreviewService $previewService; private PreviewService $previewService;
private PreviewMapper $previewMapper; private PreviewMapper $previewMapper;
private IGenerator $snowflakeGenerator;
protected function setUp(): void { protected function setUp(): void {
$this->previewService = Server::get(PreviewService::class); $this->previewService = Server::get(PreviewService::class);
$this->previewMapper = Server::get(PreviewMapper::class); $this->previewMapper = Server::get(PreviewMapper::class);
$this->snowflakeGenerator = Server::get(IGenerator::class);
$this->previewService->deleteAll(); $this->previewService->deleteAll();
} }
@ -36,6 +38,7 @@ class PreviewServiceTest extends TestCase {
public function testGetAvailableFileIds(): void { public function testGetAvailableFileIds(): void {
foreach (range(1, 20) as $i) { foreach (range(1, 20) as $i) {
$preview = new Preview(); $preview = new Preview();
$preview->setId($this->snowflakeGenerator->nextId());
$preview->setFileId($i % 10); $preview->setFileId($i % 10);
$preview->setStorageId(1); $preview->setStorageId(1);
$preview->setWidth($i); $preview->setWidth($i);

@ -9,7 +9,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number. // when updating major/minor version number.
$OC_Version = [33, 0, 0, 2]; $OC_Version = [33, 0, 0, 3];
// The human-readable string // The human-readable string
$OC_VersionString = '33.0.0 dev'; $OC_VersionString = '33.0.0 dev';