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; } }