Merge pull request #40499 from nextcloud/known-mtime-wrapper
add wrapper for external storage to ensure we don't get an mtime that is lower than we know it ispull/40574/head
commit
b11ca34bbd
@ -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,70 @@ |
||||
<?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 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']); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue