5 use Laminas\Diactoros\Response\EmptyResponse
;
6 use Laminas\Diactoros\Response\JsonResponse
;
7 use Psr\Http\Message\ResponseInterface
;
8 use Psr\Http\Message\ServerRequestInterface
;
9 use Psr\Http\Server\RequestHandlerInterface
;
10 use wcf\data\file\FileEditor
;
11 use wcf\data\file\temporary\FileTemporary
;
12 use wcf\data\file\temporary\FileTemporaryEditor
;
14 use wcf\system\exception\IllegalLinkException
;
15 use wcf\system\io\File
as IoFile
;
17 final class FileUploadAction
implements RequestHandlerInterface
20 * Read data in chunks to avoid hitting the memory limit.
21 * See https://stackoverflow.com/a/61997147
23 private const FREAD_BUFFER_SIZE
= 10 * 1_024
* 1_024
;
25 public function handle(ServerRequestInterface
$request): ResponseInterface
27 // TODO: `sequenceNo` should be of type `non-negative-int`, but requires Valinor 1.7+
28 $parameters = Helper
::mapQueryParameters(
29 $request->getQueryParams(),
32 checksum: non-empty-string,
33 identifier: non-empty-string,
39 $fileTemporary = new FileTemporary($parameters['identifier']);
40 if (!$fileTemporary->identifier
) {
41 // TODO: Proper error message
42 throw new IllegalLinkException();
45 // Check if this is a valid sequence no.
46 if ($parameters['sequenceNo'] >= $fileTemporary->getChunkCount()) {
47 // TODO: Proper error message
48 throw new IllegalLinkException();
51 // Check if this chunk has already been written.
52 if ($fileTemporary->hasChunk($parameters['sequenceNo'])) {
54 return new EmptyResponse(409);
57 // Validate the chunk size.
58 $chunkSize = $fileTemporary->getChunkSize();
59 $stream = $request->getBody();
60 $receivedSize = $stream->getSize();
61 if ($receivedSize !== null && $receivedSize > $chunkSize) {
62 // 413 Content Too Large
63 return new EmptyResponse(413);
66 $tmpPath = $fileTemporary->getPath();
67 if (!\
is_dir($tmpPath)) {
68 \
mkdir($tmpPath, recursive
: true);
71 $file = new IoFile($tmpPath . $fileTemporary->getFilename(), 'cb+');
72 $file->lock(\LOCK_EX
);
73 $file->seek($parameters['sequenceNo'] * $chunkSize);
75 // Check if the checksum matches the received data.
76 $ctx = \
hash_init('sha256');
78 while (!$stream->eof()) {
79 // Write the chunk using a buffer to avoid blowing up the memory limit.
80 // See https://stackoverflow.com/a/61997147
81 $chunk = $stream->read(self
::FREAD_BUFFER_SIZE
);
82 $total +
= \
strlen($chunk);
84 if ($total > $chunkSize) {
85 // 413 Content Too Large
86 return new EmptyResponse(413);
89 \
hash_update($ctx, $chunk);
95 $result = \
hash_final($ctx);
97 if ($result !== $parameters['checksum']) {
98 // TODO: Proper error message
99 throw new IllegalLinkException();
102 // Mark the chunk as written.
103 $chunks = $fileTemporary->chunks
;
104 $chunks[$parameters['sequenceNo']] = '1';
105 (new FileTemporaryEditor($fileTemporary))->update([
109 // Check if we have all chunks.
110 if ($chunks === \
str_repeat('1', $fileTemporary->getChunkCount())) {
111 // Check if the final result matches the expected checksum.
112 $checksum = \
hash_file('sha256', $tmpPath . $fileTemporary->getFilename());
113 if ($checksum !== $fileTemporary->fileHash
) {
114 // TODO: Proper error message
115 throw new IllegalLinkException();
118 $file = FileEditor
::createFromTemporary($fileTemporary);
120 $context = $fileTemporary->getContext();
121 (new FileTemporaryEditor($fileTemporary))->delete();
122 unset($fileTemporary);
124 $processor = $file->getProcessor();
125 if ($processor === null) {
126 // TODO: Mark the file as orphaned.
127 \assert
($processor !== null);
130 $processor->adopt($file, $context);
132 // TODO: This is just debug code.
133 return new JsonResponse([
134 'file' => $file->getPath() . $file->getSourceFilename(),
138 return new EmptyResponse();