Fix gestione allegati e creato metodo di esportazione log

This commit is contained in:
2026-03-04 11:51:42 +01:00
parent 3760e38c8d
commit 2d938fb210
26 changed files with 986 additions and 384 deletions

View File

@@ -1,10 +1,14 @@
using Microsoft.Extensions.Logging;
using SteUp.Maui.Core.UtilityException;
namespace SteUp.Maui
{
public partial class App
{
public App()
public App(ILogger<App> logger)
{
InitializeComponent();
GlobalExceptionHandler.Register(logger);
}
protected override Window CreateWindow(IActivationState? activationState)

View File

@@ -1,8 +1,10 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SteUp.Data.LocalDb;
using SteUp.Data.LocalDb.EntityServices;
using SteUp.Maui.Core.Logger;
using SteUp.Maui.Core.Services;
using SteUp.Maui.Core.System;
using SteUp.Maui.Core.System.Network;
@@ -45,6 +47,7 @@ public static class CoreModule
{
builder.Services.AddSingleton<INetworkService, NetworkService>();
builder.Services.AddSingleton<IAttachedService, AttachedService>();
builder.Services.AddSingleton<IFileManager, FileManager>();
builder.Services.AddSingleton<IBarcodeReaderService, HoneywellScannerService>();
}
@@ -75,5 +78,17 @@ public static class CoreModule
builder.Services.AddSingleton<IDbInitializer, DbInitializer>();
builder.Services.AddSingleton<IIspezioniService, IspezioniService>();
}
public void RegisterLoggerServices()
{
var logPath = Path.Combine(FileSystem.AppDataDirectory, "logs");
const string logFilePrefix = "SteUp-log";
builder.Services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddProvider(new FileLoggerProvider(logPath, logFilePrefix));
loggingBuilder.SetMinimumLevel(LogLevel.Information);
});
}
}
}

View File

@@ -0,0 +1,185 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text;
namespace SteUp.Maui.Core.Logger;
public class FileLogger : ILogger
{
private readonly string _path;
private readonly string _fileNamePrefix;
private readonly string _categoryName;
private readonly Lock _lock = new();
private readonly int _retentionDays;
private string? _currentFileName;
private DateTime _currentFileDate;
private DateTime _lastCleanupDate;
public FileLogger(string path, string fileNamePrefix, string categoryName, int retentionDays = 60)
{
_path = path;
_fileNamePrefix = fileNamePrefix;
_retentionDays = retentionDays;
_lastCleanupDate = DateTime.MinValue;
_categoryName = categoryName;
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
UpdateCurrentFileName();
TryCleanOldLogs();
}
/// <summary>
/// Elimina i log più vecchi di <see cref="_retentionDays"/> giorni.
/// Viene eseguita al massimo una volta al giorno.
/// </summary>
private void ClearOldLogs()
{
try
{
var cutoff = DateTime.Now.Date.AddDays(-_retentionDays);
var logFiles = Directory.GetFiles(_path, $"{_fileNamePrefix}-*.log");
foreach (var file in logFiles)
{
try
{
var fileName = Path.GetFileNameWithoutExtension(file);
var datePart = fileName[(_fileNamePrefix.Length + 1)..];
if (!DateTime.TryParseExact(datePart, "yyyy-MM-dd",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var fileDate) || fileDate >= cutoff) continue;
File.Delete(file);
Debug.WriteLine($"[FileLogger] Log eliminato: {file}");
}
catch (Exception ex)
{
Debug.WriteLine($"[FileLogger] Errore durante l'eliminazione del file {file}: {ex.Message}");
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[FileLogger] Errore durante la pulizia dei log: {ex.Message}");
}
}
/// <summary>
/// Esegue la pulizia dei log solo se non è già stata eseguita oggi.
/// </summary>
private void TryCleanOldLogs()
{
var today = DateTime.Now.Date;
if (_lastCleanupDate == today) return;
_lastCleanupDate = today;
ClearOldLogs();
}
private void UpdateCurrentFileName()
{
var today = DateTime.Now.Date;
if (_currentFileName != null && _currentFileDate == today) return;
_currentFileDate = today;
_currentFileName = $"{_fileNamePrefix}-{today:yyyy-MM-dd}.log";
}
public IDisposable? BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
return;
try
{
lock (_lock)
{
UpdateCurrentFileName();
TryCleanOldLogs();
if (_currentFileName == null) return;
var fullPath = Path.Combine(_path, _currentFileName);
var logEntry = BuildLogEntry(logLevel, eventId, state, exception, formatter);
File.AppendAllText(fullPath, logEntry + Environment.NewLine + Environment.NewLine);
Debug.WriteLine($"[FileLogger] {logEntry}");
}
}
catch (Exception ex)
{
Debug.WriteLine($"[FileLogger] Errore durante la scrittura del log: {ex}");
}
}
private string BuildLogEntry<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var sb = new StringBuilder();
sb.Append($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]");
sb.Append($" [{GetLogLevelShort(logLevel)}]");
sb.Append($" [{_categoryName}]");
if (eventId.Id != 0 || !string.IsNullOrEmpty(eventId.Name))
sb.Append($" [{eventId}]");
sb.Append($" {formatter(state, exception)}");
if (exception != null)
AppendException(sb, exception);
return sb.ToString();
}
private static void AppendException(StringBuilder sb, Exception exception, int depth = 0)
{
while (true)
{
var indent = depth == 0 ? "" : " Inner ";
sb.AppendLine();
sb.Append($"{indent}Exception: {exception.GetType().FullName}: {exception.Message}");
if (!string.IsNullOrWhiteSpace(exception.StackTrace))
{
sb.AppendLine();
sb.Append($"{indent}StackTrace: {exception.StackTrace.Trim()}");
}
if (exception.InnerException != null)
{
exception = exception.InnerException;
depth += 1;
continue;
}
break;
}
}
private static string GetLogLevelShort(LogLevel level) => level switch
{
LogLevel.Trace => "TRC",
LogLevel.Debug => "DBG",
LogLevel.Information => "INF",
LogLevel.Warning => "WRN",
LogLevel.Error => "ERR",
LogLevel.Critical => "CRT",
_ => "???"
};
}

View File

@@ -0,0 +1,14 @@
using Microsoft.Extensions.Logging;
namespace SteUp.Maui.Core.Logger;
public class FileLoggerProvider(string path, string logFilePrefix, int retentionDays = 60)
: ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(path, logFilePrefix, categoryName, retentionDays);
}
public void Dispose() { }
}

View File

