Initial commit

This commit is contained in:
2026-07-03 16:31:37 +07:00
commit 899c7c637d
1939 changed files with 641750 additions and 0 deletions

View File

@@ -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
}
}
}