Files
LehrerApp/LehrerApp.Templating/TemplatePackage.cs
2026-08-31 19:56:25 +02:00

115 lines
5.8 KiB
C#

using System.IO.Compression;
using System.Text.Json;
namespace LehrerApp.Templating;
public static class TemplatePackage
{
public const string Extension = ".lavorlage";
public static void Create(string outputPath, TemplateManifest manifest, string layoutSource,
IReadOnlyDictionary<string, byte[]> assets, string? continuationLayoutSource = null)
{
if (!TemplateLoader.IsSafeRelativePath(manifest.MetadataFile))
throw new InvalidDataException("metadataFile enthält einen unsicheren Pfad.");
if (TemplateLoader.Normalize(manifest.MetadataFile).Equals(
TemplateLoader.Normalize(manifest.LayoutFile), StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("metadataFile und layoutFile dürfen nicht identisch sein.");
var reservedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ "manifest.json", TemplateLoader.Normalize(manifest.LayoutFile), TemplateLoader.Normalize(manifest.MetadataFile) };
if (manifest.ContinuationLayoutFile is { } continuationPath)
{
if (!TemplateLoader.IsSafeRelativePath(continuationPath))
throw new InvalidDataException("continuationLayoutFile enthält einen unsicheren Pfad.");
reservedPaths.Add(TemplateLoader.Normalize(continuationPath));
if (continuationLayoutSource is null)
throw new InvalidDataException("Das Folgeseiten-Layout fehlt.");
}
if (!string.Equals(Path.GetExtension(outputPath), Extension, StringComparison.OrdinalIgnoreCase))
outputPath += Extension;
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory);
var temporary = outputPath + ".tmp";
try
{
using (var archive = ZipFile.Open(temporary, ZipArchiveMode.Create))
{
WriteText(archive, "manifest.json", JsonSerializer.Serialize(manifest, TemplateLoader.JsonOptions));
WriteText(archive, manifest.LayoutFile, layoutSource);
if (manifest.ContinuationLayoutFile is { } continuationFile)
WriteText(archive, continuationFile, continuationLayoutSource!);
WriteText(archive, manifest.MetadataFile, TemplateMetadataText.Serialize(manifest.Metadata));
foreach (var asset in assets)
{
if (!TemplateLoader.IsSafeRelativePath(asset.Key))
throw new InvalidDataException($"Unsicherer Assetpfad „{asset.Key}“.");
if (reservedPaths.Contains(TemplateLoader.Normalize(asset.Key)))
throw new InvalidDataException($"Assetpfad „{asset.Key}“ ist für Paketdaten reserviert.");
var entry = archive.CreateEntry(TemplateLoader.Normalize(asset.Key), CompressionLevel.Optimal);
using var stream = entry.Open(); stream.Write(asset.Value);
}
}
using (var verify = File.OpenRead(temporary)) new TemplateLoader().LoadFromPackage(verify, Path.GetFileName(outputPath));
File.Move(temporary, outputPath, overwrite: true);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
private static void WriteText(ZipArchive archive, string path, string content)
{
var entry = archive.CreateEntry(TemplateLoader.Normalize(path), CompressionLevel.Optimal);
using var writer = new StreamWriter(entry.Open()); writer.Write(content);
}
}
public sealed record InstalledTemplate(string Id, string Name, string Description, string PackagePath,
DateTime ImportedAtUtc);
public sealed class TemplateStore
{
private readonly string _directory;
private readonly ITemplateLoader _loader;
public TemplateStore(string appDataPath, ITemplateLoader? loader = null)
{
_directory = Path.Combine(appDataPath, "letter-template-packages");
Directory.CreateDirectory(_directory);
_loader = loader ?? new TemplateLoader();
}
public IReadOnlyList<InstalledTemplate> GetTemplates()
{
var result = new List<InstalledTemplate>();
foreach (var path in Directory.EnumerateFiles(_directory, $"*{TemplatePackage.Extension}"))
{
try
{
var loaded = _loader.LoadFromPackage(path);
result.Add(new(loaded.Manifest.Id, loaded.Manifest.Name, loaded.Manifest.Description, path,
File.GetLastWriteTimeUtc(path)));
}
catch (Exception ex) when (ex is InvalidDataException or TemplateValidationException) { }
}
return result.OrderBy(x => x.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
}
public InstalledTemplate Import(string sourcePath)
{
var loaded = _loader.LoadFromPackage(sourcePath);
var safeId = string.Concat(loaded.Manifest.Id.Select(c => char.IsAsciiLetterOrDigit(c) || c == '-' ? c : '-')).Trim('-');
if (safeId.Length == 0) throw new InvalidDataException("Die Vorlagen-ID ist ungültig.");
var destination = Path.Combine(_directory, safeId + TemplatePackage.Extension);
var temporary = destination + ".tmp";
File.Copy(sourcePath, temporary, overwrite: true);
File.Move(temporary, destination, overwrite: true);
return new(loaded.Manifest.Id, loaded.Manifest.Name, loaded.Manifest.Description, destination, DateTime.UtcNow);
}
public LoadedTemplate Load(InstalledTemplate template) => _loader.LoadFromPackage(template.PackagePath);
public void Delete(string id)
{
var match = GetTemplates().FirstOrDefault(x => x.Id == id);
if (match is not null && File.Exists(match.PackagePath)) File.Delete(match.PackagePath);
}
}