@@ -1,15 +1,13 @@
using SteUp.Shared.Core.Dto;
using SteUp.Shared.Core.Entities;
using Microsoft.Extensions.Logging;
using SteUp.Maui.Core.Utility;
using SteUp.Shared.Core.Dto;
using SteUp.Shared.Core.Helpers;
using SteUp.Shared.Core.Interface.System;
namespace SteUp.Maui.Core.Services;
public class AttachedService : IAttachedService
public class AttachedService(ILogger<FileManager> logger) : IAttachedService
{
private static string AttachedRoot =>
Path.Combine(FileSystem.CacheDirectory, "attached");
public async Task<AttachedDto?> SelectImageFromCamera()
{
var cameraPerm = await Permissions.RequestAsync<Permissions.Camera>();
@@ -27,12 +25,13 @@ public class AttachedService : IAttachedService
}
catch (Exception ex)
{
logger.LogError(ex, ex.Message);
Console.WriteLine($"Errore cattura foto: {ex.Message}");
SentrySdk.CaptureException(ex);
return null;
}
return result is null ? null : await ConvertToDto(result, AttachedDto.TypeAttached.Image);
return result is null ? null : await UtilityFile.ConvertToDto(result, AttachedDto.TypeAttached.Image);
}
public async Task<List<AttachedDto>?> SelectImageFromGallery()
@@ -48,6 +47,7 @@ public class AttachedService : IAttachedService
}
catch (Exception ex)
{
logger.LogError(ex, ex.Message);
Console.WriteLine($"Errore selezione galleria: {ex.Message}");
SentrySdk.CaptureException(ex);
return null;
@@ -58,308 +58,9 @@ public class AttachedService : IAttachedService
List<AttachedDto> returnList = [];
foreach (var fileResult in resultList)
{
returnList.Add(await ConvertToDto(fileResult, AttachedDto.TypeAttached.Image));
returnList.Add(await UtilityFile.ConvertToDto(fileResult, AttachedDto.TypeAttached.Image));
}
return returnList;
}
private static async Task<AttachedDto> ConvertToDto(FileResult file, AttachedDto.TypeAttached type)
{
var stream = await file.OpenReadAsync();
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
return new AttachedDto
{
Name = file.FileName,
Path = file.FullPath,
MimeType = file.ContentType,
DimensionBytes = ms.Length,
FileBytes = ms.ToArray(),
Type = type
};
}
private async Task<AttachedDto> ConvertToDto(FileInfo file, AttachedDto.TypeAttached type, bool isFromToUpload)
{
var (origUrl, thumbUrl) = await SaveAndCreateThumbAsync(
await File.ReadAllBytesAsync(file.FullName),
file.Name
);
return new AttachedDto
{
Name = file.Name,
Path = file.FullName,
TempPath = origUrl,
ThumbPath = thumbUrl,
Type = type,
SavedOnAppData = true,
ToUpload = isFromToUpload
};
}
private const string ToUploadFolderName = "toUpload";
private string GetInspectionBaseDir(Ispezione ispezione)
{
var baseDir = FileSystem.AppDataDirectory;
return Path.Combine(baseDir, $"attached_{GetInspectionKey(ispezione)}");
}
private string GetInspectionToUploadDir(Ispezione ispezione)
=> Path.Combine(GetInspectionBaseDir(ispezione), ToUploadFolderName);
private string GetInspectionFinalDir(Ispezione ispezione)
=> GetInspectionBaseDir(ispezione);
/// <summary>
/// Ritorna i file dell'ispezione filtrati per nome.
/// Per default include sia "final" sia "toUpload" (utile per UI).
/// </summary>
public async Task<List<AttachedDto>?> GetInspectionFiles(
Ispezione ispezione,
List<string> fileNameFilter,
bool includeToUpload,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentNullException.ThrowIfNull(fileNameFilter);
var baseDir = GetInspectionBaseDir(ispezione);
if (!Directory.Exists(baseDir)) return null;
var result = new List<AttachedDto>();
var finalDir = GetInspectionFinalDir(ispezione);
if (Directory.Exists(finalDir))
{
var finalFiles = new DirectoryInfo(finalDir)
.GetFiles("*", SearchOption.TopDirectoryOnly);
foreach (var file in finalFiles)
{
if (file.Directory?.Name == ToUploadFolderName)
continue;
if (!fileNameFilter.Contains(file.Name))
continue;
ct.ThrowIfCancellationRequested();
result.Add(await ConvertToDto(
file,
AttachedDto.TypeAttached.Image,
isFromToUpload: false));
}
}
if (!includeToUpload) return result;
var toUploadDir = GetInspectionToUploadDir(ispezione);
if (!Directory.Exists(toUploadDir)) return result;
var toUploadFiles = new DirectoryInfo(toUploadDir)
.GetFiles("*", SearchOption.TopDirectoryOnly);
foreach (var file in toUploadFiles)
{
if (!fileNameFilter.Contains(file.Name))
continue;
ct.ThrowIfCancellationRequested();
result.Add(await ConvertToDto(
file,
AttachedDto.TypeAttached.Image,
isFromToUpload: true));
}
return result;
}
/// <summary>
/// Salva SEMPRE in /toUpload.
/// </summary>
public async Task<string?> SaveInspectionFile(
Ispezione ispezione,
byte[] file,
string fileName,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentNullException.ThrowIfNull(file);
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
var toUploadDir = GetInspectionToUploadDir(ispezione);
Directory.CreateDirectory(toUploadDir);
var filePath = Path.Combine(toUploadDir, fileName);
await File.WriteAllBytesAsync(filePath, file, ct);
return filePath;
}
public Task<bool> MoveInspectionFileFromToUploadToFinal(
Ispezione ispezione,
string fileName,
bool overwrite,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
ct.ThrowIfCancellationRequested();
var toUploadDir = GetInspectionToUploadDir(ispezione);
var finalDir = GetInspectionFinalDir(ispezione);
if (!Directory.Exists(toUploadDir)) return Task.FromResult(false);
var sourcePath = Path.Combine(toUploadDir, fileName);
if (!File.Exists(sourcePath)) return Task.FromResult(false);
Directory.CreateDirectory(finalDir);
var destPath = Path.Combine(finalDir, fileName);
if (File.Exists(destPath))
{
if (!overwrite) return Task.FromResult(false);
File.Delete(destPath);
}
File.Move(sourcePath, destPath);
// Pulizia: se /toUpload resta vuota la elimino
CleanupDirectoriesIfEmpty(ispezione);
return Task.FromResult(true);
}
/// <summary>
/// Rimuove un file cercandolo prima in /toUpload e poi in final (o viceversa).
/// Default: prova a cancellare ovunque.
/// </summary>
public bool RemoveInspectionFile(
Ispezione ispezione,
string fileName,
bool removeAlsoFromFinal,
bool removeAlsoFromToUpload)
{
ArgumentNullException.ThrowIfNull(ispezione);
if (string.IsNullOrWhiteSpace(fileName)) return false;
var removed = false;
if (removeAlsoFromToUpload)
{
var toUploadPath = Path.Combine(GetInspectionToUploadDir(ispezione), fileName);
if (File.Exists(toUploadPath))
{
File.Delete(toUploadPath);
removed = true;
}
}
if (removeAlsoFromFinal)
{
var finalPath = Path.Combine(GetInspectionFinalDir(ispezione), fileName);
if (File.Exists(finalPath))
{
File.Delete(finalPath);
removed = true;
}
}
if (removed)
CleanupDirectoriesIfEmpty(ispezione);
return removed;
}
private void CleanupDirectoriesIfEmpty(Ispezione ispezione)
{
var baseDir = GetInspectionBaseDir(ispezione);
var toUploadDir = GetInspectionToUploadDir(ispezione);
// 1) se /toUpload esiste e vuota => delete
if (Directory.Exists(toUploadDir) && !Directory.EnumerateFileSystemEntries(toUploadDir).Any())
Directory.Delete(toUploadDir);
// 2) se base dir vuota (attenzione: dopo delete toUpload) => delete
if (Directory.Exists(baseDir) && !Directory.EnumerateFileSystemEntries(baseDir).Any())
Directory.Delete(baseDir);
}
public async Task<string> SaveToTempStorage(Stream file, string fileName, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(file);
if (file.CanSeek)
file.Position = 0;
fileName = Path.GetFileName(fileName);
var dir = FileSystem.CacheDirectory;
var filePath = Path.Combine(dir, fileName);
await using var fileStream = File.Create(filePath);
await file.CopyToAsync(fileStream, ct);
return filePath;
}
public Task CleanTempStorageAsync(CancellationToken ct = default)
{
return Task.Run(() =>
{
if (Directory.Exists(AttachedRoot))
Directory.Delete(AttachedRoot, true);
}, ct);
}
public Task OpenFile(string fileName, string filePath)
{
#if IOS
throw new NotImplementedException();
#else
return Launcher.OpenAsync(new OpenFileRequest
{
Title = "Apri file",
File = new ReadOnlyFile(filePath)
});
#endif
}
public async Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(
byte[] bytes, string fileName, CancellationToken ct = default)
{
Directory.CreateDirectory(AttachedRoot);
var id = Guid.NewGuid().ToString("N");
var safeName = SanitizeFileName(fileName);
var originalFile = $"{id}_{safeName}";
var thumbFile = $"{id}_thumb.jpg";
var originalPath = Path.Combine(AttachedRoot, originalFile);
await File.WriteAllBytesAsync(originalPath, bytes, ct);
var thumbPath = Path.Combine(AttachedRoot, thumbFile);
await ImageThumb.CreateThumbnailAsync(originalPath, thumbPath, maxSide: 320, quality: 70, ct);
return ($"https://localfiles/attached/{originalFile}",
$"https://localfiles/attached/{thumbFile}");
}
private static string SanitizeFileName(string fileName)
{
var name = Path.GetFileName(fileName);
return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
}
private static string GetInspectionKey(Ispezione ispezione) =>
$"{ispezione.CodMdep}_{ispezione.Data:ddMMyyyy}_{ispezione.Rilevatore.ToLower()}";
}

