3636 */
3737namespace OCA \Files_External \Lib \Storage ;
3838
39+ use Icewind \Streams \CountWrapper ;
3940use Icewind \Streams \IteratorDirectory ;
4041use Icewind \Streams \RetryWrapper ;
42+ use OC \Files \Filesystem ;
43+ use OC \Files \Storage \Common ;
44+ use OCP \Constants ;
45+ use OCP \Files \FileInfo ;
46+ use OCP \Files \IMimeTypeDetector ;
4147use phpseclib \Net \SFTP \Stream ;
4248
4349/**
4450 * Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
4551 * provide access to SFTP servers.
4652 */
47- class SFTP extends \ OC \ Files \ Storage \ Common {
53+ class SFTP extends Common {
4854 private $ host ;
4955 private $ user ;
5056 private $ root ;
@@ -56,6 +62,9 @@ class SFTP extends \OC\Files\Storage\Common {
5662 * @var \phpseclib\Net\SFTP
5763 */
5864 protected $ client ;
65+ private IMimeTypeDetector $ mimeTypeDetector ;
66+
67+ const COPY_CHUNK_SIZE = 8 * 1024 * 1024 ;
5968
6069 /**
6170 * @param string $host protocol://server:port
@@ -111,6 +120,7 @@ public function __construct($params) {
111120
112121 $ this ->root = '/ ' . ltrim ($ this ->root , '/ ' );
113122 $ this ->root = rtrim ($ this ->root , '/ ' ) . '/ ' ;
123+ $ this ->mimeTypeDetector = \OC ::$ server ->get (IMimeTypeDetector::class);
114124 }
115125
116126 /**
@@ -370,20 +380,24 @@ public function unlink($path) {
370380 public function fopen ($ path , $ mode ) {
371381 try {
372382 $ absPath = $ this ->absPath ($ path );
383+ $ connection = $ this ->getConnection ();
373384 switch ($ mode ) {
374385 case 'r ' :
375386 case 'rb ' :
376- if (!$ this ->file_exists ($ path )) {
387+ $ stat = $ this ->stat ($ path );
388+ if (!$ stat ) {
377389 return false ;
378390 }
379391 SFTPReadStream::register ();
380- $ context = stream_context_create (['sftp ' => ['session ' => $ this -> getConnection () ]]);
392+ $ context = stream_context_create (['sftp ' => ['session ' => $ connection , ' size ' => $ stat [ ' size ' ] ]]);
381393 $ handle = fopen ('sftpread:// ' . trim ($ absPath , '/ ' ), 'r ' , false , $ context );
382394 return RetryWrapper::wrap ($ handle );
383395 case 'w ' :
384396 case 'wb ' :
385397 SFTPWriteStream::register ();
386- $ context = stream_context_create (['sftp ' => ['session ' => $ this ->getConnection ()]]);
398+ // the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache.
399+ $ connection ->_remove_from_stat_cache ($ absPath );
400+ $ context = stream_context_create (['sftp ' => ['session ' => $ connection ]]);
387401 return fopen ('sftpwrite:// ' . trim ($ absPath , '/ ' ), 'w ' , false , $ context );
388402 case 'a ' :
389403 case 'ab ' :
@@ -395,7 +409,7 @@ public function fopen($path, $mode) {
395409 case 'x+ ' :
396410 case 'c ' :
397411 case 'c+ ' :
398- $ context = stream_context_create (['sftp ' => ['session ' => $ this -> getConnection () ]]);
412+ $ context = stream_context_create (['sftp ' => ['session ' => $ connection ]]);
399413 $ handle = fopen ($ this ->constructUrl ($ path ), $ mode , false , $ context );
400414 return RetryWrapper::wrap ($ handle );
401415 }
@@ -450,14 +464,14 @@ public function rename($source, $target) {
450464 }
451465
452466 /**
453- * {@inheritdoc}
467+ * @return array{mtime: int, size: int, ctime: int}|false
454468 */
455469 public function stat ($ path ) {
456470 try {
457471 $ stat = $ this ->getConnection ()->stat ($ this ->absPath ($ path ));
458472
459- $ mtime = $ stat ? $ stat ['mtime ' ] : -1 ;
460- $ size = $ stat ? $ stat ['size ' ] : 0 ;
473+ $ mtime = $ stat ? ( int ) $ stat ['mtime ' ] : -1 ;
474+ $ size = $ stat ? ( int ) $ stat ['size ' ] : 0 ;
461475
462476 return ['mtime ' => $ mtime , 'size ' => $ size , 'ctime ' => -1 ];
463477 } catch (\Exception $ e ) {
@@ -476,4 +490,99 @@ public function constructUrl($path) {
476490 $ url = 'sftp:// ' . urlencode ($ this ->user ) . '@ ' . $ this ->host . ': ' . $ this ->port . $ this ->root . $ path ;
477491 return $ url ;
478492 }
493+
494+ public function file_put_contents ($ path , $ data ) {
495+ /** @psalm-suppress InternalMethod */
496+ $ result = $ this ->getConnection ()->put ($ this ->absPath ($ path ), $ data );
497+ if ($ result ) {
498+ return strlen ($ data );
499+ } else {
500+ return false ;
501+ }
502+ }
503+
504+ public function writeStream (string $ path , $ stream , int $ size = null ): int {
505+ if ($ size === null ) {
506+ $ stream = CountWrapper::wrap ($ stream , function (int $ writtenSize ) use (&$ size ) {
507+ $ size = $ writtenSize ;
508+ });
509+ if (!$ stream ) {
510+ throw new \Exception ("Failed to wrap stream " );
511+ }
512+ }
513+ /** @psalm-suppress InternalMethod */
514+ $ result = $ this ->getConnection ()->put ($ this ->absPath ($ path ), $ stream );
515+ fclose ($ stream );
516+ if ($ result ) {
517+ return $ size ;
518+ } else {
519+ throw new \Exception ("Failed to write steam to sftp storage " );
520+ }
521+ }
522+
523+ public function copy ($ source , $ target ) {
524+ if ($ this ->is_dir ($ source ) || $ this ->is_dir ($ target )) {
525+ return parent ::copy ($ source , $ target );
526+ } else {
527+ $ absSource = $ this ->absPath ($ source );
528+ $ absTarget = $ this ->absPath ($ target );
529+
530+ $ connection = $ this ->getConnection ();
531+ $ size = $ connection ->size ($ absSource );
532+ if ($ size === false ) {
533+ return false ;
534+ }
535+ for ($ i = 0 ; $ i < $ size ; $ i += self ::COPY_CHUNK_SIZE ) {
536+ /** @psalm-suppress InvalidArgument */
537+ $ chunk = $ connection ->get ($ absSource , false , $ i , self ::COPY_CHUNK_SIZE );
538+ if ($ chunk === false ) {
539+ return false ;
540+ }
541+ /** @psalm-suppress InternalMethod */
542+ if (!$ connection ->put ($ absTarget , $ chunk , \phpseclib \Net \SFTP ::SOURCE_STRING , $ i )) {
543+ return false ;
544+ }
545+ }
546+ return true ;
547+ }
548+ }
549+
550+ public function getPermissions ($ path ) {
551+ $ stat = $ this ->getConnection ()->stat ($ this ->absPath ($ path ));
552+ if (!$ stat ) {
553+ return 0 ;
554+ }
555+ if ($ stat ['type ' ] === NET_SFTP_TYPE_DIRECTORY ) {
556+ return Constants::PERMISSION_ALL ;
557+ } else {
558+ return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE ;
559+ }
560+ }
561+
562+ public function getMetaData ($ path ) {
563+ $ stat = $ this ->getConnection ()->stat ($ this ->absPath ($ path ));
564+ if (!$ stat ) {
565+ return null ;
566+ }
567+
568+ if ($ stat ['type ' ] === NET_SFTP_TYPE_DIRECTORY ) {
569+ $ stat ['permissions ' ] = Constants::PERMISSION_ALL ;
570+ } else {
571+ $ stat ['permissions ' ] = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE ;
572+ }
573+
574+ if ($ stat ['type ' ] === NET_SFTP_TYPE_DIRECTORY ) {
575+ $ stat ['size ' ] = -1 ;
576+ $ stat ['mimetype ' ] = FileInfo::MIMETYPE_FOLDER ;
577+ } else {
578+ $ stat ['mimetype ' ] = $ this ->mimeTypeDetector ->detectPath ($ path );
579+ }
580+
581+ $ stat ['etag ' ] = $ this ->getETag ($ path );
582+ $ stat ['storage_mtime ' ] = $ stat ['mtime ' ];
583+ $ stat ['name ' ] = basename ($ path );
584+
585+ $ keys = ['size ' , 'mtime ' , 'mimetype ' , 'etag ' , 'storage_mtime ' , 'permissions ' , 'name ' ];
586+ return array_intersect_key ($ stat , array_flip ($ keys ));
587+ }
479588}
0 commit comments