Browse Source
Merge pull request #40499 from nextcloud/known-mtime-wrapper
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
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 224 additions and 18 deletions
-
28apps/files_external/lib/Config/ConfigAdapter.php
-
1lib/composer/composer/autoload_classmap.php
-
1lib/composer/composer/autoload_static.php
-
142lib/private/Files/Storage/Wrapper/KnownMtime.php
-
70tests/lib/Files/Storage/Wrapper/KnownMtimeTest.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,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']); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue