feat: commit before changing gitea

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-02-08 17:58:01 +01:00
parent b05bd98f63
commit ffceda606f
22 changed files with 1653 additions and 22 deletions

View File

@@ -0,0 +1,145 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Controller;
use App\Domain\Manga\Application\Command\ImportChapter;
use App\Domain\Manga\Application\CommandHandler\ImportChapterHandler;
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportChapterResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
#[AsController]
final class ImportChapterController extends AbstractController
{
public function __construct(
private readonly ImportChapterHandler $commandHandler
) {}
public function __invoke(Request $request): Response
{
// Get form parameters
$mangaId = $request->request->get('mangaId');
$chapterNumber = $request->request->get('chapterNumber');
$uploadedFile = $request->files->get('file');
// Validate required fields
if (!$mangaId) {
return $this->json([
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
], 422);
}
if (!$chapterNumber) {
return $this->json([
['propertyPath' => 'chapterNumber', 'message' => 'chapterNumber is required']
], 422);
}
if (!$uploadedFile) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file']
], 422);
}
// Validate file
$errors = $this->validateFile($uploadedFile);
if (!empty($errors)) {
return $this->json($errors, 422);
}
try {
// Read file binary content
$fileBinary = file_get_contents($uploadedFile->getPathname());
if ($fileBinary === false) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
], 400);
}
// Create the command
$command = new ImportChapter(
mangaId: $mangaId,
chapterNumber: (float) $chapterNumber,
fileBinary: $fileBinary
);
// Execute the import
$this->commandHandler->handle($command);
return $this->json([
'message' => 'Chapter imported successfully',
'mangaId' => $mangaId,
'chapterNumber' => $chapterNumber
], 200);
} catch (MangaNotFoundException $e) {
return $this->json([
'error' => 'Manga not found',
'details' => $e->getMessage()
], 404);
} catch (ChapterNotFoundException $e) {
return $this->json([
'error' => 'Chapter not found',
'details' => $e->getMessage()
], 404);
} catch (\InvalidArgumentException $e) {
return $this->json([
'error' => 'Invalid file',
'details' => $e->getMessage()
], 400);
} catch (\Exception $e) {
return $this->json([
'error' => 'Import failed',
'details' => $e->getMessage()
], 500);
}
}
private function validateFile($uploadedFile): array
{
$errors = [];
// Check if file is valid
if (!$uploadedFile->isValid()) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
];
return $errors;
}
// Check file size (500MB max)
$maxSize = 500 * 1024 * 1024; // 500MB in bytes
if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
];
}
// Check file extension
$allowedExtensions = ['cbz'];
$extension = strtolower($uploadedFile->getClientOriginalExtension());
if (!in_array($extension, $allowedExtensions)) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'Please upload a valid CBZ file'
];
}
return $errors;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Controller;
use App\Domain\Manga\Application\Command\ImportVolume;
use App\Domain\Manga\Application\CommandHandler\ImportVolumeHandler;
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportVolumeResource;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
final class ImportVolumeController extends AbstractController
{
public function __construct(
private readonly ImportVolumeHandler $commandHandler
) {}
public function __invoke(Request $request): Response
{
// Get form parameters
$mangaId = $request->request->get('mangaId');
$volumeNumber = $request->request->get('volumeNumber');
$uploadedFile = $request->files->get('file');
// Validate required fields
if (!$mangaId) {
return $this->json([
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
], 422);
}
if (!$volumeNumber) {
return $this->json([
['propertyPath' => 'volumeNumber', 'message' => 'volumeNumber is required']
], 422);
}
if (!$uploadedFile) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Please upload a file']
], 422);
}
// Validate file
$errors = $this->validateFile($uploadedFile);
if (!empty($errors)) {
return $this->json($errors, 422);
}
try {
// Read file binary content
$fileBinary = file_get_contents($uploadedFile->getPathname());
if ($fileBinary === false) {
return $this->json([
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
], 400);
}
// Create the command
$command = new ImportVolume(
mangaId: $mangaId,
volumeNumber: (int) $volumeNumber,
fileBinary: $fileBinary
);
// Execute the import
$this->commandHandler->handle($command);
return $this->json([
'message' => 'Volume imported successfully',
'mangaId' => $mangaId,
'volumeNumber' => (int) $volumeNumber
], 200);
} catch (MangaNotFoundException $e) {
return $this->json([
'error' => 'Manga not found',
'details' => $e->getMessage()
], 404);
} catch (\InvalidArgumentException $e) {
$statusCode = str_contains($e->getMessage(), 'not found') ? 404 : 400;
return $this->json([
'error' => 'Invalid request',
'details' => $e->getMessage()
], $statusCode);
} catch (\Exception $e) {
return $this->json([
'error' => 'Import failed',
'details' => $e->getMessage()
], 500);
}
}
private function validateFile($uploadedFile): array
{
$errors = [];
// Check if file is valid
if (!$uploadedFile->isValid()) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
];
return $errors;
}
// Check file size (500MB max)
$maxSize = 500 * 1024 * 1024; // 500MB in bytes
if ($uploadedFile->getSize() > $maxSize) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
];
}
// Check file extension
$allowedExtensions = ['cbz'];
$extension = strtolower($uploadedFile->getClientOriginalExtension());
if (!in_array($extension, $allowedExtensions)) {
$errors[] = [
'propertyPath' => 'file',
'message' => 'Please upload a valid CBZ file'
];
}
return $errors;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Domain\Manga\Infrastructure\ApiPlatform\Controller\ImportChapterController;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
shortName: 'ImportChapter',
operations: [
new Post(
uriTemplate: '/chapters/import',
controller: ImportChapterController::class,
deserialize: false,
openapiContext: [
'summary' => 'Import a chapter from CBZ file',
'description' => 'Imports a CBZ file for an existing chapter and stores it',
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'required' => ['mangaId', 'chapterNumber', 'file'],
'properties' => [
'mangaId' => [
'type' => 'string',
'format' => 'uuid',
'description' => 'The manga UUID'
],
'chapterNumber' => [
'type' => 'number',
'description' => 'The chapter number (e.g., 1.5)'
],
'file' => [
'type' => 'string',
'format' => 'binary',
'description' => 'CBZ file to import (max 500MB)'
]
]
]
]
]
],
'responses' => [
'200' => [
'description' => 'Chapter imported successfully',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'message' => ['type' => 'string'],
'chapterId' => ['type' => 'string']
]
]
]
]
]
]
]
)
]
)]
class ImportChapterResource
{
public ?string $mangaId = null;
public ?float $chapterNumber = null;
public ?File $file = null;
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Domain\Manga\Infrastructure\ApiPlatform\Controller\ImportVolumeController;
use Symfony\Component\HttpFoundation\File\File;
#[ApiResource(
shortName: 'ImportVolume',
operations: [
new Post(
uriTemplate: '/volumes/import',
controller: ImportVolumeController::class,
deserialize: false,
openapiContext: [
'summary' => 'Import a volume from CBZ file',
'description' => 'Imports a CBZ file for an existing volume and updates all chapters',
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'required' => ['mangaId', 'volumeNumber', 'file'],
'properties' => [
'mangaId' => [
'type' => 'string',
'format' => 'uuid',
'description' => 'The manga UUID'
],
'volumeNumber' => [
'type' => 'integer',
'description' => 'The volume number'
],
'file' => [
'type' => 'string',
'format' => 'binary',
'description' => 'CBZ file to import (max 500MB)'
]
]
]
]
]
],
'responses' => [
'200' => [
'description' => 'Volume imported successfully',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'message' => ['type' => 'string'],
'mangaId' => ['type' => 'string'],
'volumeNumber' => ['type' => 'integer']
]
]
]
]
]
]
]
)
]
)]
class ImportVolumeResource
{
public ?string $mangaId = null;
public ?int $volumeNumber = null;
public ?File $file = null;
}

View File

@@ -36,6 +36,21 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
return $entity ? $this->toDomainModel($entity) : null;
}
public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter
{
$qb = $this->entityManager->createQueryBuilder()
->select('c')
->from(ChapterEntity::class, 'c')
->where('c.manga = :mangaId')
->andWhere('c.number = :chapterNumber')
->setParameter('mangaId', $mangaId)
->setParameter('chapterNumber', $chapterNumber);
$entity = $qb->getQuery()->getOneOrNullResult();
return $entity ? $this->toDomainModel($entity) : null;
}
public function save(Chapter $chapter): void
{
$entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId());