outputdl : allow to download a file from filesystem, and manage the partial download if the browser request it
git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@5374 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
This commit is contained in:
200
Tests/outputdlTest.php
Normal file
200
Tests/outputdlTest.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/** DomFramework - Tests
|
||||
* @package domframework
|
||||
* @author Dominique Fournier <dominique@fournier38.fr>
|
||||
*/
|
||||
|
||||
/** Test the outputdl.php file */
|
||||
class test_outputdl extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function test_outputdl_init ()
|
||||
{
|
||||
exec ("rm -f /tmp/testDFWoutputDL*");
|
||||
file_put_contents ("/tmp/testDFWoutputDL",
|
||||
str_repeat (
|
||||
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n",
|
||||
1000)
|
||||
);
|
||||
symlink ("/etc/passwd", "/tmp/testDFWoutputDL2");
|
||||
symlink ("/tmp/testDFWoutputDL", "/tmp/testDFWoutputDL3");
|
||||
}
|
||||
|
||||
public function test_outputdl_1 ()
|
||||
{
|
||||
// Check the full download content
|
||||
$outputdl = new outputdl ();
|
||||
$this->expectOutputString (str_repeat (
|
||||
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n",
|
||||
1000));
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_Announce_1 ()
|
||||
{
|
||||
// Check the announce of Resume mode Enabled
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
$res = $outputdl->headers ();
|
||||
$this->assertSame (in_array ("Accept-Ranges: bytes", $res), true);
|
||||
}
|
||||
|
||||
public function test_outputdl_Announce_2 ()
|
||||
{
|
||||
// Check the announce of Resume mode Disabled
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->resumeAllow (false);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
$res = $outputdl->headers ();
|
||||
$this->assertSame (in_array ("Accept-Ranges: none", $res), true);
|
||||
}
|
||||
|
||||
public function test_outputdl_Partial_1 ()
|
||||
{
|
||||
// Check the content get with provided range
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=3-9";
|
||||
$this->expectOutputString ("4567890");
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
}
|
||||
|
||||
public function test_outputdl_Partial_2 ()
|
||||
{
|
||||
// Check the content get with provided range
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=3-";
|
||||
$this->expectOutputString (substr (str_repeat (
|
||||
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n",
|
||||
1000), 3));
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
}
|
||||
|
||||
public function test_outputdl_Partial_3 ()
|
||||
{
|
||||
// Check the content get with provided range
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=0-";
|
||||
$this->expectOutputString (str_repeat (
|
||||
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n",
|
||||
1000));
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
}
|
||||
|
||||
public function test_outputdl_Partial_4 ()
|
||||
{
|
||||
// Check the content get with provided range
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=-5";
|
||||
$this->expectOutputString ("wxyz\n");
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
}
|
||||
|
||||
public function test_outputdl_Partial_5 ()
|
||||
{
|
||||
// Check the content get with provided range
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=0-3,6-9";
|
||||
$this->expectOutputString ("--Qm91bmRhcnk=\r
|
||||
Content-Range: bytes 0-3/63000\r
|
||||
Content-Type: application/octet-stream\r
|
||||
\r
|
||||
1234\r
|
||||
--Qm91bmRhcnk=\r
|
||||
Content-Range: bytes 6-9/63000\r
|
||||
Content-Type: application/octet-stream\r
|
||||
\r
|
||||
7890\r
|
||||
--Qm91bmRhcnk=--\r
|
||||
");
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
}
|
||||
|
||||
public function test_outputdl_PartialWrong_1 ()
|
||||
{
|
||||
// Check the invalid provided range
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=99999-";
|
||||
$this->expectException ("Exception", "Invalid range provided", 416);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_PartialWrong_2 ()
|
||||
{
|
||||
// Check the invalid provided range
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=9-3";
|
||||
$this->expectException ("Exception", "Invalid range provided", 416);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_PartialWrong_3 ()
|
||||
{
|
||||
// Check the invalid provided range
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=9-999999";
|
||||
$this->expectException ("Exception", "Invalid range provided", 416);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_PartialWrong_4 ()
|
||||
{
|
||||
// Check the invalid provided range
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$_SERVER["HTTP_RANGE"] = "bytes=-";
|
||||
$this->expectException ("Exception", "Invalid range provided", 416);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_invalidBase_1 ()
|
||||
{
|
||||
// Check the base comparison : OK
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->base ("/tmp");
|
||||
$this->expectOutputString (str_repeat (
|
||||
"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n",
|
||||
1000));
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL");
|
||||
}
|
||||
|
||||
public function test_outputdl_invalidBase_2 ()
|
||||
{
|
||||
// Check the base comparison : BAD
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->base ("/tmp");
|
||||
$this->expectException ("Exception",
|
||||
"Invalid file to download : out of base", 406);
|
||||
$outputdl->downloadFile ("../../../../../..//etc/passwd");
|
||||
}
|
||||
|
||||
public function test_outputdl_invalidFile_1 ()
|
||||
{
|
||||
// Check the base comparison : BAD Symlink
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->base ("/tmp");
|
||||
$this->expectException ("Exception",
|
||||
"Invalid file to download : not a file", 406);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputDL3");
|
||||
}
|
||||
|
||||
public function test_outputdl_invalidFile_2 ()
|
||||
{
|
||||
// Check the base comparison : Non existing
|
||||
unset ($_SERVER["HTTP_RANGE"]);
|
||||
$outputdl = new outputdl ();
|
||||
$outputdl->base ("/tmp");
|
||||
$this->expectException ("Exception",
|
||||
"Invalid file to download : file doesn't exists", 404);
|
||||
$outputdl->downloadFile ("/tmp/testDFWoutputNON");
|
||||
}
|
||||
}
|
||||
231
outputdl.php
Normal file
231
outputdl.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?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);
|
||||
}
|
||||
// }}}
|
||||
}
|
||||
Reference in New Issue
Block a user