| File: BrotliCompress.cs | Web Access |
| Project: src\sdk\src\BlazorWasmSdk\Tasks\Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.csproj (Microsoft.NET.Sdk.BlazorWebAssembly.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 System.IO.Hashing; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Microsoft.NET.Sdk.BlazorWebAssembly { public class BrotliCompress : ToolTask { private static readonly char[] InvalidPathChars = Path.GetInvalidFileNameChars(); private string _dotnetPath; [Required] public ITaskItem[] FilesToCompress { get; set; } [Output] public ITaskItem[] CompressedFiles { get; set; } [Required] public string OutputDirectory { get; set; } public string CompressionLevel { get; set; } public bool SkipIfOutputIsNewer { get; set; } [Required] public string ToolAssembly { get; set; } protected override string ToolName => Path.GetDirectoryName(DotNetPath); private string DotNetPath { get { if (!string.IsNullOrEmpty(_dotnetPath)) { return _dotnetPath; } _dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); if (string.IsNullOrEmpty(_dotnetPath)) { throw new InvalidOperationException("DOTNET_HOST_PATH is not set"); } return _dotnetPath; } } private static string Quote(string path) { if (string.IsNullOrEmpty(path) || (path[0] == '\"' && path[path.Length - 1] == '\"')) { // it's already quoted return path; } return $"\"{path}\""; } protected override string GenerateCommandLineCommands() => Quote(ToolAssembly); protected override string GenerateResponseFileCommands() { var builder = new StringBuilder(); builder.AppendLine("brotli"); if (!string.IsNullOrEmpty(CompressionLevel)) { builder.AppendLine("-c"); builder.AppendLine(CompressionLevel); } CompressedFiles = new ITaskItem[FilesToCompress.Length]; for (var i = 0; i < FilesToCompress.Length; i++) { var file = FilesToCompress[i]; var inputFullPath = file.GetMetadata("FullPath"); var relativePath = file.GetMetadata("RelativePath"); var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(inputFullPath, ".br")); var outputFullPath = Path.GetFullPath(outputRelativePath); var outputItem = new TaskItem(outputRelativePath, file.CloneCustomMetadata()); outputItem.SetMetadata("RelativePath", relativePath + ".br"); outputItem.SetMetadata("OriginalItemSpec", file.ItemSpec); CompressedFiles[i] = outputItem; if (!File.Exists(outputRelativePath)) { Log.LogMessage(MessageImportance.Low, "Compressing '{0}' because compressed file '{1}' does not exist.", file.ItemSpec, outputRelativePath); } else if (File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputRelativePath)) { // Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing. Log.LogMessage(MessageImportance.Low, "Skipping '{0}' because '{1}' is newer than '{2}'.", file.ItemSpec, outputRelativePath, file.ItemSpec); continue; } else { Log.LogMessage(MessageImportance.Low, "Compressing '{0}' because file is newer than '{1}'.", inputFullPath, outputRelativePath); } builder.AppendLine("-s"); builder.AppendLine(Quote(inputFullPath)); builder.AppendLine("-o"); builder.AppendLine(Quote(outputFullPath)); } return builder.ToString(); } internal static string CalculateTargetPath(string relativePath, string extension) { // RelativePath can be long and if used as-is to write the output, might result in long path issues on Windows. // Instead we'll calculate a fixed length path by hashing the input file name. This uses xXHash3 since it has no crytographic significance. var bytes = Encoding.UTF8.GetBytes(relativePath); var hashString = Convert.ToBase64String(XxHash3.Hash(bytes)); var builder = new StringBuilder(); for (var i = 0; i < 8; i++) { var c = hashString[i]; builder.Append(InvalidPathChars.Contains(c) ? '+' : c); } builder.Append(extension); return builder.ToString(); } protected override string GenerateFullPathToTool() => DotNetPath; } }