Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
85.71% covered (warning)
85.71%
6 / 7
CRAP
91.89% covered (success)
91.89%
34 / 37
FileChunkSaver
0.00% covered (danger)
0.00%
0 / 1
85.71% covered (warning)
85.71%
6 / 7
13.09
91.89% covered (success)
91.89%
34 / 37
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 setLogger
n/a
0 / 0
1
n/a
0 / 0
 getHandle
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 saveFileChunk
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 throwExceptionIfOnShortWrite
0.00% covered (danger)
0.00%
0 / 1
2.86
40.00% covered (danger)
40.00%
2 / 5
 throwExceptionIfMaxBytesExceeded
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 closeHandleLogAndThrowException
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 closeHandle
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
<?php
namespace FileImporter\Services\Http;
use FileImporter\Exceptions\ImportException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
 * This should not be used directly.
 * Please see HttpRequestExecutor::executeAndSave
 *
 * TODO this could end up in core? and used by UploadFromUrl?
 *
 * @license GPL-2.0-or-later
 * @author Addshore
 */
class FileChunkSaver implements LoggerAwareInterface {
    private const ERROR_CHUNK_OPEN = 'chunkNotOpened';
    private const ERROR_CHUNK_SAVE = 'chunkNotSaved';
    /**
     * @var string
     */
    private $filePath;
    /**
     * @var int
     */
    private $maxBytes;
    /**
     * @var null|resource|bool
     */
    private $handle = null;
    /**
     * @var int
     */
    private $fileSize = 0;
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @param string $filePath
     * @param int $maxBytes
     */
    public function __construct( $filePath, $maxBytes ) {
        $this->filePath = $filePath;
        $this->maxBytes = $maxBytes;
        $this->logger = new NullLogger();
    }
    /**
     * @param LoggerInterface $logger
     * @codeCoverageIgnore
     */
    public function setLogger( LoggerInterface $logger ) {
        $this->logger = $logger;
    }
    /**
     * Get the file resource. Open the file if it was not already open.
     * @return resource|bool
     */
    private function getHandle() {
        if ( $this->handle === null ) {
            try {
                $this->handle = fopen( $this->filePath, 'wb' );
            } catch ( \Exception $e ) {
                $this->logger->debug( 'Failed to get file handle: "' . $e->getMessage() . '"' );
            }
            if ( !$this->handle ) {
                $this->logger->debug( 'File creation failed "' . $this->filePath . '"' );
                throw new ImportException(
                    'Failed to open file "' . $this->filePath . '"', self::ERROR_CHUNK_OPEN );
            } else {
                $this->logger->debug( 'File created "' . $this->filePath . '"' );
            }
        }
        return $this->handle;
    }
    /**
     * Callback: save a chunk of the result of a HTTP request to the file.
     * Intended for use with Http::request
     *
     * @param int $curlResource Required by the cURL library, see CURLOPT_WRITEFUNCTION
     * @param string $buffer
     *
     * @return int Number of bytes handled
     * @throws ImportException
     */
    public function saveFileChunk( $curlResource, $buffer ) {
        $handle = $this->getHandle();
        $this->logger->debug( 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
        $nbytes = fwrite( $handle, $buffer );
        $this->throwExceptionIfOnShortWrite( $nbytes, $buffer );
        $this->fileSize += $nbytes;
        $this->throwExceptionIfMaxBytesExceeded();
        return $nbytes;
    }
    private function throwExceptionIfOnShortWrite( $nbytes, $buffer ) {
        if ( $nbytes != strlen( $buffer ) ) {
            $this->closeHandleLogAndThrowException(
                'Short write ' . $nbytes . '/' . strlen( $buffer ) .
                ' bytes, aborting with ' . $this->fileSize . ' uploaded so far'
            );
        }
    }
    private function throwExceptionIfMaxBytesExceeded() {
        if ( $this->fileSize > $this->maxBytes ) {
            $this->closeHandleLogAndThrowException(
                'File downloaded ' . $this->fileSize . ' bytes, ' .
                'exceeds maximum ' . $this->maxBytes . ' bytes.'
            );
        }
    }
    private function closeHandleLogAndThrowException( $message ) {
        $this->closeHandle();
        $this->logger->debug( $message );
        throw new ImportException( $message, self::ERROR_CHUNK_SAVE );
    }
    private function closeHandle() {
        fclose( $this->handle );
        $this->handle = false;
    }
}