Browse Source

fix(preview): Fix some tests

Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
pull/54543/head
Carl Schwan 1 month ago
parent
commit
6f56dcf73e
  1. 8
      core/BackgroundJobs/MovePreviewJob.php
  2. 58
      lib/private/Preview/BackgroundCleanupJob.php
  3. 35
      lib/private/Preview/Db/PreviewMapper.php
  4. 5
      lib/private/Preview/Generator.php
  5. 44
      lib/private/Preview/Storage/LocalPreviewStorage.php
  6. 1
      lib/private/Preview/Storage/ObjectStorePreviewStorage.php
  7. 74
      lib/private/Preview/Storage/Root.php
  8. 36
      lib/private/Preview/Watcher.php
  9. 3
      lib/private/PreviewManager.php
  10. 16
      lib/private/Server.php
  11. 7
      tests/lib/Preview/BackgroundCleanupJobTest.php
  12. 75
      tests/lib/Preview/MovePreviewJobTest.php
  13. 4
      tests/lib/Preview/PreviewMapperTest.php
  14. 2
      version.php

8
core/BackgroundJobs/MovePreviewJob.php

@ -52,7 +52,7 @@ class MovePreviewJob extends TimedJob {
private function doRun($argument): void {
if ($this->appConfig->getValueBool('core', 'previewMovedDone')) {
//return;
return;
}
$emptyHierarchicalPreviewFolders = false;
@ -88,7 +88,7 @@ class MovePreviewJob extends TimedJob {
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('filecache')
->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%.jpg')))
->where($qb->expr()->like('path', $qb->createNamedParameter('appdata_%/preview/%/%.%')))
->setMaxResults(100);
$result = $qb->executeQuery();
@ -118,13 +118,13 @@ class MovePreviewJob extends TimedJob {
}
}
// Delete any left over preview directory
// Delete any leftover preview directory
$this->appData->getFolder('.')->delete();
$this->appConfig->setValueBool('core', 'previewMovedDone', true);
}
/**
* @param array<string, string[]> $previewFolders
* @param array<string|int, string[]> $previewFolders
*/
private function processPreviews(array $previewFolders, bool $simplePaths): void {
foreach ($previewFolders as $fileId => $previewFolder) {

58
lib/private/Preview/BackgroundCleanupJob.php

@ -35,23 +35,18 @@ class BackgroundCleanupJob extends TimedJob {
}
public function run($argument): void {
foreach ($this->getDeletedFiles() as $chunk) {
foreach ($chunk as $storage => $fileIds) {
foreach ($this->previewMapper->getByFileIds($storage, $fileIds) as $previews) {
$previewIds = [];
foreach ($previews as $preview) {
$previewIds[] = $preview->getId();
$this->storageFactory->deletePreview($preview);
}
$this->previewMapper->deleteByIds($storage, $previewIds);
};
foreach ($this->getDeletedFiles() as $fileId) {
$previewIds = [];
foreach ($this->previewMapper->getByFileId($fileId) as $preview) {
$previewIds[] = $preview->getId();
$this->storageFactory->deletePreview($preview);
}
$this->previewMapper->deleteByIds($previewIds);
}
}
/**
* @return \Iterator<array<StorageId, FileId[]>>
* @return \Iterator<FileId>
*/
private function getDeletedFiles(): \Iterator {
if ($this->connection->getShardDefinition('filecache')) {
@ -81,7 +76,7 @@ class BackgroundCleanupJob extends TimedJob {
* If the related file is deleted, b.fileid will be null and the preview folder can be deleted.
*/
$qb = $this->connection->getQueryBuilder();
$qb->select('p.storage_id', 'p.file_id')
$qb->select('p.file_id')
->from('previews', 'p')
->leftJoin('p', 'filecache', 'f', $qb->expr()->eq(
'p.file_id', 'f.fileid'
@ -93,30 +88,14 @@ class BackgroundCleanupJob extends TimedJob {
}
$cursor = $qb->executeQuery();
$lastStorageId = null;
/** @var FileId[] $tmpResult */
$tmpResult = [];
while ($row = $cursor->fetch()) {
if ($lastStorageId === null) {
$lastStorageId = $row['storage_id'];
} else if ($lastStorageId !== $row['storage_id']) {
yield [$lastStorageId => $tmpResult];
$tmpResult = [];
$lastStorageId = $row['storage_id'];
}
$tmpResult[] = $row['file_id'];
yield $row['file_id'];
}
if (!empty($tmpResult)) {
yield [$lastStorageId => $tmpResult];
}
$cursor->closeCursor();
}
/**
* @return \Iterator<array<StorageId, FileId[]>>
* @return \Iterator<FileId>
*/
private function getAllPreviewIds(int $chunkSize): \Iterator {
$qb = $this->connection->getQueryBuilder();
@ -131,20 +110,11 @@ class BackgroundCleanupJob extends TimedJob {
$minId = 0;
while (true) {
$qb->setParameter('min_id', $minId);
$rows = $qb->executeQuery()->fetchAll();
if (count($rows) > 0) {
$minId = $rows[count($rows) - 1]['id'];
$result = [];
foreach ($rows as $row) {
if (!isset($result[$row['storage_id']])) {
$result[$row['storage_id']] = [];
}
$result[$row['storage_id']][] = $row['file_id'];
}
yield $result;
} else {
break;
$cursor = $qb->executeQuery();
while ($row = $cursor->fetch()) {
yield $row['file_id'];
}
$cursor->closeCursor();
}
}

35
lib/private/Preview/Db/PreviewMapper.php

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OC\Preview\Db;
use OC\Preview\Generator;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
@ -28,6 +29,17 @@ class PreviewMapper extends QBMapper {
parent::__construct($db, self::TABLE_NAME, Preview::class);
}
/**
* @return \Generator<Preview>
* @throws Exception
*/
public function getAvailablePreviewForFile(int $fileId): \Generator {
$selectQb = $this->db->getQueryBuilder();
$this->joinLocation($selectQb)
->where($selectQb->expr()->eq('p.file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
yield from $this->yieldEntities($selectQb);
}
/**
* @param int[] $fileIds
* @return array<int, Preview[]>
@ -37,7 +49,7 @@ class PreviewMapper extends QBMapper {
$selectQb = $this->db->getQueryBuilder();
$this->joinLocation($selectQb)
->where(
$selectQb->expr()->in('file_id', $selectQb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)),
$selectQb->expr()->in('p.file_id', $selectQb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)),
);
$previews = array_fill_keys($fileIds, []);
foreach ($this->yieldEntities($selectQb) as $preview) {
@ -64,20 +76,13 @@ class PreviewMapper extends QBMapper {
}
/**
* @param int[] $fileIds
* @return array<int, Preview[]>
* @return \Generator<Preview>
*/
public function getByFileIds(array $fileIds): array {
public function getByFileId(int $fileId): \Generator {
$selectQb = $this->db->getQueryBuilder();
$this->joinLocation($selectQb)
->where($selectQb->expr()->andX(
$selectQb->expr()->in('file_id', $selectQb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)),
));
$previews = array_fill_keys($fileIds, []);
foreach ($this->yieldEntities($selectQb) as $preview) {
$previews[$preview->getFileId()][] = $preview;
}
return $previews;
->where($selectQb->expr()->eq('file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
yield from $this->yieldEntities($selectQb);
}
/**
@ -94,9 +99,9 @@ class PreviewMapper extends QBMapper {
protected function joinLocation(IQueryBuilder $qb): IQueryBuilder {
return $qb->select('p.*', 'l.bucket_name', 'l.object_store_name')
->from(self::TABLE_NAME, 'p')
->join('p', 'preview_locations', 'l', $qb->expr()->eq(
'p.location_id', 'l.id'
));
->leftJoin('p', 'preview_locations', 'l', $qb->expr()->eq(
'p.location_id', 'l.id'
));
}
public function getLocationId(string $bucket, string $objectStore): int {

5
lib/private/Preview/Generator.php

@ -349,7 +349,7 @@ class Generator {
try {
return $this->savePreview($file, $preview->width(), $preview->height(), $crop, $max, $preview, $version);
} catch (NotPermittedException $e) {
} catch (NotPermittedException) {
throw new NotFoundException();
}
}
@ -571,6 +571,9 @@ class Generator {
} else {
$size = $this->storageFactory->writePreview($previewEntry, $preview->data());
}
if (!$size) {
throw new \RuntimeException('Unable to write preview file');
}
} catch (\Exception $e) {
$this->previewMapper->delete($previewEntry);
throw $e;

44
lib/private/Preview/Storage/LocalPreviewStorage.php

@ -17,7 +17,6 @@ use OC\Preview\Db\Preview;
use OCP\IConfig;
class LocalPreviewStorage implements IPreviewStorage {
private const PREVIEW_DIRECTORY = '__preview';
private readonly string $rootFolder;
private readonly string $instanceId;
@ -30,19 +29,18 @@ class LocalPreviewStorage implements IPreviewStorage {
public function writePreview(Preview $preview, $stream): false|int {
$previewPath = $this->constructPath($preview);
$this->createParentFiles($previewPath);
$file = @fopen($this->getPreviewRootFolder() . $previewPath, 'w');
return fwrite($file, $stream);
if (!$this->createParentFiles($previewPath)) {
return false;
}
return file_put_contents($previewPath, $stream);
}
public function readPreview(Preview $preview) {
$previewPath = $this->constructPath($preview);
return @fopen($this->getPreviewRootFolder() . $previewPath, 'r');
return @fopen($this->constructPath($preview), 'r');
}
public function deletePreview(Preview $preview) {
$previewPath = $this->constructPath($preview);
@unlink($this->getPreviewRootFolder() . $previewPath);
@unlink($this->constructPath($preview));
}
public function getPreviewRootFolder(): string {
@ -50,36 +48,28 @@ class LocalPreviewStorage implements IPreviewStorage {
}
private function constructPath(Preview $preview): string {
return implode('/', str_split(substr(md5((string)$preview->getFileId()), 0, 7))) . '/' . $preview->getFileId() . '/' . $preview->getName();
return $this->getPreviewRootFolder() . implode('/', str_split(substr(md5((string)$preview->getFileId()), 0, 7))) . '/' . $preview->getFileId() . '/' . $preview->getName();
}
private function createParentFiles($path) {
['basename' => $basename, 'dirname' => $dirname] = pathinfo($path);
$currentDir = $this->rootFolder . '/' . self::PREVIEW_DIRECTORY;
mkdir($currentDir);
foreach (explode('/', $dirname) as $suffix) {
$currentDir .= "/$suffix";
mkdir($currentDir);
}
private function createParentFiles(string $path): bool {
['dirname' => $dirname] = pathinfo($path);
return mkdir($dirname, recursive: true);
}
public function migratePreview(Preview $preview, SimpleFile $file): void {
$instanceId = $this->config->getSystemValueString('instanceid');
$previewPath = $this->constructPath($preview);
$sourcePath = $this->rootFolder . '/appdata_' . $instanceId . '/preview/' . $previewPath;
$destinationPath = $this->rootFolder . '/' . self::PREVIEW_DIRECTORY . '/' . $previewPath;
if (file_exists($sourcePath)) {
return; // No need to migrate
// legacy flat directory
$sourcePath = $this->getPreviewRootFolder() . $preview->getFileId() . '/' . $preview->getName();
if (!file_exists($sourcePath)) {
return;
}
// legacy flat directory
$sourcePath = $this->rootFolder . '/appdata_' . $instanceId . '/preview/' . $preview->getFileId() . '/' . $preview->getName();
$destinationPath = $this->constructPath($preview);
if (file_exists($destinationPath)) {
@unlink($sourcePath); // We already have a new preview, just delete the old one
return;
}
$this->createParentFiles($previewPath);
echo 'Copying ' . $sourcePath . ' to ' . $destinationPath . PHP_EOL;
$this->createParentFiles($destinationPath);
$ok = rename($sourcePath, $destinationPath);
if (!$ok) {
throw new LogicException('Failed to copy ' . $sourcePath . ' to ' . $destinationPath);

1
lib/private/Preview/Storage/ObjectStorePreviewStorage.php

@ -83,6 +83,7 @@ class ObjectStorePreviewStorage implements IPreviewStorage {
public function migratePreview(Preview $preview, SimpleFile $file): void {
// Just set the Preview::bucket and Preview::objectStore
$this->getObjectStoreForPreview($preview, true);
$this->previewMapper->update($preview);
}
/**

74
lib/private/Preview/Storage/Root.php

@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Preview\Storage;
use OC\Files\AppData\AppData;
use OC\SystemConfig;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFolder;
class Root extends AppData {
private $isMultibucketPreviewDistributionEnabled = false;
public function __construct(IRootFolder $rootFolder, SystemConfig $systemConfig) {
parent::__construct($rootFolder, $systemConfig, 'preview');
$this->isMultibucketPreviewDistributionEnabled = $systemConfig->getValue('objectstore.multibucket.preview-distribution', false) === true;
}
public function getFolder(string $name): ISimpleFolder {
$internalFolder = self::getInternalFolder($name);
try {
return parent::getFolder($internalFolder);
} catch (NotFoundException $e) {
/*
* The new folder structure is not found.
* Lets try the old one
*/
}
try {
return parent::getFolder($name);
} catch (NotFoundException $e) {
/*
* The old folder structure is not found.
* Lets try the multibucket fallback if available
*/
if ($this->isMultibucketPreviewDistributionEnabled) {
return parent::getFolder('old-multibucket/' . $internalFolder);
}
// when there is no further fallback just throw the exception
throw $e;
}
}
public function newFolder(string $name): ISimpleFolder {
$internalFolder = self::getInternalFolder($name);
return parent::newFolder($internalFolder);
}
/*
* Do not allow directory listing on this special root
* since it gets to big and time consuming
*/
public function getDirectoryListing(): array {
return [];
}
public static function getInternalFolder(string $name): string {
return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
}
public function getStorageId(): int {
return $this->getAppDataRootFolder()->getStorage()->getCache()->getNumericStorageId();
}
}

36
lib/private/Preview/Watcher.php

@ -8,11 +8,11 @@ declare(strict_types=1);
*/
namespace OC\Preview;
use OC\Preview\Db\PreviewMapper;
use OC\Preview\Storage\StorageFactory;
use OCP\Files\FileInfo;
use OCP\Files\Folder;
use OCP\Files\IAppData;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
/**
* Class Watcher
@ -22,40 +22,36 @@ use OCP\Files\NotFoundException;
* Class that will watch filesystem activity and remove previews as needed.
*/
class Watcher {
/** @var IAppData */
private $appData;
/**
* Watcher constructor.
*
* @param IAppData $appData
*/
public function __construct(IAppData $appData) {
$this->appData = $appData;
public function __construct(
readonly private StorageFactory $storageFactory,
readonly private PreviewMapper $previewMapper,
) {
}
public function postWrite(Node $node) {
public function postWrite(Node $node): void {
$this->deleteNode($node);
}
protected function deleteNode(FileInfo $node) {
protected function deleteNode(FileInfo $node): void {
// We only handle files
if ($node instanceof Folder) {
return;
}
try {
if (is_null($node->getId())) {
return;
}
$folder = $this->appData->getFolder((string)$node->getId());
$folder->delete();
} catch (NotFoundException $e) {
//Nothing to do
if (is_null($node->getId())) {
return;
}
[$node->getId() => $previews] = $this->previewMapper->getAvailablePreviews([$node->getId()]);
foreach ($previews as $preview) {
$this->storageFactory->deletePreview($preview);
}
}
public function versionRollback(array $data) {
public function versionRollback(array $data): void {
if (isset($data['node'])) {
$this->deleteNode($data['node']);
}

3
lib/private/PreviewManager.php

@ -32,7 +32,6 @@ use function array_key_exists;
class PreviewManager implements IPreview {
protected IConfig $config;
protected IRootFolder $rootFolder;
protected IAppData $appData;
protected IEventDispatcher $eventDispatcher;
private ?Generator $generator = null;
private GeneratorHelper $helper;
@ -59,7 +58,6 @@ class PreviewManager implements IPreview {
public function __construct(
IConfig $config,
IRootFolder $rootFolder,
IAppData $appData,
IEventDispatcher $eventDispatcher,
GeneratorHelper $helper,
?string $userId,
@ -70,7 +68,6 @@ class PreviewManager implements IPreview {
) {
$this->config = $config;
$this->rootFolder = $rootFolder;
$this->appData = $appData;
$this->eventDispatcher = $eventDispatcher;
$this->helper = $helper;
$this->userId = $userId;

16
lib/private/Server.php

@ -82,9 +82,11 @@ use OC\Notification\Manager;
use OC\OCM\Model\OCMProvider;
use OC\OCM\OCMDiscoveryService;
use OC\OCS\DiscoveryService;
use OC\Preview\Db\PreviewMapper;
use OC\Preview\GeneratorHelper;
use OC\Preview\IMagickSupport;
use OC\Preview\MimeIconProvider;
use OC\Preview\Watcher;
use OC\Profile\ProfileManager;
use OC\Profiler\Profiler;
use OC\Remote\Api\ApiFactory;
@ -291,10 +293,6 @@ class Server extends ServerContainer implements IServerContainer {
return new PreviewManager(
$c->get(\OCP\IConfig::class),
$c->get(IRootFolder::class),
new \OC\Preview\Storage\Root(
$c->get(IRootFolder::class),
$c->get(SystemConfig::class)
),
$c->get(IEventDispatcher::class),
$c->get(GeneratorHelper::class),
$c->get(ISession::class)->get('user_id'),
@ -306,12 +304,10 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerAlias(IMimeIconProvider::class, MimeIconProvider::class);
$this->registerService(\OC\Preview\Watcher::class, function (ContainerInterface $c) {
return new \OC\Preview\Watcher(
new \OC\Preview\Storage\Root(
$c->get(IRootFolder::class),
$c->get(SystemConfig::class)
)
$this->registerService(Watcher::class, function (ContainerInterface $c): Watcher {
return new Watcher(
$c->get(\OC\Preview\Storage\StorageFactory::class),
$c->get(PreviewMapper::class),
);
});

7
tests/lib/Preview/BackgroundCleanupJobTest.php

@ -82,6 +82,11 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$this->logout();
foreach ($this->previewMapper->getAvailablePreviews(5) as $preview) {
$this->previewStorageFactory->deletePreview($preview);
$this->previewMapper->delete($preview);
}
parent::tearDown();
}
@ -89,7 +94,7 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
$files = [];
for ($i = 0; $i < 11; $i++) {
foreach (range(0, 10) as $i) {
$file = $userFolder->newFile($i . '.txt');
$file->putContent('hello world!');
$this->previewManager->getPreview($file);

75
tests/lib/Preview/MovePreviewJobTest.php

@ -3,11 +3,17 @@
namespace lib\Preview;
use OC\Core\BackgroundJobs\MovePreviewJob;
use OC\Preview\Db\PreviewMapper;
use OC\Preview\Storage\StorageFactory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\IAppData;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\Server;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/**
@ -16,17 +22,78 @@ use Test\TestCase;
#[CoversClass(MovePreviewJob::class)]
class MovePreviewJobTest extends TestCase {
private IAppData $previewAppData;
private PreviewMapper $previewMapper;
private IAppConfig&MockObject $appConfig;
private StorageFactory $storageFactory;
public function setUp(): void {
parent::setUp();
$this->previewAppData = Server::get(IAppDataFactory::class)->get('preview');
$this->previewMapper = Server::get(PreviewMapper::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->appConfig->expects($this->any())
->method('getValueBool')
->willReturn(false);
$this->storageFactory = Server::get(StorageFactory::class);
}
#[TestDox("Test the migration from the legacy flat hierarchy to the new one")]
public function tearDown(): void {
foreach ($this->previewMapper->getAvailablePreviewForFile(5) as $preview) {
$this->storageFactory->deletePreview($preview);
$this->previewMapper->delete($preview);
}
foreach ($this->previewAppData->getDirectoryListing() as $folder) {
$folder->delete();
}
}
#[TestDox("Test the migration from the legacy flat hierarchy to the new database format")]
function testMigrationLegacyPath(): void {
$folder = $this->previewAppData->newFolder(5);
$file = $folder->newFile('64-64-crop.png', 'abcdefg');
$job = Server::get(MovePreviewJob::class);
$this->invokePrivate($job, 'run', []);
$folder->newFile('64-64-crop.jpg', 'abcdefg');
$folder->newFile('128-128-crop.png', 'abcdefg');
$this->assertEquals(1, count($this->previewAppData->getDirectoryListing()));
$this->assertEquals(2, count($folder->getDirectoryListing()));
$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
$job = new MovePreviewJob(
Server::get(ITimeFactory::class),
$this->appConfig,
$this->previewMapper,
$this->storageFactory,
Server::get(IDBConnection::class),
Server::get(IAppDataFactory::class)
);
$this->invokePrivate($job, 'run', [[]]);
$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
$this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
}
private static function getInternalFolder(string $name): string {
return implode('/', str_split(substr(md5($name), 0, 7))) . '/' . $name;
}
#[TestDox("Test the migration from the 'new' nested hierarchy to the database format")]
function testMigrationPath(): void {
$folder = $this->previewAppData->newFolder(self::getInternalFolder(5));
$folder->newFile('64-64-crop.jpg', 'abcdefg');
$folder->newFile('128-128-crop.png', 'abcdefg');
$folder = $this->previewAppData->getFolder(self::getInternalFolder(5));
$this->assertEquals(2, count($folder->getDirectoryListing()));
$this->assertEquals(0, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
$job = new MovePreviewJob(
Server::get(ITimeFactory::class),
$this->appConfig,
$this->previewMapper,
$this->storageFactory,
Server::get(IDBConnection::class),
Server::get(IAppDataFactory::class)
);
$this->invokePrivate($job, 'run', [[]]);
$this->assertEquals(0, count($this->previewAppData->getDirectoryListing()));
$this->assertEquals(2, count(iterator_to_array($this->previewMapper->getAvailablePreviewForFile(5))));
}
}

4
tests/lib/Preview/PreviewMapperTest.php

@ -55,8 +55,8 @@ class PreviewMapperTest extends TestCase {
$qb = $this->connection->getQueryBuilder();
$qb->insert('preview_locations')
->values([
'bucket' => $qb->createNamedParameter('preview-' . $bucket),
'object_store' => $qb->createNamedParameter('default'),
'bucket_name' => $qb->createNamedParameter('preview-' . $bucket),
'object_store_name' => $qb->createNamedParameter('default'),
]);
$locationId = $qb->executeStatement();
}

2
version.php

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

Loading…
Cancel
Save