View File

@@ -0,0 +1,442 @@
using System.IO.Compression;
using SteUp.Data.LocalDb;
using SteUp.Shared.Core.Dto;
using SteUp.Shared.Core.Entities;
using SteUp.Shared.Core.Helpers;
using SteUp.Shared.Core.Interface.System;
namespace SteUp.Maui.Core.Services;
public class FileManager(IDbPathProvider dbPathProvider) : IFileManager
{
private static string AttachedRoot =>
Path.Combine(FileSystem.CacheDirectory, "attached");
private async Task<AttachedDto> ConvertToDto(FileInfo file, AttachedDto.TypeAttached type, bool isFromToUpload)
{
var (origUrl, thumbUrl) = await SaveAndCreateThumbAsync(
await File.ReadAllBytesAsync(file.FullName),
file.Name
);
return new AttachedDto
{
Name = file.Name,
Path = file.FullName,
TempPath = origUrl,
ThumbPath = thumbUrl,
Type = type,
SavedOnAppData = true,
ToUpload = isFromToUpload
};
}
private const string ToUploadFolderName = "toUpload";
private string GetInspectionBaseDir(Ispezione ispezione)
{
var baseDir = FileSystem.AppDataDirectory;
return Path.Combine(baseDir, "attached", $"inspection_{GetInspectionKey(ispezione)}");
}
private string GetInspectionToUploadDir(Ispezione ispezione)
=> Path.Combine(GetInspectionBaseDir(ispezione), ToUploadFolderName);
public string GetFileToUploadDir(Ispezione ispezione, string fileName) =>
Path.Combine(GetInspectionToUploadDir(ispezione), fileName);
private string GetInspectionFinalDir(Ispezione ispezione)
=> GetInspectionBaseDir(ispezione);
/// <summary>
/// Ritorna i file dell'ispezione filtrati per nome.
/// Per default include sia "final" sia "toUpload" (utile per UI).
/// </summary>
public async Task<List<AttachedDto>?> GetInspectionFiles(
Ispezione ispezione,
List<string> fileNameFilter,
bool includeToUpload,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentNullException.ThrowIfNull(fileNameFilter);
var baseDir = GetInspectionBaseDir(ispezione);
if (!Directory.Exists(baseDir)) return null;
var result = new List<AttachedDto>();
var finalDir = GetInspectionFinalDir(ispezione);
if (Directory.Exists(finalDir))
{
var finalFiles = new DirectoryInfo(finalDir)
.GetFiles("*", SearchOption.TopDirectoryOnly);
foreach (var file in finalFiles)
{
if (file.Directory?.Name == ToUploadFolderName)
continue;
if (!fileNameFilter.Contains(file.FullName))
continue;
ct.ThrowIfCancellationRequested();
result.Add(await ConvertToDto(
file,
AttachedDto.TypeAttached.Image,
isFromToUpload: false));
}
}
if (!includeToUpload) return result;
var toUploadDir = GetInspectionToUploadDir(ispezione);
if (!Directory.Exists(toUploadDir)) return result;
var toUploadFiles = new DirectoryInfo(toUploadDir)
.GetFiles("*", SearchOption.TopDirectoryOnly);
foreach (var file in toUploadFiles)
{
if (!fileNameFilter.Contains(file.Name))
continue;
ct.ThrowIfCancellationRequested();
result.Add(await ConvertToDto(
file,
AttachedDto.TypeAttached.Image,
isFromToUpload: true));
}
return result;
}
/// <summary>
/// Salva SEMPRE in /toUpload.
/// </summary>
public async Task<string?> SaveInspectionFile(
Ispezione ispezione,
byte[] file,
string fileName,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentNullException.ThrowIfNull(file);
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
var toUploadDir = GetInspectionToUploadDir(ispezione);
Directory.CreateDirectory(toUploadDir);
var filePath = Path.Combine(toUploadDir, fileName);
await File.WriteAllBytesAsync(filePath, file, ct);
return filePath;
}
public Task<string?> MoveInspectionFileFromToUploadToFinal(
Ispezione ispezione,
string fileName,
bool overwrite,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(ispezione);
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
ct.ThrowIfCancellationRequested();
var toUploadDir = GetInspectionToUploadDir(ispezione);
var finalDir = GetInspectionFinalDir(ispezione);
if (!Directory.Exists(toUploadDir)) return Task.FromResult<string?>(null);
var sourcePath = Path.Combine(toUploadDir, fileName);
if (!File.Exists(sourcePath)) return Task.FromResult<string?>(null);
Directory.CreateDirectory(finalDir);
var destPath = Path.Combine(finalDir, fileName);
if (File.Exists(destPath))
{
if (!overwrite) return Task.FromResult<string?>(null);
File.Delete(destPath);
}
File.Move(sourcePath, destPath);
// Pulizia: se /toUpload resta vuota la elimino
CleanupDirectoriesIfEmpty(ispezione);
return Task.FromResult<string?>(destPath);
}
/// <summary>
/// Rimuove un file cercandolo prima in /toUpload e poi in final (o viceversa).
/// Default: prova a cancellare ovunque.
/// </summary>
public bool RemoveInspectionFile(
Ispezione ispezione,
string fileName,
bool removeAlsoFromFinal,
bool removeAlsoFromToUpload)
{
ArgumentNullException.ThrowIfNull(ispezione);
if (string.IsNullOrWhiteSpace(fileName)) return false;
var removed = false;
if (removeAlsoFromToUpload)
{
var toUploadPath = Path.Combine(GetInspectionToUploadDir(ispezione), fileName);
if (File.Exists(toUploadPath))
{
File.Delete(toUploadPath);
removed = true;
}
}
if (removeAlsoFromFinal)
{
var finalPath = Path.Combine(GetInspectionFinalDir(ispezione), fileName);
if (File.Exists(finalPath))
{
File.Delete(finalPath);
removed = true;
}
}
if (removed)
CleanupDirectoriesIfEmpty(ispezione);
return removed;
}
private void CleanupDirectoriesIfEmpty(Ispezione ispezione)
{
var baseDir = GetInspectionBaseDir(ispezione);
var toUploadDir = GetInspectionToUploadDir(ispezione);
// 1) se /toUpload esiste e vuota => delete
if (Directory.Exists(toUploadDir) && !Directory.EnumerateFileSystemEntries(toUploadDir).Any())
Directory.Delete(toUploadDir);
// 2) se base dir vuota (attenzione: dopo delete toUpload) => delete
if (Directory.Exists(baseDir) && !Directory.EnumerateFileSystemEntries(baseDir).Any())
Directory.Delete(baseDir);
}
public async Task<string> SaveToTempStorage(Stream file, string fileName, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(file);
if (file.CanSeek)
file.Position = 0;
fileName = Path.GetFileName(fileName);
var dir = FileSystem.CacheDirectory;
var filePath = Path.Combine(dir, fileName);
await using var fileStream = File.Create(filePath);
await file.CopyToAsync(fileStream, ct);
return filePath;
}
public Task CleanTempStorageAsync(CancellationToken ct = default)
{
return Task.Run(() =>
{
if (Directory.Exists(AttachedRoot))
Directory.Delete(AttachedRoot, true);
}, ct);
}
public Task OpenFile(string fileName, string filePath)
{
#if IOS
throw new NotImplementedException();
#else
return Launcher.OpenAsync(new OpenFileRequest
{
Title = "Apri file",
File = new ReadOnlyFile(filePath)
});
#endif
}
public async Task<(string originalUrl, string thumbUrl)> SaveAndCreateThumbAsync(
byte[] bytes, string fileName, CancellationToken ct = default)
{
Directory.CreateDirectory(AttachedRoot);
var id = Guid.NewGuid().ToString("N");
var safeName = SanitizeFileName(fileName);
var originalFile = $"{id}_{safeName}";
var thumbFile = $"{id}_thumb.jpg";
var originalPath = Path.Combine(AttachedRoot, originalFile);
await File.WriteAllBytesAsync(originalPath, bytes, ct);
var thumbPath = Path.Combine(AttachedRoot, thumbFile);
await ImageThumb.CreateThumbnailAsync(originalPath, thumbPath, maxSide: 320, quality: 70, ct);
return ($"https://localfiles/attached/{originalFile}",
$"https://localfiles/attached/{thumbFile}");
}
private static string SanitizeFileName(string fileName)
{
var name = Path.GetFileName(fileName);
return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, '_'));
}
private static string GetInspectionKey(Ispezione ispezione) =>
$"{ispezione.CodMdep}_{ispezione.Data:ddMMyyyy}_{ispezione.Rilevatore.ToLower()}";
public List<SendEmailDto.AttachmentsDto> GetFileForExport()
{
var attachments = new List<SendEmailDto.AttachmentsDto>();
// 1) log file singolo (se ti serve ancora)
var logFile = RetrieveLogFile();
if (!logFile.IsNullOrEmpty())
{
attachments.Add(new SendEmailDto.AttachmentsDto
{
FileName = $"logs_{DateTime.Today:yyyyMMdd}.zip",
FileContent = CreateZipBytes(logFile!)
});
}
// 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
.EnumerateFiles("*", SearchOption.AllDirectories)
.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<FileInfo>? RetrieveLogFile()
{
var appDataPath = FileSystem.AppDataDirectory;
var targetDirectory = Path.Combine(appDataPath, "logs");
var directory = new DirectoryInfo(targetDirectory);
List<FileInfo>? 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<FileInfo> 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<FileInfo> 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();
}
}

