mirror of
https://github.com/nextcloud/server.git
synced 2026-10-13 00:51:06 +02:00
cleanByMountId() deleted the filecache rows of an unmounted storage but left their filecache_extended and file metadata rows behind. It now goes through Storage::removeFileCacheEntries(), the same path clear() uses. files:cleanup also removes filecache_extended, files_metadata and files_metadata_index rows whose file is no longer in the filecache, so rows orphaned before this fix are cleaned up too. Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
231 lines
6.9 KiB
PHP
231 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
namespace OC\Files\Cache;
|
|
|
|
use OC\DB\Exceptions\DbalException;
|
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
|
use OCP\Files\Storage\IStorage;
|
|
use OCP\FilesMetadata\IFilesMetadataManager;
|
|
use OCP\IDBConnection;
|
|
use OCP\Server;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Handle the mapping between the string and numeric storage ids
|
|
*
|
|
* Each storage has 2 different ids
|
|
* a string id which is generated by the storage backend and reflects the configuration of the storage (e.g. 'smb://user@host/share')
|
|
* and a numeric storage id which is referenced in the file cache
|
|
*
|
|
* A mapping between the two storage ids is stored in the database and accessible through this class
|
|
*
|
|
* @package OC\Files\Cache
|
|
* @psalm-api
|
|
*/
|
|
class Storage {
|
|
private string $storageId;
|
|
|
|
private int $numericId;
|
|
|
|
public static function getGlobalCache(): StorageGlobal {
|
|
return Server::get(StorageGlobal::class);
|
|
}
|
|
|
|
/**
|
|
* @throws \RuntimeException
|
|
*/
|
|
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 = $row['numeric_id'];
|
|
} else {
|
|
$available = $isAvailable ? 1 : 0;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{id: string, numeric_id: int, available: bool, last_checked: int}|null
|
|
*/
|
|
public static function getStorageById(string $storageId): ?array {
|
|
return self::getGlobalCache()->getStorageInfo($storageId);
|
|
}
|
|
|
|
/**
|
|
* Adjusts the storage id to use md5 if too long
|
|
* @param string $storageId storage id
|
|
* @return string unchanged $storageId if its length is less than 64 characters,
|
|
* else returns the md5 of $storageId
|
|
*/
|
|
public static function adjustStorageId(string $storageId): string {
|
|
if (strlen($storageId) > 64) {
|
|
return md5($storageId);
|
|
}
|
|
|
|
return $storageId;
|
|
}
|
|
|
|
/**
|
|
* Get the numeric id for the storage
|
|
*/
|
|
public function getNumericId(): int {
|
|
return $this->numericId;
|
|
}
|
|
|
|
/**
|
|
* Get the string id for the storage
|
|
*
|
|
* @return string|null either the storage id string or null if the numeric id is not known
|
|
*/
|
|
public static function getStorageId(int $numericId): ?string {
|
|
$storage = self::getGlobalCache()->getStorageInfoByNumericId($numericId);
|
|
return $storage['id'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return array{available: bool, last_checked: int}
|
|
*/
|
|
public function getAvailability(): array {
|
|
if ($row = self::getStorageById($this->storageId)) {
|
|
return [
|
|
'available' => (int)$row['available'] === 1,
|
|
'last_checked' => $row['last_checked']
|
|
];
|
|
}
|
|
|
|
return [
|
|
'available' => true,
|
|
'last_checked' => time(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param int $delay amount of seconds to delay reconsidering that storage further
|
|
*/
|
|
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']);
|
|
}
|
|
|
|
$query = Server::get(IDBConnection::class)->getQueryBuilder();
|
|
$query->update('storages')
|
|
->set('available', $query->createNamedParameter($available))
|
|
->set('last_checked', $query->createNamedParameter(time() + $delay))
|
|
->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
|
|
$query->executeStatement();
|
|
}
|
|
|
|
/**
|
|
* Remove the entry for the storage by the mount id.
|
|
*/
|
|
public static function cleanByMountId(int $mountId): void {
|
|
$db = Server::get(IDBConnection::class);
|
|
|
|
try {
|
|
$db->beginTransaction();
|
|
|
|
$query = $db->getQueryBuilder();
|
|
$query->select('storage_id')
|
|
->from('mounts')
|
|
->where($query->expr()->eq('mount_id', $query->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
|
|
$storageIds = array_unique(array_map(intval(...), $query->executeQuery()->fetchFirstColumn()));
|
|
|
|
foreach ($storageIds as $storageId) {
|
|
self::removeFileCacheEntries($storageId);
|
|
}
|
|
|
|
$query = $db->getQueryBuilder();
|
|
$query->delete('storages')
|
|
->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)))
|
|
->executeStatement();
|
|
|
|
$db->commit();
|
|
} catch (\Exception $exception) {
|
|
$db->rollBack();
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the filecache entries of a storage together with their filecache_extended and metadata rows
|
|
*/
|
|
public static function removeFileCacheEntries(int $numericStorageId): void {
|
|
$db = Server::get(IDBConnection::class);
|
|
$metadataManager = Server::get(IFilesMetadataManager::class);
|
|
|
|
while (true) {
|
|
$query = $db->getQueryBuilder();
|
|
$query->select('fileid')
|
|
->from('filecache')
|
|
->where($query->expr()->eq('storage', $query->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)))
|
|
->setMaxResults(IQueryBuilder::MAX_IN_PARAMETERS);
|
|
$fileIds = array_map(intval(...), $query->executeQuery()->fetchFirstColumn());
|
|
if ($fileIds === []) {
|
|
return;
|
|
}
|
|
|
|
$query = $db->getQueryBuilder();
|
|
$query->delete('filecache_extended')
|
|
->where($query->expr()->in('fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
|
->hintShardKey('storage', $numericStorageId)
|
|
->executeStatement();
|
|
|
|
$metadataManager->deleteMetadataForFiles($numericStorageId, $fileIds);
|
|
|
|
$query = $db->getQueryBuilder();
|
|
$query->delete('filecache')
|
|
->where($query->expr()->eq('storage', $query->createNamedParameter($numericStorageId, IQueryBuilder::PARAM_INT)))
|
|
->andWhere($query->expr()->in('fileid', $query->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
|
->executeStatement();
|
|
}
|
|
}
|
|
}
|