203 lines
6.8 KiB
C#
203 lines
6.8 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace LMEMappeCatastaliApp.Services;
|
|
|
|
public sealed class ProjectStorageService
|
|
{
|
|
private const string ProjectExtension = ".lmeproject.json";
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
WriteIndented = true
|
|
};
|
|
|
|
private string ProjectsDirectory => Path.Combine(FileSystem.AppDataDirectory, "Projects");
|
|
|
|
public async Task<IReadOnlyList<MapProjectSummary>> GetProjectsAsync()
|
|
{
|
|
await _gate.WaitAsync();
|
|
try
|
|
{
|
|
var files = await ReadProjectFilesAsync();
|
|
return files
|
|
.Select(item => item.Document.ToSummary())
|
|
.OrderByDescending(item => item.UpdatedAt)
|
|
.ThenBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
|
|
.ToArray();
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<MapProjectSummary> SaveAsync(
|
|
string name,
|
|
string snapshotJson,
|
|
bool includesCadastralParcels,
|
|
string? projectId = null)
|
|
{
|
|
var normalizedName = NormalizeName(name);
|
|
using var snapshotDocument = JsonDocument.Parse(snapshotJson);
|
|
if (snapshotDocument.RootElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
throw new InvalidDataException("I dati della mappa non sono validi.");
|
|
}
|
|
|
|
await _gate.WaitAsync();
|
|
try
|
|
{
|
|
Directory.CreateDirectory(ProjectsDirectory);
|
|
var files = await ReadProjectFilesAsync();
|
|
var existing = files.FirstOrDefault(item =>
|
|
(!string.IsNullOrWhiteSpace(projectId) && item.Document.Id == projectId)
|
|
|| string.Equals(item.Document.Name, normalizedName, StringComparison.CurrentCultureIgnoreCase));
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
var id = existing?.Document.Id ?? Guid.NewGuid().ToString("N");
|
|
var document = new MapProjectDocument
|
|
{
|
|
SchemaVersion = 1,
|
|
Id = id,
|
|
Name = normalizedName,
|
|
CreatedAt = existing?.Document.CreatedAt ?? now,
|
|
UpdatedAt = now,
|
|
IncludesCadastralParcels = includesCadastralParcels,
|
|
Map = snapshotDocument.RootElement.Clone()
|
|
};
|
|
|
|
var path = existing?.Path ?? Path.Combine(ProjectsDirectory, BuildFileName(normalizedName, id));
|
|
var temporaryPath = path + ".tmp";
|
|
try
|
|
{
|
|
await File.WriteAllTextAsync(
|
|
temporaryPath,
|
|
JsonSerializer.Serialize(document, _jsonOptions),
|
|
new UTF8Encoding(false));
|
|
File.Move(temporaryPath, path, true);
|
|
}
|
|
finally
|
|
{
|
|
if (File.Exists(temporaryPath))
|
|
{
|
|
File.Delete(temporaryPath);
|
|
}
|
|
}
|
|
|
|
return document.ToSummary();
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<string> LoadAsync(string projectId)
|
|
{
|
|
await _gate.WaitAsync();
|
|
try
|
|
{
|
|
var project = (await ReadProjectFilesAsync())
|
|
.FirstOrDefault(item => item.Document.Id == projectId)
|
|
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
|
|
return project.Document.Map.GetRawText();
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
public async Task DeleteAsync(string projectId)
|
|
{
|
|
await _gate.WaitAsync();
|
|
try
|
|
{
|
|
var project = (await ReadProjectFilesAsync())
|
|
.FirstOrDefault(item => item.Document.Id == projectId)
|
|
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
|
|
File.Delete(project.Path);
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<IReadOnlyList<ProjectFileEntry>> ReadProjectFilesAsync()
|
|
{
|
|
Directory.CreateDirectory(ProjectsDirectory);
|
|
var projects = new List<ProjectFileEntry>();
|
|
foreach (var path in Directory.EnumerateFiles(ProjectsDirectory, $"*{ProjectExtension}"))
|
|
{
|
|
try
|
|
{
|
|
await using var stream = File.OpenRead(path);
|
|
var document = await JsonSerializer.DeserializeAsync<MapProjectDocument>(stream, _jsonOptions);
|
|
if (document is not null
|
|
&& !string.IsNullOrWhiteSpace(document.Id)
|
|
&& !string.IsNullOrWhiteSpace(document.Name)
|
|
&& document.Map.ValueKind == JsonValueKind.Object)
|
|
{
|
|
projects.Add(new ProjectFileEntry(path, document));
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// A damaged project is ignored so the remaining archive stays usable.
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
}
|
|
|
|
return projects;
|
|
}
|
|
|
|
private static string NormalizeName(string name)
|
|
{
|
|
var normalized = string.Join(' ', (name ?? string.Empty).Split(
|
|
[' ', '\t', '\r', '\n'],
|
|
StringSplitOptions.RemoveEmptyEntries));
|
|
if (string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
throw new ArgumentException("Inserisci il nome del progetto.");
|
|
}
|
|
|
|
return normalized.Length <= 80 ? normalized : normalized[..80];
|
|
}
|
|
|
|
private static string BuildFileName(string name, string id)
|
|
{
|
|
var invalid = Path.GetInvalidFileNameChars().ToHashSet();
|
|
var safeName = new string(name
|
|
.Select(character => invalid.Contains(character) ? '-' : character)
|
|
.ToArray())
|
|
.Trim(' ', '.');
|
|
safeName = string.IsNullOrWhiteSpace(safeName) ? "Progetto" : safeName;
|
|
return $"{safeName}--{id[..8]}{ProjectExtension}";
|
|
}
|
|
|
|
private sealed record ProjectFileEntry(string Path, MapProjectDocument Document);
|
|
|
|
private sealed class MapProjectDocument
|
|
{
|
|
public int SchemaVersion { get; init; }
|
|
public string Id { get; init; } = string.Empty;
|
|
public string Name { get; init; } = string.Empty;
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
public DateTimeOffset UpdatedAt { get; init; }
|
|
public bool IncludesCadastralParcels { get; init; }
|
|
public JsonElement Map { get; init; }
|
|
|
|
public MapProjectSummary ToSummary() => new(Id, Name, UpdatedAt, IncludesCadastralParcels);
|
|
}
|
|
}
|
|
|
|
public sealed record MapProjectSummary(
|
|
string Id,
|
|
string Name,
|
|
DateTimeOffset UpdatedAt,
|
|
bool IncludesCadastralParcels);
|