Rilascia strumenti avanzati e archivio 0.3.0

This commit is contained in:
livio committed 2026-09-24 16:15:23 +02:00
1 parent b19afbce91
commit 620a4bec26
8 files changed
+1351 -33

No files matched your search

+223 -11
View File
@@ -1,5 +1,6 @@
using System.Text;
using System.Text.Json;
using System.IO.Compression;
namespace LMEMappeCatastaliApp.Services;
@@ -13,8 +14,9 @@ public sealed class ProjectStorageService
};
private string ProjectsDirectory => Path.Combine(FileSystem.AppDataDirectory, "Projects");
private string HistoryDirectory => Path.Combine(ProjectsDirectory, ".history");
public async Task<IReadOnlyList<MapProjectSummary>> GetProjectsAsync()
public async Task<IReadOnlyList<MapProjectSummary>> GetProjectsAsync(bool includeArchived = true)
{
await _gate.WaitAsync();
try
@@ -22,6 +24,7 @@ public sealed class ProjectStorageService
var files = await ReadProjectFilesAsync();
return files
.Select(item => item.Document.ToSummary())
.Where(item => includeArchived || !item.IsArchived)
.OrderByDescending(item => item.UpdatedAt)
.ThenBy(item => item.Name, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
@@ -56,14 +59,23 @@ public sealed class ProjectStorageService
var now = DateTimeOffset.UtcNow;
var id = existing?.Document.Id ?? Guid.NewGuid().ToString("N");
if (existing is not null)
{
Directory.CreateDirectory(HistoryDirectory);
var revisionName = $"{id}--{existing.Document.UpdatedAt:yyyyMMddHHmmssfff}{ProjectExtension}";
File.Copy(existing.Path, Path.Combine(HistoryDirectory, revisionName), true);
PruneHistory(id, 15);
}
var document = new MapProjectDocument
{
SchemaVersion = 1,
SchemaVersion = 2,
Id = id,
Name = normalizedName,
CreatedAt = existing?.Document.CreatedAt ?? now,
UpdatedAt = now,
IncludesCadastralParcels = includesCadastralParcels,
Folder = existing?.Document.Folder ?? string.Empty,
IsArchived = existing?.Document.IsArchived ?? false,
Map = snapshotDocument.RootElement.Clone()
};
@@ -93,6 +105,178 @@ public sealed class ProjectStorageService
}
}
public async Task<MapProjectSummary> DuplicateAsync(string projectId)
{
await _gate.WaitAsync();
try
{
var files = await ReadProjectFilesAsync();
var source = files.FirstOrDefault(item => item.Document.Id == projectId)
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
var usedNames = files.Select(item => item.Document.Name).ToHashSet(StringComparer.CurrentCultureIgnoreCase);
var baseName = $"{source.Document.Name} - copia";
var name = baseName;
for (var index = 2; usedNames.Contains(name); index++)
{
name = $"{baseName} {index}";
}
var now = DateTimeOffset.UtcNow;
var id = Guid.NewGuid().ToString("N");
var copy = new MapProjectDocument
{
SchemaVersion = 2,
Id = id,
Name = name,
CreatedAt = now,
UpdatedAt = now,
IncludesCadastralParcels = source.Document.IncludesCadastralParcels,
Folder = source.Document.Folder,
Map = source.Document.Map.Clone()
};
var path = Path.Combine(ProjectsDirectory, BuildFileName(name, id));
await File.WriteAllTextAsync(path, JsonSerializer.Serialize(copy, _jsonOptions), new UTF8Encoding(false));
return copy.ToSummary();
}
finally { _gate.Release(); }
}
public async Task UpdateOrganizationAsync(string projectId, string? folder = null, bool? isArchived = null)
{
await _gate.WaitAsync();
try
{
var project = (await ReadProjectFilesAsync()).FirstOrDefault(item => item.Document.Id == projectId)
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
project.Document.Folder = NormalizeFolder(folder ?? project.Document.Folder);
project.Document.IsArchived = isArchived ?? project.Document.IsArchived;
project.Document.UpdatedAt = DateTimeOffset.UtcNow;
await File.WriteAllTextAsync(project.Path, JsonSerializer.Serialize(project.Document, _jsonOptions), new UTF8Encoding(false));
}
finally { _gate.Release(); }
}
public async Task<IReadOnlyList<MapProjectVersion>> GetVersionsAsync(string projectId)
{
await _gate.WaitAsync();
try
{
Directory.CreateDirectory(HistoryDirectory);
var versions = new List<MapProjectVersion>();
foreach (var path in Directory.EnumerateFiles(HistoryDirectory, $"{projectId}--*{ProjectExtension}"))
{
try
{
await using var stream = File.OpenRead(path);
var document = await JsonSerializer.DeserializeAsync<MapProjectDocument>(stream, _jsonOptions);
if (document is not null)
{
versions.Add(new(Path.GetFileName(path), document.UpdatedAt));
}
}
catch (JsonException) { }
catch (IOException) { }
}
return versions.OrderByDescending(item => item.UpdatedAt).ToArray();
}
finally { _gate.Release(); }
}
public async Task<string> LoadVersionAsync(string projectId, string versionId)
{
await _gate.WaitAsync();
try
{
if (!versionId.StartsWith($"{projectId}--", StringComparison.Ordinal)
|| Path.GetFileName(versionId) != versionId)
{
throw new InvalidDataException("Revisione non valida.");
}
var path = Path.Combine(HistoryDirectory, versionId);
await using var stream = File.OpenRead(path);
var document = await JsonSerializer.DeserializeAsync<MapProjectDocument>(stream, _jsonOptions)
?? throw new InvalidDataException("Revisione non leggibile.");
return document.Map.GetRawText();
}
finally { _gate.Release(); }
}
public async Task<string> CreateBackupAsync()
{
await _gate.WaitAsync();
try
{
Directory.CreateDirectory(ProjectsDirectory);
var directory = Path.Combine(FileSystem.CacheDirectory, "Backups");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, $"LMEMappeCatastali-backup-{DateTime.Now:yyyyMMdd-HHmmss}.lmebackup.zip");
if (File.Exists(path)) File.Delete(path);
ZipFile.CreateFromDirectory(ProjectsDirectory, path, CompressionLevel.Optimal, false);
return path;
}
finally { _gate.Release(); }
}
public async Task<int> RestoreBackupAsync(string backupPath)
{
await _gate.WaitAsync();
try
{
Directory.CreateDirectory(ProjectsDirectory);
using var archive = ZipFile.OpenRead(backupPath);
var restored = 0;
foreach (var entry in archive.Entries.Where(item => item.FullName.EndsWith(ProjectExtension, StringComparison.OrdinalIgnoreCase)))
{
var fileName = Path.GetFileName(entry.FullName);
if (string.IsNullOrWhiteSpace(fileName)) continue;
var targetDirectory = entry.FullName.Contains(".history/", StringComparison.OrdinalIgnoreCase)
|| entry.FullName.Contains(".history\\", StringComparison.OrdinalIgnoreCase)
? HistoryDirectory : ProjectsDirectory;
Directory.CreateDirectory(targetDirectory);
entry.ExtractToFile(Path.Combine(targetDirectory, fileName), true);
if (targetDirectory == ProjectsDirectory) restored++;
}
return restored;
}
finally { _gate.Release(); }
}
public async Task<string> CreateProjectPackageAsync(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'.");
var directory = Path.Combine(FileSystem.CacheDirectory, "Projects");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, Path.GetFileName(project.Path));
File.Copy(project.Path, path, true);
return path;
}
finally { _gate.Release(); }
}
public async Task<MapProjectSummary> ImportProjectAsync(string path)
{
await _gate.WaitAsync();
try
{
await using var stream = File.OpenRead(path);
var source = await JsonSerializer.DeserializeAsync<MapProjectDocument>(stream, _jsonOptions)
?? throw new InvalidDataException("Progetto non leggibile.");
if (source.Map.ValueKind != JsonValueKind.Object) throw new InvalidDataException("Mappa del progetto non valida.");
source.Id = Guid.NewGuid().ToString("N");
source.Name = NormalizeName(source.Name);
source.CreatedAt = source.UpdatedAt = DateTimeOffset.UtcNow;
source.IsArchived = false;
Directory.CreateDirectory(ProjectsDirectory);
var target = Path.Combine(ProjectsDirectory, BuildFileName(source.Name, source.Id));
await File.WriteAllTextAsync(target, JsonSerializer.Serialize(source, _jsonOptions), new UTF8Encoding(false));
return source.ToSummary();
}
finally { _gate.Release(); }
}
public async Task<string> LoadAsync(string projectId)
{
await _gate.WaitAsync();
@@ -118,6 +302,13 @@ public sealed class ProjectStorageService
.FirstOrDefault(item => item.Document.Id == projectId)
?? throw new FileNotFoundException("Il progetto selezionato non esiste piu'.");
File.Delete(project.Path);
if (Directory.Exists(HistoryDirectory))
{
foreach (var path in Directory.EnumerateFiles(HistoryDirectory, $"{projectId}--*{ProjectExtension}"))
{
File.Delete(path);
}
}
}
finally
{
@@ -179,19 +370,36 @@ public sealed class ProjectStorageService
return $"{safeName}--{id[..8]}{ProjectExtension}";
}
private void PruneHistory(string projectId, int keep)
{
foreach (var path in Directory.EnumerateFiles(HistoryDirectory, $"{projectId}--*{ProjectExtension}")
.OrderByDescending(File.GetLastWriteTimeUtc).Skip(keep))
{
File.Delete(path);
}
}
private static string NormalizeFolder(string value)
{
var normalized = string.Join(' ', (value ?? string.Empty).Split([' ', '\t', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries));
return normalized[..Math.Min(normalized.Length, 60)];
}
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 int SchemaVersion { get; set; }
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public bool IncludesCadastralParcels { get; set; }
public string Folder { get; set; } = string.Empty;
public bool IsArchived { get; set; }
public JsonElement Map { get; set; }
public MapProjectSummary ToSummary() => new(Id, Name, UpdatedAt, IncludesCadastralParcels);
public MapProjectSummary ToSummary() => new(Id, Name, UpdatedAt, IncludesCadastralParcels, Folder, IsArchived);
}
}
@@ -199,4 +407,8 @@ public sealed record MapProjectSummary(
string Id,
string Name,
DateTimeOffset UpdatedAt,
bool IncludesCadastralParcels);
bool IncludesCadastralParcels,
string Folder,
bool IsArchived);
public sealed record MapProjectVersion(string Id, DateTimeOffset UpdatedAt);