diff --git a/Frends.AzureBlobStorage.UploadBlob/CHANGELOG.md b/Frends.AzureBlobStorage.UploadBlob/CHANGELOG.md index 61cabce..123fe42 100644 --- a/Frends.AzureBlobStorage.UploadBlob/CHANGELOG.md +++ b/Frends.AzureBlobStorage.UploadBlob/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [3.0.0] - 2024-11-25 +### Added +- New parameter: Destination.TargetFolder to specify a target folder path for uploaded blobs. +### Changed +- Renamed Source.BlobName to Source.RenameToBlobName and Source.BlobFolderName to Source.RenameToFolderName for improved clarity. + ## [2.1.0] - 2024-08-21 ### Updated - Updated Azure.Identity to version 1.12.0. diff --git a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob.Tests/UnitTests.cs b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob.Tests/UnitTests.cs index 24c9254..731f135 100644 --- a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob.Tests/UnitTests.cs +++ b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob.Tests/UnitTests.cs @@ -17,7 +17,7 @@ public class UnitTests private readonly string _appID = Environment.GetEnvironmentVariable("Frends_AzureBlobStorage_AppID"); private readonly string _clientSecret = Environment.GetEnvironmentVariable("Frends_AzureBlobStorage_ClientSecret"); private readonly string _tenantID = Environment.GetEnvironmentVariable("Frends_AzureBlobStorage_TenantID"); - private readonly string _storageAccount = "frendstaskstestcontainer"; + private readonly string _storageAccount = "stataskdevelopment"; private readonly string _testFileDir = Path.Combine(Environment.CurrentDirectory, "TestFiles"); private readonly string _testfile = Path.Combine(Environment.CurrentDirectory, "TestFiles", "testfile.txt"); private readonly string _testfile2 = Path.Combine(Environment.CurrentDirectory, "TestFiles", "testfile2.txt"); @@ -63,6 +63,7 @@ public void TestSetup() PageOffset = default, ParallelOperations = default, ResizeFile = default, + TargetFolder = "targetfolder", }; _destinationOA = new Destination @@ -83,6 +84,7 @@ public void TestSetup() PageOffset = default, ParallelOperations = default, ResizeFile = default, + TargetFolder = "targetfolder", }; _options = new Options() { ThrowErrorOnFailure = true }; @@ -104,6 +106,8 @@ public async Task UploadFile_WithAndWithoutTags() var _sourceWithoutTags = _source; _sourceWithoutTags.Tags = null; + _destinationCS.TargetFolder = null; + _destinationOA.TargetFolder = null; foreach (var blobtype in _blobtypes) { @@ -137,11 +141,13 @@ public async Task UploadFile_WithAndWithoutTags() [TestMethod] public async Task UploadFile_SetBlobName_ForceEncoding_ForceContentType() { - _source.BlobName = "SomeBlob"; + _source.RenameToBlobName = "SomeBlob"; _destinationCS.FileEncoding = "foo/bar"; _destinationCS.ContentType = "text/xml"; + _destinationCS.TargetFolder = null; _destinationOA.FileEncoding = "foo/bar"; _destinationOA.ContentType = "text/xml"; + _destinationOA.TargetFolder = null; var container = GetBlobContainer(_connectionString, _containerName); @@ -167,7 +173,7 @@ public async Task UploadFile_SetBlobName_ForceEncoding_ForceContentType() [TestMethod] public async Task UploadFile_Compress() { - _source.BlobName = "compress.gz"; + _source.RenameToBlobName = "compress.gz"; _source.Compress = true; var container = GetBlobContainer(_connectionString, _containerName); @@ -180,14 +186,14 @@ public async Task UploadFile_Compress() // connection string var result = await AzureBlobStorage.UploadBlob(_source, _destinationCS, _options, default); Assert.IsTrue(result.Success); - Assert.IsTrue(result.Data.ContainsValue($"{container.Uri}/compress.gz")); - Assert.IsTrue(await container.GetBlobClient("compress.gz").ExistsAsync(), "Uploaded SomeBlob blob should exist"); + Assert.IsTrue(result.Data.ContainsValue($"{container.Uri}/targetfolder/compress.gz")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/compress.gz").ExistsAsync(), "Uploaded SomeBlob blob should exist"); // OAuth var result2 = await AzureBlobStorage.UploadBlob(_source, _destinationOA, _options, default); Assert.IsTrue(result2.Success); - Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/compress.gz")); - Assert.IsTrue(await container.GetBlobClient("compress.gz").ExistsAsync(), "Uploaded SomeBlob blob should exist"); + Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/targetfolder/compress.gz")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/compress.gz").ExistsAsync(), "Uploaded SomeBlob blob should exist"); } } @@ -196,7 +202,7 @@ public async Task UploadFile_HandleExistingFile() { var errorHandlers = new List() { HandleExistingFile.Append, HandleExistingFile.Overwrite, HandleExistingFile.Error }; var _source2 = _source; - _source2.BlobName = "testfile.txt"; + _source2.RenameToBlobName = "testfile.txt"; _source2.SourceFile = _testfile2; foreach (var blobtype in _blobtypes) @@ -217,23 +223,23 @@ public async Task UploadFile_HandleExistingFile() var container = GetBlobContainer(_connectionString, _containerName); var result = await AzureBlobStorage.UploadBlob(_source, _destinationCS, _options, default); Assert.IsTrue(result.Success); - Assert.IsTrue(result.Data.ContainsValue($"{container.Uri}/testfile.txt")); - Assert.IsTrue(await container.GetBlobClient("testfile.txt").ExistsAsync(), "Uploaded testfile.txt blob should exist"); + Assert.IsTrue(result.Data.ContainsValue($"{container.Uri}/targetfolder/testfile.txt")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/testfile.txt").ExistsAsync(), "Uploaded targetfolder/testfile.txt blob should exist"); if (handler is HandleExistingFile.Append) { var result2 = await AzureBlobStorage.UploadBlob(_source2, _destinationCS, _options, default); // You can use Azure Portal to check if the blob contains 2x "Etiam dui". Assert.IsTrue(result2.Success); - Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/testfile.txt")); - Assert.IsTrue(await container.GetBlobClient("testfile.txt").ExistsAsync(), "Uploaded testfile.txt blob should exist"); + Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/targetfolder/testfile.txt")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/testfile.txt").ExistsAsync(), "Uploaded targetfolder/testfile.txt blob should exist"); } else if (handler is HandleExistingFile.Overwrite) { var result2 = await AzureBlobStorage.UploadBlob(_source, _destinationCS, _options, default); Assert.IsTrue(result2.Success); - Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/testfile.txt")); - Assert.IsTrue(await container.GetBlobClient("testfile.txt").ExistsAsync(), "Uploaded testfile.txt blob should exist"); + Assert.IsTrue(result2.Data.ContainsValue($"{container.Uri}/targetfolder/testfile.txt")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/testfile.txt").ExistsAsync(), "Uploaded targetfolder/testfile.txt blob should exist"); } else { @@ -269,10 +275,10 @@ public async Task UploadDirectory_WithAndWithoutTags() var resultWithTags = await AzureBlobStorage.UploadBlob(_source, _destinationCS, _options, default); Assert.IsTrue(resultWithTags.Success); - Assert.IsTrue(resultWithTags.Data.ContainsValue($"{container.Uri}/TestFiles/testfile.txt")); - Assert.IsTrue(await container.GetBlobClient("TestFiles/testfile.txt").ExistsAsync(), "Uploaded testfile.txt blob should exist"); - Assert.IsTrue(resultWithTags.Data.ContainsValue($"{container.Uri}/TestFiles/testfile2.txt")); - Assert.IsTrue(await container.GetBlobClient("TestFiles/testfile2.txt").ExistsAsync(), "Uploaded testfile2.txt blob should exist"); + Assert.IsTrue(resultWithTags.Data.ContainsValue($"{container.Uri}/targetfolder/TestFiles/testfile.txt")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/TestFiles/testfile.txt").ExistsAsync(), "Uploaded testfile.txt blob should exist"); + Assert.IsTrue(resultWithTags.Data.ContainsValue($"{container.Uri}/targetfolder/TestFiles/testfile2.txt")); + Assert.IsTrue(await container.GetBlobClient("targetfolder/TestFiles/testfile2.txt").ExistsAsync(), "Uploaded testfile2.txt blob should exist"); } } @@ -282,7 +288,9 @@ public async Task UploadDirectory_RenameDir() _source.SourceType = UploadSourceType.Directory; _source.SourceDirectory = _testFileDir; _source.SourceFile = default; - _source.BlobFolderName = "RenameDir"; + _source.RenameToFolderName = "RenameDir"; + _destinationCS.TargetFolder = ""; + _destinationOA.TargetFolder = ""; var container = GetBlobContainer(_connectionString, _containerName); diff --git a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Destination.cs b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Destination.cs index ee870e4..f195748 100644 --- a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Destination.cs +++ b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Destination.cs @@ -61,6 +61,12 @@ public class Destination [PasswordPropertyText] public string ClientSecret { get; set; } + /// + /// Specifies the target folder path for the blob in Azure Blob Storage. + /// This value will be prepended to blob names during upload, effectively creating a virtual directory structure. + /// + /// backups/2024/ + public string TargetFolder { get; set; } /// /// Determines if the container should be created if it does not exist. diff --git a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Source.cs b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Source.cs index db9efda..3abda0e 100644 --- a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Source.cs +++ b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/Definitions/Source.cs @@ -41,20 +41,20 @@ public class Source public string SourceFile { get; set; } /// - /// Name of the blob. If left empty, the blob's name will be the same as the source file. + /// Specifies a custom name for the blob in Azure Blob Storage. If left empty, the blob's name will be the same as the source file. /// /// Renamed.txt [UIHint(nameof(SourceType), "", UploadSourceType.File)] [DisplayFormat(DataFormatString = "Text")] - public string BlobName { get; set; } + public string RenameToBlobName { get; set; } /// - /// Name of the blob folder. If left empty, blob folder's name will be the same as source directory (e.g., 'Example' in C:\temp\Example). + /// Specifies a custom name for the blob folder in Azure Blob Storage.. If left empty, blob folder's name will be the same as source directory (e.g., 'Example' in C:\temp\Example). /// /// ExampleDir [UIHint(nameof(SourceType), "", UploadSourceType.Directory)] [DisplayFormat(DataFormatString = "Text")] - public string BlobFolderName { get; set; } + public string RenameToFolderName { get; set; } /// /// Use a stream to read the file's content diff --git a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/UploadBlob.cs b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/UploadBlob.cs index e018fab..139283b 100644 --- a/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/UploadBlob.cs +++ b/Frends.AzureBlobStorage.UploadBlob/Frends.AzureBlobStorage.UploadBlob/UploadBlob.cs @@ -33,6 +33,11 @@ public class AzureBlobStorage /// Object { bool Success, Dictionary<string, string> Data } public static async Task UploadBlob([PropertyTab] Source source, [PropertyTab] Destination destination, [PropertyTab] Options options, CancellationToken cancellationToken) { + if (destination.ContainerName.Contains('/') || destination.ContainerName.Contains('\\')) + { + throw new Exception("The container name cannot contain '/' or '\\'."); + } + var results = new Dictionary(); var fi = string.IsNullOrEmpty(source.SourceFile) ? null : new FileInfo(source.SourceFile); @@ -52,8 +57,12 @@ public static async Task UploadBlob([PropertyTab] Source source, [Proper if (fi == null) throw new FileNotFoundException($"Source file '{source.SourceFile}' was empty."); blobName = fi.Name; - if (!string.IsNullOrWhiteSpace(source.BlobName) || source.Compress) - blobName = RenameFile(!string.IsNullOrWhiteSpace(source.BlobName) ? source.BlobName : fi.Name, source.Compress, fi); + if (!string.IsNullOrWhiteSpace(source.RenameToBlobName) || source.Compress) + blobName = RenameFile(!string.IsNullOrWhiteSpace(source.RenameToBlobName) ? source.RenameToBlobName : fi.Name, source.Compress, fi); + if (!string.IsNullOrWhiteSpace(destination.TargetFolder)) + { + blobName = $"{destination.TargetFolder.TrimEnd('/', '\\')}/{blobName}"; + } results.Add(source.SourceFile, await HandleUpload(source, destination, options, fi, blobName, cancellationToken)); break; case UploadSourceType.Directory: @@ -66,12 +75,17 @@ public static async Task UploadBlob([PropertyTab] Source source, [Proper fileName = RenameFile(fileName, source.Compress, file); var parentDirectory = Path.GetFileName(Path.GetDirectoryName(file.ToString())); - var withDir = string.IsNullOrWhiteSpace(source.BlobFolderName) + var withDir = string.IsNullOrWhiteSpace(source.RenameToFolderName) ? Path.Combine(parentDirectory, fileName) - : Path.Combine(source.BlobFolderName, fileName); + : Path.Combine(source.RenameToFolderName, fileName); blobName = withDir.Replace("\\", "/"); + if (!string.IsNullOrWhiteSpace(destination.TargetFolder)) + { + blobName = $"{destination.TargetFolder.TrimEnd('/', '\\')}/{blobName}"; + } + results.Add(file.FullName, await HandleUpload(source, destination, options, file, blobName, cancellationToken)); handledFile = file.FullName; } @@ -121,7 +135,6 @@ public static async Task UploadBlob([PropertyTab] Source source, [Proper private static async Task HandleUpload(Source source, Destination destination, Options options, FileInfo fi, string blobName, CancellationToken cancellationToken) { - blobName = string.IsNullOrWhiteSpace(source.BlobName) ? blobName : source.BlobName; var contentType = string.IsNullOrWhiteSpace(destination.ContentType) ? MimeUtility.GetMimeMapping(fi.Name) : destination.ContentType; var encoding = GetEncoding(destination.FileEncoding); @@ -383,7 +396,16 @@ private static async Task AppendAny(BlobClient blob, AppendBlobClient } else { - var tempFile = Path.Combine(Path.GetTempPath(), blobName); + string normalizedBlobName = blobName.Replace('/', '\\'); + + var tempFile = Path.Combine(Path.GetTempPath(), normalizedBlobName); + + string directoryPath = Path.GetDirectoryName(tempFile); + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } if (blob != null) await blob.DownloadToAsync(tempFile, cancellationToken);