service/components/contents/StoredContent.php
/**
     * Sends content to browser
     *
     * Content is read from content stream and returned to client(browser) as chunked content.
     * Partial content HTTP requests are also supported.
     *
     * @return mixed
     */
Sends content to browser
Content is read from content stream and returned to client(browser) as chunked content.
Partial content HTTP requests are also supported.

@return mixed
Sends content to browser Content is read from content stream and returned to client ( browser ) as chunked content . Partial content HTTP requests are also supported . @return mixed
public function sendContent()
    {
        set_time_limit(0);
        ignore_user_abort(false);
        $handler = function ($owner) {
            if (connection_status() == CONNECTION_ABORTED) {
                $afterContentSentEvent = new AfterContentSentEvent();
                $storedContent = $owner->getModel();
                $afterContentSentEvent->bytes_sended = $owner->getBytesDownloaded();
                $afterContentSentEvent->content_link = $storedContent->ext_url;
                $afterContentSentEvent->content_name = $storedContent->name;
                $afterContentSentEvent->content_size = $storedContent->size;
                $afterContentSentEvent->user_id = (Yii::$app->user->identity != null) ? Yii::$app->user->identity->id : null;
                $owner->trigger(DownloadBehavior::EVENT_AFTER_DOWNLOAD, $afterContentSentEvent);
                Yii::$app->statistic->flush();
            }
        };
        register_shutdown_function($handler, $this);
        $this->bytesDownloaded = 0;
        $this->trigger(DownloadBehavior::EVENT_BEFORE_DOWNLOAD);
        $range = $this->getHttpRange($this->length);
        if ($range === false) {
            header('Content-Range: bytes */'.$this->length);
            throw new HttpException(416, 'Requested range not satisfiable');
        }
        list($start, $end) = $range;
        $isRangeReq = ($start !== null && $end !== null && ($start !== 0 || $end !== $this->length - 1));
        if ($isRangeReq) {
            http_response_code(206);
            header('Content-Range: bytes '.$start.'-'.$end.'/'.$this->length);
            header('Content-Length: '.($end - $start) + 1);
        } else {
            header('Content-Length: '.$this->length);
        }
        header('Content-Disposition: attachment; filename="'.rawurlencode($this->name).'"; filename*=UTF-8\'\''.rawurlencode($this->name));
        header('Content-Type: application/octet-stream');
        header('Accept-Ranges: bytes');
        if (!ob_get_level()) {
            ob_start();
        }
        $chunks = ($isRangeReq) ? $this->getChunks($start, $end) : $this->getChunks();
        $lastChunkEnd = 0;
        foreach ($chunks as $chunk) {
            $params = [];
            $chunkStart = $chunk->start;
            $chunkEnd = $chunk->end;
            try {
                $content = Yii::$app->storage->getFileContent($chunk->file);
            } catch (\Exception $e) {
                $content = null;
            }
            if ($content === null) {
                Yii::error('Failed to create content for stored content chunk.');
                if (!headers_sent()) {
                    header_remove();
                    throw new HttpException(500, 'Internal server error occured. Please try again later.');
                } else {
                    exit(1);
                }
            }
            if ($isRangeReq) {
                if ($start > $chunk->start && $start <= $chunk->end) {
                    $chunkStart = $start;
                }
                if ($end >= $chunk->start && $end < $chunk->end) {
                    $chunkEnd = $end;
                }
            }
            if ($lastChunkEnd >= $chunkEnd) {
                continue;
            }
            if ($lastChunkEnd > $chunkStart) {
                $chunkStart = $lastChunkEnd + 1;
            }
            if ($chunkStart > $chunk->start || $chunkEnd < $chunk->end) {
                $params['rangeStart'] = ($chunkStart > $chunk->start) ? $chunkStart - $chunk->start : 0;
                $params['rangeEnd'] = ($chunkEnd < $chunk->end) ? $chunk->length - 1 - ($chunk->end - $chunkEnd) : $chunk->length - 1;
            }
            $context = $content->createStreamContext();
            $stream = $content->createStream($context, 'r', $params);
            $contentChunkSentEvent = new ContentChunkSentEvent();
            $contentChunkSentEvent->stream = $stream;
            $contentChunkSentEvent->isPartial = $isRangeReq;
            try {
                while (($data = $stream->read())) {
                    $contentChunkSentEvent->content = $data;
                    $contentChunkSentEvent->bytes = strlen($data);
                    $this->trigger(DownloadBehavior::EVENT_BEFORE_CONTENT_CHUNK_SENT, $contentChunkSentEvent);
                    echo $data;
                    ob_flush();
                    flush();
                    $this->trigger(DownloadBehavior::EVENT_AFTER_CONTENT_CHUNK_SENT, $contentChunkSentEvent);
                    $this->bytesDownloaded += strlen($data);
                }
            } catch (\Exception $e) {
                @ob_end_clean();
                $stream->close();
                Yii::error($e);
                exit(1);
            }
            $stream->close();
            $lastChunkEnd = $chunk->end;
        }
        @ob_end_clean();
        $afterContentSentEvent = new AfterContentSentEvent();
        $storedContent = $this->getModel();
        $afterContentSentEvent->bytes_sended = $this->getBytesDownloaded();
        $afterContentSentEvent->content_link = $storedContent->ext_url;
        $afterContentSentEvent->content_name = $storedContent->name;
        $afterContentSentEvent->content_size = $storedContent->size;
        $afterContentSentEvent->user_id = (Yii::$app->user->identity != null) ? Yii::$app->user->identity->id : null;
        $this->trigger(DownloadBehavior::EVENT_AFTER_DOWNLOAD, $afterContentSentEvent);
    }
