git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5374 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
232 lines
6.9 KiB
PHP
232 lines
6.9 KiB
PHP
<?php
|
|
/** DomFramework
|
|
* @package domframework
|
|
* @author Dominique Fournier <dominique@fournier38.fr>
|
|
*/
|
|
|
|
/** This class allow a program to download a specific file from the filesystem,
|
|
* without using too much memory. In also allow to manage the resuming of a
|
|
* paused transfert by getting the range of download.
|
|
*/
|
|
class outputdl
|
|
{
|
|
/** Allow or deny the resuming of the HTTP transferts
|
|
*/
|
|
private $resumeAllow = true;
|
|
|
|
/** The base dir used as root
|
|
*/
|
|
private $base = "/";
|
|
|
|
/** Store the headers to allow the unit tests to get them
|
|
*/
|
|
private $headers = array ();
|
|
|
|
/** Headers sent
|
|
*/
|
|
private $headersSent = false;
|
|
|
|
/** Get/Set Allow/Deny the resuming of transferts
|
|
* @param boolean|null $resumeAllow True : resume OK, false : resume forbid
|
|
* @return $this|boolean
|
|
*/
|
|
public function resumeAllow ($resumeAllow = null)
|
|
// {{{
|
|
{
|
|
if ($resumeAllow === null)
|
|
return $this->resumeAllow;
|
|
$this->resumeAllow = !! $resumeAllow;
|
|
return $this;
|
|
}
|
|
// }}}
|
|
|
|
/** Get/Set Base of filesystem
|
|
* The base directory is use to secure the download. A user can not request
|
|
* a file outside the base. Example : if the base is /var/lib/files, the
|
|
* user can not request /etc/passwd file (out of scope). If not defined,
|
|
* all the filesystem is allowed
|
|
* @param string|null $base The filesystem base
|
|
* @return $this|string
|
|
*/
|
|
public function base ($base = null)
|
|
// {{{
|
|
{
|
|
if ($base === null)
|
|
return $this->base;
|
|
$this->base = $base;
|
|
return $this;
|
|
}
|
|
// }}}
|
|
|
|
/** Get headers from headers list
|
|
* The headers can be tested too
|
|
* @return array
|
|
*/
|
|
public function headers ()
|
|
// {{{
|
|
{
|
|
return $this->headers;
|
|
}
|
|
// }}}
|
|
|
|
/** Add a new header and send it if possible
|
|
* @param string $header The header to send
|
|
* @return $this
|
|
*/
|
|
private function header ($header)
|
|
// {{{
|
|
{
|
|
if (! headers_sent())
|
|
header ($header);
|
|
$this->headers[] = $header;
|
|
return $this;
|
|
}
|
|
// }}}
|
|
|
|
/** Download a file with management of Partial Download (like resume)
|
|
* Manage the HTTP headers to allow to resume the download if it is allowed
|
|
* Do not go throw the renderer, exit at end of transfert
|
|
* @param string $path The path to download
|
|
* @param string|null $filename The filename to send to the browser
|
|
*/
|
|
public function downloadFile ($path, $filename = null)
|
|
// {{{
|
|
{
|
|
if (!file_exists ($path))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid file to download : file doesn't exists"), 404);
|
|
if (is_link ($path) || is_dir ($path))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid file to download : not a file"), 406);
|
|
$path = realpath ($path);
|
|
if (substr ($path, 0, strlen ($this->base)) !== $this->base)
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid file to download : out of base"), 406);
|
|
if (! is_file ($path))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid file to download : not a file"), 406);
|
|
if (! is_readable ($path))
|
|
throw new \Exception (dgettext ("domframework",
|
|
"Invalid file to download : file not readable"), 406);
|
|
ini_set ('display_errors', 0);
|
|
ini_set ('display_startup_errors', 0);
|
|
if (! defined ("PHPUNIT") && ob_get_level ())
|
|
ob_end_clean ();
|
|
|
|
if ($filename === null)
|
|
$filename = basename ($path);
|
|
$this->header ('Content-Description: File Transfer');
|
|
$this->header ('Content-Type: application/octet-stream');
|
|
$this->header ('Content-Disposition: attachment; filename="'.$filename.'"');
|
|
$this->header ('Content-Transfer-Encoding: binary');
|
|
//header ('Expires: 0');
|
|
//header ('Cache-Control: must-revalidate, post-check=0, pre-check=0');
|
|
//header ('Pragma: public');
|
|
$filesize = filesize ($path);
|
|
if (! $this->resumeAllow)
|
|
{
|
|
$this->header ('Accept-Ranges: none');
|
|
}
|
|
else
|
|
{
|
|
$this->header ('Accept-Ranges: bytes');
|
|
if (isset ($_SERVER["HTTP_RANGE"]) &&
|
|
strtolower (substr ($_SERVER["HTTP_RANGE"], 0, 6)) === "bytes=")
|
|
{
|
|
$boundary = "Qm91bmRhcnk=";
|
|
$rangeTxt = trim (substr ($_SERVER["HTTP_RANGE"], 6));
|
|
$ranges = explode (",", $rangeTxt);
|
|
if (count ($ranges) > 1)
|
|
$this->header (
|
|
"Content-Type: multipart/byteranges; boundary=$boundary");
|
|
foreach ($ranges as $nb => $range)
|
|
{
|
|
if (trim ($range) === "-")
|
|
throw new \Exception ("Invalid range provided", 416);
|
|
@list ($start, $stop) = explode ("-", trim ($range));
|
|
if ($stop === null || $stop === "")
|
|
$stop = $filesize;
|
|
if ($stop && ($start === null || $start === ""))
|
|
{
|
|
$start = $filesize - $stop;
|
|
$stop = $filesize;
|
|
}
|
|
$start = intval ($start);
|
|
$stop = intval ($stop);
|
|
if ($start > $stop)
|
|
throw new \Exception ("Invalid range provided", 416);
|
|
if ($start < 0 || $stop > $filesize || $start > $filesize)
|
|
throw new \Exception ("Invalid range provided", 416);
|
|
if (count ($ranges) > 1)
|
|
{
|
|
if ($nb > 0)
|
|
echo "\r\n";
|
|
echo "--".$boundary."\r\n";
|
|
echo "Content-Range: bytes $start-$stop/$filesize\r\n";
|
|
echo "Content-Type: application/octet-stream\r\n";
|
|
echo "\r\n";
|
|
}
|
|
else
|
|
{
|
|
$this->header ("Content-Range: bytes $start-$stop/$filesize");
|
|
}
|
|
http_response_code (206);
|
|
$this->downloadFileRange ($path, $start, $stop);
|
|
|
|
}
|
|
if (count ($ranges) > 1)
|
|
{
|
|
if ($nb > 0)
|
|
echo "\r\n";
|
|
echo "--".$boundary."--\r\n";
|
|
}
|
|
if (! defined ("PHPUNIT"))
|
|
exit;
|
|
return;
|
|
}
|
|
}
|
|
// No range or error : send all the file
|
|
$this->header ('Content-Length: '. $filesize);
|
|
$this->downloadFileRange ($path, 0, $filesize);
|
|
if (! defined ("PHPUNIT"))
|
|
exit;
|
|
return;
|
|
}
|
|
// }}}
|
|
|
|
/** Download the file. Do not go through the renderer
|
|
* @param string $path The path to download
|
|
* @param integer $start The start range
|
|
* @param integer $stop The stop range
|
|
*/
|
|
private function downloadFileRange ($path, $start, $stop)
|
|
// {{{
|
|
{
|
|
$file = realpath ($path);
|
|
$chunksize = 10*1024*1024; // how many bytes per chunk
|
|
$start = intval ($start);
|
|
$stop = intval ($stop);
|
|
$handle = fopen ($file, 'rb');
|
|
if ($handle === false)
|
|
{
|
|
die ("error");
|
|
}
|
|
if (fseek ($handle, $start) === false)
|
|
die ("Can not seek");
|
|
$size = $start;
|
|
while (! feof ($handle))
|
|
{
|
|
$block = fread ($handle, $chunksize);
|
|
if ($size + $chunksize > $stop)
|
|
$block = substr ($block, 0, $stop - $size + 1);
|
|
$size += strlen ($block);
|
|
echo $block;
|
|
flush ();
|
|
if ($size >= $stop)
|
|
break;
|
|
}
|
|
fclose($handle);
|
|
}
|
|
// }}}
|
|
}
|