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 assets) { 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); foreach (var asset in assets) { if (!TemplateLoader.IsSafeRelativePath(asset.Key)) throw new InvalidDataException($"Unsicherer Assetpfad „{asset.Key}“."); 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 GetTemplates() { var result = new List(); 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); } }