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

68publishers / file-storage / 22677287901

04 Mar 2026 03:53PM UTC coverage: 88.615% (-1.2%) from 89.86%
22677287901

Pull #10

github

web-flow
Merge b57e5aba9 into bf980b5f0
Pull Request #10: Updated HTTP headers when a resource is created using URL

0 of 8 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

576 of 650 relevant lines covered (88.62%)

0.89 hits per line

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

53.97
/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 implode;
30
use function is_file;
31
use function is_resource;
32
use function parse_url;
33
use function rewind;
34
use function sprintf;
35
use function str_starts_with;
36
use function stream_context_create;
37
use function stream_get_meta_data;
38
use function strlen;
39
use function strtolower;
40
use function trim;
41
use const FILEINFO_MIME_TYPE;
42

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

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

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

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

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

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

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

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

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

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

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

141
                fwrite($source, $chunk);
1✔
142
            }
143
            rewind($source);
1✔
144
        }
145

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

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

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

160
                rewind($source);
1✔
161

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

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

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

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

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

NEW
187
        $headers = [
×
188
            "Accept-language: en\r\n",
189
            "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n",
190
        ];
191

NEW
192
        $parsedUrl = parse_url($url) ?: [];
×
193

NEW
194
        if (isset($parsedUrl['scheme'], $parsedUrl['host'])) {
×
NEW
195
            $headers[] = sprintf(
×
NEW
196
                "Referer: %s://%s\r\n",
×
NEW
197
                $parsedUrl['scheme'],
×
NEW
198
                $parsedUrl['host'],
×
199
            );
200
        }
201

UNCOV
202
        $context = stream_context_create(
×
203
            options: [
204
                'http' => [
205
                    'method' => 'GET',
×
UNCOV
206
                    'protocol_version' => 1.1,
×
207
                    'follow_location' => true,
NEW
208
                    'header' => implode('', $headers),
×
209
                ],
210
            ],
211
        );
212

213
        $source = @fopen(
×
214
            filename: $url,
×
215
            mode: 'rb',
×
216
            context: $context,
217
        );
218

219
        if (false === $source) {
×
220
            throw new FilesystemException(
×
221
                message: sprintf(
×
222
                    'Can not read stream from url "%s". %s',
×
223
                    $url,
224
                    error_get_last()['message'] ?? '',
×
225
                ),
226
            );
227
        }
228

229
        $headers = stream_get_meta_data($source)['wrapper_data'] ?? [];
×
230

231
        return new StreamResource(
×
232
            pathInfo: $pathInfo,
233
            source: $source,
234
            mimeType: function () use ($headers): ?string {
×
235
                $contentTypeHeader = $this->getHeaderValue(
×
236
                    headers: $headers,
237
                    name: 'Content-Type',
×
238
                );
239

240
                if (null === $contentTypeHeader) {
×
241
                    return null;
×
242
                }
243

244
                $parts = explode(
×
245
                    separator: ';',
×
246
                    string: $contentTypeHeader,
247
                );
248

249
                return array_shift($parts);
×
250
            },
×
251
            filesize: function () use ($headers): ?int {
×
252
                $filesize = $this->getHeaderValue(
×
253
                    headers: $headers,
254
                    name: 'Content-Length',
×
255
                );
256

257
                return null !== $filesize ? (int) $filesize : null;
×
258
            },
×
259
        );
260
    }
261

262
    /**
263
     * @throws FilesystemException
264
     */
265
    private function getResourceFromLocalFile(PathInfoInterface $pathInfo, string $filename): ResourceInterface
266
    {
267
        error_clear_last();
1✔
268

269
        $source = @fopen(
1✔
270
            filename: $filename,
271
            mode: 'rb',
1✔
272
        );
273

274
        if (false === $source) {
1✔
275
            throw new FilesystemException(
1✔
276
                message: sprintf(
1✔
277
                    'Can not read stream from file "%s". %s',
1✔
278
                    $filename,
279
                    error_get_last()['message'] ?? '',
1✔
280
                ),
281
            );
282
        }
283

284
        return new StreamResource(
1✔
285
            pathInfo: $pathInfo,
286
            source: $source,
287
            mimeType: function (StreamResource $resource): ?string {
1✔
288
                $mimeType = @mime_content_type($resource->getSource());
1✔
289

290
                return false === $mimeType ? null : $mimeType;
1✔
291
            },
1✔
292
            filesize: function () use ($filename): ?int {
1✔
293
                $filesize = @filesize(
1✔
294
                    filename: $filename,
1✔
295
                );
296

297
                return false === $filesize ? null : $filesize;
1✔
298
            },
1✔
299
        );
300
    }
301

302
    /**
303
     * @param array<int, string> $headers
304
     */
305
    private function getHeaderValue(array $headers, string $name): ?string
306
    {
307
        $name = strtolower($name);
×
308

309
        foreach ($headers as $header) {
×
310
            $header = trim(strtolower($header));
×
311

312
            if (!str_starts_with($header, $name . ':')) {
×
313
                continue;
×
314
            }
315

316
            $value = substr(
×
317
                string: $header,
318
                offset: strlen($name) + 1,
×
319
            );
320

321
            return trim($value);
×
322
        }
323

324
        return null;
×
325
    }
326
}
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

© 2026 Coveralls, Inc