Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions lib/private/Files/Cache/Storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
use Psr\Log\LoggerInterface;

/**
* Handle the mapping between the string and numeric storage ids
* Represents the persisted storage-id mapping for a single storage.
*
* 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 storage has:
* - a backend-provided string identifier that reflects its configuration (e.g. 'smb://user@host/share')
* - a numeric identifier 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
* This class resolves or creates that mapping, exposes the numeric id for the
* current storage, and updates persisted storage state such as availability.
*/
class Storage {
private string $storageId;
Expand Down Expand Up @@ -153,6 +152,8 @@ public function setAvailability(bool $isAvailable, int $delay = 0): void {
->set('last_checked', $query->createNamedParameter(time() + $delay))
->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
$query->executeStatement();

self::getGlobalCache()->clearStorageInfo($this->storageId);
}

/**
Expand Down
32 changes: 25 additions & 7 deletions lib/private/Files/Cache/StorageGlobal.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@
use OCP\IDBConnection;

/**
* Handle the mapping between the string and numeric storage ids
* Provides a process-local cache of persisted storage-id mappings.
*
* 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
* Caches lookups in both directions:
* - string storage id to storage record
* - numeric storage id to storage record
*
* A mapping between the two storage ids is stored in the database and accessible through this class
*
* @package OC\Files\Cache
* This class reduces repeated database lookups for storage mapping metadata
* and provides invalidation helpers for callers that update those records.
*/
class StorageGlobal {
/** @var array<string, array{id: string, numeric_id: int, available: bool, last_checked: int}> */
Expand Down Expand Up @@ -118,7 +117,26 @@ public function getStorageInfoByNumericId(int $numericId): ?array {
return $this->numericIdCache[$numericId] ?? null;
}

public function clearStorageInfo(string $storageId): void {
$row = $this->cache[$storageId] ?? null;
unset($this->cache[$storageId]);

if ($row !== null) {
unset($this->numericIdCache[$row['numeric_id']]);
}
}

public function clearStorageInfoByNumericId(int $numericId): void {
$row = $this->numericIdCache[$numericId] ?? null;
unset($this->numericIdCache[$numericId]);

if ($row !== null) {
unset($this->cache[$row['id']]);
}
}

public function clearCache(): void {
$this->cache = [];
$this->numericIdCache = [];
}
}
96 changes: 96 additions & 0 deletions tests/lib/Files/Cache/StorageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace Test\Files\Cache;

use OC\Files\Cache\Storage;
use OCP\IDBConnection;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;

#[Group('DB')]
class StorageTest extends \Test\TestCase {
private IDBConnection $connection;
private ?string $createdStorageId = null;

protected function setUp(): void {
parent::setUp();

$this->connection = Server::get(IDBConnection::class);
Storage::getGlobalCache()->clearCache();
}

protected function tearDown(): void {
Storage::getGlobalCache()->clearCache();

if ($this->createdStorageId !== null) {
$query = $this->connection->getQueryBuilder();
$query->delete('storages')
->where(
$query->expr()->eq('id', $query->createNamedParameter($this->createdStorageId))
)
->executeStatement();

$this->createdStorageId = null;
}

parent::tearDown();
}

public function testSetAvailabilityInvalidatesGlobalCacheForStringAndNumericLookups(): void {
$storageId = 'test::availability-cache-' . uniqid('', true);
$this->createdStorageId = $storageId;

$storage = new Storage($storageId, true, $this->connection);
$numericId = $storage->getNumericId();

$globalCache = Storage::getGlobalCache();

// Prime both process-local caches with the initial persisted state.
$initialById = $globalCache->getStorageInfo($storageId);
$initialByNumericId = $globalCache->getStorageInfoByNumericId($numericId);

$this->assertIsArray($initialById);
$this->assertIsArray($initialByNumericId);

$this->assertSame($storageId, $initialById['id']);
$this->assertSame($numericId, $initialById['numeric_id']);
$this->assertTrue($initialById['available']);
$this->assertSame(0, $initialById['last_checked']);

$this->assertSame($storageId, $initialByNumericId['id']);
$this->assertSame($numericId, $initialByNumericId['numeric_id']);
$this->assertTrue($initialByNumericId['available']);
$this->assertSame(0, $initialByNumericId['last_checked']);

$before = time();
$delay = 600;

$storage->setAvailability(false, $delay);

// Both lookup directions must now observe fresh persisted state rather than
// stale data from the process-local cache.
$updatedById = $globalCache->getStorageInfo($storageId);
$updatedByNumericId = $globalCache->getStorageInfoByNumericId($numericId);

$this->assertIsArray($updatedById);
$this->assertIsArray($updatedByNumericId);

$this->assertSame($storageId, $updatedById['id']);
$this->assertSame($numericId, $updatedById['numeric_id']);
$this->assertFalse($updatedById['available']);
$this->assertGreaterThanOrEqual($before + $delay, $updatedById['last_checked']);
$this->assertLessThanOrEqual($before + $delay + 1, $updatedById['last_checked']);

$this->assertSame($storageId, $updatedByNumericId['id']);
$this->assertSame($numericId, $updatedByNumericId['numeric_id']);
$this->assertFalse($updatedByNumericId['available']);
$this->assertSame($updatedById['last_checked'], $updatedByNumericId['last_checked']);
}
}
Loading