mirror of https://github.com/nextcloud/server.git
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