[php] PHP를 사용하여 파일을 제공하는 가장 빠른 방법

나는 파일 경로를 수신하고, 그것이 무엇인지 식별하고, 적절한 헤더를 설정하고, Apache처럼 제공하는 함수를 모 으려고합니다.

이 작업을 수행하는 이유는 파일을 제공하기 전에 요청에 대한 일부 정보를 처리하기 위해 PHP를 사용해야하기 때문입니다.

속도가 중요합니다

virtual ()은 옵션이 아닙니다.

사용자가 웹 서버 (Apache / nginx 등)를 제어 할 수없는 공유 호스팅 환경에서 작동해야합니다.

지금까지 내가 얻은 정보는 다음과 같습니다.

File::output($path);

<?php
class File {
static function output($path) {
    // Check if the file exists
    if(!File::exists($path)) {
        header('HTTP/1.0 404 Not Found');
        exit();
    }

    // Set the content-type header
    header('Content-Type: '.File::mimeType($path));

    // Handle caching
    $fileModificationTime = gmdate('D, d M Y H:i:s', File::modificationTime($path)).' GMT';
    $headers = getallheaders();
    if(isset($headers['If-Modified-Since']) && $headers['If-Modified-Since'] == $fileModificationTime) {
        header('HTTP/1.1 304 Not Modified');
        exit();
    }
    header('Last-Modified: '.$fileModificationTime);

    // Read the file
    readfile($path);

    exit();
}

static function mimeType($path) {
    preg_match("|\.([a-z0-9]{2,4})$|i", $path, $fileSuffix);

    switch(strtolower($fileSuffix[1])) {
        case 'js' :
            return 'application/x-javascript';
        case 'json' :
            return 'application/json';
        case 'jpg' :
        case 'jpeg' :
        case 'jpe' :
            return 'image/jpg';
        case 'png' :
        case 'gif' :
        case 'bmp' :
        case 'tiff' :
            return 'image/'.strtolower($fileSuffix[1]);
        case 'css' :
            return 'text/css';
        case 'xml' :
            return 'application/xml';
        case 'doc' :
        case 'docx' :
            return 'application/msword';
        case 'xls' :
        case 'xlt' :
        case 'xlm' :
        case 'xld' :
        case 'xla' :
        case 'xlc' :
        case 'xlw' :
        case 'xll' :
            return 'application/vnd.ms-excel';
        case 'ppt' :
        case 'pps' :
            return 'application/vnd.ms-powerpoint';
        case 'rtf' :
            return 'application/rtf';
        case 'pdf' :
            return 'application/pdf';
        case 'html' :
        case 'htm' :
        case 'php' :
            return 'text/html';
        case 'txt' :
            return 'text/plain';
        case 'mpeg' :
        case 'mpg' :
        case 'mpe' :
            return 'video/mpeg';
        case 'mp3' :
            return 'audio/mpeg3';
        case 'wav' :
            return 'audio/wav';
        case 'aiff' :
        case 'aif' :
            return 'audio/aiff';
        case 'avi' :
            return 'video/msvideo';
        case 'wmv' :
            return 'video/x-ms-wmv';
        case 'mov' :
            return 'video/quicktime';
        case 'zip' :
            return 'application/zip';
        case 'tar' :
            return 'application/x-tar';
        case 'swf' :
            return 'application/x-shockwave-flash';
        default :
            if(function_exists('mime_content_type')) {
                $fileSuffix = mime_content_type($path);
            }
            return 'unknown/' . trim($fileSuffix[0], '.');
    }
}
}
?>



답변

내 이전 답변은 부분적이고 잘 문서화되지 않았습니다. 여기에 토론의 다른 솔루션과 다른 솔루션의 요약이 포함 된 업데이트가 있습니다.

솔루션은 최상의 솔루션에서 최악으로 정렬되지만 웹 서버를 가장 많이 제어해야하는 솔루션부터 덜 필요한 솔루션까지 순서가 지정됩니다. 빠르고 모든 곳에서 작동하는 하나의 솔루션을 갖는 쉬운 방법은없는 것 같습니다.


X-SendFile 헤더 사용

다른 사람들이 문서화 한 것처럼 실제로 가장 좋은 방법입니다. 기본은 PHP에서 액세스 제어를 수행 한 다음 파일을 직접 보내는 대신 웹 서버에 지시하는 것입니다.

기본 PHP 코드는 다음과 같습니다.

header("X-Sendfile: $file_name");
header("Content-type: application/octet-stream");
header('Content-Disposition: attachment; filename="' . basename($file_name) . '"');

$file_name파일 시스템의 전체 경로는 어디에 있습니까 ?

이 솔루션의 주요 문제점은 웹 서버에서 허용해야하며 기본적으로 설치되지 않았거나 (apache) 기본적으로 활성화되지 않았거나 (lighttpd) 특정 구성 (nginx)이 필요하다는 것입니다.

Apache

apache에서 mod_php를 사용하는 경우 mod_xsendfile 이라는 모듈을 설치 한 다음 구성해야합니다 (허용하는 경우 apache config 또는 .htaccess에서).

XSendFile on
XSendFilePath /home/www/example.com/htdocs/files/

이 모듈을 사용하면 파일 경로는 지정된 XSendFilePath.

Lighttpd

mod_fastcgi는 다음과 같이 구성 될 때이를 지원합니다.

"allow-x-send-file" => "enable" 

기능에 대한 문서는 lighttpd wiki에 있으며 X-LIGHTTPD-send-file헤더 를 문서화 하지만 X-Sendfile이름도 작동합니다.

Nginx

Nginx에서는 X-Sendfile헤더를 사용할 수 없습니다 X-Accel-Redirect. 라는 자체 헤더를 사용해야합니다 . 기본적으로 활성화되어 있으며 유일한 차이점은 인수가 파일 시스템이 아닌 URI 여야한다는 것입니다. 결과적으로 클라이언트가 실제 파일 URL을 찾아 직접 이동하지 않도록 구성에서 내부로 표시된 위치를 정의해야합니다. 위키에는 이에 대한 좋은 설명 이 포함되어 있습니다 .

심볼릭 링크 및 위치 헤더

심볼릭 링크를 사용 하여 리디렉션 할 수 있습니다 . 사용자가 파일에 액세스 할 수있는 권한이 부여 된 경우 임의의 이름으로 파일에 심볼릭 링크를 만들고 다음을 사용하여 사용자를 리디렉션 할 수 있습니다.

header("Location: " . $url_of_symlink);

분명히 스크립트를 생성하는 스크립트가 호출 될 때 또는 cron을 통해 (액세스 권한이있는 경우 머신에서, 그렇지 않은 경우 일부 webcron 서비스를 통해) 정리할 방법이 필요합니다.

아파치 FollowSymLinks에서 .htaccess또는 아파치 구성에서 활성화 할 수 있어야합니다 .

IP 및 Location 헤더에 의한 액세스 제어

또 다른 해킹은 명시적인 사용자 IP를 허용하는 PHP에서 아파치 액세스 파일을 생성하는 것입니다. 아파치에서는 mod_authz_host( mod_access) Allow from명령을 사용하는 것을 의미 합니다.

문제는 파일에 대한 액세스 잠금 (여러 사용자가 동시에이 작업을 수행하기를 원할 수 있으므로)이 사소하지 않고 일부 사용자가 오랜 시간을 기다리게 할 수 있다는 것입니다. 어쨌든 파일을 정리해야합니다.

분명히 또 다른 문제는 동일한 IP 뒤에있는 여러 사람이 잠재적으로 파일에 액세스 할 수 있다는 것입니다.

다른 모든 것이 실패 할 때

웹 서버에서 도움을받을 수있는 방법이 없다면 현재 사용중인 모든 PHP 버전에서 사용할 수있는 readfile 만 있으면됩니다 (하지만 실제로는 효율적이지 않습니다).


솔루션 결합

모든 곳에서 PHP 코드를 사용할 수 있도록하려면 파일을 정말 빠르게 보내는 가장 좋은 방법은 웹 서버에 따라 활성화하는 방법에 대한 지침과 설치시 자동 감지 기능이있는 구성 가능한 옵션을 어딘가에 두는 것입니다. 스크립트.

