diff options
Diffstat (limited to 'apps/files_external/lib/Lib/Storage/AmazonS3.php')
-rw-r--r-- | apps/files_external/lib/Lib/Storage/AmazonS3.php | 343 |
1 files changed, 167 insertions, 176 deletions
diff --git a/apps/files_external/lib/Lib/Storage/AmazonS3.php b/apps/files_external/lib/Lib/Storage/AmazonS3.php index cfd78689fa4..5dc9e114532 100644 --- a/apps/files_external/lib/Lib/Storage/AmazonS3.php +++ b/apps/files_external/lib/Lib/Storage/AmazonS3.php @@ -1,103 +1,68 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author André Gaul <gaul@web-yard.de> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christian Berendt <berendt@b1-systems.de> - * @author Christopher T. Johnson <ctjctj@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author enoch <lanxenet@hotmail.com> - * @author Johan Björk <johanimon@gmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Martin Mattel <martin.mattel@diemattels.at> - * @author Michael Gapczynski <GapczynskiM@gmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Kapfer <philipp.kapfer@gmx.at> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_External\Lib\Storage; -use Aws\Result; use Aws\S3\Exception\S3Exception; -use Aws\S3\S3Client; use Icewind\Streams\CallbackWrapper; +use Icewind\Streams\CountWrapper; use Icewind\Streams\IteratorDirectory; -use OC\Cache\CappedMemoryCache; use OC\Files\Cache\CacheEntry; use OC\Files\ObjectStore\S3ConnectionTrait; use OC\Files\ObjectStore\S3ObjectTrait; +use OC\Files\Storage\Common; +use OCP\Cache\CappedMemoryCache; use OCP\Constants; use OCP\Files\FileInfo; use OCP\Files\IMimeTypeDetector; +use OCP\ICache; use OCP\ICacheFactory; -use OCP\IMemcache; +use OCP\ITempManager; +use OCP\Server; +use Psr\Log\LoggerInterface; -class AmazonS3 extends \OC\Files\Storage\Common { +class AmazonS3 extends Common { use S3ConnectionTrait; use S3ObjectTrait; - public function needsPartFile() { + private LoggerInterface $logger; + + public function needsPartFile(): bool { return false; } - /** @var CappedMemoryCache|Result[] */ - private $objectCache; - - /** @var CappedMemoryCache|bool[] */ - private $directoryCache; + /** @var CappedMemoryCache<array|false> */ + private CappedMemoryCache $objectCache; - /** @var CappedMemoryCache|array */ - private $filesCache; + /** @var CappedMemoryCache<bool> */ + private CappedMemoryCache $directoryCache; - /** @var IMimeTypeDetector */ - private $mimeDetector; + /** @var CappedMemoryCache<array> */ + private CappedMemoryCache $filesCache; - /** @var bool|null */ - private $versioningEnabled = null; + private IMimeTypeDetector $mimeDetector; + private ?bool $versioningEnabled = null; + private ICache $memCache; - /** @var IMemcache */ - private $memCache; - - public function __construct($parameters) { + public function __construct(array $parameters) { parent::__construct($parameters); $this->parseParams($parameters); $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']); $this->objectCache = new CappedMemoryCache(); $this->directoryCache = new CappedMemoryCache(); $this->filesCache = new CappedMemoryCache(); - $this->mimeDetector = \OC::$server->get(IMimeTypeDetector::class); + $this->mimeDetector = Server::get(IMimeTypeDetector::class); /** @var ICacheFactory $cacheFactory */ - $cacheFactory = \OC::$server->get(ICacheFactory::class); + $cacheFactory = Server::get(ICacheFactory::class); $this->memCache = $cacheFactory->createLocal('s3-external'); + $this->logger = Server::get(LoggerInterface::class); } - /** - * @param string $path - * @return string correctly encoded path - */ - private function normalizePath($path) { + private function normalizePath(string $path): string { $path = trim($path, '/'); if (!$path) { @@ -107,24 +72,24 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $path; } - private function isRoot($path) { + private function isRoot(string $path): bool { return $path === '.'; } - private function cleanKey($path) { + private function cleanKey(string $path): string { if ($this->isRoot($path)) { return '/'; } return $path; } - private function clearCache() { + private function clearCache(): void { $this->objectCache = new CappedMemoryCache(); $this->directoryCache = new CappedMemoryCache(); $this->filesCache = new CappedMemoryCache(); } - private function invalidateCache($key) { + private function invalidateCache(string $key): void { unset($this->objectCache[$key]); $keys = array_keys($this->objectCache->getData()); $keyLength = strlen($key); @@ -144,17 +109,13 @@ class AmazonS3 extends \OC\Files\Storage\Common { unset($this->directoryCache[$key]); } - /** - * @param $key - * @return array|false - */ - private function headObject($key) { + private function headObject(string $key): array|false { if (!isset($this->objectCache[$key])) { try { $this->objectCache[$key] = $this->getConnection()->headObject([ 'Bucket' => $this->bucket, 'Key' => $key - ])->toArray(); + ] + $this->getSSECParameters())->toArray(); } catch (S3Exception $e) { if ($e->getStatusCode() >= 500) { throw $e; @@ -163,8 +124,9 @@ class AmazonS3 extends \OC\Files\Storage\Common { } } - if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) { - $this->objectCache[$key]["Key"] = $key; + if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]['Key'])) { + /** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */ + $this->objectCache[$key]['Key'] = $key; } return $this->objectCache[$key]; } @@ -178,11 +140,9 @@ class AmazonS3 extends \OC\Files\Storage\Common { * Implementation from flysystem-aws-s3-v3: * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694 * - * @param $path - * @return bool * @throws \Exception */ - private function doesDirectoryExist($path) { + private function doesDirectoryExist(string $path): bool { if ($path === '.' || $path === '') { return true; } @@ -224,13 +184,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { return false; } - /** - * Remove a file or folder - * - * @param string $path - * @return bool - */ - protected function remove($path) { + protected function remove(string $path): bool { // remember fileType to reduce http calls $fileType = $this->filetype($path); if ($fileType === 'dir') { @@ -242,7 +196,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { } } - public function mkdir($path) { + public function mkdir(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -255,10 +209,13 @@ class AmazonS3 extends \OC\Files\Storage\Common { 'Key' => $path . '/', 'Body' => '', 'ContentType' => FileInfo::MIMETYPE_FOLDER - ]); + ] + $this->getSSECParameters()); $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -267,12 +224,12 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function file_exists($path) { + public function file_exists(string $path): bool { return $this->filetype($path) !== false; } - public function rmdir($path) { + public function rmdir(string $path): bool { $path = $this->normalizePath($path); if ($this->isRoot($path)) { @@ -287,20 +244,13 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $this->batchDelete($path); } - protected function clearBucket() { + protected function clearBucket(): bool { $this->clearCache(); - try { - $this->getConnection()->clearBucket([ - "Bucket" => $this->bucket - ]); - return true; - // clearBucket() is not working with Ceph, so if it fails we try the slower approach - } catch (\Exception $e) { - return $this->batchDelete(); - } + return $this->batchDelete(); } - private function batchDelete($path = null) { + private function batchDelete(?string $path = null): bool { + // TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html $params = [ 'Bucket' => $this->bucket ]; @@ -330,13 +280,16 @@ class AmazonS3 extends \OC\Files\Storage\Common { $this->deleteObject($path); } } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return true; } - public function opendir($path) { + public function opendir(string $path) { try { $content = iterator_to_array($this->getDirectoryContent($path)); return IteratorDirectory::wrap(array_map(function (array $item) { @@ -347,7 +300,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { } } - public function stat($path) { + public function stat(string $path): array|false { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -369,11 +322,8 @@ class AmazonS3 extends \OC\Files\Storage\Common { * * When the information is already present (e.g. opendir has been called before) * this value is return. Otherwise a headObject is emitted. - * - * @param $path - * @return int|mixed */ - private function getContentLength($path) { + private function getContentLength(string $path): int { if (isset($this->filesCache[$path])) { return (int)$this->filesCache[$path]['ContentLength']; } @@ -391,11 +341,8 @@ class AmazonS3 extends \OC\Files\Storage\Common { * * When the information is already present (e.g. opendir has been called before) * this value is return. Otherwise a headObject is emitted. - * - * @param $path - * @return mixed|string */ - private function getLastModified($path) { + private function getLastModified(string $path): string { if (isset($this->filesCache[$path])) { return $this->filesCache[$path]['LastModified']; } @@ -408,7 +355,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { return 'now'; } - public function is_dir($path) { + public function is_dir(string $path): bool { $path = $this->normalizePath($path); if (isset($this->filesCache[$path])) { @@ -418,12 +365,15 @@ class AmazonS3 extends \OC\Files\Storage\Common { try { return $this->doesDirectoryExist($path); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } - public function filetype($path) { + public function filetype(string $path): string|false { $path = $this->normalizePath($path); if ($this->isRoot($path)) { @@ -441,14 +391,17 @@ class AmazonS3 extends \OC\Files\Storage\Common { return 'dir'; } } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return false; } - public function getPermissions($path) { + public function getPermissions(string $path): int { $type = $this->filetype($path); if (!$type) { return 0; @@ -456,7 +409,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE; } - public function unlink($path) { + public function unlink(string $path): bool { $path = $this->normalizePath($path); if ($this->is_dir($path)) { @@ -467,14 +420,17 @@ class AmazonS3 extends \OC\Files\Storage\Common { $this->deleteObject($path); $this->invalidateCache($path); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } return true; } - public function fopen($path, $mode) { + public function fopen(string $path, string $mode) { $path = $this->normalizePath($path); switch ($mode) { @@ -488,16 +444,19 @@ class AmazonS3 extends \OC\Files\Storage\Common { try { return $this->readObject($path); - } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } case 'w': case 'wb': - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile(); + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile(); $handle = fopen($tmpFile, 'w'); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { $this->writeBack($tmpFile, $path); }); case 'a': @@ -515,21 +474,21 @@ class AmazonS3 extends \OC\Files\Storage\Common { } else { $ext = ''; } - $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext); + $tmpFile = Server::get(ITempManager::class)->getTemporaryFile($ext); if ($this->file_exists($path)) { $source = $this->readObject($path); file_put_contents($tmpFile, $source); } $handle = fopen($tmpFile, $mode); - return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) { + return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void { $this->writeBack($tmpFile, $path); }); } return false; } - public function touch($path, $mtime = null) { + public function touch(string $path, ?int $mtime = null): bool { if (is_null($mtime)) { $mtime = time(); } @@ -538,20 +497,25 @@ class AmazonS3 extends \OC\Files\Storage\Common { ]; try { - if (!$this->file_exists($path)) { - $mimeType = $this->mimeDetector->detectPath($path); - $this->getConnection()->putObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path), - 'Metadata' => $metadata, - 'Body' => '', - 'ContentType' => $mimeType, - 'MetadataDirective' => 'REPLACE', - ]); - $this->testTimeout(); + if ($this->file_exists($path)) { + return false; } + + $mimeType = $this->mimeDetector->detectPath($path); + $this->getConnection()->putObject([ + 'Bucket' => $this->bucket, + 'Key' => $this->cleanKey($path), + 'Metadata' => $metadata, + 'Body' => '', + 'ContentType' => $mimeType, + 'MetadataDirective' => 'REPLACE', + ] + $this->getSSECParameters()); + $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } @@ -559,65 +523,69 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function copy($path1, $path2, $isFile = null) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function copy(string $source, string $target, ?bool $isFile = null): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - if ($isFile === true || $this->is_file($path1)) { + if ($isFile === true || $this->is_file($source)) { try { - $this->getConnection()->copyObject([ - 'Bucket' => $this->bucket, - 'Key' => $this->cleanKey($path2), - 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1) + $this->copyObject($source, $target, [ + 'StorageClass' => $this->storageClass, ]); $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } else { - $this->remove($path2); + $this->remove($target); try { - $this->mkdir($path2); + $this->mkdir($target); $this->testTimeout(); } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } - foreach ($this->getDirectoryContent($path1) as $item) { - $source = $path1 . '/' . $item['name']; - $target = $path2 . '/' . $item['name']; - $this->copy($source, $target, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER); + foreach ($this->getDirectoryContent($source) as $item) { + $childSource = $source . '/' . $item['name']; + $childTarget = $target . '/' . $item['name']; + $this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER); } } - $this->invalidateCache($path2); + $this->invalidateCache($target); return true; } - public function rename($path1, $path2) { - $path1 = $this->normalizePath($path1); - $path2 = $this->normalizePath($path2); + public function rename(string $source, string $target): bool { + $source = $this->normalizePath($source); + $target = $this->normalizePath($target); - if ($this->is_file($path1)) { - if ($this->copy($path1, $path2) === false) { + if ($this->is_file($source)) { + if ($this->copy($source, $target) === false) { return false; } - if ($this->unlink($path1) === false) { - $this->unlink($path2); + if ($this->unlink($source) === false) { + $this->unlink($target); return false; } } else { - if ($this->copy($path1, $path2) === false) { + if ($this->copy($source, $target) === false) { return false; } - if ($this->rmdir($path1) === false) { - $this->rmdir($path2); + if ($this->rmdir($source) === false) { + $this->rmdir($target); return false; } } @@ -625,18 +593,18 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } - public function test() { + public function test(): bool { $this->getConnection()->headBucket([ 'Bucket' => $this->bucket ]); return true; } - public function getId() { + public function getId(): string { return $this->id; } - public function writeBack($tmpFile, $path) { + public function writeBack(string $tmpFile, string $path): bool { try { $source = fopen($tmpFile, 'r'); $this->writeObject($path, $source, $this->mimeDetector->detectPath($path)); @@ -645,7 +613,10 @@ class AmazonS3 extends \OC\Files\Storage\Common { unlink($tmpFile); return true; } catch (S3Exception $e) { - \OC::$server->getLogger()->logException($e, ['app' => 'files_external']); + $this->logger->error($e->getMessage(), [ + 'app' => 'files_external', + 'exception' => $e, + ]); return false; } } @@ -653,11 +624,11 @@ class AmazonS3 extends \OC\Files\Storage\Common { /** * check if curl is installed */ - public static function checkDependencies() { + public static function checkDependencies(): bool { return true; } - public function getDirectoryContent($directory): \Traversable { + public function getDirectoryContent(string $directory): \Traversable { $path = $this->normalizePath($directory); if ($this->isRoot($path)) { @@ -699,7 +670,7 @@ class AmazonS3 extends \OC\Files\Storage\Common { 'mimetype' => $this->mimeDetector->detectPath($object['Key']), 'mtime' => strtotime($object['LastModified']), 'storage_mtime' => strtotime($object['LastModified']), - 'etag' => $object['ETag'], + 'etag' => trim($object['ETag'], '"'), 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'size' => (int)($object['Size'] ?? $object['ContentLength']), ]; @@ -747,14 +718,14 @@ class AmazonS3 extends \OC\Files\Storage\Common { return $result->get('Status') === 'Enabled'; } catch (S3Exception $s3Exception) { // This is needed for compatibility with Storj gateway which does not support versioning yet - if ($s3Exception->getAwsErrorCode() === 'NotImplemented') { + if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') { return false; } throw $s3Exception; } } - public function hasUpdated($path, $time) { + public function hasUpdated(string $path, int $time): bool { // for files we can get the proper mtime if ($path !== '' && $object = $this->headObject($path)) { $stat = $this->objectToMetaData($object); @@ -766,4 +737,24 @@ class AmazonS3 extends \OC\Files\Storage\Common { return true; } } + + public function writeStream(string $path, $stream, ?int $size = null): int { + if ($size === null) { + $size = 0; + // track the number of bytes read from the input stream to return as the number of written bytes. + $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void { + $size = $writtenSize; + }); + } + + if (!is_resource($stream)) { + throw new \InvalidArgumentException('Invalid stream provided'); + } + + $path = $this->normalizePath($path); + $this->writeObject($path, $stream, $this->mimeDetector->detectPath($path)); + $this->invalidateCache($path); + + return $size; + } } |