Skip to content

Commit 575cbfe

Browse files
committed
first commit
1 parent 7ecdad3 commit 575cbfe

File tree

6 files changed

+266
-2
lines changed

6 files changed

+266
-2
lines changed

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/vendor
2+
composer.phar
3+
composer.lock
4+
.DS_Store
5+
Thumbs.db
6+
/phpunit.xml
7+
/.idea
8+
/.vscode
9+
.phpunit.result.cache

README.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
1-
# php-curl-file-downloader
2-
Download large files using PHP and cURL
1+
# Download large files using PHP and cURL
2+
3+
There's too many code snippets on the Internet on how to do this, but not enough libraries. So I made this.
4+
5+
```php
6+
<?php
7+
8+
use Curl\Client;
9+
use CurlDownloader\CurlDownloader;
10+
11+
$browser = new Client();
12+
$downloader = new CurlDownloader($browser);
13+
14+
$downloader->download("https://download.ccleaner.com/cctrialsetup.exe", function ($filename) {
15+
return './2020-06-07-' . $filename;
16+
});
17+
```
18+
19+
## Installation
20+
21+
```bash
22+
composer require athlon1600/php-curl-file-downloader
23+
```
24+
25+
### Links
26+
27+
- https://demo.borland.com/testsite/download_testpage.php
28+

composer.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "athlon1600/php-curl-file-downloader",
3+
"require": {
4+
"athlon1600/php-curl-client": "^1.0"
5+
},
6+
"require-dev": {
7+
"phpunit/phpunit": "^7.0"
8+
},
9+
"autoload": {
10+
"psr-4": {
11+
"CurlDownloader\\": "src/"
12+
}
13+
},
14+
"autoload-dev": {
15+
"psr-4": {
16+
"CurlDownloader\\Tests\\": "tests"
17+
}
18+
}
19+
}

src/CurlDownloader.php

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace CurlDownloader;
4+
5+
use Curl\Client;
6+
7+
class CurlDownloader
8+
{
9+
/** @var Client */
10+
private $client;
11+
12+
// Timeout after 10 minutes.
13+
protected $max_timeout = 600;
14+
15+
public function __construct(Client $client)
16+
{
17+
$this->client = $client;
18+
}
19+
20+
protected function createTempFile()
21+
{
22+
return tempnam(sys_get_temp_dir(), uniqid());
23+
}
24+
25+
protected function getFilenameFromUrl($url)
26+
{
27+
$url_path = parse_url($url, PHP_URL_PATH);
28+
return basename($url_path);
29+
}
30+
31+
/**
32+
* @param $url
33+
* @param $destination
34+
* @return \Curl\Response
35+
*/
36+
public function download($url, $destination)
37+
{
38+
$handler = new HeaderHandler();
39+
40+
// Will download file to temp for now
41+
$temp_filename = $this->createTempFile();
42+
43+
$handle = fopen($temp_filename, 'w+');
44+
45+
$response = $this->client->request('GET', $url, [], [], [
46+
CURLOPT_FILE => $handle,
47+
CURLOPT_HEADERFUNCTION => $handler->callback(),
48+
CURLOPT_TIMEOUT => $this->max_timeout
49+
]);
50+
51+
if ($response->info->http_code === 200) {
52+
$filename = $handler->getContentDispositionFilename();
53+
54+
if (empty($filename)) {
55+
$filename = $this->getFilenameFromUrl($response->info->url);
56+
}
57+
58+
$save_to = call_user_func($destination, $filename);
59+
60+
rename($temp_filename, $save_to);
61+
}
62+
63+
@fclose($handle);
64+
@unlink($temp_filename);
65+
66+
return $response;
67+
}
68+
}

