115 lines
5.8 KiB
C#
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);
|
|
}
|
|
}
|