using Aspose.Gis;
using GitConverter.Lib.Logging;
using GitConverter.Lib.Models;
using SharpCompress.Archives;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace GitConverter.Lib.Converters
{
///
/// Reusable utility helpers used by converters and the orchestration service.
///
///
/// Responsibilities
/// - Path validation and preparation for conversion workflows (input, output, temp).
/// - Lightweight archive inspection without extraction (entry listing, presence checks).
/// - Directory creation and writability probing used by .
///
/// Behaviour details
/// - ValidateAndPreparePaths:
/// - Ensures the input path exists (file or directory).
/// - Ensures the output path is a directory (creates it if missing) and verifies basic writability via a probe file.
/// - Ensures the temp folder path is present or creatable and verifies writability.
/// - Returns a friendly on expected problems (missing path, permission issues) rather than throwing.
/// - TryListArchiveEntries:
/// - Uses SharpCompress (preferred) and falls back to System.IO.Compression.ZipArchive for listing entries.
/// - Returns null when the archive cannot be inspected.
/// - ArchiveContainsAllRequiredExtensions:
/// - Detects required extensions and folder markers (for example .gdb directory markers) across archive entry paths.
///
/// Safety & diagnostics
/// - TryCreateAndVerifyDirectory performs a small probe (create + delete a zero-byte file) to assert writability and surfaces clear log messages on failure.
/// - Helpers are conservative: they swallow unexpected exceptions where appropriate and return friendly failures to callers.
///
/// Threading / side-effects
/// - Helpers are stateless and suitable for concurrent use. They perform local I/O and may create directories as a side-effect.
/// - Callers should prefer ephemeral per-conversion temp/output folders to avoid cross-run contention.
///
public static class ConverterUtils
{
///
/// Validate the input / output / temp paths before conversion.
/// Returns a non-null Failure on validation failure, otherwise null.
///
///
/// Checks performed
/// - gisInputFilePath must exist (file or directory).
/// - outputFilePath will be created if missing; if it exists but is a file an error is returned.
/// - tempFolderPath will be created if missing; if it exists but is a file an error is returned.
/// - Attempts to create the temp directories and verifies basic writability by creating a small probe file.
/// Behaviour
/// - Returns a ConversionResult.Failure(...) describing the problem for expected issues (missing path, permission).
/// - Logs diagnostic details (including exception messages) but does not rethrow exceptions for expected errors.
///
public static ConversionResult ValidateAndPreparePaths(string gisInputFilePath, string outputFilePath, string tempFolderPath)
{
if (string.IsNullOrWhiteSpace(gisInputFilePath))
{
Log.Error("ValidateAndPreparePaths: Input path is null or empty.");
return ConversionResult.Failure("Input path is required.");
}
if (!File.Exists(gisInputFilePath) && !Directory.Exists(gisInputFilePath))
{
Log.Error($"ValidateAndPreparePaths: Input path does not exist: '{gisInputFilePath}'.");
return ConversionResult.Failure($"Input path does not exist: '{gisInputFilePath}'.");
}
if (string.IsNullOrWhiteSpace(outputFilePath))
{
Log.Error("ValidateAndPreparePaths: Output path is null or empty.");
return ConversionResult.Failure("Output path is required.");
}
// If an output path exists and is a file -> error
if (File.Exists(outputFilePath) && !Directory.Exists(outputFilePath))
{
Log.Error($"ValidateAndPreparePaths: Output path '{outputFilePath}' is a file, not a directory.");
return ConversionResult.Failure($"Cannot use output path '{outputFilePath}' because is a file, not a directory.");
}
Log.Debug($"ValidateAndPreparePaths: ensuring output folder exists and is writable: '{outputFilePath}'");
// Ensure output folder exists and is writable (create if missing)
var outPrep = TryCreateAndVerifyDirectory(outputFilePath);
if (outPrep != null)
{
Log.Warn($"ValidateAndPreparePaths: failed to prepare output folder '{outputFilePath}': {outPrep.Message}");
return outPrep;
}
if (string.IsNullOrWhiteSpace(tempFolderPath))
{
Log.Error("ValidateAndPreparePaths: Temporary folder path is null or empty.");
return ConversionResult.Failure("Temporary folder path is required.");
}
if (File.Exists(tempFolderPath) && !Directory.Exists(tempFolderPath))
{
Log.Error($"ValidateAndPreparePaths: Temp path '{tempFolderPath}' is a file, not a directory.");
return ConversionResult.Failure($"Cannot use temp path '{tempFolderPath}' because is a file, not a directory.");
}
var tempPrep = TryCreateAndVerifyDirectory(tempFolderPath);
if (tempPrep != null) return tempPrep;
Log.Debug("ValidateAndPreparePaths: Path validation and preparation succeeded.");
return null;
}
///
/// Heuristic: returns true when the path looks like a common archive file by extension.
///
public static bool IsArchiveFile(string path)
{
if (string.IsNullOrWhiteSpace(path)) return false;
var lower = path.Trim().ToLowerInvariant();
// Composite extensions - check first
var compositeSuffixes = new[] { ".tar.gz", ".tar.bz2", ".tar.xz" };
foreach (var suf in compositeSuffixes)
{
if (lower.EndsWith(suf, StringComparison.OrdinalIgnoreCase)) return true;
}
var extensions = new[]
{
".zip", ".kmz", ".tar", ".tgz", ".gz", ".bz2", ".xz", ".7z", ".rar"
};
var ext = Path.GetExtension(lower);
if (string.IsNullOrEmpty(ext)) return false;
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
}
///
/// Read entry names from an archive without extracting.
/// Returns null when the archive cannot be inspected.
///
public static IEnumerable TryListArchiveEntries(string archivePath)
{
if (string.IsNullOrWhiteSpace(archivePath))
{
Log.Warn("TryListArchiveEntries: archivePath is null or empty.");
return null;
}
if (!File.Exists(archivePath))
{
Log.Warn($"TryListArchiveEntries: archive file not found: '{archivePath}'.");
return null;
}
// Prefer SharpCompress for broad archive support.
try
{
using (var archive = ArchiveFactory.Open(archivePath))
{
var list = new List();
foreach (var entry in archive.Entries)
{
try
{
if (entry == null) continue;
if (entry.IsDirectory) continue;
var name = entry.Key;
if (!string.IsNullOrEmpty(name))
list.Add(name);
}
catch (Exception inner)
{
Log.Debug($"TryListArchiveEntries: skipped entry due to error: {inner.Message}");
}
}
return list;
}
}
catch (Exception ex)
{
Log.Debug($"TryListArchiveEntries: SharpCompress failed to open '{archivePath}': {ex.Message}. Attempting Zip fallback.");
try
{
using (var stream = File.OpenRead(archivePath))
using (var za = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false))
{
return za.Entries.Select(e => e.FullName).ToList();
}
}
catch (Exception zipEx)
{
Log.Warn($"TryListArchiveEntries: failed to read archive '{archivePath}' with SharpCompress and ZipArchive: {zipEx.Message}");
return null;
}
}
}
///
/// Check whether the archive contains at least one entry with each of the provided extensions (case-insensitive).
/// This method now also inspects path segments for directory markers like "Name.gdb" so zipped FileGDB folders are detected.
///
public static bool ArchiveContainsAllRequiredExtensions(string archivePath, IEnumerable requiredExtensions)
{
if (string.IsNullOrWhiteSpace(archivePath)) return false;
if (requiredExtensions == null) return false;
var entries = TryListArchiveEntries(archivePath);
if (entries == null) return false;
// Build a normalized set of discovered "extensions" including:
// - extension of the entry name (last segment)
// - any suffixes found on intermediate path segments (e.g. "MyGdb.gdb")
var normalized = entries
.SelectMany(e =>
{
var found = new List();
try
{
var ext = Path.GetExtension(e);
if (!string.IsNullOrEmpty(ext))
found.Add(ext.ToLowerInvariant());
var segments = e.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var seg in segments)
{
var idx = seg.LastIndexOf('.');
if (idx > 0 && idx < seg.Length - 1)
{
var segExt = seg.Substring(idx).ToLowerInvariant();
found.Add(segExt);
}
}
}
catch { }
return found;
})
.Where(x => !string.IsNullOrEmpty(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var req in requiredExtensions)
{
if (string.IsNullOrWhiteSpace(req)) continue;
var r = req.ToLowerInvariant();
if (!normalized.Any(n => n == r)) return false;
}
return true;
}
///
/// Map a canonical conversion option name to an Aspose instance.
/// Returns null when unknown.
///
public static Driver ConversionOptionToDriver(string option)
{
if (string.IsNullOrWhiteSpace(option)) return null;
switch (option.Trim().ToLowerInvariant())
{
case "geojson": return Drivers.GeoJson;
case "geojsonseq": return Drivers.GeoJsonSeq;
case "esrijson": return Drivers.EsriJson;
case "gdb": return Drivers.FileGdb;
case "kml": return Drivers.Kml;
case "kmz": return Drivers.Kml;
case "shapefile": return Drivers.Shapefile;
case "topojson": return Drivers.TopoJson;
case "osm": return Drivers.OsmXml;
case "gpx": return Drivers.Gpx;
case "gml": return Drivers.Gml;
case "mapinfointerchange": return Drivers.MapInfoInterchange;
case "mapinfotab": return Drivers.MapInfoTab;
case "csv": return Drivers.Csv;
case "geopackage": return Drivers.GeoPackage;
default: return null;
}
}
///
/// Best-effort cleanup of the temp folder. This deletes the directory recursively.
/// Swallows exceptions and logs warnings.
///
public static void TryCleanupTempFolder(string tempFolderPath)
{
if (string.IsNullOrWhiteSpace(tempFolderPath)) return;
try
{
if (Directory.Exists(tempFolderPath))
{
Log.Debug($"Cleaning up temp folder '{tempFolderPath}'.");
Directory.Delete(tempFolderPath, recursive: true);
Log.Info($"Temp folder '{tempFolderPath}' deleted.");
}
}
catch (Exception ex)
{
Log.Warn($"Failed to clean up temp folder '{tempFolderPath}': {ex.Message}");
}
}
///
/// Best-effort delete of a set of extracted/intermediate files.
///
public static void CleanupExtractedFiles(IEnumerable files)
{
if (files == null) return;
foreach (var f in files)
{
if (string.IsNullOrWhiteSpace(f)) continue;
try
{
if (File.Exists(f))
{
File.Delete(f);
Log.Debug($"CleanupExtractedFiles: deleted '{f}'.");
}
}
catch (Exception ex)
{
Log.Warn($"CleanupExtractedFiles: failed to delete '{f}': {ex.Message}");
}
}
}
///
/// Format a UTC timestamp for human presentation.
/// Includes both local time and canonical UTC value.
///
public static string FormatTimestampForDisplay(DateTime utcTimestamp)
{
if (utcTimestamp.Kind != DateTimeKind.Utc)
utcTimestamp = DateTime.SpecifyKind(utcTimestamp, DateTimeKind.Utc);
var local = utcTimestamp.ToLocalTime();
return $"{local:o} (Local) | {utcTimestamp:o} (UTC)";
}
// --- internal helpers ------------------------------------------------
///
/// Attempt to create the directory and verify writable by creating a short probe file.
/// Returns a ConversionResult.Failure on error, or null on success.
///
private static ConversionResult TryCreateAndVerifyDirectory(string folderPath)
{
try
{
if (!Directory.Exists(folderPath))
{
Log.Debug($"TryCreateAndVerifyDirectory: Creating folder '{folderPath}'.");
Directory.CreateDirectory(folderPath);
}
// Quick writability probe: create and delete a small temp file.
var probeFile = Path.Combine(folderPath, Path.GetRandomFileName());
try
{
using (var fs = File.Create(probeFile)) { /* zero-byte */ }
File.Delete(probeFile);
}
catch (UnauthorizedAccessException uex)
{
Log.Error($"TryCreateAndVerifyDirectory: access denied for '{folderPath}': {uex.Message}", uex);
return ConversionResult.Failure($"Access denied for folder '{folderPath}': {uex.Message}");
}
catch (IOException ioex)
{
Log.Error($"TryCreateAndVerifyDirectory: I/O error for '{folderPath}': {ioex.Message}", ioex);
return ConversionResult.Failure($"I/O error for folder '{folderPath}': {ioex.Message}");
}
catch (Exception ex)
{
Log.Error($"TryCreateAndVerifyDirectory: unexpected error for '{folderPath}': {ex.Message}", ex);
return ConversionResult.Failure($"Unable to prepare folder '{folderPath}': {ex.Message}");
}
return null;
}
catch (UnauthorizedAccessException uex)
{
Log.Error($"TryCreateAndVerifyDirectory: access denied creating '{folderPath}': {uex.Message}", uex);
return ConversionResult.Failure($"Access denied creating folder '{folderPath}': {uex.Message}");
}
catch (IOException ioex)
{
Log.Error($"TryCreateAndVerifyDirectory: I/O error creating '{folderPath}': {ioex.Message}", ioex);
return ConversionResult.Failure($"I/O error creating folder '{folderPath}': {ioex.Message}");
}
catch (Exception ex)
{
Log.Error($"TryCreateAndVerifyDirectory: failed to create '{folderPath}': {ex.Message}", ex);
return ConversionResult.Failure($"Unable to create folder '{folderPath}': {ex.Message}");
}
}
}
}