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


98

나는 파일 경로를 수신하고, 그것이 무엇인지 식별하고, 적절한 헤더를 설정하고, 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], '.');
    }
}
}
?>

10
왜 아파치가이 작업을하게하지 않습니까? 항상 ... 빠른 PHP 인터프리터를 시작보다 상당히 될 것
빌리 ONeal

4
파일을 출력하기 전에 요청을 처리하고 데이터베이스에 일부 정보를 저장해야합니다.
Kirk Ouimet

3
더 비싼 정규식없이 확장을 얻는 방법을 제안 $extension = end(explode(".", $pathToFile))해도 될까요 :, 또는 substr 및 strrpos :를 사용하여 할 수 있습니다 $extension = substr($pathToFile, strrpos($pathToFile, '.')). 또한에 대한 폴백으로 mime_content_type()시스템 호출을 시도 할 수 있습니다.$mimetype = exec("file -bi '$pathToFile'", $output);
Fanis Hatzidakis

가장 빠르다 는 것은 무엇을 의미 합니까? 가장 빠른 다운로드 시간?
Alix Axel

답변:


140

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

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


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 모듈)

수행하기 전에 일부 PHP 작업 (데이터베이스에 대한 쿠키 / 기타 GET / POST 매개 변수 확인)을 수행하는 데 문제가 header("Location: " . $path);있습니까?
Afriza N. Arief

2
이러한 작업에는 문제가 없습니다. 헤더가 콘텐츠 앞에 와야하고이 헤더를 보낸 후 작업을 수행해야하므로주의해야 할 것은 콘텐츠 (인쇄, 에코)를 보내는 것입니다. 이는 즉각적인 리디렉션이 아니며 이후 코드가 대부분의 시간 동안 실행되지만 브라우저가 연결을 끊지 않을 것이라는 보장은 없습니다.
Julien Roncaglia

Jords : 아파치도이 기능을 지원한다는 사실을 몰랐습니다. 시간이있을 때 답변에 추가하겠습니다. 유일한 문제는 통합되지 않았기 때문에 (예 : X-Accel-Redirect nginx) 서버가 지원하지 않는 경우 두 번째 솔루션이 필요하다는 것입니다. 하지만 내 대답에 추가해야합니다.
Julien Roncaglia

.htaccess가 XSendFilePath를 제어하도록 어디에서 허용 할 수 있습니까?
Keyne Viana 2011

1
@ Keyne 나는 당신이 할 수 있다고 생각하지 않습니다. tn123.org/mod_xsendfile는 XSendFilePath 옵션의 맥락에서하지리스트의 .htaccess를 않습니다
cheshirekow

33

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

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

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


4
아파치에도 존재한다고 덧붙이겠습니다 : jasny.net/articles/how-i-php-x-sendfile . 스크립트가 서버를 스니핑하고 적절한 헤더를 보내도록 할 수 있습니다. 존재하지 않는 경우 (사용자가 질문에 따라 서버를 제어 할 수없는 경우) 정상으로 readfile()
돌아가십시오

이제 이것은 정말 대단합니다. PHP가 파일을 제공 할 수 있도록 가상 호스트의 메모리 제한을 늘리는 것이 항상 싫었습니다. 곧 시험해 볼게요.
Greg W

1
신용이 필요한 신용의 경우 Lighttpd 가이를 구현 한 최초의 웹 서버였습니다 (나머지는 복사했습니다. 좋은 생각이기 때문에 괜찮습니다.하지만 신용이 필요한 곳에 신용을 부여하십시오) ...
ircmaxell

1
이 답변은 계속 찬성되지만 웹 서버와 해당 설정이 사용자의 제어를 벗어난 환경에서는 작동하지 않습니다.
Kirk Ouimet

이 답변을 게시 한 후에 실제로 질문에 추가했습니다. 성능이 문제라면 웹 서버를 제어 할 수 있어야합니다.
Jords

23

여기 순수한 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)를 지정할 수도 있습니다 .


2
분명히 이것은 커널이 파일을 보내도록 X-Sendfile 또는 변종 중 하나를 사용할 수없는 경우에만 좋은 생각입니다. 위의 feof () / fread () 루프 를 PHP에서 동일한 작업을 수행하는 [ php.net/manual/en/function.eio-sendfile.php](PHP의 eio_sendfile ()] 호출 로 대체 할 수 있어야합니다 . 이것은 커널에서 직접 수행하는 것만 큼 빠르지는 않습니다. PHP에서 생성 된 모든 출력은 여전히 ​​웹 서버 프로세스를 통해 되돌아 가야하기 때문에 PHP 코드에서 수행하는 것보다 훨씬 더 빠릅니다.
Brian C

@BrianC : 물론입니다.하지만 X-Sendfile (사용할 수 없을 수도 있음)을 사용하여 속도 나 멀티 파트 기능을 제한 할 수 eio없으며 항상 사용 가능한 것은 아닙니다. 그래도 +1은 해당 pecl 확장에 대해 몰랐습니다. =)
Alix Axel

transfer-encoding : chunked 및 content-encoding : gzip을 지원하는 것이 유용할까요?
skibulk 2014

$size = sprintf('%u', filesize($path))?
Svish

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

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


12
x-sendfile 방법보다 간단하지만 로그인 한 사람 만 파일에 대한 액세스를 제한하는 것은 아닙니다. 그렇게 할 필요가 없다면 훌륭합니다!
Jords

또한 mod_rewrite로 참조 자 검사를 추가하십시오.
sanmai

1
헤더를 전달하기 전에 인증 할 수 있습니다. 이렇게하면 PHP의 메모리를 통해 많은 것을 펌핑하지 않아도됩니다.
Brent

7
위치는 여전히 모든 액세스 할 수있다 @UltimateBrent .. 그리고는 클라이언트에서 오는 이후 검사가 전혀 안전하지 않습니다 참조
Øyvind Skaar

@Jimbo 어떻게 확인할 사용자 토큰? PHP로? 갑자기 솔루션이 반복됩니다.
Mark Amery 2014-06-21

1

더 나은 구현, 캐시 지원, 사용자 정의 된 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;
    }
}


0

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


0

PHP 및 자동 MIME 유형 감지로 파일을 제공하는 매우 간단한 함수를 코딩했습니다.

function serve_file($filepath, $new_filename=null) {
    $filename = basename($filepath);
    if (!$new_filename) {
        $new_filename = $filename;
    }
    $mime_type = mime_content_type($filepath);
    header('Content-type: '.$mime_type);
    header('Content-Disposition: attachment; filename="downloaded.pdf"');
    readfile($filepath);
}

용법

serve_file("/no_apache/invoice243.pdf");
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.