This commit is contained in:
2025-07-29 08:14:40 +02:00
parent 5762df8df9
commit 540f6cd1d6
6 changed files with 246 additions and 17 deletions

View File

@ -6,19 +6,66 @@ use App\Service\FileService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class FileController extends AbstractController
{
#[Route('/user/file/{id}', name: 'app_files', methods: ['GET'])]
public function browse(int $id, Request $request, FileService $fileService): JsonResponse
private FileService $fileService;
public function __construct(FileService $fileService)
{
$this->fileService = $fileService;
}
#[Route('/user/file/{domain}/{id}', name: 'app_files', methods: ['GET'])]
public function browse(string $domain, int $id, Request $request): Response
{
$relativePath = $request->query->get('path', '');
try {
$files = $fileService->list($id, '');
$files = $this->fileService->list($domain, (string) $id, $relativePath);
return $this->json(['files' => $files]);
return $this->render('file/browse.html.twig', [
'domain' => $domain,
'id' => $id,
'files' => $files,
'path' => $relativePath,
'editable' => true,
]);
} catch (\Exception $e) {
$this->addFlash('danger', $e->getMessage());
return $this->redirectToRoute('app_files', [
'domain' => $domain,
'id' => $id,
'editable' => true,
]);
}
}
#[Route('/user/file/{domain}/{id}/uploadmodal', name: 'app_files_uploadmodal', methods: ['GET'])]
public function upload(string $domain, int $id, Request $request): Response
{
$relativePath = $request->query->get('path', '');
return new JsonResponse(['success' => true]);
}
#[Route('/user/file/{domain}/{id}/delete', name: 'app_files_delete', methods: ['POST'])]
public function delete(string $domain, int $id, Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$relativePath = $data['path'] ?? null;
if (!$relativePath) {
return $this->json(['error' => 'Chemin non fourni.'], 400);
}
try {
$this->fileService->delete($domain, (string) $id, $relativePath);
return $this->json(['success' => true]);
} catch (\Exception $e) {
return $this->json(['error' => $e->getMessage()], 400);
}

View File

@ -6,6 +6,7 @@ use App\Entity\Project;
use App\Entity\User;
use App\Form\ProjectType;
use App\Repository\ProjectRepository;
use App\Service\FileService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@ -15,6 +16,13 @@ use Symfony\Component\Routing\Attribute\Route;
class ProjectController extends AbstractController
{
private FileService $fileService;
public function __construct(FileService $fileService)
{
$this->fileService = $fileService;
}
#[Route('/admin/project', name: 'app_admin_project')]
public function list(ProjectRepository $projectRepository): Response
{
@ -41,6 +49,7 @@ class ProjectController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($project);
$em->flush();
$this->fileService->init('project', $project->getId());
return $this->redirectToRoute('app_admin_project');
}

View File

@ -8,7 +8,6 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_project_title', fields: ['title'])]
class Project
{
#[ORM\Id]

View File

@ -2,32 +2,58 @@
namespace App\Service;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelInterface;
class FileService
{
private string $basePath;
private Filesystem $filesystem;
public function __construct()
public function __construct(KernelInterface $kernel)
{
// Répertoire en dur dans le projet, pas dans /public
$this->basePath = realpath(__DIR__.'/../../uploads');
if (!$this->basePath) {
throw new \RuntimeException('Répertoire /uploads introuvable.');
$this->filesystem = new Filesystem();
$projectDir = $kernel->getProjectDir(); // chemin racine du projet
$this->basePath = $projectDir.'/uploads';
if (!is_dir($this->basePath)) {
// On crée le dossier uploads s'il n'existe pas
try {
$this->filesystem->mkdir($this->basePath, 0775);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException('Impossible de créer le dossier /uploads : '.$e->getMessage());
}
}
}
/**
* Liste les fichiers/dossiers pour un projet donné
* Initialise un répertoire pour une entité (ex: project/123)
*/
public function list(string $projectId, string $relativePath = ''): array
public function init(string $domain, string $id): void
{
$targetPath = $this->basePath.'/'.$projectId.'/'.ltrim($relativePath, '/');
$entityPath = $this->getEntityPath($domain, $id);
if (!is_dir($entityPath)) {
try {
$this->filesystem->mkdir($entityPath, 0775);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException(sprintf('Impossible de créer le répertoire pour %s/%s : %s', $domain, $id, $e->getMessage()));
}
}
}
/**
* Liste les fichiers dun répertoire lié à une entité (ex: project/123)
*/
public function list(string $domain, string $id, string $relativePath = ''): array
{
$targetPath = $this->getEntityPath($domain, $id).'/'.ltrim($relativePath, '/');
$realPath = realpath($targetPath);
// Sécurité : protection contre les accès hors du dossier projet
if (!$realPath || !str_starts_with($realPath, $this->basePath.'/'.$projectId)) {
$baseEntityPath = $this->getEntityPath($domain, $id);
if (!$realPath || !str_starts_with($realPath, $baseEntityPath)) {
throw new NotFoundHttpException('Répertoire non autorisé ou inexistant.');
}
@ -39,10 +65,37 @@ class FileService
$results[] = [
'name' => $file->getFilename(),
'isDirectory' => $file->isDir(),
'path' => ltrim(str_replace($this->basePath.'/'.$projectId, '', $file->getRealPath()), '/'),
'path' => ltrim(str_replace($baseEntityPath, '', $file->getRealPath()), '/'),
];
}
return $results;
}
/**
* Supprime un fichier ou dossier (de façon sécurisée)
*/
public function delete(string $domain, string $id, string $relativePath): void
{
$baseEntityPath = $this->getEntityPath($domain, $id);
$targetPath = realpath($baseEntityPath.'/'.ltrim($relativePath, '/'));
if (!$targetPath || !str_starts_with($targetPath, $baseEntityPath)) {
throw new NotFoundHttpException('Fichier ou dossier non autorisé.');
}
try {
$this->filesystem->remove($targetPath);
} catch (IOExceptionInterface $e) {
throw new \RuntimeException('Erreur lors de la suppression : '.$e->getMessage());
}
}
/**
* Construit le chemin absolu dun domaine/id
*/
private function getEntityPath(string $domain, string $id): string
{
return $this->basePath.'/'.$domain.'/'.$id;
}
}