refactor: Rename Snowflake Generator and Decoder

And introduce the Snowflake DTO

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2025-12-18 20:25:59 +01:00 committed by Carl Schwan
parent c32009fd32
commit f546daada7
No known key found for this signature in database
GPG key ID: 02325448204E452A
28 changed files with 201 additions and 122 deletions

View file

@ -44,7 +44,7 @@ use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IProviderFactory;
use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use OCP\Util;
use Override;
use Psr\Log\LoggerInterface;
@ -70,7 +70,7 @@ class CloudFederationProviderFiles implements ISignedCloudFederationProvider {
private readonly IFilenameValidator $filenameValidator,
private readonly IProviderFactory $shareProviderFactory,
private readonly SetupManager $setupManager,
private readonly IGenerator $snowflakeGenerator,
private readonly ISnowflakeGenerator $snowflakeGenerator,
private readonly ExternalShareMapper $externalShareMapper,
) {
}

View file

@ -34,7 +34,7 @@ use OCP\IUserSession;
use OCP\Notification\IManager;
use OCP\OCS\IDiscoveryService;
use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Psr\Log\LoggerInterface;
class Manager {
@ -57,7 +57,7 @@ class Manager {
private SetupManager $setupManager,
private ICertificateManager $certificateManager,
private ExternalShareMapper $externalShareMapper,
private IGenerator $snowflakeGenerator,
private ISnowflakeGenerator $snowflakeGenerator,
) {
$this->user = $userSession->getUser();
}

View file

@ -14,7 +14,7 @@ use OCP\Federation\ICloudId;
use OCP\Federation\ICloudIdManager;
use OCP\IDBConnection;
use OCP\Server;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\Console\Input\InputInterface;
@ -64,7 +64,7 @@ class CleanupRemoteStoragesTest extends TestCase {
if (isset($storage['share_token'])) {
$externalShare = new ExternalShare();
$externalShare->setId(Server::get(IGenerator::class)->nextId());
$externalShare->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$externalShare->setShareToken($storage['share_token']);
$externalShare->setRemote($storage['remote']);
$externalShare->setName('irrelevant');

View file

@ -40,7 +40,7 @@ use OCP\IUserSession;
use OCP\OCS\IDiscoveryService;
use OCP\Server;
use OCP\Share\IShare;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\Traits\UserTrait;
@ -170,7 +170,7 @@ class ManagerTest extends TestCase {
$this->setupManager,
$this->certificateManager,
$this->externalShareMapper,
Server::get(IGenerator::class),
Server::get(ISnowflakeGenerator::class),
]
)->onlyMethods(['tryOCMEndPoint'])->getMock();
}
@ -190,7 +190,7 @@ class ManagerTest extends TestCase {
public function testAddUserShare(): void {
$userShare = new ExternalShare();
$userShare->setId(Server::get(IGenerator::class)->nextId());
$userShare->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$userShare->setRemote('http://localhost');
$userShare->setShareToken('token1');
$userShare->setPassword('');
@ -205,7 +205,7 @@ class ManagerTest extends TestCase {
public function testAddGroupShare(): void {
$groupShare = new ExternalShare();
$groupShare->setId(Server::get(IGenerator::class)->nextId());
$groupShare->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$groupShare->setRemote('http://localhost');
$groupShare->setOwner('foobar');
$groupShare->setShareType(IShare::TYPE_GROUP);
@ -237,10 +237,10 @@ class ManagerTest extends TestCase {
$shareData2 = $shareData1->clone();
$shareData2->setShareToken('token2');
$shareData2->setId(\OCP\Server::get(IGenerator::class)->nextId());
$shareData2->setId(\OCP\Server::get(ISnowflakeGenerator::class)->nextId());
$shareData3 = $shareData1->clone();
$shareData3->setShareToken('token3');
$shareData3->setId(\OCP\Server::get(IGenerator::class)->nextId());
$shareData3->setId(\OCP\Server::get(ISnowflakeGenerator::class)->nextId());
$this->setupMounts();
$this->assertNotMount('SharedFolder');
@ -440,7 +440,7 @@ class ManagerTest extends TestCase {
$user = $this->createMock(IUser::class);
$user->expects($this->any())->method('getUID')->willReturn($userId);
$share = new ExternalShare();
$share->setId(Server::get(IGenerator::class)->nextId());
$share->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$share->setRemote('http://localhost');
$share->setShareToken('token1');
$share->setPassword('');
@ -460,7 +460,7 @@ class ManagerTest extends TestCase {
*/
private function createTestGroupShare(string $groupId = 'group1'): array {
$share = new ExternalShare();
$share->setId(Server::get(IGenerator::class)->nextId());
$share->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$share->setRemote('http://localhost');
$share->setShareToken('token1');
$share->setPassword('');
@ -646,7 +646,7 @@ class ManagerTest extends TestCase {
// user 2 shares
$manager2 = $this->createManagerForUser($user2);
$share = new ExternalShare();
$share->setId(Server::get(IGenerator::class)->nextId());
$share->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$share->setRemote('http://localhost');
$share->setShareToken('token1');
$share->setPassword('');
@ -696,7 +696,7 @@ class ManagerTest extends TestCase {
$manager2 = $this->createManagerForUser($user);
$share = new ExternalShare();
$share->setId(Server::get(IGenerator::class)->nextId());
$share->setId(Server::get(ISnowflakeGenerator::class)->nextId());
$share->setRemote('http://localhost');
$share->setShareToken('token1');
$share->setPassword('');

View file

@ -26,7 +26,7 @@ use OCP\Files\IRootFolder;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
use Psr\Log\LoggerInterface;
@ -45,7 +45,7 @@ class MovePreviewJob extends TimedJob {
private readonly IMimeTypeDetector $mimeTypeDetector,
private readonly IMimeTypeLoader $mimeTypeLoader,
private readonly LoggerInterface $logger,
private readonly IGenerator $generator,
private readonly ISnowflakeGenerator $generator,
IAppDataFactory $appDataFactory,
) {
parent::__construct($time);

View file

@ -8,7 +8,7 @@ declare(strict_types=1);
*/
namespace OC\Core\Command;
use OCP\Snowflake\IDecoder;
use OCP\Snowflake\ISnowflakeDecoder;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -16,7 +16,7 @@ use Symfony\Component\Console\Output\OutputInterface;
class SnowflakeDecodeId extends Base {
public function __construct(
private readonly IDecoder $decoder,
private readonly ISnowflakeDecoder $decoder,
) {
parent::__construct();
}
@ -36,13 +36,13 @@ class SnowflakeDecodeId extends Base {
$rows = [
['Snowflake ID', $snowflakeId],
['Seconds', $data['seconds']],
['Milliseconds', $data['milliseconds']],
['Created from CLI', $data['isCli'] ? 'yes' : 'no'],
['Server ID', $data['serverId']],
['Sequence ID', $data['sequenceId']],
['Creation timestamp', $data['createdAt']->format('U.v')],
['Creation date', $data['createdAt']->format('Y-m-d H:i:s.v')],
['Seconds', $data->getSeconds()],
['Milliseconds', $data->getMilliseconds()],
['Created from CLI', $data->isCli() ? 'yes' : 'no'],
['Server ID', $data->getServerId()],
['Sequence ID', $data->getSequenceId()],
['Creation timestamp', $data->getCreatedAt()->format('U.v')],
['Creation date', $data->getCreatedAt()->format('Y-m-d H:i:s.v')],
];
$table = new Table($output);

View file

@ -832,8 +832,9 @@ return array(
'OCP\\Share_Backend' => $baseDir . '/lib/public/Share_Backend.php',
'OCP\\Share_Backend_Collection' => $baseDir . '/lib/public/Share_Backend_Collection.php',
'OCP\\Share_Backend_File_Dependent' => $baseDir . '/lib/public/Share_Backend_File_Dependent.php',
'OCP\\Snowflake\\IDecoder' => $baseDir . '/lib/public/Snowflake/IDecoder.php',
'OCP\\Snowflake\\IGenerator' => $baseDir . '/lib/public/Snowflake/IGenerator.php',
'OCP\\Snowflake\\ISnowflakeDecoder' => $baseDir . '/lib/public/Snowflake/ISnowflakeDecoder.php',
'OCP\\Snowflake\\ISnowflakeGenerator' => $baseDir . '/lib/public/Snowflake/ISnowflakeGenerator.php',
'OCP\\Snowflake\\Snowflake' => $baseDir . '/lib/public/Snowflake/Snowflake.php',
'OCP\\SpeechToText\\Events\\AbstractTranscriptionEvent' => $baseDir . '/lib/public/SpeechToText/Events/AbstractTranscriptionEvent.php',
'OCP\\SpeechToText\\Events\\TranscriptionFailedEvent' => $baseDir . '/lib/public/SpeechToText/Events/TranscriptionFailedEvent.php',
'OCP\\SpeechToText\\Events\\TranscriptionSuccessfulEvent' => $baseDir . '/lib/public/SpeechToText/Events/TranscriptionSuccessfulEvent.php',
@ -2127,10 +2128,10 @@ return array(
'OC\\Share\\Helper' => $baseDir . '/lib/private/Share/Helper.php',
'OC\\Share\\Share' => $baseDir . '/lib/private/Share/Share.php',
'OC\\Snowflake\\APCuSequence' => $baseDir . '/lib/private/Snowflake/APCuSequence.php',
'OC\\Snowflake\\Decoder' => $baseDir . '/lib/private/Snowflake/Decoder.php',
'OC\\Snowflake\\FileSequence' => $baseDir . '/lib/private/Snowflake/FileSequence.php',
'OC\\Snowflake\\Generator' => $baseDir . '/lib/private/Snowflake/Generator.php',
'OC\\Snowflake\\ISequence' => $baseDir . '/lib/private/Snowflake/ISequence.php',
'OC\\Snowflake\\SnowflakeDecoder' => $baseDir . '/lib/private/Snowflake/SnowflakeDecoder.php',
'OC\\Snowflake\\SnowflakeGenerator' => $baseDir . '/lib/private/Snowflake/SnowflakeGenerator.php',
'OC\\SpeechToText\\SpeechToTextManager' => $baseDir . '/lib/private/SpeechToText/SpeechToTextManager.php',
'OC\\SpeechToText\\TranscriptionJob' => $baseDir . '/lib/private/SpeechToText/TranscriptionJob.php',
'OC\\StreamImage' => $baseDir . '/lib/private/StreamImage.php',

View file

@ -873,8 +873,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Share_Backend' => __DIR__ . '/../../..' . '/lib/public/Share_Backend.php',
'OCP\\Share_Backend_Collection' => __DIR__ . '/../../..' . '/lib/public/Share_Backend_Collection.php',
'OCP\\Share_Backend_File_Dependent' => __DIR__ . '/../../..' . '/lib/public/Share_Backend_File_Dependent.php',
'OCP\\Snowflake\\IDecoder' => __DIR__ . '/../../..' . '/lib/public/Snowflake/IDecoder.php',
'OCP\\Snowflake\\IGenerator' => __DIR__ . '/../../..' . '/lib/public/Snowflake/IGenerator.php',
'OCP\\Snowflake\\ISnowflakeDecoder' => __DIR__ . '/../../..' . '/lib/public/Snowflake/ISnowflakeDecoder.php',
'OCP\\Snowflake\\ISnowflakeGenerator' => __DIR__ . '/../../..' . '/lib/public/Snowflake/ISnowflakeGenerator.php',
'OCP\\Snowflake\\Snowflake' => __DIR__ . '/../../..' . '/lib/public/Snowflake/Snowflake.php',
'OCP\\SpeechToText\\Events\\AbstractTranscriptionEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/AbstractTranscriptionEvent.php',
'OCP\\SpeechToText\\Events\\TranscriptionFailedEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/TranscriptionFailedEvent.php',
'OCP\\SpeechToText\\Events\\TranscriptionSuccessfulEvent' => __DIR__ . '/../../..' . '/lib/public/SpeechToText/Events/TranscriptionSuccessfulEvent.php',
@ -2168,10 +2169,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Share\\Helper' => __DIR__ . '/../../..' . '/lib/private/Share/Helper.php',
'OC\\Share\\Share' => __DIR__ . '/../../..' . '/lib/private/Share/Share.php',
'OC\\Snowflake\\APCuSequence' => __DIR__ . '/../../..' . '/lib/private/Snowflake/APCuSequence.php',
'OC\\Snowflake\\Decoder' => __DIR__ . '/../../..' . '/lib/private/Snowflake/Decoder.php',
'OC\\Snowflake\\FileSequence' => __DIR__ . '/../../..' . '/lib/private/Snowflake/FileSequence.php',
'OC\\Snowflake\\Generator' => __DIR__ . '/../../..' . '/lib/private/Snowflake/Generator.php',
'OC\\Snowflake\\ISequence' => __DIR__ . '/../../..' . '/lib/private/Snowflake/ISequence.php',
'OC\\Snowflake\\SnowflakeDecoder' => __DIR__ . '/../../..' . '/lib/private/Snowflake/SnowflakeDecoder.php',
'OC\\Snowflake\\SnowflakeGenerator' => __DIR__ . '/../../..' . '/lib/private/Snowflake/SnowflakeGenerator.php',
'OC\\SpeechToText\\SpeechToTextManager' => __DIR__ . '/../../..' . '/lib/private/SpeechToText/SpeechToTextManager.php',
'OC\\SpeechToText\\TranscriptionJob' => __DIR__ . '/../../..' . '/lib/private/SpeechToText/TranscriptionJob.php',
'OC\\StreamImage' => __DIR__ . '/../../..' . '/lib/private/StreamImage.php',

View file

@ -16,7 +16,7 @@ use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;
@ -34,7 +34,7 @@ class JobList implements IJobList {
protected readonly IConfig $config,
protected readonly ITimeFactory $timeFactory,
protected readonly LoggerInterface $logger,
protected readonly IGenerator $generator,
protected readonly ISnowflakeGenerator $snowflakeGenerator,
) {
}
@ -55,7 +55,7 @@ class JobList implements IJobList {
if (!$this->has($job, $argument)) {
$query->insert('jobs')
->values([
'id' => $query->createNamedParameter($this->generator->nextId()),
'id' => $query->createNamedParameter($this->snowflakeGenerator->nextId()),
'class' => $query->createNamedParameter($class),
'argument' => $query->createNamedParameter($argumentJson),
'argument_hash' => $query->createNamedParameter(hash('sha256', $argumentJson)),

View file

@ -15,7 +15,7 @@ use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\IMimeTypeLoader;
use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
/**
@ -30,7 +30,7 @@ class PreviewMapper extends QBMapper {
public function __construct(
IDBConnection $db,
private readonly IMimeTypeLoader $mimeTypeLoader,
private readonly IGenerator $snowflake,
private readonly ISnowflakeGenerator $snowflake,
) {
parent::__construct($db, self::TABLE_NAME, Preview::class);
}

View file

@ -23,7 +23,7 @@ use OCP\IPreview;
use OCP\IStreamImage;
use OCP\Preview\BeforePreviewFetchedEvent;
use OCP\Preview\IVersionedPreviewFile;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Psr\Log\LoggerInterface;
class Generator {
@ -38,7 +38,7 @@ class Generator {
private LoggerInterface $logger,
private PreviewMapper $previewMapper,
private StorageFactory $storageFactory,
private IGenerator $snowflakeGenerator,
private ISnowflakeGenerator $snowflakeGenerator,
) {
}

View file

@ -22,7 +22,7 @@ use OCP\Files\NotPermittedException;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
use Psr\Log\LoggerInterface;
use RecursiveDirectoryIterator;
@ -39,7 +39,7 @@ class LocalPreviewStorage implements IPreviewStorage {
private readonly IDBConnection $connection,
private readonly IMimeTypeDetector $mimeTypeDetector,
private readonly LoggerInterface $logger,
private readonly IGenerator $generator,
private readonly ISnowflakeGenerator $generator,
) {
$this->instanceId = $this->config->getSystemValueString('instanceid');
$this->rootFolder = $this->config->getSystemValue('datadirectory', OC::$SERVERROOT . '/data');

View file

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

View file

@ -117,10 +117,10 @@ use OC\SetupCheck\SetupCheckManager;
use OC\Share20\ProviderFactory;
use OC\Share20\ShareHelper;
use OC\Snowflake\APCuSequence;
use OC\Snowflake\Decoder;
use OC\Snowflake\FileSequence;
use OC\Snowflake\Generator;
use OC\Snowflake\ISequence;
use OC\Snowflake\SnowflakeDecoder;
use OC\Snowflake\SnowflakeGenerator;
use OC\SpeechToText\SpeechToTextManager;
use OC\SystemTag\ManagerFactory as SystemTagManagerFactory;
use OC\Talk\Broker;
@ -179,6 +179,7 @@ use OCP\IBinaryFinder;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IDateTimeZone;
use OCP\IDBConnection;
@ -230,8 +231,8 @@ use OCP\Settings\IDeclarativeManager;
use OCP\SetupCheck\ISetupCheckManager;
use OCP\Share\IProviderFactory;
use OCP\Share\IShareHelper;
use OCP\Snowflake\IDecoder;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeDecoder;
use OCP\Snowflake\ISnowflakeGenerator;
use OCP\SpeechToText\ISpeechToTextManager;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
@ -1266,7 +1267,7 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(ISignatureManager::class, SignatureManager::class);
$this->registerAlias(IGenerator::class, Generator::class);
$this->registerAlias(ISnowflakeGenerator::class, SnowflakeGenerator::class);
$this->registerService(ISequence::class, function (ContainerInterface $c): ISequence {
if (PHP_SAPI !== 'cli') {
$sequence = $c->get(APCuSequence::class);
@ -1277,7 +1278,7 @@ class Server extends ServerContainer implements IServerContainer {
return $c->get(FileSequence::class);
}, false);
$this->registerAlias(IDecoder::class, Decoder::class);
$this->registerAlias(ISnowflakeDecoder::class, SnowflakeDecoder::class);
$this->connectDispatcher();
}

View file

@ -26,11 +26,7 @@ class APCuSequence implements ISequence {
}
$key = 'seq:' . $seconds . ':' . $milliseconds;
$sequenceId = apcu_inc($key, success: $success, ttl: 1);
if ($success === true) {
return $sequenceId;
}
throw new \Exception('Failed to generate SnowflakeId with APCu');
$sequenceId = apcu_inc($key, ttl: 1);
return $sequenceId === false ? throw new \Exception('Failed to generate SnowflakeId with APCu') : $sequenceId;
}
}

View file

@ -9,8 +9,9 @@ declare(strict_types=1);
namespace OC\Snowflake;
use OCP\Snowflake\IDecoder;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeDecoder;
use OCP\Snowflake\ISnowflakeGenerator;
use OCP\Snowflake\Snowflake;
use Override;
/**
@ -20,9 +21,9 @@ use Override;
*
* @since 33.0.0
*/
final class Decoder implements IDecoder {
final class SnowflakeDecoder implements ISnowflakeDecoder {
#[Override]
public function decode(string $snowflakeId): array {
public function decode(string $snowflakeId): Snowflake {
if (!ctype_digit($snowflakeId)) {
throw new \Exception('Invalid Snowflake ID: ' . $snowflakeId);
}
@ -35,12 +36,19 @@ final class Decoder implements IDecoder {
$data['createdAt'] = new \DateTimeImmutable(
sprintf(
'@%d.%03d',
$data['seconds'] + IGenerator::TS_OFFSET + intdiv($data['milliseconds'], 1000),
$data['seconds'] + ISnowflakeGenerator::TS_OFFSET + intdiv($data['milliseconds'], 1000),
$data['milliseconds'] % 1000,
)
);
return $data;
return new Snowflake(
$data['serverId'],
$data['sequenceId'],
$data['isCli'],
$data['seconds'],
$data['milliseconds'],
$data['createdAt'],
);
}
private function decode64bits(int $snowflakeId): array {
@ -51,11 +59,11 @@ final class Decoder implements IDecoder {
$milliseconds = $secondHalf >> 22;
return [
'seconds' => $seconds,
'milliseconds' => $milliseconds,
'serverId' => ($secondHalf >> 13) & 0x1FF,
'sequenceId' => $secondHalf & 0xFFF,
'isCli' => (bool)(($secondHalf >> 12) & 0x1),
'seconds' => $seconds,
'milliseconds' => $milliseconds,
];
}
@ -88,12 +96,12 @@ final class Decoder implements IDecoder {
$hex = '';
$digits = '0123456789ABCDEF';
while (strlen($decimal) > 0 && $decimal !== '0') {
while ($decimal !== '' && $decimal !== '0') {
$remainder = 0;
$newDecimal = '';
// Perform division by 16 manually for arbitrary precision
for ($i = 0; $i < strlen($decimal); $i++) {
for ($i = 0, $iMax = strlen($decimal); $i < $iMax; $i++) {
$digit = (int)$decimal[$i];
$current = $remainder * 10 + $digit;
@ -104,7 +112,7 @@ final class Decoder implements IDecoder {
} else {
$remainder = $current;
// Only add quotient digit if we already have some digits in result
if (strlen($newDecimal) > 0) {
if ($newDecimal !== '') {
$newDecimal .= '0';
}
}

View file

@ -11,7 +11,7 @@ namespace OC\Snowflake;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Override;
/**
@ -21,11 +21,11 @@ use Override;
*
* @since 33.0.0
*/
final class Generator implements IGenerator {
final readonly class SnowflakeGenerator implements ISnowflakeGenerator {
public function __construct(
private readonly ITimeFactory $timeFactory,
private readonly IConfig $config,
private readonly ISequence $sequenceGenerator,
private ITimeFactory $timeFactory,
private IConfig $config,
private ISequence $sequenceGenerator,
) {
}

View file

@ -14,11 +14,11 @@ use OCP\AppFramework\Attribute\Consumable;
/**
* Nextcloud Snowflake ID decoder
*
* @see \OCP\Snowflake\IGenerator for format
* @see \OCP\Snowflake\ISnowflakeGenerator for format
* @since 33.0.0
*/
#[Consumable(since: '33.0.0')]
interface IDecoder {
interface ISnowflakeDecoder {
/**
* Decode information contained into Snowflake ID
*
@ -28,8 +28,8 @@ interface IDecoder {
* - createdAt: timestamp at which ID was generated
* - isCli: if ID was generated using CLI or not
*
* @return array{createdAt: \DateTimeImmutable, serverId: int<0,1023>, sequenceId: int<0,4095>, isCli: bool, seconds: positive-int, milliseconds: int<0,999>}
* @return Snowflake
* @since 33.0
*/
public function decode(string $snowflakeId): array;
public function decode(string $snowflakeId): Snowflake;
}

View file

@ -25,7 +25,7 @@ use OCP\AppFramework\Attribute\Consumable;
* @since 33.0.0
*/
#[Consumable(since: '33.0.0')]
interface IGenerator {
interface ISnowflakeGenerator {
/**
* Offset applied on timestamps to keep it short

View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Snowflake;
use OCP\AppFramework\Attribute\Consumable;
/**
* Nextcloud Snowflake DTO
*
* @since 33.0.0
*/
#[Consumable(since: '33.0.0')]
final readonly class Snowflake {
/**
* @psalm-param int<0,1023> $serverId
* @psalm-param int<0,4095> $sequenceId
* @psalm-param non-negative-int $seconds
* @psalm-param int<0,999> $milliseconds
*/
public function __construct(
private int $serverId,
private int $sequenceId,
private bool $isCli,
private int $seconds,
private int $milliseconds,
private \DateTimeImmutable $createdAt,
) {
}
/**
* @psalm-return int<0,1023>
*/
public function getServerId(): int {
return $this->serverId;
}
/**
* @psalm-return int<0,4095>
*/
public function getSequenceId(): int {
return $this->sequenceId;
}
public function isCli(): bool {
return $this->isCli;
}
/**
* @psalm-return non-negative-int
*/
public function getSeconds(): int {
return $this->seconds;
}
/**
* @psalm-return int<0,999>
*/
public function getMilliseconds(): int {
return $this->milliseconds;
}
public function getCreatedAt(): \DateTimeImmutable {
return $this->createdAt;
}
}

View file

@ -13,7 +13,7 @@ use OC\BackgroundJob\JobList;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\Job;
use OCP\Server;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
/**
* Class DummyJobList
@ -46,7 +46,7 @@ class DummyJobList extends JobList {
$job = Server::get($job);
}
$job->setArgument($argument);
$job->setId(Server::get(IGenerator::class)->nextId());
$job->setId(Server::get(ISnowflakeGenerator::class)->nextId());
if (!$this->has($job, null)) {
$this->jobs[] = $job;
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016-2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@ -16,7 +16,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Server;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@ -46,7 +46,7 @@ class JobListTest extends TestCase {
$this->config,
$this->timeFactory,
Server::get(LoggerInterface::class),
Server::get(IGenerator::class),
Server::get(ISnowflakeGenerator::class),
);
}
@ -146,7 +146,7 @@ class JobListTest extends TestCase {
if ($lastChecked === 0) {
$lastChecked = time();
}
$id = Server::get(IGenerator::class)->nextId();
$id = Server::get(ISnowflakeGenerator::class)->nextId();
$query = $this->connection->getQueryBuilder();
$query->insert('jobs')

View file

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

View file

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

View file

@ -14,20 +14,20 @@ use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper;
use OCP\IDBConnection;
use OCP\Server;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use Test\TestCase;
#[\PHPUnit\Framework\Attributes\Group('DB')]
class PreviewMapperTest extends TestCase {
private PreviewMapper $previewMapper;
private IDBConnection $connection;
private IGenerator $snowflake;
private ISnowflakeGenerator $snowflake;
public function setUp(): void {
parent::setUp();
$this->previewMapper = Server::get(PreviewMapper::class);
$this->connection = Server::get(IDBConnection::class);
$this->snowflake = Server::get(IGenerator::class);
$this->snowflake = Server::get(ISnowflakeGenerator::class);
$qb = $this->connection->getQueryBuilder();
$qb->delete('preview_locations')->executeStatement();

View file

@ -14,7 +14,7 @@ use OC\Preview\Db\Preview;
use OC\Preview\Db\PreviewMapper;
use OC\Preview\PreviewService;
use OCP\Server;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\TestCase;
#[CoversClass(PreviewService::class)]
@ -22,13 +22,13 @@ use PHPUnit\Framework\TestCase;
class PreviewServiceTest extends TestCase {
private PreviewService $previewService;
private PreviewMapper $previewMapper;
private IGenerator $snowflakeGenerator;
private ISnowflakeGenerator $snowflakeGenerator;
protected function setUp(): void {
parent::setUp();
$this->previewService = Server::get(PreviewService::class);
$this->previewMapper = Server::get(PreviewMapper::class);
$this->snowflakeGenerator = Server::get(IGenerator::class);
$this->snowflakeGenerator = Server::get(ISnowflakeGenerator::class);
$this->previewService->deleteAll();
}

View file

@ -7,7 +7,7 @@
namespace Test\Snowflake;
use OC\Snowflake\Decoder;
use OC\Snowflake\SnowflakeDecoder;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;
@ -15,10 +15,10 @@ use Test\TestCase;
* @package Test
*/
class DecoderTest extends TestCase {
private Decoder $decoder;
private SnowflakeDecoder $decoder;
public function setUp():void {
$this->decoder = new Decoder();
$this->decoder = new SnowflakeDecoder();
}
#[DataProvider('provideSnowflakeIds')]
@ -31,10 +31,10 @@ class DecoderTest extends TestCase {
): void {
$data = $this->decoder->decode($snowflakeId);
$this->assertEquals($timestamp, (float)$data['createdAt']->format('U.v'));
$this->assertEquals($serverId, $data['serverId']);
$this->assertEquals($sequenceId, $data['sequenceId']);
$this->assertEquals($isCli, $data['isCli']);
$this->assertEquals($timestamp, (float)$data->getCreatedAt()->format('U.v'));
$this->assertEquals($serverId, $data->getServerId());
$this->assertEquals($sequenceId, $data->getSequenceId());
$this->assertEquals($isCli, $data->isCli());
}
public static function provideSnowflakeIds(): array {

View file

@ -10,12 +10,12 @@ declare(strict_types=1);
namespace Test\Snowflake;
use OC\AppFramework\Utility\TimeFactory;
use OC\Snowflake\Decoder;
use OC\Snowflake\Generator;
use OC\Snowflake\ISequence;
use OC\Snowflake\SnowflakeDecoder;
use OC\Snowflake\SnowflakeGenerator;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use OCP\Snowflake\ISnowflakeGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -24,12 +24,12 @@ use Test\TestCase;
* @package Test
*/
class GeneratorTest extends TestCase {
private Decoder $decoder;
private SnowflakeDecoder $decoder;
private IConfig&MockObject $config;
private ISequence&MockObject $sequence;
public function setUp():void {
$this->decoder = new Decoder();
$this->decoder = new SnowflakeDecoder();
$this->config = $this->createMock(IConfig::class);
$this->config->method('getSystemValueInt')
@ -43,27 +43,27 @@ class GeneratorTest extends TestCase {
}
public function testGenerator(): void {
$generator = new Generator(new TimeFactory(), $this->config, $this->sequence);
$generator = new SnowflakeGenerator(new TimeFactory(), $this->config, $this->sequence);
$snowflakeId = $generator->nextId();
$data = $this->decoder->decode($generator->nextId());
$this->assertIsString($snowflakeId);
// Check timestamp
$this->assertGreaterThan(time() - 30, $data['createdAt']->format('U'));
$this->assertGreaterThan(time() - 30, $data->getCreatedAt()->format('U'));
// Check serverId
$this->assertGreaterThanOrEqual(0, $data['serverId']);
$this->assertLessThanOrEqual(1023, $data['serverId']);
$this->assertGreaterThanOrEqual(0, $data->getServerId());
$this->assertLessThanOrEqual(1023, $data->getServerId());
// Check sequenceId
$this->assertGreaterThanOrEqual(0, $data['sequenceId']);
$this->assertLessThanOrEqual(4095, $data['sequenceId']);
$this->assertGreaterThanOrEqual(0, $data->getSequenceId());
$this->assertLessThanOrEqual(4095, $data->getSequenceId());
// Check CLI
$this->assertTrue($data['isCli']);
$this->assertTrue($data->isCli());
// Check serverId
$this->assertEquals(42, $data['serverId']);
$this->assertEquals(42, $data->getServerId());
}
#[DataProvider('provideSnowflakeData')]
@ -72,12 +72,12 @@ class GeneratorTest extends TestCase {
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->method('now')->willReturn($dt);
$generator = new Generator($timeFactory, $this->config, $this->sequence);
$generator = new SnowflakeGenerator($timeFactory, $this->config, $this->sequence);
$data = $this->decoder->decode($generator->nextId());
$this->assertEquals($expectedSeconds, ($data['createdAt']->format('U') - IGenerator::TS_OFFSET));
$this->assertEquals($expectedMilliseconds, (int)$data['createdAt']->format('v'));
$this->assertEquals(42, $data['serverId']);
$this->assertEquals($expectedSeconds, ($data->getCreatedAt()->format('U') - ISnowflakeGenerator::TS_OFFSET));
$this->assertEquals($expectedMilliseconds, (int)$data->getCreatedAt()->format('v'));
$this->assertEquals(42, $data->getServerId());
}
public static function provideSnowflakeData(): array {