Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/deploiement.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,4 @@ seul l'email de notification manque.
| SSH refusé | IP non whitelistée | cPanel → Accès SSH → Autorisation SSH |
| Pas de vignette/EXIF dans la Galerie | Worker Messenger absent | Vérifier la tâche cron (voir « Worker Messenger » ci-dessus) |
| Messages Messenger jamais consommés (`messenger:stats` ne baisse jamais) | `var/log/` absent sur le serveur | Le cron redirige vers `var/log/messenger.log` (`>>`) : si le dossier n'existe pas, la redirection échoue et **la commande PHP ne s'exécute jamais**, sans erreur visible. `mkdir -p var/log` (corrigé dans `bin/deploy-all.sh` depuis le 2026-07-18, mais les instances déployées avant cette date doivent l'avoir manuellement). |
| Une seule instance en `❌` sur un `deploy-all.sh`, différente à chaque run, sans message d'erreur clair | Instabilité SSH transitoire (timeout/latence ponctuelle sur le mutualisé, distincte du piège OOM/LVE déjà documenté ci-dessus) | Relancer simplement `bash bin/deploy-all.sh` une seconde fois — le script est idempotent (`git pull`/`composer install`/migrations ne font rien si déjà à jour) ; vécu le 2026-07-23 sur `yannick.lenouvel.me`, résolu au 2ᵉ run sans autre action. Si l'échec persiste sur la même instance après 2 essais, chercher la cause précise (cf. ligne OOM/LVE ci-dessus) plutôt que de continuer à relancer en boucle. |
8 changes: 8 additions & 0 deletions .github/avancement.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

---

## ✅ Stockage utilisé sur le dashboard (2026-07-23, #301, branche `feature/301-storage-used-dashboard`)

- `HomeController::index()` affichait un placeholder statique (`'Calcul à implémenter'`) à la place du poids réel de stockage.
- `FileRepository::sumSizeByOwner(User $owner): int` : agrégation SQL `SUM(f.size)` filtrée par owner, retourne `0` (pas `null`) si aucun fichier — pas de scan disque, `File::$size` est déjà connu à l'upload.
- `FileSizeFormatter` (nouveau service, `src/Service/`) : formatte un nombre d'octets en Ko/Mo/Go/To lisible (virgule française, `1,5 Ko`), testé unitairement en isolation (`TestCase`, pas de kernel).
- Décision : les fichiers neutralisés (#278) comptent dans le total — ils occupent toujours l'espace disque réel, seule leur interprétation MIME est neutralisée. `Media` (vignettes/previews) n'a pas de champ `size` en base, donc hors périmètre naturellement.
- Tests : `FileRepositoryTest` (0 fichier, somme multi-fichiers, isolation entre owners), `FileSizeFormatterTest` (paliers o/Ko/Mo/Go/To), `DashboardTest` mis à jour (l'ancien test vérifiait littéralement le placeholder — remplacé par une vérification du calcul réel bout-en-bout).

## ✅ Espace « Mes partages » pour l'invité (2026-07-23, #273, branche `feature/273-mes-partages`)

- Nouvelle page `/mes-partages` listant les `Share` actifs (non révoqués, non expirés) dont l'utilisateur connecté est le `guest` — seule porte d'entrée jusqu'ici était le lien contenu dans l'email de notification.
Expand Down
4 changes: 3 additions & 1 deletion src/Controller/Web/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Repository\FileRepository;
use App\Repository\FolderRepository;
use App\Repository\ShareRepository;
use App\Service\FileSizeFormatter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
Expand All @@ -24,6 +25,7 @@ public function __construct(
private readonly FolderRepository $folderRepository,
private readonly FileRepository $fileRepository,
private readonly ShareRepository $shareRepository,
private readonly FileSizeFormatter $fileSizeFormatter,
) {}

#[Route('/', name: 'app_home')]
Expand Down Expand Up @@ -68,7 +70,7 @@ public function index(): Response
'totalCount' => $folderCount + $fileCount,
'activeSharesCount' => $activeSharesCount,
'recentItems' => $recentItems,
'storageUsedLabel' => 'Calcul à implémenter',
'storageUsedLabel' => $this->fileSizeFormatter->format($this->fileRepository->sumSizeByOwner($user)),
]);
}
}
13 changes: 13 additions & 0 deletions src/Repository/FileRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ public function findRecentByOwner(User $owner, int $limit = 5): array
->getResult();
}

/** Somme des tailles (octets) de tous les fichiers d'un owner. */
public function sumSizeByOwner(User $owner): int
{
$result = $this->createQueryBuilder('f')
->select('SUM(f.size)')
->andWhere('IDENTITY(f.owner) = :ownerId')
->setParameter('ownerId', $owner->getId()->toBinary())
->getQuery()
->getSingleScalarResult();

return $result !== null ? (int) $result : 0;
}

public function findWithoutMedia(): array
{
return $this->createQueryBuilder('f')
Expand Down
25 changes: 25 additions & 0 deletions src/Service/FileSizeFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace App\Service;

final class FileSizeFormatter
{
private const array UNITS = ['o', 'Ko', 'Mo', 'Go', 'To'];

public function format(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . ' o';
}

$power = min((int) floor(log($bytes, 1024)), count(self::UNITS) - 1);
$value = $bytes / (1024 ** $power);
$rounded = round($value, 2);

$formatted = rtrim(rtrim(number_format($rounded, 2, ',', ''), '0'), ',');

return $formatted . ' ' . self::UNITS[$power];
}
}
90 changes: 90 additions & 0 deletions tests/Repository/FileRepositoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace App\Tests\Repository;

