Signed-off-by: Robin Appelman <robin@icewind.nl>tags/v28.0.0beta1
@@ -1346,6 +1346,7 @@ return array( | |||
'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', |
@@ -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', |
@@ -0,0 +1,142 @@ | |||
<?php | |||
namespace OC\Files\Storage\Wrapper; | |||
use OCP\Cache\CappedMemoryCache; | |||
use OCP\Files\Storage\IStorage; | |||
use Psr\Clock\ClockInterface; | |||
/** | |||
* Wrapper that overwrites the mtime return by stat/getMetaData if the returned value | |||
* is lower than when we last modified the file. | |||
* | |||
* This is useful because some storage servers can return an outdated mtime right after writes | |||
*/ | |||
class KnownMtime extends Wrapper { | |||
private CappedMemoryCache $knowMtimes; | |||
private ClockInterface $clock; | |||
public function __construct($arguments) { | |||
parent::__construct($arguments); | |||
$this->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; | |||
} | |||
} |
@@ -0,0 +1,71 @@ | |||
<?php | |||
/** | |||
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com> | |||
* 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']); | |||
} | |||
} |