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