using System.Collections.Concurrent; namespace Inspectron.Epson.Templates.Storage; public class FileTemplateStorage : ITemplateStorage { private readonly string _basePath; private readonly string _templateExtension; private readonly ConcurrentDictionary _cache = new(); public FileTemplateStorage(string basePath, string templateExtension = ".template") { _basePath = Path.GetFullPath(basePath); _templateExtension = templateExtension; if (!Directory.Exists(_basePath)) { Directory.CreateDirectory(_basePath); } } public string? Load(string templatePath) { var normalizedPath = NormalizePath(templatePath); if (_cache.TryGetValue(normalizedPath, out var cached)) { return cached; } var fullPath = GetFullPath(normalizedPath); if (!File.Exists(fullPath)) { return null; } var content = File.ReadAllText(fullPath); _cache[normalizedPath] = content; return content; } public bool Exists(string templatePath) { var normalizedPath = NormalizePath(templatePath); var fullPath = GetFullPath(normalizedPath); return File.Exists(fullPath); } public void Reload() { _cache.Clear(); } public IEnumerable ListTemplates() { if (!Directory.Exists(_basePath)) { yield break; } var files = Directory.GetFiles(_basePath, $"*{_templateExtension}", SearchOption.AllDirectories); foreach (var file in files) { var relativePath = Path.GetRelativePath(_basePath, file); yield return relativePath.Replace('\\', '/'); } } private string NormalizePath(string path) { // Ensure the path has the correct extension if (!path.EndsWith(_templateExtension, StringComparison.OrdinalIgnoreCase)) { path += _templateExtension; } // Normalize path separators return path.Replace('\\', '/'); } private string GetFullPath(string normalizedPath) { // Security: prevent directory traversal var fullPath = Path.GetFullPath(Path.Combine(_basePath, normalizedPath)); if (!fullPath.StartsWith(_basePath, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"Invalid template path: {normalizedPath}"); } return fullPath; } }