Skip to content

Commit 4f71915

Browse files
committed
First attempt to make multipartPart upload working
Signed-off-by: Julius Härtl <jus@bitgrid.net>
1 parent 903b99b commit 4f71915

18 files changed

+585
-9
lines changed

apps/dav/composer/composer/autoload_classmap.php

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@
260260
'OCA\\DAV\\Traits\\PrincipalProxyTrait' => $baseDir . '/../lib/Traits/PrincipalProxyTrait.php',
261261
'OCA\\DAV\\Upload\\AssemblyStream' => $baseDir . '/../lib/Upload/AssemblyStream.php',
262262
'OCA\\DAV\\Upload\\ChunkingPlugin' => $baseDir . '/../lib/Upload/ChunkingPlugin.php',
263+
'OCA\\DAV\\Upload\\ChunkingV2Plugin' => $baseDir . '/../lib/Upload/ChunkingV2Plugin.php',
263264
'OCA\\DAV\\Upload\\CleanupService' => $baseDir . '/../lib/Upload/CleanupService.php',
264265
'OCA\\DAV\\Upload\\FutureFile' => $baseDir . '/../lib/Upload/FutureFile.php',
265266
'OCA\\DAV\\Upload\\RootCollection' => $baseDir . '/../lib/Upload/RootCollection.php',

apps/dav/composer/composer/autoload_static.php

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ class ComposerStaticInitDAV
275275
'OCA\\DAV\\Traits\\PrincipalProxyTrait' => __DIR__ . '/..' . '/../lib/Traits/PrincipalProxyTrait.php',
276276
'OCA\\DAV\\Upload\\AssemblyStream' => __DIR__ . '/..' . '/../lib/Upload/AssemblyStream.php',
277277
'OCA\\DAV\\Upload\\ChunkingPlugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingPlugin.php',
278+
'OCA\\DAV\\Upload\\ChunkingV2Plugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingV2Plugin.php',
278279
'OCA\\DAV\\Upload\\CleanupService' => __DIR__ . '/..' . '/../lib/Upload/CleanupService.php',
279280
'OCA\\DAV\\Upload\\FutureFile' => __DIR__ . '/..' . '/../lib/Upload/FutureFile.php',
280281
'OCA\\DAV\\Upload\\RootCollection' => __DIR__ . '/..' . '/../lib/Upload/RootCollection.php',

apps/dav/lib/Connector/Sabre/Directory.php

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
3939
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
4040
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
41+
use OCA\DAV\Upload\FutureFile;
4142
use OCP\Files\FileInfo;
4243
use OCP\Files\ForbiddenException;
4344
use OCP\Files\InvalidPathException;

apps/dav/lib/Connector/Sabre/Node.php

+8
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ public function getInternalFileId() {
248248
return $this->info->getId();
249249
}
250250

