Gestion des fichiers utilisateur et pièces jointes (demande/devis)

Espace de stockage personnel géré depuis PostIt, avec attachement par
référence (sans duplication) des fichiers à la demande du client
(NominativeServiceCommand) et au devis du fournisseur (Estimate).

Backend:
- UploadedFile.OwnerId (FK ApplicationUser, nullable) ; les téléversements
  vers api/v1/fs créent/mettent à jour une ligne propriétaire.
- Tables de jointure EstimateAttachedFile et QueryAttachedFile (PK composite),
  migration UserFileAttachments.
- EstimateApiController: GET/POST/DELETE estimate/{id}/attachments
  (fournisseur uniquement, fichier devant appartenir au caller).
- FrontOfficeApiController: GET/POST/DELETE front/query/{id}/attachments
  (client uniquement).
- FileSystemApiController: GET fs/file/{fileId} sert les octets depuis le
  host Blogs (autorisation propriétaire ou partie au devis/demande via DB
  partagée) — le host API ne gère que les métadonnées, contrainte des
  conteneurs docker à disques distincts.

Client .NET:
- UserFilesApiClient (list/upload/delete/download) pointant vers BlogsApiUrl.
- DTOs UserDirectoryDto/FileEntryDto/AttachmentDto/FileReceivedDto/UploadFile.
- Méthodes d'attachement sur EstimateApiClient et FrontOfficeApiClient.

PostIt (Avalonia):
- Page « Mes fichiers » (naviguer/téléverser/télécharger/supprimer), servant
  aussi de sélecteur pour l'attachement.
- Section pièces jointes dans l'édition du devis (fournisseur) et le détail
  de la demande (client), avec ajout depuis My Files, détachement,
  téléchargement.
- Enregistrement DI + mapping ViewLocator + bouton d'accès.

Tests:
- EstimateEditionPageViewModelTests mis à jour (nouveau ctor fsClient,
  historique d'appels du stub pour les assertions post-envoi).

Nommage HTTP/méthodes en anglais, documentation en français.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Schneider 2026-09-24 16:35:36 +01:00
commit 70ab05522b
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
31 changed files with 6515 additions and 20 deletions

View file

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
namespace Yavsc.Api.Client;
/// <summary>
/// Représentation d'un répertoire de l'espace de stockage personnel,
/// renvoyée par <c>GET api/v1/fs/{subdir}</c> (host Blogs). Ne dépend pas
/// des helpers serveur : c'est un pur DTO de désérialisation.
/// </summary>
public sealed class UserDirectoryDto
{
public string? UserName { get; set; }
public string? SubPath { get; set; }
public FileEntryDto[] Files { get; set; } = Array.Empty<FileEntryDto>();
public DirEntryDto[] SubDirectories { get; set; } = Array.Empty<DirEntryDto>();
}
public sealed class FileEntryDto
{
/// <summary>Identifiant de la ligne UploadedFile, pour l'attachement par référence.</summary>
public long? Id { get; set; }
public string? Name { get; set; }
public long Size { get; set; }
public DateTime LastModified { get; set; }
}
public sealed class DirEntryDto
{
public string? Name { get; set; }
}
/// <summary>
/// Métadonnées d'une pièce jointe (fichier de l'espace perso rattaché à
/// un devis ou une demande), renvoyées par les endpoints d'attachement.
/// </summary>
public sealed class AttachmentDto
{
public long FileId { get; set; }
/// <summary>Chemin relatif dans l'espace perso du propriétaire.</summary>
public string? Path { get; set; }
public long Size { get; set; }
public string? ContentType { get; set; }
public string? OwnerId { get; set; }
}
/// <summary>
/// Métadonnées d'un fichier reçu après un téléversement multipart vers
/// <c>POST api/v1/fs/{subdir}</c>. Porte notamment l'identifiant
/// <see cref="FileId"/> de la ligne UploadedFile créée côté serveur.
/// </summary>
public sealed class FileReceivedDto
{
public long? FileId { get; set; }
public string? FileName { get; set; }
public long Length { get; set; }
public string? ContentType { get; set; }
public bool QuotaOffense { get; set; }
public bool Overridden { get; set; }
}
/// <summary>
/// Contenu d'un fichier à téléverser (nom, octets, type MIME), à l'image
/// de <see cref="BlogUploadFile"/> mais pour l'espace perso générique.
/// </summary>
public sealed record UploadFile(string FileName, byte[] Content, string? ContentType);