파일을 전송하기 위해 PHP를 사용할 때 다운로드를 다시 시작할 수 있습니까?
다운로드 가능한 파일의 절대 경로를 노출시키고 싶지 않기 때문에 PHP 스크립트를 사용하여 파일을 다운로드하고 있습니다.
header("Content-Type: $ctype");
header("Content-Length: " . filesize($file));
header("Content-Disposition: attachment; filename=\"$fileName\"");
readfile($file);
안타깝게도 이 스크립트를 통해 전달된 다운로드를 최종 사용자가 재개할 수 없습니다.
이러한 PHP 기반 솔루션으로 재기동 가능한 다운로드를 지원하는 방법이 있습니까?
해야 할 은 '보내기'를입니다.Accept-Ranges: bytes
header in all response : in in in콘 header header header header header header header header header header header header header header header header header header header 。 후, 「」가 는, 「」가 됩니다.Range: bytes=x-y
됩니다(「with」가 있습니다).x
★★★★★★★★★★★★★★★★★」y
) 평소처럼 seek be number)를 합니다.x
를 먼저 보내고 .y
-x
또한 을 바이트로 합니다. 또한 응답을HTTP/1.0 206 Partial Content
.
테스트하지 않아도 어느 정도 동작할 수 있습니다.
$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if ( isset($_SERVER['HTTP_RANGE']) ) {
// if the HTTP_RANGE header is set we're dealing with partial content
$partialContent = true;
// find the requested range
// this might be too simplistic, apparently the client can request
// multiple ranges, which can become pretty complex, so ignore it for now
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$length = intval($matches[2]) - $offset;
} else {
$partialContent = false;
}
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
if ( $partialContent ) {
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $offset . '-' . ($offset + $length) . '/' . $filesize);
}
// output the regular HTTP headers
header('Content-Type: ' . $ctype);
header('Content-Length: ' . $filesize);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
// don't forget to send the data too
print($data);
제가 명백한 것을 놓쳤을 수도 있고, 잠재적인 오류의 원인을 분명히 무시해 왔지만, 이것이 시작일 것입니다.
여기 부분 콘텐츠에 대한 설명이 있고 fread용 문서 페이지에서 부분 콘텐츠에 대한 정보를 찾았습니다.
2017/01년 편집 - PHP > = 7.0 https://github.com/DaveRandom/Resume에서 이 작업을 수행하기 위해 라이브러리를 작성했습니다.
EDIT 2016/02 - 모듈러형 도구 세트로 코드가 완전히 다시 작성되었으며, 이는 단일 함수가 아닌 사용 예시로 사용됩니다.아래의 코멘트에 기재되어 있는 수정이 포함되어 있습니다.
몇 가지 스탠드아론 툴 세트로 리줌 가능한 다운로드를 처리하는 테스트 완료 솔루션(상기 Theo의 답변에 상당 부분 근거)이 코드를 사용하려면 PHP 5.4 이상이 필요합니다.
이 솔루션은 요구당 1개의 범위 밖에 대응할 수 없지만 표준 브라우저에서는 어떤 경우에도 문제가 발생하지 않습니다.
<?php
/**
* Get the value of a header in the current request context
*
* @param string $name Name of the header
* @return string|null Returns null when the header was not sent or cannot be retrieved
*/
function get_request_header($name)
{
$name = strtoupper($name);
// IIS/Some Apache versions and configurations
if (isset($_SERVER['HTTP_' . $name])) {
return trim($_SERVER['HTTP_' . $name]);
}
// Various other SAPIs
foreach (apache_request_headers() as $header_name => $value) {
if (strtoupper($header_name) === $name) {
return trim($value);
}
}
return null;
}
class NonExistentFileException extends \RuntimeException {}
class UnreadableFileException extends \RuntimeException {}
class UnsatisfiableRangeException extends \RuntimeException {}
class InvalidRangeHeaderException extends \RuntimeException {}
class RangeHeader
{
/**
* The first byte in the file to send (0-indexed), a null value indicates the last
* $end bytes
*
* @var int|null
*/
private $firstByte;
/**
* The last byte in the file to send (0-indexed), a null value indicates $start to
* EOF
*
* @var int|null
*/
private $lastByte;
/**
* Create a new instance from a Range header string
*
* @param string $header
* @return RangeHeader
*/
public static function createFromHeaderString($header)
{
if ($header === null) {
return null;
}
if (!preg_match('/^\s*(\S+)\s*(\d*)\s*-\s*(\d*)\s*(?:,|$)/', $header, $info)) {
throw new InvalidRangeHeaderException('Invalid header format');
} else if (strtolower($info[1]) !== 'bytes') {
throw new InvalidRangeHeaderException('Unknown range unit: ' . $info[1]);
}
return new self(
$info[2] === '' ? null : $info[2],
$info[3] === '' ? null : $info[3]
);
}
/**
* @param int|null $firstByte
* @param int|null $lastByte
* @throws InvalidRangeHeaderException
*/
public function __construct($firstByte, $lastByte)
{
$this->firstByte = $firstByte === null ? $firstByte : (int)$firstByte;
$this->lastByte = $lastByte === null ? $lastByte : (int)$lastByte;
if ($this->firstByte === null && $this->lastByte === null) {
throw new InvalidRangeHeaderException(
'Both start and end position specifiers empty'
);
} else if ($this->firstByte < 0 || $this->lastByte < 0) {
throw new InvalidRangeHeaderException(
'Position specifiers cannot be negative'
);
} else if ($this->lastByte !== null && $this->lastByte < $this->firstByte) {
throw new InvalidRangeHeaderException(
'Last byte cannot be less than first byte'
);
}
}
/**
* Get the start position when this range is applied to a file of the specified size
*
* @param int $fileSize
* @return int
* @throws UnsatisfiableRangeException
*/
public function getStartPosition($fileSize)
{
$size = (int)$fileSize;
if ($this->firstByte === null) {
return ($size - 1) - $this->lastByte;
}
if ($size <= $this->firstByte) {
throw new UnsatisfiableRangeException(
'Start position is after the end of the file'
);
}
return $this->firstByte;
}
/**
* Get the end position when this range is applied to a file of the specified size
*
* @param int $fileSize
* @return int
* @throws UnsatisfiableRangeException
*/
public function getEndPosition($fileSize)
{
$size = (int)$fileSize;
if ($this->lastByte === null) {
return $size - 1;
}
if ($size <= $this->lastByte) {
throw new UnsatisfiableRangeException(
'End position is after the end of the file'
);
}
return $this->lastByte;
}
/**
* Get the length when this range is applied to a file of the specified size
*
* @param int $fileSize
* @return int
* @throws UnsatisfiableRangeException
*/
public function getLength($fileSize)
{
$size = (int)$fileSize;
return $this->getEndPosition($size) - $this->getStartPosition($size) + 1;
}
/**
* Get a Content-Range header corresponding to this Range and the specified file
* size
*
* @param int $fileSize
* @return string
*/
public function getContentRangeHeader($fileSize)
{
return 'bytes ' . $this->getStartPosition($fileSize) . '-'
. $this->getEndPosition($fileSize) . '/' . $fileSize;
}
}
class PartialFileServlet
{
/**
* The range header on which the data transmission will be based
*
* @var RangeHeader|null
*/
private $range;
/**
* @param RangeHeader $range Range header on which the transmission will be based
*/
public function __construct(RangeHeader $range = null)
{
$this->range = $range;
}
/**
* Send part of the data in a seekable stream resource to the output buffer
*
* @param resource $fp Stream resource to read data from
* @param int $start Position in the stream to start reading
* @param int $length Number of bytes to read
* @param int $chunkSize Maximum bytes to read from the file in a single operation
*/
private function sendDataRange($fp, $start, $length, $chunkSize = 8192)
{
if ($start > 0) {
fseek($fp, $start, SEEK_SET);
}
while ($length) {
$read = ($length > $chunkSize) ? $chunkSize : $length;
$length -= $read;
echo fread($fp, $read);
}
}
/**
* Send the headers that are included regardless of whether a range was requested
*
* @param string $fileName
* @param int $contentLength
* @param string $contentType
*/
private function sendDownloadHeaders($fileName, $contentLength, $contentType)
{
header('Content-Type: ' . $contentType);
header('Content-Length: ' . $contentLength);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
}
/**
* Send data from a file based on the current Range header
*
* @param string $path Local file system path to serve
* @param string $contentType MIME type of the data stream
*/
public function sendFile($path, $contentType = 'application/octet-stream')
{
// Make sure the file exists and is a file, otherwise we are wasting our time
$localPath = realpath($path);
if ($localPath === false || !is_file($localPath)) {
throw new NonExistentFileException(
$path . ' does not exist or is not a file'
);
}
// Make sure we can open the file for reading
if (!$fp = fopen($localPath, 'r')) {
throw new UnreadableFileException(
'Failed to open ' . $localPath . ' for reading'
);
}
$fileSize = filesize($localPath);
if ($this->range == null) {
// No range requested, just send the whole file
header('HTTP/1.1 200 OK');
$this->sendDownloadHeaders(basename($localPath), $fileSize, $contentType);
fpassthru($fp);
} else {
// Send the request range
header('HTTP/1.1 206 Partial Content');
header('Content-Range: ' . $this->range->getContentRangeHeader($fileSize));
$this->sendDownloadHeaders(
basename($localPath),
$this->range->getLength($fileSize),
$contentType
);
$this->sendDataRange(
$fp,
$this->range->getStartPosition($fileSize),
$this->range->getLength($fileSize)
);
}
fclose($fp);
}
}
사용 예:
<?php
$path = '/local/path/to/file.ext';
$contentType = 'application/octet-stream';
// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
ini_set('display_errors', '0');
try {
$rangeHeader = RangeHeader::createFromHeaderString(get_request_header('Range'));
(new PartialFileServlet($rangeHeader))->sendFile($path, $contentType);
} catch (InvalidRangeHeaderException $e) {
header("HTTP/1.1 400 Bad Request");
} catch (UnsatisfiableRangeException $e) {
header("HTTP/1.1 416 Range Not Satisfiable");
} catch (NonExistentFileException $e) {
header("HTTP/1.1 404 Not Found");
} catch (UnreadableFileException $e) {
header("HTTP/1.1 500 Internal Server Error");
}
// It's usually a good idea to explicitly exit after sending a file to avoid sending any
// extra data on the end that might corrupt the file
exit;
네, 보조 장비입니다RFC 2616 섹션14.35 를 참조해 주세요.
말은 으로 ' 한다'는 뜻입니다.Range
지정된 오프셋부터 파일 서비스를 시작합니다.
즉, readfile()은 파일 전체에 적용되므로 사용할 수 없습니다.대신 fopen()을 먼저 사용하고 fseek()를 올바른 위치로 이동한 다음 fpassthru()를 사용하여 파일을 처리합니다.
이것은 100% 슈퍼 체크로 동작합니다.사용하고 있기 때문에 문제 없습니다.
/* Function: download with resume/speed/stream options */
/* List of File Types */
function fileTypes($extension){
$fileTypes['swf'] = 'application/x-shockwave-flash';
$fileTypes['pdf'] = 'application/pdf';
$fileTypes['exe'] = 'application/octet-stream';
$fileTypes['zip'] = 'application/zip';
$fileTypes['doc'] = 'application/msword';
$fileTypes['xls'] = 'application/vnd.ms-excel';
$fileTypes['ppt'] = 'application/vnd.ms-powerpoint';
$fileTypes['gif'] = 'image/gif';
$fileTypes['png'] = 'image/png';
$fileTypes['jpeg'] = 'image/jpg';
$fileTypes['jpg'] = 'image/jpg';
$fileTypes['rar'] = 'application/rar';
$fileTypes['ra'] = 'audio/x-pn-realaudio';
$fileTypes['ram'] = 'audio/x-pn-realaudio';
$fileTypes['ogg'] = 'audio/x-pn-realaudio';
$fileTypes['wav'] = 'video/x-msvideo';
$fileTypes['wmv'] = 'video/x-msvideo';
$fileTypes['avi'] = 'video/x-msvideo';
$fileTypes['asf'] = 'video/x-msvideo';
$fileTypes['divx'] = 'video/x-msvideo';
$fileTypes['mp3'] = 'audio/mpeg';
$fileTypes['mp4'] = 'audio/mpeg';
$fileTypes['mpeg'] = 'video/mpeg';
$fileTypes['mpg'] = 'video/mpeg';
$fileTypes['mpe'] = 'video/mpeg';
$fileTypes['mov'] = 'video/quicktime';
$fileTypes['swf'] = 'video/quicktime';
$fileTypes['3gp'] = 'video/quicktime';
$fileTypes['m4a'] = 'video/quicktime';
$fileTypes['aac'] = 'video/quicktime';
$fileTypes['m3u'] = 'video/quicktime';
return $fileTypes[$extention];
};
/*
Parameters: downloadFile(File Location, File Name,
max speed, is streaming
If streaming - videos will show as videos, images as images
instead of download prompt
*/
function downloadFile($fileLocation, $fileName, $maxSpeed = 100, $doStream = false) {
if (connection_status() != 0)
return(false);
// in some old versions this can be pereferable to get extention
// $extension = strtolower(end(explode('.', $fileName)));
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$contentType = fileTypes($extension);
header("Cache-Control: public");
header("Content-Transfer-Encoding: binary\n");
header('Content-Type: $contentType');
$contentDisposition = 'attachment';
if ($doStream == true) {
/* extensions to stream */
$array_listen = array('mp3', 'm3u', 'm4a', 'mid', 'ogg', 'ra', 'ram', 'wm',
'wav', 'wma', 'aac', '3gp', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'wmv', 'divx', 'asf');
if (in_array($extension, $array_listen)) {
$contentDisposition = 'inline';
}
}
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) {
$fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
header("Content-Disposition: $contentDisposition;
filename=\"$fileName\"");
} else {
header("Content-Disposition: $contentDisposition;
filename=\"$fileName\"");
}
header("Accept-Ranges: bytes");
$range = 0;
$size = filesize($fileLocation);
if (isset($_SERVER['HTTP_RANGE'])) {
list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
str_replace($range, "-", $range);
$size2 = $size - 1;
$new_length = $size - $range;
header("HTTP/1.1 206 Partial Content");
header("Content-Length: $new_length");
header("Content-Range: bytes $range$size2/$size");
} else {
$size2 = $size - 1;
header("Content-Range: bytes 0-$size2/$size");
header("Content-Length: " . $size);
}
if ($size == 0) {
die('Zero byte file! Aborting download');
}
set_magic_quotes_runtime(0);
$fp = fopen("$fileLocation", "rb");
fseek($fp, $range);
while (!feof($fp) and ( connection_status() == 0)) {
set_time_limit(0);
print(fread($fp, 1024 * $maxSpeed));
flush();
ob_flush();
sleep(1);
}
fclose($fp);
return((connection_status() == 0) and ! connection_aborted());
}
/* Implementation */
// downloadFile('path_to_file/1.mp3', '1.mp3', 1024, false);
PHP 코드를 롤하지 않고 이 문제를 해결할 수 있는 정말 좋은 방법은 mod_xfile Apache 모듈을 사용하는 것입니다.그런 다음 PHP에서 적절한 헤더를 설정합니다.아파치가 그 일을 하게 될 거야
header("X-Sendfile: /path/to/file");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; file=\"filename\"");
새로운 PECL 모듈을 설치할 의향이 있다면 PHP를 사용하여 재개 가능한 다운로드를 지원하는 가장 쉬운 방법은http_send_file()
이렇게.
<?php
http_send_content_disposition("document.pdf", true);
http_send_content_type("application/pdf");
http_throttle(0.1, 2048);
http_send_file("../report.pdf");
?>
출처 : http://www.php.net/manual/en/function.http-send-file.php
데이터베이스 저장 콘텐츠에 사용하고 있으며, 매우 효과적입니다!
네, 이 경우 Range 헤더를 사용할 수 있습니다.완전한 다운로드를 위해서는 클라이언트에 3개의 헤더를 더 제공해야 합니다.
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $fileSize);
header ("Content-Range: bytes 0-" . $fileSize - 1 . "/" . $fileSize . ";");
중단된 다운로드의 경우 다음 방법으로 Range 요청 헤더를 확인해야 합니다.
$headers = getAllHeaders ();
$range = substr ($headers['Range'], '6');
이 경우 206개의 상태 코드를 가진 콘텐츠를 제공하는 것을 잊지 마십시오.
header ("HTTP/1.1 206 Partial content");
header ("Accept-Ranges: bytes");
header ("Content-Length: " . $remaining_length);
header ("Content-Range: bytes " . $start . "-" . $to . "/" . $fileSize . ";");
요청 헤더에서 $start 변수와 $to 변수를 가져와 fseek()을 사용하여 파일 내의 올바른 위치를 찾습니다.
상위 정답에는 다양한 버그가 있습니다.
- 헤더를 레인지 range range range range range range range range range 。
bytes a-b
하면 '아주 좋다'는 뜻이어야 .[a, b]
[a, b)
, , , , 입니다.bytes a-
처리되지 않습니다. - 마이너 버그:출력 처리에 버퍼를 사용하지 않습니다.이로 인해 메모리가 너무 많이 소모되어 대용량 파일의 속도가 느려질 수 있습니다.
변경된 코드는 다음과 같습니다.
// TODO: configurations here
$fileName = "File Name";
$file = "File Path";
$bufferSize = 2097152;
$filesize = filesize($file);
$offset = 0;
$length = $filesize;
if (isset($_SERVER['HTTP_RANGE'])) {
// if the HTTP_RANGE header is set we're dealing with partial content
// find the requested range
// this might be too simplistic, apparently the client can request
// multiple ranges, which can become pretty complex, so ignore it for now
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$end = $matches[2] || $matches[2] === '0' ? intval($matches[2]) : $filesize - 1;
$length = $end + 1 - $offset;
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $offset-$end/$filesize");
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($file));
header("Content-Length: $filesize");
header("Content-Disposition: attachment; filename=\"$fileName\"");
header('Accept-Ranges: bytes');
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
// don't forget to send the data too
ini_set('memory_limit', '-1');
while ($length >= $bufferSize)
{
print(fread($file, $bufferSize));
$length -= $bufferSize;
}
if ($length) print(fread($file, $length));
fclose($file);
이것은 저에게 있어서 매우 효과가 있었습니다.https://github.com/pomle/php-serveFilePartial
pecl http_send_file과 동일하게 동작하는 Small Composer 지원 클래스입니다.즉, 다시 시작할 수 있는 다운로드 및 스로틀을 지원합니다.https://github.com/diversen/http-send-file
모든 브라우저에서 바이트 범위 요청 지원을 위해 다음 코드를 사용할 수 있습니다.
<?php
$file = 'YouTube360p.mp4';
$fileLoc = $file;
$filesize = filesize($file);
$offset = 0;
$fileLength = $filesize;
$length = $filesize - 1;
if ( isset($_SERVER['HTTP_RANGE']) ) {
// if the HTTP_RANGE header is set we're dealing with partial content
$partialContent = true;
preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
$offset = intval($matches[1]);
$tempLength = intval($matches[2]) - 0;
if($tempLength != 0)
{
$length = $tempLength;
}
$fileLength = ($length - $offset) + 1;
} else {
$partialContent = false;
$offset = $length;
}
$file = fopen($file, 'r');
// seek to the requested offset, this is 0 if it's not a partial content request
fseek($file, $offset);
$data = fread($file, $length);
fclose($file);
if ( $partialContent ) {
// output the right headers for partial content
header('HTTP/1.1 206 Partial Content');
}
// output the regular HTTP headers
header('Content-Type: ' . mime_content_type($fileLoc));
header('Content-Length: ' . $fileLength);
header('Content-Disposition: inline; filename="' . $file . '"');
header('Accept-Ranges: bytes');
header('Content-Range: bytes ' . $offset . '-' . $length . '/' . $filesize);
// don't forget to send the data too
print($data);
?>
재개는 HTTP를 .Range
머리글에 "Drughta" 가 Range
및 headerIf-Match
,If-Unmodified-Since
후 않았음을 코드(200이 를.는 에 .Content-Range
그 범위를 응답 본문에 입력합니다.
하지만 PHP에서는 어떻게 하는지 모릅니다.
Thanks Theo! 당신의 메서드는 스트리밍 divx에 직접 작동하지 않았습니다. 왜냐하면 divx 플레이어가 bytes=9932800-과 같은 범위를 전송하고 있었기 때문입니다.
하지만 어떻게 하는지 보여줘서 고마워:D
if(isset($_SERVER['HTTP_RANGE']))
{
file_put_contents('showrange.txt',$_SERVER['HTTP_RANGE']);
조건부 요청(변경되지 않는 한 파일을 다시 다운로드하지 않음) 및 범위 지정(다운로드 일시 중지 및 재개)을 지원하는 파일 서비스 라이브러리를 만들었습니다.Flysystem과 같은 가상 파일 시스템에서도 작동합니다.
여기를 봐주세요: FileWaiter
사용 예:
use Stadly\FileWaiter\Adapter\Local;
use Stadly\FileWaiter\File;
use Stadly\FileWaiter\Waiter;
$streamFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible stream factory.
$file = new File(new Local('filename.txt', $streamFactory)); // Or another file adapter. See below.
$responseFactory = new \GuzzleHttp\Psr7\HttpFactory(); // Any PSR-17 compatible response factory.
$waiter = new Waiter($file, $responseFactory);
$request = \GuzzleHttp\Psr7\ServerRequest::fromGlobals(); // Any PSR-7 compatible server request.
$response = $waiter->handle($request); // The response is created by the response factory.
$emitter = new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter(); // Any way of emitting PSR-7 responses.
$emitter->emit($response);
die();
언급URL : https://stackoverflow.com/questions/157318/resumable-downloads-when-using-php-to-send-the-file
'programing' 카테고리의 다른 글
MySQL의 다중 컬럼인덱스 필드 순서가 중요합니까? (0) | 2023.01.21 |
---|---|
python에서 matplotlib를 사용하여 로그 축 표시 (0) | 2023.01.21 |
for loop 동안 목록 엔트리를 수정하려면 어떻게 해야 합니까? (0) | 2023.01.21 |
카운터를 값별로 정렬하는 방법- 비단뱀 (0) | 2023.01.01 |
Java에서 2배에서 정수로 변환 (0) | 2023.01.01 |