Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Address review comments #37150

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ public function __construct(IRequest $request, string $baseUri) {

$this->server->addPlugin(new CopyEtagHeaderPlugin());
$this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class)));
$this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class)));
$this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class), \OCP\Server::get(LoggerInterface::class)));
$this->server->addPlugin(new ChunkingPlugin());

// allow setup of additional plugins
Expand Down
59 changes: 33 additions & 26 deletions apps/dav/lib/Upload/ChunkingV2Plugin.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
Expand All @@ -27,25 +28,23 @@
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\Lock\ILockingProvider;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\InsufficientStorage;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\PreconditionFailed;
use Sabre\DAV\ICollection;
use Sabre\DAV\INode;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\Uri;

class ChunkingV2Plugin extends ServerPlugin {
/** @var Server */
private $server;
/** @var UploadFolder */
private $uploadFolder;
/** @var ICache */
private $cache;
private ?Server $server = null;
private ?UploadFolder $uploadFolder;

Check notice

Code scanning / Psalm

PropertyNotSetInConstructor Note

Property OCA\DAV\Upload\ChunkingV2Plugin::$uploadFolder is not defined in constructor of OCA\DAV\Upload\ChunkingV2Plugin or in any private or final methods called in the constructor
private ICache $cache;

private ?string $uploadId = null;
private ?string $uploadPath = null;
Expand All @@ -59,14 +58,14 @@

private const DESTINATION_HEADER = 'Destination';

public function __construct(ICacheFactory $cacheFactory) {
public function __construct(ICacheFactory $cacheFactory, private LoggerInterface $logger) {
$this->cache = $cacheFactory->createDistributed(self::CACHE_KEY);
}

/**
* @inheritdoc
*/
public function initialize(Server $server) {
public function initialize(Server $server): void {
$server->on('afterMethod:MKCOL', [$this, 'afterMkcol']);
$server->on('beforeMethod:PUT', [$this, 'beforePut']);
$server->on('beforeMethod:DELETE', [$this, 'beforeDelete']);
Expand All @@ -75,12 +74,7 @@
$this->server = $server;
}

/**
* @param string $path
* @param bool $createIfNotExists
* @return FutureFile|UploadFile|ICollection|INode
*/
private function getUploadFile(string $path, bool $createIfNotExists = false) {
private function getUploadFile(string $path, bool $createIfNotExists = false): UploadFile|File {
try {
$actualFile = $this->server->tree->getNodeForPath($path);
// Only directly upload to the target file if it is on the same storage
Expand Down Expand Up @@ -109,6 +103,7 @@
$this->prepareUpload($request->getPath());
$this->checkPrerequisites(false);
} catch (BadRequest|StorageInvalidException|NotFound $e) {
$this->logger->debug('Preconditions for chunking v2 not matched, fallback to v1', ['exception' => $e]);
return true;
}

Expand All @@ -133,12 +128,14 @@
$this->prepareUpload(dirname($request->getPath()));
$this->checkPrerequisites();
} catch (StorageInvalidException|BadRequest|NotFound $e) {
$this->logger->debug('Preconditions for chunking v2 not matched, fallback to v1', ['exception' => $e]);
return true;
}

[$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);

$chunkName = basename($request->getPath());
// S3 Multipart upload allows up to 10000 chunks at maximum and required them to be numeric
$partId = is_numeric($chunkName) ? (int)$chunkName : -1;
if (!($partId >= 1 && $partId <= 10000)) {
throw new BadRequest('Invalid chunk name, must be numeric between 1 and 10000');
Expand All @@ -151,7 +148,7 @@
if ($this->uploadFolder->childExists(self::TEMP_TARGET) && $this->uploadPath) {
/** @var UploadFile $tempTargetFile */
$tempTargetFile = $this->uploadFolder->getChild(self::TEMP_TARGET);
[$destinationDir, $destinationName] = Uri\split($this->uploadPath);
[$destinationDir, ] = Uri\split($this->uploadPath);
/** @var Directory $destinationParent */
$destinationParent = $this->server->tree->getNodeForPath($destinationDir);
$free = $destinationParent->getNode()->getFreeSpace();
Expand All @@ -173,14 +170,15 @@
return false;
}

public function beforeMove($sourcePath, $destination): bool {
public function beforeMove(string $sourcePath, string $destination): bool {
try {
$this->prepareUpload(dirname($sourcePath));
$this->checkPrerequisites();
} catch (StorageInvalidException|BadRequest|NotFound|PreconditionFailed $e) {
} catch (StorageInvalidException|BadRequest|NotFound $e) {
$this->logger->debug('Preconditions for chunking v2 not matched, fallback to v1', ['exception' => $e]);
return true;
}
[$storage, $storagePath] = $this->getUploadStorage($this->uploadPath);
[$storage, ] = $this->getUploadStorage($this->uploadPath);

Check notice

Code scanning / Psalm

PossiblyNullArgument Note

Argument 1 of OCA\DAV\Upload\ChunkingV2Plugin::getUploadStorage cannot be null, possibly null value provided

$targetFile = $this->getUploadFile($this->uploadPath);

Expand Down Expand Up @@ -239,11 +237,12 @@
return false;
}

public function beforeDelete(RequestInterface $request, ResponseInterface $response) {
public function beforeDelete(RequestInterface $request, ResponseInterface $response): bool {
try {
$this->prepareUpload(dirname($request->getPath()));
$this->checkPrerequisites();
} catch (StorageInvalidException|BadRequest|NotFound $e) {
$this->logger->debug('Preconditions for chunking v2 not matched, fallback to v1', ['exception' => $e]);
return true;
}

Expand All @@ -256,6 +255,8 @@
* @throws BadRequest
* @throws PreconditionFailed
* @throws StorageInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function checkPrerequisites(bool $checkUploadMetadata = true): void {
$distributedCacheConfig = \OCP\Server::get(IConfig::class)->getSystemValue('memcache.distributed', null);
Expand All @@ -266,10 +267,12 @@
if (!$this->uploadFolder instanceof UploadFolder || empty($this->server->httpRequest->getHeader(self::DESTINATION_HEADER))) {
throw new BadRequest('Skipping chunked file writing as the destination header was not passed');
}
if (!$this->uploadFolder->getStorage()->instanceOfStorage(IChunkedFileWrite::class)) {
/** @var ObjectStoreStorage $storage */
$storage = $this->uploadFolder->getStorage();
if (!$storage->instanceOfStorage(IChunkedFileWrite::class)) {
throw new StorageInvalidException('Storage does not support chunked file writing');
}
if ($this->uploadFolder->getStorage()->instanceOfStorage(ObjectStoreStorage::class) && !$this->uploadFolder->getStorage()->getObjectStore() instanceof IObjectStoreMultiPartUpload) {
if ($storage->instanceOfStorage(ObjectStoreStorage::class) && !$storage->getObjectStore() instanceof IObjectStoreMultiPartUpload) {
throw new StorageInvalidException('Storage does not support multi part uploads');
}

Expand Down Expand Up @@ -300,8 +303,12 @@
/**
* @throws NotFound
*/
public function prepareUpload($path): void {
$this->uploadFolder = $this->server->tree->getNodeForPath($path);
public function prepareUpload(string $path): void {
$uploadFolder = $this->server->tree->getNodeForPath($path);

Check notice

Code scanning / Psalm

PossiblyNullPropertyFetch Note

Cannot get property on possibly null variable $this->server of type Sabre\DAV\Server|null

Check notice

Code scanning / Psalm

PossiblyNullReference Note

Cannot call method getNodeForPath on possibly null value
if (!$uploadFolder instanceof UploadFolder) {
throw new \RuntimeException('Unexpected node returned, should be the upload folder');
}
$this->uploadFolder = $uploadFolder;
$uploadMetadata = $this->cache->get($this->uploadFolder->getName());
$this->uploadId = $uploadMetadata[self::UPLOAD_ID] ?? null;
$this->uploadPath = $uploadMetadata[self::UPLOAD_TARGET_PATH] ?? null;
Expand Down Expand Up @@ -329,7 +336,7 @@
try {
$uploadFileAbsolutePath = $uploadFile->getFileInfo()->getPath();
if ($uploadFileAbsolutePath !== $targetAbsolutePath) {
$uploadFile = $rootFolder->get($uploadFile->getFileInfo()->getPath());
$uploadFile = $rootFolder->get($uploadFile->getPath());
if ($exists) {
$uploadFile->copy($targetAbsolutePath);
} else {
Expand Down
2 changes: 1 addition & 1 deletion apps/dav/lib/Upload/FutureFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function get() {
return AssemblyStream::wrap($nodes);
}

public function getPath() {
public function getPath(): string {
return $this->root->getFileInfo()->getInternalPath() . '/.file';
}

Expand Down
11 changes: 6 additions & 5 deletions apps/dav/lib/Upload/PartFile.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/

namespace OCA\DAV\Upload;

use OCA\DAV\Connector\Sabre\Directory;
Expand All @@ -16,10 +19,8 @@
* but handled directly by external storage services like S3 with Multipart Upload
*/
class PartFile implements IFile {
/** @var Directory */
private $root;
/** @var array */
private $partInfo;
private Directory $root;
private array $partInfo;

public function __construct(Directory $root, array $partInfo) {
$this->root = $root;
Expand All @@ -40,7 +41,7 @@ public function get() {
throw new Forbidden('Permission denied to get this file');
}

public function getPath() {
public function getPath(): string {
return $this->root->getFileInfo()->getInternalPath() . '/' . $this->partInfo['PartNumber'];
}

Expand Down
4 changes: 2 additions & 2 deletions apps/dav/lib/Upload/UploadFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function get() {
return $this->file->get();
}

public function getId() {
public function getId(): int {
return $this->file->getId();
}

Expand Down Expand Up @@ -71,7 +71,7 @@ public function getFile(): File {
return $this->file;
}

public function getNode() {
public function getNode(): \OCP\Files\File {
return $this->file->getNode();
}
}
14 changes: 6 additions & 8 deletions apps/dav/lib/Upload/UploadFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
use OCP\Files\Storage\IStorage;
use OCP\ICacheFactory;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;

class UploadFolder implements ICollection {
/** @var Directory */
private $node;
/** @var CleanupService */
private $cleanupService;
/** @var IStorage */
private $storage;
private Directory $node;
private CleanupService $cleanupService;
private IStorage $storage;

public function __construct(Directory $node, CleanupService $cleanupService, IStorage $storage) {
$this->node = $node;
Expand Down Expand Up @@ -66,7 +64,7 @@ public function getChildren() {
/** @var ObjectStoreStorage $storage */
$objectStore = $this->storage->getObjectStore();
if ($objectStore instanceof IObjectStoreMultiPartUpload) {
$cache = \OC::$server->getMemCacheFactory()->createDistributed(ChunkingV2Plugin::CACHE_KEY);
$cache = \OCP\Server::get(ICacheFactory::class)->createDistributed(ChunkingV2Plugin::CACHE_KEY);
$uploadSession = $cache->get($this->getName());
if ($uploadSession) {
$uploadId = $uploadSession[ChunkingV2Plugin::UPLOAD_ID];
Expand Down Expand Up @@ -108,7 +106,7 @@ public function getLastModified() {
return $this->node->getLastModified();
}

public function getStorage() {
public function getStorage(): IStorage {
return $this->storage;
}
}
7 changes: 4 additions & 3 deletions apps/dav/lib/Upload/UploadHome.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use OC\Files\Filesystem;
use OC\Files\View;
use OCA\DAV\Connector\Sabre\Directory;
use OCP\Files\Storage\IStorage;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ICollection;

Expand Down Expand Up @@ -69,13 +70,13 @@ public function getLastModified() {
/**
* @return Directory
*/
private function impl() {
private function impl(): Directory {
$view = $this->getView();
$rootInfo = $view->getFileInfo('');
return new Directory($view, $rootInfo);
}

private function getView() {
private function getView(): View {
$rootView = new View();
$user = \OC::$server->getUserSession()->getUser();
Filesystem::initMountPoints($user->getUID());
Expand All @@ -85,7 +86,7 @@ private function getView() {
return new View('/' . $user->getUID() . '/uploads');
}

private function getStorage() {
private function getStorage(): IStorage {
$view = $this->getView();
$storage = $view->getFileInfo('')->getStorage();
return $storage;
Expand Down
9 changes: 5 additions & 4 deletions lib/private/Files/ObjectStore/ObjectStoreStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -661,10 +661,6 @@ public function startChunkedWrite(string $targetPath): string {
return $this->objectStore->initiateMultipartUpload($urn);
}

/**
*
* @throws GenericFileException
*/
public function putChunkedWritePart(
string $targetPath,
string $writeToken,
Expand All @@ -675,6 +671,11 @@ public function putChunkedWritePart(
if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
throw new GenericFileException('Object store does not support multipart upload');
}

if (!is_numeric($chunkId)) {
throw new GenericFileException('Chunk ID must be numeric for S3 multipart upload');
}

$cacheEntry = $this->getCache()->get($targetPath);
$urn = $this->getURN($cacheEntry->getId());

Expand Down
10 changes: 10 additions & 0 deletions lib/public/Files/ObjectStore/IObjectStoreMultiPartUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\Files\ObjectStore;

use Aws\Result;
Expand Down Expand Up @@ -35,6 +36,15 @@ public function abortMultipartUpload(string $urn, string $uploadId): void;

/**
* @since 26.0.0
* @return array of the parts in ListParts response
*
* Sample data:
* [[
* 'PartNumber' => 1,
* 'LastModified' => '2010-11-10T20:48:34.000Z',
* 'ETag' => '"7778aef83f66abc1fa1e8477f296d394"',
* 'Size' => 10485760,
* ]]
*/
public function getMultipartUploads(string $urn, string $uploadId): array;
}
5 changes: 4 additions & 1 deletion lib/public/Files/Storage/IChunkedFileWrite.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);


namespace OCP\Files\Storage;
Expand All @@ -29,6 +31,7 @@ public function startChunkedWrite(string $targetPath): string;
* @param string $chunkId
* @param resource $data
* @param int|null $size
* @return array|null
* @throws GenericFileException
* @since 26.0.0
*/
Expand Down
Loading