Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve symbolic links from sync clients #41321

Open
wants to merge 51 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7b1ee27
feature(storage): Allow unlink operation on symlinks
taminob Nov 7, 2023
1c75b70
feature(storage): Add symlink operation to local storage
taminob Nov 7, 2023
cf75521
feature(dav/connector): Add createSymlink to Directory node
taminob Nov 7, 2023
b9ab528
feature(dav/upload): Add symlink single-file upload to ChunkingV2Plugin
taminob Nov 7, 2023
af70873
feature(dav/bulk): Add symlink upload to BulkUploadPlugin
taminob Nov 7, 2023
4ee5062
feature(files): Add file type TYPE_SYMLINK to FileInfo
taminob Nov 7, 2023
97d3dc5
feature(storage): Add symlink, is_link and readlink to IStorage, Wrap…
taminob Nov 7, 2023
5cc9108
feature(dav/file): Add readlink method
taminob Nov 7, 2023
23ee19a
feature(storage): Allow deletion of symlinks with remove method in Co…
taminob Nov 7, 2023
da98a9f
feature(storage): Add default implementation for readlink and symlink…
taminob Nov 7, 2023
1b31d59
feature(fileinfo): Add symlink mimetype
taminob Nov 8, 2023
0ef014a
Add couple of TODOs, mostly related to symlink mimetype
taminob Nov 15, 2023
3274c8e
feature(dav/connector): Draft for handling GET request for symlinks
taminob Nov 15, 2023
3b02d61
Revert commits for storing actual symlinks on server
taminob Nov 15, 2023
88f510e
First draft for metadata-only symlinks
taminob Nov 15, 2023
b537727
Initial draft for PROPFIND for symlinks
taminob Nov 15, 2023
7f18076
Revert "Initial draft for PROPFIND for symlinks"
taminob Nov 22, 2023
4f7ae29
Draft for symlink mimetype on regular file for symlinks
taminob Nov 22, 2023
7348605
SymlinkManager: Initial implementation for symlink database management
taminob Nov 22, 2023
15f6ac6
SymlinkManager: Introduce symlink table via database migration
taminob Nov 22, 2023
d182a3a
SymlinkManager: Accept FileInfo type and escape path for purgeSymlink
taminob Nov 22, 2023
4e8748f
BulkUpload: Use SymlinkManager to create symlinks
taminob Nov 22, 2023
edf4f95
FilesPlugin: Use SymlinkManager to check for symlinks
taminob Nov 22, 2023
8127b70
FileInfo: Remove unused TYPE_SYMLINK
taminob Nov 22, 2023
8b21c07
SymlinkPlugin: Initialize SymlinkPlugin with basic implementation
taminob Nov 22, 2023
c6e2e8c
Server: Register SymlinkPlugin
taminob Nov 22, 2023
47ee7ba
ChunkingV2Plugin: Remove symlink handling
taminob Nov 23, 2023
641eac7
SymlinkPlugin: Pass correct object to symlinkManager for httpDelete
taminob Nov 23, 2023
61faf44
migrations: Rename symlink migration to correct format
taminob Nov 23, 2023
8d7dcf6
SymlinkManager: Various small but important fixes
taminob Nov 23, 2023
6dc1aa6
Revert now unnecessary changes
taminob Nov 23, 2023
e0d5164
SymlinkPlugin/FilesPlugin: Fix httpGet and httpDelete for symlinks
taminob Nov 27, 2023
eac2b0a
migrations: Remove last_updated column from symlinks table
taminob Nov 29, 2023
2eb1252
SymlinkManager: Remove last_updated from operations
taminob Nov 29, 2023
ed77937
FilesPlugin: Send Last-Modified and ETag header for GET response
taminob Dec 2, 2023
7bf32f3
SymlinkPlugin: Handle move operation for symlinks
taminob Dec 4, 2023
b480d40
SymlinkPlugin: Fix PUT for regular file at previous symlink location
taminob Dec 4, 2023
a09a196
BulkUploadPlugin: Fix POST for regular file at previous symlink location
taminob Dec 4, 2023
90a75b5
migrations: Remove default for storage column
taminob Dec 4, 2023
b8051ce
SymlinkManager: Use numeric_id for storage in database
taminob Dec 5, 2023
24cea5d
SymlinkManager: Reduce code duplication for numeric_id and use intern…
taminob Dec 6, 2023
8a9a48c
SymlinkPlugin: Fix update of symlinks for move operation
taminob Dec 6, 2023
c44b5af
BulkUploadPlugin: Cleanup for "feature(dav/bulk): Add symlink upload …
taminob Dec 6, 2023
0e1fbed
SymlinkManager: Move to correct namespace OC\Files and update all Plu…
taminob Dec 6, 2023
22324eb
composer: Update namespace of SymlinkPlugin in autoload files
taminob Dec 6, 2023
b5c1828
Connector\Sabre\File: Add string as possible parameter type to put
taminob Dec 6, 2023
6f60e7e
FilesPlugin: Fix resourcetype of symlinks for PROPFIND
taminob Dec 6, 2023
47d3eae
FilesPlugin/SymlinkPlugin/SymlinkManager: Fix some style guide violat…
taminob Dec 6, 2023
65b5796
PreviewController: Indicate symlinks via preview icon in web ui
taminob Dec 14, 2023
4fac7d8
SymlinkPlugin: Fix copying of symlinks
taminob Dec 14, 2023
6666955
FilesPlugin: Fix linter issue
taminob Jan 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Upload\\FutureFile' => __DIR__ . '/..' . '/../lib/Upload/FutureFile.php',
'OCA\\DAV\\Upload\\PartFile' => __DIR__ . '/..' . '/../lib/Upload/PartFile.php',
'OCA\\DAV\\Upload\\RootCollection' => __DIR__ . '/..' . '/../lib/Upload/RootCollection.php',
'OCA\\DAV\\Upload\\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',
Expand Down
15 changes: 15 additions & 0 deletions apps/dav/lib/BulkUpload/BulkUploadPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,12 +38,18 @@ class BulkUploadPlugin extends ServerPlugin {
private Folder $userFolder;
private LoggerInterface $logger;

/**
* @var SymlinkManager
*/
private $symlinkManager;

public function __construct(
Folder $userFolder,
LoggerInterface $logger
) {
$this->userFolder = $userFolder;
$this->logger = $logger;
$this->symlinkManager = new SymlinkManager();
}

/**
Expand Down Expand Up @@ -93,6 +100,14 @@ public function httpPost(RequestInterface $request, ResponseInterface $response)
$node->touch($mtime);
$node = $this->userFolder->getById($node->getId())[0];

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']] = [
"error" => false,
"etag" => $node->getETag(),
Expand Down
2 changes: 1 addition & 1 deletion apps/dav/lib/Connector/Sabre/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions apps/dav/lib/Connector/Sabre/FilesPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,6 +74,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';
Expand Down Expand Up @@ -103,6 +105,7 @@ class FilesPlugin extends ServerPlugin {
private IConfig $config;
private IRequest $request;
private IPreview $previewManager;
private SymlinkManager $symlinkManager;

public function __construct(Tree $tree,
IConfig $config,
Expand All @@ -118,6 +121,7 @@ public function __construct(Tree $tree,
$this->isPublic = $isPublic;
$this->downloadAttachment = $downloadAttachment;
$this->previewManager = $previewManager;
$this->symlinkManager = new SymlinkManager();
}

/**
Expand Down Expand Up @@ -159,7 +163,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();
Expand Down Expand Up @@ -224,13 +229,35 @@ public function handleDownloadToken(RequestInterface $request, ResponseInterface
}
}

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 true;
}

$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');
$response->setHeader('OC-ETag', $node->getEtag());
$response->setHeader('ETag', $node->getEtag());
$response->setBody($node->get());
$response->setStatus(200);
// 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): void {
// Only handle valid files
$node = $this->tree->getNodeForPath($request->getPath());
if (!($node instanceof IFile)) {
Expand Down Expand Up @@ -431,6 +458,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 ($this->symlinkManager->isSymlink($info)) {
return new \Sabre\DAV\Xml\Property\ResourceType(['{DAV:}symlink']);
}
return null;
});
}

if ($node instanceof Directory) {
Expand Down
2 changes: 2 additions & 0 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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($logger));

// allow setup of additional plugins
$dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event);
Expand Down
169 changes: 169 additions & 0 deletions apps/dav/lib/Upload/SymlinkPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);
/*
* @copyright Copyright (c) 2023 Tamino Bauknecht <[email protected]>
*
* @author Tamino Bauknecht <[email protected]>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\DAV\Upload;

use OC\Files\SymlinkManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;

class SymlinkPlugin extends ServerPlugin {
/**
* @var Server
*
* @psalm-suppress PropertyNotSetInConstructor
*/
private $server;
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
/** @var SymlinkManager */
private $symlinkManager;
/** @var LoggerInterface */
private $logger;

public function __construct(LoggerInterface $logger) {
$this->symlinkManager = new SymlinkManager();
$this->logger = $logger;
}

/**
* @inheritdoc
*/
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;
}

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);
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
$this->symlinkManager->storeSymlink($symlinkNode->getFileInfo());

if ($etag) {
$response->setHeader('OC-ETag', $etag);
}
$response->setStatus(201);
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; // continue handling this request
}

public function httpDelete(RequestInterface $request, ResponseInterface $response): bool {
$path = $request->getPath();
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof \OCA\DAV\Connector\Sabre\Node) {
return true;
}
$info = $node->getFileInfo();
if ($this->symlinkManager->isSymlink($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;
}

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);
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 (!$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($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);
}
}
}
17 changes: 17 additions & 0 deletions core/Controller/PreviewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -51,6 +58,8 @@ public function __construct(
private IMimeIconProvider $mimeIconProvider,
) {
parent::__construct($appName, $request);

$this->symlinkManager = new SymlinkManager();
}

/**
Expand Down Expand Up @@ -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, [
Expand Down
Loading