mirror of
https://github.com/nextcloud/server.git
synced 2026-06-13 18:50:47 -04:00
Merge pull request #53621 from nextcloud/feature/53428-autoCreateCollectionOnUpload
Feature/53428 auto create collection on upload
This commit is contained in:
commit
08b58a2225
5 changed files with 207 additions and 0 deletions
|
|
@ -395,6 +395,7 @@ return array(
|
|||
'OCA\\DAV\\Upload\\FutureFile' => $baseDir . '/../lib/Upload/FutureFile.php',
|
||||
'OCA\\DAV\\Upload\\PartFile' => $baseDir . '/../lib/Upload/PartFile.php',
|
||||
'OCA\\DAV\\Upload\\RootCollection' => $baseDir . '/../lib/Upload/RootCollection.php',
|
||||
'OCA\\DAV\\Upload\\UploadAutoMkcolPlugin' => $baseDir . '/../lib/Upload/UploadAutoMkcolPlugin.php',
|
||||
'OCA\\DAV\\Upload\\UploadFile' => $baseDir . '/../lib/Upload/UploadFile.php',
|
||||
'OCA\\DAV\\Upload\\UploadFolder' => $baseDir . '/../lib/Upload/UploadFolder.php',
|
||||
'OCA\\DAV\\Upload\\UploadHome' => $baseDir . '/../lib/Upload/UploadHome.php',
|
||||
|
|
|
|||
|
|
@ -410,6 +410,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\Upload\\FutureFile' => __DIR__ . '/..' . '/../lib/Upload/FutureFile.php',
|
||||
'OCA\\DAV\\Upload\\PartFile' => __DIR__ . '/..' . '/../lib/Upload/PartFile.php',
|
||||
'OCA\\DAV\\Upload\\RootCollection' => __DIR__ . '/..' . '/../lib/Upload/RootCollection.php',
|
||||
'OCA\\DAV\\Upload\\UploadAutoMkcolPlugin' => __DIR__ . '/..' . '/../lib/Upload/UploadAutoMkcolPlugin.php',
|
||||
'OCA\\DAV\\Upload\\UploadFile' => __DIR__ . '/..' . '/../lib/Upload/UploadFile.php',
|
||||
'OCA\\DAV\\Upload\\UploadFolder' => __DIR__ . '/..' . '/../lib/Upload/UploadFolder.php',
|
||||
'OCA\\DAV\\Upload\\UploadHome' => __DIR__ . '/..' . '/../lib/Upload/UploadHome.php',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin;
|
|||
use OCA\DAV\SystemTag\SystemTagPlugin;
|
||||
use OCA\DAV\Upload\ChunkingPlugin;
|
||||
use OCA\DAV\Upload\ChunkingV2Plugin;
|
||||
use OCA\DAV\Upload\UploadAutoMkcolPlugin;
|
||||
use OCA\Theming\ThemingDefaults;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\App\IAppManager;
|
||||
|
|
@ -232,6 +233,7 @@ class Server {
|
|||
|
||||
$this->server->addPlugin(new CopyEtagHeaderPlugin());
|
||||
$this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class)));
|
||||
$this->server->addPlugin(new UploadAutoMkcolPlugin());
|
||||
$this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class)));
|
||||
$this->server->addPlugin(new ChunkingPlugin());
|
||||
$this->server->addPlugin(new ZipFolderPlugin(
|
||||
|
|
|
|||
68
apps/dav/lib/Upload/UploadAutoMkcolPlugin.php
Normal file
68
apps/dav/lib/Upload/UploadAutoMkcolPlugin.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Upload;
|
||||
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\ICollection;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\ServerPlugin;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
use function Sabre\Uri\split as uriSplit;
|
||||
|
||||
/**
|
||||
* Class that allows automatically creating non-existing collections on file
|
||||
* upload.
|
||||
*
|
||||
* Since this functionality is not WebDAV compliant, it needs a special
|
||||
* header to be activated.
|
||||
*/
|
||||
class UploadAutoMkcolPlugin extends ServerPlugin {
|
||||
|
||||
private Server $server;
|
||||
|
||||
public function initialize(Server $server): void {
|
||||
$server->on('beforeMethod:PUT', [$this, 'beforeMethod']);
|
||||
$this->server = $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotFound a node expected to exist cannot be found
|
||||
*/
|
||||
public function beforeMethod(RequestInterface $request, ResponseInterface $response): bool {
|
||||
if ($request->getHeader('X-NC-WebDAV-Auto-Mkcol') !== '1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
[$path,] = uriSplit($request->getPath());
|
||||
|
||||
if ($this->server->tree->nodeExists($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
$rootPath = array_shift($parts);
|
||||
$node = $this->server->tree->getNodeForPath('/' . $rootPath);
|
||||
|
||||
if (!($node instanceof ICollection)) {
|
||||
// the root node is not a collection, let SabreDAV handle it
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (!$node->childExists($part)) {
|
||||
$node->createDirectory($part);
|
||||
}
|
||||
|
||||
$node = $node->getChild($part);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
135
apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
Normal file
135
apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\Upload;
|
||||
|
||||
use Generator;
|
||||
use OCA\DAV\Upload\UploadAutoMkcolPlugin;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\DAV\ICollection;
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\Tree;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
class UploadAutoMkcolPluginTest extends TestCase {
|
||||
|
||||
private Tree&MockObject $tree;
|
||||
private RequestInterface&MockObject $request;
|
||||
private ResponseInterface&MockObject $response;
|
||||
|
||||
public static function dataMissingHeaderShouldReturnTrue(): Generator {
|
||||
yield 'missing X-NC-WebDAV-Auto-Mkcol header' => [null];
|
||||
yield 'empty X-NC-WebDAV-Auto-Mkcol header' => [''];
|
||||
yield 'invalid X-NC-WebDAV-Auto-Mkcol header' => ['enable'];
|
||||
}
|
||||
|
||||
public function testBeforeMethodWithRootNodeNotAnICollectionShouldReturnTrue(): void {
|
||||
$this->request->method('getHeader')->willReturn('1');
|
||||
$this->request->expects(self::once())
|
||||
->method('getPath')
|
||||
->willReturn('/non-relevant/path.txt');
|
||||
$this->tree->expects(self::once())
|
||||
->method('nodeExists')
|
||||
->with('/non-relevant')
|
||||
->willReturn(false);
|
||||
|
||||
$mockNode = $this->getMockBuilder(INode::class);
|
||||
$this->tree->expects(self::once())
|
||||
->method('getNodeForPath')
|
||||
->willReturn($mockNode);
|
||||
|
||||
$return = $this->plugin->beforeMethod($this->request, $this->response);
|
||||
$this->assertTrue($return);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataMissingHeaderShouldReturnTrue
|
||||
*/
|
||||
public function testBeforeMethodWithMissingHeaderShouldReturnTrue(?string $header): void {
|
||||
$this->request->expects(self::once())
|
||||
->method('getHeader')
|
||||
->with('X-NC-WebDAV-Auto-Mkcol')
|
||||
->willReturn($header);
|
||||
|
||||
$this->request->expects(self::never())
|
||||
->method('getPath');
|
||||
|
||||
$return = $this->plugin->beforeMethod($this->request, $this->response);
|
||||
self::assertTrue($return);
|
||||
}
|
||||
|
||||
public function testBeforeMethodWithExistingPathShouldReturnTrue(): void {
|
||||
$this->request->method('getHeader')->willReturn('1');
|
||||
$this->request->expects(self::once())
|
||||
->method('getPath')
|
||||
->willReturn('/files/user/deep/image.jpg');
|
||||
$this->tree->expects(self::once())
|
||||
->method('nodeExists')
|
||||
->with('/files/user/deep')
|
||||
->willReturn(true);
|
||||
|
||||
$this->tree->expects(self::never())
|
||||
->method('getNodeForPath');
|
||||
|
||||
$return = $this->plugin->beforeMethod($this->request, $this->response);
|
||||
self::assertTrue($return);
|
||||
}
|
||||
|
||||
public function testBeforeMethodShouldSucceed(): void {
|
||||
$this->request->method('getHeader')->willReturn('1');
|
||||
$this->request->expects(self::once())
|
||||
->method('getPath')
|
||||
->willReturn('/files/user/my/deep/path/image.jpg');
|
||||
$this->tree->expects(self::once())
|
||||
->method('nodeExists')
|
||||
->with('/files/user/my/deep/path')
|
||||
->willReturn(false);
|
||||
|
||||
$mockNode = $this->createMock(ICollection::class);
|
||||
$this->tree->expects(self::once())
|
||||
->method('getNodeForPath')
|
||||
->with('/files')
|
||||
->willReturn($mockNode);
|
||||
$mockNode->expects(self::exactly(4))
|
||||
->method('childExists')
|
||||
->willReturnMap([
|
||||
['user', true],
|
||||
['my', true],
|
||||
['deep', false],
|
||||
['path', false],
|
||||
]);
|
||||
$mockNode->expects(self::exactly(2))
|
||||
->method('createDirectory');
|
||||
$mockNode->expects(self::exactly(4))
|
||||
->method('getChild')
|
||||
->willReturn($mockNode);
|
||||
|
||||
$return = $this->plugin->beforeMethod($this->request, $this->response);
|
||||
self::assertTrue($return);
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$server = $this->createMock(Server::class);
|
||||
$this->tree = $this->createMock(Tree::class);
|
||||
|
||||
$server->tree = $this->tree;
|
||||
$this->plugin = new UploadAutoMkcolPlugin();
|
||||
|
||||
$this->request = $this->createMock(RequestInterface::class);
|
||||
$this->response = $this->createMock(ResponseInterface::class);
|
||||
$server->httpRequest = $this->request;
|
||||
$server->httpResponse = $this->response;
|
||||
|
||||
$this->plugin->initialize($server);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue