using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GitConverter.Lib.Licensing;
using GitConverter.Lib.Logging;
using GitConverter.Lib.Models;
using SharpCompress.Archives;
using Aspose.Gis;
namespace GitConverter.Lib.Converters
{
///
/// Converter that handles CSV inputs and produces outputs according to the requested target format.
///
///
/// Responsibilities
/// - Validate input / output / temp paths (delegates to ).
/// - Accept either:
/// * a single .csv file as input, or
/// * an archive containing one or more .csv entries (the first .csv entry found is used).
/// - When given an archive, extract entries into the provided temp folder using safe extraction (zip-slip protection).
/// - Map the requested target option to an Aspose driver via .
/// - Apply Aspose license via and call Aspose.GIS vector conversion APIs.
///
public class CsvConverter : IConverter
{
///
/// Convert the given CSV input (single .csv or an archive containing .csv entries) into the requested target format.
///
public ConversionResult Convert(string gisInputFilePath, string gisTargetFormatOption, string outputFolderPath, string tempFolderPath)
{
// Parameter trace for diagnostics
Log.Debug($"CsvConverter.Convert params: gisInputFilePath='{gisInputFilePath}', gisTargetFormatOption='{gisTargetFormatOption}', outputFolderPath='{outputFolderPath}', tempFolderPath='{tempFolderPath}'");
Log.Info($"CsvConverter: starting conversion (option='{gisTargetFormatOption}', input='{gisInputFilePath}').");
// Remember whether the temp folder existed before this call.
var tempFolderNotExistedAtStart = !string.IsNullOrWhiteSpace(tempFolderPath) && !Directory.Exists(tempFolderPath);
// Validate and prepare paths
var validation = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outputFolderPath, tempFolderPath);
if (validation != null)
return validation;
string sourceCsvPath = gisInputFilePath;
var extractedToTemp = false;
try
{
var inputIsArchive = ConverterUtils.IsArchiveFile(gisInputFilePath);
if (inputIsArchive)
{
Log.Debug("CsvConverter: input detected as archive. Inspecting entries.");
var entries = ConverterUtils.TryListArchiveEntries(gisInputFilePath);
if (entries == null)
{
Log.Error("CsvConverter: failed to list archive entries.");
return ConversionResult.Failure("Failed to inspect archive contents.");
}
// Find first .csv entry (case-insensitive)
var csvEntry = entries.FirstOrDefault(e => string.Equals(Path.GetExtension(e), ".csv", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrWhiteSpace(csvEntry))
{
Log.Error("CsvConverter: archive does not contain a .csv entry.");
return ConversionResult.Failure("Archive does not contain a .csv entry.");
}
// Extract archive safely into tempFolderPath
try
{
Log.Debug($"CsvConverter: extracting archive '{gisInputFilePath}' into '{tempFolderPath}'.");
if (!Directory.Exists(tempFolderPath))
{
Directory.CreateDirectory(tempFolderPath);
}
var tempFull = Path.GetFullPath(tempFolderPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
var skipped = new List();
var extractedFiles = new List();
using (var archive = ArchiveFactory.Open(gisInputFilePath))
{
foreach (var entry in archive.Entries.Where(en => !en.IsDirectory))
{
var entryKey = entry.Key;
if (string.IsNullOrEmpty(entryKey)) continue;
var destPath = Path.Combine(tempFolderPath, entryKey);
var destFull = Path.GetFullPath(destPath);
// zip-slip guard
if (!destFull.StartsWith(tempFull, StringComparison.OrdinalIgnoreCase))
{
Log.Warn($"CsvConverter: skipping entry '{entryKey}' which would extract outside the temp folder.");
skipped.Add(entryKey);
continue;
}
try
{
Directory.CreateDirectory(Path.GetDirectoryName(destFull) ?? tempFolderPath);
using (var src = entry.OpenEntryStream())
using (var dst = File.Create(destFull))
{
src.CopyTo(dst);
}
extractedFiles.Add(destFull);
Log.Debug($"CsvConverter: extracted '{entryKey}' -> '{destFull}'.");
}
catch (Exception exEntry)
{
Log.Error($"CsvConverter: failed to extract '{entryKey}': {exEntry.Message}");
skipped.Add(entryKey);
}
}
}
// If any entries were skipped or failed to extract -> treat as extraction failure
if (skipped.Count > 0)
{
Log.Error($"CsvConverter: some archive entries skipped or failed: {string.Join(", ", skipped)}");
try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { }
return ConversionResult.Failure($"Archive extraction failed for entries: {string.Join(", ", skipped)}");
}
// locate extracted .csv
var found = Directory.GetFiles(tempFolderPath, "*.csv", SearchOption.AllDirectories).FirstOrDefault();
if (found == null)
{
Log.Error("CsvConverter: extracted archive but no .csv file found.");
try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { }
return ConversionResult.Failure("Archive extraction did not yield a .csv file.");
}
sourceCsvPath = found;
extractedToTemp = true;
Log.Info($"CsvConverter: using extracted CSV '{sourceCsvPath}'.");
}
catch (Exception ex)
{
Log.Error($"CsvConverter: extraction failed: {ex.Message}", ex);
try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { }
return ConversionResult.Failure($"Failed to extract CSV from archive: {ex.Message}");
}
}
else
{
// Single-file path must be .csv and exist
var ext = Path.GetExtension(gisInputFilePath) ?? string.Empty;
if (!string.Equals(ext, ".csv", StringComparison.OrdinalIgnoreCase))
{
Log.Error($"CsvConverter: input '{gisInputFilePath}' is not a .csv file.");
return ConversionResult.Failure("CSV converter requires a .csv file or archive containing .csv.");
}
if (!File.Exists(gisInputFilePath))
{
Log.Error($"CsvConverter: input file does not exist: '{gisInputFilePath}'.");
return ConversionResult.Failure($"Input file does not exist: '{gisInputFilePath}'.");
}
sourceCsvPath = gisInputFilePath;
Log.Debug($"CsvConverter: using input CSV '{sourceCsvPath}'.");
}
// Resolve destination driver via ConverterUtils
var destDriverGeneric = ConverterUtils.ConversionOptionToDriver(gisTargetFormatOption);
if (destDriverGeneric == null)
{
Log.Warn($"CsvConverter: target format option '{gisTargetFormatOption}' did not map to a known Aspose driver.");
return ConversionResult.Failure($"Target format option '{gisTargetFormatOption}' did not map to a known Aspose driver.");
}
var destFileDriver = destDriverGeneric as FileDriver;
var srcFileDriver = Drivers.Csv as FileDriver;
Log.Info($"CsvConverter: preparing Aspose conversion source='{sourceCsvPath}', target='{gisTargetFormatOption}'.");
if (srcFileDriver == null)
{
Log.Error("CsvConverter: Aspose Drivers.Csv is not available as a FileDriver.");
return ConversionResult.Failure("Internal error: source driver not available.");
}
if (destFileDriver == null)
{
Log.Error($"CsvConverter: resolved destination driver for '{gisTargetFormatOption}' is not a FileDriver.");
return ConversionResult.Failure("Internal error: destination driver not available.");
}
// Apply Aspose license (embedded)
var licenseApplied = AsposeLicenseManager.ApplyLicense();
if (!licenseApplied)
{
Log.Error("CsvConverter: Aspose license was not applied (embedded). Aborting conversion.");
return ConversionResult.Failure("Aspose license not applied (embedded). Place Aspose.Total.Net.lic as an EmbeddedResource in GitConverter.Lib for releases.");
}
Log.Info("CsvConverter: Aspose license applied successfully (embedded).");
// Ensure output folder exists and prepare deterministic output filename
Directory.CreateDirectory(outputFolderPath);
var timeStamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
// Determine output extension based on target option. FromOption may return null; fall back to Csv.
var maybeOutFileExt = FileExtensionHelpers.FromOption(gisTargetFormatOption);
var outFileExt = maybeOutFileExt ?? FileExtension.Csv;
if (maybeOutFileExt == null)
Log.Warn($"CsvConverter: could not map option '{gisTargetFormatOption}' to a known FileExtension; falling back to '{outFileExt}'.");
var extDot = FileExtensionHelpers.ToDotExtension(outFileExt);
var destCsvOutputPath = Path.Combine(outputFolderPath, $"output_{timeStamp}{extDot}");
Log.Info($"CsvConverter: target output file will be '{destCsvOutputPath}'.");
Log.Info($"CsvConverter: converting '{sourceCsvPath}' -> '{destCsvOutputPath}' (target option='{gisTargetFormatOption}').");
// Perform conversion via Aspose
VectorLayer.Convert(sourceCsvPath, srcFileDriver, destCsvOutputPath, destFileDriver);
Log.Info("CsvConverter: Aspose conversion succeeded.");
return ConversionResult.Success($"Converted with Aspose to {gisTargetFormatOption}; output: {destCsvOutputPath}");
}
catch (Exception ex)
{
Log.Error($"CsvConverter: unexpected error: {ex.Message}", ex);
return ConversionResult.Failure($"Unexpected error: {ex.Message}");
}
finally
{
Log.Info("CsvConverter: finished conversion attempt.");
// Only cleanup when we actually extracted files into the temp folder
if (extractedToTemp || tempFolderNotExistedAtStart)
{
Log.Debug("CsvConverter: cleaning up temp folder.");
try
{
ConverterUtils.TryCleanupTempFolder(tempFolderPath);
}
catch (Exception ex)
{
Log.Debug($"CsvConverter: cleanup failed: {ex.Message}");
}
}
Log.Info("CsvConverter: finished conversion attempt.");
}
}
}
}