Merge pull request #58826 from nextcloud/carl/code-cleaning-storage-cache
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Integration sqlite / changes (push) Waiting to run
Integration sqlite / integration-sqlite (master, 8.4, main, --tags ~@large files_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, capabilities_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, collaboration_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, comments_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, dav_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, federation_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, file_conversions) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, files_reminders) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, filesdrop_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, ldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, openldap_numerical_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, remoteapi_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, routing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, setup_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharees_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, sharing_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, theming_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite (master, 8.4, main, videoverification_features) (push) Blocked by required conditions
Integration sqlite / integration-sqlite-summary (push) Blocked by required conditions
Psalm static code analysis / static-code-analysis (push) Waiting to run
Psalm static code analysis / static-code-analysis-security (push) Waiting to run
Psalm static code analysis / static-code-analysis-ocp (push) Waiting to run
Psalm static code analysis / static-code-analysis-ncu (push) Waiting to run
Psalm static code analysis / static-code-analysis-strict (push) Waiting to run

refactor(cache-storage): Make Storage and StorageGlobal psalm strict
This commit is contained in:
Côme Chilliet 2026-03-10 22:01:43 +01:00 committed by GitHub
commit a119716a7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 188 additions and 290 deletions

View file

@ -14,19 +14,14 @@ use OCA\Files_External\Event\StorageUpdatedEvent;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\MountConfig;
use OCP\IGroup;
use Override;
/**
* Service class to manage global external storage
*/
class GlobalStoragesService extends StoragesService {
/**
* Triggers $signal for all applicable users of the given
* storage
*
* @param StorageConfig $storage storage data
* @param string $signal signal to trigger
*/
protected function triggerHooks(StorageConfig $storage, $signal) {
#[Override]
protected function triggerHooks(StorageConfig $storage, $signal): void {
// FIXME: Use as expression in empty once PHP 5.4 support is dropped
$applicableUsers = $storage->getApplicableUsers();
$applicableGroups = $storage->getApplicableGroups();
@ -55,15 +50,8 @@ class GlobalStoragesService extends StoragesService {
);
}
/**
* Triggers signal_create_mount or signal_delete_mount to
* accommodate for additions/deletions in applicableUsers
* and applicableGroups fields.
*
* @param StorageConfig $oldStorage old storage config
* @param StorageConfig $newStorage new storage config
*/
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
#[Override]
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage): void {
// if mount point changed, it's like a deletion + creation
if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
$this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage));
@ -139,16 +127,13 @@ class GlobalStoragesService extends StoragesService {
}
}
/**
* Get the visibility type for this controller, used in validation
*
* @return int BackendService::VISIBILITY_* constants
*/
public function getVisibilityType() {
#[Override]
public function getVisibilityType(): int {
return BackendService::VISIBILITY_ADMIN;
}
protected function isApplicable(StorageConfig $config) {
#[Override]
protected function isApplicable(StorageConfig $config): bool {
return true;
}
@ -157,16 +142,14 @@ class GlobalStoragesService extends StoragesService {
*
* @return StorageConfig[] map of storage id to storage config
*/
public function getStorageForAllUsers() {
public function getStorageForAllUsers(): array {
$mounts = $this->dbConfig->getAllMounts();
$configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
$configs = array_filter($configs, function ($config) {
return $config instanceof StorageConfig;
});
$keys = array_map(function (StorageConfig $config) {
return $config->getId();
}, $configs);
$keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs);
return array_combine($keys, $configs);
}

View file

@ -31,14 +31,10 @@ use Psr\Log\LoggerInterface;
/**
* Service class to manage external storage
*
* @psalm-import-type ExternalMountInfo from DBConfigService
*/
abstract class StoragesService {
/**
* @param BackendService $backendService
* @param DBConfigService $dbConfig
* @param IEventDispatcher $eventDispatcher
*/
public function __construct(
protected BackendService $backendService,
protected DBConfigService $dbConfig,
@ -47,24 +43,21 @@ abstract class StoragesService {
) {
}
protected function readDBConfig() {
/**
* @return list<ExternalMountInfo>
*/
protected function readDBConfig(): array {
return $this->dbConfig->getAdminMounts();
}
protected function getStorageConfigFromDBMount(array $mount) {
$applicableUsers = array_filter($mount['applicable'], function ($applicable) {
return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER;
});
$applicableUsers = array_map(function ($applicable) {
return $applicable['value'];
}, $applicableUsers);
protected function getStorageConfigFromDBMount(array $mount): ?StorageConfig {
$applicableUsers = array_filter($mount['applicable'], static fn (array $applicable): bool
=> $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER);
$applicableUsers = array_map(static fn (array $applicable) => $applicable['value'], $applicableUsers);
$applicableGroups = array_filter($mount['applicable'], function ($applicable) {
return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP;
});
$applicableGroups = array_map(function ($applicable) {
return $applicable['value'];
}, $applicableGroups);
$applicableGroups = array_filter($mount['applicable'], static fn (array $applicable): bool
=> $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP);
$applicableGroups = array_map(static fn (array $applicable) => $applicable['value'], $applicableGroups);
try {
$config = $this->createStorage(
@ -99,18 +92,14 @@ abstract class StoragesService {
/**
* Read the external storage config
*
* @return array map of storage id to storage config
* @return array<int, StorageConfig> map of storage id to storage config
*/
protected function readConfig() {
protected function readConfig(): array {
$mounts = $this->readDBConfig();
$configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
$configs = array_filter($configs, function ($config) {
return $config instanceof StorageConfig;
});
$configs = array_map($this->getStorageConfigFromDBMount(...), $mounts);
$configs = array_filter($configs);
$keys = array_map(function (StorageConfig $config) {
return $config->getId();
}, $configs);
$keys = array_map(static fn (StorageConfig $config): int => $config->getId(), $configs);
return array_combine($keys, $configs);
}
@ -139,18 +128,15 @@ abstract class StoragesService {
/**
* Check whether this storage service should provide access to a storage
*
* @param StorageConfig $config
* @return bool
*/
abstract protected function isApplicable(StorageConfig $config);
abstract protected function isApplicable(StorageConfig $config): bool;
/**
* Gets all storages, valid or not
*
* @return StorageConfig[] array of storage configs
*/
public function getAllStorages() {
public function getAllStorages(): array {
return $this->readConfig();
}
@ -159,18 +145,16 @@ abstract class StoragesService {
*
* @return StorageConfig[]
*/
public function getStorages() {
return array_filter($this->getAllStorages(), [$this, 'validateStorage']);
public function getStorages(): array {
return array_filter($this->getAllStorages(), $this->validateStorage(...));
}
/**
* Validate storage
* FIXME: De-duplicate with StoragesController::validate()
*
* @param StorageConfig $storage
* @return bool
*/
protected function validateStorage(StorageConfig $storage) {
protected function validateStorage(StorageConfig $storage): bool {
/** @var Backend */
$backend = $storage->getBackend();
/** @var AuthMechanism */
@ -180,25 +164,19 @@ abstract class StoragesService {
// not permitted to use backend
return false;
}
if (!$authMechanism->isVisibleFor($this->getVisibilityType())) {
// not permitted to use auth mechanism
return false;
}
return true;
// permitted to use auth mechanism
return $authMechanism->isVisibleFor($this->getVisibilityType());
}
/**
* Get the visibility type for this controller, used in validation
*
* @return int BackendService::VISIBILITY_* constants
* @return BackendService::VISIBILITY_*
*/
abstract public function getVisibilityType();
abstract public function getVisibilityType(): int;
/**
* @return integer
*/
protected function getType() {
protected function getType(): int {
return DBConfigService::MOUNT_TYPE_ADMIN;
}
@ -209,7 +187,7 @@ abstract class StoragesService {
*
* @return StorageConfig storage config, with added id
*/
public function addStorage(StorageConfig $newStorage) {
public function addStorage(StorageConfig $newStorage): StorageConfig {
$allStorages = $this->readConfig();
$configId = $this->dbConfig->addMount(
@ -275,7 +253,7 @@ abstract class StoragesService {
?array $applicableUsers = null,
?array $applicableGroups = null,
?int $priority = null,
) {
): StorageConfig {
$backend = $this->backendService->getBackend($backendIdentifier);
if (!$backend) {
$backend = new InvalidBackend($backendIdentifier);
@ -313,7 +291,7 @@ abstract class StoragesService {
* @param string $mountType hook mount type param
* @param array $applicableArray array of applicable users/groups for which to trigger the hook
*/
protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray): void {
protected function triggerApplicableHooks(string $signal, string $mountPoint, string $mountType, array $applicableArray): void {
$this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent(null));
foreach ($applicableArray as $applicable) {
Util::emitHook(
@ -335,7 +313,7 @@ abstract class StoragesService {
* @param StorageConfig $storage storage data
* @param string $signal signal to trigger
*/
abstract protected function triggerHooks(StorageConfig $storage, $signal);
abstract protected function triggerHooks(StorageConfig $storage, string $signal): void;
/**
* Triggers signal_create_mount or signal_delete_mount to
@ -345,7 +323,7 @@ abstract class StoragesService {
* @param StorageConfig $oldStorage old storage data
* @param StorageConfig $newStorage new storage data
*/
abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage);
abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage): void;
/**
* Update storage to the configuration
@ -355,7 +333,7 @@ abstract class StoragesService {
* @return StorageConfig storage config
* @throws NotFoundException if the given storage does not exist in the config
*/
public function updateStorage(StorageConfig $updatedStorage) {
public function updateStorage(StorageConfig $updatedStorage): StorageConfig {
$id = $updatedStorage->getId();
$existingMount = $this->dbConfig->getMountById($id);
@ -435,7 +413,7 @@ abstract class StoragesService {
*
* @throws NotFoundException if no storage was found with the given id
*/
public function removeStorage(int $id) {
public function removeStorage(int $id): void {
$existingMount = $this->dbConfig->getMountById($id);
if (!is_array($existingMount)) {
@ -454,29 +432,6 @@ abstract class StoragesService {
$this->updateOverwriteHomeFolders();
}
/**
* Construct the storage implementation
*
* @param StorageConfig $storageConfig
* @return int
*/
private function getStorageId(StorageConfig $storageConfig) {
try {
$class = $storageConfig->getBackend()->getStorageClass();
/** @var \OC\Files\Storage\Storage $storage */
$storage = new $class($storageConfig->getBackendOptions());
// auth mechanism should fire first
$storage = $storageConfig->getBackend()->wrapStorage($storage);
$storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
/** @var \OC\Files\Storage\Storage $storage */
return $storage->getStorageCache()->getNumericId();
} catch (\Exception $e) {
return -1;
}
}
public function updateOverwriteHomeFolders(): void {
$appIdsList = $this->appConfig->getValueArray(FilesApplication::APP_ID, ConfigLexicon::OVERWRITES_HOME_FOLDERS);

View file

@ -13,6 +13,7 @@ use OCP\IAppConfig;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use Override;
/**
* Service class to read global storages applicable to the user
@ -33,20 +34,8 @@ class UserGlobalStoragesService extends GlobalStoragesService {
$this->userSession = $userSession;
}
/**
* Replace config hash ID with real IDs, for migrating legacy storages
*
* @param StorageConfig[] $storages Storages with real IDs
* @param StorageConfig[] $storagesWithConfigHash Storages with config hash IDs
*/
protected function setRealStorageIds(array &$storages, array $storagesWithConfigHash) {
// as a read-only view, storage IDs don't need to be real
foreach ($storagesWithConfigHash as $storage) {
$storages[$storage->getId()] = $storage;
}
}
protected function readDBConfig() {
#[Override]
protected function readDBConfig(): array {
$userMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID());
$globalMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
$groups = $this->groupManager->getUserGroupIds($this->getUser());
@ -58,18 +47,18 @@ class UserGlobalStoragesService extends GlobalStoragesService {
return array_merge($userMounts, $groupMounts, $globalMounts);
}
#[Override]
public function addStorage(StorageConfig $newStorage): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
#[Override]
public function updateStorage(StorageConfig $updatedStorage): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
/**
* @param integer $id
*/
public function removeStorage($id): never {
#[Override]
public function removeStorage(int $id): never {
throw new \DomainException('UserGlobalStoragesService writing disallowed');
}
@ -79,7 +68,7 @@ class UserGlobalStoragesService extends GlobalStoragesService {
*
* @return StorageConfig[]
*/
public function getUniqueStorages() {
public function getUniqueStorages(): array {
$storages = $this->getStorages();
$storagesByMountpoint = [];
@ -116,20 +105,17 @@ class UserGlobalStoragesService extends GlobalStoragesService {
* @param StorageConfig $storage
* @return int
*/
protected function getPriorityType(StorageConfig $storage) {
protected function getPriorityType(StorageConfig $storage): int {
$applicableUsers = $storage->getApplicableUsers();
$applicableGroups = $storage->getApplicableGroups();
if ($applicableUsers && $applicableUsers[0] !== 'all') {
return 2;
}
if ($applicableGroups) {
return 1;
}
return 0;
return $applicableGroups ? 1 : 0;
}
protected function isApplicable(StorageConfig $config) {
protected function isApplicable(StorageConfig $config): bool {
$applicableUsers = $config->getApplicableUsers();
$applicableGroups = $config->getApplicableGroups();
@ -155,7 +141,7 @@ class UserGlobalStoragesService extends GlobalStoragesService {
* @param IUser|null $user user to get the storages for, if not set the currently logged in user will be used
* @return StorageConfig[] array of storage configs
*/
public function getAllStoragesForUser(?IUser $user = null) {
public function getAllStoragesForUser(?IUser $user = null): array {
if (is_null($user)) {
$user = $this->getUser();
}

View file

@ -12,10 +12,10 @@ use OCA\Files_External\Event\StorageCreatedEvent;
use OCA\Files_External\Event\StorageDeletedEvent;
use OCA\Files_External\Lib\StorageConfig;
use OCA\Files_External\MountConfig;
use OCA\Files_External\NotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig;
use OCP\IUserSession;
use Override;
/**
* Service class to manage user external storage
@ -38,7 +38,8 @@ class UserStoragesService extends StoragesService {
parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig);
}
protected function readDBConfig() {
#[Override]
protected function readDBConfig(): array {
return $this->dbConfig->getUserMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID());
}
@ -49,7 +50,7 @@ class UserStoragesService extends StoragesService {
* @param StorageConfig $storage storage data
* @param string $signal signal to trigger
*/
protected function triggerHooks(StorageConfig $storage, $signal) {
protected function triggerHooks(StorageConfig $storage, string $signal): void {
$user = $this->getUser()->getUID();
// trigger hook for the current user
@ -69,7 +70,7 @@ class UserStoragesService extends StoragesService {
* @param StorageConfig $oldStorage old storage data
* @param StorageConfig $newStorage new storage data
*/
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage): void {
// if mount point changed, it's like a deletion + creation
if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
$this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage));
@ -79,31 +80,19 @@ class UserStoragesService extends StoragesService {
}
}
protected function getType() {
#[Override]
protected function getType(): int {
return DBConfigService::MOUNT_TYPE_PERSONAL;
}
/**
* Add new storage to the configuration
*
* @param StorageConfig $newStorage storage attributes
*
* @return StorageConfig storage config, with added id
*/
public function addStorage(StorageConfig $newStorage) {
#[Override]
public function addStorage(StorageConfig $newStorage): StorageConfig {
$newStorage->setApplicableUsers([$this->getUser()->getUID()]);
return parent::addStorage($newStorage);
}
/**
* Update storage to the configuration
*
* @param StorageConfig $updatedStorage storage attributes
*
* @return StorageConfig storage config
* @throws NotFoundException if the given storage does not exist in the config
*/
public function updateStorage(StorageConfig $updatedStorage) {
#[Override]
public function updateStorage(StorageConfig $updatedStorage): StorageConfig {
// verify ownership through $this->isApplicable() and otherwise throws an exception
$this->getStorage($updatedStorage->getId());
@ -111,20 +100,18 @@ class UserStoragesService extends StoragesService {
return parent::updateStorage($updatedStorage);
}
/**
* Get the visibility type for this controller, used in validation
*
* @return int BackendService::VISIBILITY_* constants
*/
#[Override]
public function getVisibilityType(): int {
return BackendService::VISIBILITY_PERSONAL;
}
#[Override]
protected function isApplicable(StorageConfig $config): bool {
return ($config->getApplicableUsers() === [$this->getUser()->getUID()]) && $config->getType() === StorageConfig::MOUNT_TYPE_PERSONAL;
}
public function removeStorage($id) {
#[Override]
public function removeStorage(int $id): void {
// verify ownership through $this->isApplicable() and otherwise throws an exception
$this->getStorage($id);
parent::removeStorage($id);

View file

@ -3440,14 +3440,6 @@
<code><![CDATA[self::SCAN_RECURSIVE_INCOMPLETE]]></code>
</InvalidArgument>
</file>
<file src="lib/private/Files/Cache/Storage.php">
<InvalidNullableReturnType>
<code><![CDATA[array]]></code>
</InvalidNullableReturnType>
<NullableReturnStatement>
<code><![CDATA[self::getGlobalCache()->getStorageInfo($storageId)]]></code>
</NullableReturnStatement>
</file>
<file src="lib/private/Files/Config/MountProviderCollection.php">
<InvalidOperand>
<code><![CDATA[$user]]></code>

View file

@ -18,6 +18,8 @@ return (require __DIR__ . '/rector-shared.php')
$nextcloudDir . '/lib/private/Settings/AuthorizedGroupMapper.php',
$nextcloudDir . '/apps/settings/lib/Service/AuthorizedGroupService.php',
$nextcloudDir . '/lib/private/Files/Storage/Storage.php',
$nextcloudDir . '/lib/private/Files/Cache/Storage.php',
$nextcloudDir . '/lib/private/Files/Cache/StorageGlobal.php',
$nextcloudDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php',
$nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
$nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@ -7,6 +9,7 @@
*/
namespace OC\Files\Cache;
use OC\DB\Exceptions\DbalException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\Storage\IStorage;
use OCP\IDBConnection;
@ -25,55 +28,71 @@ use Psr\Log\LoggerInterface;
* @package OC\Files\Cache
*/
class Storage {
/** @var StorageGlobal|null */
private static $globalCache = null;
private $storageId;
private $numericId;
private static ?StorageGlobal $globalCache = null;
/**
* @return StorageGlobal
*/
public static function getGlobalCache() {
private string $storageId;
private int $numericId;
public static function getGlobalCache(): StorageGlobal {
if (is_null(self::$globalCache)) {
self::$globalCache = new StorageGlobal(Server::get(IDBConnection::class));
}
return self::$globalCache;
}
/**
* @param \OC\Files\Storage\Storage|string $storage
* @param bool $isAvailable
* @throws \RuntimeException
*/
public function __construct($storage, $isAvailable, IDBConnection $connection) {
if ($storage instanceof IStorage) {
$this->storageId = $storage->getId();
} else {
$this->storageId = $storage;
}
public function __construct(
IStorage|string $storage,
bool $isAvailable,
IDBConnection $connection,
) {
$this->storageId = $storage instanceof IStorage ? $storage->getId() : $storage;
$this->storageId = self::adjustStorageId($this->storageId);
if ($row = self::getStorageById($this->storageId)) {
$this->numericId = (int)$row['numeric_id'];
$this->numericId = $row['numeric_id'];
} else {
$available = $isAvailable ? 1 : 0;
if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) {
$this->numericId = $connection->lastInsertId('*PREFIX*storages');
} else {
if ($row = self::getStorageById($this->storageId)) {
$this->numericId = (int)$row['numeric_id'];
} else {
throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId);
try {
$qb = $connection->getQueryBuilder();
$qb->insert('storages')
->values([
'id' => $qb->createNamedParameter($this->storageId),
'available' => $qb->createNamedParameter($available),
])
->executeStatement();
$this->numericId = $qb->getLastInsertId();
} catch (DbalException $e) {
if ($e->getReason() === DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
$qb = $connection->getQueryBuilder();
$result = $qb->select('numeric_id')
->from('storages')
->where($qb->expr()->eq('id', $qb->createNamedParameter($this->storageId)))
->executeQuery();
/** @var false|string|int $row */
$row = $result->fetchOne();
if ($row === false) {
throw new \RuntimeException('Storage could neither be inserted nor be selected from the database: ' . $this->storageId, $e->getCode(), $e);
}
$this->numericId = (int)$row;
}
throw $e;
}
}
}
/**
* @param string $storageId
* @return array
* @return array{id: string, numeric_id: int, available: bool, last_checked: int}|null
*/
public static function getStorageById($storageId) {
public static function getStorageById(string $storageId): ?array {
return self::getGlobalCache()->getStorageInfo($storageId);
}
@ -83,26 +102,24 @@ class Storage {
* @return string unchanged $storageId if its length is less than 64 characters,
* else returns the md5 of $storageId
*/
public static function adjustStorageId($storageId) {
public static function adjustStorageId(string $storageId): string {
if (strlen($storageId) > 64) {
return md5($storageId);
}
return $storageId;
}
/**
* Get the numeric id for the storage
*
* @return int
*/
public function getNumericId() {
public function getNumericId(): int {
return $this->numericId;
}
/**
* Get the string id for the storage
*
* @param int $numericId
* @return string|null either the storage id string or null if the numeric id is not known
*/
public static function getStorageId(int $numericId): ?string {
@ -110,44 +127,27 @@ class Storage {
return $storage['id'] ?? null;
}
/**
* Get the numeric of the storage with the provided string id
*
* @param $storageId
* @return int|null either the numeric storage id or null if the storage id is not known
*/
public static function getNumericStorageId($storageId) {
$storageId = self::adjustStorageId($storageId);
if ($row = self::getStorageById($storageId)) {
return (int)$row['numeric_id'];
} else {
return null;
}
}
/**
* @return array{available: bool, last_checked: int}
*/
public function getAvailability() {
public function getAvailability(): array {
if ($row = self::getStorageById($this->storageId)) {
return [
'available' => (int)$row['available'] === 1,
'last_checked' => $row['last_checked']
];
} else {
return [
'available' => true,
'last_checked' => time(),
];
}
return [
'available' => true,
'last_checked' => time(),
];
}
/**
* @param bool $isAvailable
* @param int $delay amount of seconds to delay reconsidering that storage further
*/
public function setAvailability($isAvailable, int $delay = 0) {
public function setAvailability(bool $isAvailable, int $delay = 0): void {
$available = $isAvailable ? 1 : 0;
if (!$isAvailable) {
Server::get(LoggerInterface::class)->info('Storage with ' . $this->storageId . ' marked as unavailable', ['app' => 'lib']);
@ -162,43 +162,9 @@ class Storage {
}
/**
* Check if a string storage id is known
*
* @param string $storageId
* @return bool
* Remove the entry for the storage by the mount id.
*/
public static function exists($storageId) {
return !is_null(self::getNumericStorageId($storageId));
}
/**
* remove the entry for the storage
*
* @param string $storageId
*/
public static function remove($storageId) {
$storageId = self::adjustStorageId($storageId);
$numericId = self::getNumericStorageId($storageId);
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->delete('storages')
->where($query->expr()->eq('id', $query->createNamedParameter($storageId)));
$query->executeStatement();
if (!is_null($numericId)) {
$query = Server::get(IDBConnection::class)->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->eq('storage', $query->createNamedParameter($numericId)));
$query->executeStatement();
}
}
/**
* remove the entry for the storage by the mount id
*
* @param int $mountId
*/
public static function cleanByMountId(int $mountId) {
public static function cleanByMountId(int $mountId): void {
$db = Server::get(IDBConnection::class);
try {
@ -213,24 +179,24 @@ class Storage {
$query = $db->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
$query->runAcrossAllShards();
$query->executeStatement();
->where($query->expr()->in('storage', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)))
->runAcrossAllShards()
->executeStatement();
$query = $db->getQueryBuilder();
$query->delete('storages')
->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)));
$query->executeStatement();
->where($query->expr()->in('numeric_id', $query->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY)))
->executeStatement();
$query = $db->getQueryBuilder();
$query->delete('mounts')
->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
$query->executeStatement();
->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
->executeStatement();
$db->commit();
} catch (\Exception $e) {
} catch (\Exception $exception) {
$db->rollBack();
throw $e;
throw $exception;
}
}
}

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@ -21,35 +23,43 @@ use OCP\IDBConnection;
* @package OC\Files\Cache
*/
class StorageGlobal {
/** @var array<string, array> */
private $cache = [];
/** @var array<int, array> */
private $numericIdCache = [];
/** @var array<string, array{id: string, numeric_id: int, available: bool, last_checked: int}> */
private array $cache = [];
/** @var array<int, array{id: string, numeric_id: int, available: bool, last_checked: int}> */
private array $numericIdCache = [];
public function __construct(
private IDBConnection $connection,
private readonly IDBConnection $connection,
) {
}
/**
* @param string[] $storageIds
*/
public function loadForStorageIds(array $storageIds) {
public function loadForStorageIds(array $storageIds): void {
$builder = $this->connection->getQueryBuilder();
$query = $builder->select(['id', 'numeric_id', 'available', 'last_checked'])
->from('storages')
->where($builder->expr()->in('id', $builder->createNamedParameter(array_values($storageIds), IQueryBuilder::PARAM_STR_ARRAY)));
$result = $query->executeQuery();
while ($row = $result->fetch()) {
$this->cache[$row['id']] = $row;
while (($row = $result->fetch()) !== false) {
$normalizedRow = [
'id' => (string)$row['id'],
'numeric_id' => (int)$row['numeric_id'],
'available' => (bool)$row['available'],
'last_checked' => (int)$row['last_checked'],
];
$this->cache[$normalizedRow['id']] = $normalizedRow;
}
$result->closeCursor();
}
/**
* @param string $storageId
* @return array|null
* @return array{id: string, numeric_id: int, available: bool, last_checked: int}|null
*/
public function getStorageInfo(string $storageId): ?array {
if (!isset($this->cache[$storageId])) {
@ -62,17 +72,24 @@ class StorageGlobal {
$row = $result->fetch();
$result->closeCursor();
if ($row) {
$this->cache[$storageId] = $row;
$this->numericIdCache[(int)$row['numeric_id']] = $row;
if ($row !== false) {
$normalizedRow = [
'id' => (string)$row['id'],
'numeric_id' => (int)$row['numeric_id'],
'available' => (bool)$row['available'],
'last_checked' => (int)$row['last_checked'],
];
$this->cache[$storageId] = $normalizedRow;
$this->numericIdCache[$normalizedRow['numeric_id']] = $normalizedRow;
}
}
return $this->cache[$storageId] ?? null;
}
/**
* @param int $numericId
* @return array|null
* @return array{id: string, numeric_id: int, available: bool, last_checked: int}|null
*/
public function getStorageInfoByNumericId(int $numericId): ?array {
if (!isset($this->numericIdCache[$numericId])) {
@ -85,15 +102,23 @@ class StorageGlobal {
$row = $result->fetch();
$result->closeCursor();
if ($row) {
$this->numericIdCache[$numericId] = $row;
$this->cache[$row['id']] = $row;
if ($row !== false) {
$normalizedRow = [
'id' => (string)$row['id'],
'numeric_id' => (int)$row['numeric_id'],
'available' => (bool)$row['available'],
'last_checked' => (int)$row['last_checked'],
];
$this->numericIdCache[$numericId] = $normalizedRow;
$this->cache[$normalizedRow['id']] = $normalizedRow;
}
}
return $this->numericIdCache[$numericId] ?? null;
}
public function clearCache() {
public function clearCache(): void {
$this->cache = [];
}
}

View file

@ -24,6 +24,8 @@
<file name="lib/private/Settings/AuthorizedGroupMapper.php"/>
<file name="apps/settings/lib/Service/AuthorizedGroupService.php"/>
<file name="lib/private/Files/Storage/Storage.php"/>
<file name="lib/private/Files/Cache/Storage.php"/>
<file name="lib/private/Files/Cache/StorageGlobal.php"/>
<file name="lib/private/Files/Storage/Wrapper/Wrapper.php"/>
<file name="build/psalm/ITypedQueryBuilderTest.php"/>
<file name="lib/private/DB/QueryBuilder/TypedQueryBuilder.php"/>