많은 소프트웨어에서 수행되는 작업과 매우 유사합니다.

  • 깨끗한 URL ( mod_rewrite아파치)
  • 암호화 기능 ( mcryptphp 모듈)
  • 멀티 바이트 문자열 지원 ( mbstringPHP 모듈)


답변

가장 빠른 방법 :하지 마십시오. nginx대한 x-sendfile 헤더를 살펴보십시오 . 다른 웹 서버에도 비슷한 것이 있습니다. 이것은 여전히 ​​php에서 액세스 제어 등을 할 수 있지만 파일을 실제로 전송하도록 설계된 웹 서버에 위임 할 수 있음을 의미합니다.

추신 : PHP로 파일을 읽고 보내는 것과 비교하여 nginx와 함께 이것을 사용하는 것이 얼마나 효율적인지 생각하면 오싹합니다. 100 명의 사람들이 파일을 다운로드하고 있다고 생각해보십시오. php + apache를 사용하면 관대하면 아마도 100 * 15mb = 1.5GB (대략, 저를 쏘세요), 램이 바로 거기에있을 것입니다. Nginx는 파일을 커널로 전송 한 다음 디스크에서 네트워크 버퍼로 직접로드됩니다. 빠른!

PPS : 그리고이 방법을 사용하면 원하는 모든 액세스 제어, 데이터베이스 작업을 수행 할 수 있습니다.


답변

여기 순수한 PHP 솔루션이 있습니다. 내 개인 프레임 워크에서 다음 기능 조정했습니다 .

function Download($path, $speed = null, $multipart = true)
{
    while (ob_get_level() > 0)
    {
        ob_end_clean();
    }

    if (is_file($path = realpath($path)) === true)
    {
        $file = @fopen($path, 'rb');
        $size = sprintf('%u', filesize($path));
        $speed = (empty($speed) === true) ? 1024 : floatval($speed);

        if (is_resource($file) === true)
        {
            set_time_limit(0);

            if (strlen(session_id()) > 0)
            {
                session_write_close();
            }

            if ($multipart === true)
            {
                $range = array(0, $size - 1);

                if (array_key_exists('HTTP_RANGE', $_SERVER) === true)
                {
                    $range = array_map('intval', explode('-', preg_replace('~.*=([^,]*).*~', '$1', $_SERVER['HTTP_RANGE'])));

                    if (empty($range[1]) === true)
                    {
                        $range[1] = $size - 1;
                    }

                    foreach ($range as $key => $value)
                    {
                        $range[$key] = max(0, min($value, $size - 1));
                    }

                    if (($range[0] > 0) || ($range[1] < ($size - 1)))
                    {
                        header(sprintf('%s %03u %s', 'HTTP/1.1', 206, 'Partial Content'), true, 206);
                    }
                }

                header('Accept-Ranges: bytes');
                header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
            }

            else
            {
                $range = array(0, $size - 1);
            }

            header('Pragma: public');
            header('Cache-Control: public, no-cache');
            header('Content-Type: application/octet-stream');
            header('Content-Length: ' . sprintf('%u', $range[1] - $range[0] + 1));
            header('Content-Disposition: attachment; filename="' . basename($path) . '"');
            header('Content-Transfer-Encoding: binary');

            if ($range[0] > 0)
            {
                fseek($file, $range[0]);
            }

            while ((feof($file) !== true) && (connection_status() === CONNECTION_NORMAL))
            {
                echo fread($file, round($speed * 1024)); flush(); sleep(1);
            }

            fclose($file);
        }

        exit();
    }

    else
    {
        header(sprintf('%s %03u %s', 'HTTP/1.1', 404, 'Not Found'), true, 404);
    }

    return false;
}

