mirror of
https://github.com/DirectoryLister/DirectoryLister.git
synced 2025-08-27 15:50:40 +02:00
Added the ability to download a directory as a zip archive
This commit is contained in:
@@ -4,7 +4,11 @@ LABEL maintainer="Chris Kankiewicz <Chris@ChrisKankiewicz.com>"
|
||||
COPY .docker/apache/config/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
COPY .docker/php/config/php.ini /usr/local/etc/php/php.ini
|
||||
|
||||
RUN apt-get update && apt-get install --assume-yes libzip-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN docker-php-ext-install zip && pecl install xdebug && docker-php-ext-enable xdebug
|
||||
|
||||
RUN a2enmod rewrite
|
||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
@@ -46,7 +46,10 @@ class AppManager
|
||||
{
|
||||
$this->registerProviders();
|
||||
$app = Bridge::create($this->container);
|
||||
$app->add(new Middlewares\Expires(['text/json' => '+1 hour']));
|
||||
$app->add(new Middlewares\Expires([
|
||||
'application/zip' => '+1 hour',
|
||||
'text/json' => '+1 hour',
|
||||
]));
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
@@ -39,6 +39,9 @@ class IndexController
|
||||
case array_key_exists('search', $request->getQueryParams()):
|
||||
return $this->container->call(Handlers\SearchHandler::class, [$request, $response]);
|
||||
|
||||
case array_key_exists('zip', $request->getQueryParams()):
|
||||
return $this->container->call(Handlers\ZipHandler::class, [$request, $response]);
|
||||
|
||||
default:
|
||||
return $this->container->call(Handlers\DirectoryHandler::class, [$request, $response]);
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace App\Handlers;
|
||||
|
||||
use PHLAK\Config\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
@@ -43,7 +44,7 @@ class DirectoryHandler
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response)
|
||||
public function __invoke(Request $request, Response $response): ResponseInterface
|
||||
{
|
||||
$path = $request->getQueryParams()['dir'] ?? '.';
|
||||
|
||||
|
@@ -4,6 +4,7 @@ namespace App\Handlers;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use SplFileInfo;
|
||||
@@ -33,8 +34,10 @@ class FileInfoHandler
|
||||
*
|
||||
* @param \Slim\Psr7\Request $request
|
||||
* @param \Slim\Psr7\Response $response
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response)
|
||||
public function __invoke(Request $request, Response $response): ResponseInterface
|
||||
{
|
||||
$path = $request->getQueryParams()['info'];
|
||||
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
@@ -35,7 +36,7 @@ class SearchHandler
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response)
|
||||
public function __invoke(Request $request, Response $response): ResponseInterface
|
||||
{
|
||||
$search = $request->getQueryParams()['search'];
|
||||
|
||||
|
73
app/src/Handlers/ZipHandler.php
Normal file
73
app/src/Handlers/ZipHandler.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\TemporaryFile;
|
||||
use DI\Container;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
use ZipArchive;
|
||||
|
||||
class ZipHandler
|
||||
{
|
||||
/** @var Container The application container */
|
||||
protected $container;
|
||||
|
||||
/** @var Finder The Finder Component */
|
||||
protected $finder;
|
||||
|
||||
/**
|
||||
* Create a new ZipHandler object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
* @param \PhpCsFixer\Finder $finder
|
||||
*/
|
||||
public function __construct(Container $container, Finder $finder)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->finder = $finder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the ZipHandler.
|
||||
*
|
||||
* @param \Slim\Psr7\Request $request
|
||||
* @param \Slim\Psr7\Response $response
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response): ResponseInterface
|
||||
{
|
||||
$path = $request->getQueryParams()['zip'];
|
||||
|
||||
if (! realpath($path)) {
|
||||
return $response->withStatus(404, 'File not found');
|
||||
}
|
||||
|
||||
$tempFile = new TemporaryFile(
|
||||
$this->container->get('base_path') . '/app/cache'
|
||||
);
|
||||
|
||||
$zip = new ZipArchive;
|
||||
$zip->open((string) $tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||
|
||||
foreach ($this->finder->in($path) as $file) {
|
||||
if ($file->isFile()) {
|
||||
$zip->addFile($file->getRealPath(), $file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
$response->getBody()->write($tempFile->getContents());
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/zip')
|
||||
->withHeader('Content-Disposition', sprintf(
|
||||
'attachment; filename="%s.zip"',
|
||||
Collection::make(explode('/', $path))->last()
|
||||
));
|
||||
}
|
||||
}
|
46
app/src/TemporaryFile.php
Normal file
46
app/src/TemporaryFile.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
class TemporaryFile
|
||||
{
|
||||
/** @var string Path to the temporary file */
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Create a new TemporaryFile object.
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $prefix
|
||||
*/
|
||||
public function __construct(string $dir, string $prefix = '')
|
||||
{
|
||||
$this->path = tempnam($dir, $prefix);
|
||||
}
|
||||
|
||||
/** Destroy this TemporaryFile object. */
|
||||
public function __destruct()
|
||||
{
|
||||
unlink($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the temporary file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw contents of the file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getContents(): string
|
||||
{
|
||||
return file_get_contents($this->path);
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
<header id="header" class="bg-blue-600 shadow sticky top-0">
|
||||
<div class="container flex flex-col justify-between items-center mx-auto p-4 md:flex-row">
|
||||
<div class="font-mono text-white text-sm tracking-tight mb-2 md:my-1">
|
||||
<div class="container flex flex-coljustify-between items-center mx-auto p-4 md:flex-row">
|
||||
<div class="flex-grow font-mono text-white text-sm tracking-tight mb-2 md:my-1">
|
||||
<a href="." class="hover:underline">Home</a>
|
||||
|
||||
{% if path %}
|
||||
@@ -10,23 +10,31 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form action="." method="get" id="search" class="group relative block w-full bg-blue-700 rounded-full shadow-inner md:w-4/12 md:-my-2">
|
||||
<input type="text" name="search" placeholder="Search this directory..." value="{{ search }}"
|
||||
class="bg-transparent placeholder-gray-900 text-white w-full px-10 py-2"
|
||||
v-model="search"
|
||||
>
|
||||
|
||||
<div class="flex items-center absolute left-0 inset-y-0 ml-2 pointer-events-none">
|
||||
<div class="flex justify-center items-center text-blue-900 w-6 h-6">
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center absolute right-0 inset-y-0 mr-2" v-show="search">
|
||||
<a href="." class="flex justify-center items-center rounded-full text-blue-900 w-6 h-6 hover:bg-red-700 hover:text-white hover:shadow">
|
||||
<i class="fas fa-times"></i>
|
||||
<div class="flex justify-end items-center">
|
||||
{% if not search %}
|
||||
<a href="?zip={{ path }}" title="Download this Directory" class="inline-block text-white mx-2">
|
||||
<i class="fas fa-download fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<form action="." method="get" id="search" class="flex-grow group relative block bg-blue-700 rounded-full shadow-inner">
|
||||
<input type="text" name="search" placeholder="Search..." value="{{ search }}"
|
||||
class="bg-transparent placeholder-gray-900 text-white w-full px-10 py-2"
|
||||
v-model="search"
|
||||
>
|
||||
|
||||
<div class="flex items-center absolute left-0 inset-y-0 ml-2 pointer-events-none">
|
||||
<div class="flex justify-center items-center text-blue-900 w-6 h-6">
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center absolute right-0 inset-y-0 mr-2" v-show="search">
|
||||
<a href="." class="flex justify-center items-center rounded-full text-blue-900 w-6 h-6 hover:bg-red-700 hover:text-white hover:shadow">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -43,6 +43,22 @@ class IndexControllerTest extends TestCase
|
||||
$controller($request, $response);
|
||||
}
|
||||
|
||||
public function test_it_handles_a_zip_request(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getQueryParams')->willReturn(['zip' => 'subdir']);
|
||||
|
||||
$container = $this->createMock(Container::class);
|
||||
$container->expects($this->once())->method('call')->with(
|
||||
Handlers\ZipHandler::class,
|
||||
[$request, $response = new Response]
|
||||
);
|
||||
|
||||
$controller = new IndexController($container);
|
||||
|
||||
$controller($request, $response);
|
||||
}
|
||||
|
||||
public function test_it_handles_a_directory_request(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
|
44
tests/Handlers/ZipHandlerTest.php
Normal file
44
tests/Handlers/ZipHandlerTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Handlers;
|
||||
|
||||
use App\Handlers\ZipHandler;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ZipHandlerTest extends TestCase
|
||||
{
|
||||
public function test_it_returns_a_successful_response_for_a_zip_request(): void
|
||||
{
|
||||
$handler = new ZipHandler($this->container, new Finder);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getQueryParams')->willReturn(['zip' => 'subdir']);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $handler($request, new Response);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals('application/zip', finfo_buffer(
|
||||
finfo_open(), (string) $response->getBody(), FILEINFO_MIME_TYPE
|
||||
));
|
||||
}
|
||||
|
||||
public function test_it_returns_a_404_error_when_not_found(): void
|
||||
{
|
||||
$handler = new ZipHandler($this->container, new Finder);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getQueryParams')->willReturn(['zip' => '404']);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $handler($request, new Response);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
}
|
37
tests/TemporaryFileTest.php
Normal file
37
tests/TemporaryFileTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use App\TemporaryFile;
|
||||
|
||||
class TemporaryFileTest extends TestCase
|
||||
{
|
||||
public function test_it_can_create_a_temporary_file(): void
|
||||
{
|
||||
$tempFile = new TemporaryFile(sys_get_temp_dir());
|
||||
|
||||
$this->assertFileExists((string) $tempFile);
|
||||
}
|
||||
|
||||
public function test_it_can_write_to_and_read_from_a_temporary_file(): void
|
||||
{
|
||||
$tempFile = new TemporaryFile(sys_get_temp_dir());
|
||||
|
||||
$this->assertFileIsReadable((string) $tempFile);
|
||||
$this->assertFileIsWritable((string) $tempFile);
|
||||
|
||||
file_put_contents((string) $tempFile, 'Test file; please ignore');
|
||||
|
||||
$this->assertEquals('Test file; please ignore', $tempFile->getContents());
|
||||
}
|
||||
|
||||
public function test_it_removes_the_underlying_file_on_destruction(): void
|
||||
{
|
||||
$tempFile = new TemporaryFile(sys_get_temp_dir());
|
||||
$filePath = (string) $tempFile;
|
||||
|
||||
unset($tempFile);
|
||||
|
||||
$this->assertFileNotExists($filePath);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user