diff --git a/SteUp.Maui/Core/Services/FileManager.cs b/SteUp.Maui/Core/Services/FileManager.cs index fbdea41..9764249 100644 --- a/SteUp.Maui/Core/Services/FileManager.cs +++ b/SteUp.Maui/Core/Services/FileManager.cs @@ -1,5 +1,4 @@ -using System.IO.Compression; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using SteUp.Data.LocalDb; using SteUp.Shared.Core.Dto; using SteUp.Shared.Core.Entities; @@ -343,146 +342,57 @@ public class FileManager( private static string GetInspectionKey(Ispezione ispezione) => $"{ispezione.CodMdep}_{ispezione.Data:ddMMyyyy}_{ispezione.Rilevatore.ToLower()}"; - public List GetFileForExport() + public Task GetLogExportImagesSizeAsync(CancellationToken ct = default) => + Task.Run(() => RetrieveAttachedImages().Sum(x => x.File.Length), ct); + + public Task PrepareLogExportAsync(bool includeImages, CancellationToken ct = default) { - var attachments = new List(); - - // 1) log file singolo (se ti serve ancora) - var logFile = RetrieveLogFile(); - if (!logFile.IsNullOrEmpty()) + return Task.Run(() => { - attachments.Add(new SendEmailDto.AttachmentsDto - { - FileName = $"logs_{DateTime.Today:yyyyMMdd}.zip", - FileContent = CreateZipBytes(logFile!) - }); - } + var workDir = Path.Combine(FileSystem.CacheDirectory, "log_export"); - // 2) database zip - var dbZip = CreateDatabaseZipAttachment(); - if (dbZip != null) attachments.Add(dbZip); - - // 3) Img zip - var attachedInfo = new DirectoryInfo(Path.Combine(FileSystem.AppDataDirectory, "attached")); - if (!attachedInfo.Exists) return attachments; - - var attachedFiles = attachedInfo + // Residui di un invio interrotto (app chiusa a metà): si riparte da zero. + if (Directory.Exists(workDir)) + Directory.Delete(workDir, true); + Directory.CreateDirectory(workDir); + + var logFiles = RetrieveLogFiles() + .Select(f => new LogExport.ZipItem(f, f.Name)) + .ToList(); + + // Col journal WAL le modifiche recenti stanno ancora nel file -wal: senza, il database + // inviato può non avere le ultime ispezioni. + var dbPath = dbPathProvider.GetDbPath(); + var dbFiles = new[] { dbPath, $"{dbPath}-wal" } + .Select(p => new FileInfo(p)) + .Where(f => f.Exists) + .Select(f => new LogExport.ZipItem(f, f.Name)) + .ToList(); + + var images = includeImages ? RetrieveAttachedImages() : []; + + return new LogExport(workDir, logFiles, dbFiles, images, logger); + }, ct); + } + + private static List RetrieveLogFiles() + { + var directory = new DirectoryInfo(Path.Combine(FileSystem.AppDataDirectory, "logs")); + return directory.Exists ? directory.GetFiles().ToList() : []; + } + + private static List RetrieveAttachedImages() + { + var root = new DirectoryInfo(Path.Combine(FileSystem.AppDataDirectory, "attached")); + if (!root.Exists) return []; + + // Ordine per percorso: le foto della stessa ispezione finiscono nella stessa parte. + // Il percorso relativo mantiene le cartelle dentro lo zip (separatore "/" per compatibilità). + return root .EnumerateFiles("*", SearchOption.AllDirectories) + .OrderBy(f => f.FullName, StringComparer.Ordinal) + .Select(f => new LogExport.ZipItem(f, + Path.GetRelativePath(root.FullName, f.FullName).Replace('\\', '/'))) .ToList(); - - if (attachedFiles.Count > 0) - { - attachments.Add(new SendEmailDto.AttachmentsDto - { - FileName = $"immagini_allegate_{DateTime.Today:yyyyMMdd}.zip", - FileContent = CreateZipBytes(attachedInfo.FullName, attachedFiles) - }); - } - - return attachments; } - - private static List? RetrieveLogFile() - { - var appDataPath = FileSystem.AppDataDirectory; - var targetDirectory = Path.Combine(appDataPath, "logs"); - - var directory = new DirectoryInfo(targetDirectory); - - List? files = null; - - if (directory.Exists) - files = directory.GetFiles().ToList(); - - return files; - } - - private SendEmailDto.AttachmentsDto? CreateDatabaseZipAttachment() - { - var files = new[] - { - new FileInfo(dbPathProvider.GetDbPath()) - }; - - // Filtra solo quelli esistenti - var existingFiles = files.Where(f => f.Exists).ToList(); - - if (existingFiles.Count == 0) - return null; - - using var memoryStream = new MemoryStream(); - - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) - { - foreach (var file in existingFiles) - { - var entry = archive.CreateEntry(file.Name, CompressionLevel.Optimal); - - using var entryStream = entry.Open(); - using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - fileStream.CopyTo(entryStream); - } - } - - return new SendEmailDto.AttachmentsDto - { - FileName = $"database_{DateTime.Now:yyyyMMdd_HHmm}.zip", - FileContent = memoryStream.ToArray() - }; - } - - private static byte[] CreateZipBytes(IEnumerable files) - { - using var ms = new MemoryStream(); - using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) - { - foreach (var file in files) - { - if (!file.Exists) - continue; - - // Nome dentro lo zip (evita path e collisioni minime) - var entryName = file.Name; - - var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal); - - using var entryStream = entry.Open(); - using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - fileStream.CopyTo(entryStream); - } - } - - return ms.ToArray(); - } - - private static byte[] CreateZipBytes(string rootDir, IEnumerable files) - { - using var ms = new MemoryStream(); - - using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) - { - foreach (var file in files) - { - if (!file.Exists) - continue; - - // Path relativo rispetto a rootDir -> mantiene le directory nello zip - var relativePath = Path.GetRelativePath(rootDir, file.FullName); - - // Zip usa "/" come separatore: normalizziamo per compatibilità - var entryName = relativePath.Replace('\\', '/'); - - var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal); - - using var entryStream = entry.Open(); - using var fileStream = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - - fileStream.CopyTo(entryStream); - } - } - - return ms.ToArray(); - } -} \ No newline at end of file +} diff --git a/SteUp.Maui/Core/Services/LogExport.cs b/SteUp.Maui/Core/Services/LogExport.cs new file mode 100644 index 0000000..f0f87d0 --- /dev/null +++ b/SteUp.Maui/Core/Services/LogExport.cs @@ -0,0 +1,171 @@ +using System.IO.Compression; +using Microsoft.Extensions.Logging; +using SteUp.Shared.Core.Dto; +using SteUp.Shared.Core.Interface.System; + +namespace SteUp.Maui.Core.Services; + +/// +/// Divide l'invio dei log in parti sotto : la prima porta log, database +/// e le prime immagini, le successive solo immagini. Gli zip vengono scritti su disco (mai in RAM) +/// una parte alla volta, così lo spazio occupato resta quello di una sola parte. +/// +internal sealed class LogExport : ILogExport +{ + // Ben sotto il max-file-size dei servizi (100 MB) e i limiti tipici dei server di posta. + // Una parte piccola regge anche l'upload da rete mobile entro il timeout HTTP del client. + internal const long MaxPartBytes = 40L * 1024 * 1024; + + private const int BufferSize = 81920; + + internal readonly record struct ZipItem(FileInfo File, string EntryName); + + private readonly string _workDir; + private readonly List _logFiles; + private readonly List _dbFiles; + private readonly List> _imageParts; + private readonly ILogger _logger; + private readonly List _currentPartFiles = []; + + public int PartCount => _imageParts.Count; + + internal LogExport(string workDir, List logFiles, List dbFiles, List images, + ILogger logger) + { + _workDir = workDir; + _logFiles = logFiles; + _dbFiles = dbFiles; + _logger = logger; + _imageParts = SplitImages(images, logFiles.Concat(dbFiles).Sum(x => x.File.Length)); + } + + /// + /// Riempie le parti in ordine (le foto di un'ispezione restano vicine). La prima parte parte + /// già occupata da log e database: uso le dimensioni non compresse, una stima per eccesso. + /// Un file più grande di una parte finisce da solo in una parte sua. + /// + private static List> SplitImages(List images, long firstPartReserved) + { + var parts = new List> { new() }; + var currentSize = firstPartReserved; + + foreach (var image in images) + { + var length = image.File.Length; + if (currentSize > 0 && currentSize + length > MaxPartBytes) + { + parts.Add([]); + currentSize = 0; + } + + parts[^1].Add(image); + currentSize += length; + } + + return parts; + } + + public Task> BuildPartAsync(int index, CancellationToken ct = default) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, PartCount); + + // Task.Run: la compressione non deve bloccare il thread della UI. + return Task.Run(() => + { + DeleteCurrentPartFiles(); + + var attachments = new List(); + var date = DateTime.Today.ToString("yyyyMMdd"); + + if (index == 0) + { + if (_logFiles.Count > 0) + attachments.Add(WriteZip($"logs_{date}.zip", _logFiles, CompressionLevel.Optimal, ct)); + + if (_dbFiles.Count > 0) + attachments.Add(WriteZip($"database_{DateTime.Now:yyyyMMdd_HHmm}.zip", _dbFiles, + CompressionLevel.Optimal, ct)); + } + + var images = _imageParts[index]; + if (images.Count > 0) + { + var suffix = PartCount > 1 ? $"_parte{index + 1}di{PartCount}" : string.Empty; + + // JPEG già compressi: ricomprimerli costa CPU e non riduce quasi niente. + attachments.Add(WriteZip($"immagini_allegate_{date}{suffix}.zip", images, + CompressionLevel.NoCompression, ct)); + } + + return attachments; + }, ct); + } + + private SendEmailDto.AttachmentsDto WriteZip(string fileName, List items, CompressionLevel level, + CancellationToken ct) + { + var path = Path.Combine(_workDir, fileName); + _currentPartFiles.Add(path); + + using (var zipStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize)) + using (var archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + foreach (var item in items) + { + ct.ThrowIfCancellationRequested(); + + try + { + // FileShare.ReadWrite: log e database possono essere ancora aperti in scrittura. + using var source = item.File.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + var entry = archive.CreateEntry(item.EntryName, level); + entry.LastWriteTime = item.File.LastWriteTime; + + using var entryStream = entry.Open(); + source.CopyTo(entryStream, BufferSize); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + // Un file sparito o bloccato non deve far saltare tutto l'invio. + _logger.LogWarning(e, "Export log: file {File} saltato", item.File.FullName); + } + } + } + + return new SendEmailDto.AttachmentsDto { FileName = fileName, FilePath = path }; + } + + private void DeleteCurrentPartFiles() + { + foreach (var path in _currentPartFiles) + { + try + { + File.Delete(path); + } + catch (Exception e) + { + _logger.LogWarning(e, "Export log: impossibile eliminare {File}", path); + } + } + + _currentPartFiles.Clear(); + } + + public ValueTask DisposeAsync() + { + try + { + if (Directory.Exists(_workDir)) + Directory.Delete(_workDir, true); + } + catch (Exception e) + { + _logger.LogWarning(e, "Export log: impossibile eliminare {Dir}", _workDir); + } + + return ValueTask.CompletedTask; + } +} diff --git a/SteUp.Shared/Components/Pages/UserPage.razor b/SteUp.Shared/Components/Pages/UserPage.razor index 45ad9fd..7ae9c69 100644 --- a/SteUp.Shared/Components/Pages/UserPage.razor +++ b/SteUp.Shared/Components/Pages/UserPage.razor @@ -1,5 +1,6 @@ @page "/user" @attribute [Authorize] +@using Microsoft.Extensions.Logging @using SteUp.Shared.Components.Layout @using SteUp.Shared.Components.Layout.Overlay @using SteUp.Shared.Components.SingleElements @@ -14,6 +15,7 @@ @inject IGenericSystemService GenericSystemService @inject IFileManager FileManager @inject IIntegryApiService IntegryApiService +@inject ILogger Logger @@ -87,6 +89,9 @@ + @code { private bool IsLoggedIn { get; set; } @@ -95,6 +100,8 @@ private string? CodHash { get; set; } = ""; private ConfirmMessageBox _logoutBox = null!; + private ConfirmMessageBox _imagesBox = null!; + private string _imagesBoxMessage = string.Empty; private Core.Helpers.StatusBadgeHelper.BadgeSpec ConnessioneBadge => NetworkService.ConnectionAvailable ? new Core.Helpers.StatusBadgeHelper.BadgeSpec("success", "ri-cloud-line", "Online") @@ -109,33 +116,69 @@ private async Task ExportLog() { - _progressMessage = "Invio dei log…"; + if (VisibleOverlay) return; + + if (!NetworkService.ConnectionAvailable) + { + Snackbar.Add("Sei offline: per inviare i log serve la connessione.", Severity.Warning); + return; + } + + var includeImages = await AskIncludeImages(); + if (includeImages is null) return; + + _progressMessage = "Preparazione dei log…"; VisibleOverlay = true; StateHasChanged(); + var partCount = 0; + var sentParts = 0; + try { + // Un'email per parte: ogni parte resta sotto i limiti di upload dei servizi. + await using var export = await FileManager.PrepareLogExportAsync(includeImages.Value); + partCount = export.PartCount; + var profiloAzienda = LocalStorage.GetString("codHash"); + var subject = $"SteUP - Log del {DateTime.Today:d} di {UserSession.User.Username}"; + var body = $"Username: {UserSession.User.Username}
" + + $"Profilo azienda: {profiloAzienda}
" + + $"ProfileDb: {UserSession.ProfileDb}
" + + $"Versione app: {GenericSystemService.GetCurrentAppVersion()}"; - var email = new SendEmailDto + for (var i = 0; i < partCount; i++) { - FromName = "Integry Log", - To = "developer@integry.it", - Subject = $"SteUP - Log del {DateTime.Today:d} di {UserSession.User.Username}", - IsHtml = true, - MsgText = $"Username: {UserSession.User.Username}
" + - $"Profilo azienda: {profiloAzienda}
" + - $"ProfileDb: {UserSession.ProfileDb}
" + - $"Versione app: {GenericSystemService.GetCurrentAppVersion()}", - Attachments = FileManager.GetFileForExport() - }; + var multiPart = partCount > 1; - await IntegryApiService.SendEmail(email); - Snackbar.Add("Log inviati allo sviluppo", Severity.Success); + _progressMessage = multiPart ? $"Invio dei log… parte {i + 1} di {partCount}" : "Invio dei log…"; + StateHasChanged(); + + var attachments = await export.BuildPartAsync(i); + + await IntegryApiService.SendEmail(new SendEmailDto + { + FromName = "Integry Log", + To = "developer@integry.it", + Subject = multiPart ? $"{subject} (parte {i + 1}/{partCount})" : subject, + IsHtml = true, + MsgText = multiPart ? $"{body}
Parte: {i + 1} di {partCount}" : body, + Attachments = attachments + }); + + sentParts++; + } + + Snackbar.Add(partCount > 1 ? $"Log inviati allo sviluppo in {partCount} parti" : "Log inviati allo sviluppo", + Severity.Success); } - catch + catch (Exception e) { - Snackbar.Add("Invio dei log non riuscito. Controlla la connessione e riprova.", Severity.Error); + Logger.LogError(e, "Invio dei log interrotto dopo {Sent} parti su {Total}", sentParts, partCount); + Snackbar.Add(sentParts > 0 + ? $"Invio interrotto: inviate {sentParts} parti su {partCount}. Controlla la connessione e riprova." + : "Invio dei log non riuscito. Controlla la connessione e riprova.", + Severity.Error); } finally { @@ -145,6 +188,49 @@ } } + /// + /// Chiede se allegare le foto, indicando quanto pesano. Null = invio annullato. + /// Se sul dispositivo non ci sono foto non chiede niente. + /// + private async Task AskIncludeImages() + { + long imagesSize; + + _progressMessage = "Controllo delle foto…"; + VisibleOverlay = true; + StateHasChanged(); + + try + { + imagesSize = await FileManager.GetLogExportImagesSizeAsync(); + } + catch (Exception e) + { + // Senza sapere quanto pesano, meglio un invio leggero che uno che rischia di fallire. + Logger.LogWarning(e, "Impossibile calcolare la dimensione delle foto da inviare"); + imagesSize = 0; + } + finally + { + VisibleOverlay = false; + _progressMessage = null; + StateHasChanged(); + } + + if (imagesSize == 0) return false; + + var sizeText = imagesSize < 1024 * 1024 + ? "meno di 1 MB" + : $"circa {imagesSize / (1024d * 1024):N0} MB"; + _imagesBoxMessage = $"Le foto delle ispezioni occupano {sizeText}. Servono solo se il problema " + + "riguarda le foto: senza, l'invio è molto più veloce."; + StateHasChanged(); + + // "Solo i log" è la scelta consigliata (pulsante pieno, true); "Includi le foto" -> false. + var result = await _imagesBox.ShowAsync(); + return result is null ? null : result == false; + } + private async Task UpdateDb() { _progressMessage = "Sincronizzazione…"; diff --git a/SteUp.Shared/Components/SingleElements/MessageBox/ConfirmMessageBox.razor b/SteUp.Shared/Components/SingleElements/MessageBox/ConfirmMessageBox.razor index 143756f..aa8a4ad 100644 --- a/SteUp.Shared/Components/SingleElements/MessageBox/ConfirmMessageBox.razor +++ b/SteUp.Shared/Components/SingleElements/MessageBox/ConfirmMessageBox.razor @@ -2,8 +2,9 @@ che conseguenze ha: davanti a un'azione irreversibile la domanda deve essere leggibile prima del testo, non dedotta da esso. - YesText -> true, NoText -> false, chiusura senza scelta -> null. *@ - + YesText -> true, NoText o chiusura senza scelta -> null. + AltText (opzionale) aggiunge una seconda scelta valida -> false. *@ + @Message @@ -20,6 +21,7 @@ [Parameter] public string Message { get; set; } = string.Empty; [Parameter] public string YesText { get; set; } = string.Empty; [Parameter] public string NoText { get; set; } = "Annulla"; + [Parameter] public string? AltText { get; set; } [Parameter] public Color YesColor { get; set; } = Color.Primary; private MudMessageBox? _confirmDelete; diff --git a/SteUp.Shared/Core/Dto/SendEmailDto.cs b/SteUp.Shared/Core/Dto/SendEmailDto.cs index 387fa6c..b2d2b15 100644 --- a/SteUp.Shared/Core/Dto/SendEmailDto.cs +++ b/SteUp.Shared/Core/Dto/SendEmailDto.cs @@ -6,28 +6,31 @@ public class SendEmailDto { [JsonPropertyName("from")] public string? From { get; set; } - + [JsonPropertyName("fromName")] public string? FromName { get; set; } - + [JsonPropertyName("to")] public string? To { get; set; } - + [JsonPropertyName("subject")] public string? Subject { get; set; } - + [JsonPropertyName("msgText")] public string? MsgText { get; set; } - + [JsonPropertyName("html")] public bool IsHtml { get; set; } - - [JsonPropertyName("attachments")] + + // Gli allegati viaggiano come parti multipart "allegati", non dentro il JSON della richiesta. + [JsonIgnore] public List? Attachments { get; set; } public class AttachmentsDto { public string FileName { get; set; } = string.Empty; - public byte[] FileContent { get; set; } = []; + + // Il file resta su disco e viene letto in streaming durante l'upload. + public string FilePath { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/SteUp.Shared/Core/Helpers/FileHttpContent.cs b/SteUp.Shared/Core/Helpers/FileHttpContent.cs new file mode 100644 index 0000000..23aef89 --- /dev/null +++ b/SteUp.Shared/Core/Helpers/FileHttpContent.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace SteUp.Shared.Core.Helpers; + +/// +/// Contenuto HTTP letto dal file durante l'invio, senza caricarlo in RAM. Il file viene riaperto +/// a ogni serializzazione, così regge anche un eventuale nuovo tentativo del client +/// (es. dopo il refresh del token). +/// +public sealed class FileHttpContent : HttpContent +{ + private const int BufferSize = 81920; + + private readonly string _path; + + public FileHttpContent(string path, string mediaType) + { + _path = path; + Headers.ContentType = new MediaTypeHeaderValue(mediaType); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + SerializeToStreamAsync(stream, context, CancellationToken.None); + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, + CancellationToken cancellationToken) + { + await using var file = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read, + BufferSize, useAsync: true); + await file.CopyToAsync(stream, BufferSize, cancellationToken); + } + + protected override bool TryComputeLength(out long length) + { + length = new FileInfo(_path).Length; + return true; + } +} diff --git a/SteUp.Shared/Core/Interface/System/IFileManager.cs b/SteUp.Shared/Core/Interface/System/IFileManager.cs index d7f95d6..e212fef 100644 --- a/SteUp.Shared/Core/Interface/System/IFileManager.cs +++ b/SteUp.Shared/Core/Interface/System/IFileManager.cs @@ -26,5 +26,6 @@ public interface IFileManager Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(byte[] bytes, string fileName, CancellationToken ct = default); - List GetFileForExport(); + Task GetLogExportImagesSizeAsync(CancellationToken ct = default); + Task PrepareLogExportAsync(bool includeImages, CancellationToken ct = default); } \ No newline at end of file diff --git a/SteUp.Shared/Core/Interface/System/ILogExport.cs b/SteUp.Shared/Core/Interface/System/ILogExport.cs new file mode 100644 index 0000000..a1c4561 --- /dev/null +++ b/SteUp.Shared/Core/Interface/System/ILogExport.cs @@ -0,0 +1,15 @@ +using SteUp.Shared.Core.Dto; + +namespace SteUp.Shared.Core.Interface.System; + +/// +/// Invio dei log diviso in parti: ogni parte diventa un'email separata, sotto i limiti di upload +/// dei servizi. Gli zip di una parte vengono creati solo quando la si chiede e cancellati alla +/// parte successiva; il dispose elimina tutto quello che resta su disco. +/// +public interface ILogExport : IAsyncDisposable +{ + int PartCount { get; } + + Task> BuildPartAsync(int index, CancellationToken ct = default); +} diff --git a/SteUp.Shared/Core/Services/IntegryApiService.cs b/SteUp.Shared/Core/Services/IntegryApiService.cs index 5fd1600..ae4c5ba 100644 --- a/SteUp.Shared/Core/Services/IntegryApiService.cs +++ b/SteUp.Shared/Core/Services/IntegryApiService.cs @@ -1,9 +1,9 @@ -using System.Net.Http.Headers; -using System.Text; +using System.Text; using System.Text.Json; using IntegryApiClient.Core.Domain.RestClient.Contacts; using Microsoft.Extensions.Logging; using SteUp.Shared.Core.Dto; +using SteUp.Shared.Core.Helpers; using SteUp.Shared.Core.Interface.IntegryApi; namespace SteUp.Shared.Core.Services; @@ -37,21 +37,18 @@ public class IntegryApiService( public async Task SendEmail(SendEmailDto sendEmail) { - var content = new MultipartFormDataContent(); - + using var content = new MultipartFormDataContent(); + try { - if (sendEmail.Attachments != null) + foreach (var a in sendEmail.Attachments ?? []) { - foreach (var a in sendEmail.Attachments) - { - var fileContent = new ByteArrayContent(a.FileContent); - fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); - content.Add(fileContent, "allegati", a.FileName); - } + var mediaType = a.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) + ? "application/zip" + : "application/octet-stream"; + content.Add(new FileHttpContent(a.FilePath, mediaType), "allegati", a.FileName); } - sendEmail.Attachments = null; content.Add( new StringContent( JsonSerializer.Serialize(sendEmail), @@ -69,9 +66,5 @@ public class IntegryApiService( logger.LogError(e, e.Message); throw; } - finally - { - content.Dispose(); - } } } \ No newline at end of file