코드는 가능한 한 효율적이며 다른 PHP 스크립트가 동일한 사용자 / 세션에 대해 동시에 실행될 수 있도록 세션 핸들러를 닫습니다. 또한 사용자가 다운로드를 일시 중지 / 재개 할 수 있고 다운로드 가속기를 사용하여 더 빠른 다운로드 속도의 이점을 누릴 수 있도록 범위 (아파치가 기본적으로 수행하는 작업이기도 함)에서 다운로드 제공을 지원합니다. 또한 $speed인수 를 통해 다운로드 (부분)를 제공해야하는 최대 속도 (Kbps)를 지정할 수도 있습니다 .


답변

header('Location: ' . $path);
exit(0);

Apache가 작업을 수행하도록하십시오.


답변

더 나은 구현, 캐시 지원, 사용자 정의 된 http 헤더.

serveStaticFile($fn, array(
        'headers'=>array(
            'Content-Type' => 'image/x-icon',
            'Cache-Control' =>  'public, max-age=604800',
            'Expires' => gmdate("D, d M Y H:i:s", time() + 30 * 86400) . " GMT",
        )
    ));

function serveStaticFile($path, $options = array()) {
    $path = realpath($path);
    if (is_file($path)) {
        if(session_id())
            session_write_close();

        header_remove();
        set_time_limit(0);
        $size = filesize($path);
        $lastModifiedTime = filemtime($path);
        $fp = @fopen($path, 'rb');
        $range = array(0, $size - 1);

        header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModifiedTime)." GMT");
        if (( ! empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime ) ) {
            header("HTTP/1.1 304 Not Modified", true, 304);
            return true;
        }

        if (isset($_SERVER['HTTP_RANGE'])) {
            //$valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']);
            if(substr($_SERVER['HTTP_RANGE'], 0, 6) != 'bytes=') {
                header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
                header('Content-Range: bytes */' . $size); // Required in 416.
                return false;
            }

            $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
            $range = explode('-', $ranges[0]); // to do: only support the first range now.

            if ($range[0] === '') $range[0] = 0;
            if ($range[1] === '') $range[1] = $size - 1;

            if (($range[0] >= 0) && ($range[1] <= $size - 1) && ($range[0] <= $range[1])) {
                header('HTTP/1.1 206 Partial Content', true, 206);
                header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
            }
            else {
                header('HTTP/1.1 416 Requested Range Not Satisfiable', true, 416);
                header('Content-Range: bytes */' . $size);
                return false;
            }
        }

        $contentLength = $range[1] - $range[0] + 1;

        //header('Content-Disposition: attachment; filename="xxxxx"');
        $headers = array(
            'Accept-Ranges' => 'bytes',
            'Content-Length' => $contentLength,
            'Content-Type' => 'application/octet-stream',
        );

        if(!empty($options['headers'])) {
            $headers = array_merge($headers, $options['headers']);
        }
        foreach($headers as $k=>$v) {
            header("$k: $v", true);
        }

        if ($range[0] > 0) {
            fseek($fp, $range[0]);
        }
        $sentSize = 0;
        while (!feof($fp) && (connection_status() === CONNECTION_NORMAL)) {
            $readingSize = $contentLength - $sentSize;
            $readingSize = min($readingSize, 512 * 1024);
            if($readingSize <= 0) break;

            $data = fread($fp, $readingSize);
            if(!$data) break;
            $sentSize += strlen($data);
            echo $data;
            flush();
        }

        fclose($fp);
        return true;
    }
    else {
        header('HTTP/1.1 404 Not Found', true, 404);
        return false;
    }
}


답변

PHP에 PECL 확장을 추가 할 가능성이 있다면 Fileinfo 패키지 의 함수 를 사용하여 콘텐츠 유형을 결정한 다음 적절한 헤더를 보낼 수 있습니다.


답변

Download여기에 언급 된 PHP 함수로 인해 파일이 실제로 다운로드되기 전에 약간의 지연이 발생했습니다. 이 니스 캐시 또는 무엇을 사용하여 발생 된 경우는 모르겠지만, 나를 위해 그것을 제거하는 데 도움이 sleep(1);완전히 세트 $speed1024. 이제 그것은 지옥처럼 빠르게 문제없이 작동합니다. 인터넷에서 사용되는 것을 보았 기 때문에 그 기능을 수정할 수도 있습니다.