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 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); /// /// Ritorna i file dell'ispezione filtrati per nome. /// Per default include sia "final" sia "toUpload" (utile per UI). /// public async Task?> GetInspectionFiles( Ispezione ispezione, List 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(); 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; } /// /// Salva SEMPRE in /toUpload. /// public async Task 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 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(null); var sourcePath = Path.Combine(toUploadDir, fileName); if (!File.Exists(sourcePath)) return Task.FromResult(null); Directory.CreateDirectory(finalDir); var destPath = Path.Combine(finalDir, fileName); if (File.Exists(destPath)) { if (!overwrite) return Task.FromResult(null); File.Delete(destPath); } File.Move(sourcePath, destPath); // Pulizia: se /toUpload resta vuota la elimino CleanupDirectoriesIfEmpty(ispezione); return Task.FromResult(destPath); } /// /// Rimuove un file cercandolo prima in /toUpload e poi in final (o viceversa). /// Default: prova a cancellare ovunque. /// 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 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 GetFileForExport() { var attachments = new List(); // 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? 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(); } }