diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index d2a71eb3e7b8a..e6f92cd5319a3 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -18,6 +18,7 @@ use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; use OCP\App\IAppManager; +use OCP\Constants; use OCP\Encryption\Exceptions\GenericEncryptionException; use OCP\Files; use OCP\Files\EntityTooLargeException; @@ -539,18 +540,24 @@ public function getContentType() { } /** - * @return array|bool + * @throws NotFoundException + * @throws NotPermittedException */ - public function getDirectDownload() { + public function getDirectDownload(): array|false { if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) { - return []; + return false; } - [$storage, $internalPath] = $this->fileView->resolvePath($this->path); - if (is_null($storage)) { - return []; + $node = $this->getNode(); + $storage = $node->getStorage(); + if (!$storage) { + return false; + } + + if (!($node->getPermissions() & Constants::PERMISSION_READ)) { + return false; } - return $storage->getDirectDownload($internalPath); + return $storage->getDirectDownloadById((string)$node->getId()); } /** diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index eed57ee0be8eb..7b2f144dfa1d8 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -50,6 +50,7 @@ class FilesPlugin extends ServerPlugin { public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions'; public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes'; public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; + public const DOWNLOADURL_EXPIRATION_PROPERTYNAME = '{http://nextcloud.org/ns}download-url-expiration'; public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; public const GETETAG_PROPERTYNAME = '{DAV:}getetag'; public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; @@ -120,6 +121,7 @@ public function initialize(Server $server) { $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME; $server->protectedProperties[] = self::SIZE_PROPERTYNAME; $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; + $server->protectedProperties[] = self::DOWNLOADURL_EXPIRATION_PROPERTYNAME; $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; @@ -471,19 +473,30 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) } if ($node instanceof File) { - $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node) { + $requestProperties = $propFind->getRequestedProperties(); + + if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true) + || in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) { try { $directDownloadUrl = $node->getDirectDownload(); - if (isset($directDownloadUrl['url'])) { + } catch (StorageNotAvailableException|ForbiddenException) { + $directDownloadUrl = null; + } + + $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) { + if ($directDownloadUrl && isset($directDownloadUrl['url'])) { return $directDownloadUrl['url']; } - } catch (StorageNotAvailableException $e) { return false; - } catch (ForbiddenException $e) { + }); + + $propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) { + if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) { + return $directDownloadUrl['expiration']; + } return false; - } - return false; - }); + }); + } $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) { $checksum = $node->getChecksum(); diff --git a/apps/files_sharing/lib/SharedStorage.php b/apps/files_sharing/lib/SharedStorage.php index eb63f93b07dfb..aeaaa54f3708d 100644 --- a/apps/files_sharing/lib/SharedStorage.php +++ b/apps/files_sharing/lib/SharedStorage.php @@ -40,6 +40,7 @@ use OCP\Server; use OCP\Share\IShare; use OCP\Util; +use Override; use Psr\Log\LoggerInterface; /** @@ -558,8 +559,15 @@ public function getUnjailedPath(string $path): string { return parent::getUnjailedPath($path); } + #[Override] public function getDirectDownload(string $path): array|false { // disable direct download for shares - return []; + return false; + } + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + // disable direct download for shares + return false; } } diff --git a/lib/private/Files/ObjectStore/Azure.php b/lib/private/Files/ObjectStore/Azure.php index 2729bb3c0379f..4e7b0c51678fb 100644 --- a/lib/private/Files/ObjectStore/Azure.php +++ b/lib/private/Files/ObjectStore/Azure.php @@ -117,4 +117,8 @@ public function objectExists($urn) { public function copyObject($from, $to) { $this->getBlobClient()->copyBlob($this->containerName, $to, $this->containerName, $from); } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } } diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 8ce992e93c050..c151e661939ee 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -29,6 +29,7 @@ use OCP\Files\Storage\IStorage; use OCP\IDBConnection; use OCP\Server; +use Override; use Psr\Log\LoggerInterface; class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite { @@ -844,4 +845,22 @@ public function free_space(string $path): int|float|false { return $available; } + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + $expiration = new \DateTimeImmutable('+60 minutes'); + $url = $this->objectStore->preSignedUrl($this->getURN((int)$fileId), $expiration); + return $url ? ['url' => $url, 'expiration' => $expiration->getTimestamp()] : false; + } + + #[Override] + public function getDirectDownload(string $path): array|false { + $path = $this->normalizePath($path); + $cacheEntry = $this->getCache()->get($path); + + if (!$cacheEntry || $cacheEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) { + return false; + } + return $this->getDirectDownloadById((string)$cacheEntry->getId()); + } } diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index 48fa8efdec384..360b92e76636b 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -31,8 +31,8 @@ trait S3ConnectionTrait { protected bool $test; protected ?S3Client $connection = null; - private ?ICache $existingBucketsCache = null; + private bool $usePresignedUrl = false; protected function parseParams($params) { if (empty($params['bucket'])) { @@ -109,12 +109,15 @@ public function getConnection() { ) ); + $this->usePresignedUrl = $this->params['use_presigned_url'] ?? false; + $options = [ 'version' => $this->params['version'] ?? 'latest', 'credentials' => $provider, 'endpoint' => $base_url, 'region' => $this->params['region'], 'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false, + 'proxy' => isset($this->params['proxy']) ? $this->params['proxy'] : false, 'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()), 'csm' => false, 'use_arn_region' => false, @@ -291,4 +294,8 @@ protected function getSSECParameters(bool $copy = false): array { 'SSECustomerKeyMD5' => md5($rawKey, true) ]; } + + public function isUsePresignedUrl(): bool { + return $this->usePresignedUrl; + } } diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index f47a01dab18b3..91eff90babb88 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -7,6 +7,7 @@ namespace OC\Files\ObjectStore; use Aws\Command; +use Aws\Exception\AwsException; use Aws\Exception\MultipartUploadException; use Aws\S3\Exception\S3MultipartUploadException; use Aws\S3\MultipartCopy; @@ -295,4 +296,23 @@ public function copyObject($from, $to, array $options = []) { ], $options)); } } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + $command = $this->getConnection()->getCommand('GetObject', [ + 'Bucket' => $this->getBucket(), + 'Key' => $urn, + ]); + + if (!$this->isUsePresignedUrl()) { + return null; + } + + try { + return (string)$this->getConnection()->createPresignedRequest($command, $expiration, [ + 'signPayload' => true, + ])->getUri(); + } catch (AwsException) { + return null; + } + } } diff --git a/lib/private/Files/ObjectStore/StorageObjectStore.php b/lib/private/Files/ObjectStore/StorageObjectStore.php index 888602a62e40d..c20efb346a17a 100644 --- a/lib/private/Files/ObjectStore/StorageObjectStore.php +++ b/lib/private/Files/ObjectStore/StorageObjectStore.php @@ -74,4 +74,8 @@ public function objectExists($urn) { public function copyObject($from, $to) { $this->storage->copy($from, $to); } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } } diff --git a/lib/private/Files/ObjectStore/Swift.php b/lib/private/Files/ObjectStore/Swift.php index aa8b3bb34ec2b..9006f29160d64 100644 --- a/lib/private/Files/ObjectStore/Swift.php +++ b/lib/private/Files/ObjectStore/Swift.php @@ -134,4 +134,7 @@ public function copyObject($from, $to) { 'destination' => $this->getContainer()->name . '/' . $to ]); } + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index e197c3c9d263e..1a2aeb0e0210f 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -38,6 +38,7 @@ use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; use OCP\Server; +use Override; use Psr\Log\LoggerInterface; /** @@ -445,13 +446,14 @@ public function instanceOfStorage(string $class): bool { return is_a($this, $class); } - /** - * A custom storage implementation can return an url for direct download of a give file. - * - * For now the returned array can hold the parameter url - in future more attributes might follow. - */ + #[Override] public function getDirectDownload(string $path): array|false { - return []; + return false; + } + + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + return false; } public function verifyPath(string $path, string $fileName): void { diff --git a/lib/private/Files/Storage/FailedStorage.php b/lib/private/Files/Storage/FailedStorage.php index a8288de48d032..1b1123921aa43 100644 --- a/lib/private/Files/Storage/FailedStorage.php +++ b/lib/private/Files/Storage/FailedStorage.php @@ -154,6 +154,10 @@ public function getDirectDownload(string $path): never { throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); } + public function getDirectDownloadById(string $fileId): never { + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); + } + public function verifyPath(string $path, string $fileName): void { } diff --git a/lib/private/Files/Storage/Wrapper/Availability.php b/lib/private/Files/Storage/Wrapper/Availability.php index dc2f9a2e6ac08..9d184e6a66069 100644 --- a/lib/private/Files/Storage/Wrapper/Availability.php +++ b/lib/private/Files/Storage/Wrapper/Availability.php @@ -228,6 +228,10 @@ public function getDirectDownload(string $path): array|false { return $this->handleAvailability('getDirectDownload', $path); } + public function getDirectDownloadById(string $fileId): array|false { + return $this->handleAvailability('getDirectDownloadById', $fileId); + } + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); } diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index 8d9381a87f7c5..d5454050fcb09 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -20,6 +20,7 @@ use OCP\Files\Storage\IWriteStreamStorage; use OCP\Lock\ILockingProvider; use OCP\Server; +use Override; use Psr\Log\LoggerInterface; class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStreamStorage { @@ -258,10 +259,16 @@ public function __call(string $method, array $args) { return call_user_func_array([$this->getWrapperStorage(), $method], $args); } + #[Override] public function getDirectDownload(string $path): array|false { return $this->getWrapperStorage()->getDirectDownload($path); } + #[Override] + public function getDirectDownloadById(string $fileId): array|false { + return $this->getWrapperStorage()->getDirectDownloadById($fileId); + } + public function getAvailability(): array { return $this->getWrapperStorage()->getAvailability(); } diff --git a/lib/private/Lockdown/Filesystem/NullStorage.php b/lib/private/Lockdown/Filesystem/NullStorage.php index fd952fae63785..d10763287c8a6 100644 --- a/lib/private/Lockdown/Filesystem/NullStorage.php +++ b/lib/private/Lockdown/Filesystem/NullStorage.php @@ -145,6 +145,10 @@ public function getDirectDownload(string $path): array|false { return false; } + public function getDirectDownloadById(string $fileId): array|false { + return false; + } + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never { throw new \OC\ForbiddenException('This request is not allowed to access the filesystem'); } diff --git a/lib/public/Files/ObjectStore/IObjectStore.php b/lib/public/Files/ObjectStore/IObjectStore.php index 35099ef8ba88f..2b33ed485ad9c 100644 --- a/lib/public/Files/ObjectStore/IObjectStore.php +++ b/lib/public/Files/ObjectStore/IObjectStore.php @@ -63,4 +63,10 @@ public function objectExists($urn); * @since 21.0.0 */ public function copyObject($from, $to); + + /** + * Get pre signed url for an object + * @since 33.0.0 + */ + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string; } diff --git a/lib/public/Files/Storage/IStorage.php b/lib/public/Files/Storage/IStorage.php index 5f6c8a0e8a0d5..f09f899d4f278 100644 --- a/lib/public/Files/Storage/IStorage.php +++ b/lib/public/Files/Storage/IStorage.php @@ -302,15 +302,28 @@ public function isLocal(); public function instanceOfStorage(string $class); /** - * A custom storage implementation can return an url for direct download of a give file. + * A custom storage implementation can return a url for direct download of a give file. * - * For now the returned array can hold the parameter url - in future more attributes might follow. + * For now the returned array can hold the parameter url and expiration - in future more attributes might follow. * - * @return array|false + * @param string $path Either the path or the fileId + * @return array{url: ?string, expiration: ?int}|false * @since 9.0.0 + * @deprecated Use IStorage::getDirectDownloadById instead. */ public function getDirectDownload(string $path); + /** + * A custom storage implementation can return a url for direct download of a give file. + * + * For now the returned array can hold the parameter url and expiration - in future more attributes might follow. + * + * @param string $fileId The fileId of the file. + * @return array{url: ?string, expiration: ?int}|false + * @since 33.0.0 + */ + public function getDirectDownloadById(string $fileId): array|false; + /** * @return void * @throws InvalidPathException diff --git a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php b/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php index ae0a53f2cc06e..a9200732a1857 100644 --- a/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php +++ b/tests/lib/Files/Mount/ObjectHomeMountProviderTest.php @@ -268,4 +268,8 @@ public function objectExists($urn) { public function copyObject($from, $to) { } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } } diff --git a/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php b/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php index 767125d42aa22..b842ec6dfd61f 100644 --- a/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php +++ b/tests/lib/Files/ObjectStore/FailDeleteObjectStore.php @@ -39,4 +39,8 @@ public function objectExists($urn) { public function copyObject($from, $to) { $this->objectStore->copyObject($from, $to); } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } } diff --git a/tests/lib/Files/ObjectStore/FailWriteObjectStore.php b/tests/lib/Files/ObjectStore/FailWriteObjectStore.php index 924bbdada4f44..a3cd1e8c92a16 100644 --- a/tests/lib/Files/ObjectStore/FailWriteObjectStore.php +++ b/tests/lib/Files/ObjectStore/FailWriteObjectStore.php @@ -40,4 +40,8 @@ public function objectExists($urn) { public function copyObject($from, $to) { $this->objectStore->copyObject($from, $to); } + + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { + return null; + } }