Skip to content

Commit b11ca34

Browse files
authored
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 is
2 parents 472440b + ccf8843 commit b11ca34

File tree

5 files changed

+224
-18
lines changed

5 files changed

+224
-18
lines changed

apps/files_external/lib/Config/ConfigAdapter.php

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
use OC\Files\Storage\FailedStorage;
3333
use OC\Files\Storage\Wrapper\Availability;
34+
use OC\Files\Storage\Wrapper\KnownMtime;
3435
use OCA\Files_External\Lib\PersonalMount;
3536
use OCA\Files_External\Lib\StorageConfig;
3637
use OCA\Files_External\Service\UserGlobalStoragesService;
@@ -40,29 +41,17 @@
4041
use OCP\Files\Storage\IStorageFactory;
4142
use OCP\Files\StorageNotAvailableException;
4243
use OCP\IUser;
44+
use Psr\Clock\ClockInterface;
4345

4446
/**
4547
* Make the old files_external config work with the new public mount config api
4648
*/
4749
class ConfigAdapter implements IMountProvider {
48-
49-
/** @var UserStoragesService */
50-
private $userStoragesService;
51-
52-
/** @var UserGlobalStoragesService */
53-
private $userGlobalStoragesService;
54-
55-
/**
56-
* @param UserStoragesService $userStoragesService
57-
* @param UserGlobalStoragesService $userGlobalStoragesService
58-
*/
5950
public function __construct(
60-
UserStoragesService $userStoragesService,
61-
UserGlobalStoragesService $userGlobalStoragesService
62-
) {
63-
$this->userStoragesService = $userStoragesService;
64-
$this->userGlobalStoragesService = $userGlobalStoragesService;
65-
}
51+
private UserStoragesService $userStoragesService,
52+
private UserGlobalStoragesService $userGlobalStoragesService,
53+
private ClockInterface $clock,
54+
) {}
6655