251+
public function getInternalPath(): string {
252+
return $this->info->getInternalPath();
253+
}
254+
255+
public function getAbsoluteInternalPath(): string {
256+
return $this->info->getPath();
257+
}
258+
251259
/**
252260
* @param string $user
253261
* @return int

apps/dav/lib/Server.php

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin;
6767
use OCA\DAV\SystemTag\SystemTagPlugin;
6868
use OCA\DAV\Upload\ChunkingPlugin;
69+
use OCA\DAV\Upload\ChunkingV2Plugin;
6970
use OCP\EventDispatcher\IEventDispatcher;
7071
use OCP\IRequest;
7172
use OCP\SabrePluginEvent;
@@ -203,6 +204,7 @@ public function __construct(IRequest $request, $baseUri) {
203204
));
204205

205206
$this->server->addPlugin(new CopyEtagHeaderPlugin());
207+
$this->server->addPlugin(new ChunkingV2Plugin());
206208
$this->server->addPlugin(new ChunkingPlugin());
207209

208210
// allow setup of additional plugins
+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/*
5+
* @copyright Copyright (c) 2021 Julius Härtl <jus@bitgrid.net>
6+
*
7+
* @author Julius Härtl <jus@bitgrid.net>
8+
*
9+
* @license GNU AGPL version 3 or any later version
10+
*
11+
* This program is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Affero General Public License as
13+
* published by the Free Software Foundation, either version 3 of the
14+
* License, or (at your option) any later version.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
23+
*
24+
*/
25+
26+
namespace OCA\DAV\Upload;
27+
28+
use OC\Files\View;
29+
use OCA\DAV\Connector\Sabre\Directory;
30+
use OCP\Files\Storage\IChunkedFileWrite;
31+
use OCP\Files\Storage\IStorage;
32+
use OCP\Files\StorageInvalidException;
33+
use Sabre\DAV\Exception\BadRequest;
34+
use Sabre\DAV\Exception\NotFound;
35+
use Sabre\DAV\Server;
36+
use Sabre\DAV\ServerPlugin;
37+
use Sabre\HTTP\RequestInterface;
38+
use Sabre\HTTP\ResponseInterface;
39+
use Sabre\Uri;
40+
41+
class ChunkingV2Plugin extends ServerPlugin {
42+
43+
/** @var Server */
44+
private $server;
45+
/** @var UploadFolder */
46+
private $uploadFolder;
47+
48+
private const TEMP_TARGET = '.target';
49+
50+
private const OBJECT_UPLOAD_TARGET = '{http://nextcloud.org/ns}upload-target';
51+
private const OBJECT_UPLOAD_CHUNKTOKEN = '{http://nextcloud.org/ns}upload-chunktoken';
52+
53+
private const DESTINATION_HEADER = 'X-Chunking-Destination';
54+
55+
/**
56+
* @inheritdoc
57+
*/
58+
public function initialize(Server $server) {
59+
$server->on('afterMethod:MKCOL', [$this, 'beforeMkcol']);
60+
// 200 priority to call after the custom properties backend is registered
61+
$server->on('beforeMethod:PUT', [$this, 'beforePut'], 200);
62+
$server->on('beforeMethod:DELETE', [$this, 'beforeDelete'], 200);
63+
$server->on('beforeMove', [$this, 'beforeMove'], 90);
64+
65+
$this->server = $server;
66+
}
67+
68+
/**
69+
* @param string $path
70+
* @param bool $createIfNotExists
71+
* @return FutureFile|UploadFile|\Sabre\DAV\ICollection|\Sabre\DAV\INode
72+
*/
73+
private function getTargetFile(string $path, bool $createIfNotExists = false) {
74+
try {
75+
$targetFile = $this->server->tree->getNodeForPath($path);
76+
} catch (NotFound $e) {
77+
if ($createIfNotExists) {
78+
$this->uploadFolder->createFile(self::TEMP_TARGET);
79+
}
80+
$targetFile = $this->uploadFolder->getChild(self::TEMP_TARGET);
81+
}
82+
return $targetFile;
83+
}
84+
85+
public function beforeMkcol(RequestInterface $request, ResponseInterface $response): bool {
86+
$this->uploadFolder = $this->server->tree->getNodeForPath($request->getPath());
87+
try {
88+
$this->checkPrerequisites();
89+
$storage = $this->getStorage();
90+
} catch (StorageInvalidException | BadRequest $e) {
91+
return true;
92+
}
93+
94+
$targetPath = $this->server->httpRequest->getHeader(self::DESTINATION_HEADER);
95+
if (!$targetPath) {
96+
return true;
97+
}
98+
99+
$targetFile = $this->getTargetFile($targetPath, true);
100+
101+
$uploadId = $storage->beginChunkedFile($targetFile->getInternalPath());
102+
103+
// DAV properties on the UploadFolder are used in order to properly cleanup stale chunked file writes and to persist the target path
104+
$this->server->updateProperties($request->getPath(), [
105+
self::OBJECT_UPLOAD_CHUNKTOKEN => $uploadId,
106+
self::OBJECT_UPLOAD_TARGET => $targetPath,
107+
]);
108+
109+
$response->setStatus(201);
110+
return true;
111+
}
112+
113+
public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
114+
$this->uploadFolder = $this->server->tree->getNodeForPath(dirname($request->getPath()));
115+
try {
116+
$this->checkPrerequisites();
117+
$storage = $this->getStorage();
118+
} catch (StorageInvalidException | BadRequest $e) {
119+
return true;
120+
}
121+
122+
$properties = $this->server->getProperties(dirname($request->getPath()) . '/', [ self::OBJECT_UPLOAD_CHUNKTOKEN, self::OBJECT_UPLOAD_TARGET ]);
123+
$targetPath = $properties[self::OBJECT_UPLOAD_TARGET];
124+
$uploadId = $properties[self::OBJECT_UPLOAD_CHUNKTOKEN];
125+
$partId = (int)basename($request->getPath());
126+
127+
if (!($partId >= 1 && $partId <= 10000)) {
128+
throw new BadRequest('Invalid chunk id');
129+
}
130+
131+
$targetFile = $this->getTargetFile($targetPath);
132+
$stream = $request->getBodyAsStream();
133+
$storage->putChunkedFilePart($targetFile->getInternalPath(), $uploadId, (string)$partId, $stream, (int)$request->getHeader('Content-Length'));
134+
135+
$response->setStatus(201);
136+
return false;
137+
}
138+
139+
public function beforeMove($sourcePath, $destination): bool {
140+
$this->uploadFolder = $this->server->tree->getNodeForPath(dirname($sourcePath));
141+
try {
142+
$this->checkPrerequisites();
143+
$this->getStorage();
144+
} catch (StorageInvalidException | BadRequest $e) {
145+
return true;
146+
}
147+
$properties = $this->server->getProperties(dirname($sourcePath) . '/', [ self::OBJECT_UPLOAD_CHUNKTOKEN, self::OBJECT_UPLOAD_TARGET ]);
148+
$targetPath = $properties[self::OBJECT_UPLOAD_TARGET];
149+
$uploadId = $properties[self::OBJECT_UPLOAD_CHUNKTOKEN];
150+
151+
$targetFile = $this->getTargetFile($targetPath);
152+
153+
[$destinationDir, $destinationName] = Uri\split($destination);
154+
/** @var Directory $destinationParent */
155+
$destinationParent = $this->server->tree->getNodeForPath($destinationDir);
156+
$destinationExists = $destinationParent->childExists($destinationName);
157+
158+
$rootView = new View();
159+
$rootView->writeChunkedFile($targetFile->getAbsoluteInternalPath(), $uploadId);
160+
if (!$destinationExists) {
161+
$destinationInView = $destinationParent->getFileInfo()->getPath() . '/' . $destinationName;
162+
$rootView->rename($targetFile->getAbsoluteInternalPath(), $destinationInView);
163+
}
164+
165+
$sourceNode = $this->server->tree->getNodeForPath($sourcePath);
166+
if ($sourceNode instanceof FutureFile) {
167+
$sourceNode->delete();
168+
}
169+
170+
$this->server->emit('afterMove', [$sourcePath, $destination]);
171+
$this->server->emit('afterUnbind', [$sourcePath]);
172+
$this->server->emit('afterBind', [$destination]);
173+
174+
$response = $this->server->httpResponse;
175+
$response->setHeader('Content-Length', '0');
176+
$response->setStatus($destinationExists ? 204 : 201);
177+
return false;
178+
}
179+
180+
public function beforeDelete(RequestInterface $request, ResponseInterface $response) {
181+
$this->uploadFolder = $this->server->tree->getNodeForPath($request->getPath());
182+
try {
183+
if (!$this->uploadFolder instanceof UploadFolder) {
184+
return true;
185+
}
186+
$storage = $this->getStorage();
187+
} catch (StorageInvalidException | BadRequest $e) {
188+
return true;
189+
}
190+
191+
$properties = $this->server->getProperties($request->getPath() . '/', [ self::OBJECT_UPLOAD_CHUNKTOKEN, self::OBJECT_UPLOAD_TARGET ]);
192+
$targetPath = $properties[self::OBJECT_UPLOAD_TARGET];
193+
$uploadId = $properties[self::OBJECT_UPLOAD_CHUNKTOKEN];
194+
if (!$targetPath || !$uploadId) {
195+
return true;
196+
}
197+
$targetFile = $this->getTargetFile($targetPath);
198+
$storage->cancelChunkedFile($targetFile->getInternalPath(), $uploadId);
199+
return true;
200+
}
201+
202+
/** @throws BadRequest */
203+
private function checkPrerequisites(): void {
204+
if (!$this->uploadFolder instanceof UploadFolder || !$this->server->httpRequest->getHeader(self::DESTINATION_HEADER)) {
205+
throw new BadRequest('Chunking destination header not set');
206+
}
207+
}
208+
209+
/**
210+
* @return IChunkedFileWrite
211+
* @throws BadRequest
212+
* @throws StorageInvalidException
213+
*/
214+
private function getStorage(): IStorage {
215+
$this->checkPrerequisites();
216+
$storage = $this->uploadFolder->getStorage();
217+
if (!$storage->instanceOfStorage(IChunkedFileWrite::class)) {
218+
throw new StorageInvalidException('Storage does not support chunked file write');
219+
}
220+
/** @var IChunkedFileWrite $storage */
221+
return $storage;
222+
}
223+
}

