From cc783e4a3a3f4fcf9115aa9079f730b3eb40ca77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:35:50 +0000 Subject: [PATCH 1/3] Initial plan From 660d2826ca79de9d6455a623df93674aa63e3d8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:45:09 +0000 Subject: [PATCH 2/3] Convert project to .NET 8 console application with cross-platform support Co-authored-by: stefangordon <4087345+stefangordon@users.noreply.github.com> --- .gitignore | 7 ++ ASGE.csproj | 116 +++++++--------------- ASGE.sln | 22 ----- App.config | 13 --- Options.cs | 63 ++++-------- Program.cs | 92 ++++++++++++----- Properties/AssemblyInfo.cs | 36 ------- README.md | 83 ++++++++++++++-- Utility.cs | 198 ++++++++++++++++++++++--------------- packages.config | 11 --- 10 files changed, 320 insertions(+), 321 deletions(-) delete mode 100644 ASGE.sln delete mode 100644 App.config delete mode 100644 Properties/AssemblyInfo.cs delete mode 100644 packages.config diff --git a/.gitignore b/.gitignore index f1e3d20..702a8a0 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,12 @@ DocProject/Help/html # Click-Once directory publish/ +# .NET Framework to .NET 8 migration - old files +*_old.* +packages.config +App.config +Properties/AssemblyInfo.cs + # Publish Web Output *.[Pp]ublish.xml *.azurePubxml @@ -250,3 +256,4 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml +publish/ diff --git a/ASGE.csproj b/ASGE.csproj index f2a400a..6bac680 100644 --- a/ASGE.csproj +++ b/ASGE.csproj @@ -1,95 +1,47 @@ - - - + + - Debug - AnyCPU - {84285BB3-2556-45F3-965F-4667855A6EB6} Exe - Properties + net8.0 + asge ASGE - ASGE - v4.5.2 - 512 - true + true + true + linux-x64 + true + partial + false + enable - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + + + ASGE + Azure Storage GZip Encoding Tool + ASGE + Copyright © Stefan Gordon 2016 + 2.0.0.0 + 2.0.0.0 + true - - - packages\CommandLineParser.1.9.71\lib\net45\CommandLine.dll - True - - - packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll - True - - - packages\Microsoft.Data.Edm.5.6.4\lib\net40\Microsoft.Data.Edm.dll - True - - - packages\Microsoft.Data.OData.5.6.4\lib\net40\Microsoft.Data.OData.dll - True - - - packages\Microsoft.Data.Services.Client.5.6.4\lib\net40\Microsoft.Data.Services.Client.dll - True - - - packages\WindowsAzure.Storage.7.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll - True - - - packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll - True - - - - - packages\System.Spatial.5.6.4\lib\net40\System.Spatial.dll - True - - - - - - - - + - + + + + + + + + - - + + + + - - + \ No newline at end of file diff --git a/ASGE.sln b/ASGE.sln deleted file mode 100644 index 4e859bf..0000000 --- a/ASGE.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASGE", "ASGE.csproj", "{84285BB3-2556-45F3-965F-4667855A6EB6}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {84285BB3-2556-45F3-965F-4667855A6EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {84285BB3-2556-45F3-965F-4667855A6EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {84285BB3-2556-45F3-965F-4667855A6EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {84285BB3-2556-45F3-965F-4667855A6EB6}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/App.config b/App.config deleted file mode 100644 index 2239672..0000000 --- a/App.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/Options.cs b/Options.cs index 3034b06..bfcf643 100644 --- a/Options.cs +++ b/Options.cs @@ -1,73 +1,50 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using CommandLine; -using CommandLine.Text; namespace ASGE { - class Options + public class Options { - [Option('a', "account", Required = false, MutuallyExclusiveSet = "Account and Key", - HelpText = "Storage account host. [mystorage]")] - public string StorageAccount { get; set; } + [Option('a', "account", Required = false, SetName = "AccountAndKey", + HelpText = "Storage account name. [mystorage]")] + public string? StorageAccount { get; set; } - [Option('k', "key", Required = false, MutuallyExclusiveSet = "Account and Key", + [Option('k', "key", Required = false, SetName = "AccountAndKey", HelpText = "Storage account key.")] - public string StorageKey { get; set; } + public string? StorageKey { get; set; } - [Option('c', "connectionstring", Required = false, MutuallyExclusiveSet = "ConnectionString", + [Option('c', "connectionstring", Required = false, SetName = "ConnectionString", HelpText = "Storage account connection string.")] - public string ConnectionString { get; set; } + public string? ConnectionString { get; set; } - [OptionArray('e', "extensions", Required = true, + [Option('e', "extensions", Required = true, HelpText = "Extensions to operate on. [.js, .css, .dat]")] - public string[] Extensions { get; set; } + public IEnumerable Extensions { get; set; } = Array.Empty(); - [Option('r', "replace", Required = false, DefaultValue = false, + [Option('r', "replace", Required = false, Default = false, HelpText = "Replace existing files in-place.")] public bool Replace { get; set; } - [Option('s', "simulate", Required = false, DefaultValue = false, + [Option('s', "simulate", Required = false, Default = false, HelpText = "Do everything except write to blob store.")] public bool Simulate { get; set; } [Option('n', "newextension", Required = false, HelpText = "Copy file with a new postfix. [.gz]")] - public string NewExtension { get; set; } + public string? NewExtension { get; set; } [Option('f', "container", Required = true, HelpText = "Container to search in.")] - public string Container { get; set; } + public string Container { get; set; } = string.Empty; - [Option('x', "cacheage", Required = false, DefaultValue = 2592000, - HelpText = "Duration for cache control max age header, in seconds. Default 2592000 (30 days).")] + [Option('x', "cacheage", Required = false, Default = 2592000, + HelpText = "Duration for cache control max age header, in seconds. Default 2592000 (30 days).")] public int MaxAgeSeconds { get; set; } - [Option('w', "wildcardcors", Required = false, DefaultValue = false, + [Option('w', "wildcardcors", Required = false, Default = false, HelpText = "Enable wildcard CORS for this storage account.")] - public bool wildcard { get; set; } - - - [HelpOption] - public string GetUsage() - { - var help = new HelpText - { - Heading = new HeadingInfo("Azure Storage GZip Encoder", "1.0"), - Copyright = new CopyrightInfo("Stefan Gordon", 2016), - AdditionalNewLineAfterOption = true, - AddDashesToOption = true - }; - help.AddPreOptionsLine("https://github.com/stefangordon/azure-storage-gzip-encoding\n"); - help.AddPreOptionsLine("\nExample 1: asge --extensions .css .js --container assets --replace --connectionstring "); - help.AddPreOptionsLine("Example 2: asge --extensions .css .js --container assets --newextension .gz --account mystorage --key "); - help.AddPreOptionsLine("\nUse either connection string (-c) or account and key (-a and -k)."); - help.AddOptions(this); - return help; - } - + public bool Wildcard { get; set; } } -} +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index be507d7..0e99895 100644 --- a/Program.cs +++ b/Program.cs @@ -1,60 +1,98 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; +using System; using System.Threading.Tasks; using CommandLine; -using Microsoft.Azure; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Auth; -using Microsoft.WindowsAzure.Storage.Blob; +using Azure.Storage.Blobs; +using Microsoft.Extensions.Logging; namespace ASGE { class Program { - static void Main(string[] args) + static async Task Main(string[] args) { - var options = new Options(); - if (CommandLine.Parser.Default.ParseArguments(args, options)) + // Set up logging + using var loggerFactory = LoggerFactory.Create(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Information)); + var logger = loggerFactory.CreateLogger(); + + try + { + var result = await Parser.Default.ParseArguments(args) + .MapResult( + async (Options options) => await RunAsync(options, logger), + errors => Task.FromResult(1)); + + return result; + } + catch (Exception ex) + { + logger.LogError(ex, "Unhandled exception occurred"); + return 1; + } + } + + static async Task RunAsync(Options options, ILogger logger) + { + try { if (string.IsNullOrEmpty(options.NewExtension) && !options.Replace) { - Console.WriteLine("Must provide either -r (in-place replacement) or -n (new extension/postfix to append to compressed version)."); - return; + logger.LogError("Must provide either -r (in-place replacement) or -n (new extension/postfix to append to compressed version)."); + return 1; } - CloudStorageAccount storageAccount; + BlobServiceClient blobServiceClient; if (!string.IsNullOrEmpty(options.ConnectionString)) { - storageAccount = CloudStorageAccount.Parse(options.ConnectionString); + blobServiceClient = new BlobServiceClient(options.ConnectionString); } - else if (!string.IsNullOrEmpty(options.StorageAccount) && !String.IsNullOrEmpty(options.StorageKey)) + else if (!string.IsNullOrEmpty(options.StorageAccount) && !string.IsNullOrEmpty(options.StorageKey)) { - storageAccount = new CloudStorageAccount(new StorageCredentials(options.StorageAccount, options.StorageKey), true); + var connectionString = $"DefaultEndpointsProtocol=https;AccountName={options.StorageAccount};AccountKey={options.StorageKey};EndpointSuffix=core.windows.net"; + blobServiceClient = new BlobServiceClient(connectionString); } else { - options.GetUsage(); - return; + logger.LogError("Must provide either connection string (-c) or account name and key (-a and -k)."); + return 1; } - CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); - CloudBlobContainer blobContainer = blobClient.GetContainerReference(options.Container); + var containerClient = blobServiceClient.GetBlobContainerClient(options.Container); + + // Verify container exists + try + { + var exists = await containerClient.ExistsAsync(); + if (!exists.Value) + { + logger.LogError("Container '{Container}' does not exist.", options.Container); + return 1; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error accessing container '{Container}'.", options.Container); + return 1; + } // Do the compression work - Utility.EnsureGzipFiles(blobContainer, options.Extensions, options.Replace, options.NewExtension, options.MaxAgeSeconds, options.Simulate); + await Utility.EnsureGzipFilesAsync(containerClient, options.Extensions, options.Replace, options.NewExtension, options.MaxAgeSeconds, options.Simulate, logger); // Enable CORS if appropriate - if (options.wildcard) + if (options.Wildcard) { - Utility.SetWildcardCorsOnBlobService(storageAccount); + await Utility.SetWildcardCorsOnBlobServiceAsync(blobServiceClient, logger); } - Trace.TraceInformation("Complete."); + logger.LogInformation("Complete."); + return 0; + } + catch (Exception ex) + { + logger.LogError(ex, "Error occurred during execution"); + return 1; } } } -} +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs deleted file mode 100644 index b7aff2b..0000000 --- a/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ASGE")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ASGE")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("84285bb3-2556-45f3-965f-4667855a6eb6")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md index e5ceb91..5721cee 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ [![Build status](https://ci.appveyor.com/api/projects/status/5b7d5wk4pwv21htt?svg=true)](https://ci.appveyor.com/project/stefangordon/azure-storage-gzip-encoding) # Azure Storage GZip Encoding -A utility to automatically configure [HTTP Compression](https://en.wikipedia.org/wiki/HTTP_compression) for blobs in Azure Blob storage. Blobs can be consumed directly from a client browser or via Azure CDN. +A cross-platform utility to automatically configure [HTTP Compression](https://en.wikipedia.org/wiki/HTTP_compression) for blobs in Azure Blob storage. Blobs can be consumed directly from a client browser or via Azure CDN. This tool is inspired by a code sample from David Rousset for optimizing BablyonJS Assets. +## Cross-Platform Support +This tool is built on .NET 8 and runs natively on: +- **Linux** (x64) - Perfect for Jenkins pipelines and CI/CD systems +- **Windows** (x64) +- **macOS** (x64) + +Pre-compiled binaries are available for all platforms, or you can run it with the .NET 8 runtime. + ## Why Azure storage is an excellent option for storing assets and data consumed by web applications, but it is often preferable to have this data delivered to the browser compressed. Azure CDN can be used to provide compression and performance improvements on top of blob storage but has an upper limit of 1MB for HTTP compression. @@ -18,6 +26,43 @@ The utility can enumerate all of the files in a container. It then filters to f The utility can also automatically configure your storage account with wildcard CORS settings which are often desirable if serving certain types of assets through Azure CDN. ## Getting Started + +### Prerequisites +- .NET 8 runtime (if using the cross-platform binaries) +- OR use the self-contained executables that include the runtime + +### Installation Options + +#### Option 1: Self-Contained Executables (Recommended) +Download the appropriate executable for your platform: +- Linux: `asge` (no extension) +- Windows: `asge.exe` +- macOS: `asge` (no extension) + +No additional runtime installation required. + +#### Option 2: .NET Runtime Required +If you have .NET 8 installed: +```bash +dotnet run --project ASGE.csproj -- [arguments] +``` + +#### Option 3: Build from Source +```bash +# Clone the repository +git clone https://github.com/stefangordon/azure-storage-gzip-encoding +cd azure-storage-gzip-encoding + +# Build for your platform +dotnet build + +# Or publish self-contained for specific platform +dotnet publish -c Release --self-contained -r linux-x64 -o ./linux +dotnet publish -c Release --self-contained -r win-x64 -o ./windows +dotnet publish -c Release --self-contained -r osx-x64 -o ./macos +``` + +### Usage You must provide - Either an account name and key, or connection string - Container to enumerate (recursively) @@ -26,17 +71,41 @@ You must provide ## Examples -Replacing .css files in-place. Blobs will be replaced with compressed version and headers updated: -`asge.exe -e .css -f myContainer -r -a myStorageAccount -k ` +### Linux/macOS +Replacing .css files in-place. Blobs will be replaced with compressed version and headers updated: +```bash +./asge -e .css -f myContainer -r -a myStorageAccount -k +``` Copy .css and .js to a compressed version and append a .gz extension: -`asge.exe -e .css .js -f myContainer -n .gz -a myStorageAccount -k ` +```bash +./asge -e .css .js -f myContainer -n .gz -a myStorageAccount -k +``` Replacing .js files in-place and enabling CORS for the account: -`asge.exe -w -e .js -f myContainer -r -a myStorageAccount -k ` +```bash +./asge -w -e .js -f myContainer -r -a myStorageAccount -k +``` + +### Windows +Replacing .js files in-place using a connection string: +```cmd +asge.exe -e .js -f myContainer -r -c "" +``` -Replacing .js files in-place using a connection string instead of host/key: -`asge.exe -e .js -f myContainer -r -c ` +### Jenkins Pipeline Example +```groovy +pipeline { + agent any + stages { + stage('Compress Assets') { + steps { + sh './asge -e .css .js -f assets -r -c "${AZURE_STORAGE_CONNECTION_STRING}"' + } + } + } +} +``` ``` -a, --account Storage account host. [mystorage] diff --git a/Utility.cs b/Utility.cs index 808101e..6cd4669 100644 --- a/Utility.cs +++ b/Utility.cs @@ -1,134 +1,172 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Text; +using System.Threading; using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; -using Microsoft.WindowsAzure.Storage.Shared.Protocol; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Microsoft.Extensions.Logging; namespace ASGE { - static class Utility + public static class Utility { - public static void EnsureGzipFiles(CloudBlobContainer container, IEnumerable extensions, bool inPlace, string newExtension, int cacheControlMaxAgeSeconds, bool simulate) + public static async Task EnsureGzipFilesAsync(BlobContainerClient containerClient, IEnumerable extensions, bool inPlace, string? newExtension, int cacheControlMaxAgeSeconds, bool simulate, ILogger logger) { - Trace.TraceInformation("Enumerating files."); + logger.LogInformation("Enumerating files."); - string cacheControlHeader = "public, max-age=" + cacheControlMaxAgeSeconds.ToString(); + string cacheControlHeader = $"public, max-age={cacheControlMaxAgeSeconds}"; - var blobInfos = container.ListBlobs(null, true, BlobListingDetails.Metadata); + var blobs = containerClient.GetBlobsAsync(BlobTraits.Metadata); - Parallel.ForEach(blobInfos, (blobInfo) => + var tasks = new List(); + await foreach (var blobItem in blobs) { - CloudBlob gzipBlob = null; - CloudBlob blob = (CloudBlob)blobInfo; + tasks.Add(ProcessBlobAsync(containerClient, blobItem, extensions, inPlace, newExtension, cacheControlHeader, simulate, logger)); + + // Process in batches to avoid overwhelming the service + if (tasks.Count >= 10) + { + await Task.WhenAll(tasks); + tasks.Clear(); + } + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } - // Only work with desired extensions - string extension = Path.GetExtension(blobInfo.Uri.LocalPath); - if (!extensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + private static async Task ProcessBlobAsync(BlobContainerClient containerClient, BlobItem blobItem, IEnumerable extensions, bool inPlace, string? newExtension, string cacheControlHeader, bool simulate, ILogger logger) + { + // Only work with desired extensions + string extension = Path.GetExtension(blobItem.Name); + if (!extensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + var blobClient = containerClient.GetBlobClient(blobItem.Name); + BlobClient? gzipBlobClient = null; + + // Check if it is already done + if (inPlace) + { + if (string.Equals(blobItem.Properties.ContentEncoding, "gzip", StringComparison.OrdinalIgnoreCase)) { + logger.LogInformation("Skipping already compressed blob: {BlobName}", blobItem.Name); return; } + } + else + { + string gzipBlobName = blobItem.Name + newExtension; + gzipBlobClient = containerClient.GetBlobClient(gzipBlobName); - // Check if it is already done - if (inPlace) + try { - if (string.Equals(blob.Properties.ContentEncoding, "gzip", StringComparison.OrdinalIgnoreCase)) + var exists = await gzipBlobClient.ExistsAsync(); + if (exists.Value) { - Trace.TraceInformation("Skipping already compressed blob: " + blob.Name); + logger.LogInformation("Skipping already compressed blob: {BlobName}", blobItem.Name); return; } } - else + catch (Exception ex) { - string gzipUrl = blob.Name + newExtension; - gzipBlob = container.GetBlockBlobReference(gzipUrl); - - if (gzipBlob.Exists()) - { - Trace.TraceInformation("Skipping already compressed blob: " + blob.Name); - return; - } + logger.LogWarning(ex, "Error checking if blob exists: {BlobName}", gzipBlobName); + return; } + } + try + { // Compress blob contents - Trace.TraceInformation("Downloading blob: " + blob.Name); + logger.LogInformation("Downloading blob: {BlobName}", blobItem.Name); byte[] compressedBytes; + string contentType = blobItem.Properties.ContentType ?? "application/octet-stream"; - using (MemoryStream memoryStream = new MemoryStream()) + using (var memoryStream = new MemoryStream()) { using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Compress)) - using (var blobStream = blob.OpenRead()) { - blobStream.CopyTo(gzipStream); + var downloadInfo = await blobClient.DownloadStreamingAsync(); + await downloadInfo.Value.Content.CopyToAsync(gzipStream); } - + compressedBytes = memoryStream.ToArray(); } // Blob to write to - CloudBlockBlob destinationBlob; - - if (inPlace) - { - destinationBlob = (CloudBlockBlob)blob; - } - else - { - destinationBlob = (CloudBlockBlob)gzipBlob; - } + BlobClient destinationBlobClient = inPlace ? blobClient : gzipBlobClient!; if (simulate) { - Trace.TraceInformation("NOT writing blob, due to simulation: " + blob.Name); + logger.LogInformation("NOT writing blob, due to simulation: {BlobName}", blobItem.Name); } else - { + { // Upload the compressed bytes to the new blob - Trace.TraceInformation("Writing blob: " + blob.Name); - destinationBlob.UploadFromByteArray(compressedBytes, 0, compressedBytes.Length); - - // Set the blob headers - Trace.TraceInformation("Configuring headers"); - destinationBlob.Properties.CacheControl = cacheControlHeader; - destinationBlob.Properties.ContentType = blob.Properties.ContentType; - destinationBlob.Properties.ContentEncoding = "gzip"; - destinationBlob.SetProperties(); - } + logger.LogInformation("Writing blob: {BlobName}", blobItem.Name); - }); + using var uploadStream = new MemoryStream(compressedBytes); + var uploadOptions = new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders + { + CacheControl = cacheControlHeader, + ContentType = contentType, + ContentEncoding = "gzip" + } + }; + + await destinationBlobClient.UploadAsync(uploadStream, uploadOptions, cancellationToken: CancellationToken.None); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing blob: {BlobName}", blobItem.Name); + } } - public static void SetWildcardCorsOnBlobService(this CloudStorageAccount storageAccount) - { - storageAccount.SetCORSPropertiesOnBlobService(cors => - { - var wildcardRule = new CorsRule() { AllowedMethods = CorsHttpMethods.Get, AllowedOrigins = { "*" } }; - cors.CorsRules.Clear(); - cors.CorsRules.Add(wildcardRule); - return cors; - }); - } - - public static void SetCORSPropertiesOnBlobService(this CloudStorageAccount storageAccount, - Func alterCorsRules) + public static async Task SetWildcardCorsOnBlobServiceAsync(BlobServiceClient blobServiceClient, ILogger logger) { - Trace.TraceInformation("Configuring CORS."); + logger.LogInformation("Configuring CORS."); - if (storageAccount == null || alterCorsRules == null) throw new ArgumentNullException(); - - CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); - - ServiceProperties serviceProperties = blobClient.GetServiceProperties(); + try + { + var serviceProperties = await blobServiceClient.GetPropertiesAsync(); + + var corsRules = new List + { + new BlobCorsRule + { + AllowedMethods = "GET", + AllowedOrigins = "*", + AllowedHeaders = "*", + ExposedHeaders = "*", + MaxAgeInSeconds = 3600 + } + }; - serviceProperties.Cors = alterCorsRules(serviceProperties.Cors) ?? new CorsProperties(); + serviceProperties.Value.Cors.Clear(); + foreach (var rule in corsRules) + { + serviceProperties.Value.Cors.Add(rule); + } - blobClient.SetServiceProperties(serviceProperties); + await blobServiceClient.SetPropertiesAsync(serviceProperties.Value); + logger.LogInformation("CORS configuration completed."); + } + catch (Exception ex) + { + logger.LogError(ex, "Error configuring CORS"); + throw; + } } } -} +} \ No newline at end of file diff --git a/packages.config b/packages.config deleted file mode 100644 index 95c3a82..0000000 --- a/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file From 6bdf306674a2d01551646d457c5431a2c19e1f72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:46:48 +0000 Subject: [PATCH 3/3] Final cleanup - add release directory to gitignore Co-authored-by: stefangordon <4087345+stefangordon@users.noreply.github.com> --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 702a8a0..87b3acc 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,4 @@ paket-files/ .idea/ *.sln.iml publish/ +release/