Migrate the file upload preflight to the new API
authorAlexander Ebert <ebert@woltlab.com>
Thu, 21 Mar 2024 13:08:57 +0000 (14:08 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 27 Mar 2024 22:55:27 +0000 (23:55 +0100)
ts/WoltLabSuite/Core/Api/Files/Upload.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Component/File/Upload.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Component/File/Upload.js
wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php
wcfsetup/install/files/lib/system/endpoint/controller/core/files/PostUpload.class.php [new file with mode: 0644]

diff --git a/ts/WoltLabSuite/Core/Api/Files/Upload.ts b/ts/WoltLabSuite/Core/Api/Files/Upload.ts
new file mode 100644 (file)
index 0000000..6125d2b
--- /dev/null
@@ -0,0 +1,34 @@
+import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
+import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";
+
+type Response = {
+  identifier: string;
+  numberOfChunks: number;
+};
+
+export async function upload(
+  filename: string,
+  fileSize: number,
+  fileHash: string,
+  typeName: string,
+  context: string,
+): Promise<ApiResult<Response>> {
+  const url = new URL(window.WSC_API_URL + "index.php?api/rpc/core/files/upload");
+
+  const payload = {
+    filename,
+    fileSize,
+    fileHash,
+    typeName,
+    context,
+  };
+
+  let response: Response;
+  try {
+    response = (await prepareRequest(url).post(payload).fetchAsJson()) as Response;
+  } catch (e) {
+    return apiResultFromError(e);
+  }
+
+  return apiResultFromValue(response);
+}
index b9d30bb437cc6ed184ecad28d63f8e324691a723..1636aba3c29a60bbcfbd16f4b84c042376b6a691 100644 (file)
@@ -2,12 +2,9 @@ import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
 import { StatusNotOk } from "WoltLabSuite/Core/Ajax/Error";
 import { isPlainObject } from "WoltLabSuite/Core/Core";
 import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { upload as filesUpload } from "WoltLabSuite/Core/Api/Files/Upload";
 import WoltlabCoreFileElement from "./woltlab-core-file";
 
-type PreflightResponse = {
-  endpoints: string[];
-};
-
 type UploadResponse =
   | { completed: false }
   | ({
@@ -46,47 +43,33 @@ async function upload(element: WoltlabCoreFileUploadElement, file: File): Promis
   const event = new CustomEvent<WoltlabCoreFileElement>("uploadStart", { detail: fileElement });
   element.dispatchEvent(event);
 
-  let response: PreflightResponse | undefined = undefined;
-  try {
-    response = (await prepareRequest(element.dataset.endpoint!)
-      .post({
-        filename: file.name,
-        fileSize: file.size,
-        fileHash,
-        typeName,
-        context: element.dataset.context,
-      })
-      .fetchAsJson()) as PreflightResponse;
-  } catch (e) {
-    if (e instanceof StatusNotOk) {
-      const body = await e.response.clone().json();
-      if (isPlainObject(body) && isPlainObject(body.error)) {
-        console.log(body);
-        return;
-      } else {
-        throw e;
-      }
-    } else {
-      throw e;
-    }
-  } finally {
-    if (response === undefined) {
+  const response = await filesUpload(file.name, file.size, fileHash, typeName, element.dataset.context || "");
+  if (!response.ok) {
+    const validationError = response.error.getValidationError();
+    if (validationError === undefined) {
       fileElement.uploadFailed();
+
+      throw response.error;
     }
+
+    console.log(validationError);
+    return;
   }
 
-  const { endpoints } = response;
+  const { identifier, numberOfChunks } = response.value;
 
-  const chunkSize = Math.ceil(file.size / endpoints.length);
+  const chunkSize = Math.ceil(file.size / numberOfChunks);
 
   // TODO: Can we somehow report any meaningful upload progress?
 
-  for (let i = 0, length = endpoints.length; i < length; i++) {
+  for (let i = 0; i < numberOfChunks; i++) {
     const start = i * chunkSize;
     const end = start + chunkSize;
     const chunk = file.slice(start, end);
 
-    const endpoint = new URL(endpoints[i]);
+    // TODO fix the URL
+    throw new Error("TODO: fix the url");
+    const endpoint = new URL(String(i));
 
     const checksum = await getSha256Hash(await chunk.arrayBuffer());
     endpoint.searchParams.append("checksum", checksum);
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Files/Upload.js
new file mode 100644 (file)
index 0000000..fa70e0a
--- /dev/null
@@ -0,0 +1,24 @@
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.upload = void 0;
+    async function upload(filename, fileSize, fileHash, typeName, context) {
+        const url = new URL(window.WSC_API_URL + "index.php?api/rpc/core/files/upload");
+        const payload = {
+            filename,
+            fileSize,
+            fileHash,
+            typeName,
+            context,
+        };
+        let response;
+        try {
+            response = (await (0, Backend_1.prepareRequest)(url).post(payload).fetchAsJson());
+        }
+        catch (e) {
+            return (0, Result_1.apiResultFromError)(e);
+        }
+        return (0, Result_1.apiResultFromValue)(response);
+    }
+    exports.upload = upload;
+});
index 31bc27703ad2003fc54b11b1699ecf0baec81900..bf96359f6cb3507cc524afa84f373dfe5ba155ce 100644 (file)
@@ -1,4 +1,4 @@
-define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Ajax/Error", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Helper/Selector"], function (require, exports, Backend_1, Error_1, Core_1, Selector_1) {
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Api/Files/Upload"], function (require, exports, Backend_1, Selector_1, Upload_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.setup = void 0;
@@ -9,46 +9,26 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Co
         fileElement.dataset.filename = file.name;
         const event = new CustomEvent("uploadStart", { detail: fileElement });
         element.dispatchEvent(event);
-        let response = undefined;
-        try {
-            response = (await (0, Backend_1.prepareRequest)(element.dataset.endpoint)
-                .post({
-                filename: file.name,
-                fileSize: file.size,
-                fileHash,
-                typeName,
-                context: element.dataset.context,
-            })
-                .fetchAsJson());
-        }
-        catch (e) {
-            if (e instanceof Error_1.StatusNotOk) {
-                const body = await e.response.clone().json();
-                if ((0, Core_1.isPlainObject)(body) && (0, Core_1.isPlainObject)(body.error)) {
-                    console.log(body);
-                    return;
-                }
-                else {
-                    throw e;
-                }
-            }
-            else {
-                throw e;
-            }
-        }
-        finally {
-            if (response === undefined) {
+        const response = await (0, Upload_1.upload)(file.name, file.size, fileHash, typeName, element.dataset.context || "");
+        if (!response.ok) {
+            const validationError = response.error.getValidationError();
+            if (validationError === undefined) {
                 fileElement.uploadFailed();
+                throw response.error;
             }
+            console.log(validationError);
+            return;
         }
-        const { endpoints } = response;
-        const chunkSize = Math.ceil(file.size / endpoints.length);
+        const { identifier, numberOfChunks } = response.value;
+        const chunkSize = Math.ceil(file.size / numberOfChunks);
         // TODO: Can we somehow report any meaningful upload progress?
-        for (let i = 0, length = endpoints.length; i < length; i++) {
+        for (let i = 0; i < numberOfChunks; i++) {
             const start = i * chunkSize;
             const end = start + chunkSize;
             const chunk = file.slice(start, end);
-            const endpoint = new URL(endpoints[i]);
+            // TODO fix the URL
+            throw new Error("TODO: fix the url");
+            const endpoint = new URL(String(i));
             const checksum = await getSha256Hash(await chunk.arrayBuffer());
             endpoint.searchParams.append("checksum", checksum);
             let response;
index 64c2c69e3df13464c87383a6ec54c809810c518b..df0524ab74a9b4188541e1613183a0c8e4015c0f 100644 (file)
@@ -86,6 +86,7 @@ return static function (): void {
     });
 
     $eventHandler->register(ControllerCollecting::class, static function (ControllerCollecting $event) {
+        $event->register(new \wcf\system\endpoint\controller\core\files\PostUpload);
         $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions);
         $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession);
     });
diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PostUpload.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PostUpload.class.php
new file mode 100644 (file)
index 0000000..39e2d62
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+
+namespace wcf\system\endpoint\controller\core\files;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use wcf\data\file\temporary\FileTemporary;
+use wcf\data\file\temporary\FileTemporaryAction;
+use wcf\http\Helper;
+use wcf\system\endpoint\IController;
+use wcf\system\endpoint\PostRequest;
+use wcf\system\exception\SystemException;
+use wcf\system\exception\UserInputException;
+use wcf\system\file\processor\FileProcessor;
+use wcf\util\JSON;
+
+#[PostRequest('/core/files/upload')]
+final class PostUpload implements IController
+{
+    public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface
+    {
+        $parameters = Helper::mapApiParameters($request, PostUploadParameters::class);
+
+        $fileProcessor = FileProcessor::getInstance()->forTypeName($parameters->typeName);
+        if ($fileProcessor === null) {
+            throw new UserInputException('typeName', 'unknown');
+        }
+
+        try {
+            $decodedContext = JSON::decode($parameters->context);
+        } catch (SystemException) {
+            throw new UserInputException('context', 'invalid');
+        }
+
+        $validationResult = $fileProcessor->acceptUpload($parameters->filename, $parameters->fileSize, $decodedContext);
+        if (!$validationResult->ok()) {
+            throw new UserInputException('filename', $validationResult->toString());
+        }
+
+        $numberOfChunks = FileTemporary::getNumberOfChunks($parameters->fileSize);
+        if ($numberOfChunks > FileTemporary::MAX_CHUNK_COUNT) {
+            throw new UserInputException('fileSize', 'tooLarge');
+        }
+
+        $fileTemporary = $this->createTemporaryFile($parameters, $numberOfChunks);
+
+        return new JsonResponse([
+            'identifier' => $fileTemporary->identifier,
+            'numberOfChunks' => $numberOfChunks,
+        ]);
+    }
+
+    private function createTemporaryFile(PostUploadParameters $parameters, int $numberOfChunks): FileTemporary
+    {
+        $identifier = \bin2hex(\random_bytes(20));
+
+        $action = new FileTemporaryAction([], 'create', [
+            'data' => [
+                'identifier' => $identifier,
+                'time' => \TIME_NOW,
+                'filename' => $parameters->filename,
+                'fileSize' => $parameters->fileSize,
+                'fileHash' => $parameters->fileHash,
+                'typeName' => $parameters->typeName,
+                'context' => $parameters->context,
+                'chunks' => \str_repeat('0', $numberOfChunks),
+            ],
+        ]);
+
+        return $action->executeAction()['returnValues'];
+    }
+}
+
+/** @internal */
+final class PostUploadParameters
+{
+    public function __construct(
+        /** @var non-empty-string */
+        public readonly string $filename,
+
+        /** @var positive-int **/
+        public readonly int $fileSize,
+
+        /** @var non-empty-string */
+        public readonly string $fileHash,
+
+        /** @var non-empty-string */
+        public readonly string $typeName,
+
+        /** @var non-empty-string */
+        public readonly string $context,
+    ) {
+    }
+}