File: Compression\DiscoverPrecompressedAssets.cs
Web Access
Project: src\src\sdk\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;
    }
}