/// <summary> /// Constructor. /// </summary> /// <param name="root">The root directory or <c>null</c> if this is the root.</param> /// <param name="parent">The parent directory or <c>null</c> for the root directory.</param> /// <param name="name">The directory name (this must be <c>null</c> for the root directory.</param> public StaticResourceDirectory(StaticResourceDirectory root, StaticResourceDirectory parent, string name) : base(root, parent, name) { }
/// <summary> /// Adds a subdirectory if it doesn't already exist. /// </summary> /// <param name="directory">The child resource directory.</param> /// <returns>The existing <see cref="StaticResourceDirectory"/> or <paramref name="directory"/> if it was added</returns> internal StaticResourceDirectory AddDirectory(StaticResourceDirectory directory) { Covenant.Requires <ArgumentNullException>(directory != null, nameof(directory)); return((StaticResourceDirectory)base.AddDirectory(directory)); }
//--------------------------------------------------------------------- // Implementation /// <summary> /// Returns an eumlated static file system that includes some or all of an assembly's /// embedded resources. This method returns the root <see cref="IStaticDirectory"/> /// for the file system. /// </summary> /// <param name="assembly">The target assembly.</param> /// <param name="resourcePrefix"> /// <para> /// Specifies the resource name prefix to be used to identify the embedded resources /// to be included in the static file system. See the remarks for more information. /// </para> /// </param> /// <returns>The root <see cref="IStaticDirectory"/>.</returns> /// <remarks> /// <para> /// This method maps Linux style paths (using forward not back slashes) to embedded /// resources in the assembly. Resources in .NET projects are embedded and named /// like: /// </para> /// <example> /// ASSEMBLY-NAMESPACE [ "." DIR ] "." RESOURCE-FILENAME /// </example> /// <para> /// where <b>ASSEMBLY-NAME</b> is the name of the source assembly, <b>DIR</b> optionally /// specifies the directoriea to the resource and <b>RESOURCE-FILENAME</b> specifies the name /// of the resource file. /// </para> /// <para> /// When a .NET project is built, any source files with build actions set to <b>Embedded Resource</b> /// will be included in the assembly using the naming convention described above. The /// <b>ASSEMBLY-NAMESPACE</b> will be set to the source projects default namespace and the <b>DIR</b>s /// will be set to the relative path from the project file to the source resource file. For /// example, if your project is structured like: /// </para> /// <code> /// my-project/ /// my-project.csproj /// /// top-level.txt /// resources/ /// resource1.dat /// resource2.dat /// tests/ /// test1.txt /// test2.txt /// samples/ /// sample1.txt /// sample2.txt /// </code> /// <para> /// and its default namespace is <b>company.mproject</b>, then the following resources embedded /// in your project assembly: /// </para> /// <code> /// company.my-project.top-level.txt /// company.my-project.resources.resource1.dat /// company.my-project.resources.resource2.dat /// company.my-project.resources.tests.test1.txt /// company.my-project.resources.tests.test2.txt /// company.my-project.resources.samples.sample1.txt /// company.my-project.resources.samples.sample2.txt /// </code> /// <para> /// By default, calling <see cref="NeonAssemblyExtensions.GetResourceFileSystem(Assembly, string)"/> on your project assembly /// will return a <see cref="IStaticDirectory"/> with a directory structure holding all of the resources. /// The paths are mapped from the resource names by converting any dots except for the last one into forward /// slashes. /// </para> /// <code> /// / /// company/ /// my-project/ /// top-level.txt /// resources/ /// resource1.dat /// resource2.dat /// tests/ /// test1.txt /// test2.txt /// samples/ /// sample1.txt /// sample2.txt /// </code> /// <para> /// You can also pass an optional resource name prefix so that only a subset of the resources /// are included in the file system. For example, by passing <b>company.my-project.resources</b>, /// file system returned will look like: /// </para> /// <code> /// / /// resource1.dat /// resource2.dat /// tests/ /// test1.txt /// test2.txt /// samples/ /// sample1.txt /// sample2.txt /// </code> /// <para><b>RESOURCE NAMING AMBIGUITIES</b></para> /// <para> /// When creating the file system from resource names, it's possible to encounter situations /// where it's not possible to distingish between a a directory and a file name. For example: /// </para> /// <code> /// company.my-project.resources.resource1.dat /// </code> /// <para> /// Here are some possibilities: /// </para> /// <list type="bullet"> /// <item> /// path is <b>/company.my-project.resources.resource1</b> and the file name <b>dat</b> /// </item> /// <item> /// path is <b>/company.my-project.resources</b> and the file name <b>resource1.dat</b> /// </item> /// <item> /// path is <b>/company.my-project</b> and the file name <b>.resources.resource1.dat</b> /// </item> /// </list> /// <para> /// There is really no way for this method to know what the original resource source files /// and directory paths were. To resolve this, we're going to assume that resource file /// names include a file extension with a single dot and that any additional dots will form /// the file's parent directory path. /// </para> /// <note> /// All resource file names must include an extension to be loaded properly. If you need /// a file to be loaded <b>without an extension</b>, save the file with the special <b>"._"</b> /// extension like <b>test._</b>. This method will remove that special extension when reading /// files with it. /// </note> /// <para> /// What this means is that your resource file names must include a file extension. So, file /// names like this are OK: /// </para> /// <code> /// schema.sql /// config.json /// </code> /// <para> /// You should avoid file names like: /// </para> /// <code> /// schema.1.sql /// </code> /// <para> /// and use something like a dash instead so that <b>schema</b> won't be considered to /// be part of the file's parent directory path: /// </para> /// <code> /// schema-1.sql /// </code> /// </remarks> public static IStaticDirectory GetResourceFileSystem(this Assembly assembly, string resourcePrefix = null) { if (!string.IsNullOrEmpty(resourcePrefix)) { if (resourcePrefix.Last() != '.') { resourcePrefix += '.'; } } var pathToDirectory = new Dictionary <string, StaticResourceDirectory>(StringComparer.InvariantCultureIgnoreCase); var resourceNames = assembly.GetManifestResourceNames(); var filteredResourceNames = string.IsNullOrEmpty(resourcePrefix) ? resourceNames : resourceNames .Where(name => name.StartsWith(resourcePrefix) && name != resourcePrefix); // Add the root directory. var root = new StaticResourceDirectory(root: null, parent: null, name: string.Empty); pathToDirectory[root.Name] = root; foreach (var resourceName in filteredResourceNames) { // Split the resource name into the directory path and filename parts. var trimmedName = resourceName.Substring(resourcePrefix?.Length ?? 0); var pos = trimmedName.LastIndexOf('.'); var path = (string)null; var filename = (string)null; if (pos == -1) { // Special case of a file without an extension. This can happen // when a resource prefix is used. This can only happen at the // file system root directory. path = "/"; filename = trimmedName; } else { // The second dot from the end will indicate start of the file name // when there is a second dot. pos = trimmedName.LastIndexOf('.', pos - 1); if (pos != -1) { path = "/" + trimmedName.Substring(0, pos).Replace('.', '/'); filename = trimmedName.Substring(pos + 1); } else { path = "/"; filename = trimmedName; } } // Handle the special [._] extension by removing the extension from the // file name. if (Path.GetExtension(filename) == "._") { filename = Path.GetFileNameWithoutExtension(filename); } if (!pathToDirectory.TryGetValue(path, out var directory)) { // Create the new directory, ensuring that any parent // directories exist as well. var parent = root; foreach (var directoryName in path.Split('/').Skip(1)) { if (!pathToDirectory.TryGetValue(directoryName, out directory)) { directory = parent.AddDirectory(new StaticResourceDirectory(root, parent, directoryName)); } parent = directory; } } directory.AddFile(new StaticResourceFile(LinuxPath.Combine(path, filename), assembly, resourceName)); } return(root); }