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