6756
/**
6857
* Process storage ready for mounting
@@ -155,7 +144,10 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) {
155144
$this->userStoragesService,
156145
$storageConfig,
157146
$storageConfig->getId(),
158-
$storage,
147+
new KnownMtime([
148+
'storage' => $storage,
149+
'clock' => $this->clock,
150+
]),
159151
'/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(),
160152
null,
161153
$loader,

lib/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,7 @@
13521352
'OC\\Files\\Storage\\Wrapper\\EncodingDirectoryWrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php',
13531353
'OC\\Files\\Storage\\Wrapper\\Encryption' => $baseDir . '/lib/private/Files/Storage/Wrapper/Encryption.php',
13541354
'OC\\Files\\Storage\\Wrapper\\Jail' => $baseDir . '/lib/private/Files/Storage/Wrapper/Jail.php',
1355+
'OC\\Files\\Storage\\Wrapper\\KnownMtime' => $baseDir . '/lib/private/Files/Storage/Wrapper/KnownMtime.php',
13551356
'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => $baseDir . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php',
13561357
'OC\\Files\\Storage\\Wrapper\\Quota' => $baseDir . '/lib/private/Files/Storage/Wrapper/Quota.php',
13571358
'OC\\Files\\Storage\\Wrapper\\Wrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php',

lib/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
13851385
'OC\\Files\\Storage\\Wrapper\\EncodingDirectoryWrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/EncodingDirectoryWrapper.php',
13861386
'OC\\Files\\Storage\\Wrapper\\Encryption' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Encryption.php',
13871387
'OC\\Files\\Storage\\Wrapper\\Jail' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Jail.php',
1388+
'OC\\Files\\Storage\\Wrapper\\KnownMtime' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/KnownMtime.php',
13881389
'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php',
13891390
'OC\\Files\\Storage\\Wrapper\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Quota.php',
13901391
'OC\\Files\\Storage\\Wrapper\\Wrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Wrapper.php',
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
namespace OC\Files\Storage\Wrapper;
4+
5+
use OCP\Cache\CappedMemoryCache;
6+
use OCP\Files\Storage\IStorage;
7+
use Psr\Clock\ClockInterface;
8+
9+
/**
10+
* Wrapper that overwrites the mtime return by stat/getMetaData if the returned value
11+
* is lower than when we last modified the file.
12+
*
13+
* This is useful because some storage servers can return an outdated mtime right after writes
14+
*/
15+
class KnownMtime extends Wrapper {
16+
private CappedMemoryCache $knowMtimes;
17+
private ClockInterface $clock;
18+
19+
public function __construct($arguments) {
20+
parent::__construct($arguments);
21+
$this->knowMtimes = new CappedMemoryCache();
22+
$this->clock = $arguments['clock'];
23+
}
24+
25+
public function file_put_contents($path, $data) {
26+
$result = parent::file_put_contents($path, $data);
27+
if ($result) {
28+
$now = $this->clock->now()->getTimestamp();
29+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
30+
}
31+
return $result;
32+
}
33+
34+
public function stat($path) {
35+
$stat = parent::stat($path);
36+
if ($stat) {
37+
$this->applyKnownMtime($path, $stat);
38+
}
39+
return $stat;
40+
}
41+
42+
public function getMetaData($path) {
43+
$stat = parent::getMetaData($path);
44+
if ($stat) {
45+
$this->applyKnownMtime($path, $stat);
46+
}
47+
return $stat;
48+
}
49+
50+
private function applyKnownMtime(string $path, array &$stat) {
51+
if (isset($stat['mtime'])) {
52+
$knownMtime = $this->knowMtimes->get($path) ?? 0;
53+
$stat['mtime'] = max($stat['mtime'], $knownMtime);
54+
}
55+
}
56+
57+
public function filemtime($path) {
58+
$knownMtime = $this->knowMtimes->get($path) ?? 0;
59+
return max(parent::filemtime($path), $knownMtime);
60+
}
61+
62+
public function mkdir($path) {
63+
$result = parent::mkdir($path);
64+
if ($result) {
65+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
66+
}
67+
return $result;
68+
}
69+
70+
public function rmdir($path) {
71+
$result = parent::rmdir($path);
72+
if ($result) {
73+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
74+
}
75+
return $result;
76+
}
77+
78+
public function unlink($path) {
79+
$result = parent::unlink($path);
80+
if ($result) {
81+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
82+
}
83+
return $result;
84+
}
85+
86+
public function rename($source, $target) {
87+
$result = parent::rename($source, $target);
88+
if ($result) {
89+
$this->knowMtimes->set($target, $this->clock->now()->getTimestamp());
90+
$this->knowMtimes->set($source, $this->clock->now()->getTimestamp());
91+
}
92+
return $result;
93+
}
94+
95+
public function copy($source, $target) {
96+
$result = parent::copy($source, $target);
97+
if ($result) {
98+
$this->knowMtimes->set($target, $this->clock->now()->getTimestamp());
99+
}
100+
return $result;
101+
}
102+
103+
public function fopen($path, $mode) {
104+
$result = parent::fopen($path, $mode);
105+
if ($result && $mode === 'w') {
106+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
107+
}
108+
return $result;
109+
}
110+
111+
public function touch($path, $mtime = null) {
112+
$result = parent::touch($path, $mtime);
113+
if ($result) {
114+
$this->knowMtimes->set($path, $mtime ?? $this->clock->now()->getTimestamp());
115+
}
116+
return $result;
117+
}
118+
119+
public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
120+
$result = parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
121+
if ($result) {
122+
$this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp());
123+
}
124+
return $result;
125+
}
126+
127+
public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
128+
$result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
129+
if ($result) {
130+
$this->knowMtimes->set($targetInternalPath, $this->clock->now()->getTimestamp());
131+
}
132+
return $result;
133+
}
134+
135+
public function writeStream(string $path, $stream, int $size = null): int {
136+
$result = parent::writeStream($path, $stream, $size);
137+
if ($result) {
138+
$this->knowMtimes->set($path, $this->clock->now()->getTimestamp());
139+
}
140+
return $result;
141+
}
142+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
/**
3+
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
4+
* This file is licensed under the Affero General Public License version 3 or
5+
* later.
6+
* See the COPYING-README file.
7+
*/
8+
9+
namespace lib\Files\Storage\Wrapper;
10+
11+
use OC\Files\Storage\Temporary;
12+
use OC\Files\Storage\Wrapper\KnownMtime;
13+
use PHPUnit\Framework\MockObject\MockObject;
14+
use Psr\Clock\ClockInterface;
15+
use Test\Files\Storage\Storage;
16+
17+
/**
18+
* @group DB
19+
*/
20+
class KnownMtimeTest extends Storage {
21+
/** @var Temporary */
22+
private $sourceStorage;
23+
24+
/** @var ClockInterface|MockObject */
25+
private $clock;
26+
private int $fakeTime = 0;
27+
28+
protected function setUp(): void {
29+
parent::setUp();
30+
$this->fakeTime = 0;
31+
$this->sourceStorage = new Temporary([]);
32+
$this->clock = $this->createMock(ClockInterface::class);
33+
$this->clock->method('now')->willReturnCallback(function () {
34+
if ($this->fakeTime) {
35+
return new \DateTimeImmutable("@{$this->fakeTime}");
36+
} else {
37+
return new \DateTimeImmutable();
38+
}
39+
});
40+
$this->instance = $this->getWrappedStorage();
41+
}
42+
43+
protected function tearDown(): void {
44+
$this->sourceStorage->cleanUp();
45+
parent::tearDown();
46+
}
47+
48+
protected function getWrappedStorage() {
49+
return new KnownMtime([
50+
'storage' => $this->sourceStorage,
51+
'clock' => $this->clock,
52+
]);
53+
}
54+
55+
public function testNewerKnownMtime() {
56+
$future = time() + 1000;
57+
$this->fakeTime = $future;
58+
59+
$this->instance->file_put_contents('foo.txt', 'bar');
60+
61+
// fuzzy match since the clock might have ticked
62+
$this->assertLessThan(2, abs(time() - $this->sourceStorage->filemtime('foo.txt')));
63+
$this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->stat('foo.txt')['mtime']);
64+
$this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->getMetaData('foo.txt')['mtime']);
65+
66+
$this->assertEquals($future, $this->instance->filemtime('foo.txt'));
67+
$this->assertEquals($future, $this->instance->stat('foo.txt')['mtime']);
68+
$this->assertEquals($future, $this->instance->getMetaData('foo.txt')['mtime']);
69+
}
70+
}

0 commit comments

Comments
 (0)