View File

@@ -0,0 +1,23 @@
using SteUp.Shared.Core.Dto;
namespace SteUp.Maui.Core.Utility;
public static class UtilityFile
{
public static async Task<AttachedDto> ConvertToDto(FileResult file, AttachedDto.TypeAttached type)
{
var stream = await file.OpenReadAsync();
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
return new AttachedDto
{
Name = file.FileName,
Path = file.FullPath,
MimeType = file.ContentType,
DimensionBytes = ms.Length,
FileBytes = ms.ToArray(),
Type = type
};
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
namespace SteUp.Maui.Core.UtilityException;
public static class GlobalExceptionHandler
{
public static void Register(ILogger logger)
{
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
var ex = args.ExceptionObject as Exception;
logger.LogCritical(ex, "UnhandledException (AppDomain) — IsTerminating: {t}", args.IsTerminating);
};
TaskScheduler.UnobservedTaskException += (_, args) =>
{
logger.LogCritical(args.Exception, "UnobservedTaskException");
args.SetObserved();
};
#if ANDROID
Android.Runtime.AndroidEnvironment.UnhandledExceptionRaiser += (_, args) =>
{
logger.LogCritical(args.Exception, "Android UnhandledException");
args.Handled = true;
};
#endif
#if IOS || MACCATALYST
ObjCRuntime.Runtime.MarshalManagedException += (_, args) =>
{
logger.LogCritical(args.Exception, "iOS MarshalManagedException");
args.ExceptionMode = ObjCRuntime.MarshalManagedExceptionMode.UnwindNativeCode;
};
#endif
}
}

View File

@@ -46,6 +46,7 @@ namespace SteUp.Maui
builder.RegisterSystemService();
builder.RegisterDbServices();
builder.RegisterMessageServices();
builder.RegisterLoggerServices();
return builder.Build();
}