4e14983b4e34b8bac99e71574edbf2d5fd83fd72
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / action / FileUploadAction.class.php
1 <?php
2
3 namespace wcf\action;
4
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\http\Helper;
10 use wcf\system\exception\IllegalLinkException;
11 use wcf\system\io\AtomicWriter;
12 use wcf\system\WCF;
13
14 final class FileUploadAction implements RequestHandlerInterface
15 {
16 public function handle(ServerRequestInterface $request): ResponseInterface
17 {
18 // TODO: `sequenceNo` should be of type `non-negative-int`, but requires Valinor 1.7+
19 $parameters = Helper::mapQueryParameters(
20 $request->getQueryParams(),
21 <<<'EOT'
22 array {
23 identifier: non-empty-string,
24 sequenceNo: int,
25 }
26 EOT,
27 );
28
29 $sql = "SELECT *
30 FROM wcf1_file_temporary
31 WHERE identifier = ?";
32 $statement = WCF::getDB()->prepare($sql);
33 $statement->execute([$parameters['identifier']]);
34 $row = $statement->fetchSingleRow();
35
36 if ($row === false) {
37 throw new IllegalLinkException();
38 }
39
40 // Check if this is a valid sequence no.
41 // TODO: The chunk calculation shouldn’t be based on a fixed number.
42 $chunkSize = 2_000_000;
43 $chunks = (int)\ceil($row['filesize'] / $chunkSize);
44 if ($parameters['sequenceNo'] >= $chunks) {
45 throw new IllegalLinkException();
46 }
47
48 $folderA = \substr($row['identifier'], 0, 2);
49 $folderB = \substr($row['identifier'], 2, 2);
50
51 $tmpPath = \sprintf(
52 \WCF_DIR . '_data/private/fileUpload/%s/%s/',
53 $folderA,
54 $folderB,
55 );
56 if (!\is_dir($tmpPath)) {
57 \mkdir($tmpPath, recursive: true);
58 }
59
60 $filename = \sprintf(
61 '%s-%d.bin',
62 $row['identifier'],
63 $parameters['sequenceNo'],
64 );
65
66 // Write the chunk using a buffer to avoid blowing up the memory limit.
67 // See https://stackoverflow.com/a/61997147
68 $file = new AtomicWriter($tmpPath . $filename);
69 $bufferSize = 1 * 1024 * 1024;
70
71 $fh = \fopen('php://input', 'rb');
72 while (!\feof($fh)) {
73 $file->write(\fread($fh, $bufferSize));
74 }
75 \fclose($fh);
76
77 $file->flush();
78
79 // Check if we have all chunks.
80 $data = [];
81 for ($i = 0; $i < $chunks; $i++) {
82 $filename = \sprintf(
83 '%s-%d.bin',
84 $row['identifier'],
85 $i,
86 );
87
88 if (\file_exists($tmpPath . $filename)) {
89 $data[] = $tmpPath . $filename;
90 }
91 }
92
93 if (\count($data) === $chunks) {
94 // Concatenate the files by reading only a limited buffer at a time
95 // to avoid blowing up the memory limit.
96 // See https://stackoverflow.com/a/61997147
97 $bufferSize = 1 * 1024 * 1024;
98
99 $newFilename = \sprintf('%s-final.bin', $row['identifier']);
100 $file = new AtomicWriter($tmpPath . $newFilename);
101 foreach ($data as $fileChunk) {
102 $fh = \fopen($fileChunk, 'rb');
103 while (!\feof($fh)) {
104 $file->write(\fread($fh, $bufferSize));
105 }
106 \fclose($fh);
107 }
108
109 $file->flush();
110
111 \wcfDebug(
112 \memory_get_peak_usage(true),
113 \hash_file(
114 'sha256',
115 $tmpPath . $newFilename,
116 )
117 );
118 }
119
120 \wcfDebug(\memory_get_peak_usage(true));
121
122 // TODO: Dummy response to simulate a successful upload of a chunk.
123 return new EmptyResponse();
124 }
125 }