<div class="messageTabMenuContent" id="attachments_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}">
<button type="button">
<woltlab-core-file-upload
- data-endpoint="{link controller='FileUpload'}{/link}"
+ data-endpoint="{link controller='FileUploadPreflight'}{/link}"
></woltlab-core-file-upload>
</button>
<dl class="wide">
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+type PreflightResponse = {
+ endpoints: string[];
+};
+
async function upload(element: WoltlabCoreFileUploadElement, file: File): Promise<void> {
+ const response = (await prepareRequest(element.dataset.endpoint!)
+ .post({
+ filename: file.name,
+ filesize: file.size,
+ })
+ .fetchAsJson()) as PreflightResponse;
+ const { endpoints } = response;
+
const chunkSize = 2_000_000;
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
- const chunk = file.slice(i * chunkSize, i * chunkSize + chunkSize + 1);
+ const start = i * chunkSize;
+ const end = start + chunkSize;
+ const chunk = file.slice(start, end);
- const response = await prepareRequest(element.dataset.endpoint!).post(chunk).fetchAsResponse();
- console.log(response);
+ await prepareRequest(endpoints[i]).post(chunk).fetchAsResponse();
}
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = void 0;
async function upload(element, file) {
+ const response = (await (0, Backend_1.prepareRequest)(element.dataset.endpoint)
+ .post({
+ filename: file.name,
+ filesize: file.size,
+ })
+ .fetchAsJson());
+ const { endpoints } = response;
const chunkSize = 2000000;
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
- const chunk = file.slice(i * chunkSize, i * chunkSize + chunkSize + 1);
- const response = await (0, Backend_1.prepareRequest)(element.dataset.endpoint).post(chunk).fetchAsResponse();
- console.log(response);
+ const start = i * chunkSize;
+ const end = start + chunkSize;
+ const chunk = file.slice(start, end);
+ await (0, Backend_1.prepareRequest)(endpoints[i]).post(chunk).fetchAsResponse();
}
}
function setup() {
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use wcf\http\Helper;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
final class FileUploadAction implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
+ // TODO: `sequenceNo` should be of type `non-negative-int`, but requires Valinor 1.7+
+ $parameters = Helper::mapQueryParameters(
+ $request->getQueryParams(),
+ <<<'EOT'
+ array {
+ identifier: non-empty-string,
+ sequenceNo: int,
+ }
+ EOT,
+ );
+
+ $sql = "SELECT *
+ FROM wcf1_file_temporary
+ WHERE identifier = ?";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute([$parameters['identifier']]);
+ $row = $statement->fetchSingleRow();
+
+ if ($row === false) {
+ throw new IllegalLinkException();
+ }
+
+ // Check if this is a valid sequence no.
+ // TODO: The chunk calculation shouldn’t be based on a fixed number.
+ $chunkSize = 2_000_000;
+ $chunks = (int)\ceil($row['filesize'] / $chunkSize);
+ if ($parameters['sequenceNo'] >= $chunks) {
+ throw new IllegalLinkException();
+ }
+
+ // Check if the actual size matches the expectations.
+ if ($parameters['sequenceNo'] === $chunks - 1) {
+ // The last chunk is most likely smaller than our chunk size.
+ $expectedSize = $row['filesize'] - $chunkSize * ($chunks - 1);
+ } else {
+ $expectedSize = $chunkSize;
+ }
+
+ $chunk = \file_get_contents('php://input');
+ $actualSize = \strlen($chunk);
+
+ if ($actualSize !== $expectedSize) {
+ throw new IllegalLinkException();
+ }
+
+ $folderA = \substr($row['identifier'], 0, 2);
+ $folderB = \substr($row['identifier'], 2, 2);
+
+ $tmpPath = \sprintf(
+ \WCF_DIR . '_data/private/fileUpload/%s/%s/',
+ $folderA,
+ $folderB,
+ );
+ if (!\is_dir($tmpPath)) {
+ \mkdir($tmpPath, recursive: true);
+ }
+
+ $filename = \sprintf(
+ '%s-%d.bin',
+ $row['identifier'],
+ $parameters['sequenceNo'],
+ );
+
+ \file_put_contents($tmpPath . $filename, $chunk);
+
// TODO: Dummy response to simulate a successful upload of a chunk.
return new EmptyResponse();
}
--- /dev/null
+<?php
+
+namespace wcf\action;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\http\Helper;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+final class FileUploadPreflightAction implements RequestHandlerInterface
+{
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ // TODO: For now we only require the filename and size to be provided.
+ $parameters = Helper::mapQueryParameters(
+ $request->getParsedBody(),
+ <<<'EOT'
+ array {
+ filename: non-empty-string,
+ filesize: positive-int,
+ }
+ EOT,
+ );
+
+ // TODO: The chunk calculation shouldn’t be based on a fixed number.
+ $chunkSize = 2_000_000;
+ $chunks = (int)\ceil($parameters['filesize'] / $chunkSize);
+
+ $identifier = $this->createTemporaryFile($parameters);
+
+ $endpoints = [];
+ for ($i = 0; $i < $chunks; $i++) {
+ $endpoints[] = LinkHandler::getInstance()->getControllerLink(
+ FileUploadAction::class,
+ [
+ 'identifier' => $identifier,
+ 'sequenceNo' => $i,
+ ]
+ );
+ }
+
+ return new JsonResponse([
+ 'endpoints' => $endpoints,
+ ]);
+ }
+
+ private function createTemporaryFile(array $parameters): string
+ {
+ $identifier = \bin2hex(\random_bytes(20));
+
+ $sql = "INSERT INTO wcf1_file_temporary
+ (identifier, time, filename, filesize)
+ VALUES (?, ?, ?, ?)";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute([
+ $identifier,
+ \TIME_NOW,
+ $parameters['filename'],
+ $parameters['filesize'],
+ ]);
+
+ return $identifier;
+ }
+}
DROP TABLE IF EXISTS wcf1_file_temporary;
CREATE TABLE wcf1_file_temporary (
- uuidv4 CHAR(36) NOT NULL PRIMARY KEY,
- prefix CHAR(40) NOT NULL,
- lastModified INT NOT NULL,
+ identifier CHAR(40) NOT NULL PRIMARY KEY,
+ time INT NOT NULL,
filename VARCHAR(255) NOT NULL,
- filesize BIGINT NOT NULL,
- chunks SMALLINT NOT NULL
+ filesize BIGINT NOT NULL
);
DROP TABLE IF EXISTS wcf1_file_chunk;
CREATE TABLE wcf1_file_chunk (
- uuidv4 CHAR(36) NOT NULL,
+ identifier CHAR(40) NOT NULL,
sequenceNo SMALLINT NOT NULL,
- PRIMARY KEY chunk (uuidv4, sequenceNo)
+ PRIMARY KEY chunk (identifier, sequenceNo)
);
/* As the flood control table can be a high traffic table and as it is periodically emptied,