5 use Laminas\Diactoros\Response\EmptyResponse
;
6 use Psr\Http\Message\ResponseInterface
;
7 use Psr\Http\Message\ServerRequestInterface
;
8 use Psr\Http\Server\RequestHandlerInterface
;
9 use wcf\data\file\temporary\FileTemporary
;
11 use wcf\system\exception\IllegalLinkException
;
12 use wcf\system\io\AtomicWriter
;
13 use wcf\system\io\File
;
15 final class FileUploadAction
implements RequestHandlerInterface
18 * Read data in chunks to avoid hitting the memory limit.
19 * See https://stackoverflow.com/a/61997147
21 private const FREAD_BUFFER_SIZE
= 10 * 1_024
* 1_024
;
23 public function handle(ServerRequestInterface
$request): ResponseInterface
25 // TODO: `sequenceNo` should be of type `non-negative-int`, but requires Valinor 1.7+
26 $parameters = Helper
::mapQueryParameters(
27 $request->getQueryParams(),
30 checksum: non-empty-string,
31 identifier: non-empty-string,
37 $fileTemporary = new FileTemporary($parameters['identifier']);
38 if (!$fileTemporary->identifier
) {
39 // TODO: Proper error message
40 throw new IllegalLinkException();
43 // Check if this is a valid sequence no.
44 $numberOfChunks = $fileTemporary->getNumberOfChunks();
45 if ($parameters['sequenceNo'] >= $numberOfChunks) {
46 // TODO: Proper error message
47 throw new IllegalLinkException();
50 // Check if the checksum matches the received data.
51 $ctx = \
hash_init('sha256');
52 $stream = $request->getBody();
53 while (!$stream->eof()) {
54 \
hash_update($ctx, $stream->read(self
::FREAD_BUFFER_SIZE
));
56 $result = \
hash_final($ctx);
59 if ($result !== $parameters['checksum']) {
60 // TODO: Proper error message
61 throw new IllegalLinkException();
64 $folderA = \
substr($fileTemporary->identifier
, 0, 2);
65 $folderB = \
substr($fileTemporary->identifier
, 2, 2);
68 \WCF_DIR
. '_data/private/fileUpload/%s/%s/',
72 if (!\
is_dir($tmpPath)) {
73 \
mkdir($tmpPath, recursive
: true);
76 // Write the chunk using a buffer to avoid blowing up the memory limit.
77 // See https://stackoverflow.com/a/61997147
78 $result = new AtomicWriter($tmpPath . $fileTemporary->getChunkFilename($parameters['sequenceNo']));
80 while (!$stream->eof()) {
81 $result->write($stream->read(self
::FREAD_BUFFER_SIZE
));
86 // Check if we have all chunks.
88 for ($i = 0; $i < $numberOfChunks; $i++
) {
89 $chunkFilename = $fileTemporary->getChunkFilename($i);
91 if (\file_exists
($tmpPath . $chunkFilename)) {
92 $data[] = $tmpPath . $chunkFilename;
96 if (\
count($data) === $numberOfChunks) {
97 // Concatenate the files by reading only a limited buffer at a time
98 // to avoid blowing up the memory limit.
99 // See https://stackoverflow.com/a/61997147
101 $resultFilename = $fileTemporary->getResultFilename();
102 $result = new AtomicWriter($tmpPath . $resultFilename);
103 foreach ($data as $fileChunk) {
104 $source = new File($fileChunk, 'rb');
106 while (!$source->eof()) {
107 $result->write($source->read(self
::FREAD_BUFFER_SIZE
));
116 // Check if the final result matches the expected checksum.
117 $checksum = \
hash_file('sha256', $tmpPath . $resultFilename);
118 if ($checksum !== $fileTemporary->fileHash
) {
119 // TODO: Proper error message
120 throw new IllegalLinkException();
123 // Remove the temporary chunks.
124 foreach ($data as $fileChunk) {
128 // TODO: Move the data from the temporary file to the actual "file".
131 return new EmptyResponse();