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