Files
Maksim Sukharev b63e4dbe62 fix(AmazonS3#getMetaData): preserve storage_mtime after parent clobbers it
Common::getMetaData (Common.php:667) always sets storage_mtime = mtime before
returning. For S3 virtual directories this is wrong: mtime can be bumped by
child propagation while storage_mtime should remain the actual last S3 change.

When the scanner later calls getMetaData() it compares data['storage_mtime']
against cacheData['storage_mtime']. If they differ it writes the value back,
triggering View::getCacheEntry to fire propagateChange on every read even when
nothing on S3 changed.

Override getMetaData() to restore storage_mtime from the live cache entry (or,
for non-root directories not yet in the cache, from the S3 directory marker
LastModified header) after the parent call.

Assisted-by: ClaudeCode:claude-sonnet-4-6
Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
2026-07-09 19:01:33 +02:00

95 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Files_External\Tests\Storage;
use OC\Files\Cache\Scanner;
use OCA\Files_External\Lib\Storage\AmazonS3;
/**
* Class Amazons3Test
*
*
* @package OCA\Files_External\Tests\Storage
*/
#[\PHPUnit\Framework\Attributes\Group(name: 'DB')]
#[\PHPUnit\Framework\Attributes\Group('S3')]
class Amazons3Test extends \Test\Files\Storage\Storage {
use ConfigurableStorageTrait;
/** @var AmazonS3 */
protected $instance;
protected function setUp(): void {
parent::setUp();
$this->loadConfig(__DIR__ . '/../config.amazons3.php');
$this->instance = new AmazonS3($this->config);
}
protected function tearDown(): void {
if ($this->instance) {
$this->instance->rmdir('');
}
parent::tearDown();
}
/**
* Regression test for the '.' vs '' root path mismatch in getDirectoryMetaData.
*
* normalizePath('') returns '.' for S3 object keys, but the filecache stores the
* storage root under the key ''. Before the fix, getCache()->get('.') returned false,
* causing getDirectoryMetaData to return a fabricated time() on every call, which
* made getCacheEntry always see a changed storage_mtime and fire propagateChange.
*/
public function testStatRootPreservesStorageMtimeFromCache(): void {
$this->instance->getScanner()->scan('', Scanner::SCAN_SHALLOW);
$cachedRoot = $this->instance->getCache()->get('');
$this->assertNotFalse($cachedRoot, 'Root entry must exist in cache after scan');
$cachedStorageMtime = $cachedRoot['storage_mtime'];
$stat = $this->instance->stat('');
$this->assertNotFalse($stat, 'stat(\'\') must return data');
$this->assertEquals(
$cachedStorageMtime,
$stat['storage_mtime'],
'stat(\'\') must return storage_mtime from the cache entry, not a fabricated time()'
);
}
/**
* Regression test: Common::getMetaData sets storage_mtime = mtime, but for S3 virtual
* directories mtime may have been bumped by propagation while storage_mtime should stay
* stable. The override restores storage_mtime from the cache entry so the scanner does
* not see a spurious mismatch and re-write the cache on every scan.
*/
public function testGetMetaDataDirectoryPreservesStorageMtimeSeparateFromMtime(): void {
$this->instance->getScanner()->scan('', Scanner::SCAN_SHALLOW);
$cachedRoot = $this->instance->getCache()->get('');
$this->assertNotFalse($cachedRoot, 'Root entry must exist in cache after scan');
// Simulate propagation bumping mtime without touching storage_mtime
$originalStorageMtime = $cachedRoot['storage_mtime'];
$this->instance->getCache()->update($cachedRoot->getId(), [
'mtime' => $originalStorageMtime + 9999,
]);
$meta = $this->instance->getMetaData('');
$this->assertNotNull($meta, 'getMetaData(\'\') must return data');
$this->assertEquals(
$originalStorageMtime,
$meta['storage_mtime'],
'getMetaData must return storage_mtime from cache, not the propagated mtime'
);
}
}