/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Shared/StaticWebAssets/ManifestStaticWebAssetFileProvider.cs
438 строк
17 KB
Jiri Cincura ↹
Fix typos in code. (#67428)
02 июл 2026, 13:29
Не верифицирован
02 июл 2026, 13:29
fa8126f
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.StaticWebAssets; internal sealed partial class ManifestStaticWebAssetFileProvider : IFileProvider { private static readonly StringComparison _fsComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; private static readonly IEqualityComparer<IFileInfo> _nameComparer = new FileNameComparer(); private readonly IFileProvider[] _fileProviders; private readonly StaticWebAssetNode _root; public ManifestStaticWebAssetFileProvider(StaticWebAssetManifest manifest, Func<string, IFileProvider> fileProviderFactory) { _fileProviders = new IFileProvider[manifest.ContentRoots.Length]; for (int i = 0; i < manifest.ContentRoots.Length; i++) { _fileProviders[i] = fileProviderFactory(manifest.ContentRoots[i]); } _root = manifest.Root; } // For testing purposes only internal IFileProvider[] FileProviders => _fileProviders; public IDirectoryContents GetDirectoryContents(string subpath) { ArgumentNullException.ThrowIfNull(subpath); var segments = Normalize(subpath).Split('/', StringSplitOptions.RemoveEmptyEntries); var candidate = _root; // Iterate over the path segments until we reach the destination. Whenever we encounter // a pattern, we start tracking it as well as the content root directory. On every segment // we evaluate the directory to see if there is a subfolder with the current segment and // replace it on the dictionary if it exists or remove it if it does not. // When we reach our destination we enumerate the files in that folder and evaluate them against // the pattern to compute the final list. HashSet<IFileInfo>? files = null; for (var i = 0; i < segments.Length; i++) { files = GetFilesForCandidatePatterns(segments, candidate, files); if (candidate.HasChildren() && candidate.Children.TryGetValue(segments[i], out var child)) { candidate = child; } else { candidate = null; break; } } if ((candidate == null || (!candidate.HasChildren() && !candidate.HasPatterns())) && files == null) { return NotFoundDirectoryContents.Singleton; } else { // We do this to make sure we account for the patterns on the last segment which are not covered by the loop above files = GetFilesForCandidatePatterns(segments, candidate, files); if (candidate != null && candidate.HasChildren()) { files ??= new(_nameComparer); GetCandidateFilesForNode(candidate, files); } return new StaticWebAssetsDirectoryContents((files as IEnumerable<IFileInfo>) ?? Array.Empty<IFileInfo>()); } HashSet<IFileInfo>? GetFilesForCandidatePatterns(string[] segments, StaticWebAssetNode? candidate, HashSet<IFileInfo>? files) { if (candidate != null && candidate.HasPatterns()) { var depth = candidate.Patterns[0].Depth; var candidateDirectoryPath = string.Join('/', segments[depth..]); foreach (var pattern in candidate.Patterns) { var contentRoot = _fileProviders[pattern.ContentRoot]; var matcher = new Matcher(_fsComparison); matcher.AddInclude(pattern.Pattern); foreach (var result in contentRoot.GetDirectoryContents(candidateDirectoryPath)) { var fileCandidate = string.IsNullOrEmpty(candidateDirectoryPath) ? result.Name : $"{candidateDirectoryPath}/{result.Name}"; if (result.Exists && (result.IsDirectory || matcher.Match(fileCandidate).HasMatches)) { files ??= new(_nameComparer); if (!files.Contains(result)) { // Multiple patterns might match the same file (even at different locations on disk) at runtime. We don't // try to disambiguate anything here, since there is already a build step for it. We just pick the first // file that matches the pattern. The manifest entries are ordered, so while this choice is random, it is // nonetheless deterministic. files.Add(result); } } } } } return files; } void GetCandidateFilesForNode(StaticWebAssetNode candidate, HashSet<IFileInfo> files) { foreach (var child in candidate.Children!) { var match = child.Value.Match; if (match == null) { // This is a folder var file = new StaticWebAssetsDirectoryInfo(child.Key); // Entries from the manifest always win over any content based on patterns, // so remove any potentially existing file or folder in favor of the manifest // entry. files.Remove(file); files.Add(file); } else { // This is a file. files.RemoveWhere(f => string.Equals(match.Path, f.Name, _fsComparison)); var file = _fileProviders[match.ContentRoot].GetFileInfo(match.Path); files.Add(string.Equals(child.Key, match.Path, _fsComparison) ? file : // This means that this file was mapped, there is a chance that we added it to the list // of files by one of the patterns, so we need to replace it with the mapped file. new StaticWebAssetsFileInfo(child.Key, file)); } } } } private static string Normalize(string path) { return path.Replace('\\', '/'); } public IFileInfo GetFileInfo(string subpath) { ArgumentNullException.ThrowIfNull(subpath); var segments = subpath.Split('/', StringSplitOptions.RemoveEmptyEntries); StaticWebAssetNode? candidate = _root; List<StaticWebAssetPattern>? patterns = null; // Iterate over the path segments until we reach the destination, collecting // all pattern candidates along the way except for any pattern at the root. for (var i = 0; i < segments.Length; i++) { if (candidate.HasPatterns()) { patterns ??= new(); patterns.AddRange(candidate.Patterns); } if (candidate.HasChildren() && candidate.Children.TryGetValue(segments[i], out var child)) { candidate = child; } else { candidate = null; break; } } var match = candidate?.Match; if (match != null) { // If we found a file, that wins over anything else. If there are conflicts with files added after // we've built the manifest, we'll be notified the next time we do a build. This is not different // from previous Static Web Assets versions. var file = _fileProviders[match.ContentRoot].GetFileInfo(match.Path); if (!file.Exists || string.Equals(subpath, Normalize(match.Path), _fsComparison)) { return file; } else { return new StaticWebAssetsFileInfo(segments[^1], file); } } // The list of patterns is ordered by pattern depth, so we compute the string to check for patterns only // once per level. We don't aim to solve conflicts here where multiple files could match a given path, // we have a build check that takes care of that. var currentDepth = -1; var candidatePath = subpath; if (patterns != null) { for (var i = 0; i < patterns.Count; i++) { var pattern = patterns[i]; if (pattern.Depth != currentDepth) { currentDepth = pattern.Depth; candidatePath = string.Join('/', segments[currentDepth..]); } var result = _fileProviders[pattern.ContentRoot].GetFileInfo(candidatePath); if (result.Exists) { if (!result.IsDirectory) { var matcher = new Matcher(); matcher.AddInclude(pattern.Pattern); if (!matcher.Match(candidatePath).HasMatches) { continue; } return result; } } } } return new NotFoundFileInfo(subpath); } public IChangeToken Watch(string filter) => NullChangeToken.Singleton; private sealed class StaticWebAssetsDirectoryContents : IDirectoryContents { private readonly IEnumerable<IFileInfo> _files; public StaticWebAssetsDirectoryContents(IEnumerable<IFileInfo> files) => _files = files; public bool Exists => true; public IEnumerator<IFileInfo> GetEnumerator() => _files.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } private sealed class StaticWebAssetsDirectoryInfo : IFileInfo { private static readonly DateTimeOffset _lastModified = DateTimeOffset.FromUnixTimeSeconds(0); public StaticWebAssetsDirectoryInfo(string name) { Name = name; } public bool Exists => true; public long Length => 0; public string? PhysicalPath => null; public DateTimeOffset LastModified => _lastModified; public bool IsDirectory => true; public string Name { get; } public Stream CreateReadStream() => throw new InvalidOperationException("Can not create a stream for a directory."); } private sealed class StaticWebAssetsFileInfo : IFileInfo { private readonly IFileInfo _source; public StaticWebAssetsFileInfo(string name, IFileInfo source) { Name = name; _source = source; } public bool Exists => _source.Exists; public long Length => _source.Length; public string PhysicalPath => _source.PhysicalPath ?? string.Empty; public DateTimeOffset LastModified => _source.LastModified; public bool IsDirectory => _source.IsDirectory; public string Name { get; } public Stream CreateReadStream() => _source.CreateReadStream(); } private sealed class FileNameComparer : IEqualityComparer<IFileInfo> { public bool Equals(IFileInfo? x, IFileInfo? y) => string.Equals(x?.Name, y?.Name, _fsComparison); public int GetHashCode(IFileInfo obj) => obj.Name.GetHashCode(_fsComparison); } internal sealed class StaticWebAssetManifest { internal static readonly StringComparer PathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; public string[] ContentRoots { get; set; } = Array.Empty<string>(); public StaticWebAssetNode Root { get; set; } = null!; internal static StaticWebAssetManifest Parse(Stream manifest) { return JsonSerializer.Deserialize( manifest, SourceGenerationContext.DefaultWithConverter.StaticWebAssetManifest)!; } } [JsonSourceGenerationOptions] [JsonSerializable(typeof(StaticWebAssetManifest))] [JsonSerializable(typeof(IDictionary<string, StaticWebAssetNode>))] internal sealed partial class SourceGenerationContext : JsonSerializerContext { public static readonly SourceGenerationContext DefaultWithConverter = new SourceGenerationContext(new JsonSerializerOptions { Converters = { new OSBasedCaseConverter() } }); } internal sealed class StaticWebAssetNode { [JsonPropertyName("Asset")] public StaticWebAssetMatch? Match { get; set; } [JsonConverter(typeof(OSBasedCaseConverter))] public Dictionary<string, StaticWebAssetNode>? Children { get; set; } public StaticWebAssetPattern[]? Patterns { get; set; } [MemberNotNullWhen(true, nameof(Children))] internal bool HasChildren() => Children != null && Children.Count > 0; [MemberNotNullWhen(true, nameof(Patterns))] internal bool HasPatterns() => Patterns != null && Patterns.Length > 0; } internal sealed class StaticWebAssetMatch { [JsonPropertyName("ContentRootIndex")] public int ContentRoot { get; set; } [JsonPropertyName("SubPath")] public string Path { get; set; } = null!; } internal sealed class StaticWebAssetPattern { [JsonPropertyName("ContentRootIndex")] public int ContentRoot { get; set; } public int Depth { get; set; } public string Pattern { get; set; } = null!; } private sealed class OSBasedCaseConverter : JsonConverter<Dictionary<string, StaticWebAssetNode>> { public override Dictionary<string, StaticWebAssetNode> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Need to recursively deserialize `Dictionary<string, StaticWebAssetNode>` but can't deserialize // that type directly because this converter will call into itself and stackoverflow. // Workaround is to deserialize to IDictionary, and then perform custom convert logic on the result. var parsed = JsonSerializer.Deserialize(ref reader, SourceGenerationContext.DefaultWithConverter.IDictionaryStringStaticWebAssetNode)!; var result = new Dictionary<string, StaticWebAssetNode>(StaticWebAssetManifest.PathComparer); MergeChildren(parsed, result); return result; static void MergeChildren( IDictionary<string, StaticWebAssetNode> newChildren, IDictionary<string, StaticWebAssetNode> existing) { foreach (var (key, value) in newChildren) { if (!existing.TryGetValue(key, out var existingNode)) { existing.Add(key, value); } else { if (value.Patterns != null) { if (existingNode.Patterns == null) { existingNode.Patterns = value.Patterns; } else { if (value.Patterns.Length > 0) { var newList = new StaticWebAssetPattern[existingNode.Patterns.Length + value.Patterns.Length]; existingNode.Patterns.CopyTo(newList, 0); value.Patterns.CopyTo(newList, existingNode.Patterns.Length); existingNode.Patterns = newList; } } } if (value.Children != null) { if (existingNode.Children == null) { existingNode.Children = value.Children; } else { if (value.Children.Count > 0) { MergeChildren(value.Children, existingNode.Children); } } } } } } } public override void Write(Utf8JsonWriter writer, Dictionary<string, StaticWebAssetNode> value, JsonSerializerOptions options) { throw new NotSupportedException(); } } }