diff --git a/src/vpk/Velopack.Deployment/AzureRepository.cs b/src/vpk/Velopack.Deployment/AzureRepository.cs index 9b5445d9..4c3f47c0 100644 --- a/src/vpk/Velopack.Deployment/AzureRepository.cs +++ b/src/vpk/Velopack.Deployment/AzureRepository.cs @@ -20,8 +20,8 @@ public class AzureDownloadOptions : RepositoryOptions, IObjectDownloadOptions public string Container { get; set; } public string SasToken { get; set; } - - public string Folder { get; set; } + + public string Prefix { get; set; } } public class AzureUploadOptions : AzureDownloadOptions, IObjectUploadOptions @@ -29,13 +29,26 @@ public class AzureUploadOptions : AzureDownloadOptions, IObjectUploadOptions public int KeepMaxReleases { get; set; } } -public class AzureRepository : ObjectRepository +public class AzureBlobClient(BlobContainerClient client, string prefix) +{ + public virtual Task DeleteBlobIfExistsAsync(string key, CancellationToken cancellationToken = default) + { + return client.DeleteBlobIfExistsAsync(prefix + key, cancellationToken: cancellationToken); + } + + public virtual BlobClient GetBlobClient(string key) + { + return client.GetBlobClient(prefix + key); + } +} + +public class AzureRepository : ObjectRepository { public AzureRepository(ILogger logger) : base(logger) { } - protected override BlobContainerClient CreateClient(AzureDownloadOptions options) + protected override AzureBlobClient CreateClient(AzureDownloadOptions options) { var serviceUrl = options.Endpoint ?? "https://" + options.Account + ".blob.core.windows.net"; if (options.Endpoint == null) { @@ -54,16 +67,22 @@ public class AzureRepository : ObjectRepository { await client.DeleteBlobIfExistsAsync(key); @@ -71,7 +90,7 @@ public class AzureRepository : ObjectRepository GetObjectBytes(BlobContainerClient client, string key) + protected override async Task GetObjectBytes(AzureBlobClient client, string key) { return await RetryAsyncRet( async () => { @@ -90,12 +109,6 @@ public class AzureRepository : ObjectRepository GetReleasesAsync(AzureDownloadOptions options) { var releasesName = CoreUtil.GetVeloReleaseIndexName(options.Channel); - - // Prepend folder path if specified - if (!string.IsNullOrEmpty(options.Folder)) { - releasesName = options.Folder.TrimEnd('/') + "/" + releasesName; - } - var client = CreateClient(options); var bytes = await GetObjectBytes(client, releasesName); if (bytes == null || bytes.Length == 0) { @@ -109,40 +122,15 @@ public class AzureRepository : ObjectRepository { var client = CreateClient(options); - var key = entry.FileName; - - // Prepend folder path if specified - if (!string.IsNullOrEmpty(options.Folder)) { - key = options.Folder.TrimEnd('/') + "/" + key; - } - - var obj = client.GetBlobClient(key); + var obj = client.GetBlobClient(entry.FileName); using var response = await obj.DownloadToAsync(filePath, CancellationToken.None); }, $"Downloading {entry.FileName}..."); } - public override async Task UploadMissingAssetsAsync(AzureUploadOptions options) + + protected override async Task UploadObject(AzureBlobClient client, string key, FileInfo f, bool overwriteRemote, bool noCache) { - // Store the folder in a private field for use in UploadObject - // Note: Azure Blob Storage will handle path validation and reject invalid paths - _uploadFolder = options.Folder; - try { - await base.UploadMissingAssetsAsync(options); - } finally { - _uploadFolder = null; - } - } - - private string _uploadFolder; - - protected override async Task UploadObject(BlobContainerClient client, string key, FileInfo f, bool overwriteRemote, bool noCache) - { - // Prepend folder path if specified - if (!string.IsNullOrEmpty(_uploadFolder)) { - key = _uploadFolder.TrimEnd('/') + "/" + key; - } - var blobClient = client.GetBlobClient(key); try { var properties = await blobClient.GetPropertiesAsync(); diff --git a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureBaseCommand.cs b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureBaseCommand.cs index 18fa1c80..9ac309fb 100644 --- a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureBaseCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureBaseCommand.cs @@ -12,31 +12,37 @@ public class AzureBaseCommand : OutputCommand public string SasToken { get; private set; } + public string Prefix { get; private set; } + public double Timeout { get; private set; } protected AzureBaseCommand(string name, string description) : base(name, description) { AddOption((v) => Account = v, "--account") - .SetDescription("Account name") + .SetDescription("Account name.") .SetArgumentHelpName("ACCOUNT") .SetRequired(); var key = AddOption((v) => Key = v, "--key") - .SetDescription("Account secret key") + .SetDescription("Account secret key.") .SetArgumentHelpName("KEY"); var sas = AddOption((v) => SasToken = v, "--sas") - .SetDescription("Shared access signature token (not the url)") + .SetDescription("Shared access signature token (not the url).") .SetArgumentHelpName("TOKEN"); AddOption((v) => Container = v, "--container") - .SetDescription("Azure container name") + .SetDescription("Azure storage container name.") .SetArgumentHelpName("NAME") .SetRequired(); + AddOption((v) => Prefix = v, "--prefix") + .SetDescription("Optional blob filename path prefix.") + .SetArgumentHelpName("PREFIX"); + AddOption((v) => Endpoint = v.ToAbsoluteOrNull(), "--endpoint") - .SetDescription("Service url (eg. https://.blob.core.windows.net)") + .SetDescription("Service url (eg. https://.blob.core.windows.net).") .SetArgumentHelpName("URL") .MustBeValidHttpUri(); diff --git a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureDownloadCommand.cs b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureDownloadCommand.cs index 092eda2e..2e25e577 100644 --- a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureDownloadCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureDownloadCommand.cs @@ -2,13 +2,8 @@ public class AzureDownloadCommand : AzureBaseCommand { - public string Folder { get; private set; } - public AzureDownloadCommand() : base("az", "Download latest release from an Azure Blob Storage container.") { - AddOption((x) => Folder = x, "--folder") - .SetDescription("The folder path within the container where files are stored.") - .SetArgumentHelpName("PATH"); } } diff --git a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureUploadCommand.cs b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureUploadCommand.cs index 3dd6b3a4..aa2f8a19 100644 --- a/src/vpk/Velopack.Vpk/Commands/Deployment/AzureUploadCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Deployment/AzureUploadCommand.cs @@ -3,8 +3,6 @@ public class AzureUploadCommand : AzureBaseCommand { public int KeepMaxReleases { get; private set; } - - public string Folder { get; private set; } public AzureUploadCommand() : base("az", "Upload releases to an Azure Blob Storage container.") @@ -13,10 +11,6 @@ public class AzureUploadCommand : AzureBaseCommand .SetDescription("The maximum number of releases to keep in the container, anything older will be deleted.") .SetArgumentHelpName("COUNT"); - AddOption((x) => Folder = x, "--folder") - .SetDescription("The folder path within the container where files should be uploaded.") - .SetArgumentHelpName("PATH"); - ReleaseDirectoryOption.SetRequired(); ReleaseDirectoryOption.MustNotBeEmpty(); } diff --git a/src/vpk/Velopack.Vpk/Commands/Deployment/S3BaseCommand.cs b/src/vpk/Velopack.Vpk/Commands/Deployment/S3BaseCommand.cs index c44dfc67..7bb364bb 100644 --- a/src/vpk/Velopack.Vpk/Commands/Deployment/S3BaseCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Deployment/S3BaseCommand.cs @@ -55,7 +55,7 @@ public class S3BaseCommand : OutputCommand .SetRequired(); AddOption((v) => Prefix = v, "--prefix") - .SetDescription("Prefix to the S3 url.") + .SetDescription("Optional filename path prefix.") .SetArgumentHelpName("PREFIX"); AddOption((v) => DisablePathStyle = v, "--disablePathStyle") diff --git a/test/Velopack.CommandLine.Tests/Commands/AzureCommandTests.cs b/test/Velopack.CommandLine.Tests/Commands/AzureCommandTests.cs index 3621d540..af705176 100644 --- a/test/Velopack.CommandLine.Tests/Commands/AzureCommandTests.cs +++ b/test/Velopack.CommandLine.Tests/Commands/AzureCommandTests.cs @@ -34,10 +34,10 @@ public class AzureDownloadCommandTests : AzureCommandTests { var command = new AzureDownloadCommand(); - string cli = GetRequiredDefaultOptions() + " --folder \"releases/v1\""; + string cli = GetRequiredDefaultOptions() + " --prefix \"releases/v1\""; ParseResult parseResult = command.ParseAndApply(cli); - Assert.Equal("releases/v1", command.Folder); + Assert.Equal("releases/v1", command.Prefix); } } @@ -55,10 +55,10 @@ public class AzureUploadCommandTests : AzureCommandTests { var command = new AzureUploadCommand(); - string cli = GetRequiredDefaultOptions() + " --folder \"releases/v1\""; + string cli = GetRequiredDefaultOptions() + " --prefix \"releases/v1\""; ParseResult parseResult = command.ParseAndApply(cli); - Assert.Equal("releases/v1", command.Folder); + Assert.Equal("releases/v1", command.Prefix); } } diff --git a/test/Velopack.Packaging.Tests/AzureFolderTests.cs b/test/Velopack.Packaging.Tests/AzureFolderTests.cs deleted file mode 100644 index e8b3332f..00000000 --- a/test/Velopack.Packaging.Tests/AzureFolderTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Velopack.Deployment; -using Velopack.Core; -using Xunit; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Velopack.Packaging.Tests; - -public class AzureFolderTests -{ - [Fact] - public void AzureRepository_UploadWithFolder_PrependsPathToKeys() - { - // This test verifies that when a folder is specified, - // all blob keys (filenames) are prepended with the folder path - - var options = new AzureUploadOptions - { - Folder = "releases/v1", - Account = "testaccount", - Key = "testkey", - Container = "testcontainer" - }; - - Assert.Equal("releases/v1", options.Folder); - } - - [Fact] - public void AzureRepository_DownloadWithFolder_PrependsPathToKeys() - { - // This test verifies that download options can have a folder - - var options = new AzureDownloadOptions - { - Folder = "releases/v1", - Account = "testaccount", - Key = "testkey", - Container = "testcontainer" - }; - - Assert.Equal("releases/v1", options.Folder); - } - - [Fact] - public void AzureRepository_FolderPath_NormalizesSlashes() - { - // Test that trailing slashes are handled correctly - var options1 = new AzureUploadOptions { Folder = "releases/v1/" }; - var options2 = new AzureUploadOptions { Folder = "releases/v1" }; - - // Both should work correctly when used - Assert.Equal("releases/v1/", options1.Folder); - Assert.Equal("releases/v1", options2.Folder); - } - - [Theory] - [InlineData("releases/v1", "releases/v1/file.nupkg")] - [InlineData("releases/v1/", "releases/v1/file.nupkg")] - [InlineData("releases/v1//", "releases/v1/file.nupkg")] - [InlineData("/releases/v1", "/releases/v1/file.nupkg")] - [InlineData("", "file.nupkg")] - [InlineData(null, "file.nupkg")] - public void AzureRepository_FolderPath_ProducesCorrectBlobKey(string folder, string expectedKey) - { - // Test the logic we use to combine folder and filename - string filename = "file.nupkg"; - string result; - - if (!string.IsNullOrEmpty(folder)) - { - result = folder.TrimEnd('/') + "/" + filename; - } - else - { - result = filename; - } - - Assert.Equal(expectedKey, result); - } - -} \ No newline at end of file diff --git a/test/Velopack.Packaging.Tests/DeploymentTests.cs b/test/Velopack.Packaging.Tests/DeploymentTests.cs index 8c039fa3..629b28ae 100644 --- a/test/Velopack.Packaging.Tests/DeploymentTests.cs +++ b/test/Velopack.Packaging.Tests/DeploymentTests.cs @@ -71,7 +71,7 @@ public class DeploymentTests }; var updateUrl = $"https://{AZ_ENDPOINT}/{AZ_CONTAINER}"; - await Deploy("AZTestApp", repo, options, releaseDir, updateUrl, logger); + await Deploy("AZTestApp", repo, options, releaseDir, updateUrl, logger); } static SemanticVersion GenerateSemverFromDateTime()