From 7b1ee271d447ca4384666e6bb3b4669de9fd1066 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 09:57:11 +0100 Subject: [PATCH 01/51] feature(storage): Allow unlink operation on symlinks Signed-off-by: Tamino Bauknecht --- lib/private/Files/Storage/Local.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 0fca853da5987..aaabc52434168 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -181,6 +181,10 @@ public function is_file($path) { return is_file($this->getSourcePath($path)); } + public function is_link($path) { + return is_link($this->getSourcePath($path)); + } + public function stat($path) { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); @@ -340,7 +344,7 @@ public function file_put_contents($path, $data) { public function unlink($path) { if ($this->is_dir($path)) { return $this->rmdir($path); - } elseif ($this->is_file($path)) { + } elseif ($this->is_file($path) || $this->is_link($path)) { return unlink($this->getSourcePath($path)); } else { return false; From 1c75b70aaf8a8cb90a4720489373d66bfb11edb2 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 10:10:11 +0100 Subject: [PATCH 02/51] feature(storage): Add symlink operation to local storage Signed-off-by: Tamino Bauknecht --- lib/private/Files/Storage/Local.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index aaabc52434168..660d68adc7a8a 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -341,6 +341,17 @@ public function file_put_contents($path, $data) { return $result; } + /** + * create symlink + * + * @param string $path + * @param string $target + * @return bool + */ + public function symlink($target, $path) { + return symlink($target, $this->getSourcePath($path)); + } + public function unlink($path) { if ($this->is_dir($path)) { return $this->rmdir($path); From cf75521313b4f530c99e63bbf92369edb38b6dc1 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 10:12:01 +0100 Subject: [PATCH 03/51] feature(dav/connector): Add createSymlink to Directory node Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 441e6ea8f570c..db02d883a2881 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -192,6 +192,56 @@ public function createDirectory($name) { } } + /** + * Creates a new symlink + * + * @param string $name + * @param string $target + * @return null|string + * @throws FileLocked + * @throws InvalidPath + * @throws ServiceUnavailable + * @throws \Sabre\DAV\Exception + * @throws \Sabre\DAV\Exception\Forbidden + */ + public function createSymlink($name, $target) { + try { + if (!$this->info->isCreatable()) { + throw new \Sabre\DAV\Exception\Forbidden(); + } + + $this->fileView->verifyPath($this->path, $name); + + [$storage, $internalPath] = $this->fileView->resolvePath($this->path); + if (!$storage || !$storage->instanceOfStorage(\OC\Files\Storage\Local::class)) { + throw new \Sabre\DAV\Exception\NotImplemented("Symlinks currently not supported on non-local storages - failed to create '$name'!"); + } + + $internalPath = $internalPath . '/' . $name; + $storage->unlink($internalPath); + if (!$storage->symlink($target, $internalPath)) { + throw new \Sabre\DAV\Exception\Forbidden("Could not create symlink '$name'!"); + } + $storage->getUpdater()->update($internalPath); + $newEtag = $storage->getETag($internalPath); + $infoData = [ + 'type' => FileInfo::TYPE_FILE, + 'etag' => $newEtag, + ]; + $path = \OC\Files\Filesystem::normalizePath($this->path . '/' . $name); + $this->fileView->putFileInfo($path, $infoData); + return '"' . $newEtag . '"'; + } catch (\OCP\Files\StorageNotAvailableException $e) { + throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e); + } catch (InvalidPathException $ex) { + throw new InvalidPath($ex->getMessage(), false, $ex); + } catch (ForbiddenException $ex) { + throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex); + } catch (LockedException $e) { + throw new FileLocked($e->getMessage(), $e->getCode(), $e); + } + } + /** * Returns a specific child node, referenced by its name * From b9ab528ee811704c42d29625d1c0ec6d9d41e225 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 10:17:33 +0100 Subject: [PATCH 04/51] feature(dav/upload): Add symlink single-file upload to ChunkingV2Plugin Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/ChunkingV2Plugin.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php index 29017155d458e..867e12ed5235d 100644 --- a/apps/dav/lib/Upload/ChunkingV2Plugin.php +++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php @@ -45,6 +45,7 @@ use OCP\IConfig; use OCP\Lock\ILockingProvider; use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\InsufficientStorage; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Exception\PreconditionFailed; @@ -146,6 +147,25 @@ public function afterMkcol(RequestInterface $request, ResponseInterface $respons } public function beforePut(RequestInterface $request, ResponseInterface $response): bool { + if ($request->getHeader('OC-File-Type') == 1) { + // TODO: store default value in global location + $allowSymlinks = \OC::$server->get(\OC\AllConfig::class)->getSystemValueBool( + 'localstorage.allowsymlinks', false); + if (!$allowSymlinks) { + throw new Forbidden("Server does not allow the creation of symlinks!"); + } + $symlinkPath = $request->getPath(); + $symlinkTarget = $request->getBodyAsString(); + $parentNode = $this->server->tree->getNodeForPath(dirname($symlinkPath)); + if(!$parentNode instanceof \OCA\DAV\Connector\Sabre\Directory) { + throw new Exception("Unable to upload '$symlinkPath' because the remote directory does not support symlink creation!"); + } + $etag = $parentNode->createSymlink(basename($symlinkPath), $symlinkTarget); + $response->setHeader("OC-ETag", $etag); + $response->setStatus(201); + $this->server->sapi->sendResponse($response); + return false; + } try { $this->prepareUpload(dirname($request->getPath())); $this->checkPrerequisites(); From af70873cc57fc65a10405e71dca3d54dd8416b40 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 13:39:56 +0100 Subject: [PATCH 05/51] feature(dav/bulk): Add symlink upload to BulkUploadPlugin Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 4d838d255eb08..52623ed8c6c78 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -27,7 +27,9 @@ use OCP\AppFramework\Http; use OCP\Files\DavUtil; use OCP\Files\Folder; +use Exception; use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -37,6 +39,13 @@ class BulkUploadPlugin extends ServerPlugin { private Folder $userFolder; private LoggerInterface $logger; + /** + * Reference to main server object + * + * @var Server + */ + private $server; + public function __construct( Folder $userFolder, LoggerInterface $logger @@ -49,6 +58,7 @@ public function __construct( * Register listener on POST requests with the httpPost method. */ public function initialize(Server $server): void { + $this->server = $server; $server->on('method:POST', [$this, 'httpPost'], 10); } @@ -89,6 +99,26 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) $mtime = null; } + if (isset($headers['oc-file-type']) && $headers['oc-file-type'] == 1) { + // TODO: store default value in global location + $allowSymlinks = \OC::$server->get(\OC\AllConfig::class)->getSystemValueBool( + 'localstorage.allowsymlinks', false); + if (!$allowSymlinks) { + throw new Forbidden("Server does not allow the creation of symlinks!"); + } + $symlinkPath = $headers['x-file-path']; + $parentNode = $this->server->tree->getNodeForPath(dirname($symlinkPath)); + if(!$parentNode instanceof \OCA\DAV\Connector\Sabre\Directory) { + throw new Exception("Unable to upload '$symlinkPath' because the remote directory does not support symlink creation!"); + } + $etag = $parentNode->createSymlink(basename($symlinkPath), $content); + $writtenFiles[$headers['x-file-path']] = [ + "error" => false, + "etag" => $etag, + ]; + continue; + } + $node = $this->userFolder->newFile($headers['x-file-path'], $content); $node->touch($mtime); $node = $this->userFolder->getById($node->getId())[0]; From 4ee5062c539bd254938105a063fb11cecb7e6e97 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 18:39:55 +0100 Subject: [PATCH 06/51] feature(files): Add file type TYPE_SYMLINK to FileInfo Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 2 +- apps/files_reminders/lib/Notification/Notifier.php | 1 + lib/private/Encryption/Util.php | 5 +++-- lib/private/Files/FileInfo.php | 4 +++- lib/private/Files/Node/HookConnector.php | 2 +- lib/public/Files/DavUtil.php | 2 +- lib/public/Files/FileInfo.php | 8 +++++++- 7 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index db02d883a2881..b573c5f2f4f43 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -225,7 +225,7 @@ public function createSymlink($name, $target) { $storage->getUpdater()->update($internalPath); $newEtag = $storage->getETag($internalPath); $infoData = [ - 'type' => FileInfo::TYPE_FILE, + 'type' => FileInfo::TYPE_SYMLINK, 'etag' => $newEtag, ]; $path = \OC\Files\Filesystem::normalizePath($this->path . '/' . $name); diff --git a/apps/files_reminders/lib/Notification/Notifier.php b/apps/files_reminders/lib/Notification/Notifier.php index f7ffa0b443754..7f683676d8d87 100644 --- a/apps/files_reminders/lib/Notification/Notifier.php +++ b/apps/files_reminders/lib/Notification/Notifier.php @@ -105,6 +105,7 @@ public function prepare(INotification $notification, string $languageCode): INot $label = match ($node->getType()) { FileInfo::TYPE_FILE => $l->t('View file'), FileInfo::TYPE_FOLDER => $l->t('View folder'), + FileInfo::TYPE_SYMLINK => $l->t('View symlink'), }; $this->addActionButton($notification, $label); diff --git a/lib/private/Encryption/Util.php b/lib/private/Encryption/Util.php index a828483265b5a..5f5660568c9b7 100644 --- a/lib/private/Encryption/Util.php +++ b/lib/private/Encryption/Util.php @@ -154,6 +154,7 @@ public function createHeader(array $headerData, IEncryptionModule $encryptionMod /** * go recursively through a dir and collect all files and sub files. + * the resulting list will not contain symlinks * * @param string $dir relative to the users files folder * @return array with list of files relative to the users files folder @@ -167,9 +168,9 @@ public function getAllFiles($dir) { $content = $this->rootView->getDirectoryContent($dir); foreach ($content as $c) { - if ($c->getType() === 'dir') { + if ($c->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) { $dirList[] = $c->getPath(); - } else { + } elseif($c->getType() === \OCP\Files\FileInfo::TYPE_FILE) { $result[] = $c->getPath(); } } diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index 5ba2f27b78b69..d8283d5f0c2b8 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -242,7 +242,9 @@ public function getPermissions() { } /** - * @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER + * @return string \OCP\Files\FileInfo::TYPE_FILE| + * \OCP\Files\FileInfo::TYPE_FOLDER + * \OCP\Files\FileInfo::TYPE_SYMLINK */ public function getType() { if (!isset($this->data['type'])) { diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index f61eedee66e55..ef4db857e8cfc 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -229,7 +229,7 @@ private function getNodeForPath(string $path): Node { return new NonExistingFile($this->root, $this->view, $fullPath, $info); } } - if ($info->getType() === FileInfo::TYPE_FILE) { + if ($info->getType() === FileInfo::TYPE_FILE || $info->getType() === FileInfo::TYPE_SYMLINK) { return new File($this->root, $this->view, $info->getPath(), $info); } else { return new Folder($this->root, $this->view, $info->getPath(), $info); diff --git a/lib/public/Files/DavUtil.php b/lib/public/Files/DavUtil.php index 89fd3b1864341..d60ba64100ed8 100644 --- a/lib/public/Files/DavUtil.php +++ b/lib/public/Files/DavUtil.php @@ -94,7 +94,7 @@ public static function getDavPermissions(FileInfo $info): string { if ($isWritable) { $p .= 'W'; } - } else { + } elseif ($info->getType() === FileInfo::TYPE_FOLDER) { if ($permissions & Constants::PERMISSION_CREATE) { $p .= 'CK'; } diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index 817b03dfc65f1..2f1e0c7fc2500 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -45,6 +45,10 @@ interface FileInfo { * @since 7.0.0 */ public const TYPE_FOLDER = 'dir'; + /** + * @since TODO + */ + public const TYPE_SYMLINK = 'symlink'; /** * @const \OCP\Files\FileInfo::SPACE_NOT_COMPUTED Return value for a not computed space value @@ -179,7 +183,9 @@ public function getPermissions(); /** * Check whether this is a file or a folder * - * @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER + * @return string \OCP\Files\FileInfo::TYPE_FILE| + * \OCP\Files\FileInfo::TYPE_FOLDER| + * \OCP\Files\FileInfo::TYPE_SYMLINK * @since 7.0.0 */ public function getType(); From 97d3dc5c6f9b81a6be9bf964997152f2ef7e2631 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 21:52:38 +0100 Subject: [PATCH 07/51] feature(storage): Add symlink, is_link and readlink to IStorage, Wrapper and Common storage and View Signed-off-by: Tamino Bauknecht --- lib/private/Files/Storage/Common.php | 4 +++ lib/private/Files/Storage/Local.php | 11 +++---- lib/private/Files/Storage/Wrapper/Wrapper.php | 30 +++++++++++++++++++ lib/private/Files/View.php | 27 +++++++++++++++++ lib/public/Files/Storage/IStorage.php | 28 +++++++++++++++++ 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 3d5a2f098b2ac..3c7818d96fd58 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -121,6 +121,10 @@ public function is_file($path) { return $this->filetype($path) === 'file'; } + public function is_link($path) { + return $this->filetype($path) === 'link'; + } + public function filesize($path): false|int|float { if ($this->is_dir($path)) { return 0; //by definition diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 660d68adc7a8a..2f986f676bae2 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -341,17 +341,14 @@ public function file_put_contents($path, $data) { return $result; } - /** - * create symlink - * - * @param string $path - * @param string $target - * @return bool - */ public function symlink($target, $path) { return symlink($target, $this->getSourcePath($path)); } + public function readlink($path) { + return readlink($this->getSourcePath($path)); + } + public function unlink($path) { if ($this->is_dir($path)) { return $this->rmdir($path); diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index 9f5564b449038..b2b1d1b50c83b 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -104,6 +104,26 @@ public function opendir($path) { return $this->getWrapperStorage()->opendir($path); } + /** + * see https://www.php.net/manual/en/function.symlink.php + * + * @param string $path + * @return string|false + */ + public function symlink($target, $link) { + return $this->getWrapperStorage()->symlink($target, $link); + } + + /** + * see https://www.php.net/manual/en/function.readlink.php + * + * @param string $path + * @return string|false + */ + public function readlink($path) { + return $this->getWrapperStorage()->readlink($path); + } + /** * see https://www.php.net/manual/en/function.is_dir.php * @@ -124,6 +144,16 @@ public function is_file($path) { return $this->getWrapperStorage()->is_file($path); } + /** + * see https://www.php.net/manual/en/function.is_link.php + * + * @param string $path + * @return bool + */ + public function is_link($path) { + return $this->getWrapperStorage()->is_link($path); + } + /** * see https://www.php.net/manual/en/function.stat.php * only the following keys are required in the result: size and mtime diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 6eefb093795dd..4168b7daeb3e4 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -410,6 +410,33 @@ public function readfile($path) { return false; } + /** + * Return symlink target of given symlink + * + * @param string $path + * @return mixed + * @throws LockedException + * @throws InvalidPathException + */ + public function readlink($path) { + $this->assertPathLength($path); + return $this->basicOperation('readlink', $path, ['read']); + } + + /** + * Create symlink at given path to the specified target + * + * @param string $target + * @param string $path + * @return mixed + * @throws LockedException + * @throws InvalidPathException + */ + public function symlink($target, $path) { + $this->assertPathLength($path); + return $this->basicOperation('symlink', $path, ['create', 'write'], $target); + } + /** * @param string $path * @param int $from diff --git a/lib/public/Files/Storage/IStorage.php b/lib/public/Files/Storage/IStorage.php index 00e98fdfbb6e6..902828241dea9 100644 --- a/lib/public/Files/Storage/IStorage.php +++ b/lib/public/Files/Storage/IStorage.php @@ -108,6 +108,15 @@ public function is_dir($path); */ public function is_file($path); + /** + * see https://www.php.net/manual/en/function.is-link.php + * + * @param string $path + * @return bool + * @since TODO + */ + public function is_link($path); + /** * see https://www.php.net/manual/en/function.stat.php * only the following keys are required in the result: size and mtime @@ -238,6 +247,25 @@ public function file_put_contents($path, $data); */ public function unlink($path); + /** + * see https://www.php.net/manual/en/function.symlink.php + * + * @param string $target + * @param string $link + * @return bool + * @since TODO + */ + public function symlink($target, $link); + + /** + * see https://www.php.net/manual/en/function.readlink.php + * + * @param string $path + * @return string|false + * @since TODO + */ + public function readlink($path); + /** * see https://www.php.net/manual/en/function.rename.php * From 5cc9108f977947ff40a648e414079c3675476b78 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 21:53:55 +0100 Subject: [PATCH 08/51] feature(dav/file): Add readlink method Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/File.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index f188490fd938f..2eb274731a5ca 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -553,6 +553,26 @@ public function delete() { } } + /** + * Return symlink target of the file + * + * @return string|null + * @throws NotFound + * @throws Forbidden + * @throws FileLocked + */ + public function readlink() { + if (!$this->info->isReadable()) { + throw new NotFound(); + } + + try { + return $this->fileView->readlink($this->path); + } catch (\Exception $e) { + $this->convertToSabreException($e); + } + } + /** * Returns the mime-type for a file * From 23ee19af6c551c136fa48653d94220040fb32713 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 21:56:34 +0100 Subject: [PATCH 09/51] feature(storage): Allow deletion of symlinks with remove method in Common storage Signed-off-by: Tamino Bauknecht --- lib/private/Files/Storage/Common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 3c7818d96fd58..c0af0d4902fc2 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -106,7 +106,7 @@ public function __construct($parameters) { protected function remove($path) { if ($this->is_dir($path)) { return $this->rmdir($path); - } elseif ($this->is_file($path)) { + } elseif ($this->is_file($path) || $this->is_link($path)) { return $this->unlink($path); } else { return false; From da98a9f75d240b9840e208163f3c2d62b10ad05a Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 7 Nov 2023 22:32:24 +0100 Subject: [PATCH 10/51] feature(storage): Add default implementation for readlink and symlink to Common storage This default implementation does always throw, but it will ensure that storages which cannot support symlinks or plugins won't break with the addition of the symlink operations. Signed-off-by: Tamino Bauknecht --- lib/private/Files/Storage/Common.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index c0af0d4902fc2..6c79a531ed28a 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -113,6 +113,26 @@ protected function remove($path) { } } + /** + * Default implementation to ensure backwards compatibility. + * Should be overwritten by any storage class that can provide this operation. + * + * @throws \OCP\Files\NotPermittedException + */ + public function readlink($path) { + throw new \OCP\Files\NotPermittedException("Storage does not provide an implementation for 'readlink'"); + } + + /** + * Default implementation to ensure backwards compatibility. + * Should be overwritten by any storage class that can provide this operation. + * + * @throws \OCP\Files\NotPermittedException + */ + public function symlink($target, $link) { + throw new \OCP\Files\NotPermittedException("Storage does not provide an implementation for 'symlink'"); + } + public function is_dir($path) { return $this->filetype($path) === 'dir'; } From 1b31d5970ad863af4ef635710d65e2b1f58e3baa Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 8 Nov 2023 19:12:09 +0100 Subject: [PATCH 11/51] feature(fileinfo): Add symlink mimetype Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 1 + apps/files_trashbin/lib/Helper.php | 14 +++++++++++--- lib/private/Files/FileInfo.php | 5 ++++- lib/private/Files/Storage/Common.php | 7 +++++-- lib/private/Files/Type/Detection.php | 4 +++- lib/private/Files/View.php | 4 +++- lib/public/Files/FileInfo.php | 4 ++++ 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index b573c5f2f4f43..5a2db7f447b92 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -226,6 +226,7 @@ public function createSymlink($name, $target) { $newEtag = $storage->getETag($internalPath); $infoData = [ 'type' => FileInfo::TYPE_SYMLINK, + 'mimetype' => FileInfo::MIMETYPE_SYMLINK, 'etag' => $newEtag, ]; $path = \OC\Files\Filesystem::normalizePath($this->path . '/' . $name); diff --git a/apps/files_trashbin/lib/Helper.php b/apps/files_trashbin/lib/Helper.php index 61d8eb9c715a4..28169ee0fd014 100644 --- a/apps/files_trashbin/lib/Helper.php +++ b/apps/files_trashbin/lib/Helper.php @@ -32,7 +32,6 @@ use OC\Files\FileInfo; use OCP\Constants; -use OCP\Files\Cache\ICacheEntry; class Helper { /** @@ -82,11 +81,20 @@ public static function getTrashFiles($dir, $user, $sortAttribute = '', $sortDesc $originalPath = substr($originalPath, 0, -1); } } - $type = $entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE ? 'dir' : 'file'; + if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) { + $type = FileInfo::TYPE_FOLDER; + $mimetype = FileInfo::MIMETYPE_FOLDER; + } elseif ($entry->getMimeType() === FileInfo::MIMETYPE_SYMLINK) { + $type = FileInfo::TYPE_SYMLINK; + $mimetype = FileInfo::MIMETYPE_SYMLINK; + } else { + $type = FileInfo::TYPE_FILE; + $mimetype = \OC::$server->get(IMimeTypeDetector::class)->detectPath($name); + } $i = [ 'name' => $name, 'mtime' => $timestamp, - 'mimetype' => $type === 'dir' ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($name), + 'mimetype' => $mimetype, 'type' => $type, 'directory' => ($dir === '/') ? '' : $dir, 'size' => $entry->getSize(), diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index d8283d5f0c2b8..5e45ff20cef7c 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -248,7 +248,10 @@ public function getPermissions() { */ public function getType() { if (!isset($this->data['type'])) { - $this->data['type'] = ($this->getMimetype() === self::MIMETYPE_FOLDER) ? self::TYPE_FOLDER : self::TYPE_FILE; + $this->data['type'] = $this->getMimetype() === self::MIMETYPE_FOLDER ? + self::TYPE_FOLDER : ($this->getMimetype() === self::MIMETYPE_SYMLINK ? + self::TYPE_SYMLINK : + self::TYPE_FILE); } return $this->data['type']; } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 6c79a531ed28a..99216c7a03459 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -48,6 +48,7 @@ use OC\Files\Cache\Updater; use OC\Files\Cache\Watcher; use OC\Files\Filesystem; +use OC\Files\FileInfo; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\Wrapper; use OCP\Files\EmptyFileNameException; @@ -276,9 +277,11 @@ public function copy($source, $target) { public function getMimeType($path) { if ($this->is_dir($path)) { - return 'httpd/unix-directory'; + return FileInfo::MIMETYPE_FOLDER; + } elseif ($this->is_link($path)) { + return FileInfo::MIMETYPE_SYMLINK; } elseif ($this->file_exists($path)) { - return \OC::$server->getMimeTypeDetector()->detectPath($path); + return \OC::$server->get(IMimeTypeDetector::class)->detectPath($path); } else { return false; } diff --git a/lib/private/Files/Type/Detection.php b/lib/private/Files/Type/Detection.php index 71b8cb986d71a..4bbaf9a3d5d7e 100644 --- a/lib/private/Files/Type/Detection.php +++ b/lib/private/Files/Type/Detection.php @@ -228,7 +228,9 @@ public function detectContent(string $path): string { if (@is_dir($path)) { // directories are easy - return 'httpd/unix-directory'; + return \OCP\Files\FileInfo::MIMETYPE_FOLDER; + } elseif (@is_link($path)) { + return \OCP\Files\FileInfo::MIMETYPE_SYMLINK; } if (function_exists('finfo_open') diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 4168b7daeb3e4..bac2e81c2b632 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -1539,7 +1539,9 @@ public function getDirectoryContent($directory, $mimetype_filter = '', \OCP\File } } else { //mountpoint in this folder, add an entry for it $rootEntry['name'] = $relativePath; - $rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file'; + $rootEntry['type'] = $rootEntry['mimetype'] === FileInfo::MIMETYPE_FOLDER ? + FileInfo::TYPE_FOLDER : ($rootEntry['mimetype'] === FileInfo::MIMETYPE_SYMLINK ? + FileInfo::TYPE_SYMLINK : FileInfo::TYPE_FILE); $permissions = $rootEntry['permissions']; // do not allow renaming/deleting the mount point if they are not shared files/folders // for shared files/folders we use the permissions given by the owner diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index 2f1e0c7fc2500..822da2473fb80 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -70,6 +70,10 @@ interface FileInfo { * @since 9.1.0 */ public const MIMETYPE_FOLDER = 'httpd/unix-directory'; + /** + * @since TODO + */ + public const MIMETYPE_SYMLINK = 'inode/symlink'; /** * @const \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX Return regular expression to test filenames against (blacklisting) From 0ef014aa4959ef48bad8814723af8272d62d452e Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 15 Nov 2023 17:05:39 +0100 Subject: [PATCH 12/51] Add couple of TODOs, mostly related to symlink mimetype Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 3 +++ apps/dav/lib/Connector/Sabre/Node.php | 1 + apps/dav/lib/Connector/Sabre/ObjectTree.php | 1 + apps/dav/lib/Connector/Sabre/ServerFactory.php | 1 + apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php | 1 + apps/files_trashbin/lib/Sabre/TrashRoot.php | 1 + lib/private/Files/Cache/Scanner.php | 2 +- lib/private/Files/Node/Folder.php | 1 + lib/private/Files/Node/HookConnector.php | 1 + lib/private/Files/Node/Root.php | 1 + lib/private/Files/Storage/Common.php | 2 +- lib/private/Files/Storage/DAV.php | 2 ++ 12 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 5a2db7f447b92..efa388b088877 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -210,6 +210,7 @@ public function createSymlink($name, $target) { throw new \Sabre\DAV\Exception\Forbidden(); } + // TODO: check if symlink target leaves data dir $this->fileView->verifyPath($this->path, $name); [$storage, $internalPath] = $this->fileView->resolvePath($this->path); @@ -218,6 +219,7 @@ public function createSymlink($name, $target) { } $internalPath = $internalPath . '/' . $name; + // TODO: use View::unlink instead and create View::symlink? $storage->unlink($internalPath); if (!$storage->symlink($target, $internalPath)) { throw new \Sabre\DAV\Exception\Forbidden("Could not create symlink '$name'!"); @@ -280,6 +282,7 @@ public function getChild($name, $info = null, IRequest $request = null, IL10N $l if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) { $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager); } else { + // TODO(taminob): create SymlinkNode here instead? $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n); } if ($this->tree) { diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index c9407b127860d..5ca7938fd1022 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -113,6 +113,7 @@ protected function refreshInfo(): void { if ($this->info->getType() === FileInfo::TYPE_FOLDER) { $this->node = new Folder($root, $rootView, $this->path, $this->info); } else { + // TODO(taminob): create symlink node here instead? $this->node = new File($root, $rootView, $this->path, $this->info); } } diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php index c129371e37632..1bf5b663bbeda 100644 --- a/apps/dav/lib/Connector/Sabre/ObjectTree.php +++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php @@ -175,6 +175,7 @@ public function getNodeForPath($path) { if ($info->getType() === 'dir') { $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this); } else { + // TODO(taminob): create symlink node here instead? $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 828977fd81287..711139b781b75 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -141,6 +141,7 @@ public function createServer(string $baseUri, if ($rootInfo->getType() === 'dir') { $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); } else { + // TODO(taminob): create symlink node here instead? $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo); } $objectTree->init($root, $view, $this->mountManager); diff --git a/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php b/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php index 8a61823cf1029..a7b412ecd84d6 100644 --- a/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php +++ b/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php @@ -36,6 +36,7 @@ public function getChildren(): array { $entries = $this->trashManager->listTrashFolder($this->data); $children = array_map(function (ITrashItem $entry) { + // TODO(taminob): probably not necessary, but check if TrashFolderSymlink is required if ($entry->getType() === FileInfo::TYPE_FOLDER) { return new TrashFolderFolder($this->trashManager, $entry); } diff --git a/apps/files_trashbin/lib/Sabre/TrashRoot.php b/apps/files_trashbin/lib/Sabre/TrashRoot.php index b61daacdf432f..b296c7c322382 100644 --- a/apps/files_trashbin/lib/Sabre/TrashRoot.php +++ b/apps/files_trashbin/lib/Sabre/TrashRoot.php @@ -75,6 +75,7 @@ public function getChildren(): array { $entries = $this->trashManager->listTrashRoot($this->user); $children = array_map(function (ITrashItem $entry) { + // TODO(taminob): probably not necessary, but check if TrashSymlink is required if ($entry->getType() === FileInfo::TYPE_FOLDER) { return new TrashFolder($this->trashManager, $entry); } diff --git a/lib/private/Files/Cache/Scanner.php b/lib/private/Files/Cache/Scanner.php index 2711fc8ad19cd..b1f3cb76e4071 100644 --- a/lib/private/Files/Cache/Scanner.php +++ b/lib/private/Files/Cache/Scanner.php @@ -169,7 +169,7 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = try { if ($data) { // pre-emit only if it was a file. By that we avoid counting/treating folders as files - if ($data['mimetype'] !== 'httpd/unix-directory') { + if ($data['mimetype'] !== 'httpd/unix-directory') { // TODO(taminob): maybe also check for symlink? $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]); \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]); } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index c7462572fed06..e7d96110ab26f 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -120,6 +120,7 @@ protected function createNode(string $path, ?FileInfo $info = null, bool $infoHa if ($isDir) { return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded); } else { + // TODO(taminob): create symlink here instead of file? return new File($this->root, $this->view, $path, $info, $parent); } } diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index ef4db857e8cfc..74cbc38f5fa42 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -229,6 +229,7 @@ private function getNodeForPath(string $path): Node { return new NonExistingFile($this->root, $this->view, $fullPath, $info); } } + // TODO(taminob): create symlink node instead of file here if ($info->getType() === FileInfo::TYPE_FILE || $info->getType() === FileInfo::TYPE_SYMLINK) { return new File($this->root, $this->view, $info->getPath(), $info); } else { diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index ee344f9be8b41..341e81bf3a31d 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -510,6 +510,7 @@ public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoi if ($isDir) { return new Folder($this, $view, $path, $info, $parent); } else { + // TODO(taminob): create symlink node here instead? return new File($this, $view, $path, $info, $parent); } } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 99216c7a03459..7af6e878c323e 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -753,7 +753,7 @@ public function getMetaData($path) { if ($data['mtime'] === false) { $data['mtime'] = time(); } - if ($data['mimetype'] == 'httpd/unix-directory') { + if ($data['mimetype'] == 'httpd/unix-directory') { // TODO(taminob): handle size of symlinks? $data['size'] = -1; //unknown } else { $data['size'] = $this->filesize($path); diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 35add2c606b42..6739c4058f1ed 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -622,6 +622,8 @@ private function getMetaFromPropfind(string $path, array $response): array { $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; if ($type === 'dir') { $mimeType = 'httpd/unix-directory'; + } elseif ($type === 'symlink') { // TODO(taminob): This type cannot occur at the moment + $mimeType = FileInfo::MIMETYPE_SYMLINK; } elseif (isset($response['{DAV:}getcontenttype'])) { $mimeType = $response['{DAV:}getcontenttype']; } else { From 3274c8ebacc5fe97e857d6a0f6bfee9f189e2b86 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 15 Nov 2023 17:07:08 +0100 Subject: [PATCH 13/51] feature(dav/connector): Draft for handling GET request for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 23 ++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index f7904e8788331..121c66a184df2 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -159,7 +159,8 @@ public function initialize(Server $server) { $this->server->on('propPatch', [$this, 'handleUpdateProperties']); $this->server->on('afterBind', [$this, 'sendFileIdHeader']); $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); - $this->server->on('afterMethod:GET', [$this,'httpGet']); + $this->server->on('method:GET', [$this,'httpGet']); + $this->server->on('afterMethod:GET', [$this,'afterHttpGet']); $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); $this->server->on('afterResponse', function ($request, ResponseInterface $response) { $body = $response->getBody(); @@ -224,13 +225,31 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface } } + public function httpGet(RequestInterface $request, ResponseInterface $response) { + // only handle symlinks + // TODO(taminob): does not work if not in cache (which it is currently not because the path here is /files/root/path/to/symlink (request path) and the path in cache is only /root/files/path/to/symlink (internal path)); to make it work without cache, e.g. \Sabre\DAV\Server::getResourceTypeForNode would have to be extended to handle symlinks + $node = $this->tree->getNodeForPath($request->getPath()); + if (!($node instanceof \OCA\DAV\Connector\Sabre\File)) { + return; + } + if ($node->getFileInfo()->getType() !== \OCP\Files\FileInfo::TYPE_SYMLINK) { + return; + } + + $response->addHeader('OC-File-Type', '1'); + $response->setBody($node->readlink()); + // do not continue processing this request + return false; + } + + /** * Add headers to file download * * @param RequestInterface $request * @param ResponseInterface $response */ - public function httpGet(RequestInterface $request, ResponseInterface $response) { + public function afterHttpGet(RequestInterface $request, ResponseInterface $response) { // Only handle valid files $node = $this->tree->getNodeForPath($request->getPath()); if (!($node instanceof IFile)) { From 3b02d617bf38cbe163dd5b40cc594e09e8a9332a Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 15 Nov 2023 17:15:34 +0100 Subject: [PATCH 14/51] Revert commits for storing actual symlinks on server This commits reverts the following commits: * 0a86ef7d7c92c4f5a6a274007604843a970d724f * af24f8519662e7628791ef0902b31042980078a0 * 3cbe098b910f4bbab723567e5f3800788cc48dc5 * 05073bec21160c1b2f8b4cea071e8657e528a4c3 * d36295e966eb6969f0643ce8e469eade9b95284a * eb7616898d179261d793822d1721b1282135de18 * d0f1a3efb38c3bffe260c80ace5d75bb3ad0d55f Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 6 +--- apps/dav/lib/Connector/Sabre/File.php | 20 ----------- apps/dav/lib/Connector/Sabre/Node.php | 1 - apps/dav/lib/Connector/Sabre/ObjectTree.php | 1 - .../dav/lib/Connector/Sabre/ServerFactory.php | 1 - .../lib/Notification/Notifier.php | 1 - apps/files_trashbin/lib/Helper.php | 14 ++------ .../lib/Sabre/AbstractTrashFolder.php | 1 - apps/files_trashbin/lib/Sabre/TrashRoot.php | 1 - lib/private/Encryption/Util.php | 5 ++- lib/private/Files/Cache/Scanner.php | 2 +- lib/private/Files/FileInfo.php | 9 ++--- lib/private/Files/Node/Folder.php | 1 - lib/private/Files/Node/HookConnector.php | 3 +- lib/private/Files/Node/Root.php | 1 - lib/private/Files/Storage/Common.php | 35 +++---------------- lib/private/Files/Storage/DAV.php | 2 -- lib/private/Files/Storage/Local.php | 11 +++--- lib/private/Files/Storage/Wrapper/Wrapper.php | 30 ---------------- lib/private/Files/Type/Detection.php | 4 +-- lib/private/Files/View.php | 31 +--------------- lib/public/Files/DavUtil.php | 2 +- lib/public/Files/FileInfo.php | 12 +------ lib/public/Files/Storage/IStorage.php | 28 --------------- 24 files changed, 25 insertions(+), 197 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index efa388b088877..db02d883a2881 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -210,7 +210,6 @@ public function createSymlink($name, $target) { throw new \Sabre\DAV\Exception\Forbidden(); } - // TODO: check if symlink target leaves data dir $this->fileView->verifyPath($this->path, $name); [$storage, $internalPath] = $this->fileView->resolvePath($this->path); @@ -219,7 +218,6 @@ public function createSymlink($name, $target) { } $internalPath = $internalPath . '/' . $name; - // TODO: use View::unlink instead and create View::symlink? $storage->unlink($internalPath); if (!$storage->symlink($target, $internalPath)) { throw new \Sabre\DAV\Exception\Forbidden("Could not create symlink '$name'!"); @@ -227,8 +225,7 @@ public function createSymlink($name, $target) { $storage->getUpdater()->update($internalPath); $newEtag = $storage->getETag($internalPath); $infoData = [ - 'type' => FileInfo::TYPE_SYMLINK, - 'mimetype' => FileInfo::MIMETYPE_SYMLINK, + 'type' => FileInfo::TYPE_FILE, 'etag' => $newEtag, ]; $path = \OC\Files\Filesystem::normalizePath($this->path . '/' . $name); @@ -282,7 +279,6 @@ public function getChild($name, $info = null, IRequest $request = null, IL10N $l if ($info->getMimeType() === FileInfo::MIMETYPE_FOLDER) { $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager); } else { - // TODO(taminob): create SymlinkNode here instead? $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager, $request, $l10n); } if ($this->tree) { diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 2eb274731a5ca..f188490fd938f 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -553,26 +553,6 @@ public function delete() { } } - /** - * Return symlink target of the file - * - * @return string|null - * @throws NotFound - * @throws Forbidden - * @throws FileLocked - */ - public function readlink() { - if (!$this->info->isReadable()) { - throw new NotFound(); - } - - try { - return $this->fileView->readlink($this->path); - } catch (\Exception $e) { - $this->convertToSabreException($e); - } - } - /** * Returns the mime-type for a file * diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index 5ca7938fd1022..c9407b127860d 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -113,7 +113,6 @@ protected function refreshInfo(): void { if ($this->info->getType() === FileInfo::TYPE_FOLDER) { $this->node = new Folder($root, $rootView, $this->path, $this->info); } else { - // TODO(taminob): create symlink node here instead? $this->node = new File($root, $rootView, $this->path, $this->info); } } diff --git a/apps/dav/lib/Connector/Sabre/ObjectTree.php b/apps/dav/lib/Connector/Sabre/ObjectTree.php index 1bf5b663bbeda..c129371e37632 100644 --- a/apps/dav/lib/Connector/Sabre/ObjectTree.php +++ b/apps/dav/lib/Connector/Sabre/ObjectTree.php @@ -175,7 +175,6 @@ public function getNodeForPath($path) { if ($info->getType() === 'dir') { $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this); } else { - // TODO(taminob): create symlink node here instead? $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info); } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 711139b781b75..828977fd81287 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -141,7 +141,6 @@ public function createServer(string $baseUri, if ($rootInfo->getType() === 'dir') { $root = new \OCA\DAV\Connector\Sabre\Directory($view, $rootInfo, $objectTree); } else { - // TODO(taminob): create symlink node here instead? $root = new \OCA\DAV\Connector\Sabre\File($view, $rootInfo); } $objectTree->init($root, $view, $this->mountManager); diff --git a/apps/files_reminders/lib/Notification/Notifier.php b/apps/files_reminders/lib/Notification/Notifier.php index 7f683676d8d87..f7ffa0b443754 100644 --- a/apps/files_reminders/lib/Notification/Notifier.php +++ b/apps/files_reminders/lib/Notification/Notifier.php @@ -105,7 +105,6 @@ public function prepare(INotification $notification, string $languageCode): INot $label = match ($node->getType()) { FileInfo::TYPE_FILE => $l->t('View file'), FileInfo::TYPE_FOLDER => $l->t('View folder'), - FileInfo::TYPE_SYMLINK => $l->t('View symlink'), }; $this->addActionButton($notification, $label); diff --git a/apps/files_trashbin/lib/Helper.php b/apps/files_trashbin/lib/Helper.php index 28169ee0fd014..61d8eb9c715a4 100644 --- a/apps/files_trashbin/lib/Helper.php +++ b/apps/files_trashbin/lib/Helper.php @@ -32,6 +32,7 @@ use OC\Files\FileInfo; use OCP\Constants; +use OCP\Files\Cache\ICacheEntry; class Helper { /** @@ -81,20 +82,11 @@ public static function getTrashFiles($dir, $user, $sortAttribute = '', $sortDesc $originalPath = substr($originalPath, 0, -1); } } - if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) { - $type = FileInfo::TYPE_FOLDER; - $mimetype = FileInfo::MIMETYPE_FOLDER; - } elseif ($entry->getMimeType() === FileInfo::MIMETYPE_SYMLINK) { - $type = FileInfo::TYPE_SYMLINK; - $mimetype = FileInfo::MIMETYPE_SYMLINK; - } else { - $type = FileInfo::TYPE_FILE; - $mimetype = \OC::$server->get(IMimeTypeDetector::class)->detectPath($name); - } + $type = $entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE ? 'dir' : 'file'; $i = [ 'name' => $name, 'mtime' => $timestamp, - 'mimetype' => $mimetype, + 'mimetype' => $type === 'dir' ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($name), 'type' => $type, 'directory' => ($dir === '/') ? '' : $dir, 'size' => $entry->getSize(), diff --git a/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php b/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php index a7b412ecd84d6..8a61823cf1029 100644 --- a/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php +++ b/apps/files_trashbin/lib/Sabre/AbstractTrashFolder.php @@ -36,7 +36,6 @@ public function getChildren(): array { $entries = $this->trashManager->listTrashFolder($this->data); $children = array_map(function (ITrashItem $entry) { - // TODO(taminob): probably not necessary, but check if TrashFolderSymlink is required if ($entry->getType() === FileInfo::TYPE_FOLDER) { return new TrashFolderFolder($this->trashManager, $entry); } diff --git a/apps/files_trashbin/lib/Sabre/TrashRoot.php b/apps/files_trashbin/lib/Sabre/TrashRoot.php index b296c7c322382..b61daacdf432f 100644 --- a/apps/files_trashbin/lib/Sabre/TrashRoot.php +++ b/apps/files_trashbin/lib/Sabre/TrashRoot.php @@ -75,7 +75,6 @@ public function getChildren(): array { $entries = $this->trashManager->listTrashRoot($this->user); $children = array_map(function (ITrashItem $entry) { - // TODO(taminob): probably not necessary, but check if TrashSymlink is required if ($entry->getType() === FileInfo::TYPE_FOLDER) { return new TrashFolder($this->trashManager, $entry); } diff --git a/lib/private/Encryption/Util.php b/lib/private/Encryption/Util.php index 5f5660568c9b7..a828483265b5a 100644 --- a/lib/private/Encryption/Util.php +++ b/lib/private/Encryption/Util.php @@ -154,7 +154,6 @@ public function createHeader(array $headerData, IEncryptionModule $encryptionMod /** * go recursively through a dir and collect all files and sub files. - * the resulting list will not contain symlinks * * @param string $dir relative to the users files folder * @return array with list of files relative to the users files folder @@ -168,9 +167,9 @@ public function getAllFiles($dir) { $content = $this->rootView->getDirectoryContent($dir); foreach ($content as $c) { - if ($c->getType() === \OCP\Files\FileInfo::TYPE_FOLDER) { + if ($c->getType() === 'dir') { $dirList[] = $c->getPath(); - } elseif($c->getType() === \OCP\Files\FileInfo::TYPE_FILE) { + } else { $result[] = $c->getPath(); } } diff --git a/lib/private/Files/Cache/Scanner.php b/lib/private/Files/Cache/Scanner.php index b1f3cb76e4071..2711fc8ad19cd 100644 --- a/lib/private/Files/Cache/Scanner.php +++ b/lib/private/Files/Cache/Scanner.php @@ -169,7 +169,7 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = try { if ($data) { // pre-emit only if it was a file. By that we avoid counting/treating folders as files - if ($data['mimetype'] !== 'httpd/unix-directory') { // TODO(taminob): maybe also check for symlink? + if ($data['mimetype'] !== 'httpd/unix-directory') { $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]); \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]); } diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php index 5e45ff20cef7c..5ba2f27b78b69 100644 --- a/lib/private/Files/FileInfo.php +++ b/lib/private/Files/FileInfo.php @@ -242,16 +242,11 @@ public function getPermissions() { } /** - * @return string \OCP\Files\FileInfo::TYPE_FILE| - * \OCP\Files\FileInfo::TYPE_FOLDER - * \OCP\Files\FileInfo::TYPE_SYMLINK + * @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER */ public function getType() { if (!isset($this->data['type'])) { - $this->data['type'] = $this->getMimetype() === self::MIMETYPE_FOLDER ? - self::TYPE_FOLDER : ($this->getMimetype() === self::MIMETYPE_SYMLINK ? - self::TYPE_SYMLINK : - self::TYPE_FILE); + $this->data['type'] = ($this->getMimetype() === self::MIMETYPE_FOLDER) ? self::TYPE_FOLDER : self::TYPE_FILE; } return $this->data['type']; } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index e7d96110ab26f..c7462572fed06 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -120,7 +120,6 @@ protected function createNode(string $path, ?FileInfo $info = null, bool $infoHa if ($isDir) { return new Folder($this->root, $this->view, $path, $info, $parent, $infoHasSubMountsIncluded); } else { - // TODO(taminob): create symlink here instead of file? return new File($this->root, $this->view, $path, $info, $parent); } } diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index 74cbc38f5fa42..f61eedee66e55 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -229,8 +229,7 @@ private function getNodeForPath(string $path): Node { return new NonExistingFile($this->root, $this->view, $fullPath, $info); } } - // TODO(taminob): create symlink node instead of file here - if ($info->getType() === FileInfo::TYPE_FILE || $info->getType() === FileInfo::TYPE_SYMLINK) { + if ($info->getType() === FileInfo::TYPE_FILE) { return new File($this->root, $this->view, $info->getPath(), $info); } else { return new Folder($this->root, $this->view, $info->getPath(), $info); diff --git a/lib/private/Files/Node/Root.php b/lib/private/Files/Node/Root.php index 341e81bf3a31d..ee344f9be8b41 100644 --- a/lib/private/Files/Node/Root.php +++ b/lib/private/Files/Node/Root.php @@ -510,7 +510,6 @@ public function getNodeFromCacheEntryAndMount(ICacheEntry $cacheEntry, IMountPoi if ($isDir) { return new Folder($this, $view, $path, $info, $parent); } else { - // TODO(taminob): create symlink node here instead? return new File($this, $view, $path, $info, $parent); } } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 7af6e878c323e..3d5a2f098b2ac 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -48,7 +48,6 @@ use OC\Files\Cache\Updater; use OC\Files\Cache\Watcher; use OC\Files\Filesystem; -use OC\Files\FileInfo; use OC\Files\Storage\Wrapper\Jail; use OC\Files\Storage\Wrapper\Wrapper; use OCP\Files\EmptyFileNameException; @@ -107,33 +106,13 @@ public function __construct($parameters) { protected function remove($path) { if ($this->is_dir($path)) { return $this->rmdir($path); - } elseif ($this->is_file($path) || $this->is_link($path)) { + } elseif ($this->is_file($path)) { return $this->unlink($path); } else { return false; } } - /** - * Default implementation to ensure backwards compatibility. - * Should be overwritten by any storage class that can provide this operation. - * - * @throws \OCP\Files\NotPermittedException - */ - public function readlink($path) { - throw new \OCP\Files\NotPermittedException("Storage does not provide an implementation for 'readlink'"); - } - - /** - * Default implementation to ensure backwards compatibility. - * Should be overwritten by any storage class that can provide this operation. - * - * @throws \OCP\Files\NotPermittedException - */ - public function symlink($target, $link) { - throw new \OCP\Files\NotPermittedException("Storage does not provide an implementation for 'symlink'"); - } - public function is_dir($path) { return $this->filetype($path) === 'dir'; } @@ -142,10 +121,6 @@ public function is_file($path) { return $this->filetype($path) === 'file'; } - public function is_link($path) { - return $this->filetype($path) === 'link'; - } - public function filesize($path): false|int|float { if ($this->is_dir($path)) { return 0; //by definition @@ -277,11 +252,9 @@ public function copy($source, $target) { public function getMimeType($path) { if ($this->is_dir($path)) { - return FileInfo::MIMETYPE_FOLDER; - } elseif ($this->is_link($path)) { - return FileInfo::MIMETYPE_SYMLINK; + return 'httpd/unix-directory'; } elseif ($this->file_exists($path)) { - return \OC::$server->get(IMimeTypeDetector::class)->detectPath($path); + return \OC::$server->getMimeTypeDetector()->detectPath($path); } else { return false; } @@ -753,7 +726,7 @@ public function getMetaData($path) { if ($data['mtime'] === false) { $data['mtime'] = time(); } - if ($data['mimetype'] == 'httpd/unix-directory') { // TODO(taminob): handle size of symlinks? + if ($data['mimetype'] == 'httpd/unix-directory') { $data['size'] = -1; //unknown } else { $data['size'] = $this->filesize($path); diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 6739c4058f1ed..35add2c606b42 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -622,8 +622,6 @@ private function getMetaFromPropfind(string $path, array $response): array { $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file'; if ($type === 'dir') { $mimeType = 'httpd/unix-directory'; - } elseif ($type === 'symlink') { // TODO(taminob): This type cannot occur at the moment - $mimeType = FileInfo::MIMETYPE_SYMLINK; } elseif (isset($response['{DAV:}getcontenttype'])) { $mimeType = $response['{DAV:}getcontenttype']; } else { diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 2f986f676bae2..660d68adc7a8a 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -341,14 +341,17 @@ public function file_put_contents($path, $data) { return $result; } + /** + * create symlink + * + * @param string $path + * @param string $target + * @return bool + */ public function symlink($target, $path) { return symlink($target, $this->getSourcePath($path)); } - public function readlink($path) { - return readlink($this->getSourcePath($path)); - } - public function unlink($path) { if ($this->is_dir($path)) { return $this->rmdir($path); diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index b2b1d1b50c83b..9f5564b449038 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -104,26 +104,6 @@ public function opendir($path) { return $this->getWrapperStorage()->opendir($path); } - /** - * see https://www.php.net/manual/en/function.symlink.php - * - * @param string $path - * @return string|false - */ - public function symlink($target, $link) { - return $this->getWrapperStorage()->symlink($target, $link); - } - - /** - * see https://www.php.net/manual/en/function.readlink.php - * - * @param string $path - * @return string|false - */ - public function readlink($path) { - return $this->getWrapperStorage()->readlink($path); - } - /** * see https://www.php.net/manual/en/function.is_dir.php * @@ -144,16 +124,6 @@ public function is_file($path) { return $this->getWrapperStorage()->is_file($path); } - /** - * see https://www.php.net/manual/en/function.is_link.php - * - * @param string $path - * @return bool - */ - public function is_link($path) { - return $this->getWrapperStorage()->is_link($path); - } - /** * see https://www.php.net/manual/en/function.stat.php * only the following keys are required in the result: size and mtime diff --git a/lib/private/Files/Type/Detection.php b/lib/private/Files/Type/Detection.php index 4bbaf9a3d5d7e..71b8cb986d71a 100644 --- a/lib/private/Files/Type/Detection.php +++ b/lib/private/Files/Type/Detection.php @@ -228,9 +228,7 @@ public function detectContent(string $path): string { if (@is_dir($path)) { // directories are easy - return \OCP\Files\FileInfo::MIMETYPE_FOLDER; - } elseif (@is_link($path)) { - return \OCP\Files\FileInfo::MIMETYPE_SYMLINK; + return 'httpd/unix-directory'; } if (function_exists('finfo_open') diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index bac2e81c2b632..6eefb093795dd 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -410,33 +410,6 @@ public function readfile($path) { return false; } - /** - * Return symlink target of given symlink - * - * @param string $path - * @return mixed - * @throws LockedException - * @throws InvalidPathException - */ - public function readlink($path) { - $this->assertPathLength($path); - return $this->basicOperation('readlink', $path, ['read']); - } - - /** - * Create symlink at given path to the specified target - * - * @param string $target - * @param string $path - * @return mixed - * @throws LockedException - * @throws InvalidPathException - */ - public function symlink($target, $path) { - $this->assertPathLength($path); - return $this->basicOperation('symlink', $path, ['create', 'write'], $target); - } - /** * @param string $path * @param int $from @@ -1539,9 +1512,7 @@ public function getDirectoryContent($directory, $mimetype_filter = '', \OCP\File } } else { //mountpoint in this folder, add an entry for it $rootEntry['name'] = $relativePath; - $rootEntry['type'] = $rootEntry['mimetype'] === FileInfo::MIMETYPE_FOLDER ? - FileInfo::TYPE_FOLDER : ($rootEntry['mimetype'] === FileInfo::MIMETYPE_SYMLINK ? - FileInfo::TYPE_SYMLINK : FileInfo::TYPE_FILE); + $rootEntry['type'] = $rootEntry['mimetype'] === 'httpd/unix-directory' ? 'dir' : 'file'; $permissions = $rootEntry['permissions']; // do not allow renaming/deleting the mount point if they are not shared files/folders // for shared files/folders we use the permissions given by the owner diff --git a/lib/public/Files/DavUtil.php b/lib/public/Files/DavUtil.php index d60ba64100ed8..89fd3b1864341 100644 --- a/lib/public/Files/DavUtil.php +++ b/lib/public/Files/DavUtil.php @@ -94,7 +94,7 @@ public static function getDavPermissions(FileInfo $info): string { if ($isWritable) { $p .= 'W'; } - } elseif ($info->getType() === FileInfo::TYPE_FOLDER) { + } else { if ($permissions & Constants::PERMISSION_CREATE) { $p .= 'CK'; } diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index 822da2473fb80..817b03dfc65f1 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -45,10 +45,6 @@ interface FileInfo { * @since 7.0.0 */ public const TYPE_FOLDER = 'dir'; - /** - * @since TODO - */ - public const TYPE_SYMLINK = 'symlink'; /** * @const \OCP\Files\FileInfo::SPACE_NOT_COMPUTED Return value for a not computed space value @@ -70,10 +66,6 @@ interface FileInfo { * @since 9.1.0 */ public const MIMETYPE_FOLDER = 'httpd/unix-directory'; - /** - * @since TODO - */ - public const MIMETYPE_SYMLINK = 'inode/symlink'; /** * @const \OCP\Files\FileInfo::BLACKLIST_FILES_REGEX Return regular expression to test filenames against (blacklisting) @@ -187,9 +179,7 @@ public function getPermissions(); /** * Check whether this is a file or a folder * - * @return string \OCP\Files\FileInfo::TYPE_FILE| - * \OCP\Files\FileInfo::TYPE_FOLDER| - * \OCP\Files\FileInfo::TYPE_SYMLINK + * @return string \OCP\Files\FileInfo::TYPE_FILE|\OCP\Files\FileInfo::TYPE_FOLDER * @since 7.0.0 */ public function getType(); diff --git a/lib/public/Files/Storage/IStorage.php b/lib/public/Files/Storage/IStorage.php index 902828241dea9..00e98fdfbb6e6 100644 --- a/lib/public/Files/Storage/IStorage.php +++ b/lib/public/Files/Storage/IStorage.php @@ -108,15 +108,6 @@ public function is_dir($path); */ public function is_file($path); - /** - * see https://www.php.net/manual/en/function.is-link.php - * - * @param string $path - * @return bool - * @since TODO - */ - public function is_link($path); - /** * see https://www.php.net/manual/en/function.stat.php * only the following keys are required in the result: size and mtime @@ -247,25 +238,6 @@ public function file_put_contents($path, $data); */ public function unlink($path); - /** - * see https://www.php.net/manual/en/function.symlink.php - * - * @param string $target - * @param string $link - * @return bool - * @since TODO - */ - public function symlink($target, $link); - - /** - * see https://www.php.net/manual/en/function.readlink.php - * - * @param string $path - * @return string|false - * @since TODO - */ - public function readlink($path); - /** * see https://www.php.net/manual/en/function.rename.php * From 88f510e5c5bb03e5a32157dc320e28a5b8361d30 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 15 Nov 2023 20:38:08 +0100 Subject: [PATCH 15/51] First draft for metadata-only symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 21 ++++++++------------ apps/dav/lib/Connector/Sabre/FilesPlugin.php | 13 ++++++------ apps/dav/lib/Upload/ChunkingV2Plugin.php | 19 +++++++----------- lib/public/Files/FileInfo.php | 4 ++++ 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 52623ed8c6c78..d4615abcf6ac1 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -100,21 +100,16 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) } if (isset($headers['oc-file-type']) && $headers['oc-file-type'] == 1) { - // TODO: store default value in global location - $allowSymlinks = \OC::$server->get(\OC\AllConfig::class)->getSystemValueBool( - 'localstorage.allowsymlinks', false); - if (!$allowSymlinks) { - throw new Forbidden("Server does not allow the creation of symlinks!"); - } $symlinkPath = $headers['x-file-path']; - $parentNode = $this->server->tree->getNodeForPath(dirname($symlinkPath)); - if(!$parentNode instanceof \OCA\DAV\Connector\Sabre\Directory) { - throw new Exception("Unable to upload '$symlinkPath' because the remote directory does not support symlink creation!"); - } - $etag = $parentNode->createSymlink(basename($symlinkPath), $content); - $writtenFiles[$headers['x-file-path']] = [ + $newEtag = "'$symlinkPath'->'$content'"; + $infoData = [ + 'type' => \OC\Files\FileInfo::TYPE_SYMLINK, + 'etag' => $newEtag, + ]; + \OC\Files\Filesystem::getView()->putFileInfo($symlinkPath, $infoData); + $writtenFiles[$symlinkPath] = [ "error" => false, - "etag" => $etag, + "etag" => $newEtag, ]; continue; } diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 121c66a184df2..68c623ae359d5 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -227,17 +227,18 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface public function httpGet(RequestInterface $request, ResponseInterface $response) { // only handle symlinks - // TODO(taminob): does not work if not in cache (which it is currently not because the path here is /files/root/path/to/symlink (request path) and the path in cache is only /root/files/path/to/symlink (internal path)); to make it work without cache, e.g. \Sabre\DAV\Server::getResourceTypeForNode would have to be extended to handle symlinks - $node = $this->tree->getNodeForPath($request->getPath()); - if (!($node instanceof \OCA\DAV\Connector\Sabre\File)) { + $symlinkPath = $request->getPath(); + $fileInfo = \OC\Files\Filesystem::getView()->getFileInfo($symlinkPath); + if (!$fileInfo || $fileInfo->getType() !== \OC\Files\FileInfo::TYPE_SYMLINK) { return; } - if ($node->getFileInfo()->getType() !== \OCP\Files\FileInfo::TYPE_SYMLINK) { - return; + $symlinkTarget = $fileInfo->getMetadata()['symlinkTarget']; + if (isset($symlinkTarget)) { + throw new NotFound("Symlink has no target!"); } $response->addHeader('OC-File-Type', '1'); - $response->setBody($node->readlink()); + $response->setBody($symlinkTarget); // do not continue processing this request return false; } diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php index 867e12ed5235d..b0e4cb487b5ee 100644 --- a/apps/dav/lib/Upload/ChunkingV2Plugin.php +++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php @@ -148,20 +148,15 @@ public function afterMkcol(RequestInterface $request, ResponseInterface $respons public function beforePut(RequestInterface $request, ResponseInterface $response): bool { if ($request->getHeader('OC-File-Type') == 1) { - // TODO: store default value in global location - $allowSymlinks = \OC::$server->get(\OC\AllConfig::class)->getSystemValueBool( - 'localstorage.allowsymlinks', false); - if (!$allowSymlinks) { - throw new Forbidden("Server does not allow the creation of symlinks!"); - } $symlinkPath = $request->getPath(); $symlinkTarget = $request->getBodyAsString(); - $parentNode = $this->server->tree->getNodeForPath(dirname($symlinkPath)); - if(!$parentNode instanceof \OCA\DAV\Connector\Sabre\Directory) { - throw new Exception("Unable to upload '$symlinkPath' because the remote directory does not support symlink creation!"); - } - $etag = $parentNode->createSymlink(basename($symlinkPath), $symlinkTarget); - $response->setHeader("OC-ETag", $etag); + $newEtag = "'$symlinkPath'->'$symlinkTarget'"; + $infoData = [ + 'type' => \OC\Files\FileInfo::TYPE_SYMLINK, + 'etag' => $newEtag, + ]; + \OC\Files\Filesystem::getView()->putFileInfo($symlinkPath, $infoData); + $response->setHeader("OC-ETag", $newEtag); $response->setStatus(201); $this->server->sapi->sendResponse($response); return false; diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index 817b03dfc65f1..6d843bccd78a7 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -45,6 +45,10 @@ interface FileInfo { * @since 7.0.0 */ public const TYPE_FOLDER = 'dir'; + /** + * @since TODO + */ + public const TYPE_SYMLINK = 'symlink'; /** * @const \OCP\Files\FileInfo::SPACE_NOT_COMPUTED Return value for a not computed space value From b53772764d44e14d321b6fdd387eb8d4db9bb57c Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 15 Nov 2023 21:35:09 +0100 Subject: [PATCH 16/51] Initial draft for PROPFIND for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 68c623ae359d5..bda816f4d998c 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -160,6 +160,7 @@ public function initialize(Server $server) { $this->server->on('afterBind', [$this, 'sendFileIdHeader']); $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); $this->server->on('method:GET', [$this,'httpGet']); + $this->server->on('method:PROPFIND', [$this, 'httpPropFind']); $this->server->on('afterMethod:GET', [$this,'afterHttpGet']); $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); $this->server->on('afterResponse', function ($request, ResponseInterface $response) { @@ -225,6 +226,76 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface } } + /** + * Handle PROPFIND http requests for symlinks + * @param RequestInterface $request + * @param ResponseInterface $response + */ + public function httpPropFind(RequestInterface $request, ResponseInterface $response) + { + // only handle symlinks + // TODO: probably doesn't work because PROPFIND will be called on root dir and children are returned + $symlinkPath = $request->getPath(); + $fileInfo = \OC\Files\Filesystem::getView()->getFileInfo($symlinkPath); + if (!$fileInfo || $fileInfo->getType() !== \OC\Files\FileInfo::TYPE_SYMLINK) { + return; + } + + $requestBody = $request->getBodyAsString(); + if (strlen($requestBody)) { + try { + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); + } catch (\Sabre\XML\ParseException $e) { + throw new \Sabre\DAV\Exception\BadRequest($e->getMessage(), 0, $e); + } + } else { + $propFindXml = new \Sabre\DAV\Xml\Request\PropFind(); + $propFindXml->allProp = true; + $propFindXml->properties = []; + } + + $newProperties = []; + foreach ($propFindXml->properties as $property) { + if ($property === self::GETETAG_PROPERTYNAME) { + $newProperties[$property] = [200, $fileInfo->getETag()]; + } elseif ($property === self::LASTMODIFIED_PROPERTYNAME) { + // TODO + } elseif ($property === self::CREATIONDATE_PROPERTYNAME) { + // TODO + } elseif ($property === self::UPLOAD_TIME_PROPERTYNAME) { + // TODO + } elseif ($property === self::SIZE_PROPERTYNAME) { + $newProperties[$property] = [200, $fileInfo->getSize()]; + } else { // TODO: handle other properties that make sense for symlinks + $newProperties[$property] = [404]; + } + } + + // This is a multi-status response + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + // Normally this header is only needed for OPTIONS responses, however.. + // iCal seems to also depend on these being set for PROPFIND. Since + // this is not harmful, we'll add it. + $features = ['1', '3', 'extended-mkcol']; + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + $response->setHeader('DAV', implode(', ', $features)); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = 'minimal' === $prefer['return']; + + $data = $this->server->generateMultiStatus($newProperties, $minimal); + $response->setBody($data); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + public function httpGet(RequestInterface $request, ResponseInterface $response) { // only handle symlinks $symlinkPath = $request->getPath(); From 7f18076019e4d7660f169ecddef2e78be3ff5fcc Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 22 Nov 2023 21:16:59 +0100 Subject: [PATCH 17/51] Revert "Initial draft for PROPFIND for symlinks" This reverts commit 9d681ddf810f9d82394fcc1bade83bd0afdc15ed. Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 71 -------------------- 1 file changed, 71 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index bda816f4d998c..68c623ae359d5 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -160,7 +160,6 @@ public function initialize(Server $server) { $this->server->on('afterBind', [$this, 'sendFileIdHeader']); $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); $this->server->on('method:GET', [$this,'httpGet']); - $this->server->on('method:PROPFIND', [$this, 'httpPropFind']); $this->server->on('afterMethod:GET', [$this,'afterHttpGet']); $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); $this->server->on('afterResponse', function ($request, ResponseInterface $response) { @@ -226,76 +225,6 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface } } - /** - * Handle PROPFIND http requests for symlinks - * @param RequestInterface $request - * @param ResponseInterface $response - */ - public function httpPropFind(RequestInterface $request, ResponseInterface $response) - { - // only handle symlinks - // TODO: probably doesn't work because PROPFIND will be called on root dir and children are returned - $symlinkPath = $request->getPath(); - $fileInfo = \OC\Files\Filesystem::getView()->getFileInfo($symlinkPath); - if (!$fileInfo || $fileInfo->getType() !== \OC\Files\FileInfo::TYPE_SYMLINK) { - return; - } - - $requestBody = $request->getBodyAsString(); - if (strlen($requestBody)) { - try { - $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); - } catch (\Sabre\XML\ParseException $e) { - throw new \Sabre\DAV\Exception\BadRequest($e->getMessage(), 0, $e); - } - } else { - $propFindXml = new \Sabre\DAV\Xml\Request\PropFind(); - $propFindXml->allProp = true; - $propFindXml->properties = []; - } - - $newProperties = []; - foreach ($propFindXml->properties as $property) { - if ($property === self::GETETAG_PROPERTYNAME) { - $newProperties[$property] = [200, $fileInfo->getETag()]; - } elseif ($property === self::LASTMODIFIED_PROPERTYNAME) { - // TODO - } elseif ($property === self::CREATIONDATE_PROPERTYNAME) { - // TODO - } elseif ($property === self::UPLOAD_TIME_PROPERTYNAME) { - // TODO - } elseif ($property === self::SIZE_PROPERTYNAME) { - $newProperties[$property] = [200, $fileInfo->getSize()]; - } else { // TODO: handle other properties that make sense for symlinks - $newProperties[$property] = [404]; - } - } - - // This is a multi-status response - $response->setStatus(207); - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); - $response->setHeader('Vary', 'Brief,Prefer'); - - // Normally this header is only needed for OPTIONS responses, however.. - // iCal seems to also depend on these being set for PROPFIND. Since - // this is not harmful, we'll add it. - $features = ['1', '3', 'extended-mkcol']; - foreach ($this->server->getPlugins() as $plugin) { - $features = array_merge($features, $plugin->getFeatures()); - } - $response->setHeader('DAV', implode(', ', $features)); - - $prefer = $this->server->getHTTPPrefer(); - $minimal = 'minimal' === $prefer['return']; - - $data = $this->server->generateMultiStatus($newProperties, $minimal); - $response->setBody($data); - - // Sending back false will interrupt the event chain and tell the server - // we've handled this method. - return false; - } - public function httpGet(RequestInterface $request, ResponseInterface $response) { // only handle symlinks $symlinkPath = $request->getPath(); From 4f7ae291f7ae96b82a2265e0859557f1a406d015 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 22 Nov 2023 21:26:13 +0100 Subject: [PATCH 18/51] Draft for symlink mimetype on regular file for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 22 ++++++++------------ apps/dav/lib/Connector/Sabre/FilesPlugin.php | 9 ++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index d4615abcf6ac1..c4a8c5aee2b7d 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -99,25 +99,21 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) $mtime = null; } + $node = $this->userFolder->newFile($headers['x-file-path'], $content); + $node->touch($mtime); + $node = $this->userFolder->getById($node->getId())[0]; + if (isset($headers['oc-file-type']) && $headers['oc-file-type'] == 1) { - $symlinkPath = $headers['x-file-path']; - $newEtag = "'$symlinkPath'->'$content'"; $infoData = [ 'type' => \OC\Files\FileInfo::TYPE_SYMLINK, - 'etag' => $newEtag, - ]; - \OC\Files\Filesystem::getView()->putFileInfo($symlinkPath, $infoData); - $writtenFiles[$symlinkPath] = [ - "error" => false, - "etag" => $newEtag, + 'etag' => $node->getEtag(), + 'mimetype' => 'symlink', // TODO: good news: this actually works! will be stored in database and will be retrieved when querying cache. bad news, files:scan will override the mimetype and it will be a regular file again :( ]; - continue; + $path = $headers['x-file-path']; + $path = trim($path, '/'); + \OC\Files\Filesystem::getView()->putFileInfo($path, $infoData); } - $node = $this->userFolder->newFile($headers['x-file-path'], $content); - $node->touch($mtime); - $node = $this->userFolder->getById($node->getId())[0]; - $writtenFiles[$headers['x-file-path']] = [ "error" => false, "etag" => $node->getETag(), diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 68c623ae359d5..f213a5ad1f410 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -73,6 +73,7 @@ class FilesPlugin extends ServerPlugin { public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate'; public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname'; + public const RESOURCETYPE_PROPERTYNAME = '{DAV:}resourcetype'; public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; @@ -451,6 +452,14 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) { return $node->getFileInfo()->getUploadTime(); }); + + $propFind->handle(self::RESOURCETYPE_PROPERTYNAME, function() use ($node) { + $info = $node->getFileInfo(); + if ($info->getType() == \OC\Files\FileInfo::TYPE_SYMLINK || $info->getMimetype() == 'symlink') { + return '{DAV:}symlink'; + } + return null; + }); } if ($node instanceof Directory) { From 73486056ae0619ba3b3dbdc19a2b293cfe9ea5eb Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 22 Nov 2023 21:27:32 +0100 Subject: [PATCH 19/51] SymlinkManager: Initial implementation for symlink database management Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 211 +++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 lib/private/Files/SymlinkManager.php diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php new file mode 100644 index 0000000000000..3854881f93bf8 --- /dev/null +++ b/lib/private/Files/SymlinkManager.php @@ -0,0 +1,211 @@ + + * + * @author Tamino Bauknecht + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCP\Files; + +use OCP\Files\Storage\IStorage; +use OCP\IDBConnection; + +/** + * Class to manage symlink representation on server + */ +class SymlinkManager { + /** + * @var \OCP\IDBConnection + */ + protected $connection; + + /** + * @var string + */ + protected const TABLE_NAME = 'symlinks'; + + /** + * @param IStorage $storage + */ + public function __construct() { + $this->connection = \OC::$server->get(IDBConnection::class); + } + + /** + * Check if given node is a symlink + * + * @param \OCP\Files\Node $node + * + * @return bool + */ + public function isSymlink($node) { + return $this->getId($node) === false; + } + + /** + * Store given node in database + * + * @param \OCP\Files\Node $node + */ + public function storeSymlink($node) { + if ($this->isSymlink($node)) { + $this->insertSymlink($node); + } else { + $this->updateSymlink($node); + } + } + + /** + * Delete given node from database + * + * @param \OCP\Files\Node $node + * + * @return bool + */ + public function deleteSymlink($node) { + $id = $this->getId($node); + if ($id === false) { + return false; + } + + return $this->deleteSymlinkById($id); + } + + /** + * Delete all symlinks that have no file representation in filesystem. + * Optionally, a path can be given to only purge symlinks that are recursively located in the path. + * + * @param string $path + */ + public function purgeSymlink($path = '/') { + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from(self::TABLE_NAME) + ->where($query->expr()->like('storage', $query->createNamedParameter($path . '%'))); + $result = $query->executeQuery(); + + while ($row = $result->fetch()) { + if (!\OC\Files\Filesystem::file_exists($row['path'])) { + $this->deleteSymlinkById($row['id']); + } + } + } + + /** + * @param \OCP\Files\Node $node + * + * @return int|false + */ + private function getId($node) { + $name = $this->getNameFromNode($node); + $storageId = $this->getStorageIdFromNode($node); + $path = $this->getPathFromNode($node); + + $query = $this->connection->getQueryBuilder(); + $query->select('id') + ->from(self::TABLE_NAME) + ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId))) + ->andWhere($query->expr()->eq('path', $query->createNamedParameter($path))) + ->andWhere($query->expr()->eq('name', $query->createNamedParameter($name))); + $result = $query->executeQuery(); + + if ($result->rowCount() > 1) { + throw new \OCP\DB\Exception("Node ('$name', '$storageId', '$path') is not unique in database!"); + } + + return $result->fetchOne(); + } + + /** + * @param \OCP\Files\Node $node + */ + private function updateSymlink($node) { + $name = $this->getNameFromNode($node); + $storageId = $this->getStorageIdFromNode($node); + $path = $this->getPathFromNode($node); + $lastUpdated = $this->getLastUpdatedFromNode($node); + + $query = $this->connection->getQueryBuilder(); + $query->update(self::TABLE_NAME) + ->set('name', $query->createNamedParameter($name)) + ->set('storage', $query->createNamedParameter($storageId)) + ->set('path', $query->createNamedParameter($path)) + ->set('last_updated', $query->createNamedParameter($lastUpdated)); + } + + /** + * @param \OCP\Files\Node $node + */ + private function insertSymlink($node) { + $name = $this->getNameFromNode($node); + $storageId = $this->getStorageIdFromNode($node); + $path = $this->getPathFromNode($node); + $lastUpdated = $this->getLastUpdatedFromNode($node); + + $query = $this->connection->getQueryBuilder(); + $query->insert(self::TABLE_NAME) + ->setValue('name', $query->createNamedParameter($name)) + ->setValue('storage', $query->createNamedParameter($storageId)) + ->setValue('path', $query->createNamedParameter($path)) + ->setValue('last_updated', $query->createNamedParameter($lastUpdated)); + } + + /** + * @param \OCP\Files\Node $node + * + * @return bool + */ + private function deleteSymlinkById($id) { + $query = $this->connection->getQueryBuilder(); + $query->delete(self::TABLE_NAME) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $rowsChanged = $query->executeStatement(); + if ($rowsChanged > 1) { + throw new \OCP\DB\Exception("Too many symlink rows deleted!"); + } + return $rowsChanged == 1; + } + + /** + * @param \OCP\Files\Node $node + */ + private function getNameFromNode($node) { + return $node->getName(); + } + + /** + * @param \OCP\Files\Node $node + */ + private function getStorageIdFromNode($node) { + return $node->getStorage()->getId(); + } + + /** + * @param \OCP\Files\Node $node + */ + private function getPathFromNode($node) { + return $node->getPath(); + } + + /** + * @param \OCP\Files\Node $node + */ + private function getLastUpdatedFromNode($node) { + return $node->getMtime(); + } +} From 15f6ac6fddd0e9a3b841aa3ac51e435513e2b1bd Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 22 Nov 2023 21:28:10 +0100 Subject: [PATCH 20/51] SymlinkManager: Introduce symlink table via database migration Signed-off-by: Tamino Bauknecht --- core/Migrations/SymlinkSupportMigration.php | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 core/Migrations/SymlinkSupportMigration.php diff --git a/core/Migrations/SymlinkSupportMigration.php b/core/Migrations/SymlinkSupportMigration.php new file mode 100644 index 0000000000000..a97731796fd79 --- /dev/null +++ b/core/Migrations/SymlinkSupportMigration.php @@ -0,0 +1,86 @@ + + * + * @author Tamino Bauknecht + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Introduce symlinks table + */ +class SymlinkSupportMigration extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('symlinks')) { + $table = $schema->createTable('symlinks'); + + $table->addColumn('id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 64, + 'autoincrement' => true, + ]); + $table->addColumn('storage', Types::BIGINT, [ + 'notnull' => true, + 'length' => 20, + 'default' => 0, + ]); + $table->addColumn('path', Types::STRING, [ + 'notnull' => true, + 'length' => 4000, + ]); + $table->addColumn('name', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $table->addColumn('last_updated', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'default' => 0, + 'unsigned' => true, + ]); + + $table->setPrimaryKey(['id'], 'symlinks_id_index'); + $table->addUniqueIndex(['path'], 'symlinks_path_index'); + $table->addIndex(['last_updated'], 'symlinks_updated'); + + return $schema; + } + + return null; + } +} From d182a3a28724efbc66c3d2283dcb81646bb7a2c8 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:18:08 +0100 Subject: [PATCH 21/51] SymlinkManager: Accept FileInfo type and escape path for purgeSymlink Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index 3854881f93bf8..f93845efa479a 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -49,7 +49,7 @@ public function __construct() { /** * Check if given node is a symlink * - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node * * @return bool */ @@ -60,7 +60,7 @@ public function isSymlink($node) { /** * Store given node in database * - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ public function storeSymlink($node) { if ($this->isSymlink($node)) { @@ -73,7 +73,7 @@ public function storeSymlink($node) { /** * Delete given node from database * - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node * * @return bool */ @@ -93,10 +93,11 @@ public function deleteSymlink($node) { * @param string $path */ public function purgeSymlink($path = '/') { + $path = rtrim($path, '/'); $query = $this->connection->getQueryBuilder(); $query->select('*') ->from(self::TABLE_NAME) - ->where($query->expr()->like('storage', $query->createNamedParameter($path . '%'))); + ->where($query->expr()->like('storage', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%'))); $result = $query->executeQuery(); while ($row = $result->fetch()) { @@ -107,7 +108,7 @@ public function purgeSymlink($path = '/') { } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node * * @return int|false */ @@ -149,7 +150,7 @@ private function updateSymlink($node) { } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ private function insertSymlink($node) { $name = $this->getNameFromNode($node); @@ -166,7 +167,7 @@ private function insertSymlink($node) { } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node * * @return bool */ @@ -182,28 +183,28 @@ private function deleteSymlinkById($id) { } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ private function getNameFromNode($node) { return $node->getName(); } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ private function getStorageIdFromNode($node) { return $node->getStorage()->getId(); } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ private function getPathFromNode($node) { return $node->getPath(); } /** - * @param \OCP\Files\Node $node + * @param \OCP\Files\FileInfo $node */ private function getLastUpdatedFromNode($node) { return $node->getMtime(); From 4e8748fca9509f7125c25c62aa4b0c5e97e073ed Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:18:37 +0100 Subject: [PATCH 22/51] BulkUpload: Use SymlinkManager to create symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index c4a8c5aee2b7d..74485d4dd20b7 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -46,12 +46,18 @@ class BulkUploadPlugin extends ServerPlugin { */ private $server; + /** + * @var \OCP\Files\SymlinkManager + */ + private $symlinkManager; + public function __construct( Folder $userFolder, LoggerInterface $logger ) { $this->userFolder = $userFolder; $this->logger = $logger; + $this->symlinkManager = new \OCP\Files\SymlinkManager(); } /** @@ -104,14 +110,7 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) $node = $this->userFolder->getById($node->getId())[0]; if (isset($headers['oc-file-type']) && $headers['oc-file-type'] == 1) { - $infoData = [ - 'type' => \OC\Files\FileInfo::TYPE_SYMLINK, - 'etag' => $node->getEtag(), - 'mimetype' => 'symlink', // TODO: good news: this actually works! will be stored in database and will be retrieved when querying cache. bad news, files:scan will override the mimetype and it will be a regular file again :( - ]; - $path = $headers['x-file-path']; - $path = trim($path, '/'); - \OC\Files\Filesystem::getView()->putFileInfo($path, $infoData); + $this->symlinkManager->storeSymlink($node); } $writtenFiles[$headers['x-file-path']] = [ From edf4f9506d522773f993f22d2dd044324d3d1448 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:19:18 +0100 Subject: [PATCH 23/51] FilesPlugin: Use SymlinkManager to check for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index f213a5ad1f410..0ad5b84a2f568 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -104,6 +104,7 @@ class FilesPlugin extends ServerPlugin { private IConfig $config; private IRequest $request; private IPreview $previewManager; + private \OCP\Files\SymlinkManager $symlinkManager; public function __construct(Tree $tree, IConfig $config, @@ -119,6 +120,7 @@ public function __construct(Tree $tree, $this->isPublic = $isPublic; $this->downloadAttachment = $downloadAttachment; $this->previewManager = $previewManager; + $this->symlinkManager = new \OCP\Files\SymlinkManager(); } /** @@ -228,18 +230,13 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface public function httpGet(RequestInterface $request, ResponseInterface $response) { // only handle symlinks - $symlinkPath = $request->getPath(); - $fileInfo = \OC\Files\Filesystem::getView()->getFileInfo($symlinkPath); - if (!$fileInfo || $fileInfo->getType() !== \OC\Files\FileInfo::TYPE_SYMLINK) { + $node = $this->tree->getNodeForPath($request->getPath()); + if (!($node instanceof \OCP\Files\File && $this->symlinkManager->isSymlink($node))) { return; } - $symlinkTarget = $fileInfo->getMetadata()['symlinkTarget']; - if (isset($symlinkTarget)) { - throw new NotFound("Symlink has no target!"); - } $response->addHeader('OC-File-Type', '1'); - $response->setBody($symlinkTarget); + $response->setBody($node->getContent()); // do not continue processing this request return false; } @@ -455,7 +452,7 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) $propFind->handle(self::RESOURCETYPE_PROPERTYNAME, function() use ($node) { $info = $node->getFileInfo(); - if ($info->getType() == \OC\Files\FileInfo::TYPE_SYMLINK || $info->getMimetype() == 'symlink') { + if ($this->symlinkManager->isSymlink($info)) { return '{DAV:}symlink'; } return null; From 8127b70a87646dc28e556c79439b35f37b4e0a15 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:20:01 +0100 Subject: [PATCH 24/51] FileInfo: Remove unused TYPE_SYMLINK Signed-off-by: Tamino Bauknecht --- lib/public/Files/FileInfo.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index 6d843bccd78a7..817b03dfc65f1 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -45,10 +45,6 @@ interface FileInfo { * @since 7.0.0 */ public const TYPE_FOLDER = 'dir'; - /** - * @since TODO - */ - public const TYPE_SYMLINK = 'symlink'; /** * @const \OCP\Files\FileInfo::SPACE_NOT_COMPUTED Return value for a not computed space value From 8b21c0751b8060b292d7fdd6f9be904177975d27 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:20:37 +0100 Subject: [PATCH 25/51] SymlinkPlugin: Initialize SymlinkPlugin with basic implementation Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/SymlinkPlugin.php | 86 +++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 apps/dav/lib/Upload/SymlinkPlugin.php diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php new file mode 100644 index 0000000000000..d2ff81b5fb047 --- /dev/null +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -0,0 +1,86 @@ + + * + * @author Tamino Bauknecht + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Upload; + +use OCP\Files\SymlinkManager; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class SymlinkPlugin extends ServerPlugin { + /** @var Server */ + private $server; + /** @var SymlinkManager */ + private $symlinkManager; + + public function __construct() { + $this->symlinkManager = new SymlinkManager(); + } + + /** + * @inheritdoc + */ + public function initialize(Server $server) { + $server->on('method:PUT', [$this, 'httpPut']); + $server->on('method:DELETE', [$this, 'httpDelete']); + + $this->server = $server; + } + + public function httpPut(RequestInterface $request, ResponseInterface $response): bool { + if ($request->hasHeader('OC-File-Type') && $request->getHeader('OC-File-Type') == 1) { + $symlinkPath = $request->getPath(); + $symlinkName = basename($symlinkPath); + $symlinkTarget = $request->getBodyAsString(); + $parentPath = dirname($symlinkPath); + $parentNode = $this->server->tree->getNodeForPath($parentPath); + if (!$parentNode instanceof \Sabre\DAV\ICollection) { + throw new \Sabre\DAV\Exception\Forbidden("Directory does not allow creation of files - failed to upload '$symlinkName'"); + } + $etag = $parentNode->createFile($symlinkName); + $symlinkNode = $parentNode->getChild($symlinkName); + if (!$symlinkNode instanceof \OCA\DAV\Connector\Sabre\File) { + throw new \Sabre\DAV\Exception\NotFound("Failed to get newly created file '$symlinkName'"); + } + $symlinkNode->put($symlinkTarget); + $this->symlinkManager->storeSymlink($symlinkNode->getFileInfo()); + + $response->setHeader("OC-ETag", $etag); + $response->setStatus(201); + return false; + } + } + + public function httpDelete(RequestInterface $request, ResponseInterface $response): bool { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath(dirname($path)); + if ($this->symlinkManager->isSymlink($node)) { + $this->symlinkManager->deleteSymlink($node); + return false; + } + } +} From c6e2e8c7e095a509ebce863bb20fda0998dea183 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 00:20:59 +0100 Subject: [PATCH 26/51] Server: Register SymlinkPlugin Signed-off-by: Tamino Bauknecht --- apps/dav/composer/composer/autoload_classmap.php | 1 + apps/dav/composer/composer/autoload_static.php | 1 + apps/dav/lib/Server.php | 2 ++ lib/composer/composer/autoload_classmap.php | 2 ++ lib/composer/composer/autoload_static.php | 2 ++ 5 files changed, 8 insertions(+) diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index d4305195d461e..d4031af1f21fe 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -339,6 +339,7 @@ 'OCA\\DAV\\Upload\\AssemblyStream' => $baseDir . '/../lib/Upload/AssemblyStream.php', 'OCA\\DAV\\Upload\\ChunkingPlugin' => $baseDir . '/../lib/Upload/ChunkingPlugin.php', 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => $baseDir . '/../lib/Upload/ChunkingV2Plugin.php', + 'OCA\\DAV\\Upload\\SymlinkPlugin' => $baseDir . '/../lib/Upload/SymlinkPlugin.php', 'OCA\\DAV\\Upload\\CleanupService' => $baseDir . '/../lib/Upload/CleanupService.php', 'OCA\\DAV\\Upload\\FutureFile' => $baseDir . '/../lib/Upload/FutureFile.php', 'OCA\\DAV\\Upload\\PartFile' => $baseDir . '/../lib/Upload/PartFile.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 9afd73635ffd1..2eaad41912d6f 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -354,6 +354,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Upload\\AssemblyStream' => __DIR__ . '/..' . '/../lib/Upload/AssemblyStream.php', 'OCA\\DAV\\Upload\\ChunkingPlugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingPlugin.php', 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingV2Plugin.php', + 'OCA\\DAV\\Upload\\SymlinkPlugin' => __DIR__ . '/..' . '/../lib/Upload/SymlinkPlugin.php', 'OCA\\DAV\\Upload\\CleanupService' => __DIR__ . '/..' . '/../lib/Upload/CleanupService.php', 'OCA\\DAV\\Upload\\FutureFile' => __DIR__ . '/..' . '/../lib/Upload/FutureFile.php', 'OCA\\DAV\\Upload\\PartFile' => __DIR__ . '/..' . '/../lib/Upload/PartFile.php', diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index dedb959c1cd6a..a572adda65a78 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -74,6 +74,7 @@ use OCA\DAV\SystemTag\SystemTagPlugin; use OCA\DAV\Upload\ChunkingPlugin; use OCA\DAV\Upload\ChunkingV2Plugin; +use OCA\DAV\Upload\SymlinkPlugin; use OCP\AppFramework\Http\Response; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; @@ -222,6 +223,7 @@ public function __construct(IRequest $request, string $baseUri) { $this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class))); $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class))); $this->server->addPlugin(new ChunkingPlugin()); + $this->server->addPlugin(new SymlinkPlugin()); // allow setup of additional plugins $dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index f1de00a49bf89..47b15d357b1bb 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -418,6 +418,7 @@ 'OCP\\Files\\Storage\\IStorage' => $baseDir . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => $baseDir . '/lib/public/Files/Storage/IStorageFactory.php', 'OCP\\Files\\Storage\\IWriteStreamStorage' => $baseDir . '/lib/public/Files/Storage/IWriteStreamStorage.php', + 'OCP\\Files\\SymlinkManager' => $baseDir . '/lib/private/Files/SymlinkManager.php', 'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => $baseDir . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php', 'OCP\\Files\\Template\\ICustomTemplateProvider' => $baseDir . '/lib/public/Files/Template/ICustomTemplateProvider.php', 'OCP\\Files\\Template\\ITemplateManager' => $baseDir . '/lib/public/Files/Template/ITemplateManager.php', @@ -1243,6 +1244,7 @@ 'OC\\Core\\Migrations\\Version28000Date20231103104802' => $baseDir . '/core/Migrations/Version28000Date20231103104802.php', 'OC\\Core\\Migrations\\Version29000Date20231126110901' => $baseDir . '/core/Migrations/Version29000Date20231126110901.php', 'OC\\Core\\Migrations\\Version29000Date20231213104850' => $baseDir . '/core/Migrations/Version29000Date20231213104850.php', + 'OC\\Core\\Migrations\\Version29000Date20231123170742' => $baseDir . '/core/Migrations/Version29000Date20231123170742.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 017918b3f44b8..42df96f624628 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -451,6 +451,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Files\\Storage\\IStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorageFactory.php', 'OCP\\Files\\Storage\\IWriteStreamStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IWriteStreamStorage.php', + 'OCP\\Files\\SymlinkManager' => __DIR__ . '/../../..' . '/lib/private/Files/SymlinkManager.php', 'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php', 'OCP\\Files\\Template\\ICustomTemplateProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ICustomTemplateProvider.php', 'OCP\\Files\\Template\\ITemplateManager' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ITemplateManager.php', @@ -1276,6 +1277,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version28000Date20231103104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20231103104802.php', 'OC\\Core\\Migrations\\Version29000Date20231126110901' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20231126110901.php', 'OC\\Core\\Migrations\\Version29000Date20231213104850' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20231213104850.php', + 'OC\\Core\\Migrations\\Version29000Date20231123170742' => __DIR__ . '/../../..' . '/core/Migrations/Version29000Date20231123170742.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', From 47ee7ba9e4b9222a7f4620a25fedeb3962b88a96 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 21:07:14 +0100 Subject: [PATCH 27/51] ChunkingV2Plugin: Remove symlink handling Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/ChunkingV2Plugin.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/dav/lib/Upload/ChunkingV2Plugin.php b/apps/dav/lib/Upload/ChunkingV2Plugin.php index b0e4cb487b5ee..29017155d458e 100644 --- a/apps/dav/lib/Upload/ChunkingV2Plugin.php +++ b/apps/dav/lib/Upload/ChunkingV2Plugin.php @@ -45,7 +45,6 @@ use OCP\IConfig; use OCP\Lock\ILockingProvider; use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\InsufficientStorage; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\Exception\PreconditionFailed; @@ -147,20 +146,6 @@ public function afterMkcol(RequestInterface $request, ResponseInterface $respons } public function beforePut(RequestInterface $request, ResponseInterface $response): bool { - if ($request->getHeader('OC-File-Type') == 1) { - $symlinkPath = $request->getPath(); - $symlinkTarget = $request->getBodyAsString(); - $newEtag = "'$symlinkPath'->'$symlinkTarget'"; - $infoData = [ - 'type' => \OC\Files\FileInfo::TYPE_SYMLINK, - 'etag' => $newEtag, - ]; - \OC\Files\Filesystem::getView()->putFileInfo($symlinkPath, $infoData); - $response->setHeader("OC-ETag", $newEtag); - $response->setStatus(201); - $this->server->sapi->sendResponse($response); - return false; - } try { $this->prepareUpload(dirname($request->getPath())); $this->checkPrerequisites(); From 641eac7ff8dc06f272afceb6ed9623720bd39b2c Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 21:08:31 +0100 Subject: [PATCH 28/51] SymlinkPlugin: Pass correct object to symlinkManager for httpDelete Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/SymlinkPlugin.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index d2ff81b5fb047..e6bd327de8a7d 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -73,14 +73,20 @@ public function httpPut(RequestInterface $request, ResponseInterface $response): $response->setStatus(201); return false; } + return true; } public function httpDelete(RequestInterface $request, ResponseInterface $response): bool { $path = $request->getPath(); $node = $this->server->tree->getNodeForPath(dirname($path)); - if ($this->symlinkManager->isSymlink($node)) { - $this->symlinkManager->deleteSymlink($node); - return false; + if (!$node instanceof \OCA\DAV\Connector\Sabre\File) { + return true; + } + $info = $node->getFileInfo(); + if ($this->symlinkManager->isSymlink($info)) { + $this->symlinkManager->deleteSymlink($info); } + // always propagate to trigger deletion of regular file representing symlink in filesystem + return true; } } From 61faf449b57fed370da541197419a5297fde5ddf Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 21:09:51 +0100 Subject: [PATCH 29/51] migrations: Rename symlink migration to correct format Signed-off-by: Tamino Bauknecht --- ...kSupportMigration.php => Version29000Date20231123170742.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename core/Migrations/{SymlinkSupportMigration.php => Version29000Date20231123170742.php} (97%) diff --git a/core/Migrations/SymlinkSupportMigration.php b/core/Migrations/Version29000Date20231123170742.php similarity index 97% rename from core/Migrations/SymlinkSupportMigration.php rename to core/Migrations/Version29000Date20231123170742.php index a97731796fd79..0daa32d142858 100644 --- a/core/Migrations/SymlinkSupportMigration.php +++ b/core/Migrations/Version29000Date20231123170742.php @@ -35,7 +35,7 @@ /** * Introduce symlinks table */ -class SymlinkSupportMigration extends SimpleMigrationStep { +class Version29000Date20231123170742 extends SimpleMigrationStep { /** * @param IOutput $output * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` From 8d7dcf6e248ad3e2431774ba57a148fd6c2231f0 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 21:10:22 +0100 Subject: [PATCH 30/51] SymlinkManager: Various small but important fixes Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index f93845efa479a..bfdabae8bca43 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -54,7 +54,7 @@ public function __construct() { * @return bool */ public function isSymlink($node) { - return $this->getId($node) === false; + return $this->getId($node) !== false; } /** @@ -64,9 +64,9 @@ public function isSymlink($node) { */ public function storeSymlink($node) { if ($this->isSymlink($node)) { - $this->insertSymlink($node); - } else { $this->updateSymlink($node); + } else { + $this->insertSymlink($node); } } @@ -97,7 +97,7 @@ public function purgeSymlink($path = '/') { $query = $this->connection->getQueryBuilder(); $query->select('*') ->from(self::TABLE_NAME) - ->where($query->expr()->like('storage', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%'))); + ->where($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%'))); $result = $query->executeQuery(); while ($row = $result->fetch()) { @@ -147,6 +147,9 @@ private function updateSymlink($node) { ->set('storage', $query->createNamedParameter($storageId)) ->set('path', $query->createNamedParameter($path)) ->set('last_updated', $query->createNamedParameter($lastUpdated)); + if ($query->executeStatement() != 1) { + throw new \OCP\DB\Exception("Invalid number of rows changed while updating symlink!"); + } } /** @@ -164,6 +167,9 @@ private function insertSymlink($node) { ->setValue('storage', $query->createNamedParameter($storageId)) ->setValue('path', $query->createNamedParameter($path)) ->setValue('last_updated', $query->createNamedParameter($lastUpdated)); + if ($query->executeStatement() != 1) { + throw new \OCP\DB\Exception("Invalid number of rows changed while inserting symlink!"); + } } /** From 6dc1aa6b3f06f8c72749cfb6d579448ca6f60164 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 23 Nov 2023 21:29:16 +0100 Subject: [PATCH 31/51] Revert now unnecessary changes This reverts the following commits: * fb0045d58598ea2616c88a3daf7dada93b769746 * 9a557bf6a57b59055a38482f6463b55a4c95c593 * 50216efe2c94021205115f6ccce294f9c31462df Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/Directory.php | 50 ---------------------- lib/private/Files/Storage/Local.php | 17 +------- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index db02d883a2881..441e6ea8f570c 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -192,56 +192,6 @@ public function createDirectory($name) { } } - /** - * Creates a new symlink - * - * @param string $name - * @param string $target - * @return null|string - * @throws FileLocked - * @throws InvalidPath - * @throws ServiceUnavailable - * @throws \Sabre\DAV\Exception - * @throws \Sabre\DAV\Exception\Forbidden - */ - public function createSymlink($name, $target) { - try { - if (!$this->info->isCreatable()) { - throw new \Sabre\DAV\Exception\Forbidden(); - } - - $this->fileView->verifyPath($this->path, $name); - - [$storage, $internalPath] = $this->fileView->resolvePath($this->path); - if (!$storage || !$storage->instanceOfStorage(\OC\Files\Storage\Local::class)) { - throw new \Sabre\DAV\Exception\NotImplemented("Symlinks currently not supported on non-local storages - failed to create '$name'!"); - } - - $internalPath = $internalPath . '/' . $name; - $storage->unlink($internalPath); - if (!$storage->symlink($target, $internalPath)) { - throw new \Sabre\DAV\Exception\Forbidden("Could not create symlink '$name'!"); - } - $storage->getUpdater()->update($internalPath); - $newEtag = $storage->getETag($internalPath); - $infoData = [ - 'type' => FileInfo::TYPE_FILE, - 'etag' => $newEtag, - ]; - $path = \OC\Files\Filesystem::normalizePath($this->path . '/' . $name); - $this->fileView->putFileInfo($path, $infoData); - return '"' . $newEtag . '"'; - } catch (\OCP\Files\StorageNotAvailableException $e) { - throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e); - } catch (InvalidPathException $ex) { - throw new InvalidPath($ex->getMessage(), false, $ex); - } catch (ForbiddenException $ex) { - throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex); - } catch (LockedException $e) { - throw new FileLocked($e->getMessage(), $e->getCode(), $e); - } - } - /** * Returns a specific child node, referenced by its name * diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 660d68adc7a8a..0fca853da5987 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -181,10 +181,6 @@ public function is_file($path) { return is_file($this->getSourcePath($path)); } - public function is_link($path) { - return is_link($this->getSourcePath($path)); - } - public function stat($path) { $fullPath = $this->getSourcePath($path); clearstatcache(true, $fullPath); @@ -341,21 +337,10 @@ public function file_put_contents($path, $data) { return $result; } - /** - * create symlink - * - * @param string $path - * @param string $target - * @return bool - */ - public function symlink($target, $path) { - return symlink($target, $this->getSourcePath($path)); - } - public function unlink($path) { if ($this->is_dir($path)) { return $this->rmdir($path); - } elseif ($this->is_file($path) || $this->is_link($path)) { + } elseif ($this->is_file($path)) { return unlink($this->getSourcePath($path)); } else { return false; From e0d5164d3db1aade6d64d3707449ed0d97a5708f Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Mon, 27 Nov 2023 21:48:16 +0100 Subject: [PATCH 32/51] SymlinkPlugin/FilesPlugin: Fix httpGet and httpDelete for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 8 +++++--- apps/dav/lib/Upload/SymlinkPlugin.php | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 0ad5b84a2f568..836a672295f88 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -231,12 +231,14 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface public function httpGet(RequestInterface $request, ResponseInterface $response) { // only handle symlinks $node = $this->tree->getNodeForPath($request->getPath()); - if (!($node instanceof \OCP\Files\File && $this->symlinkManager->isSymlink($node))) { + if (!($node instanceof \OCA\DAV\Connector\Sabre\File && $this->symlinkManager->isSymlink($node->getFileInfo()))) { return; } - $response->addHeader('OC-File-Type', '1'); - $response->setBody($node->getContent()); + $response->setHeader('OC-File-Type', '1'); + $response->setHeader('OC-ETag', $node->getEtag()); + $response->setBody($node->get()); + $response->setStatus(200); // do not continue processing this request return false; } diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index e6bd327de8a7d..0cab6513d8720 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -78,13 +78,16 @@ public function httpPut(RequestInterface $request, ResponseInterface $response): public function httpDelete(RequestInterface $request, ResponseInterface $response): bool { $path = $request->getPath(); - $node = $this->server->tree->getNodeForPath(dirname($path)); - if (!$node instanceof \OCA\DAV\Connector\Sabre\File) { + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof \OCA\DAV\Connector\Sabre\Node) { return true; } $info = $node->getFileInfo(); if ($this->symlinkManager->isSymlink($info)) { - $this->symlinkManager->deleteSymlink($info); + if (!$this->symlinkManager->deleteSymlink($info)) { + $symlinkName = $info->getName(); + throw new \Sabre\DAV\Exception\NotFound("Unable to delete symlink '$symlinkName'!"); + } } // always propagate to trigger deletion of regular file representing symlink in filesystem return true; From eac2b0a0d60e819b285724ded7d59f00798e63d5 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 29 Nov 2023 01:16:52 +0100 Subject: [PATCH 33/51] migrations: Remove last_updated column from symlinks table Signed-off-by: Tamino Bauknecht --- core/Migrations/Version29000Date20231123170742.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/core/Migrations/Version29000Date20231123170742.php b/core/Migrations/Version29000Date20231123170742.php index 0daa32d142858..f226ca59301a3 100644 --- a/core/Migrations/Version29000Date20231123170742.php +++ b/core/Migrations/Version29000Date20231123170742.php @@ -67,16 +67,9 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'notnull' => false, 'length' => 255, ]); - $table->addColumn('last_updated', Types::INTEGER, [ - 'notnull' => false, - 'length' => 4, - 'default' => 0, - 'unsigned' => true, - ]); $table->setPrimaryKey(['id'], 'symlinks_id_index'); - $table->addUniqueIndex(['path'], 'symlinks_path_index'); - $table->addIndex(['last_updated'], 'symlinks_updated'); + $table->addUniqueIndex(['storage', 'path'], 'symlinks_storage_path_index'); return $schema; } From 2eb1252469ba9b2bbf35ce2d1fa3ca216d29f537 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 29 Nov 2023 01:17:20 +0100 Subject: [PATCH 34/51] SymlinkManager: Remove last_updated from operations This information is not required in the symlinks table since it is already stored in the oc_filecache table for the regular file which represents the symlink in the filesystem. Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index bfdabae8bca43..bbaa5e4c379e7 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -139,14 +139,12 @@ private function updateSymlink($node) { $name = $this->getNameFromNode($node); $storageId = $this->getStorageIdFromNode($node); $path = $this->getPathFromNode($node); - $lastUpdated = $this->getLastUpdatedFromNode($node); $query = $this->connection->getQueryBuilder(); $query->update(self::TABLE_NAME) ->set('name', $query->createNamedParameter($name)) ->set('storage', $query->createNamedParameter($storageId)) - ->set('path', $query->createNamedParameter($path)) - ->set('last_updated', $query->createNamedParameter($lastUpdated)); + ->set('path', $query->createNamedParameter($path)); if ($query->executeStatement() != 1) { throw new \OCP\DB\Exception("Invalid number of rows changed while updating symlink!"); } @@ -159,14 +157,12 @@ private function insertSymlink($node) { $name = $this->getNameFromNode($node); $storageId = $this->getStorageIdFromNode($node); $path = $this->getPathFromNode($node); - $lastUpdated = $this->getLastUpdatedFromNode($node); $query = $this->connection->getQueryBuilder(); $query->insert(self::TABLE_NAME) ->setValue('name', $query->createNamedParameter($name)) ->setValue('storage', $query->createNamedParameter($storageId)) - ->setValue('path', $query->createNamedParameter($path)) - ->setValue('last_updated', $query->createNamedParameter($lastUpdated)); + ->setValue('path', $query->createNamedParameter($path)); if ($query->executeStatement() != 1) { throw new \OCP\DB\Exception("Invalid number of rows changed while inserting symlink!"); } @@ -208,11 +204,4 @@ private function getStorageIdFromNode($node) { private function getPathFromNode($node) { return $node->getPath(); } - - /** - * @param \OCP\Files\FileInfo $node - */ - private function getLastUpdatedFromNode($node) { - return $node->getMtime(); - } } From ed779373e27597559230e5d72b48ea7ffc808d08 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Sat, 2 Dec 2023 14:28:59 +0100 Subject: [PATCH 35/51] FilesPlugin: Send Last-Modified and ETag header for GET response Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 836a672295f88..5b04fd41410f2 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -235,8 +235,12 @@ public function httpGet(RequestInterface $request, ResponseInterface $response) return; } + $date = \DateTime::createFromFormat('U', $node->getLastModified()); + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $response->setHeader('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); $response->setHeader('OC-File-Type', '1'); $response->setHeader('OC-ETag', $node->getEtag()); + $response->setHeader('ETag', $node->getEtag()); $response->setBody($node->get()); $response->setStatus(200); // do not continue processing this request From 7bf32f325e7f309ce2e5710bbc5cee35c3cfc181 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Mon, 4 Dec 2023 23:03:29 +0100 Subject: [PATCH 36/51] SymlinkPlugin: Handle move operation for symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/SymlinkPlugin.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index 0cab6513d8720..4bb7596239e54 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -47,6 +47,7 @@ public function __construct() { public function initialize(Server $server) { $server->on('method:PUT', [$this, 'httpPut']); $server->on('method:DELETE', [$this, 'httpDelete']); + $server->on('afterMove', [$this, 'afterMove']); $this->server = $server; } @@ -92,4 +93,16 @@ public function httpDelete(RequestInterface $request, ResponseInterface $respons // always propagate to trigger deletion of regular file representing symlink in filesystem return true; } + + public function afterMove(string $source, string $destination) { + $sourceNode = $this->server->tree->getNodeForPath($source); + $destinationNode = $this->server->tree->getNodeForPath($destination); + if ($this->symlinkManager->isSymlink($sourceNode)) { + $this->symlinkManager->deleteSymlink($sourceNode); + $this->symlinkManager->storeSymlink($destinationNode); + } elseif ($this->symlinkManager->isSymlink($destinationNode)) { + // source was not a symlink, but destination was a symlink before + $this->symlinkManager->deleteSymlink($destinationNode); + } + } } From b480d409a229fdb72919dccc5a678f3cff037f67 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Mon, 4 Dec 2023 23:05:04 +0100 Subject: [PATCH 37/51] SymlinkPlugin: Fix PUT for regular file at previous symlink location Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Server.php | 2 +- apps/dav/lib/Upload/SymlinkPlugin.php | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index a572adda65a78..d5361dac0703f 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -223,7 +223,7 @@ public function __construct(IRequest $request, string $baseUri) { $this->server->addPlugin(new RequestIdHeaderPlugin(\OC::$server->get(IRequest::class))); $this->server->addPlugin(new ChunkingV2Plugin(\OCP\Server::get(ICacheFactory::class))); $this->server->addPlugin(new ChunkingPlugin()); - $this->server->addPlugin(new SymlinkPlugin()); + $this->server->addPlugin(new SymlinkPlugin($logger)); // allow setup of additional plugins $dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index 4bb7596239e54..7d1b428b4c622 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -26,6 +26,7 @@ namespace OCA\DAV\Upload; use OCP\Files\SymlinkManager; +use Psr\Log\LoggerInterface; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -36,9 +37,12 @@ class SymlinkPlugin extends ServerPlugin { private $server; /** @var SymlinkManager */ private $symlinkManager; + /** @var LoggerInterface */ + private $logger; - public function __construct() { + public function __construct(LoggerInterface $logger) { $this->symlinkManager = new SymlinkManager(); + $this->logger = $logger; } /** @@ -72,9 +76,22 @@ public function httpPut(RequestInterface $request, ResponseInterface $response): $response->setHeader("OC-ETag", $etag); $response->setStatus(201); - return false; + return false; // this request was handled already + } elseif ($this->server->tree->nodeExists($request->getPath())) { + $node = $this->server->tree->getNodeForPath($request->getPath()); + if (!$node instanceof \OCA\DAV\Connector\Sabre\File) { + // cannot check if file was symlink before - let's hope it's not + $this->logger->warning('Unable to check if there was a symlink + before at the same location'); + return true; + } + // if the newly uploaded file is not a symlink, + // but there was a symlink at the same path before + if ($this->symlinkManager->isSymlink($node->getFileInfo())) { + $this->symlinkManager->deleteSymlink($node->getFileInfo()); + } } - return true; + return true; // continue handling this request } public function httpDelete(RequestInterface $request, ResponseInterface $response): bool { From a09a1963ec4b7aa5b3368d266fd7cecb98d7cdcd Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Mon, 4 Dec 2023 23:07:09 +0100 Subject: [PATCH 38/51] BulkUploadPlugin: Fix POST for regular file at previous symlink location Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 74485d4dd20b7..73b02bff2e449 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -111,6 +111,10 @@ public function httpPost(RequestInterface $request, ResponseInterface $response) if (isset($headers['oc-file-type']) && $headers['oc-file-type'] == 1) { $this->symlinkManager->storeSymlink($node); + } elseif ($this->symlinkManager->isSymlink($node)) { + // uploaded file is not a symlink, but there was a symlink + // at the same location before + $this->symlinkManager->deleteSymlink($node); } $writtenFiles[$headers['x-file-path']] = [ From 90a75b51895e10ef6cf638ad3beea8aaec9ba35a Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Mon, 4 Dec 2023 23:07:40 +0100 Subject: [PATCH 39/51] migrations: Remove default for storage column Signed-off-by: Tamino Bauknecht --- core/Migrations/Version29000Date20231123170742.php | 1 - 1 file changed, 1 deletion(-) diff --git a/core/Migrations/Version29000Date20231123170742.php b/core/Migrations/Version29000Date20231123170742.php index f226ca59301a3..f96f7ebd3da49 100644 --- a/core/Migrations/Version29000Date20231123170742.php +++ b/core/Migrations/Version29000Date20231123170742.php @@ -57,7 +57,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('storage', Types::BIGINT, [ 'notnull' => true, 'length' => 20, - 'default' => 0, ]); $table->addColumn('path', Types::STRING, [ 'notnull' => true, From b8051ce06c7f95c8f45cf8e8c47fa21fad0ff409 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Tue, 5 Dec 2023 01:00:51 +0100 Subject: [PATCH 40/51] SymlinkManager: Use numeric_id for storage in database Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index bbaa5e4c379e7..ff3e9f2f6f271 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -186,6 +186,8 @@ private function deleteSymlinkById($id) { /** * @param \OCP\Files\FileInfo $node + * + * @return string */ private function getNameFromNode($node) { return $node->getName(); @@ -193,9 +195,26 @@ private function getNameFromNode($node) { /** * @param \OCP\Files\FileInfo $node + * + * @return int */ private function getStorageIdFromNode($node) { - return $node->getStorage()->getId(); + $storageId = $node->getStorage()->getId(); + $query = $this->connection->getQueryBuilder(); + $query->select('numeric_id') + ->from('storages') + ->where($query->expr()->eq('id', $query->createNamedParameter($storageId))); + $result = $query->executeQuery(); + + if ($result->rowCount() > 1) { + throw new \OCP\DB\Exception("Storage ('$storageId') is not unique in database!"); + } + + $numericId = $result->fetchOne(); + if ($numericId === false) { + throw new \OCP\DB\Exception("Unable to find storage '$storageId' in database!"); + } + return $numericId; } /** From 24cea5db4136f33c026164b872562dd234729c95 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 20:21:17 +0100 Subject: [PATCH 41/51] SymlinkManager: Reduce code duplication for numeric_id and use internalPath The user can already be identified using the storage id. Signed-off-by: Tamino Bauknecht --- lib/private/Files/SymlinkManager.php | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index ff3e9f2f6f271..abec582569a44 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -200,27 +200,17 @@ private function getNameFromNode($node) { */ private function getStorageIdFromNode($node) { $storageId = $node->getStorage()->getId(); - $query = $this->connection->getQueryBuilder(); - $query->select('numeric_id') - ->from('storages') - ->where($query->expr()->eq('id', $query->createNamedParameter($storageId))); - $result = $query->executeQuery(); - - if ($result->rowCount() > 1) { - throw new \OCP\DB\Exception("Storage ('$storageId') is not unique in database!"); - } - - $numericId = $result->fetchOne(); - if ($numericId === false) { - throw new \OCP\DB\Exception("Unable to find storage '$storageId' in database!"); + if ($numericStorageId = \OC\Files\Cache\Storage::getNumericStorageId($storageId)) { + return $numericStorageId; + } else { + throw new \OCP\Files\StorageNotAvailableException("Unable to find storage '$storageId'!"); } - return $numericId; } /** * @param \OCP\Files\FileInfo $node */ private function getPathFromNode($node) { - return $node->getPath(); + return $node->getInternalPath(); } } From 8a9a48c3676d60a1fb1a250fc60ac1eb8c7ff1c2 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 20:22:27 +0100 Subject: [PATCH 42/51] SymlinkPlugin: Fix update of symlinks for move operation Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/SymlinkPlugin.php | 29 +++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index 7d1b428b4c622..2a1152f7e67d9 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -112,14 +112,31 @@ public function httpDelete(RequestInterface $request, ResponseInterface $respons } public function afterMove(string $source, string $destination) { - $sourceNode = $this->server->tree->getNodeForPath($source); + // source node does not exist anymore, thus use still existing parent + $sourceParentNode = dirname($source); + $sourceParentNode = $this->server->tree->getNodeForPath($sourceParentNode); + if (!$sourceParentNode instanceof \OCA\DAV\Connector\Sabre\Node) { + throw new \Sabre\DAV\Exception\NotImplemented('Unable to check if moved file is a symlink!'); + } $destinationNode = $this->server->tree->getNodeForPath($destination); - if ($this->symlinkManager->isSymlink($sourceNode)) { - $this->symlinkManager->deleteSymlink($sourceNode); - $this->symlinkManager->storeSymlink($destinationNode); - } elseif ($this->symlinkManager->isSymlink($destinationNode)) { + if (!$destinationNode instanceof \OCA\DAV\Connector\Sabre\Node) { + throw new \Sabre\DAV\Exception\NotImplemented('Unable to set symlink information on move destination!'); + } + + $sourceInfo = new \OC\Files\FileInfo( + $source, + $sourceParentNode->getFileInfo()->getStorage(), + $sourceParentNode->getInternalPath() . '/' . basename($source), + [], + $sourceParentNode->getFileInfo()->getMountPoint()); + $destinationInfo = $destinationNode->getFileInfo(); + + if ($this->symlinkManager->isSymlink($sourceInfo)) { + $this->symlinkManager->deleteSymlink($sourceInfo); + $this->symlinkManager->storeSymlink($destinationInfo); + } elseif ($this->symlinkManager->isSymlink($destinationInfo)) { // source was not a symlink, but destination was a symlink before - $this->symlinkManager->deleteSymlink($destinationNode); + $this->symlinkManager->deleteSymlink($destinationInfo); } } } From c44b5af7b87c071b66481d03bdec966d8c69bab1 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 20:29:55 +0100 Subject: [PATCH 43/51] BulkUploadPlugin: Cleanup for "feature(dav/bulk): Add symlink upload to BulkUploadPlugin" Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index 73b02bff2e449..d7192104b23a6 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -27,9 +27,7 @@ use OCP\AppFramework\Http; use OCP\Files\DavUtil; use OCP\Files\Folder; -use Exception; use Psr\Log\LoggerInterface; -use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; @@ -39,13 +37,6 @@ class BulkUploadPlugin extends ServerPlugin { private Folder $userFolder; private LoggerInterface $logger; - /** - * Reference to main server object - * - * @var Server - */ - private $server; - /** * @var \OCP\Files\SymlinkManager */ @@ -64,7 +55,6 @@ public function __construct( * Register listener on POST requests with the httpPost method. */ public function initialize(Server $server): void { - $this->server = $server; $server->on('method:POST', [$this, 'httpPost'], 10); } From 0e1fbedd6ac879adb6b711ee1c2c8c70ed852711 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 20:30:51 +0100 Subject: [PATCH 44/51] SymlinkManager: Move to correct namespace OC\Files and update all Plugins Signed-off-by: Tamino Bauknecht --- apps/dav/lib/BulkUpload/BulkUploadPlugin.php | 5 +++-- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 5 +++-- apps/dav/lib/Upload/SymlinkPlugin.php | 2 +- lib/private/Files/SymlinkManager.php | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php index d7192104b23a6..dc5ab5973a231 100644 --- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php +++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php @@ -23,6 +23,7 @@ namespace OCA\DAV\BulkUpload; +use OC\Files\SymlinkManager; use OCA\DAV\Connector\Sabre\MtimeSanitizer; use OCP\AppFramework\Http; use OCP\Files\DavUtil; @@ -38,7 +39,7 @@ class BulkUploadPlugin extends ServerPlugin { private LoggerInterface $logger; /** - * @var \OCP\Files\SymlinkManager + * @var SymlinkManager */ private $symlinkManager; @@ -48,7 +49,7 @@ public function __construct( ) { $this->userFolder = $userFolder; $this->logger = $logger; - $this->symlinkManager = new \OCP\Files\SymlinkManager(); + $this->symlinkManager = new SymlinkManager(); } /** diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 5b04fd41410f2..a699d4105dd27 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -35,6 +35,7 @@ namespace OCA\DAV\Connector\Sabre; use OC\AppFramework\Http\Request; +use OC\Files\SymlinkManager; use OCP\Constants; use OCP\Files\ForbiddenException; use OCP\Files\StorageNotAvailableException; @@ -104,7 +105,7 @@ class FilesPlugin extends ServerPlugin { private IConfig $config; private IRequest $request; private IPreview $previewManager; - private \OCP\Files\SymlinkManager $symlinkManager; + private SymlinkManager $symlinkManager; public function __construct(Tree $tree, IConfig $config, @@ -120,7 +121,7 @@ public function __construct(Tree $tree, $this->isPublic = $isPublic; $this->downloadAttachment = $downloadAttachment; $this->previewManager = $previewManager; - $this->symlinkManager = new \OCP\Files\SymlinkManager(); + $this->symlinkManager = new SymlinkManager(); } /** diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index 2a1152f7e67d9..077899c7c4194 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -25,7 +25,7 @@ namespace OCA\DAV\Upload; -use OCP\Files\SymlinkManager; +use OC\Files\SymlinkManager; use Psr\Log\LoggerInterface; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index abec582569a44..f8592ad7060f7 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -20,7 +20,7 @@ * */ -namespace OCP\Files; +namespace OC\Files; use OCP\Files\Storage\IStorage; use OCP\IDBConnection; From 22324eb252073ae761b1c53583b67be80e706e3b Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 20:34:00 +0100 Subject: [PATCH 45/51] composer: Update namespace of SymlinkPlugin in autoload files Signed-off-by: Tamino Bauknecht --- apps/dav/composer/composer/autoload_classmap.php | 2 +- apps/dav/composer/composer/autoload_static.php | 2 +- lib/composer/composer/autoload_classmap.php | 2 +- lib/composer/composer/autoload_static.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index d4031af1f21fe..671d9b43fe394 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -339,11 +339,11 @@ 'OCA\\DAV\\Upload\\AssemblyStream' => $baseDir . '/../lib/Upload/AssemblyStream.php', 'OCA\\DAV\\Upload\\ChunkingPlugin' => $baseDir . '/../lib/Upload/ChunkingPlugin.php', 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => $baseDir . '/../lib/Upload/ChunkingV2Plugin.php', - 'OCA\\DAV\\Upload\\SymlinkPlugin' => $baseDir . '/../lib/Upload/SymlinkPlugin.php', 'OCA\\DAV\\Upload\\CleanupService' => $baseDir . '/../lib/Upload/CleanupService.php', '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\\SymlinkPlugin' => $baseDir . '/../lib/Upload/SymlinkPlugin.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', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 2eaad41912d6f..8f219cd575769 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -354,11 +354,11 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Upload\\AssemblyStream' => __DIR__ . '/..' . '/../lib/Upload/AssemblyStream.php', 'OCA\\DAV\\Upload\\ChunkingPlugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingPlugin.php', 'OCA\\DAV\\Upload\\ChunkingV2Plugin' => __DIR__ . '/..' . '/../lib/Upload/ChunkingV2Plugin.php', - 'OCA\\DAV\\Upload\\SymlinkPlugin' => __DIR__ . '/..' . '/../lib/Upload/SymlinkPlugin.php', 'OCA\\DAV\\Upload\\CleanupService' => __DIR__ . '/..' . '/../lib/Upload/CleanupService.php', '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\\SymlinkPlugin' => __DIR__ . '/..' . '/../lib/Upload/SymlinkPlugin.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', diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 47b15d357b1bb..6a0406a00bb8f 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -418,7 +418,6 @@ 'OCP\\Files\\Storage\\IStorage' => $baseDir . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => $baseDir . '/lib/public/Files/Storage/IStorageFactory.php', 'OCP\\Files\\Storage\\IWriteStreamStorage' => $baseDir . '/lib/public/Files/Storage/IWriteStreamStorage.php', - 'OCP\\Files\\SymlinkManager' => $baseDir . '/lib/private/Files/SymlinkManager.php', 'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => $baseDir . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php', 'OCP\\Files\\Template\\ICustomTemplateProvider' => $baseDir . '/lib/public/Files/Template/ICustomTemplateProvider.php', 'OCP\\Files\\Template\\ITemplateManager' => $baseDir . '/lib/public/Files/Template/ITemplateManager.php', @@ -1442,6 +1441,7 @@ 'OC\\Files\\Stream\\HashWrapper' => $baseDir . '/lib/private/Files/Stream/HashWrapper.php', 'OC\\Files\\Stream\\Quota' => $baseDir . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Stream\\SeekableHttpStream' => $baseDir . '/lib/private/Files/Stream/SeekableHttpStream.php', + 'OC\\Files\\SymlinkManager' => $baseDir . '/lib/private/Files/SymlinkManager.php', 'OC\\Files\\Template\\TemplateManager' => $baseDir . '/lib/private/Files/Template/TemplateManager.php', 'OC\\Files\\Type\\Detection' => $baseDir . '/lib/private/Files/Type/Detection.php', 'OC\\Files\\Type\\Loader' => $baseDir . '/lib/private/Files/Type/Loader.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 42df96f624628..252baac53f768 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -451,7 +451,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Files\\Storage\\IStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorageFactory.php', 'OCP\\Files\\Storage\\IWriteStreamStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IWriteStreamStorage.php', - 'OCP\\Files\\SymlinkManager' => __DIR__ . '/../../..' . '/lib/private/Files/SymlinkManager.php', 'OCP\\Files\\Template\\FileCreatedFromTemplateEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Template/FileCreatedFromTemplateEvent.php', 'OCP\\Files\\Template\\ICustomTemplateProvider' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ICustomTemplateProvider.php', 'OCP\\Files\\Template\\ITemplateManager' => __DIR__ . '/../../..' . '/lib/public/Files/Template/ITemplateManager.php', @@ -1475,6 +1474,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Files\\Stream\\HashWrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/HashWrapper.php', 'OC\\Files\\Stream\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Stream\\SeekableHttpStream' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/SeekableHttpStream.php', + 'OC\\Files\\SymlinkManager' => __DIR__ . '/../../..' . '/lib/private/Files/SymlinkManager.php', 'OC\\Files\\Template\\TemplateManager' => __DIR__ . '/../../..' . '/lib/private/Files/Template/TemplateManager.php', 'OC\\Files\\Type\\Detection' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Detection.php', 'OC\\Files\\Type\\Loader' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Loader.php', From b5c1828754072d4fffbca5d29f53a64e579efc12 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 23:02:47 +0100 Subject: [PATCH 46/51] Connector\Sabre\File: Add string as possible parameter type to put The base interface IFile allows this type and it is explicitly handled in the function definition. Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index f188490fd938f..606987762c1e9 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -125,7 +125,7 @@ public function __construct(View $view, FileInfo $info, IManager $shareManager = * different object on a subsequent GET you are strongly recommended to not * return an ETag, and just return null. * - * @param resource $data + * @param resource|string $data * * @throws Forbidden * @throws UnsupportedMediaType From 6f60e7edcd5402fb32a1f12d19e67826a06e2ba9 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 23:16:09 +0100 Subject: [PATCH 47/51] FilesPlugin: Fix resourcetype of symlinks for PROPFIND This fixes the commit "Draft for symlink mimetype on regular file for symlinks" Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index a699d4105dd27..896ef623bc05d 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -460,7 +460,7 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) $propFind->handle(self::RESOURCETYPE_PROPERTYNAME, function() use ($node) { $info = $node->getFileInfo(); if ($this->symlinkManager->isSymlink($info)) { - return '{DAV:}symlink'; + return new \Sabre\DAV\Xml\Property\ResourceType(['{DAV:}symlink']); } return null; }); From 47d3eaee6e7bd1ed85fd9229d55ae7678a87a6e1 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Wed, 6 Dec 2023 23:19:19 +0100 Subject: [PATCH 48/51] FilesPlugin/SymlinkPlugin/SymlinkManager: Fix some style guide violations and warnings Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 9 +++++---- apps/dav/lib/Upload/SymlinkPlugin.php | 12 +++++++++--- lib/private/Files/SymlinkManager.php | 6 +++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index 896ef623bc05d..f9dae911b84fd 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -229,11 +229,12 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface } } - public function httpGet(RequestInterface $request, ResponseInterface $response) { + public function httpGet(RequestInterface $request, ResponseInterface $response): bool { // only handle symlinks $node = $this->tree->getNodeForPath($request->getPath()); - if (!($node instanceof \OCA\DAV\Connector\Sabre\File && $this->symlinkManager->isSymlink($node->getFileInfo()))) { - return; + if (!($node instanceof \OCA\DAV\Connector\Sabre\File + && $this->symlinkManager->isSymlink($node->getFileInfo()))) { + return true; } $date = \DateTime::createFromFormat('U', $node->getLastModified()); @@ -255,7 +256,7 @@ public function httpGet(RequestInterface $request, ResponseInterface $response) * @param RequestInterface $request * @param ResponseInterface $response */ - public function afterHttpGet(RequestInterface $request, ResponseInterface $response) { + public function afterHttpGet(RequestInterface $request, ResponseInterface $response): void { // Only handle valid files $node = $this->tree->getNodeForPath($request->getPath()); if (!($node instanceof IFile)) { diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index 077899c7c4194..e619f52aa551a 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -33,7 +33,11 @@ use Sabre\HTTP\ResponseInterface; class SymlinkPlugin extends ServerPlugin { - /** @var Server */ + /** + * @var Server + * + * @psalm-suppress PropertyNotSetInConstructor + */ private $server; /** @var SymlinkManager */ private $symlinkManager; @@ -48,7 +52,7 @@ public function __construct(LoggerInterface $logger) { /** * @inheritdoc */ - public function initialize(Server $server) { + public function initialize(Server $server): void { $server->on('method:PUT', [$this, 'httpPut']); $server->on('method:DELETE', [$this, 'httpDelete']); $server->on('afterMove', [$this, 'afterMove']); @@ -74,7 +78,9 @@ public function httpPut(RequestInterface $request, ResponseInterface $response): $symlinkNode->put($symlinkTarget); $this->symlinkManager->storeSymlink($symlinkNode->getFileInfo()); - $response->setHeader("OC-ETag", $etag); + if ($etag) { + $response->setHeader('OC-ETag', $etag); + } $response->setStatus(201); return false; // this request was handled already } elseif ($this->server->tree->nodeExists($request->getPath())) { diff --git a/lib/private/Files/SymlinkManager.php b/lib/private/Files/SymlinkManager.php index f8592ad7060f7..a1c6937cb4efe 100644 --- a/lib/private/Files/SymlinkManager.php +++ b/lib/private/Files/SymlinkManager.php @@ -146,7 +146,7 @@ private function updateSymlink($node) { ->set('storage', $query->createNamedParameter($storageId)) ->set('path', $query->createNamedParameter($path)); if ($query->executeStatement() != 1) { - throw new \OCP\DB\Exception("Invalid number of rows changed while updating symlink!"); + throw new \OCP\DB\Exception('Invalid number of rows changed while updating symlink!'); } } @@ -164,7 +164,7 @@ private function insertSymlink($node) { ->setValue('storage', $query->createNamedParameter($storageId)) ->setValue('path', $query->createNamedParameter($path)); if ($query->executeStatement() != 1) { - throw new \OCP\DB\Exception("Invalid number of rows changed while inserting symlink!"); + throw new \OCP\DB\Exception('Invalid number of rows changed while inserting symlink!'); } } @@ -179,7 +179,7 @@ private function deleteSymlinkById($id) { ->where($query->expr()->eq('id', $query->createNamedParameter($id))); $rowsChanged = $query->executeStatement(); if ($rowsChanged > 1) { - throw new \OCP\DB\Exception("Too many symlink rows deleted!"); + throw new \OCP\DB\Exception('Too many symlink rows deleted!'); } return $rowsChanged == 1; } From 65b5796a73e5e53d844ec0020140b697924746eb Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 14 Dec 2023 19:29:53 +0100 Subject: [PATCH 49/51] PreviewController: Indicate symlinks via preview icon in web ui Signed-off-by: Tamino Bauknecht --- core/Controller/PreviewController.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/Controller/PreviewController.php b/core/Controller/PreviewController.php index 7adec03814c7d..e57b642b0a916 100644 --- a/core/Controller/PreviewController.php +++ b/core/Controller/PreviewController.php @@ -27,6 +27,7 @@ */ namespace OC\Core\Controller; +use OC\Files\SymlinkManager; use OCA\Files_Sharing\SharedStorage; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -42,6 +43,12 @@ use OCP\Preview\IMimeIconProvider; class PreviewController extends Controller { + private const SYMLINK_PREVIEW_ICON_PATH = '/core/img/filetypes/link.svg'; + /** + * @var SymlinkManager + */ + private $symlinkManager; + public function __construct( string $appName, IRequest $request, @@ -51,6 +58,8 @@ public function __construct( private IMimeIconProvider $mimeIconProvider, ) { parent::__construct($appName, $request); + + $this->symlinkManager = new SymlinkManager(); } /** @@ -169,6 +178,14 @@ private function fetchPreview( } } + if ($this->symlinkManager->isSymlink($node)) { + if ($url = \OC::$server->get(\OCP\IURLGenerator::class)->getAbsoluteURL( + self::SYMLINK_PREVIEW_ICON_PATH + )) { + return new RedirectResponse($url); + } + } + try { $f = $this->preview->getPreview($node, $x, $y, !$a, $mode); $response = new FileDisplayResponse($f, Http::STATUS_OK, [ From 4fac7d8f42c93c6f030ae3c47f591c27ccfd2924 Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Thu, 14 Dec 2023 20:09:54 +0100 Subject: [PATCH 50/51] SymlinkPlugin: Fix copying of symlinks Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Upload/SymlinkPlugin.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/Upload/SymlinkPlugin.php b/apps/dav/lib/Upload/SymlinkPlugin.php index e619f52aa551a..b4123e9ff0a58 100644 --- a/apps/dav/lib/Upload/SymlinkPlugin.php +++ b/apps/dav/lib/Upload/SymlinkPlugin.php @@ -56,6 +56,7 @@ public function initialize(Server $server): void { $server->on('method:PUT', [$this, 'httpPut']); $server->on('method:DELETE', [$this, 'httpDelete']); $server->on('afterMove', [$this, 'afterMove']); + $server->on('afterCopy', [$this, 'afterCopy']); $this->server = $server; } @@ -117,7 +118,7 @@ public function httpDelete(RequestInterface $request, ResponseInterface $respons return true; } - public function afterMove(string $source, string $destination) { + public function afterMove(string $source, string $destination): void { // source node does not exist anymore, thus use still existing parent $sourceParentNode = dirname($source); $sourceParentNode = $this->server->tree->getNodeForPath($sourceParentNode); @@ -145,4 +146,24 @@ public function afterMove(string $source, string $destination) { $this->symlinkManager->deleteSymlink($destinationInfo); } } + + public function afterCopy(string $source, string $destination): void { + $sourceNode = $this->server->tree->getNodeForPath($source); + if (!$sourceNode instanceof \OCA\DAV\Connector\Sabre\Node) { + throw new \Sabre\DAV\Exception\NotImplemented( + 'Unable to check if copied file is a symlink!'); + } + $destinationNode = $this->server->tree->getNodeForPath($destination); + if (!$destinationNode instanceof \OCA\DAV\Connector\Sabre\Node) { + throw new \Sabre\DAV\Exception\NotImplemented( + 'Unable to set symlink information on copy destination!'); + } + + $sourceInfo = $sourceNode->getFileInfo(); + $destinationInfo = $destinationNode->getFileInfo(); + + if ($this->symlinkManager->isSymlink($sourceInfo)) { + $this->symlinkManager->storeSymlink($destinationInfo); + } + } } From 6666955a348d00750b6efb2274a033b78d6ccc1f Mon Sep 17 00:00:00 2001 From: Tamino Bauknecht Date: Sat, 20 Jan 2024 15:50:01 +0100 Subject: [PATCH 51/51] FilesPlugin: Fix linter issue Signed-off-by: Tamino Bauknecht --- apps/dav/lib/Connector/Sabre/FilesPlugin.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index f9dae911b84fd..8dd7fcb056ff7 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -237,7 +237,8 @@ public function httpGet(RequestInterface $request, ResponseInterface $response): return true; } - $date = \DateTime::createFromFormat('U', $node->getLastModified()); + $date = new \DateTime(); + $date->setTimestamp($node->getLastModified()); $date = $date->setTimezone(new \DateTimeZone('UTC')); $response->setHeader('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); $response->setHeader('OC-File-Type', '1');