Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.57% |
31 / 35 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
FileChunkSaver | |
88.57% |
31 / 35 |
|
85.71% |
6 / 7 |
13.25 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setLogger | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getHandle | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
saveFileChunk | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
throwExceptionIfOnShortWrite | |
20.00% |
1 / 5 |
|
0.00% |
0 / 1 |
4.05 | |||
throwExceptionIfMaxBytesExceeded | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
closeHandleLogAndThrowException | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
closeHandle | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace FileImporter\Services\Http; |
4 | |
5 | use FileImporter\Exceptions\ImportException; |
6 | use Psr\Log\LoggerAwareInterface; |
7 | use Psr\Log\LoggerInterface; |
8 | use Psr\Log\NullLogger; |
9 | |
10 | /** |
11 | * This should not be used directly. |
12 | * Please see HttpRequestExecutor::executeAndSave |
13 | * |
14 | * TODO this could end up in core? and used by UploadFromUrl? |
15 | * |
16 | * @license GPL-2.0-or-later |
17 | * @author Addshore |
18 | */ |
19 | class FileChunkSaver implements LoggerAwareInterface { |
20 | |
21 | private const ERROR_CHUNK_OPEN = 'chunkNotOpened'; |
22 | private const ERROR_CHUNK_SAVE = 'chunkNotSaved'; |
23 | |
24 | private string $filePath; |
25 | private int $maxBytes; |
26 | /** @var null|resource|bool */ |
27 | private $handle = null; |
28 | private int $fileSize = 0; |
29 | private LoggerInterface $logger; |
30 | |
31 | public function __construct( string $filePath, int $maxBytes ) { |
32 | $this->filePath = $filePath; |
33 | $this->maxBytes = $maxBytes; |
34 | $this->logger = new NullLogger(); |
35 | } |
36 | |
37 | /** |
38 | * @codeCoverageIgnore |
39 | */ |
40 | public function setLogger( LoggerInterface $logger ): void { |
41 | $this->logger = $logger; |
42 | } |
43 | |
44 | /** |
45 | * Get the file resource. Open the file if it was not already open. |
46 | * @return resource|bool |
47 | */ |
48 | private function getHandle() { |
49 | if ( $this->handle === null ) { |
50 | try { |
51 | $this->handle = fopen( $this->filePath, 'wb' ); |
52 | } catch ( \Throwable $e ) { |
53 | $this->logger->debug( 'Failed to get file handle: "' . $e->getMessage() . '"' ); |
54 | } |
55 | |
56 | if ( !$this->handle ) { |
57 | $this->logger->debug( 'File creation failed "' . $this->filePath . '"' ); |
58 | throw new ImportException( |
59 | 'Failed to open file "' . $this->filePath . '"', self::ERROR_CHUNK_OPEN ); |
60 | } else { |
61 | $this->logger->debug( 'File created "' . $this->filePath . '"' ); |
62 | } |
63 | } |
64 | |
65 | return $this->handle; |
66 | } |
67 | |
68 | /** |
69 | * Callback: save a chunk of the result of an HTTP request to the file. |
70 | * Intended for use with HttpRequestFactory::request |
71 | * |
72 | * @param mixed $curlResource Required by the cURL library, see CURLOPT_WRITEFUNCTION |
73 | * @param string $buffer |
74 | * |
75 | * @return int Number of bytes handled |
76 | * @throws ImportException |
77 | */ |
78 | public function saveFileChunk( $curlResource, string $buffer ): int { |
79 | $handle = $this->getHandle(); |
80 | $this->logger->debug( 'Received chunk of ' . strlen( $buffer ) . ' bytes' ); |
81 | $nbytes = fwrite( $handle, $buffer ); |
82 | |
83 | $this->throwExceptionIfOnShortWrite( $nbytes, $buffer ); |
84 | $this->fileSize += $nbytes; |
85 | $this->throwExceptionIfMaxBytesExceeded(); |
86 | |
87 | return $nbytes; |
88 | } |
89 | |
90 | private function throwExceptionIfOnShortWrite( int $nbytes, string $buffer ): void { |
91 | if ( $nbytes != strlen( $buffer ) ) { |
92 | $this->closeHandleLogAndThrowException( |
93 | 'Short write ' . $nbytes . '/' . strlen( $buffer ) . |
94 | ' bytes, aborting with ' . $this->fileSize . ' uploaded so far' |
95 | ); |
96 | } |
97 | } |
98 | |
99 | private function throwExceptionIfMaxBytesExceeded(): void { |
100 | if ( $this->fileSize > $this->maxBytes ) { |
101 | $this->closeHandleLogAndThrowException( |
102 | 'File downloaded ' . $this->fileSize . ' bytes, ' . |
103 | 'exceeds maximum ' . $this->maxBytes . ' bytes.' |
104 | ); |
105 | } |
106 | } |
107 | |
108 | /** |
109 | * @return never |
110 | */ |
111 | private function closeHandleLogAndThrowException( string $message ) { |
112 | $this->closeHandle(); |
113 | $this->logger->debug( $message ); |
114 | throw new ImportException( $message, self::ERROR_CHUNK_SAVE ); |
115 | } |
116 | |
117 | private function closeHandle(): void { |
118 | fclose( $this->handle ); |
119 | $this->handle = false; |
120 | } |
121 | |
122 | } |