// MIT license, https://github.com/jjxtra using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; namespace BetterEmbeddedFileProviderNamespace { public class BetterEmbeddedFileProvider : IFileProvider, IChangeToken { private static readonly string[] extensions = new string[] { ".min.js", ".min.css", ".json", ".js", ".css", ".xml", ".htm", ".html", ".zip", ".cshtml", ".txt", ".ico", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".less", ".scss" }; private static readonly DummyDisposable dummyDisposable = new DummyDisposable(); private class DummyDisposable : IDisposable { public void Dispose() { } } private class EmbeddedDirectory : IDirectoryContents { private static readonly List<EmbeddedFile> emptyList = new List<EmbeddedFile>(0); private readonly List<EmbeddedFile> files; public EmbeddedDirectory(List<EmbeddedFile> files) { this.files = files; } public bool Exists => true; public IEnumerator<IFileInfo> GetEnumerator() { return files.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return files.GetEnumerator(); } } private class EmbeddedFile : IFileInfo { private static readonly DateTime lastModified = DateTime.UtcNow; private readonly Assembly assembly; private readonly string resourcePath; private readonly string filePath; private readonly string name; private readonly long length; private readonly bool isDirectory; private readonly bool exists; public EmbeddedFile(Assembly assembly, string resourcePath, string filePath, bool isDirectory) { this.assembly = assembly; this.resourcePath = resourcePath; this.filePath = filePath; this.isDirectory = isDirectory; if (resourcePath == null) { exists = false; name = "?"; } else { exists = true; if (!isDirectory) { using (Stream s = assembly.GetManifestResourceStream(resourcePath)) { length = s.Length; } } int pos = filePath.LastIndexOf('/'); if (pos >= 0) { name = filePath.Substring(++pos); } else { name = filePath; } } } bool IFileInfo.Exists => exists; long IFileInfo.Length => length; string IFileInfo.PhysicalPath => filePath; string IFileInfo.Name => name; DateTimeOffset IFileInfo.LastModified => lastModified; bool IFileInfo.IsDirectory => isDirectory; Stream IFileInfo.CreateReadStream() { return assembly.GetManifestResourceStream(resourcePath); } } private readonly Assembly assembly; private readonly string contentRoot; private readonly Dictionary<string, List<EmbeddedFile>> folders = new Dictionary<string, List<EmbeddedFile>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, EmbeddedFile> pathsToResources = new Dictionary<string, EmbeddedFile>(StringComparer.OrdinalIgnoreCase); private readonly EmbeddedFile notFoundFile; private readonly IDirectoryContents notFoundDirectory = NotFoundDirectoryContents.Singleton; bool IChangeToken.HasChanged => false; bool IChangeToken.ActiveChangeCallbacks => false; /// <summary> /// Constructor /// </summary> /// <param name="assembly">Assembly containing resources</param> /// <param name="ns">Namespace of the resources</param> /// <param name="contentRoot">Content root, i.e. empty string or www</param> public BetterEmbeddedFileProvider(Assembly assembly, string ns, string contentRoot = "") { this.assembly = assembly; this.contentRoot = (contentRoot.Length == 0 || contentRoot == "/" ? string.Empty : ("/" + contentRoot.Trim('/'))); string[] allResources = assembly.GetManifestResourceNames(); string path; string dir; string extension; int prefixLength = ns.Length + 1; int pos; EmbeddedFile file; foreach (string s in allResources) { extension = null; foreach (string ext in extensions) { if (s.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) { extension = ext; break; } } if (extension == null) { throw new InvalidDataException("Invalid extension for embedded resource " + s); } path = "/" + s.Substring(prefixLength); path = path.Substring(0, path.Length - extension.Length); path = path.Replace('.', '/'); path += extension; pos = path.LastIndexOf('/'); pathsToResources[path] = file = new EmbeddedFile(assembly, s, path, false); if (pos >= 0) { dir = path.Substring(0, pos); if (!folders.TryGetValue(dir, out List<EmbeddedFile> files)) { folders[dir] = files = new List<EmbeddedFile>(); } files.Add(file); } } notFoundFile = new EmbeddedFile(assembly, null, null, false); } public IDirectoryContents GetDirectoryContents(string subPath) { string path = contentRoot + subPath; if (folders.TryGetValue(path, out List<EmbeddedFile> files)) { return new EmbeddedDirectory(files); } return notFoundDirectory; } public IFileInfo GetFileInfo(string subPath) { pathsToResources.TryGetValue(contentRoot + subPath, out EmbeddedFile resource); return (resource ?? notFoundFile); } public IChangeToken Watch(string filter) { return this; } IDisposable IChangeToken.RegisterChangeCallback(Action<object> callback, object state) { return dummyDisposable; } } }