using System.IO.Compression; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SatelliteProvider.Common.Configs; namespace SatelliteProvider.Services.RouteManagement; // AZ-364 / C11: extracted from RouteProcessingService.CreateTilesZipAsync. // Owns the route__tiles.zip path computation and the entry-name // resolution (relative to StorageConfig.TilesDirectory when the tile lives // under that root, or by file name otherwise). Behavior preserved verbatim: // existing zip is overwritten, missing tiles are logged but not fatal. public class TilesZipBuilder { private readonly StorageConfig _storageConfig; private readonly ILogger _logger; public TilesZipBuilder(IOptions storageConfig, ILogger logger) { ArgumentNullException.ThrowIfNull(storageConfig); _storageConfig = storageConfig.Value; _logger = logger; } public Task BuildAsync( Guid routeId, IEnumerable tiles, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(tiles); Directory.CreateDirectory(_storageConfig.ReadyDirectory); var zipFilePath = Path.Combine(_storageConfig.ReadyDirectory, $"route_{routeId}_tiles.zip"); return Task.Run(() => { if (File.Exists(zipFilePath)) { File.Delete(zipFilePath); } using var zipArchive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create); int addedFiles = 0; int missingFiles = 0; var tilesBasePath = _storageConfig.TilesDirectory; var normalizedBasePath = Path.GetFullPath(tilesBasePath) .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); foreach (var tile in tiles) { if (cancellationToken.IsCancellationRequested) break; if (File.Exists(tile.FilePath)) { try { var fullPath = Path.GetFullPath(tile.FilePath); string entryName; if (fullPath.StartsWith(normalizedBasePath, StringComparison.OrdinalIgnoreCase)) { var relativePath = fullPath.Substring(normalizedBasePath.Length + 1); relativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/'); entryName = "tiles/" + relativePath; } else { entryName = "tiles/" + Path.GetFileName(tile.FilePath); } zipArchive.CreateEntryFromFile(tile.FilePath, entryName, CompressionLevel.Optimal); addedFiles++; } catch (Exception ex) { _logger.LogWarning(ex, "Failed to add tile to zip: {FilePath}", tile.FilePath); } } else { _logger.LogWarning("Tile file not found for zip: {FilePath}", tile.FilePath); missingFiles++; } } _logger.LogInformation( "Tiles zip created: {ZipPath} with {AddedFiles} tiles ({MissingFiles} missing)", zipFilePath, addedFiles, missingFiles); return zipFilePath; }, cancellationToken); } }