feat(files): automatically create directories on upload
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>pull/53621/head
parent
d00519d7ec
commit
8167b07118
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 New Issue