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    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}