77namespace OCA \Files_External \Lib \Storage ;
88
99use Icewind \Streams \CountWrapper ;
10+ use Icewind \Streams \CallbackWrapper ;
1011use Icewind \Streams \IteratorDirectory ;
1112use Icewind \Streams \RetryWrapper ;
1213use OC \Files \Storage \Common ;
1314use OC \Files \View ;
1415use OCP \Constants ;
1516use OCP \Files \FileInfo ;
1617use OCP \Files \IMimeTypeDetector ;
18+ use OCP \Cache \CappedMemoryCache ;
1719use phpseclib \Net \SFTP \Stream ;
1820
1921/**
@@ -32,6 +34,8 @@ class SFTP extends Common {
3234 * @var \phpseclib\Net\SFTP
3335 */
3436 protected $ client ;
37+ private CappedMemoryCache $ knownMTimes ;
38+
3539 private IMimeTypeDetector $ mimeTypeDetector ;
3640
3741 public const COPY_CHUNK_SIZE = 8 * 1024 * 1024 ;
@@ -87,6 +91,9 @@ public function __construct(array $parameters) {
8791
8892 $ this ->root = '/ ' . ltrim ($ this ->root , '/ ' );
8993 $ this ->root = rtrim ($ this ->root , '/ ' ) . '/ ' ;
94+
95+ $ this ->knownMTimes = new CappedMemoryCache ();
96+
9097 $ this ->mimeTypeDetector = \OC ::$ server ->get (IMimeTypeDetector::class);
9198 }
9299
@@ -297,6 +304,7 @@ public function unlink(string $path): bool {
297304 }
298305
299306 public function fopen (string $ path , string $ mode ) {
307+ $ path = $ this ->cleanPath ($ path );
300308 try {
301309 $ absPath = $ this ->absPath ($ path );
302310 $ connection = $ this ->getConnection ();
@@ -317,7 +325,13 @@ public function fopen(string $path, string $mode) {
317325 // the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache.
318326 $ connection ->_remove_from_stat_cache ($ absPath );
319327 $ context = stream_context_create (['sftp ' => ['session ' => $ connection ]]);
320- return fopen ('sftpwrite:// ' . trim ($ absPath , '/ ' ), 'w ' , false , $ context );
328+ $ fh = fopen ('sftpwrite:// ' . trim ($ absPath , '/ ' ), 'w ' , false , $ context );
329+ if ($ fh ) {
330+ $ fh = CallbackWrapper::wrap ($ fh , null , null , function () use ($ path ) {
331+ $ this ->knownMTimes ->set ($ path , time ());
332+ });
333+ }
334+ return $ fh ;
321335 case 'a ' :
322336 case 'ab ' :
323337 case 'r+ ' :
@@ -343,14 +357,13 @@ public function touch(string $path, ?int $mtime = null): bool {
343357 return false ;
344358 }
345359 if (!$ this ->file_exists ($ path )) {
346- $ this ->getConnection ()->put ($ this ->absPath ($ path ), '' );
360+ return $ this ->getConnection ()->put ($ this ->absPath ($ path ), '' );
347361 } else {
348362 return false ;
349363 }
350364 } catch (\Exception $ e ) {
351365 return false ;
352366 }
353- return true ;
354367 }
355368
356369 /**
@@ -379,11 +392,17 @@ public function rename(string $source, string $target): bool {
379392 */
380393 public function stat (string $ path ): array |false {
381394 try {
395+ $ path = $ this ->cleanPath ($ path );
382396 $ stat = $ this ->getConnection ()->stat ($ this ->absPath ($ path ));
383397
384398 $ mtime = isset ($ stat ['mtime ' ]) ? (int )$ stat ['mtime ' ] : -1 ;
385399 $ size = isset ($ stat ['size ' ]) ? (int )$ stat ['size ' ] : 0 ;
386400
401+ // the mtime can't be less than when we last touched it
402+ if ($ knownMTime = $ this ->knownMTimes ->get ($ path )) {
403+ $ mtime = max ($ mtime , $ knownMTime );
404+ }
405+
387406 return [
388407 'mtime ' => $ mtime ,
389408 'size ' => $ size ,
0 commit comments