diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php index 7f7c5e3e2ebc3..2adc9234b6b17 100644 --- a/apps/files_external/lib/Config/ConfigAdapter.php +++ b/apps/files_external/lib/Config/ConfigAdapter.php @@ -31,10 +31,12 @@ 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; use OCA\Files_External\Service\UserStoragesService; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\Config\IMountProvider; use OCP\Files\Storage; use OCP\Files\Storage\IStorageFactory; @@ -45,24 +47,11 @@ * 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 ITimeFactory $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/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0f871f4dda841..b7dc7ee75c718 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1259,6 +1259,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 91b2cfb2c2026..de6c721944e70 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1292,6 +1292,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..70e527dfe231b --- /dev/null +++ b/lib/private/Files/Storage/Wrapper/KnownMtime.php @@ -0,0 +1,141 @@ +knowMtimes = new CappedMemoryCache(); + $this->clock = $arguments['clock']; + } + + public function file_put_contents($path, $data) { + $result = parent::file_put_contents($path, $data); + if ($result) { + $this->knowMtimes->set($path, $this->clock->getTime()); + } + 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->getTime()); + } + return $result; + } + + public function rmdir($path) { + $result = parent::rmdir($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->getTime()); + } + return $result; + } + + public function unlink($path) { + $result = parent::unlink($path); + if ($result) { + $this->knowMtimes->set($path, $this->clock->getTime()); + } + return $result; + } + + public function rename($source, $target) { + $result = parent::rename($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->getTime()); + $this->knowMtimes->set($source, $this->clock->getTime()); + } + return $result; + } + + public function copy($source, $target) { + $result = parent::copy($source, $target); + if ($result) { + $this->knowMtimes->set($target, $this->clock->getTime()); + } + return $result; + } + + public function fopen($path, $mode) { + $result = parent::fopen($path, $mode); + if ($result && $mode === 'w') { + $this->knowMtimes->set($path, $this->clock->getTime()); + } + return $result; + } + + public function touch($path, $mtime = null) { + $result = parent::touch($path, $mtime); + if ($result) { + $this->knowMtimes->set($path, $mtime ?? $this->clock->getTime()); + } + return $result; + } + + public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->getTime()); + } + return $result; + } + + public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) { + $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); + if ($result) { + $this->knowMtimes->set($targetInternalPath, $this->clock->getTime()); + } + 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->getTime()); + } + 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..4f105c3393f32 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php @@ -0,0 +1,66 @@ + + * 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\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\Files\Storage\Storage; + +/** + * @group DB + */ +class KnownMtimeTest extends Storage { + /** @var Temporary */ + private $sourceStorage; + + /** @var ITimeFactory|MockObject */ + private $clock; + private int $fakeTime = 0; + + protected function setUp(): void { + parent::setUp(); + $this->fakeTime = 0; + $this->sourceStorage = new Temporary([]); + $this->clock = $this->createMock(ITimeFactory::class); + $this->clock->method('getTime')->willReturnCallback(function () { + return $this->fakeTime; + }); + $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']); + } +}