public function sendContent ( ) { set_time_limit ( 0 ) ; ignore_user_abort ( false ) ; $ handler = function ( $ owner ) { if ( connection_status ( ) == CONNECTION_ABORTED ) { $ afterContentSentEvent = new AfterContentSentEvent ( ) ; $ storedContent = $ owner -> getModel ( ) ; $ afterContentSentEvent -> bytes_sended = $ owner -> getBytesDownloaded ( ) ; $ afterContentSentEvent -> content_link = $ storedContent -> ext_url ; $ afterContentSentEvent -> content_name = $ storedContent -> name ; $ afterContentSentEvent -> content_size = $ storedContent -> size ; $ afterContentSentEvent -> user_id = ( Yii :: $ app -> user -> identity != null ) ? Yii :: $ app -> user -> identity -> id : null ; $ owner -> trigger ( DownloadBehavior :: EVENT_AFTER_DOWNLOAD , $ afterContentSentEvent ) ; Yii :: $ app -> statistic -> flush ( ) ; } } ; register_shutdown_function ( $ handler , $ this ) ; $ this -> bytesDownloaded = 0 ; $ this -> trigger ( DownloadBehavior :: EVENT_BEFORE_DOWNLOAD ) ; $ range = $ this -> getHttpRange ( $ this -> length ) ; if ( $ range === false ) { header ( 'Content-Range: bytes */' . $ this -> length ) ; throw new HttpException ( 416 , 'Requested range not satisfiable' ) ; } list ( $ start , $ end ) = $ range ; $ isRangeReq = ( $ start !== null && $ end !== null && ( $ start !== 0 || $ end !== $ this -> length - 1 ) ) ; if ( $ isRangeReq ) { http_response_code ( 206 ) ; header ( 'Content-Range: bytes ' . $ start . '-' . $ end . '/' . $ this -> length ) ; header ( 'Content-Length: ' . ( $ end - $ start ) + 1 ) ; } else { header ( 'Content-Length: ' . $ this -> length ) ; } header ( 'Content-Disposition: attachment; filename="' . rawurlencode ( $ this -> name ) . '"; filename*=UTF-8\'\'' . rawurlencode ( $ this -> name ) ) ; header ( 'Content-Type: application/octet-stream' ) ; header ( 'Accept-Ranges: bytes' ) ; if ( ! ob_get_level ( ) ) { ob_start ( ) ; } $ chunks = ( $ isRangeReq ) ? $ this -> getChunks ( $ start , $ end ) : $ this -> getChunks ( ) ; $ lastChunkEnd = 0 ; foreach ( $ chunks as $ chunk ) { $ params = [ ] ; $ chunkStart = $ chunk -> start ; $ chunkEnd = $ chunk -> end ; try { $ content = Yii :: $ app -> storage -> getFileContent ( $ chunk -> file ) ; } catch ( \ Exception $ e ) { $ content = null ; } if ( $ content === null ) { Yii :: error ( 'Failed to create content for stored content chunk.' ) ; if ( ! headers_sent ( ) ) { header_remove ( ) ; throw new HttpException ( 500 , 'Internal server error occured. Please try again later.' ) ; } else { exit ( 1 ) ; } } if ( $ isRangeReq ) { if ( $ start > $ chunk -> start && $ start <= $ chunk -> end ) { $ chunkStart = $ start ; } if ( $ end >= $ chunk -> start && $ end < $ chunk -> end ) { $ chunkEnd = $ end ; } } if ( $ lastChunkEnd >= $ chunkEnd ) { continue ; } if ( $ lastChunkEnd > $ chunkStart ) { $ chunkStart = $ lastChunkEnd + 1 ; } if ( $ chunkStart > $ chunk -> start || $ chunkEnd < $ chunk -> end ) { $ params [ 'rangeStart' ] = ( $ chunkStart > $ chunk -> start ) ? $ chunkStart - $ chunk -> start : 0 ; $ params [ 'rangeEnd' ] = ( $ chunkEnd < $ chunk -> end ) ? $ chunk -> length - 1 - ( $ chunk -> end - $ chunkEnd ) : $ chunk -> length - 1 ; } $ context = $ content -> createStreamContext ( ) ; $ stream = $ content -> createStream ( $ context , 'r' , $ params ) ; $ contentChunkSentEvent = new ContentChunkSentEvent ( ) ; $ contentChunkSentEvent -> stream = $ stream ; $ contentChunkSentEvent -> isPartial = $ isRangeReq ; try { while ( ( $ data = $ stream -> read ( ) ) ) { $ contentChunkSentEvent -> content = $ data ; $ contentChunkSentEvent -> bytes = strlen ( $ data ) ; $ this -> trigger ( DownloadBehavior :: EVENT_BEFORE_CONTENT_CHUNK_SENT , $ contentChunkSentEvent ) ; echo $ data ; ob_flush ( ) ; flush ( ) ; $ this -> trigger ( DownloadBehavior :: EVENT_AFTER_CONTENT_CHUNK_SENT , $ contentChunkSentEvent ) ; $ this -> bytesDownloaded += strlen ( $ data ) ; } } catch ( \ Exception $ e ) { @ ob_end_clean ( ) ; $ stream -> close ( ) ; Yii :: error ( $ e ) ; exit ( 1 ) ; } $ stream -> close ( ) ; $ lastChunkEnd = $ chunk -> end ; } @ ob_end_clean ( ) ; $ afterContentSentEvent = new AfterContentSentEvent ( ) ; $ storedContent = $ this -> getModel ( ) ; $ afterContentSentEvent -> bytes_sended = $ this -> getBytesDownloaded ( ) ; $ afterContentSentEvent -> content_link = $ storedContent -> ext_url ; $ afterContentSentEvent -> content_name = $ storedContent -> name ; $ afterContentSentEvent -> content_size = $ storedContent -> size ; $ afterContentSentEvent -> user_id = ( Yii :: $ app -> user -> identity != null ) ? Yii :: $ app -> user -> identity -> id : null ; $ this -> trigger ( DownloadBehavior :: EVENT_AFTER_DOWNLOAD , $ afterContentSentEvent ) ; }
Sends content to browser
 Content is read from content stream and returned to client(browser) as chunked content.
Sends content to browser Content is read from content stream and returned to client ( browser ) as chunked content .
//This should be executed only when connection is aborted. //We must flush statistics manualy. //Register shutdown function to handle connection abort //Trigger before download event //Get stored content chunks //Last sended chunk's end //If request is range request and chunk contains start\end set start\end position of chunk //Since chunks can overlap need to check if chunk content was not already sended //If part of current chunk was already sent - update chunk's start position //Trigger before content chunk sent event //Trigger after content chunk sent event //Trigger after download event