File: Compression\DiscoverPrecompressedAssets.cs
Web Access
Project: ..\..\..\src\StaticWebAssetsSdk\Tasks\Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
using Microsoft.Build.Framework;
 
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
public class DiscoverPrecompressedAssets : Task
{
    private const string GzipAssetTraitValue = "gzip";
    private const string BrotliAssetTraitValue = "br";
 
    public ITaskItem[] CandidateAssets { get; set; }
 
    [Output]
    public ITaskItem[] DiscoveredCompressedAssets { get; set; }
 
    public override bool Execute()
    {
        if (CandidateAssets is null)
        {
            Log.LogMessage(
                MessageImportance.Low,
                "Skipping task '{0}' because no candidate assets for compression were specified.",
                nameof(ResolveCompressedAssets));
            return true;
        }
 
        var candidates = StaticWebAsset.FromTaskItemGroup(CandidateAssets);
        var assetsToUpdate = new List<ITaskItem>();
 
        var candidatesByIdentity = candidates.ToDictionary(asset => asset.Identity, OSPath.PathComparer);
 
        foreach (var candidate in candidates)
        {
            if (HasCompressionExtension(candidate.RelativePath) &&
                // We only care about assets that are not already considered compressed
                !IsCompressedAsset(candidate) &&
                // The candidate doesn't already have a related asset
                string.IsNullOrEmpty(candidate.RelatedAsset))
            {
                Log.LogMessage(
                    MessageImportance.Low,
                    "The asset '{0}' was detected as compressed but it didn't specify a related asset.",
                    candidate.Identity);
                var relatedAsset = FindRelatedAsset(candidate, candidatesByIdentity);
                if (relatedAsset is null)
                {
                    Log.LogMessage(
                        MessageImportance.Low,
                        "The asset '{0}' was detected as compressed but the related asset with relative path '{1}' was not found.",
                        candidate.Identity,
                        Path.GetFileNameWithoutExtension(candidate.RelativePath));
                    continue;
                }
 
                Log.LogMessage(
                    "The asset '{0}' was detected as compressed and the related asset '{1}' was found.",
                    candidate.Identity,
                    relatedAsset.Identity);
                UpdateCompressedAsset(candidate, relatedAsset);
                assetsToUpdate.Add(candidate.ToTaskItem());
            }
        }
 
        DiscoveredCompressedAssets = [.. assetsToUpdate];
 
        return !Log.HasLoggedErrors;
    }
 
    private static StaticWebAsset FindRelatedAsset(StaticWebAsset candidate, IDictionary<string, StaticWebAsset> candidates)
    {
        // The only pattern that we support is a related asset that lives in the same directory, with the same name,
        // but without the compression extension. In any other case we are not going to consider the assets related
        // and an error will occur.
        var identityWithoutExtension = candidate.Identity.Substring(0, candidate.Identity.Length - 3); // We take advantage we know the extension is .br or .gz.
        return candidates.TryGetValue(identityWithoutExtension, out var relatedAsset) ? relatedAsset : null;
    }
 
    private static bool HasCompressionExtension(string relativePath)
    {
        return relativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) ||
               relativePath.EndsWith(".br", StringComparison.OrdinalIgnoreCase);
    }
 
    private static bool IsCompressedAsset(StaticWebAsset asset)
        => string.Equals("Content-Encoding", asset.AssetTraitName, StringComparison.Ordinal);
 
    private static void UpdateCompressedAsset(StaticWebAsset asset, StaticWebAsset relatedAsset)
    {
        string fileExtension;
        string assetTraitValue;
 
        if (!asset.RelativePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
        {
            fileExtension = ".br";
            assetTraitValue = BrotliAssetTraitValue;
        }
        else
        {
            fileExtension = ".gz";
            assetTraitValue = GzipAssetTraitValue;
        }
 
        var relativePath = relatedAsset.EmbedTokens(relatedAsset.RelativePath);
 
        asset.RelativePath = $"{relativePath}{fileExtension}";
        asset.OriginalItemSpec = relatedAsset.Identity;
        asset.RelatedAsset = relatedAsset.Identity;
        asset.AssetRole = "Alternative";
        asset.AssetTraitName = "Content-Encoding";
        asset.AssetTraitValue = assetTraitValue;
    }
}