Skip to content

Commit

Permalink
Merge pull request #14 from biigle/use-guzzle
Browse files Browse the repository at this point in the history
Use guzzle
  • Loading branch information
mzur authored Jun 14, 2023
2 parents 6542d9a + b56d96f commit 9603c31
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 70 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"illuminate/console": "^9.0",
"illuminate/filesystem": "^9.0",
"illuminate/support": "^9.0",
"symfony/finder": "^6.0"
"symfony/finder": "^6.0",
"guzzlehttp/guzzle": "^7.7"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
Expand Down
87 changes: 31 additions & 56 deletions src/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Biigle\FileCache\Contracts\FileCache as FileCacheContract;
use Biigle\FileCache\Exceptions\FileLockedException;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Filesystem\FilesystemManager;
use SplFileInfo;
Expand Down Expand Up @@ -38,18 +40,26 @@ class FileCache implements FileCacheContract
*/
protected $storage;

/**
* Guzzle HTTP client to use
*
* @var ClientInterface
*/
protected $client;

/**
* Create an instance.
*
* @param array $config Optional custom configuration.
* @param Filesystem $files
* @param FilesystemManager $storage
*/
public function __construct(array $config = [], $files = null, $storage = null)
public function __construct(array $config = [], $files = null, $storage = null, $client = null)
{
$this->config = array_merge(config('file-cache'), $config);
$this->files = $files ?: app('files');
$this->storage = $storage ?: app('filesystem');
$this->client = $client ?: $this->makeHttpClient();
}

