Initial commit
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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<IResult> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user