Premier commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SIFinance.Shared;
|
||||
|
||||
namespace SIFinance.Api;
|
||||
|
||||
public record SharePointUploadResult(string WebUrl, string ItemId, long Size);
|
||||
|
||||
// Dépose un fichier dans un sous-dossier du drive SharePoint configuré (SharePoint:DriveId)
|
||||
// — un dossier par module (Achats / Sous-Traitants / Contrat Etudiant), créés lors du
|
||||
// provisioning du site. Réutilise le jeton Graph "app-only" déjà mis en cache par
|
||||
// EntraGraphTokenProvider plutôt que ClientSecretCredential/GraphServiceClient : plus
|
||||
// simple pour un simple PUT de contenu, et le jeton couvre déjà Sites.ReadWrite.All.
|
||||
public class SharePointUploadService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
EntraGraphTokenProvider tokenProvider,
|
||||
IOptions<SharePointOptions> options)
|
||||
{
|
||||
// Upload "simple" (PUT direct) : suffisant jusqu'à 4 Mo, ce qui couvre largement les
|
||||
// justificatifs (devis, factures, bons de commande scannés). Au-delà, Graph exige une
|
||||
// session d'upload par blocs — pas implémenté ici, hors scope du premier jet.
|
||||
// cheminDossier peut contenir plusieurs segments (ex: "Achats/Informatique/Jean Dupont") —
|
||||
// Graph crée automatiquement les dossiers intermédiaires manquants lors d'un upload
|
||||
// vers un chemin qui n'existe pas encore, pas besoin de les créer explicitement.
|
||||
public async Task<SharePointUploadResult> UploadAsync(string cheminDossier, string fileName, Stream content, string? contentType, CancellationToken ct)
|
||||
{
|
||||
var opts = options.Value;
|
||||
if (string.IsNullOrWhiteSpace(opts.DriveId))
|
||||
throw new InvalidOperationException("Configuration SharePoint incomplète (DriveId) — voir appsettings.Development.json.");
|
||||
|
||||
var token = await tokenProvider.GetTokenAsync(ct);
|
||||
var http = httpClientFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var safeName = Uri.EscapeDataString(fileName);
|
||||
var url = $"https://graph.microsoft.com/v1.0/drives/{opts.DriveId}/root:/{cheminDossier}/{safeName}:/content?@microsoft.graph.conflictBehavior=rename";
|
||||
|
||||
using var streamContent = new StreamContent(content);
|
||||
streamContent.Headers.ContentType = new MediaTypeHeaderValue(string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType);
|
||||
|
||||
var response = await http.PutAsync(url, streamContent, ct);
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException($"Échec de l'upload SharePoint ({response.StatusCode}) : {body}");
|
||||
|
||||
var json = JsonDocument.Parse(body).RootElement;
|
||||
var itemWebUrl = json.GetProperty("webUrl").GetString()!;
|
||||
var itemId = json.GetProperty("id").GetString()!;
|
||||
var size = json.TryGetProperty("size", out var sizeProp) ? sizeProp.GetInt64() : content.Length;
|
||||
|
||||
// L'URL brute de l'item n'est accessible qu'aux membres du groupe SharePoint
|
||||
// sous-jacent — un lien de partage "organisation" permet à n'importe quel
|
||||
// utilisateur du tenant, connecté avec son compte Microsoft, de l'ouvrir sans
|
||||
// avoir besoin d'être membre du groupe. On le préfère donc à l'URL brute quand
|
||||
// Graph accepte de le créer (si ça échoue, on retombe sur l'URL brute plutôt
|
||||
// que de faire échouer tout l'upload).
|
||||
var lienOrganisation = await CreerLienOrganisationAsync(opts.DriveId, itemId, http, ct);
|
||||
return new SharePointUploadResult(lienOrganisation ?? itemWebUrl, itemId, size);
|
||||
}
|
||||
|
||||
private static async Task<string?> CreerLienOrganisationAsync(string driveId, string itemId, HttpClient http, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"https://graph.microsoft.com/v1.0/drives/{driveId}/items/{itemId}/createLink",
|
||||
new { type = "view", scope = "organization" }, ct);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
|
||||
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct)).RootElement;
|
||||
return json.GetProperty("link").GetProperty("webUrl").GetString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user