src/HeaderHandler.php

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace CurlDownloader;
4+
5+
class HeaderHandler
6+
{
7+
protected $headers = array();
8+
9+
/** @var callable */
10+
protected $callback;
11+
12+
// Thanks Drupal!
13+
// const REQUEST_HEADER_FILENAME_REGEX = '@\\bfilename(?<star>\\*?)=\\"(?<filename>.+)\\"@';
14+
const REQUEST_HEADER_FILENAME_REGEX = '/filename\s*=\s*["\']*(?<filename>[^"\']+)/';
15+
16+
public function callback()
17+
{
18+
$oThis = $this;
19+
20+
$headers = array();
21+
$first_line_sent = false;
22+
23+
return function ($ch, $data) use ($oThis, &$first_line_sent, &$headers) {
24+
$line = trim($data);
25+
26+
if ($first_line_sent == false) {
27+
$first_line_sent = true;
28+
} elseif ($line === '') {
29+
$oThis->sendHeaders();
30+
} else {
31+
32+
$parts = explode(':', $line, 2);
33+
34+
// Despite that headers may be retrieved case-insensitively, the original case MUST be preserved by the implementation
35+
// Non-conforming HTTP applications may depend on a certain case,
36+
// so it is useful for a user to be able to dictate the case of the HTTP headers when creating a request or response.
37+
38+
// TODO:
39+
// Multiple message-header fields with the same field-name may be present in a message
40+
// if and only if the entire field-value for that header field is defined as a comma-separated list
41+
$oThis->headers[trim($parts[0])] = isset($parts[1]) ? trim($parts[1]) : null;
42+
}
43+
44+
return strlen($data);
45+
};
46+
}
47+
48+
protected function sendHeaders()
49+
{
50+
if (is_callable($this->callback)) {
51+
call_user_func($this->callback, $this);
52+
}
53+
}
54+
55+
/**
56+
* @param callable $callback
57+
*/
58+
public function onHeadersReceived($callback)
59+
{
60+
$this->callback = $callback;
61+
}
62+
63+
// While header names are not case-sensitive, getHeaders() will preserve the exact case in which headers were originally specified.
64+
public function getHeaders()
65+
{
66+
return $this->headers;
67+
}
68+
69+
public function getContentDispositionFilename()
70+
{
71+
$normalized = array_change_key_case($this->headers, CASE_LOWER);
72+
$header = isset($normalized['content-disposition']) ? $normalized['content-disposition'] : null;
73+
74+
if ($header && preg_match(static::REQUEST_HEADER_FILENAME_REGEX, $header, $matches)) {
75+
return $matches['filename'];
76+
}
77+
78+
return null;
79+
}
80+
}

tests/DownloadTest.php

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace CurlDownloader\Tests;
4+
5+
use Curl\Client;
6+
use CurlDownloader\CurlDownloader;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class DownloadTest extends TestCase
10+
{
11+
/** @var CurlDownloader */
12+
protected $client;
13+
14+
protected function setUp()
15+
{
16+
$this->client = new CurlDownloader(new Client());
17+
}
18+
19+
public function test_download_directly()
20+
{
21+
$this->client->download("https://demo.borland.com/testsite/downloads/Small.zip", function ($filename) {
22+
return './' . $filename;
23+
});
24+
25+
$this->assertTrue(file_exists('./Small.zip'));
26+
27+
unlink('./Small.zip');
28+
}
29+
30+
public function test_download_content_disposition()
31+
{
32+
$this->client->download("https://demo.borland.com/testsite/downloads/downloadfile.php?file=Data1KB.dat&cd=attachment+filename", function ($filename) {
33+
return './' . $filename;
34+
});
35+
36+
$this->assertTrue(file_exists('./Data1KB.dat'));
37+
38+
unlink('./Data1KB.dat');
39+
}
40+
41+
public function test_download_content_disposition_github()
42+
{
43+
$this->client->download("https://github.com/guzzle/guzzle/releases/download/6.5.4/guzzle.zip", function ($filename) {
44+
return './' . $filename;
45+
});
46+
47+
$this->assertTrue(file_exists('./guzzle.zip'));
48+
49+
unlink('./guzzle.zip');
50+
}
51+
52+
public function test_download_content_disposition_custom_filename()
53+
{
54+
$this->client->download("https://demo.borland.com/testsite/downloads/downloadfile.php?file=Data1KB.dat&cd=attachment+filename", function ($filename) {
55+
return './2020-06-07-' . $filename;
56+
});
57+
58+
$this->assertTrue(file_exists('./2020-06-07-Data1KB.dat'));
59+
60+
unlink('./2020-06-07-Data1KB.dat');
61+
}
62+
}

0 commit comments

Comments
 (0)