1: <?php declare(strict_types=1);
2:
3: namespace Salient\Http\Message;
4:
5: use Psr\Http\Message\StreamInterface as PsrStreamInterface;
6: use Psr\Http\Message\UploadedFileInterface as PsrUploadedFileInterface;
7: use Salient\Http\Exception\UploadFailedException;
8: use Salient\Http\HttpUtil;
9: use Salient\Utility\Exception\InvalidArgumentTypeException;
10: use Salient\Utility\File;
11:
12: /**
13: * @api
14: */
15: class ServerRequestUpload implements PsrUploadedFileInterface
16: {
17: /**
18: * @var array<int,string>
19: */
20: protected const ERROR_MESSAGE = [
21: \UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
22: \UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
23: \UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
24: \UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
25: \UPLOAD_ERR_NO_FILE => 'No file was uploaded',
26: \UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
27: \UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
28: \UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload',
29: ];
30:
31: private PsrStreamInterface $Stream;
32: private string $File;
33: private ?int $Size;
34: private int $Error;
35: private ?string $ClientFilename;
36: private ?string $ClientMediaType;
37: private bool $IsMoved = false;
38:
39: /**
40: * @api
41: *
42: * @param PsrStreamInterface|resource|string $resource Stream or filename.
43: */
44: public function __construct(
45: $resource,
46: ?int $size = null,
47: int $error = \UPLOAD_ERR_OK,
48: ?string $clientFilename = null,
49: ?string $clientMediaType = null
50: ) {
51: $this->Size = $size;
52: $this->Error = $error;
53: $this->ClientFilename = $clientFilename;
54: $this->ClientMediaType = $clientMediaType;
55:
56: if ($error === \UPLOAD_ERR_OK) {
57: if ($resource instanceof PsrStreamInterface) {
58: $this->Stream = $resource;
59: } elseif (File::isStream($resource)) {
60: $this->Stream = new Stream($resource);
61: } elseif (is_string($resource)) {
62: $this->File = $resource;
63: } else {
64: throw new InvalidArgumentTypeException(
65: 1,
66: 'resource',
67: PsrStreamInterface::class . '|resource|string',
68: $resource,
69: );
70: }
71: }
72: }
73:
74: /**
75: * @inheritDoc
76: */
77: public function getStream(): PsrStreamInterface
78: {
79: $this->assertIsValid();
80:
81: return $this->Stream ?? new Stream(File::open($this->File, 'r'));
82: }
83:
84: /**
85: * @inheritDoc
86: */
87: public function moveTo(string $targetPath): void
88: {
89: $this->assertIsValid();
90:
91: if (isset($this->File)) {
92: $result = \PHP_SAPI === 'cli'
93: ? @rename($this->File, $targetPath)
94: : @move_uploaded_file($this->File, $targetPath);
95: if ($result === false) {
96: $error = error_get_last();
97: throw new UploadFailedException($error['message'] ?? sprintf(
98: 'Error moving %s to %s',
99: $this->File,
100: $targetPath,
101: ));
102: }
103: } else {
104: $target = new Stream(File::open($targetPath, 'w'));
105: HttpUtil::copyStream($this->Stream, $target);
106: }
107:
108: $this->IsMoved = true;
109: }
110:
111: /**
112: * @inheritDoc
113: */
114: public function getSize(): ?int
115: {
116: return $this->Size;
117: }
118:
119: /**
120: * @inheritDoc
121: */
122: public function getError(): int
123: {
124: return $this->Error;
125: }
126:
127: /**
128: * @inheritDoc
129: */
130: public function getClientFilename(): ?string
131: {
132: return $this->ClientFilename;
133: }
134:
135: /**
136: * @inheritDoc
137: */
138: public function getClientMediaType(): ?string
139: {
140: return $this->ClientMediaType;
141: }
142:
143: private function assertIsValid(): void
144: {
145: if ($this->Error !== \UPLOAD_ERR_OK) {
146: throw new UploadFailedException(sprintf(
147: 'Upload failed (%d: %s)',
148: $this->Error,
149: static::ERROR_MESSAGE[$this->Error] ?? '',
150: ));
151: }
152:
153: if ($this->IsMoved) {
154: throw new UploadFailedException('Upload already moved');
155: }
156: }
157: }
158: