Skip to content

Commit

Permalink
Add team logo upload feature
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusklocke committed Dec 2, 2023
1 parent a874c3b commit f1d8e17
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docker-compose.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
- ./docker/nginx/dev.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/ssl/certs:/etc/ssl/certs:ro
- /etc/ssl/private:/etc/ssl/private:ro
- logos:/mnt/logos
depends_on:
- php
- ui
Expand All @@ -18,6 +19,7 @@ services:
user: 1000:1000
volumes:
- .:/var/www/api
- logos:/mnt/logos
depends_on:
- mariadb
env_file:
Expand Down Expand Up @@ -63,3 +65,4 @@ services:
volumes:
mysql-data:
mysql-backup:
logos:
6 changes: 6 additions & 0 deletions docker/nginx/dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ server {
gzip on;
gzip_types application/json application/javascript image/png application/font-woff2 image/x-icon;

client_max_body_size 64M;

location /api {
include fastcgi_params;
fastcgi_pass php:9000;
Expand All @@ -39,6 +41,10 @@ server {
proxy_set_header Connection "Upgrade";
}

location /logos {
alias /mnt/logos;
}

location / {
proxy_pass http://ui:3098;
}
Expand Down
2 changes: 2 additions & 0 deletions docker/php/php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ log_errors=On
expose_php=Off
short_open_tag=Off
xdebug.mode=coverage
upload_max_filesize=2M
post_max_size=64M
8 changes: 6 additions & 2 deletions src/Infrastructure/API/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use HexagonalPlayground\Infrastructure\API\GraphQL\ServiceProvider as GraphQLServiceProvider;
use HexagonalPlayground\Infrastructure\API\Health\RouteProvider as HealthRouteProvider;
use HexagonalPlayground\Infrastructure\API\Health\ServiceProvider as HealthServiceProvider;
use HexagonalPlayground\Infrastructure\API\Logos\RouteProvider as LogosRouteProvider;
use HexagonalPlayground\Infrastructure\API\Logos\ServiceProvider as LogosServiceProvider;
use HexagonalPlayground\Infrastructure\API\Security\AuthenticationMiddleware;
use HexagonalPlayground\Infrastructure\API\Security\ServiceProvider as SecurityServiceProvider;
use HexagonalPlayground\Infrastructure\API\Security\WebAuthn\RouteProvider as WebAuthnRouteProvider;
Expand Down Expand Up @@ -39,7 +41,8 @@ public function __construct()
new MailServiceProvider(),
new EventServiceProvider(),
new GraphQLServiceProvider(),
new WebAuthnServiceProvider()
new WebAuthnServiceProvider(),
new LogosServiceProvider()
];

$container = ContainerBuilder::build($serviceProviders);
Expand All @@ -63,7 +66,8 @@ public function __construct()
$routeProviders = [
new GraphQLRouteProvider(),
new WebAuthnRouteProvider(),
new HealthRouteProvider()
new HealthRouteProvider(),
new LogosRouteProvider()
];

foreach ($routeProviders as $provider) {
Expand Down
28 changes: 28 additions & 0 deletions src/Infrastructure/API/Logos/GetStatsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);

namespace HexagonalPlayground\Infrastructure\API\Logos;

use HexagonalPlayground\Infrastructure\API\ActionInterface;
use HexagonalPlayground\Infrastructure\API\JsonResponseWriter;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class GetStatsAction implements ActionInterface
{
private JsonResponseWriter $responseWriter;

public function __construct(JsonResponseWriter $responseWriter)
{
$this->responseWriter = $responseWriter;
}

public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
return $this->responseWriter->write($response, [
'maxFileSize' => 0,
'totalFileSize' => 0,
'totalFileCount' => 0
]);
}
}
16 changes: 16 additions & 0 deletions src/Infrastructure/API/Logos/RouteProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace HexagonalPlayground\Infrastructure\API\Logos;

use HexagonalPlayground\Infrastructure\API\RouteProviderInterface;
use Slim\Interfaces\RouteCollectorProxyInterface;

class RouteProvider implements RouteProviderInterface
{
public function register(RouteCollectorProxyInterface $routeCollectorProxy): void
{
$routeCollectorProxy->get('/logos', GetStatsAction::class);
$routeCollectorProxy->post('/logos', UploadAction::class);
}
}
17 changes: 17 additions & 0 deletions src/Infrastructure/API/Logos/ServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);

namespace HexagonalPlayground\Infrastructure\API\Logos;

use HexagonalPlayground\Application\ServiceProviderInterface;
use DI;

class ServiceProvider implements ServiceProviderInterface
{
public function getDefinitions(): array
{
return [
UploadAction::class => DI\autowire()
];
}
}
125 changes: 125 additions & 0 deletions src/Infrastructure/API/Logos/UploadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);

namespace HexagonalPlayground\Infrastructure\API\Logos;

use HexagonalPlayground\Domain\Exception\InternalException;
use HexagonalPlayground\Domain\Exception\InvalidInputException;
use HexagonalPlayground\Domain\Util\Uuid;
use HexagonalPlayground\Infrastructure\API\ActionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Log\LoggerInterface;

class UploadAction implements ActionInterface
{
private string $storageBasePath;
private string $publicBasePath;
private LoggerInterface $logger;

public function __construct(LoggerInterface $logger)
{
$this->storageBasePath = '/mnt/logos';
$this->publicBasePath = '/logos';
$this->logger = $logger;
}

public function __invoke(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
{
$file = $this->getUploadedFile($request);
$this->checkForUploadError($file);
$fileId = $this->generateFileId();

$fileSize = (int)$file->getSize();
if ($fileSize === 0) {
throw new InvalidInputException("Uploaded file is empty");
}

$mediaType = $file->getClientMediaType();
if ($mediaType !== 'image/webp') {
throw new InvalidInputException("Invalid media type. Expected: image/webp. Got: $mediaType");
}

$file->moveTo($this->buildStoragePath($fileId));

return $response->withHeader('Location', $this->buildPublicPath($fileId));
}

private function buildStoragePath(string $fileId): string
{
return join(DIRECTORY_SEPARATOR, [$this->storageBasePath, "$fileId.webp"]);
}

private function buildPublicPath(string $fileId): string
{
return join('/', [$this->publicBasePath, "$fileId.webp"]);
}

private function generateFileId(): string
{
return Uuid::create();
}

private function getMaxFileSize(): int
{
$value = ini_get('upload_max_filesize');
$value = trim($value);

if (is_numeric($value)) {
return (int)$value;
}

$unit = substr($value, -1);
$value = substr($value, 0, -1);

switch (strtolower($unit)) {
case 'g':
$value *= 2**30;
break;
case 'm':
$value *= 2**20;
break;
case 'k':
$value *= 2**10;
break;
default:
throw new InternalException('Invalid unit suffix in "upload_max_filesize"');
}

return (int)$value;
}

private function getUploadedFile(ServerRequestInterface $request): UploadedFileInterface
{
$files = $request->getUploadedFiles();
$count = count($files);

if ($count !== 1) {
throw new InvalidInputException("Invalid upload file count. Expected: 1. Got: $count");
}

/** @var UploadedFileInterface $file */
$file = array_shift($files);

return $file;
}

private function checkForUploadError(UploadedFileInterface $uploadedFile): void
{
$errorCode = $uploadedFile->getError();
switch ($errorCode) {
case UPLOAD_ERR_OK:
return;
case UPLOAD_ERR_INI_SIZE:
$maxSize = ini_get('upload_max_filesize');
throw new InvalidInputException("Invalid file upload: File exceeds max size of $maxSize");
default:
$this->logger->error("Unexpected file upload error", [
'errorCode' => $errorCode,
'files' => $_FILES
]);
throw new InternalException("Invalid file upload: Code $errorCode");
}
}
}

0 comments on commit f1d8e17

Please sign in to comment.