/**
Expand Down Expand Up @@ -254,26 +264,23 @@ public function clear()
*/
protected function existsRemote($file)
{
$context = stream_context_create(['http' => ['method'=>'HEAD']]);
$url = $this->encodeUrl($file->getUrl());
$headers = get_headers($url, 1, $context);
$headers = array_change_key_case($headers, CASE_LOWER);
$response = $this->client->head($file->getUrl());
$code = $response->getStatusCode();

$exists = explode(' ', $headers[0])[1][0] === '2';

if (!$exists) {
if ($code < 200 || $code >= 300) {
return false;
}

if (!empty($this->config['mime_types'])) {
$type = trim(explode(';', $headers['content-type'])[0]);
$type = $response->getHeaderLine('content-type');
$type = trim(explode(';', $type)[0]);
if (!in_array($type, $this->config['mime_types'])) {
throw new Exception("MIME type '{$type}' not allowed.");
}
}

$maxBytes = intval($this->config['max_file_size']);
$size = intval($headers['content-length']);
$size = intval($response->getHeaderLine('content-length'));

if ($maxBytes >= 0 && $size > $maxBytes) {
throw new Exception("The file is too large with more than {$maxBytes} bytes.");
Expand Down Expand Up @@ -437,7 +444,11 @@ protected function retrieveExistingFile($cachedPath, $handle)
protected function retrieveNewFile(File $file, $cachedPath, $handle)
{
if ($this->isRemote($file)) {
$this->getRemoteFile($file, $handle);
$source = $this->getFileStream($file->getUrl());
$cachedPath = $this->cacheFromResource($file, $source, $handle);
if (is_resource($source)) {
fclose($source);
}
} else {
$newCachedPath = $this->getDiskFile($file, $handle);

Expand All @@ -463,29 +474,6 @@ protected function retrieveNewFile(File $file, $cachedPath, $handle)
];
}

/**
* Cache a remote file and get the path to the cached file.
*
* @param File $file Remote file
* @param resource $target Target file resource
* @throws Exception If the file could not be cached.
*
* @return string
*/
protected function getRemoteFile(File $file, $target)
{
$context = stream_context_create(['http' => [
'timeout' => $this->config['timeout'],
]]);
$source = $this->getFileStream($file->getUrl(), $context);
$cachedPath = $this->cacheFromResource($file, $source, $target);
if (is_resource($source)) {
fclose($source);
}

return $cachedPath;
}

/**
* Cache an file from a storage disk and get the path to the cached file. Files
* from local disks are not cached.
Expand Down Expand Up @@ -579,21 +567,13 @@ protected function getCachedPath(File $file)
* Get the stream resource for an file.
*
* @param string $url
* @param resource|null $context Stream context
*
* @return resource
*/
protected function getFileStream($url, $context = null)
protected function getFileStream($url)
{
// Escape special characters (e.g. spaces) that may occur in parts of a HTTP URL.
// We do not use urlencode or rawurlencode because they encode some characters
// (e.g. "+") that should not be changed in the URL.
if (strpos($url, 'http') === 0) {
$url = $this->encodeUrl($url);
}

if (is_resource($context)) {
return @fopen($url, 'r', false, $context);
return $this->client->get($url)->getBody()->detach();
}

return @fopen($url, 'r');
Expand Down Expand Up @@ -627,20 +607,15 @@ protected function getDisk(File $file)
}

/**
* Escape special characters (e.g. spaces) that may occur in parts of a HTTP URL.
* We do not use urlencode or rawurlencode because they encode some characters
* (e.g. "+") that should not be changed in the URL.
*
* @param string $url
* Create a new Guzzle HTTP client.
*
* @return string
* @return ClientInterface
*/
protected function encodeUrl($url)
protected function makeHttpClient(): ClientInterface
{
// List of characters to substitute and their replacements at the same index.
$pattern = [' '];
$replacement = ['%20'];

return str_replace($pattern, $replacement, $url);
return new Client([
'timeout' => $this->config['timeout'],
'http_errors' => false,
]);
}
}
65 changes: 52 additions & 13 deletions tests/FileCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
use Biigle\FileCache\FileCache;
use Biigle\FileCache\GenericFile;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Mockery;

class FileCacheTest extends TestCase
Expand Down Expand Up @@ -385,8 +389,8 @@ public function testExistsDiskTooLarge()

public function testExistsDiskMimeNotAllowed()
{
$this->app['files']->put("{$this->diskPath}/test-image.jpg", 'abc');
$file = new GenericFile('test://test-image.jpg');
$this->app['files']->put("{$this->diskPath}/test-file.txt", 'abc');
$file = new GenericFile('test://test-file.txt');
$cache = new FileCache([
'path' => $this->cachePath,
'mime_types' => ['image/jpeg'],
Expand All @@ -402,32 +406,60 @@ public function testExistsDiskMimeNotAllowed()

public function testExistsRemote404()
{
$file = new GenericFile('https://httpbin.org/status/404');
$cache = new FileCache(['path' => $this->cachePath]);
$mock = new MockHandler([new Response(404)]);
$handlerStack = HandlerStack::create($mock);
$client = new Client([
'handler' => $handlerStack,
'http_errors' => false,
]);

$file = new GenericFile('https://example.com/file');
$cache = new FileCache(['path' => $this->cachePath], client: $client);
$this->assertFalse($cache->exists($file));
}

public function testExistsRemote500()
{
$file = new GenericFile('https://httpbin.org/status/500');
$cache = new FileCache(['path' => $this->cachePath]);
$mock = new MockHandler([new Response(500)]);
$handlerStack = HandlerStack::create($mock);
$client = new Client([
'handler' => $handlerStack,
'http_errors' => false,
]);

$file = new GenericFile('https://example.com/file');
$cache = new FileCache(['path' => $this->cachePath], client: $client);
$this->assertFalse($cache->exists($file));
}

public function testExistsRemote200()
{
$file = new GenericFile('https://httpbin.org/status/200');
$cache = new FileCache(['path' => $this->cachePath]);
$mock = new MockHandler([new Response(200)]);
$handlerStack = HandlerStack::create($mock);
$client = new Client([
'handler' => $handlerStack,
'http_errors' => false,
]);

$file = new GenericFile('https://example.com/file');
$cache = new FileCache(['path' => $this->cachePath], client: $client);
$this->assertTrue($cache->exists($file));
}

public function testExistsRemoteTooLarge()
{
$file = new GenericFile('https://httpbin.org/get');
$mock = new MockHandler([new Response(200, ['content-length' => 100])]);
$handlerStack = HandlerStack::create($mock);
$client = new Client([
'handler' => $handlerStack,
'http_errors' => false,
]);

$file = new GenericFile('https://example.com/file');
$cache = new FileCache([
'path' => $this->cachePath,
'max_file_size' => 1,
]);
'max_file_size' => 50,
], client: $client);

try {
$cache->exists($file);
Expand All @@ -439,11 +471,18 @@ public function testExistsRemoteTooLarge()

public function testExistsRemoteMimeNotAllowed()
{
$file = new GenericFile('https://httpbin.org/json');
$mock = new MockHandler([new Response(200, ['content-type' => 'application/json'])]);
$handlerStack = HandlerStack::create($mock);
$client = new Client([
'handler' => $handlerStack,
'http_errors' => false,
]);

$file = new GenericFile('https://example.com/file');
$cache = new FileCache([
'path' => $this->cachePath,
'mime_types' => ['image/jpeg'],
]);
], client: $client);

try {
$cache->exists($file);
Expand Down

0 comments on commit 9603c31

Please sign in to comment.