using System.IO.Compression; using Microsoft.AspNetCore.Http; namespace RobotNet10.RobotApp.Xloc; public static class XlocMapFileEndpoints { public static bool TryValidateMapFolderName(string mapName, out string safeName) { safeName = ""; if (string.IsNullOrWhiteSpace(mapName)) return false; var t = mapName.Trim(); if (t.Length > 200) return false; if (string.Equals(t, "tmp", StringComparison.OrdinalIgnoreCase)) return false; if (t.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) return false; if (t.Contains('/', StringComparison.Ordinal) || t.Contains('\\', StringComparison.Ordinal)) return false; safeName = t; return true; } public static IResult DeleteMapFolder(string mapName) { try { if (!TryValidateMapFolderName(mapName, out var safeName)) return Results.BadRequest(new { status = "error", message = "Invalid map name" }); var mapsDir = XlocPaths.GetMapsDirectory(); if (!Directory.Exists(mapsDir)) return Results.NotFound(new { status = "error", message = "Maps directory not found" }); var mapFolder = Path.Combine(mapsDir, safeName); if (!Directory.Exists(mapFolder)) return Results.NotFound(new { status = "error", message = $"Map '{safeName}' not found" }); Directory.Delete(mapFolder, recursive: true); return Results.Ok(new { status = "success", message = "Map deleted", map_name = safeName }); } catch (Exception ex) { return Results.BadRequest(new { status = "error", message = ex.Message }); } } public static IResult DownloadMapAsZip(string mapName) { try { if (!TryValidateMapFolderName(mapName, out var safeName)) return Results.BadRequest(new { status = "error", message = "Invalid map name" }); var mapsDir = XlocPaths.GetMapsDirectory(); var mapFolder = Path.Combine(mapsDir, safeName); if (!Directory.Exists(mapFolder)) return Results.NotFound(new { status = "error", message = $"Map '{safeName}' not found" }); using var ms = new MemoryStream(); using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { foreach (var filePath in Directory.EnumerateFiles(mapFolder, "*", SearchOption.AllDirectories)) { var rel = Path.GetRelativePath(mapFolder, filePath); var entryName = $"{safeName}/{rel.Replace('\\', '/')}"; var entry = archive.CreateEntry(entryName, CompressionLevel.Fastest); using var entryStream = entry.Open(); using var fileStream = File.OpenRead(filePath); fileStream.CopyTo(entryStream); } } return Results.File(ms.ToArray(), "application/zip", $"{safeName}.zip"); } catch (Exception ex) { return Results.BadRequest(new { status = "error", message = ex.Message }); } } public static async Task ImportMapFromZipAsync(HttpRequest request) { if (!request.HasFormContentType) return Results.BadRequest(new { status = "error", message = "Multipart form required" }); var form = await request.ReadFormAsync(); var file = form.Files.GetFile("file"); if (file == null || file.Length == 0) return Results.BadRequest(new { status = "error", message = "Zip file (field: file) required" }); if (!file.FileName.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) return Results.BadRequest(new { status = "error", message = "File must be a .zip archive" }); var zipStem = Path.GetFileNameWithoutExtension(file.FileName); if (string.IsNullOrWhiteSpace(zipStem) || zipStem is "." or "..") return Results.BadRequest(new { status = "error", message = "Invalid zip file name" }); if (!TryValidateMapFolderName(zipStem, out var finalTargetName)) return Results.BadRequest(new { status = "error", message = "Map name is taken from the zip file name. Rename the file (letters, numbers, dash, underscore; no path characters)." }); var tempZip = Path.Combine(Path.GetTempPath(), $"xloc_import_{Guid.NewGuid():N}.zip"); var tempExtract = Path.Combine(Path.GetTempPath(), $"xloc_extract_{Guid.NewGuid():N}"); try { await using (var fs = File.Create(tempZip)) await file.CopyToAsync(fs); Directory.CreateDirectory(tempExtract); ZipFile.ExtractToDirectory(tempZip, tempExtract); if (!TryResolveImportedMap(tempExtract, out var sourceDir)) return Results.BadRequest(new { status = "error", message = "Could not find a valid map. Use a zip with one top-level folder containing a .yaml file, or with .yaml files at the archive root." }); var mapsDir = XlocPaths.GetMapsDirectory(); Directory.CreateDirectory(mapsDir); var destPath = Path.Combine(mapsDir, finalTargetName); if (Directory.Exists(destPath)) return Results.Conflict(new { status = "error", message = $"A map named '{finalTargetName}' already exists. Rename the zip file or remove the existing map folder." }); if (string.Equals(sourceDir, tempExtract, StringComparison.Ordinal)) { Directory.CreateDirectory(destPath); CopyDirectoryRecursive(tempExtract, destPath); } else { Directory.Move(sourceDir, destPath); } return Results.Ok(new { status = "success", map_name = finalTargetName }); } catch (Exception ex) { return Results.BadRequest(new { status = "error", message = ex.Message }); } finally { TryDeleteFile(tempZip); TryDeleteDirectory(tempExtract); } } private static bool TryResolveImportedMap(string extractRoot, out string sourceDir) { sourceDir = ""; var rootFiles = Directory.GetFiles(extractRoot); var rootDirs = Directory.GetDirectories(extractRoot) .Where(d => !string.Equals(Path.GetFileName(d), "__MACOSX", StringComparison.OrdinalIgnoreCase)) .ToArray(); if (Directory.GetFiles(extractRoot, "*.yaml", SearchOption.TopDirectoryOnly).Length > 0) { sourceDir = extractRoot; return true; } var dirsWithYaml = rootDirs .Where(d => Directory.GetFiles(d, "*.yaml", SearchOption.AllDirectories).Length > 0) .ToList(); if (dirsWithYaml.Count == 1 && rootFiles.Length == 0) { sourceDir = dirsWithYaml[0]; return true; } return false; } private static void CopyDirectoryRecursive(string sourceDir, string destDir) { foreach (var file in Directory.GetFiles(sourceDir)) { var destFile = Path.Combine(destDir, Path.GetFileName(file)); File.Copy(file, destFile, overwrite: false); } foreach (var dir in Directory.GetDirectories(sourceDir)) { var destSub = Path.Combine(destDir, Path.GetFileName(dir)); Directory.CreateDirectory(destSub); CopyDirectoryRecursive(dir, destSub); } } private static void TryDeleteFile(string path) { try { if (File.Exists(path)) File.Delete(path); } catch { // ignore } } private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); } catch { // ignore } } }