From 5bf34979feb5b1f5b8bd4e2c0d002f8aaf8794e1 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 19 Sep 2023 12:39:55 +0200 Subject: [PATCH 1/2] add wrapper to ensure we don't get an mtime that is lower than we know it is Signed-off-by: Robin Appelman --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Files/Storage/Wrapper/KnownMtime.php | 142 ++++++++++++++++++ .../Files/Storage/Wrapper/KnownMtimeTest.php | 71 +++++++++ 4 files changed, 215 insertions(+) create mode 100644 lib/private/Files/Storage/Wrapper/KnownMtime.php create mode 100644 tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index fb063a8208821..e92ed2c55a1f4 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1346,6 +1346,7 @@ 'OC\\Files\\Storage\\Wrapper\\EncodingDirectoryWrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php', 'OC\\Files\\Storage\\Wrapper\\Encryption' => $baseDir . '/lib/private/Files/Storage/Wrapper/Encryption.php', 'OC\\Files\\Storage\\Wrapper\\Jail' => $baseDir . '/lib/private/Files/Storage/Wrapper/Jail.php', + 'OC\\Files\\Storage\\Wrapper\\KnownMtime' => $baseDir . '/lib/private/Files/Storage/Wrapper/KnownMtime.php', 'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => $baseDir . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php', 'OC\\Files\\Storage\\Wrapper\\Quota' => $baseDir . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 35b2318c4b166..ccd4adebedc16 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1379,6 +1379,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Files\\Storage\\Wrapper\\EncodingDirectoryWrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php', 'OC\\Files\\Storage\\Wrapper\\Encryption' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Encryption.php', 'OC\\Files\\Storage\\Wrapper\\Jail' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Jail.php', + 'OC\\Files\\Storage\\Wrapper\\KnownMtime' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/KnownMtime.php', 'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php', 'OC\\Files\\Storage\\Wrapper\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Wrapper.php', diff --git a/lib/private/Files/Storage/Wrapper/KnownMtime.php b/lib/private/Files/Storage/Wrapper/KnownMtime.php new file mode 100644 index 0000000000000..dde209c44ab45 --- /dev/null +++ b/lib/private/Files/Storage/Wrapper/KnownMtime.php @@ -0,0 +1,142 @@ +knowMtimes = new CappedMemoryCache(); + $this->clock = $arguments['clock']; + } + + public function file_put_contents($path, $data) { + $result = parent::file_put_contents($path, $data); + if ($result) { + $now = $this->clock->now()->getTimestamp(); + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function stat($path) { + $stat = parent::stat($path); + if ($stat) { + $this->applyKnownMtime($path, $stat); + } + return $stat; + } + + public function getMetaData($path) { + $stat = parent::getMetaData($path); + if ($stat) { + $this->applyKnownMtime($path, $stat); + } + return $stat; + } + + private function applyKnownMtime(string $path, array &$stat) { + if (isset($stat['mtime'])) { + $knownMtime = $this->knowMtimes->get($path) ?? 0; + $stat['mtime'] = max($stat['mtime'], $knownMtime); + } + } + + public function filemtime($path) { + $knownMtime = $this->knowMtimes->get($path) ?? 0; + return max(parent::filemtime($path), $knownMtime); + } + + public function mkdir($path) { + $result = parent::mkdir($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function rmdir($path) { + $result = parent::rmdir($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function unlink($path) { + $result = parent::unlink($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function rename($source, $target) { + $result = parent::rename($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); + $this->knowMtimes->set($source, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function copy($source, $target) { + $result = parent::copy($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function fopen($path, $mode) { + $result = parent::fopen($path, $mode); + if ($result && $mode === 'w') { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function touch($path, $mtime = null) { + $result = parent::touch($path, $mtime); + if ($result) { + $this->knowMtimes->set($path, $mtime ?? $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp()); + } + return $result; + } + + public function writeStream(string $path, $stream, int $size = null): int { + $result = parent::writeStream($path, $stream, $size); + if ($result) { + $this->knowMtimes->set($path, $this->clock->now()->getTimestamp()); + } + return $result; + } +} diff --git a/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php new file mode 100644 index 0000000000000..9694fc7bc9977 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php @@ -0,0 +1,71 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace lib\Files\Storage\Wrapper; + +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\KnownMtime; +use OCP\Constants; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Clock\ClockInterface; +use Test\Files\Storage\Storage; + +/** + * @group DB + */ +class KnownMtimeTest extends Storage { + /** @var Temporary */ + private $sourceStorage; + + /** @var ClockInterface|MockObject */ + private $clock; + private int $fakeTime = 0; + + protected function setUp(): void { + parent::setUp(); + $this->fakeTime = 0; + $this->sourceStorage = new Temporary([]); + $this->clock = $this->createMock(ClockInterface::class); + $this->clock->method('now')->willReturnCallback(function () { + if ($this->fakeTime) { + return new \DateTimeImmutable("@{$this->fakeTime}"); + } else { + return new \DateTimeImmutable(); + } + }); + $this->instance = $this->getWrappedStorage(); + } + + protected function tearDown(): void { + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + protected function getWrappedStorage() { + return new KnownMtime([ + 'storage' => $this->sourceStorage, + 'clock' => $this->clock, + ]); + } + + public function testNewerKnownMtime() { + $future = time() + 1000; + $this->fakeTime = $future; + + $this->instance->file_put_contents('foo.txt', 'bar'); + + // fuzzy match since the clock might have ticked + $this->assertLessThan(2, abs(time() - $this->sourceStorage->filemtime('foo.txt'))); + $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->stat('foo.txt')['mtime']); + $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->getMetaData('foo.txt')['mtime']); + + $this->assertEquals($future, $this->instance->filemtime('foo.txt')); + $this->assertEquals($future, $this->instance->stat('foo.txt')['mtime']); + $this->assertEquals($future, $this->instance->getMetaData('foo.txt')['mtime']); + } +} From ccf8843d9fcf7d57c3c90da2d46913ca88b0ba70 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 19 Sep 2023 12:42:53 +0200 Subject: [PATCH 2/2] apply known mtime wrapper for external storages Signed-off-by: Robin Appelman --- .../lib/Config/ConfigAdapter.php | 28 +++++++------------ .../Files/Storage/Wrapper/KnownMtimeTest.php | 1 - 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php index 7f7c5e3e2ebc3..1b83688418c74 100644 --- a/apps/files_external/lib/Config/ConfigAdapter.php +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -31,6 +31,7 @@ use OC\Files\Storage\FailedStorage; use OC\Files\Storage\Wrapper\Availability; +use OC\Files\Storage\Wrapper\KnownMtime; use OCA\Files_External\Lib\PersonalMount; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\UserGlobalStoragesService; @@ -40,29 +41,17 @@ use OCP\Files\Storage\IStorageFactory; use OCP\Files\StorageNotAvailableException; use OCP\IUser; +use Psr\Clock\ClockInterface; /** * Make the old files_external config work with the new public mount config api */ class ConfigAdapter implements IMountProvider { - - /** @var UserStoragesService */ - private $userStoragesService; - - /** @var UserGlobalStoragesService */ - private $userGlobalStoragesService; - - /** - * @param UserStoragesService $userStoragesService - * @param UserGlobalStoragesService $userGlobalStoragesService - */ public function __construct( - UserStoragesService $userStoragesService, - UserGlobalStoragesService $userGlobalStoragesService - ) { - $this->userStoragesService = $userStoragesService; - $this->userGlobalStoragesService = $userGlobalStoragesService; - } + private UserStoragesService $userStoragesService, + private UserGlobalStoragesService $userGlobalStoragesService, + private ClockInterface $clock, + ) {} /** * Process storage ready for mounting @@ -155,7 +144,10 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) { $this->userStoragesService, $storageConfig, $storageConfig->getId(), - $storage, + new KnownMtime([ + 'storage' => $storage, + 'clock' => $this->clock, + ]), '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(), null, $loader, diff --git a/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php index 9694fc7bc9977..0a1691f9e704a 100644 --- a/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php +++ b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php @@ -10,7 +10,6 @@ use OC\Files\Storage\Temporary; use OC\Files\Storage\Wrapper\KnownMtime; -use OCP\Constants; use PHPUnit\Framework\MockObject\MockObject; use Psr\Clock\ClockInterface; use Test\Files\Storage\Storage;