Browse Source

Allow app to register a download provider

This allows to provide a file content for a given dav path.

Signed-off-by: Louis Chemineau <louis@chmn.me>
Louis Chemineau 1 year ago
parent
commit
9c2ded54d5

+ 6 - 0
apps/files/appinfo/routes.php

@@ -209,6 +209,12 @@ $application->registerRoutes(
 				'url' => '/api/v1/openlocaleditor/{token}',
 				'verb' => 'POST',
 			],
+			[
+				/** @see DownloadController::index() */
+				'name' => 'Download#index',
+				'url' => '/api/v1/download',
+				'verb' => 'GET',
+			],
 		],
 	]
 );

+ 2 - 0
apps/files/composer/composer/autoload_classmap.php

@@ -35,6 +35,7 @@ return array(
     'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
     'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
     'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php',
+    'OCA\\Files\\Controller\\DownloadController' => $baseDir . '/../lib/Controller/DownloadController.php',
     'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php',
     'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php',
     'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php',
@@ -53,6 +54,7 @@ return array(
     'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
     'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
     'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
+    'OCA\\Files\\Provider\\FileDownloadProvider' => $baseDir . '/../lib/Provider/FileDownloadProvider.php',
     'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php',
     'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',
     'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php',

+ 2 - 0
apps/files/composer/composer/autoload_static.php

@@ -50,6 +50,7 @@ class ComposerStaticInitFiles
         'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
         'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
         'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php',
+        'OCA\\Files\\Controller\\DownloadController' => __DIR__ . '/..' . '/../lib/Controller/DownloadController.php',
         'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php',
         'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php',
         'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php',
@@ -68,6 +69,7 @@ class ComposerStaticInitFiles
         'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
         'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
         'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
+        'OCA\\Files\\Provider\\FileDownloadProvider' => __DIR__ . '/..' . '/../lib/Provider/FileDownloadProvider.php',
         'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php',
         'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',
         'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php',

+ 3 - 0
apps/files/lib/AppInfo/Application.php

@@ -45,6 +45,7 @@ use OCA\Files\Event\LoadSidebar;
 use OCA\Files\Listener\LegacyLoadAdditionalScriptsAdapter;
 use OCA\Files\Listener\LoadSidebarListener;
 use OCA\Files\Notification\Notifier;
+use OCA\Files\Provider\FileDownloadProvider;
 use OCA\Files\Search\FilesSearchProvider;
 use OCA\Files\Service\TagService;
 use OCA\Files\Service\UserConfig;
@@ -122,6 +123,8 @@ class Application extends App implements IBootstrap {
 		$context->registerSearchProvider(FilesSearchProvider::class);
 
 		$context->registerNotifierService(Notifier::class);
+
+		$context->registerFileDownloadProvider(FileDownloadProvider::class);
 	}
 
 	public function boot(IBootContext $context): void {

+ 154 - 0
apps/files/lib/Controller/DownloadController.php

@@ -0,0 +1,154 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
+ *
+ * @author Louis Chmn <louis@chmn.me>
+ *
+ * @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\Files\Controller;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OCP\AppFramework\Http\ZipResponse;
+use OCP\AppFramework\Controller;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IFileDownloadProvider;
+use OCP\Files\Node;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+class DownloadController extends Controller {
+	private Coordinator $coordinator;
+	private LoggerInterface $logger;
+
+	public function __construct(
+		string $appName,
+		IRequest $request,
+		Coordinator $coordinator,
+		LoggerInterface $logger
+	) {
+		parent::__construct($appName, $request);
+
+		$this->request = $request;
+		$this->coordinator = $coordinator;
+		$this->logger = $logger;
+	}
+
+	/**
+	 * @NoCSRFRequired
+	 * @PublicPage
+	 * @UserRateThrottle(limit=5, period=100)
+	 * @AnonRateThrottle(limit=1, period=100)
+	 * @BruteForceProtection(action='download_files')
+	 */
+	public function index(string $files): ZipResponse {
+		$response = new ZipResponse($this->request, 'download');
+
+		/** @var string[] */
+		$files = json_decode($files);
+
+		if (count($files) === 0) {
+			return $response;
+		}
+
+		[$firstPrefix,] = \Sabre\Uri\split($files[0]);
+		$commonPrefix = $firstPrefix;
+		foreach ($files as $filePath) {
+			$commonPrefix = $this->getCommonPrefix($filePath, $commonPrefix);
+		}
+
+		foreach ($files as $filePath) {
+			$node = null;
+
+			foreach ($this->getProviders() as $provider) {
+				try {
+					$node = $provider->getNode($filePath);
+					if ($node !== null) {
+						break;
+					}
+				} catch (\Throwable $ex) {
+					$providerClass = $provider::class;
+					$this->logger->warning("Error while getting file content from $providerClass", ['exception' => $ex]);
+				}
+			}
+
+			if ($node === null) {
+				continue;
+			}
+
+			$this->addNode($response, $node, substr($filePath, strlen($commonPrefix)));
+		}
+
+		return $response;
+	}
+
+	private function getCommonPrefix(string $str1, string $str2): string {
+		$mbStr1 = mb_str_split($str1);
+		$mbStr2 = mb_str_split($str2);
+
+		for ($i = 0; $i < count($mbStr1); $i++) {
+			if ($mbStr1[$i] !== $mbStr2[$i]) {
+				$i--;
+				break;
+			}
+		}
+
+		if ($i < 0) {
+			return '';
+		} else {
+			return join(array_slice($mbStr1, 0, $i));
+		}
+	}
+
+	private function addNode(ZipResponse $response, Node $node, string $path): void {
+		if ($node instanceof File) {
+			$response->addResource($node->fopen('r'), $path, $node->getSize());
+		}
+
+		if ($node instanceof Folder) {
+			foreach ($node->getDirectoryListing() as $subnode) {
+				$this->addNode($response, $subnode, $path.'/'.$subnode->getName());
+			}
+		}
+	}
+
+	/**
+	 * @return IFileDownloadProvider[]
+	 */
+	private function getProviders() {
+		/** @var IFileDownloadProvider[] */
+		$providers = [];
+
+		$context = $this->coordinator->getRegistrationContext();
+		if ($context === null) {
+			throw new \Exception("Can't get download providers");
+		}
+
+		$providerRegistrations = $context->getFileDownloadProviders();
+
+		foreach ($providerRegistrations as $registration) {
+			$providers[] = \OCP\Server::get($registration->getService());
+		}
+
+		return $providers;
+	}
+}

+ 60 - 0
apps/files/lib/Provider/FileDownloadProvider.php

@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
+ *
+ * @author Louis Chmn <louis@chmn.me>
+ *
+ * @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\Files\Provider;
+
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\Files\IFileDownloadProvider;
+
+class FileDownloadProvider implements IFileDownloadProvider {
+	private ?Folder $userFolder;
+
+	public function __construct(
+		?Folder $userFolder
+	) {
+		$this->userFolder = $userFolder;
+	}
+
+	public function getNode(string $davPath): ?Node {
+		if (!str_starts_with($davPath, "files/")) {
+			return null;
+		}
+
+		if ($this->userFolder === null) {
+			return null;
+		}
+
+		/** @var ?string */
+		$userId = explode('/', $davPath, 3)[1] ?? null;
+		if (is_null($userId) || $userId !== $this->userFolder->getOwner()->getUID()) {
+			return null;
+		}
+
+		$filePath = substr($davPath, strlen("files/$userId"));
+
+		return $this->userFolder->get($filePath);
+	}
+}

+ 1 - 0
lib/composer/composer/autoload_classmap.php

@@ -305,6 +305,7 @@ return array(
     'OCP\\Files\\ForbiddenException' => $baseDir . '/lib/public/Files/ForbiddenException.php',
     'OCP\\Files\\GenericFileException' => $baseDir . '/lib/public/Files/GenericFileException.php',
     'OCP\\Files\\IAppData' => $baseDir . '/lib/public/Files/IAppData.php',
+    'OCP\\Files\\IFileDownloadProvider' => $baseDir . '/lib/public/Files/IFileDownloadProvider.php',
     'OCP\\Files\\IHomeStorage' => $baseDir . '/lib/public/Files/IHomeStorage.php',
     'OCP\\Files\\IMimeTypeDetector' => $baseDir . '/lib/public/Files/IMimeTypeDetector.php',
     'OCP\\Files\\IMimeTypeLoader' => $baseDir . '/lib/public/Files/IMimeTypeLoader.php',

+ 1 - 0
lib/composer/composer/autoload_static.php

@@ -338,6 +338,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\Files\\ForbiddenException' => __DIR__ . '/../../..' . '/lib/public/Files/ForbiddenException.php',
         'OCP\\Files\\GenericFileException' => __DIR__ . '/../../..' . '/lib/public/Files/GenericFileException.php',
         'OCP\\Files\\IAppData' => __DIR__ . '/../../..' . '/lib/public/Files/IAppData.php',
+        'OCP\\Files\\IFileDownloadProvider' => __DIR__ . '/../../..' . '/lib/public/Files/IFileDownloadProvider.php',
         'OCP\\Files\\IHomeStorage' => __DIR__ . '/../../..' . '/lib/public/Files/IHomeStorage.php',
         'OCP\\Files\\IMimeTypeDetector' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeDetector.php',
         'OCP\\Files\\IMimeTypeLoader' => __DIR__ . '/../../..' . '/lib/public/Files/IMimeTypeLoader.php',

+ 22 - 0
lib/private/AppFramework/Bootstrap/RegistrationContext.php

@@ -47,6 +47,7 @@ use OCP\Capabilities\ICapability;
 use OCP\Dashboard\IManager;
 use OCP\Dashboard\IWidget;
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IFileDownloadProvider;
 use OCP\Files\Template\ICustomTemplateProvider;
 use OCP\Http\WellKnown\IHandler;
 use OCP\Notification\INotifier;
@@ -127,6 +128,9 @@ class RegistrationContext {
 	/** @var ParameterRegistration[] */
 	private $sensitiveMethods = [];
 
+	/** @var ServiceRegistration<IFileDownloadProvider>[] */
+	private array $fileDownloadProviders = [];
+
 	/** @var LoggerInterface */
 	private $logger;
 
@@ -326,6 +330,13 @@ class RegistrationContext {
 					$methods
 				);
 			}
+
+			public function registerFileDownloadProvider(string $class): void {
+				$this->context->registerFileDownloadProvider(
+					$this->appId,
+					$class
+				);
+			}
 		};
 	}
 
@@ -461,6 +472,10 @@ class RegistrationContext {
 		$this->sensitiveMethods[] = new ParameterRegistration($appId, $class, $methods);
 	}
 
+	public function registerFileDownloadProvider(string $appId, string $class): void {
+		$this->fileDownloadProviders[] = new ServiceRegistration($appId, $class);
+	}
+
 	/**
 	 * @param App[] $apps
 	 */
@@ -738,4 +753,11 @@ class RegistrationContext {
 	public function getSensitiveMethods(): array {
 		return $this->sensitiveMethods;
 	}
+
+	/**
+	 * @return ServiceRegistration<IFileDownloadProvider>[]
+	 */
+	public function getFileDownloadProviders(): array {
+		return $this->fileDownloadProviders;
+	}
 }

+ 10 - 0
lib/public/AppFramework/Bootstrap/IRegistrationContext.php

@@ -328,4 +328,14 @@ interface IRegistrationContext {
 	 * @since 25.0.0
 	 */
 	public function registerSensitiveMethods(string $class, array $methods): void;
+
+	/**
+	 * Register a backend to provide file based on a dav path.
+	 *
+	 * @param string $class
+	 * @param string[] $methods
+	 * @return void
+	 * @since 26.0.0
+	 */
+	public function registerFileDownloadProvider(string $class): void;
 }

+ 38 - 0
lib/public/Files/IFileDownloadProvider.php

@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
+ *
+ * @author Louis Chmn <louis@chmn.me>
+ *
+ * @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 OCP\Files;
+
+/**
+ * This interface defines how to provide a file given a dav path.
+ *
+ * @since 26.0.0
+ */
+interface IFileDownloadProvider {
+	/**
+	 * @since 26.0.0
+	 */
+	public function getNode(string $davPath): ?Node;
+}