2014-06-11 07:57:24 -04:00
< ? php
2024-05-23 03:26:56 -04:00
2014-06-11 07:57:24 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2014-06-11 07:57:24 -04:00
*/
namespace OC\Files\ObjectStore ;
2021-05-06 12:26:42 -04:00
use Aws\S3\Exception\S3Exception ;
use Aws\S3\Exception\S3MultipartUploadException ;
2017-01-03 11:26:44 -05:00
use Icewind\Streams\CallbackWrapper ;
2019-02-15 07:24:58 -05:00
use Icewind\Streams\CountWrapper ;
2015-03-10 11:30:13 -04:00
use Icewind\Streams\IteratorDirectory ;
2021-03-08 12:50:33 -05:00
use OC\Files\Cache\Cache ;
2015-12-02 09:38:17 -05:00
use OC\Files\Cache\CacheEntry ;
2020-11-05 10:30:05 -05:00
use OC\Files\Storage\PolyFill\CopyDirectory ;
2023-05-26 08:51:05 -04:00
use OCP\Files\Cache\ICache ;
2020-11-05 10:30:05 -05:00
use OCP\Files\Cache\ICacheEntry ;
2024-09-19 12:19:34 -04:00
use OCP\Files\Cache\IScanner ;
2020-11-05 10:30:05 -05:00
use OCP\Files\FileInfo ;
2021-05-06 12:26:42 -04:00
use OCP\Files\GenericFileException ;
2018-11-16 14:21:21 -05:00
use OCP\Files\NotFoundException ;
2014-06-17 16:06:56 -04:00
use OCP\Files\ObjectStore\IObjectStore ;
2025-03-28 12:18:11 -04:00
use OCP\Files\ObjectStore\IObjectStoreMetaData ;
2021-05-06 12:26:42 -04:00
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload ;
use OCP\Files\Storage\IChunkedFileWrite ;
2021-02-19 09:52:58 -05:00
use OCP\Files\Storage\IStorage ;
2024-02-08 09:47:39 -05:00
use Psr\Log\LoggerInterface ;
2014-06-11 07:57:24 -04:00
2021-05-06 12:26:42 -04:00
class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
2020-11-05 10:30:05 -05:00
use CopyDirectory ;
2023-10-20 03:09:50 -04:00
protected IObjectStore $objectStore ;
protected string $id ;
private string $objectPrefix = 'urn:oid:' ;
2016-10-27 08:15:59 -04:00
2024-02-08 09:47:39 -05:00
private LoggerInterface $logger ;
2017-06-19 07:52:51 -04:00
2023-11-17 02:43:22 -05:00
private bool $handleCopiesAsOwned ;
2023-10-20 03:08:08 -04:00
protected bool $validateWrites = true ;
2024-09-25 11:52:42 -04:00
private bool $preserveCacheItemsOnDelete = false ;
2023-11-17 02:43:22 -05:00
2023-10-20 03:08:08 -04:00
/**
2024-10-08 09:13:16 -04:00
* @ param array $parameters
2023-10-20 03:08:08 -04:00
* @ throws \Exception
*/
2024-10-08 09:13:16 -04:00
public function __construct ( array $parameters ) {
if ( isset ( $parameters [ 'objectstore' ]) && $parameters [ 'objectstore' ] instanceof IObjectStore ) {
$this -> objectStore = $parameters [ 'objectstore' ];
2014-06-17 16:06:56 -04:00
} else {
throw new \Exception ( 'missing IObjectStore instance' );
}
2024-10-08 09:13:16 -04:00
if ( isset ( $parameters [ 'storageid' ])) {
$this -> id = 'object::store:' . $parameters [ 'storageid' ];
2014-06-18 09:20:26 -04:00
} else {
2014-06-20 06:27:47 -04:00
$this -> id = 'object::store:' . $this -> objectStore -> getStorageId ();
2014-06-18 09:20:26 -04:00
}
2024-10-08 09:13:16 -04:00
if ( isset ( $parameters [ 'objectPrefix' ])) {
$this -> objectPrefix = $parameters [ 'objectPrefix' ];
2016-10-27 08:15:59 -04:00
}
2024-10-08 09:13:16 -04:00
if ( isset ( $parameters [ 'validateWrites' ])) {
$this -> validateWrites = ( bool ) $parameters [ 'validateWrites' ];
2022-08-04 06:32:20 -04:00
}
2024-10-08 09:13:16 -04:00
$this -> handleCopiesAsOwned = ( bool )( $parameters [ 'handleCopiesAsOwned' ] ? ? false );
2017-06-19 07:52:51 -04:00
2024-02-08 09:47:39 -05:00
$this -> logger = \OCP\Server :: get ( LoggerInterface :: class );
2014-06-13 09:05:54 -04:00
}
2025-05-27 13:29:44 -04:00
public function mkdir ( string $path , bool $force = false , array $metadata = []) : bool {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2023-05-23 08:28:37 -04:00
if ( ! $force && $this -> file_exists ( $path )) {
2022-05-09 10:56:40 -04:00
$this -> logger -> warning ( " Tried to create an object store folder that already exists: $path " );
2014-06-11 07:57:24 -04:00
return false ;
}
2014-06-12 06:18:19 -04:00
$mTime = time ();
2015-10-02 03:59:58 -04:00
$data = [
2014-06-12 06:18:19 -04:00
'mimetype' => 'httpd/unix-directory' ,
2025-05-27 13:29:44 -04:00
'size' => $metadata [ 'size' ] ? ? 0 ,
2014-06-12 06:18:19 -04:00
'mtime' => $mTime ,
'storage_mtime' => $mTime ,
2014-11-25 10:28:41 -05:00
'permissions' => \OCP\Constants :: PERMISSION_ALL ,
2015-10-02 03:59:58 -04:00
];
if ( $path === '' ) {
2014-06-11 07:57:24 -04:00
//create root on the fly
2014-06-27 09:00:29 -04:00
$data [ 'etag' ] = $this -> getETag ( '' );
2014-06-11 07:57:24 -04:00
$this -> getCache () -> put ( '' , $data );
2015-10-02 03:59:58 -04:00
return true ;
} else {
// if parent does not exist, create it
$parent = $this -> normalizePath ( dirname ( $path ));
$parentType = $this -> filetype ( $parent );
if ( $parentType === false ) {
if ( ! $this -> mkdir ( $parent )) {
// something went wrong
2022-05-09 10:56:40 -04:00
$this -> logger -> warning ( " Parent folder ( $parent ) doesn't exist and couldn't be created " );
2015-10-02 03:59:58 -04:00
return false ;
}
2020-04-10 04:35:09 -04:00
} elseif ( $parentType === 'file' ) {
2015-10-02 03:59:58 -04:00
// parent is a file
2022-05-09 10:56:40 -04:00
$this -> logger -> warning ( " Parent ( $parent ) is a file " );
2015-10-02 03:59:58 -04:00
return false ;
2014-06-27 09:00:29 -04:00
}
2015-10-02 03:59:58 -04:00
// finally create the new dir
$mTime = time (); // update mtime
$data [ 'mtime' ] = $mTime ;
$data [ 'storage_mtime' ] = $mTime ;
2014-06-12 06:18:19 -04:00
$data [ 'etag' ] = $this -> getETag ( $path );
2014-06-11 07:57:24 -04:00
$this -> getCache () -> put ( $path , $data );
return true ;
}
}
2024-10-01 10:12:30 -04:00
private function normalizePath ( string $path ) : string {
2014-06-20 06:27:47 -04:00
$path = trim ( $path , '/' );
//FIXME why do we sometimes get a path like 'files//username'?
$path = str_replace ( '//' , '/' , $path );
2014-06-27 09:00:29 -04:00
// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
if ( ! $path || $path === '.' ) {
$path = '' ;
2014-06-20 06:27:47 -04:00
}
return $path ;
}
/**
* Object Stores use a NoopScanner because metadata is directly stored in
* the file cache and cannot really scan the filesystem . The storage passed in is not used anywhere .
*/
2024-10-01 10:12:30 -04:00
public function getScanner ( string $path = '' , ? IStorage $storage = null ) : IScanner {
2014-06-20 06:27:47 -04:00
if ( ! $storage ) {
$storage = $this ;
}
if ( ! isset ( $this -> scanner )) {
2023-04-12 12:08:14 -04:00
$this -> scanner = new ObjectStoreScanner ( $storage );
2014-06-20 06:27:47 -04:00
}
2024-09-15 11:14:37 -04:00
/** @var \OC\Files\ObjectStore\ObjectStoreScanner */
2014-06-20 06:27:47 -04:00
return $this -> scanner ;
}
2024-09-19 12:19:34 -04:00
public function getId () : string {
2014-06-20 06:27:47 -04:00
return $this -> id ;
}
2024-10-01 10:12:30 -04:00
public function rmdir ( string $path ) : bool {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2023-03-27 11:44:33 -04:00
$entry = $this -> getCache () -> get ( $path );
2014-06-20 06:27:47 -04:00
2023-03-27 11:44:33 -04:00
if ( ! $entry || $entry -> getMimeType () !== ICacheEntry :: DIRECTORY_MIMETYPE ) {
2019-09-25 12:07:32 -04:00
return false ;
}
2014-06-20 06:27:47 -04:00
2023-03-27 11:44:33 -04:00
return $this -> rmObjects ( $entry );
2014-06-11 07:57:24 -04:00
}
2023-03-27 11:44:33 -04:00
private function rmObjects ( ICacheEntry $entry ) : bool {
$children = $this -> getCache () -> getFolderContentsById ( $entry -> getId ());
2014-06-11 16:15:42 -04:00
foreach ( $children as $child ) {
2023-03-27 11:44:33 -04:00
if ( $child -> getMimeType () === ICacheEntry :: DIRECTORY_MIMETYPE ) {
if ( ! $this -> rmObjects ( $child )) {
2019-09-25 12:07:32 -04:00
return false ;
}
2014-06-11 16:15:42 -04:00
} else {
2023-03-27 11:44:33 -04:00
if ( ! $this -> rmObject ( $child )) {
2019-09-25 12:07:32 -04:00
return false ;
}
2014-06-11 16:15:42 -04:00
}
}
2019-09-25 12:07:32 -04:00
2024-09-25 11:52:42 -04:00
if ( ! $this -> preserveCacheItemsOnDelete ) {
$this -> getCache () -> remove ( $entry -> getPath ());
}
2023-03-27 11:44:33 -04:00
2019-09-25 12:07:32 -04:00
return true ;
2014-06-11 16:15:42 -04:00
}
2014-06-11 07:57:24 -04:00
2024-10-01 10:12:30 -04:00
public function unlink ( string $path ) : bool {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2023-03-27 11:44:33 -04:00
$entry = $this -> getCache () -> get ( $path );
2014-06-11 07:57:24 -04:00
2023-03-27 11:44:33 -04:00
if ( $entry instanceof ICacheEntry ) {
if ( $entry -> getMimeType () === ICacheEntry :: DIRECTORY_MIMETYPE ) {
return $this -> rmObjects ( $entry );
} else {
return $this -> rmObject ( $entry );
2014-06-20 06:27:47 -04:00
}
2014-06-11 07:57:24 -04:00
}
2014-06-20 06:27:47 -04:00
return false ;
}
2014-06-11 07:57:24 -04:00
2023-03-27 11:44:33 -04:00
public function rmObject ( ICacheEntry $entry ) : bool {
try {
$this -> objectStore -> deleteObject ( $this -> getURN ( $entry -> getId ()));
} catch ( \Exception $ex ) {
if ( $ex -> getCode () !== 404 ) {
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not delete object ' . $this -> getURN ( $entry -> getId ()) . ' for ' . $entry -> getPath (),
[
'app' => 'objectstore' ,
'exception' => $ex ,
]
);
2023-03-27 11:44:33 -04:00
return false ;
}
//removing from cache is ok as it does not exist in the objectstore anyway
}
2024-09-25 11:52:42 -04:00
if ( ! $this -> preserveCacheItemsOnDelete ) {
$this -> getCache () -> remove ( $entry -> getPath ());
}
2023-03-27 11:44:33 -04:00
return true ;
}
2024-10-01 10:12:30 -04:00
public function stat ( string $path ) : array | false {
2014-06-23 10:29:01 -04:00
$path = $this -> normalizePath ( $path );
2015-12-02 09:38:17 -05:00
$cacheEntry = $this -> getCache () -> get ( $path );
if ( $cacheEntry instanceof CacheEntry ) {
return $cacheEntry -> getData ();
} else {
2023-05-23 08:28:37 -04:00
if ( $path === '' ) {
$this -> mkdir ( '' , true );
$cacheEntry = $this -> getCache () -> get ( $path );
if ( $cacheEntry instanceof CacheEntry ) {
return $cacheEntry -> getData ();
}
}
2015-12-02 09:38:17 -05:00
return false ;
}
2014-06-20 06:27:47 -04:00
}
2014-06-11 07:57:24 -04:00
2024-10-01 10:12:30 -04:00
public function getPermissions ( string $path ) : int {
2020-09-08 08:38:36 -04:00
$stat = $this -> stat ( $path );
if ( is_array ( $stat ) && isset ( $stat [ 'permissions' ])) {
return $stat [ 'permissions' ];
}
return parent :: getPermissions ( $path );
}
2014-06-20 06:27:47 -04:00
/**
* Override this method if you need a different unique resource identifier for your object storage implementation .
* The default implementations just appends the fileId to 'urn:oid:' . Make sure the URN is unique over all users .
* You may need a mapping table to store your URN if it cannot be generated from the fileid .
*
2024-10-01 10:12:30 -04:00
* @ return string the unified resource name used to identify the object
2014-06-20 06:27:47 -04:00
*/
2024-10-01 10:12:30 -04:00
public function getURN ( int $fileId ) : string {
return $this -> objectPrefix . $fileId ;
2014-06-11 07:57:24 -04:00
}
2024-10-01 10:12:30 -04:00
public function opendir ( string $path ) {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2014-06-20 06:27:47 -04:00
2014-06-11 07:57:24 -04:00
try {
2020-03-26 04:30:18 -04:00
$files = [];
2014-06-11 07:57:24 -04:00
$folderContents = $this -> getCache () -> getFolderContents ( $path );
foreach ( $folderContents as $file ) {
$files [] = $file [ 'name' ];
}
2014-06-20 06:27:47 -04:00
2015-03-10 11:30:13 -04:00
return IteratorDirectory :: wrap ( $files );
2015-05-19 09:27:50 -04:00
} catch ( \Exception $e ) {
2024-02-08 09:47:39 -05:00
$this -> logger -> error ( $e -> getMessage (), [ 'exception' => $e ]);
2014-06-11 07:57:24 -04:00
return false ;
}
}
2024-10-01 10:12:30 -04:00
public function filetype ( string $path ) : string | false {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
$stat = $this -> stat ( $path );
if ( $stat ) {
if ( $stat [ 'mimetype' ] === 'httpd/unix-directory' ) {
return 'dir' ;
}
return 'file' ;
} else {
return false ;
}
}
2024-10-01 10:12:30 -04:00
public function fopen ( string $path , string $mode ) {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2018-03-07 06:05:57 -05:00
if ( strrpos ( $path , '.' ) !== false ) {
$ext = substr ( $path , strrpos ( $path , '.' ));
} else {
$ext = '' ;
}
2014-06-11 07:57:24 -04:00
switch ( $mode ) {
case 'r' :
case 'rb' :
$stat = $this -> stat ( $path );
if ( is_array ( $stat )) {
2022-12-01 11:42:07 -05:00
$filesize = $stat [ 'size' ] ? ? 0 ;
2020-09-07 14:53:56 -04:00
// Reading 0 sized files is a waste of time
2022-12-01 11:42:07 -05:00
if ( $filesize === 0 ) {
2020-09-07 14:53:56 -04:00
return fopen ( 'php://memory' , $mode );
}
2014-06-11 16:15:42 -04:00
try {
2022-12-01 11:42:07 -05:00
$handle = $this -> objectStore -> readObject ( $this -> getURN ( $stat [ 'fileid' ]));
if ( $handle === false ) {
return false ; // keep backward compatibility
}
$streamStat = fstat ( $handle );
$actualSize = $streamStat [ 'size' ] ? ? - 1 ;
if ( $actualSize > - 1 && $actualSize !== $filesize ) {
$this -> getCache () -> update (( int ) $stat [ 'fileid' ], [ 'size' => $actualSize ]);
}
return $handle ;
2018-11-16 14:21:21 -05:00
} catch ( NotFoundException $e ) {
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not get object ' . $this -> getURN ( $stat [ 'fileid' ]) . ' for file ' . $path ,
[
'app' => 'objectstore' ,
'exception' => $e ,
]
);
2018-11-16 14:21:21 -05:00
throw $e ;
2024-02-08 09:47:39 -05:00
} catch ( \Exception $e ) {
$this -> logger -> error (
'Could not get object ' . $this -> getURN ( $stat [ 'fileid' ]) . ' for file ' . $path ,
[
'app' => 'objectstore' ,
'exception' => $e ,
]
);
2014-06-11 16:15:42 -04:00
return false ;
}
2014-06-11 07:57:24 -04:00
} else {
return false ;
}
2023-01-20 05:45:08 -05:00
// no break
2014-06-11 07:57:24 -04:00
case 'w' :
case 'wb' :
2018-03-07 06:05:57 -05:00
case 'w+' :
case 'wb+' :
2023-05-23 08:28:37 -04:00
$dirName = dirname ( $path );
$parentExists = $this -> is_dir ( $dirName );
if ( ! $parentExists ) {
return false ;
}
2018-03-07 06:05:57 -05:00
$tmpFile = \OC :: $server -> getTempManager () -> getTemporaryFile ( $ext );
$handle = fopen ( $tmpFile , $mode );
return CallbackWrapper :: wrap ( $handle , null , null , function () use ( $path , $tmpFile ) {
$this -> writeBack ( $tmpFile , $path );
2022-05-30 11:53:07 -04:00
unlink ( $tmpFile );
2018-03-07 06:05:57 -05:00
});
2014-06-11 07:57:24 -04:00
case 'a' :
case 'ab' :
case 'r+' :
case 'a+' :
case 'x' :
case 'x+' :
case 'c' :
case 'c+' :
2015-12-18 05:25:33 -05:00
$tmpFile = \OC :: $server -> getTempManager () -> getTemporaryFile ( $ext );
2014-06-11 07:57:24 -04:00
if ( $this -> file_exists ( $path )) {
$source = $this -> fopen ( $path , 'r' );
file_put_contents ( $tmpFile , $source );
}
2017-01-03 11:26:44 -05:00
$handle = fopen ( $tmpFile , $mode );
return CallbackWrapper :: wrap ( $handle , null , null , function () use ( $path , $tmpFile ) {
$this -> writeBack ( $tmpFile , $path );
2022-05-30 11:53:07 -04:00
unlink ( $tmpFile );
2017-01-03 11:26:44 -05:00
});
2014-06-11 07:57:24 -04:00
}
return false ;
}
2024-10-01 10:12:30 -04:00
public function file_exists ( string $path ) : bool {
2014-06-20 06:27:47 -04:00
$path = $this -> normalizePath ( $path );
return ( bool ) $this -> stat ( $path );
}
2024-10-01 10:12:30 -04:00
public function rename ( string $source , string $target ) : bool {
2014-06-23 10:29:01 -04:00
$source = $this -> normalizePath ( $source );
$target = $this -> normalizePath ( $target );
2014-09-05 07:25:59 -04:00
$this -> remove ( $target );
$this -> getCache () -> move ( $source , $target );
$this -> touch ( dirname ( $target ));
return true ;
2014-06-11 07:57:24 -04:00
}
2014-06-20 06:27:47 -04:00
2024-10-01 10:12:30 -04:00
public function getMimeType ( string $path ) : string | false {
2014-06-11 07:57:24 -04:00
$path = $this -> normalizePath ( $path );
2020-03-11 06:14:12 -04:00
return parent :: getMimeType ( $path );
2014-06-11 07:57:24 -04:00
}
2014-06-20 06:27:47 -04:00
2024-10-01 10:12:30 -04:00
public function touch ( string $path , ? int $mtime = null ) : bool {
2014-06-11 07:57:24 -04:00
if ( is_null ( $mtime )) {
$mtime = time ();
}
$path = $this -> normalizePath ( $path );
2014-06-11 16:15:42 -04:00
$dirName = dirname ( $path );
$parentExists = $this -> is_dir ( $dirName );
2014-06-11 07:57:24 -04:00
if ( ! $parentExists ) {
return false ;
}
$stat = $this -> stat ( $path );
if ( is_array ( $stat )) {
// update existing mtime in db
$stat [ 'mtime' ] = $mtime ;
$this -> getCache () -> update ( $stat [ 'fileid' ], $stat );
} else {
2014-06-11 16:15:42 -04:00
try {
2017-11-17 05:59:25 -05:00
//create a empty file, need to have at least on char to make it
// work with all object storage implementations
2017-11-08 11:51:02 -05:00
$this -> file_put_contents ( $path , ' ' );
2014-06-11 16:15:42 -04:00
} catch ( \Exception $ex ) {
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not create object for ' . $path ,
[
'app' => 'objectstore' ,
'exception' => $ex ,
]
);
2018-02-07 16:06:07 -05:00
throw $ex ;
2014-06-11 16:15:42 -04:00
}
2014-06-11 07:57:24 -04:00
}
return true ;
}
2024-10-01 10:12:30 -04:00
public function writeBack ( string $tmpFile , string $path ) {
2018-10-26 13:15:23 -04:00
$size = filesize ( $tmpFile );
$this -> writeStream ( $path , fopen ( $tmpFile , 'r' ), $size );
}
2024-10-01 10:12:30 -04:00
public function hasUpdated ( string $path , int $time ) : bool {
2018-10-26 13:15:23 -04:00
return false ;
}
2024-09-19 12:19:34 -04:00
public function needsPartFile () : bool {
2018-10-26 13:15:23 -04:00
return false ;
}
2024-10-01 10:12:30 -04:00
public function file_put_contents ( string $path , mixed $data ) : int {
2024-09-18 05:14:20 -04:00
$fh = fopen ( 'php://temp' , 'w+' );
fwrite ( $fh , $data );
rewind ( $fh );
return $this -> writeStream ( $path , $fh , strlen ( $data ));
2018-10-26 13:15:23 -04:00
}
2024-03-28 11:13:19 -04:00
public function writeStream ( string $path , $stream , ? int $size = null ) : int {
2024-12-05 09:16:22 -05:00
if ( $size === null ) {
$stats = fstat ( $stream );
if ( is_array ( $stats ) && isset ( $stats [ 'size' ])) {
$size = $stats [ 'size' ];
}
}
2014-06-11 07:57:24 -04:00
$stat = $this -> stat ( $path );
2014-06-12 06:18:19 -04:00
if ( empty ( $stat )) {
2014-06-11 07:57:24 -04:00
// create new file
2018-10-26 13:15:23 -04:00
$stat = [
2015-10-13 06:25:59 -04:00
'permissions' => \OCP\Constants :: PERMISSION_ALL - \OCP\Constants :: PERMISSION_CREATE ,
2018-10-26 13:15:23 -04:00
];
2014-06-11 07:57:24 -04:00
}
2014-06-12 06:18:19 -04:00
// update stat with new data
$mTime = time ();
2018-10-26 13:15:23 -04:00
$stat [ 'size' ] = ( int ) $size ;
2014-06-12 06:18:19 -04:00
$stat [ 'mtime' ] = $mTime ;
$stat [ 'storage_mtime' ] = $mTime ;
2017-10-25 11:57:21 -04:00
2017-11-08 11:51:02 -05:00
$mimetypeDetector = \OC :: $server -> getMimeTypeDetector ();
2017-10-25 11:57:21 -04:00
$mimetype = $mimetypeDetector -> detectPath ( $path );
2025-03-28 12:18:11 -04:00
$metadata = [
'mimetype' => $mimetype ,
2025-03-28 12:44:02 -04:00
'original-storage' => $this -> getId (),
'original-path' => $path ,
2025-03-28 12:18:11 -04:00
];
2025-07-28 13:55:20 -04:00
if ( $size ) {
$metadata [ 'size' ] = $size ;
}
2017-10-25 11:57:21 -04:00
$stat [ 'mimetype' ] = $mimetype ;
2014-06-27 12:49:06 -04:00
$stat [ 'etag' ] = $this -> getETag ( $path );
2021-06-21 06:04:49 -04:00
$stat [ 'checksum' ] = '' ;
2014-06-12 06:18:19 -04:00
2018-12-12 09:24:40 -05:00
$exists = $this -> getCache () -> inCache ( $path );
$uploadPath = $exists ? $path : $path . '.part' ;
2020-08-31 06:28:04 -04:00
if ( $exists ) {
$fileId = $stat [ 'fileid' ];
} else {
2024-09-18 07:54:21 -04:00
$parent = $this -> normalizePath ( dirname ( $path ));
if ( ! $this -> is_dir ( $parent )) {
throw new \InvalidArgumentException ( " trying to upload a file ( $path ) inside a non-directory ( $parent ) " );
}
2020-08-31 06:28:04 -04:00
$fileId = $this -> getCache () -> put ( $uploadPath , $stat );
}
2018-12-12 09:24:40 -05:00
$urn = $this -> getURN ( $fileId );
2014-06-11 16:15:42 -04:00
try {
2014-06-12 06:18:19 -04:00
//upload to object storage
2025-07-29 11:47:03 -04:00
$totalWritten = 0 ;
$countStream = CountWrapper :: wrap ( $stream , function ( $writtenSize ) use ( $fileId , $size , $exists , & $totalWritten ) {
if ( is_null ( $size ) && ! $exists ) {
2018-10-26 13:15:23 -04:00
$this -> getCache () -> update ( $fileId , [
2020-11-05 10:30:05 -05:00
'size' => $writtenSize ,
2018-10-26 13:15:23 -04:00
]);
2025-03-28 12:18:11 -04:00
}
2025-07-29 11:47:03 -04:00
$totalWritten = $writtenSize ;
});
if ( $this -> objectStore instanceof IObjectStoreMetaData ) {
$this -> objectStore -> writeObjectWithMetaData ( $urn , $countStream , $metadata );
2018-10-26 13:15:23 -04:00
} else {
2025-07-29 11:47:03 -04:00
$this -> objectStore -> writeObject ( $urn , $countStream , $metadata [ 'mimetype' ]);
2018-10-26 13:15:23 -04:00
}
2025-07-29 11:47:03 -04:00
if ( is_resource ( $countStream )) {
fclose ( $countStream );
}
$stat [ 'size' ] = $totalWritten ;
2014-06-11 16:15:42 -04:00
} catch ( \Exception $ex ) {
2020-08-31 06:25:20 -04:00
if ( ! $exists ) {
/*
* Only remove the entry if we are dealing with a new file .
* Else people lose access to existing files
*/
$this -> getCache () -> remove ( $uploadPath );
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not create object ' . $urn . ' for ' . $path ,
[
'app' => 'objectstore' ,
'exception' => $ex ,
]
);
2020-08-31 06:25:20 -04:00
} else {
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not update object ' . $urn . ' for ' . $path ,
[
'app' => 'objectstore' ,
'exception' => $ex ,
]
);
2020-08-31 06:25:20 -04:00
}
2025-07-30 08:56:41 -04:00
throw new GenericFileException ( 'Error while writing stream to object store' , 0 , $ex );
2014-06-11 16:15:42 -04:00
}
2014-06-11 07:57:24 -04:00
2020-08-31 06:28:04 -04:00
if ( $exists ) {
2023-06-14 08:38:33 -04:00
// Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
$stat [ 'unencrypted_size' ] = $stat [ 'size' ];
2020-08-31 06:28:04 -04:00
$this -> getCache () -> update ( $fileId , $stat );
} else {
2022-08-04 06:32:20 -04:00
if ( ! $this -> validateWrites || $this -> objectStore -> objectExists ( $urn )) {
2018-12-12 09:24:40 -05:00
$this -> getCache () -> move ( $uploadPath , $path );
} else {
$this -> getCache () -> remove ( $uploadPath );
throw new \Exception ( " Object not found after writing (urn: $urn , path: $path ) " , 404 );
}
}
2025-07-29 11:47:03 -04:00
return $totalWritten ;
2018-03-07 07:33:35 -05:00
}
2019-05-21 11:18:00 -04:00
public function getObjectStore () : IObjectStore {
return $this -> objectStore ;
}
2020-11-05 10:30:05 -05:00
2023-03-27 11:44:33 -04:00
public function copyFromStorage (
IStorage $sourceStorage ,
2024-10-01 10:12:30 -04:00
string $sourceInternalPath ,
string $targetInternalPath ,
bool $preserveMtime = false ,
2024-09-19 12:19:34 -04:00
) : bool {
2021-02-19 09:52:58 -05:00
if ( $sourceStorage -> instanceOfStorage ( ObjectStoreStorage :: class )) {
/** @var ObjectStoreStorage $sourceStorage */
if ( $sourceStorage -> getObjectStore () -> getStorageId () === $this -> getObjectStore () -> getStorageId ()) {
2021-10-07 05:50:33 -04:00
/** @var CacheEntry $sourceEntry */
2021-02-19 09:52:58 -05:00
$sourceEntry = $sourceStorage -> getCache () -> get ( $sourceInternalPath );
2021-10-07 05:50:33 -04:00
$sourceEntryData = $sourceEntry -> getData ();
// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
// unjailed storage.
if ( is_array ( $sourceEntryData ) && array_key_exists ( 'scan_permissions' , $sourceEntryData )) {
$sourceEntry [ 'permissions' ] = $sourceEntryData [ 'scan_permissions' ];
}
2023-05-26 08:51:05 -04:00
$this -> copyInner ( $sourceStorage -> getCache (), $sourceEntry , $targetInternalPath );
2021-02-19 09:52:58 -05:00
return true ;
}
}
return parent :: copyFromStorage ( $sourceStorage , $sourceInternalPath , $targetInternalPath );
}
2024-10-01 10:12:30 -04:00
public function moveFromStorage ( IStorage $sourceStorage , string $sourceInternalPath , string $targetInternalPath , ? ICacheEntry $sourceCacheEntry = null ) : bool {
2024-06-20 09:57:59 -04:00
$sourceCache = $sourceStorage -> getCache ();
2025-04-01 05:27:02 -04:00
if (
2025-06-30 09:04:05 -04:00
$sourceStorage -> instanceOfStorage ( ObjectStoreStorage :: class )
&& $sourceStorage -> getObjectStore () -> getStorageId () === $this -> getObjectStore () -> getStorageId ()
2025-04-01 05:27:02 -04:00
) {
if ( $this -> getCache () -> get ( $targetInternalPath )) {
$this -> unlink ( $targetInternalPath );
$this -> getCache () -> remove ( $targetInternalPath );
}
2024-09-14 17:05:12 -04:00
$this -> getCache () -> moveFromCache ( $sourceCache , $sourceInternalPath , $targetInternalPath );
// Do not import any data when source and target bucket are identical.
return true ;
}
2024-06-20 09:57:59 -04:00
if ( ! $sourceCacheEntry ) {
$sourceCacheEntry = $sourceCache -> get ( $sourceInternalPath );
}
2024-09-26 10:28:59 -04:00
$this -> copyObjects ( $sourceStorage , $sourceCache , $sourceCacheEntry );
if ( $sourceStorage -> instanceOfStorage ( ObjectStoreStorage :: class )) {
/** @var ObjectStoreStorage $sourceStorage */
$sourceStorage -> setPreserveCacheOnDelete ( true );
}
if ( $sourceCacheEntry -> getMimeType () === ICacheEntry :: DIRECTORY_MIMETYPE ) {
2024-06-20 09:57:59 -04:00
$sourceStorage -> rmdir ( $sourceInternalPath );
} else {
$sourceStorage -> unlink ( $sourceInternalPath );
}
2024-09-26 10:28:59 -04:00
if ( $sourceStorage -> instanceOfStorage ( ObjectStoreStorage :: class )) {
/** @var ObjectStoreStorage $sourceStorage */
$sourceStorage -> setPreserveCacheOnDelete ( false );
}
2025-04-01 05:27:02 -04:00
if ( $this -> getCache () -> get ( $targetInternalPath )) {
$this -> unlink ( $targetInternalPath );
$this -> getCache () -> remove ( $targetInternalPath );
}
2024-09-26 10:28:59 -04:00
$this -> getCache () -> moveFromCache ( $sourceCache , $sourceInternalPath , $targetInternalPath );
2024-06-20 09:57:59 -04:00
return true ;
}
2024-09-26 10:28:59 -04:00
/**
* Copy the object ( s ) of a file or folder into this storage , without touching the cache
*/
private function copyObjects ( IStorage $sourceStorage , ICache $sourceCache , ICacheEntry $sourceCacheEntry ) {
$copiedFiles = [];
try {
foreach ( $this -> getAllChildObjects ( $sourceCache , $sourceCacheEntry ) as $file ) {
$sourceStream = $sourceStorage -> fopen ( $file -> getPath (), 'r' );
if ( ! $sourceStream ) {
throw new \Exception ( " Failed to open source file { $file -> getPath () } ( { $file -> getId () } ) " );
}
$this -> objectStore -> writeObject ( $this -> getURN ( $file -> getId ()), $sourceStream , $file -> getMimeType ());
if ( is_resource ( $sourceStream )) {
fclose ( $sourceStream );
}
$copiedFiles [] = $file -> getId ();
}
} catch ( \Exception $e ) {
foreach ( $copiedFiles as $fileId ) {
try {
$this -> objectStore -> deleteObject ( $this -> getURN ( $fileId ));
} catch ( \Exception $e ) {
// ignore
}
}
throw $e ;
}
}
/**
* @ return \Iterator < ICacheEntry >
*/
private function getAllChildObjects ( ICache $cache , ICacheEntry $entry ) : \Iterator {
if ( $entry -> getMimeType () === FileInfo :: MIMETYPE_FOLDER ) {
foreach ( $cache -> getFolderContentsById ( $entry -> getId ()) as $child ) {
yield from $this -> getAllChildObjects ( $cache , $child );
}
} else {
yield $entry ;
}
}
2024-10-01 10:12:30 -04:00
public function copy ( string $source , string $target ) : bool {
2022-10-18 06:49:34 -04:00
$source = $this -> normalizePath ( $source );
$target = $this -> normalizePath ( $target );
2020-11-05 10:30:05 -05:00
$cache = $this -> getCache ();
2022-10-18 06:49:34 -04:00
$sourceEntry = $cache -> get ( $source );
2020-11-05 10:30:05 -05:00
if ( ! $sourceEntry ) {
throw new NotFoundException ( 'Source object not found' );
}
2023-05-26 08:51:05 -04:00
$this -> copyInner ( $cache , $sourceEntry , $target );
2020-11-05 10:30:05 -05:00
return true ;
}
2023-05-26 08:51:05 -04:00
private function copyInner ( ICache $sourceCache , ICacheEntry $sourceEntry , string $to ) {
2020-11-05 10:30:05 -05:00
$cache = $this -> getCache ();
if ( $sourceEntry -> getMimeType () === FileInfo :: MIMETYPE_FOLDER ) {
if ( $cache -> inCache ( $to )) {
$cache -> remove ( $to );
}
2025-05-27 13:29:44 -04:00
$this -> mkdir ( $to , false , [ 'size' => $sourceEntry -> getSize ()]);
2020-11-05 10:30:05 -05:00
2023-05-26 08:51:05 -04:00
foreach ( $sourceCache -> getFolderContentsById ( $sourceEntry -> getId ()) as $child ) {
$this -> copyInner ( $sourceCache , $child , $to . '/' . $child -> getName ());
2020-11-05 10:30:05 -05:00
}
} else {
$this -> copyFile ( $sourceEntry , $to );
}
}
private function copyFile ( ICacheEntry $sourceEntry , string $to ) {
$cache = $this -> getCache ();
$sourceUrn = $this -> getURN ( $sourceEntry -> getId ());
2021-03-08 12:50:33 -05:00
if ( ! $cache instanceof Cache ) {
2024-08-23 09:10:27 -04:00
throw new \Exception ( 'Invalid source cache for object store copy' );
2020-11-05 10:30:05 -05:00
}
2021-03-08 12:50:33 -05:00
$targetId = $cache -> copyFromCache ( $cache , $sourceEntry , $to );
$targetUrn = $this -> getURN ( $targetId );
2020-11-05 10:30:05 -05:00
try {
$this -> objectStore -> copyObject ( $sourceUrn , $targetUrn );
2023-11-17 02:43:22 -05:00
if ( $this -> handleCopiesAsOwned ) {
// Copied the file thus we gain all permissions as we are the owner now ! warning while this aligns with local storage it should not be used and instead fix local storage !
$cache -> update ( $targetId , [ 'permissions' => \OCP\Constants :: PERMISSION_ALL ]);
}
2020-11-05 10:30:05 -05:00
} catch ( \Exception $e ) {
$cache -> remove ( $to );
throw $e ;
}
}
2021-05-06 12:26:42 -04:00
public function startChunkedWrite ( string $targetPath ) : string {
if ( ! $this -> objectStore instanceof IObjectStoreMultiPartUpload ) {
throw new GenericFileException ( 'Object store does not support multipart upload' );
}
$cacheEntry = $this -> getCache () -> get ( $targetPath );
$urn = $this -> getURN ( $cacheEntry -> getId ());
return $this -> objectStore -> initiateMultipartUpload ( $urn );
}
/**
* @ throws GenericFileException
*/
2023-03-27 11:44:33 -04:00
public function putChunkedWritePart (
string $targetPath ,
string $writeToken ,
string $chunkId ,
$data ,
2024-09-19 05:10:31 -04:00
$size = null ,
2023-03-27 11:44:33 -04:00
) : ? array {
2021-05-06 12:26:42 -04:00
if ( ! $this -> objectStore instanceof IObjectStoreMultiPartUpload ) {
throw new GenericFileException ( 'Object store does not support multipart upload' );
}
$cacheEntry = $this -> getCache () -> get ( $targetPath );
$urn = $this -> getURN ( $cacheEntry -> getId ());
$result = $this -> objectStore -> uploadMultipartPart ( $urn , $writeToken , ( int ) $chunkId , $data , $size );
$parts [ $chunkId ] = [
'PartNumber' => $chunkId ,
2023-03-27 11:44:33 -04:00
'ETag' => trim ( $result -> get ( 'ETag' ), '"' ),
2021-05-06 12:26:42 -04:00
];
return $parts [ $chunkId ];
}
public function completeChunkedWrite ( string $targetPath , string $writeToken ) : int {
if ( ! $this -> objectStore instanceof IObjectStoreMultiPartUpload ) {
throw new GenericFileException ( 'Object store does not support multipart upload' );
}
$cacheEntry = $this -> getCache () -> get ( $targetPath );
$urn = $this -> getURN ( $cacheEntry -> getId ());
$parts = $this -> objectStore -> getMultipartUploads ( $urn , $writeToken );
$sortedParts = array_values ( $parts );
sort ( $sortedParts );
try {
$size = $this -> objectStore -> completeMultipartUpload ( $urn , $writeToken , $sortedParts );
$stat = $this -> stat ( $targetPath );
$mtime = time ();
if ( is_array ( $stat )) {
$stat [ 'size' ] = $size ;
$stat [ 'mtime' ] = $mtime ;
$stat [ 'mimetype' ] = $this -> getMimeType ( $targetPath );
$this -> getCache () -> update ( $stat [ 'fileid' ], $stat );
}
2023-03-27 11:44:33 -04:00
} catch ( S3MultipartUploadException | S3Exception $e ) {
2021-05-06 12:26:42 -04:00
$this -> objectStore -> abortMultipartUpload ( $urn , $writeToken );
2024-02-08 09:47:39 -05:00
$this -> logger -> error (
'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken ,
[
'app' => 'objectstore' ,
'exception' => $e ,
]
);
2021-05-06 12:26:42 -04:00
throw new GenericFileException ( 'Could not write chunked file' );
}
return $size ;
}
public function cancelChunkedWrite ( string $targetPath , string $writeToken ) : void {
if ( ! $this -> objectStore instanceof IObjectStoreMultiPartUpload ) {
throw new GenericFileException ( 'Object store does not support multipart upload' );
}
$cacheEntry = $this -> getCache () -> get ( $targetPath );
$urn = $this -> getURN ( $cacheEntry -> getId ());
$this -> objectStore -> abortMultipartUpload ( $urn , $writeToken );
}
2024-09-25 11:52:42 -04:00
public function setPreserveCacheOnDelete ( bool $preserve ) {
$this -> preserveCacheItemsOnDelete = $preserve ;
}
2014-06-24 08:48:59 -04:00
}