Skip to content

Commit 13ef475

Browse files
authored
Merge pull request #36788 from nextcloud/revert-36589-enh/perf-remove-icache
Revert "fix(performance): Do not set up filesystem on every call"
2 parents 93e703b + 98ed72b commit 13ef475

File tree

30 files changed

+1079
-159
lines changed

30 files changed

+1079
-159
lines changed

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

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
use OCP\Lock\ILockingProvider;
4949
use OCP\Lock\LockedException;
5050
use Psr\Log\LoggerInterface;
51-
use Sabre\DAV\Exception;
5251
use Sabre\DAV\Exception\BadRequest;
5352
use Sabre\DAV\Exception\Locked;
5453
use Sabre\DAV\Exception\NotFound;
@@ -103,19 +102,33 @@ public function __construct(View $view, FileInfo $info, ?CachingTree $tree = nul
103102
* @param string $name Name of the file
104103
* @param resource|string $data Initial payload
105104
* @return null|string
105+
* @throws Exception\EntityTooLarge
106106
* @throws Exception\UnsupportedMediaType
107107
* @throws FileLocked
108108
* @throws InvalidPath
109-
* @throws Exception
110-
* @throws BadRequest
111-
* @throws Exception\Forbidden
112-
* @throws ServiceUnavailable
109+
* @throws \Sabre\DAV\Exception
110+
* @throws \Sabre\DAV\Exception\BadRequest
111+
* @throws \Sabre\DAV\Exception\Forbidden
112+
* @throws \Sabre\DAV\Exception\ServiceUnavailable
113113
*/
114114
public function createFile($name, $data = null) {
115115
try {
116-
// For non-chunked upload it is enough to check if we can create a new file
117-
if (!$this->fileView->isCreatable($this->path)) {
118-
throw new Exception\Forbidden();
116+
// for chunked upload also updating a existing file is a "createFile"
117+
// because we create all the chunks before re-assemble them to the existing file.
118+
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
119+
120+
// exit if we can't create a new file and we don't updatable existing file
121+
$chunkInfo = \OC_FileChunking::decodeName($name);
122+
if (!$this->fileView->isCreatable($this->path) &&
123+
!$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
124+
) {
125+
throw new \Sabre\DAV\Exception\Forbidden();
126+
}
127+
} else {
128+
// For non-chunked upload it is enough to check if we can create a new file
129+
if (!$this->fileView->isCreatable($this->path)) {
130+
throw new \Sabre\DAV\Exception\Forbidden();
131+
}
119132
}
120133

121134
$this->fileView->verifyPath($this->path, $name);
@@ -140,8 +153,8 @@ public function createFile($name, $data = null) {
140153
$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
141154
$node->releaseLock(ILockingProvider::LOCK_SHARED);
142155
return $result;
143-
} catch (StorageNotAvailableException $e) {
144-
throw new ServiceUnavailable($e->getMessage());
156+
} catch (\OCP\Files\StorageNotAvailableException $e) {
157+
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
145158
} catch (InvalidPathException $ex) {
146159
throw new InvalidPath($ex->getMessage(), false, $ex);
147160
} catch (ForbiddenException $ex) {
@@ -157,22 +170,22 @@ public function createFile($name, $data = null) {
157170
* @param string $name
158171
* @throws FileLocked
159172
* @throws InvalidPath
160-
* @throws Exception\Forbidden
161-
* @throws ServiceUnavailable
173+
* @throws \Sabre\DAV\Exception\Forbidden
174+
* @throws \Sabre\DAV\Exception\ServiceUnavailable
162175
*/
163176
public function createDirectory($name) {
164177
try {
165178
if (!$this->info->isCreatable()) {
166-
throw new Exception\Forbidden();
179+
throw new \Sabre\DAV\Exception\Forbidden();
167180
}
168181

169182
$this->fileView->verifyPath($this->path, $name);
170183
$newPath = $this->path . '/' . $name;
171184
if (!$this->fileView->mkdir($newPath)) {
172-
throw new Exception\Forbidden('Could not create directory ' . $newPath);
185+
throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
173186
}
174-
} catch (StorageNotAvailableException $e) {
175-
throw new ServiceUnavailable($e->getMessage());
187+
} catch (\OCP\Files\StorageNotAvailableException $e) {
188+
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
176189
} catch (InvalidPathException $ex) {
177190
throw new InvalidPath($ex->getMessage());
178191
} catch (ForbiddenException $ex) {
@@ -190,7 +203,7 @@ public function createDirectory($name) {
190203
* @return \Sabre\DAV\INode
191204
* @throws InvalidPath
192205
* @throws \Sabre\DAV\Exception\NotFound
193-
* @throws ServiceUnavailable
206+
* @throws \Sabre\DAV\Exception\ServiceUnavailable
194207
*/
195208
public function getChild($name, $info = null) {
196209
if (!$this->info->isReadable()) {
@@ -203,12 +216,12 @@ public function getChild($name, $info = null) {
203216
try {
204217
$this->fileView->verifyPath($this->path, $name);
205218
$info = $this->fileView->getFileInfo($path);
206-
} catch (StorageNotAvailableException $e) {
207-
throw new ServiceUnavailable($e->getMessage());
219+
} catch (\OCP\Files\StorageNotAvailableException $e) {
220+
throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
208221
} catch (InvalidPathException $ex) {
209222
throw new InvalidPath($ex->getMessage());
210223
} catch (ForbiddenException $e) {
211-
throw new Exception\Forbidden();
224+
throw new \Sabre\DAV\Exception\Forbidden();
212225
}
213226
}
214227

@@ -285,17 +298,17 @@ public function childExists($name) {
285298
*
286299
* @return void
287300
* @throws FileLocked
288-
* @throws Exception\Forbidden
301+
* @throws \Sabre\DAV\Exception\Forbidden
289302
*/
290303
public function delete() {
291304
if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
292-
throw new Exception\Forbidden();
305+
throw new \Sabre\DAV\Exception\Forbidden();
293306
}
294307

295308
try {
296309
if (!$this->fileView->rmdir($this->path)) {
297310
// assume it wasn't possible to remove due to permission issue
298-
throw new Exception\Forbidden();
311+
throw new \Sabre\DAV\Exception\Forbidden();
299312
}
300313
} catch (ForbiddenException $ex) {
301314
throw new Forbidden($ex->getMessage(), $ex->getRetry());
@@ -330,7 +343,7 @@ public function getQuotaInfo() {
330343
} catch (\OCP\Files\NotFoundException $e) {
331344
$logger->warning("error while getting quota into", ['exception' => $e]);
332345
return [0, 0];
333-
} catch (StorageNotAvailableException $e) {
346+
} catch (\OCP\Files\StorageNotAvailableException $e) {
334347
$logger->warning("error while getting quota into", ['exception' => $e]);
335348
return [0, 0];
336349
} catch (NotPermittedException $e) {
@@ -362,7 +375,7 @@ public function getQuotaInfo() {
362375
* @throws ServiceUnavailable
363376
* @throws Forbidden
364377
* @throws FileLocked
365-
* @throws Exception\Forbidden
378+
* @throws \Sabre\DAV\Exception\Forbidden
366379
*/
367380
public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
368381
if (!$sourceNode instanceof Node) {
@@ -386,7 +399,7 @@ public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
386399
// at getNodeForPath we also check the path for isForbiddenFileOrDir
387400
// with that we have covered both source and destination
388401
if ($sourceNode instanceof Directory && $targetNodeExists) {
389-
throw new Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
402+
throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
390403
}
391404

392405
[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
@@ -407,19 +420,19 @@ public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
407420
if ($targetNodeExists || $sameFolder) {
408421
// note that renaming a share mount point is always allowed
409422
if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
410-
throw new Exception\Forbidden();
423+
throw new \Sabre\DAV\Exception\Forbidden();
411424
}
412425
} else {
413426
if (!$this->fileView->isCreatable($destinationDir)) {
414-
throw new Exception\Forbidden();
427+
throw new \Sabre\DAV\Exception\Forbidden();
415428
}
416429
}
417430

418431
if (!$sameFolder) {
419432
// moving to a different folder, source will be gone, like a deletion
420433
// note that moving a share mount point is always allowed
421434
if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
422-
throw new Exception\Forbidden();
435+
throw new \Sabre\DAV\Exception\Forbidden();
423436
}
424437
}
425438

@@ -432,7 +445,7 @@ public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
432445

433446
$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
434447
if (!$renameOkay) {
435-
throw new Exception\Forbidden('');
448+
throw new \Sabre\DAV\Exception\Forbidden('');
436449
}
437450
} catch (StorageNotAvailableException $e) {
438451
throw new ServiceUnavailable($e->getMessage());
@@ -452,7 +465,7 @@ public function copyInto($targetName, $sourcePath, INode $sourceNode) {
452465
$sourcePath = $sourceNode->getPath();
453466

454467
if (!$this->fileView->isCreatable($this->getPath())) {
455-
throw new Exception\Forbidden();
468+
throw new \Sabre\DAV\Exception\Forbidden();
456469
}
457470

458471
try {

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

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ public function put($data) {
148148
// verify path of the target
149149
$this->verifyPath();
150150

151+
// chunked handling
152+
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
153+
try {
154+
return $this->createFileChunked($data);
155+
} catch (\Exception $e) {
156+
$this->convertToSabreException($e);
157+
}
158+
}
159+
151160
/** @var Storage $partStorage */
152161
[$partStorage] = $this->fileView->resolvePath($this->path);
153162
$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
@@ -568,6 +577,132 @@ public function getDirectDownload() {
568577
return $storage->getDirectDownload($internalPath);
569578
}
570579

580+
/**
581+
* @param resource $data
582+
* @return null|string
583+
* @throws Exception
584+
* @throws BadRequest
585+
* @throws NotImplemented
586+
* @throws ServiceUnavailable
587+
*/
588+
private function createFileChunked($data) {
589+
[$path, $name] = \Sabre\Uri\split($this->path);
590+
591+
$info = \OC_FileChunking::decodeName($name);
592+
if (empty($info)) {
593+
throw new NotImplemented($this->l10n->t('Invalid chunk name'));
594+
}
595+
596+
$chunk_handler = new \OC_FileChunking($info);
597+
$bytesWritten = $chunk_handler->store($info['index'], $data);
598+
599+
//detect aborted upload
600+
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
601+
if (isset($_SERVER['CONTENT_LENGTH'])) {
602+
$expected = (int)$_SERVER['CONTENT_LENGTH'];
603+
if ($bytesWritten !== $expected) {
604+
$chunk_handler->remove($info['index']);
605+
throw new BadRequest(
606+
$this->l10n->t(
607+
'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.',
608+
[
609+
$this->l10n->n('%n byte', '%n bytes', $expected),
610+
$this->l10n->n('%n byte', '%n bytes', $bytesWritten),
611+
],
612+
)
613+
);
614+
}
615+
}
616+
}
617+
618+
if ($chunk_handler->isComplete()) {
619+
/** @var Storage $storage */
620+
[$storage,] = $this->fileView->resolvePath($path);
621+
$needsPartFile = $storage->needsPartFile();
622+
$partFile = null;
623+
624+
$targetPath = $path . '/' . $info['name'];
625+
/** @var \OC\Files\Storage\Storage $targetStorage */
626+
[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
627+
628+
$exists = $this->fileView->file_exists($targetPath);
629+
630+
try {
631+
$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
632+
633+
$this->emitPreHooks($exists, $targetPath);
634+
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
635+
/** @var \OC\Files\Storage\Storage $targetStorage */
636+
[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
637+
638+
if ($needsPartFile) {
639+
// we first assembly the target file as a part file
640+
$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
641+
/** @var \OC\Files\Storage\Storage $targetStorage */
642+
[$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
643+
644+
645+
$chunk_handler->file_assemble($partStorage, $partInternalPath);
646+
647+
// here is the final atomic rename
648+
$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
649+
$fileExists = $targetStorage->file_exists($targetInternalPath);
650+
if ($renameOkay === false || $fileExists === false) {
651+
\OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
652+
// only delete if an error occurred and the target file was already created
653+
if ($fileExists) {
654+
// set to null to avoid double-deletion when handling exception
655+
// stray part file
656+
$partFile = null;
657+
$targetStorage->unlink($targetInternalPath);
658+
}
659+
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
660+
throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
661+
}
662+
} else {
663+
// assemble directly into the final file
664+
$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
665+
}
666+
667+
// allow sync clients to send the mtime along in a header
668+
if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
669+
$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
670+
if ($targetStorage->touch($targetInternalPath, $mtime)) {
671+
$this->header('X-OC-MTime: accepted');
672+
}
673+
}
674+
675+
// since we skipped the view we need to scan and emit the hooks ourselves
676+
$targetStorage->getUpdater()->update($targetInternalPath);
677+
678+
$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
679+
680+
$this->emitPostHooks($exists, $targetPath);
681+
682+
// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
683+
$info = $this->fileView->getFileInfo($targetPath);
684+
685+
if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
686+
$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
687+
$this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
688+
} elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
689+
$this->fileView->putFileInfo($this->path, ['checksum' => '']);
690+
}
691+
692+
$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
693+
694+
return $info->getEtag();
695+
} catch (\Exception $e) {
696+
if ($partFile !== null) {
697+
$targetStorage->unlink($targetInternalPath);
698+
}
699+
$this->convertToSabreException($e);
700+
}
701+
}
702+
703+
return null;
704+
}
705+
571706
/**
572707
* Convert the given exception to a SabreException instance
573708
*

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,15 @@ public function handleUpdateProperties($path, PropPatch $propPatch) {
577577
* @throws \Sabre\DAV\Exception\BadRequest
578578
*/
579579
public function sendFileIdHeader($filePath, \Sabre\DAV\INode $node = null) {
580+
// chunked upload handling
581+
if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
582+
[$path, $name] = \Sabre\Uri\split($filePath);
583+
$info = \OC_FileChunking::decodeName($name);
584+
if (!empty($info)) {
585+
$filePath = $path . '/' . $info['name'];
586+
}
587+
}
588+
580589
// we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder
581590
if (!$this->server->tree->nodeExists($filePath)) {
582591
return;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function initialize(\Sabre\DAV\Server $server) {
6161
public function getLock(RequestInterface $request) {
6262
// we can't listen on 'beforeMethod:PUT' due to order of operations with setting up the tree
6363
// so instead we limit ourselves to the PUT method manually
64-
if ($request->getMethod() !== 'PUT') {
64+
if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
6565
return;
6666
}
6767
try {
@@ -84,7 +84,7 @@ public function releaseLock(RequestInterface $request) {
8484
if ($this->isLocked === false) {
8585
return;
8686
}
87-
if ($request->getMethod() !== 'PUT') {
87+
if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
8888
return;
8989
}
9090
try {

0 commit comments

Comments
 (0)