apps/dav/lib/Upload/FutureFile.php

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public function get() {
6868
return AssemblyStream::wrap($nodes);
6969
}
7070

71+
public function getPath() {
72+
return $this->root->getFileInfo()->getInternalPath() . '/.file';
73+
}
74+
7175
/**
7276
* @inheritdoc
7377
*/

apps/dav/lib/Upload/UploadFile.php

+8
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,12 @@ public function setName($name) {
7373
public function getLastModified() {
7474
return $this->file->getLastModified();
7575
}
76+
77+
public function getInternalPath(): string {
78+
return $this->file->getInternalPath();
79+
}
80+
81+
public function getAbsoluteInternalPath(): string {
82+
return $this->file->getFileInfo()->getPath();
83+
}
7684
}

apps/dav/lib/Upload/UploadFolder.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
namespace OCA\DAV\Upload;
2727

2828
use OCA\DAV\Connector\Sabre\Directory;
29+
use OCP\Files\Storage\IStorage;
2930
use Sabre\DAV\Exception\Forbidden;
3031
use Sabre\DAV\ICollection;
3132

@@ -35,10 +36,13 @@ class UploadFolder implements ICollection {
3536
private $node;
3637
/** @var CleanupService */
3738
private $cleanupService;
39+
/** @var IStorage */
40+
private $storage;
3841

39-
public function __construct(Directory $node, CleanupService $cleanupService) {
42+
public function __construct(Directory $node, CleanupService $cleanupService, IStorage $storage) {
4043
$this->node = $node;
4144
$this->cleanupService = $cleanupService;
45+
$this->storage = $storage;
4246
}
4347

4448
public function createFile($name, $data = null) {
@@ -95,4 +99,8 @@ public function setName($name) {
9599
public function getLastModified() {
96100
return $this->node->getLastModified();
97101
}
102+
103+
public function getStorage() {
104+
return $this->storage;
105+
}
98106
}

apps/dav/lib/Upload/UploadHome.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ public function createDirectory($name) {
5656
}
5757

5858
public function getChild($name): UploadFolder {
59-
return new UploadFolder($this->impl()->getChild($name), $this->cleanupService);
59+
return new UploadFolder($this->impl()->getChild($name), $this->cleanupService, $this->getStorage());
6060
}
6161

6262
public function getChildren(): array {
6363
return array_map(function ($node) {
64-
return new UploadFolder($node, $this->cleanupService);
64+
return new UploadFolder($node, $this->cleanupService, $this->getStorage());
6565
}, $this->impl()->getChildren());
6666
}
6767

@@ -90,14 +90,24 @@ public function getLastModified() {
9090
* @return Directory
9191
*/
9292
private function impl() {
93+
$view = $this->getView();
94+
$rootInfo = $view->getFileInfo('');
95+
return new Directory($view, $rootInfo);
96+
}
97+
98+
private function getView() {
9399
$rootView = new View();
94100
$user = \OC::$server->getUserSession()->getUser();
95101
Filesystem::initMountPoints($user->getUID());
96102
if (!$rootView->file_exists('/' . $user->getUID() . '/uploads')) {
97103
$rootView->mkdir('/' . $user->getUID() . '/uploads');
98104
}
99-
$view = new View('/' . $user->getUID() . '/uploads');
100-
$rootInfo = $view->getFileInfo('');
101-
return new Directory($view, $rootInfo);
105+
return new View('/' . $user->getUID() . '/uploads');
106+
}
107+
108+
private function getStorage() {
109+
$view = $this->getView();
110+
$storage = $view->getFileInfo('')->getStorage();
111+
return $storage;
102112
}
103113
}

build/psalm-baseline.xml

+5
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,11 @@
965965
<code>null</code>
966966
</NullArgument>
967967
</file>
968+
<file src="apps/dav/lib/Upload/ChunkingV2Plugin.php">
969+
<UndefinedFunction occurrences="1">
970+
<code>Uri\split($destination)</code>
971+
</UndefinedFunction>
972+
</file>
968973
<file src="apps/dav/lib/Upload/UploadHome.php">
969974
<UndefinedFunction occurrences="1">
970975
<code>\Sabre\Uri\split($this-&gt;principalInfo['uri'])</code>

0 commit comments

Comments
 (0)