• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

68publishers / file-storage / 15008415560

13 May 2025 11:04PM UTC coverage: 92.308% (-0.2%) from 92.542%
15008415560

push

github

tg666
Added stream context option `follow_location: true` when resource is created from url

648 of 702 relevant lines covered (92.31%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.42
/src/Resource/ResourceFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace SixtyEightPublishers\FileStorage\Resource;
6

7
use finfo;
8
use League\Flysystem\FilesystemException as LeagueFilesystemException;
9
use League\Flysystem\FilesystemReader;
10
use League\Flysystem\UnableToRetrieveMetadata;
11
use Psr\Http\Message\StreamInterface;
12
use RuntimeException;
13
use SixtyEightPublishers\FileStorage\Exception\FileNotFoundException;
14
use SixtyEightPublishers\FileStorage\Exception\FilesystemException;
15
use SixtyEightPublishers\FileStorage\PathInfoInterface;
16
use function array_key_exists;
17
use function array_shift;
18
use function error_clear_last;
19
use function error_get_last;
20
use function explode;
21
use function file_exists;
22
use function filter_var;
23
use function fopen;
24
use function fread;
25
use function fstat;
26
use function ftell;
27
use function fwrite;
28
use function get_debug_type;
29
use function is_file;
30
use function is_resource;
31
use function rewind;
32
use function sprintf;
33
use function str_starts_with;
34
use function stream_context_create;
35
use function stream_get_meta_data;
36
use function strlen;
37
use function strtolower;
38
use function trim;
39
use const FILEINFO_MIME_TYPE;
40

41
final class ResourceFactory implements ResourceFactoryInterface
42
{
43
    public function __construct(
1✔
44
        private readonly FilesystemReader $filesystemReader,
45
    ) {}
1✔
46

47
    /**
48
     * @throws FileNotFoundException
49
     * @throws LeagueFilesystemException
50
     * @throws FilesystemException
51
     */
52
    public function createResource(PathInfoInterface $pathInfo): ResourceInterface
1✔
53
    {
54
        $path = $pathInfo->getPath();
1✔
55

56
        if (false === $this->filesystemReader->fileExists($path)) {
1✔
57
            throw new FileNotFoundException($path);
1✔
58
        }
59

60
        try {
61
            $source = $this->filesystemReader->readStream($path);
1✔
62
        } catch (LeagueFilesystemException $e) {
1✔
63
            throw new FilesystemException(
1✔
64
                message: sprintf(
1✔
65
                    'Can not read stream from file "%s".',
1✔
66
                    $path,
1✔
67
                ),
68
                previous: $e,
69
            );
70
        }
71

72
        return new StreamResource(
1✔
73
            pathInfo: $pathInfo,
1✔
74
            source: $source,
75
            mimeType: function () use ($path): ?string {
1✔
76
                try {
77
                    return $this->filesystemReader->mimeType($path);
1✔
78
                } catch (LeagueFilesystemException|UnableToRetrieveMetadata $e) {
79
                    return null;
80
                }
81
            },
1✔
82
            filesize: function () use ($path): ?int {
1✔
83
                try {
84
                    return $this->filesystemReader->fileSize($path);
1✔
85
                } catch (LeagueFilesystemException|UnableToRetrieveMetadata $e) {
86
                    return null;
87
                }
88
            },
1✔
89
        );
90
    }
91

92
    public function createResourceFromFile(PathInfoInterface $pathInfo, string $filename): ResourceInterface
1✔
93
    {
94
        return match (true) {
95
            (bool) filter_var($filename, FILTER_VALIDATE_URL) => $this->getResourceFromUrl(
1✔
96
                pathInfo: $pathInfo,
×
97
                url: $filename,
98
            ),
99
            file_exists($filename) && is_file($filename) => $this->getResourceFromLocalFile(
1✔
100
                pathInfo: $pathInfo,
1✔
101
                filename: $filename,
102
            ),
103
            default => throw new FileNotFoundException($filename),
1✔
104
        };
105
    }
106

107
    public function createResourceFromPsrStream(PathInfoInterface $pathInfo, StreamInterface $stream): ResourceInterface
1✔
108
    {
109
        $size = $stream->getSize();
1✔
110
        $streamCopy = clone $stream;
1✔
111
        $source = $streamCopy->detach();
1✔
112

113
        # For non resource based streams:
114
        if (!is_resource($source)) {
1✔
115
            $source = @fopen('php://temp', 'rb+');
1✔
116

117
            if (false === $source) {
1✔
118
                throw new FilesystemException(
×
119
                    message: sprintf(
×
120
                        'Unable to create temporary stream for PSR-7 stream of type %s.',
×
121
                        get_debug_type($stream),
×
122
                    ),
123
                );
124
            }
125

126
            try {
127
                $stream->rewind();
1✔
128
            } catch (RuntimeException $e) {
1✔
129
                # ignore
130
            }
131

132
            # clone data to the resource
133
            while (!$stream->eof()) {
1✔
134
                $chunk = $stream->read(8192);
1✔
135
                if ('' === $chunk) {
1✔
136
                    break;
×
137
                }
138

139
                fwrite($source, $chunk);
1✔
140
            }
141
            rewind($source);
1✔
142
        }
143

144
        return new StreamResource(
1✔
145
            pathInfo: $pathInfo,
1✔
146
            source: $source,
147
            mimeType: function (StreamResource $resource): ?string {
1✔
148
                $source = $resource->getSource();
1✔
149

150
                if (ftell($source) !== 0 && stream_get_meta_data($source)['seekable']) {
1✔
151
                    rewind($source);
1✔
152
                }
153

154
                $finfo = new finfo(FILEINFO_MIME_TYPE);
1✔
155
                $mimeType = null;
1✔
156
                $chunk = fread($source, 1024) ?: '';
1✔
157

158
                rewind($source);
1✔
159

160
                if ('' !== $chunk) {
1✔
161
                    $mimeType = $finfo->buffer($chunk) ?: null;
1✔
162
                }
163

164
                if (null === $mimeType) {
1✔
165
                    $mimeType = @mime_content_type($resource->getSource()) ?: null;
166
                }
167

168
                return $mimeType;
1✔
169
            },
1✔
170
            filesize: $size ?? function (StreamResource $resource): ?int {
1✔
171
                $stat = fstat($resource->getSource());
172

173
                return false !== $stat && array_key_exists('size', $stat) ? (int) $stat['size'] : null;
174
            },
1✔
175
        );
176
    }
177

178
    /**
179
     * @throws FilesystemException
180
     */
181
    private function getResourceFromUrl(PathInfoInterface $pathInfo, string $url): ResourceInterface
182
    {
183
        error_clear_last();
×
184

185
        $context = stream_context_create(
×
186
            options: [
187
                'http' => [
188
                    'method' => 'GET',
×
189
                    'protocol_version' => 1.1,
190
                    'follow_location' => true,
191
                    'header' => "Accept-language: en\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36\r\n",
192
                ],
193
            ],
194
        );
195

196
        $source = @fopen(
×
197
            filename: $url,
×
198
            mode: 'rb',
×
199
            context: $context,
×
200
        );
201

202
        if (false === $source) {
×
203
            throw new FilesystemException(
×
204
                message: sprintf(
×
205
                    'Can not read stream from url "%s". %s',
×
206
                    $url,
×
207
                    error_get_last()['message'] ?? '',
×
208
                ),
209
            );
210
        }
211

212
        $headers = stream_get_meta_data($source)['wrapper_data'] ?? [];
×
213

214
        return new StreamResource(
×
215
            pathInfo: $pathInfo,
×
216
            source: $source,
217
            mimeType: function () use ($headers): ?string {
×
218
                $contentTypeHeader = $this->getHeaderValue(
219
                    headers: $headers,
220
                    name: 'Content-Type',
221
                );
222

223
                if (null === $contentTypeHeader) {
224
                    return null;
225
                }
226

227
                $parts = explode(
228
                    separator: ';',
229
                    string: $contentTypeHeader,
230
                );
231

232
                return array_shift($parts);
233
            },
×
234
            filesize: function () use ($headers): ?int {
×
235
                $filesize = $this->getHeaderValue(
236
                    headers: $headers,
237
                    name: 'Content-Length',
238
                );
239

240
                return null !== $filesize ? (int) $filesize : null;
241
            },
×
242
        );
243
    }
244

245
    /**
246
     * @throws FilesystemException
247
     */
248
    private function getResourceFromLocalFile(PathInfoInterface $pathInfo, string $filename): ResourceInterface
1✔
249
    {
250
        error_clear_last();
1✔
251

252
        $source = @fopen(
1✔
253
            filename: $filename,
1✔
254
            mode: 'rb',
1✔
255
        );
256

257
        if (false === $source) {
1✔
258
            throw new FilesystemException(
1✔
259
                message: sprintf(
1✔
260
                    'Can not read stream from file "%s". %s',
1✔
261
                    $filename,
1✔
262
                    error_get_last()['message'] ?? '',
1✔
263
                ),
264
            );
265
        }
266

267
        return new StreamResource(
1✔
268
            pathInfo: $pathInfo,
1✔
269
            source: $source,
270
            mimeType: function (StreamResource $resource): ?string {
1✔
271
                $mimeType = @mime_content_type($resource->getSource());
1✔
272

273
                return false === $mimeType ? null : $mimeType;
1✔
274
            },
1✔
275
            filesize: function () use ($filename): ?int {
1✔
276
                $filesize = @filesize(
1✔
277
                    filename: $filename,
1✔
278
                );
279

280
                return false === $filesize ? null : $filesize;
1✔
281
            },
1✔
282
        );
283
    }
284

285
    /**
286
     * @param array<int, string> $headers
287
     */
288
    private function getHeaderValue(array $headers, string $name): ?string
289
    {
290
        $name = strtolower($name);
×
291

292
        foreach ($headers as $header) {
×
293
            $header = trim(strtolower($header));
×
294

295
            if (!str_starts_with($header, $name . ':')) {
×
296
                continue;
×
297
            }
298

299
            $value = substr(
×
300
                string: $header,
×
301
                offset: strlen($name) + 1,
×
302
            );
303

304
            return trim($value);
×
305
        }
306

307
        return null;
×
308
    }
309
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc