Le foto venivano zippate tutte in un unico allegato: oltre i 100 MB i servizi rifiutavano l'invio (MaxUploadSizeExceededException) e, costruendo tutto in RAM sul thread della UI, il dispositivo si bloccava. - Invio diviso in più email da massimo ~40 MB ciascuna - Zip scritti su disco una parte alla volta e caricati in streaming - Preparazione in background, avanzamento "parte X di N" - Richiesta se includere le foto, con il loro peso (default: solo i log) - Database inviato insieme al file -wal - Avviso subito se si è offline Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
172 lines
6.0 KiB
C#
172 lines
6.0 KiB
C#
using System.IO.Compression;
|
|
using Microsoft.Extensions.Logging;
|
|
using SteUp.Shared.Core.Dto;
|
|
using SteUp.Shared.Core.Interface.System;
|
|
|
|
namespace SteUp.Maui.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Divide l'invio dei log in parti sotto <see cref="MaxPartBytes"/>: 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.
|
|
/// </summary>
|
|
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<ZipItem> _logFiles;
|
|
private readonly List<ZipItem> _dbFiles;
|
|
private readonly List<List<ZipItem>> _imageParts;
|
|
private readonly ILogger _logger;
|
|
private readonly List<string> _currentPartFiles = [];
|
|
|
|
public int PartCount => _imageParts.Count;
|
|
|
|
internal LogExport(string workDir, List<ZipItem> logFiles, List<ZipItem> dbFiles, List<ZipItem> images,
|
|
ILogger logger)
|
|
{
|
|
_workDir = workDir;
|
|
_logFiles = logFiles;
|
|
_dbFiles = dbFiles;
|
|
_logger = logger;
|
|
_imageParts = SplitImages(images, logFiles.Concat(dbFiles).Sum(x => x.File.Length));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static List<List<ZipItem>> SplitImages(List<ZipItem> images, long firstPartReserved)
|
|
{
|
|
var parts = new List<List<ZipItem>> { 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<List<SendEmailDto.AttachmentsDto>> 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<SendEmailDto.AttachmentsDto>();
|
|
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<ZipItem> 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;
|
|
}
|
|
}
|