This post describes a method to download a file using a PHP script and resume the download if the previous download was interrupted. The code here could be used while downloading a new version of a PHP application from the application author’s website. Two implementation examples are provided – one using cURL and another using fsockopen.
Range header of the HTTP protocol
First, lets start by seeing how the HTTP protocol allows downloads to be resumed. If you don’t already have PuTTY, get it from here. We will use PuTTY to directly connect to a HTTP server, send it some HTTP headers and see the response.
Create a text file with some text in it (at least 20 characters long to make it easier for us to test). Upload it so it is accessible at http://localhost/file.txt . I have used the URL http://localhost/file.txt . Change the settings below according to your URL. Open PuTTY and use the following settings:
Host Name: localhost
Port: 80
Connection Type: Raw
Close window on exit: Never
Click on Open. In the window that shows up, type the following:
1
|
GET /file.txt HTTP/1.1
|
2
|
Host: localhost
|
3
|
Range: bytes=5-
|
4
|
Connection: Close
|
Make sure you hit enter twice after typing the text above. You should see something like the text below if you had used something like “abcdefghijklmnopqrstuvwxyz” as the contents of file.txt:
01
|
HTTP/1.1 206 Partial Content
|
02
|
Date: Fri, 06 Nov 2009 16:25:07 GMT
|
03
|
Server: Apache/2.2.12 (Win32) mod_ssl/2.2.12 OpenSSL/0.9.8k
|
04
|
Last-Modified: Fri, 06 Nov 2009 15:56:25 GMT
|
05
|
ETag: "4000000044a82-1a-477b5e2d8093c"
|
06
|
Accept-Ranges: bytes
|
07
|
Content-Length: 21
|
08
|
Content-Range: bytes 5-25/26
|
09
|
Connection: close
|
10
|
Content-Type: text/plain
|
11
|
12
|
fghijklmnopqrstuvwxyz
|
You can see that it has returned a few headers followed by the contents of the file starting from the 6th byte. Try it a few more times modifying the Range header (for example, 5-15 to get bytes 6 to 16).
Using cURL to resume downloads
Resuming a download using cURL is quite straight forward. We use the CURLOPT_RANGE option to set the range if the file we are trying to download already exists and use the “a” option to fopen to append the contents to the file. The code is below:
01
|
$url = "http://localhost/file.txt" ;
|
02
|
$fileName = "file.txt" ;
|
03
|
04
|
$ch = curl_init();
|
05
|
curl_setopt( $ch , CURLOPT_URL, $url );
|
06
|
07
|
if ( file_exists ( $fileName )) {
|
08
|
$from = filesize ( $fileName );
|
09
|
curl_setopt( $ch , CURLOPT_RANGE, $from . "-" );
|
10
|
}
|
11
|
12
|
$fp = fopen ( $fileName , "a" );
|
13
|
if (! $fp ) {
|
14
|
exit ;
|
15
|
}
|
16
|
curl_setopt( $ch , CURLOPT_FILE, $fp );
|
17
|
$result = curl_exec( $ch );
|
18
|
curl_close( $ch );
|
19
|
20
|
fclose( $fp );
|
Using fsockopen to resume downloads
Resuming a download with fsockopen is more involved as it requires us to send the request headers and then parse the response headers:
01
|
$url = "http://localhost/file.txt" ;
|
02
|
$fileName = "file.txt" ;
|
03
|
04
|
$partialContent = false;
|
05
|
$finalFileSize = 0;
|
06
|
07
|
$urlParts = parse_url ( $url );
|
08
|
$socketHandler = fsockopen ( $urlParts [ "host" ], 80, $errno , $errstr , 30);
|
09
|
if (! $socketHandler ) {
|
10
|
exit ;
|
11
|
} else {
|
12
|
$from = 0;
|
13
|
14
|
if ( file_exists ( $fileName )) {
|
15
|
$from = filesize ( $fileName );
|
16
|
}
|
17
|
18
|
$out = "GET " . $urlParts [ "path" ] . " HTTP/1.1rn" ;
|
19
|
$out .= "Host: " . $urlParts [ "host" ] . "rn" ;
|
20
|
$out .= "Range: bytes=" . $from . "-rn" ;
|
21
|
$out .= "Connection: Closernrn" ;
|
22
|
23
|
$headerFound = false;
|
24
|
25
|
if (! $fileHandler = fopen ( $fileName , "a" )) {
|
26
|
exit ;
|
27
|
}
|
28
|
29
|
fwrite( $socketHandler , $out );
|
30
|
while (! feof ( $socketHandler )) {
|
31
|
if ( $headerFound ) {
|
32
|
if ( $partialContent ) {
|
33
|
$result = fread ( $socketHandler , 8192);
|
34
|
35
|
if (fwrite( $fileHandler , $result ) === false) {
|
36
|
exit ;
|
37
|
}
|
38
|
} else {
|
39
|
fclose( $fileHandler );
|
40
|
fclose( $socketHandler );
|
41
|
exit ;
|
42
|
}
|
43
|
} else {
|
44
|
$result = fgets ( $socketHandler , 8192);
|
45
|
$result = trim( $result );
|
46
|
if ( $result === "" ) {
|
47
|
$headerFound = true;
|
48
|
}
|
49
|
50
|
if ( strstr ( $result , "206 Partial Content" )) {
|
51
|
$partialContent = true;
|
52
|
}
|
53
|
54
|
if (preg_match( "/^Content-Range: bytes (d+)-(d+)/(d+)$/" , $result , $matches )) {
|
55
|
$finalFileSize = intval ( $matches [3]);
|
56
|
}
|
57
|
}
|
58
|
}
|
59
|
fclose( $fileHandler );
|
60
|
fclose( $socketHandler );
|
61
|
62
|
clearstatcache();
|
63
|
64
|
if ( filesize ( $fileName ) == $finalFileSize ) {
|
65
|
// success
|
66
|
} else {
|
67
|
exit ;
|
68
|
}
|
69
|
}
|
Related posts:
Hi!
You have typo here (and some other lines actually too):
$out = “GET ” . $urlParts["path"] . ” HTTP/1.1rn”;
There should be \r\n at the end:
$out = “GET ” . $urlParts["path"] . ” HTTP/1.1\r\n”;
Thanks for article, it was helpful for me.