use App\Entity\File;
use App\Entity\Folder;
use App\Entity\User;
use App\Repository\FileRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

final class FileRepositoryTest extends KernelTestCase
{
private EntityManagerInterface $em;
private FileRepository $repository;

protected function setUp(): void
{
self::bootKernel();
$this->em = static::getContainer()->get(EntityManagerInterface::class);
$this->repository = static::getContainer()->get(FileRepository::class);

$conn = $this->em->getConnection();
$conn->executeStatement('SET FOREIGN_KEY_CHECKS=0');
$conn->executeStatement('DELETE FROM files');
$conn->executeStatement('DELETE FROM folders');
$conn->executeStatement('DELETE FROM users');
$conn->executeStatement('SET FOREIGN_KEY_CHECKS=1');
$this->em->clear();
}

private function createUser(string $email): User
{
$user = new User($email, 'Test User');
$user->setPassword('irrelevant-hash');
$this->em->persist($user);
$this->em->flush();

return $user;
}

private function createFile(User $owner, Folder $folder, string $name, int $size): File
{
$file = new File($name, 'application/octet-stream', $size, 'irrelevant/path', $folder, $owner);
$this->em->persist($file);
$this->em->flush();

return $file;
}

public function testSumSizeByOwnerReturnsZeroWhenNoFiles(): void
{
$owner = $this->createUser('owner-empty@example.com');

$this->assertSame(0, $this->repository->sumSizeByOwner($owner));
}

public function testSumSizeByOwnerSumsMultipleFilesOfDifferentSizes(): void
{
$owner = $this->createUser('owner-sum@example.com');
$folder = new Folder('Uploads', $owner);
$this->em->persist($folder);
$this->em->flush();

$this->createFile($owner, $folder, 'a.txt', 100);
$this->createFile($owner, $folder, 'b.txt', 250);
$this->createFile($owner, $folder, 'c.txt', 4096);

$this->assertSame(4446, $this->repository->sumSizeByOwner($owner));
}

public function testSumSizeByOwnerExcludesOtherOwnersFiles(): void
{
$owner = $this->createUser('owner-isolated@example.com');
$other = $this->createUser('owner-other@example.com');

$ownerFolder = new Folder('Uploads', $owner);
$otherFolder = new Folder('Uploads', $other);
$this->em->persist($ownerFolder);
$this->em->persist($otherFolder);
$this->em->flush();

$this->createFile($owner, $ownerFolder, 'mine.txt', 500);
$this->createFile($other, $otherFolder, 'not-mine.txt', 999999);

$this->assertSame(500, $this->repository->sumSizeByOwner($owner));
}
}
53 changes: 53 additions & 0 deletions tests/Service/FileSizeFormatterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace App\Tests\Service;

use App\Service\FileSizeFormatter;
use PHPUnit\Framework\TestCase;

final class FileSizeFormatterTest extends TestCase
{
private FileSizeFormatter $formatter;

protected function setUp(): void
{
$this->formatter = new FileSizeFormatter();
}

public function testFormatZeroBytes(): void
{
$this->assertSame('0 o', $this->formatter->format(0));
}

public function testFormatBytesBelowOneKilobyte(): void
{
$this->assertSame('512 o', $this->formatter->format(512));
}

public function testFormatExactKilobyte(): void
{
$this->assertSame('1 Ko', $this->formatter->format(1024));
}

public function testFormatKilobytesWithDecimal(): void
{
$this->assertSame('1,5 Ko', $this->formatter->format(1536));
}

public function testFormatMegabytes(): void
{
$this->assertSame('2 Mo', $this->formatter->format(2 * 1024 * 1024));
}

public function testFormatGigabytes(): void
{
$this->assertSame('1,25 Go', $this->formatter->format((int) (1.25 * 1024 * 1024 * 1024)));
}

public function testFormatTerabytes(): void
{
$this->assertSame('3 To', $this->formatter->format(3 * 1024 * 1024 * 1024 * 1024));
}
}
25 changes: 22 additions & 3 deletions tests/Web/DashboardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,38 @@ public function testDashboardShowsThreeStatCards(): void
$this->assertStringContainsString('hc-stat-card', $this->client->getResponse()->getContent());
}

// --- Storage Card (Placeholder) ---
// --- Storage Card ---

public function testDashboardStorageCardShowsPlaceholder(): void
public function testDashboardStorageCardShowsZeroWhenNoFiles(): void
{
$this->createUser();
$this->login();

$this->client->request('GET', '/');
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString('Calcul à implémenter', $content);
$this->assertStringContainsString('0 o', $content);
$this->assertStringContainsString('hc-stat-value--placeholder', $content);
}

public function testDashboardStorageCardShowsFormattedTotalSize(): void
{
$this->createUser();
$this->login();
$user = $this->em->getRepository(User::class)->findOneBy(['email' => 'dashboard@example.com']);

$folder = new Folder('Uploads', $user);
$this->em->persist($folder);
$this->em->flush();

$file = new File('a.txt', 'text/plain', 1536, 'irrelevant/path', $folder, $user);
$this->em->persist($file);
$this->em->flush();

$this->client->request('GET', '/');
$content = $this->client->getResponse()->getContent();
$this->assertStringContainsString('1,5 Ko', $content);
}

// --- File & Folder Counts ---

public function testDashboardShowsFileAndFolderCounts(): void
Expand Down
Loading