2023-03-19 17:57:55 +00:00
|
|
|
|
2016-11-30 11:05:10 +01:00
|
|
|
using System.Security.Claims;
|
2023-03-19 17:57:55 +00:00
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2026-08-20 20:50:52 +01:00
|
|
|
using static Yavsc.Constants;
|
2016-09-23 12:26:50 +02:00
|
|
|
|
2026-06-10 16:59:23 +01:00
|
|
|
namespace Yavsc.Blogs.Controllers
|
2016-09-23 12:26:50 +02:00
|
|
|
{
|
2019-09-05 15:13:10 +01:00
|
|
|
using Yavsc.Attributes.Validation;
|
2026-06-10 16:59:23 +01:00
|
|
|
using Yavsc.Models;
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
using Yavsc.Models.Blog;
|
|
|
|
|
using Yavsc.Models.Billing;
|
2025-02-14 00:20:35 +00:00
|
|
|
using Yavsc.Exceptions;
|
2025-02-17 23:56:28 +00:00
|
|
|
using Yavsc.Server.Helpers;
|
|
|
|
|
using Yavsc.Abstract.Helpers;
|
2026-06-10 16:59:23 +01:00
|
|
|
using Yavsc.Server.Models.FileSystem;
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
using Yavsc.ViewModels.UserFiles;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2026-09-17 22:22:18 +01:00
|
|
|
using Microsoft.Extensions.Options;
|
2016-11-30 11:05:10 +01:00
|
|
|
|
2026-06-20 18:43:01 +01:00
|
|
|
[Authorize,Route(APIPrefix + "/fs")]
|
2019-09-08 05:17:21 +01:00
|
|
|
public partial class FileSystemApiController : Controller
|
2016-09-23 12:26:50 +02:00
|
|
|
{
|
2020-10-09 19:35:39 +01:00
|
|
|
readonly ApplicationDbContext dbContext;
|
|
|
|
|
private readonly IAuthorizationService AuthorizationService;
|
|
|
|
|
private readonly ILogger _logger;
|
2026-09-17 22:22:18 +01:00
|
|
|
private readonly SiteSettings siteSettings;
|
2018-01-26 13:51:09 +01:00
|
|
|
|
2016-09-23 12:26:50 +02:00
|
|
|
public FileSystemApiController(ApplicationDbContext context,
|
2026-08-20 20:50:52 +01:00
|
|
|
IAuthorizationService authorizationService,
|
2026-09-17 22:22:18 +01:00
|
|
|
ILoggerFactory loggerFactory,
|
|
|
|
|
IOptions<SiteSettings> siteSettings)
|
2016-09-23 12:26:50 +02:00
|
|
|
|
|
|
|
|
{
|
|
|
|
|
AuthorizationService = authorizationService;
|
2016-11-30 11:05:10 +01:00
|
|
|
dbContext = context;
|
2019-09-08 05:17:21 +01:00
|
|
|
_logger = loggerFactory.CreateLogger<FileSystemApiController>();
|
2026-09-17 22:22:18 +01:00
|
|
|
this.siteSettings = siteSettings.Value;
|
2016-09-23 12:26:50 +02:00
|
|
|
}
|
2017-04-10 23:54:06 +02:00
|
|
|
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
// GET: api/v1/fs
|
2016-09-23 12:26:50 +02:00
|
|
|
[HttpGet()]
|
|
|
|
|
public IActionResult Get()
|
|
|
|
|
{
|
|
|
|
|
return GetDir(null);
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
// GET: api/v1/fs/{subdir}
|
|
|
|
|
// Liste les fichiers du sous-répertoire personnel de l'utilisateur,
|
|
|
|
|
// en enrichissant chaque entrée de l'identifiant de métadonnées
|
|
|
|
|
// UploadedFile.Id quand une ligne existe (cf. POST ci-dessous).
|
2018-07-30 09:37:35 +02:00
|
|
|
[HttpGet("{*subdir}")]
|
2019-09-05 15:13:10 +01:00
|
|
|
public IActionResult GetDir([ValidRemoteUserFilePath] string subdir="")
|
2026-08-20 20:50:52 +01:00
|
|
|
{
|
2019-09-05 15:13:10 +01:00
|
|
|
if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState);
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
var uid = User.GetUserId();
|
|
|
|
|
var files = FileSystemHelpers.GetUserFiles(siteSettings, uid, subdir);
|
|
|
|
|
EnrichWithFileIds(files, uid, subdir);
|
2016-11-14 12:17:07 +01:00
|
|
|
return Ok(files);
|
2016-09-23 12:26:50 +02:00
|
|
|
}
|
|
|
|
|
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
// Reporte l'UploadedFile.Id (quand il existe) sur chaque entrée
|
|
|
|
|
// retournée par la liste, pour que le client lourd puisse attacher
|
|
|
|
|
// le fichier à un devis/demande par référence.
|
|
|
|
|
void EnrichWithFileIds(UserDirectoryInfo dir, string ownerId, string subdir)
|
|
|
|
|
{
|
|
|
|
|
if (dir?.Files == null || dir.Files.Length == 0) return;
|
|
|
|
|
var relPaths = dir.Files
|
|
|
|
|
.Select(f => RelPath(subdir, f.Name))
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
var byPath = dbContext.UploadedFiles
|
|
|
|
|
.Where(u => u.OwnerId == ownerId && relPaths.Contains(u.Path))
|
|
|
|
|
.ToDictionary(u => u.Path, u => u.Id);
|
|
|
|
|
foreach (var f in dir.Files)
|
|
|
|
|
{
|
|
|
|
|
var rel = RelPath(subdir, f.Name);
|
|
|
|
|
if (byPath.TryGetValue(rel, out var id)) f.Id = id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Chemin relatif au racine de l'espace perso de l'utilisateur,
|
|
|
|
|
// tel que stocké dans UploadedFile.Path (sans segment utilisateur).
|
|
|
|
|
static string RelPath(string subdir, string fileName)
|
|
|
|
|
{
|
|
|
|
|
// Normalize with '/' for cross-platform consistency in DB.
|
|
|
|
|
if (string.IsNullOrWhiteSpace(subdir)) return fileName;
|
|
|
|
|
return $"{subdir.TrimStart('/')}/{fileName}";
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-30 09:37:35 +02:00
|
|
|
[HttpPost("{*subdir}")]
|
2019-09-05 15:13:10 +01:00
|
|
|
public IActionResult Post([ValidRemoteUserFilePath] string subdir="")
|
2016-09-23 12:26:50 +02:00
|
|
|
{
|
2019-09-05 15:13:10 +01:00
|
|
|
if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState);
|
2018-03-12 23:49:05 +01:00
|
|
|
string destDir = null;
|
2025-03-04 18:05:21 +00:00
|
|
|
List<FileReceivedInfo> received = new List<FileReceivedInfo>();
|
2017-05-28 00:42:27 +02:00
|
|
|
InvalidPathException pathex = null;
|
2017-04-10 23:54:06 +02:00
|
|
|
try {
|
2026-09-17 22:22:18 +01:00
|
|
|
destDir = User.EnsureDestinationDirectory(subdir, siteSettings);
|
2017-05-28 00:42:27 +02:00
|
|
|
} catch (InvalidPathException ex) {
|
|
|
|
|
pathex = ex;
|
|
|
|
|
}
|
2026-08-20 20:50:52 +01:00
|
|
|
if (pathex!=null)
|
2026-02-28 14:22:33 +00:00
|
|
|
{
|
2019-09-08 05:17:21 +01:00
|
|
|
_logger.LogError($"invalid sub path: '{subdir}'.");
|
2023-03-19 17:57:55 +00:00
|
|
|
return BadRequest(pathex);
|
2018-07-30 09:37:35 +02:00
|
|
|
}
|
2019-09-08 05:17:21 +01:00
|
|
|
_logger.LogInformation($"Receiving files, saved in '{destDir}' (specified as '{subdir}').");
|
2026-08-20 20:50:52 +01:00
|
|
|
|
2024-11-10 23:12:02 +00:00
|
|
|
var uid = User.GetUserId();
|
2016-11-30 11:05:10 +01:00
|
|
|
var user = dbContext.Users.Single(
|
2018-01-26 13:51:09 +01:00
|
|
|
u => u.Id == uid
|
2016-11-30 11:05:10 +01:00
|
|
|
);
|
2018-01-16 13:35:54 +01:00
|
|
|
int i=0;
|
2019-09-08 05:17:21 +01:00
|
|
|
_logger.LogInformation($"Receiving {Request.Form.Files.Count} files.");
|
2026-08-20 20:50:52 +01:00
|
|
|
|
2016-11-30 11:05:10 +01:00
|
|
|
foreach (var f in Request.Form.Files)
|
2016-09-23 12:26:50 +02:00
|
|
|
{
|
2018-03-12 23:49:05 +01:00
|
|
|
var item = user.ReceiveUserFile(destDir, f);
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
if (!item.QuotaOffense)
|
|
|
|
|
{
|
|
|
|
|
// Persiste une ligne de métadonnées UploadedFile,
|
|
|
|
|
// propriétaire = l'utilisateur courant, chemin relatif
|
|
|
|
|
// à son espace perso. Upsert sur (OwnerId, Path) pour
|
|
|
|
|
// le re-téléversement (même fichier écrasé sur disque).
|
|
|
|
|
var rel = RelPath(subdir, item.FileName);
|
|
|
|
|
var existing = dbContext.UploadedFiles
|
|
|
|
|
.FirstOrDefault(u => u.OwnerId == uid && u.Path == rel);
|
|
|
|
|
if (existing == null)
|
|
|
|
|
{
|
|
|
|
|
var uploaded = new UploadedFile
|
|
|
|
|
{
|
|
|
|
|
OwnerId = uid,
|
|
|
|
|
Path = rel,
|
|
|
|
|
ContentType = f.ContentType,
|
|
|
|
|
Length = f.Length
|
|
|
|
|
};
|
|
|
|
|
dbContext.UploadedFiles.Add(uploaded);
|
|
|
|
|
dbContext.SaveChanges(User.GetUserId());
|
|
|
|
|
item.FileId = uploaded.Id;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
existing.ContentType = f.ContentType;
|
|
|
|
|
existing.Length = f.Length;
|
|
|
|
|
dbContext.SaveChanges(User.GetUserId());
|
|
|
|
|
item.FileId = existing.Id;
|
|
|
|
|
}
|
|
|
|
|
item.Length = f.Length;
|
|
|
|
|
item.ContentType = f.ContentType;
|
|
|
|
|
}
|
2018-01-26 13:51:09 +01:00
|
|
|
received.Add(item);
|
2019-09-08 05:17:21 +01:00
|
|
|
_logger.LogInformation($"Received '{item.FileName}'.");
|
2025-03-04 18:05:21 +00:00
|
|
|
if (item.QuotaOffense)
|
2018-01-26 13:51:09 +01:00
|
|
|
break;
|
|
|
|
|
i++;
|
2016-09-23 12:26:50 +02:00
|
|
|
};
|
2018-01-26 13:51:09 +01:00
|
|
|
return Ok(received);
|
2016-09-23 12:26:50 +02:00
|
|
|
}
|
2017-05-28 00:42:27 +02:00
|
|
|
|
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>
2026-09-24 16:35:36 +01:00
|
|
|
// GET: api/v1/fs/file/{fileId}
|
|
|
|
|
// Télécharge une pièce jointe référencée par son UploadedFile.Id.
|
|
|
|
|
// Autorise : le propriétaire du fichier, ou toute partie (client /
|
|
|
|
|
// fournisseur) d'un devis ou d'une demande auquel ce fichier est
|
|
|
|
|
// rattaché via EstimateAttachedFile / QueryAttachedFile. Les octets
|
|
|
|
|
// sont lus depuis l'espace perso du propriétaire, sur le disque de
|
|
|
|
|
// ce host Blogs (le host API ne possède pas ces octets).
|
|
|
|
|
[HttpGet("file/{fileId:long}")]
|
|
|
|
|
public IActionResult GetFile(long fileId)
|
|
|
|
|
{
|
|
|
|
|
var file = dbContext.UploadedFiles.FirstOrDefault(u => u.Id == fileId);
|
|
|
|
|
if (file == null) return NotFound(new { error = "file not found" });
|
|
|
|
|
|
|
|
|
|
var uid = User.GetUserId();
|
|
|
|
|
if (!CanReadFile(file, uid))
|
|
|
|
|
return Forbid();
|
|
|
|
|
|
|
|
|
|
var ownerUserName = dbContext.Users
|
|
|
|
|
.Where(u => u.Id == file.OwnerId)
|
|
|
|
|
.Select(u => u.UserName)
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
if (string.IsNullOrEmpty(ownerUserName))
|
|
|
|
|
return NotFound(new { error = "owner not found" });
|
|
|
|
|
|
|
|
|
|
var physicalPath = Path.Combine(siteSettings.Blog, ownerUserName, file.Path ?? "");
|
|
|
|
|
var fi = new FileInfo(physicalPath);
|
|
|
|
|
if (!fi.Exists) return NotFound(new { error = "file not on disk" });
|
|
|
|
|
|
|
|
|
|
var contentType = string.IsNullOrWhiteSpace(file.ContentType)
|
|
|
|
|
? "application/octet-stream" : file.ContentType;
|
|
|
|
|
var stream = fi.OpenRead();
|
|
|
|
|
return File(stream, contentType, fi.Name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Le propriétaire peut toujours lire son fichier ; sinon, il faut
|
|
|
|
|
// que le fichier soit rattaché à un devis ou une demande dont
|
|
|
|
|
// l'appelant est partie (client ou fournisseur). Les admin sont
|
|
|
|
|
// autorisés comme pour le reste du workflow.
|
|
|
|
|
bool CanReadFile(UploadedFile file, string uid)
|
|
|
|
|
{
|
|
|
|
|
if (User.IsInRole(Constants.AdminGroupName)) return true;
|
|
|
|
|
if (file.OwnerId == uid) return true;
|
|
|
|
|
|
|
|
|
|
if (dbContext.EstimateAttachedFiles.Any(a =>
|
|
|
|
|
a.FileId == file.Id &&
|
|
|
|
|
(a.Estimate.ClientId == uid || a.Estimate.OwnerId == uid)))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
if (dbContext.QueryAttachedFiles.Any(a =>
|
|
|
|
|
a.FileId == file.Id &&
|
|
|
|
|
(a.Command.ClientId == uid || a.Command.PerformerId == uid)))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-04 01:25:36 +01:00
|
|
|
[Route("/api/fsc/addquota/{uname}/{len}")]
|
2019-03-31 00:13:56 +00:00
|
|
|
[Authorize("AdministratorOnly")]
|
2019-08-22 16:06:45 +01:00
|
|
|
public IActionResult AddQuota(string uname, int len)
|
2019-03-31 00:13:56 +00:00
|
|
|
{
|
2023-03-19 17:57:55 +00:00
|
|
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
2019-09-05 15:13:10 +01:00
|
|
|
var user = dbContext.Users.FirstOrDefault(
|
2019-08-22 16:06:45 +01:00
|
|
|
u => u.UserName == uname
|
2019-03-31 00:13:56 +00:00
|
|
|
);
|
2019-09-05 15:13:10 +01:00
|
|
|
if (user==null) return new BadRequestObjectResult(new { error = "no such use" });
|
2019-03-31 00:13:56 +00:00
|
|
|
user.AddQuota(len);
|
|
|
|
|
dbContext.SaveChanges(uid);
|
|
|
|
|
return Ok(len);
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-08 05:17:21 +01:00
|
|
|
[HttpPost]
|
|
|
|
|
[Route("/api/fsc/mvftd")]
|
2019-08-21 17:23:58 +01:00
|
|
|
[Authorize()]
|
2019-09-08 05:17:21 +01:00
|
|
|
public IActionResult MoveFile([FromBody] RenameFileQuery query)
|
2019-08-21 17:23:58 +01:00
|
|
|
{
|
2019-09-05 15:13:10 +01:00
|
|
|
if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState);
|
2023-03-19 17:57:55 +00:00
|
|
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
2019-08-21 17:23:58 +01:00
|
|
|
var user = dbContext.Users.Single(
|
|
|
|
|
u => u.Id == uid
|
|
|
|
|
);
|
2026-09-17 22:22:18 +01:00
|
|
|
var info = user.MoveUserFileToDir(query.Id, query.To, siteSettings);
|
2019-09-08 05:17:21 +01:00
|
|
|
if (!info.Done) return new BadRequestObjectResult(info);
|
2025-07-07 07:49:18 +01:00
|
|
|
return Ok(new { moved = query.Id });
|
2019-08-22 16:06:45 +01:00
|
|
|
}
|
2019-08-21 17:23:58 +01:00
|
|
|
|
2019-09-08 05:17:21 +01:00
|
|
|
[HttpPost]
|
|
|
|
|
[Route("/api/fsc/mvf")]
|
2019-08-22 16:06:45 +01:00
|
|
|
[Authorize()]
|
2019-09-08 05:17:21 +01:00
|
|
|
public IActionResult RenameFile([FromBody] RenameFileQuery query)
|
2019-08-22 16:06:45 +01:00
|
|
|
{
|
2019-09-08 05:17:21 +01:00
|
|
|
if (!ModelState.IsValid) {
|
|
|
|
|
var idvr = new ValidRemoteUserFilePathAttribute();
|
|
|
|
|
|
2025-07-07 07:49:18 +01:00
|
|
|
return this.BadRequest(new { id = idvr.IsValid(query.Id), to = idvr.IsValid(query.To), errors = ModelState });
|
2019-09-08 05:17:21 +01:00
|
|
|
}
|
2025-07-07 07:49:18 +01:00
|
|
|
_logger.LogInformation($"Valid move query: {query.Id} => {query.To}");
|
2023-03-19 17:57:55 +00:00
|
|
|
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
2019-08-22 16:06:45 +01:00
|
|
|
var user = dbContext.Users.Single(
|
|
|
|
|
u => u.Id == uid
|
|
|
|
|
);
|
2019-09-04 01:25:36 +01:00
|
|
|
try {
|
2025-07-07 07:49:18 +01:00
|
|
|
if (Config.UserFilesOptions.FileProvider.GetFileInfo(Path.Combine(user.UserName, query.Id)).Exists)
|
2019-09-08 05:17:21 +01:00
|
|
|
{
|
2026-09-17 22:22:18 +01:00
|
|
|
var result = user.MoveUserFile(query.Id, query.To, siteSettings);
|
2019-09-08 05:17:21 +01:00
|
|
|
if (!result.Done) return new BadRequestObjectResult(result);
|
|
|
|
|
}
|
|
|
|
|
else {
|
2026-09-17 22:22:18 +01:00
|
|
|
var result = user.MoveUserDir(query.Id, query.To, siteSettings);
|
2019-09-08 05:17:21 +01:00
|
|
|
if (!result.Done) return new BadRequestObjectResult(result);
|
|
|
|
|
}
|
2019-09-04 01:25:36 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return new BadRequestObjectResult(
|
|
|
|
|
new FsOperationInfo {
|
|
|
|
|
Done = false,
|
2019-09-08 05:17:21 +01:00
|
|
|
ErrorCode = ErrorCode.InternalError,
|
|
|
|
|
ErrorMessage = ex.Message
|
2019-09-04 01:25:36 +01:00
|
|
|
});
|
|
|
|
|
}
|
2019-08-21 17:23:58 +01:00
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-05 21:01:44 +01:00
|
|
|
[HttpDelete("{*id}")]
|
|
|
|
|
public IActionResult RemoveDirOrFile ([ValidRemoteUserFilePath] string id)
|
2017-05-28 00:42:27 +02:00
|
|
|
{
|
2019-09-05 15:13:10 +01:00
|
|
|
if (!ModelState.IsValid) return new BadRequestObjectResult(ModelState);
|
2019-09-04 01:25:36 +01:00
|
|
|
|
|
|
|
|
var user = dbContext.Users.Single(
|
|
|
|
|
u => u.Id == User.GetUserId()
|
|
|
|
|
);
|
2019-09-05 21:01:44 +01:00
|
|
|
|
2019-09-04 01:25:36 +01:00
|
|
|
try {
|
2026-09-17 22:22:18 +01:00
|
|
|
var result = user.DeleteUserDirOrFile(id, siteSettings);
|
2019-09-04 01:25:36 +01:00
|
|
|
if (!result.Done)
|
|
|
|
|
return new BadRequestObjectResult(result);
|
|
|
|
|
}
|
2019-09-05 21:01:44 +01:00
|
|
|
|
2019-09-04 01:25:36 +01:00
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
return new BadRequestObjectResult(
|
|
|
|
|
new FsOperationInfo {
|
|
|
|
|
Done = false,
|
2019-09-08 05:17:21 +01:00
|
|
|
ErrorCode = ErrorCode.InternalError,
|
|
|
|
|
ErrorMessage = ex.Message
|
2019-09-04 01:25:36 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return Ok(new { deleted=id });
|
2017-05-28 00:42:27 +02:00
|
|
|
}
|
2020-10-18 23:00:17 +01:00
|
|
|
|
2026-08-20 20:50:52 +01:00
|
|
|
|
2016-09-23 12:26:50 +02:00
|
|
|
}
|
2019-09-04 01:25:36 +01:00
|
|
|
|
2017-04-10 23:54:06 +02:00
|
|
|
}
|