diff --git a/src/Akka.Persistence.Azure.TestHelpers/DbUtils.cs b/src/Akka.Persistence.Azure.TestHelpers/DbUtils.cs index 531ace1..93ece8c 100644 --- a/src/Akka.Persistence.Azure.TestHelpers/DbUtils.cs +++ b/src/Akka.Persistence.Azure.TestHelpers/DbUtils.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Akka.Actor; -using Microsoft.WindowsAzure.Storage; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.TestHelpers { diff --git a/src/Akka.Persistence.Azure/Akka.Persistence.Azure.csproj b/src/Akka.Persistence.Azure/Akka.Persistence.Azure.csproj index 6cd9c03..fa75f96 100644 --- a/src/Akka.Persistence.Azure/Akka.Persistence.Azure.csproj +++ b/src/Akka.Persistence.Azure/Akka.Persistence.Azure.csproj @@ -5,6 +5,7 @@ $(NetStandardLibVersion) Akka.Persistence support for Windows Azure Table storage and Azure blob storage. + 8.0 @@ -14,7 +15,9 @@ - + + + \ No newline at end of file diff --git a/src/Akka.Persistence.Azure/CloudTableExtensions.cs b/src/Akka.Persistence.Azure/CloudTableExtensions.cs index 8020d41..deae510 100644 --- a/src/Akka.Persistence.Azure/CloudTableExtensions.cs +++ b/src/Akka.Persistence.Azure/CloudTableExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.WindowsAzure.Storage.Table; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure { diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs index e82c076..4e1cbbf 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournal.cs @@ -11,8 +11,6 @@ using Akka.Persistence.Azure.Util; using Akka.Persistence.Journal; using Akka.Util.Internal; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -20,6 +18,7 @@ using System.Threading; using System.Threading.Tasks; using Akka.Configuration; +using Microsoft.Azure.Cosmos.Table; using Debug = System.Diagnostics.Debug; namespace Akka.Persistence.Azure.Journal @@ -712,7 +711,7 @@ private async Task> GetAllPersistenceIds() { var query = GenerateAllPersistenceIdsQuery(); - TableQuerySegment result = null; + TableQuerySegment result = null; var returnValue = ImmutableList.Empty; diff --git a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs index 2c6e0a3..41c8a6b 100644 --- a/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs +++ b/src/Akka.Persistence.Azure/Journal/AzureTableStorageJournalSettings.cs @@ -7,7 +7,7 @@ using System; using System.Linq; using Akka.Configuration; -using Microsoft.WindowsAzure.Storage; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.Journal { diff --git a/src/Akka.Persistence.Azure/Journal/HighestSequenceNrEntry.cs b/src/Akka.Persistence.Azure/Journal/HighestSequenceNrEntry.cs index 05ff7de..b8f6816 100644 --- a/src/Akka.Persistence.Azure/Journal/HighestSequenceNrEntry.cs +++ b/src/Akka.Persistence.Azure/Journal/HighestSequenceNrEntry.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; using System.Text; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.Journal { diff --git a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStore.cs b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStore.cs index 920d9ff..55ed21a 100644 --- a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStore.cs +++ b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStore.cs @@ -8,14 +8,17 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using Akka.Configuration; using Akka.Event; using Akka.Persistence.Azure.Util; using Akka.Persistence.Snapshot; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Blob; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; namespace Akka.Persistence.Azure.Snapshot { @@ -38,11 +41,11 @@ public class AzureBlobSnapshotStore : SnapshotStore private const string TimeStampMetaDataKey = "Timestamp"; private const string SeqNoMetaDataKey = "SeqNo"; - private readonly Lazy _container; + private readonly Lazy _containerClient; private readonly ILoggingAdapter _log = Context.GetLogger(); private readonly SerializationHelper _serialization; private readonly AzureBlobSnapshotStoreSettings _settings; - private readonly CloudStorageAccount _storageAccount; + private readonly BlobServiceClient _serviceClient; public AzureBlobSnapshotStore(Config config = null) { @@ -51,50 +54,49 @@ public AzureBlobSnapshotStore(Config config = null) ? AzurePersistence.Get(Context.System).BlobSettings : AzureBlobSnapshotStoreSettings.Create(config); - _storageAccount = _settings.Development ? - CloudStorageAccount.DevelopmentStorageAccount : - CloudStorageAccount.Parse(_settings.ConnectionString); + _serviceClient = _settings.Development ? + new BlobServiceClient("UseDevelopmentStorage=true") : + new BlobServiceClient(_settings.ConnectionString); - _container = new Lazy(() => InitCloudStorage(5).Result); + _containerClient = new Lazy(() => InitCloudStorage(5).Result); } - public CloudBlobContainer Container => _container.Value; + public BlobContainerClient Container => _containerClient.Value; - private async Task InitCloudStorage(int remainingTries) + private async Task InitCloudStorage(int remainingTries) { try { - var blobClient = _storageAccount.CreateCloudBlobClient(); - var containerRef = blobClient.GetContainerReference(_settings.ContainerName); - var op = new OperationContext(); + var blobClient = _serviceClient.GetBlobContainerClient(_settings.ContainerName); - using (var cts = new CancellationTokenSource(_settings.ConnectTimeout)) + using var cts = new CancellationTokenSource(_settings.ConnectTimeout); + if (!_settings.AutoInitialize) { - if (!_settings.AutoInitialize) - { - var exists = await containerRef.ExistsAsync(null, null, cts.Token); + var exists = await blobClient.ExistsAsync(cts.Token); - if (!exists) - { - remainingTries = 0; + if (!exists) + { + remainingTries = 0; - throw new Exception( - $"Container {_settings.ContainerName} doesn't exist. Either create it or turn auto-initialize on"); - } + throw new Exception( + $"Container {_settings.ContainerName} doesn't exist. Either create it or turn auto-initialize on"); + } - _log.Info("Successfully connected to existing container", _settings.ContainerName); + _log.Info("Successfully connected to existing container", _settings.ContainerName); - return containerRef; - } - - if (await containerRef.CreateIfNotExistsAsync(BlobContainerPublicAccessType.Container, - new BlobRequestOptions(), op, cts.Token)) - _log.Info("Created Azure Blob Container", _settings.ContainerName); - else - _log.Info("Successfully connected to existing container", _settings.ContainerName); + return blobClient; } - return containerRef; + var response = await blobClient.CreateIfNotExistsAsync( + PublicAccessType.BlobContainer, + cancellationToken: cts.Token); + + if (response.GetRawResponse().Status == (int)HttpStatusCode.Created) + _log.Info("Created Azure Blob Container", _settings.ContainerName); + else + _log.Info("Successfully connected to existing container", _settings.ContainerName); + + return blobClient; } catch (Exception ex) { @@ -123,29 +125,25 @@ protected override void PreStart() protected override async Task LoadAsync(string persistenceId, SnapshotSelectionCriteria criteria) { - var requestOptions = GenerateOptions(); - BlobResultSegment results = null; - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) + using var cts = new CancellationTokenSource(_settings.RequestTimeout); { - results = await Container.ListBlobsSegmentedAsync(SeqNoHelper.ToSnapshotSearchQuery(persistenceId), - true, - BlobListingDetails.Metadata, null, null, requestOptions, new OperationContext(), cts.Token); - } + var results = Container.GetBlobsAsync( + prefix: SeqNoHelper.ToSnapshotSearchQuery(persistenceId), + traits: BlobTraits.Metadata, + cancellationToken: cts.Token); - // if we made it down here, the initial request succeeded. + var pageEnumerator = results.AsPages().GetAsyncEnumerator(cts.Token); - async Task FilterAndFetch(BlobResultSegment segment) - { + if (!await pageEnumerator.MoveNextAsync()) + return null; + + // TODO: see if there's ever a scenario where the most recent snapshots aren't in the first page of the pagination list. // apply filter criteria - var filtered = segment.Results - .Where(x => x is CloudBlockBlob) - .Cast() + var filtered = pageEnumerator.Current.Values .Where(x => FilterBlobSeqNo(criteria, x)) .Where(x => FilterBlobTimestamp(criteria, x)) - .OrderByDescending(x => FetchBlobSeqNo(x)) // ordering matters - get highest seqNo item - .ThenByDescending(x => - FetchBlobTimestamp( - x)) // if there are multiple snapshots taken at same SeqNo, need latest timestamp + .OrderByDescending(FetchBlobSeqNo) // ordering matters - get highest seqNo item + .ThenByDescending(FetchBlobTimestamp) // if there are multiple snapshots taken at same SeqNo, need latest timestamp .FirstOrDefault(); // couldn't find what we were looking for. Onto the next part of the query @@ -153,160 +151,101 @@ async Task FilterAndFetch(BlobResultSegment segment) if (filtered == null) return null; - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) - using (var memoryStream = new MemoryStream()) - { - await filtered.DownloadToStreamAsync(memoryStream, AccessCondition.GenerateEmptyCondition(), - GenerateOptions(), new OperationContext(), cts.Token); + using var memoryStream = new MemoryStream(); + var blobClient = Container.GetBlockBlobClient(filtered.Name); + var downloadInfo = await blobClient.DownloadAsync(cts.Token); + await downloadInfo.Value.Content.CopyToAsync(memoryStream); - var snapshot = _serialization.SnapshotFromBytes(memoryStream.ToArray()); + var snapshot = _serialization.SnapshotFromBytes(memoryStream.ToArray()); - var returnValue = - new SelectedSnapshot( - new SnapshotMetadata( - persistenceId, - FetchBlobSeqNo(filtered), - new DateTime(FetchBlobTimestamp(filtered))), - snapshot.Data); + var result = + new SelectedSnapshot( + new SnapshotMetadata( + persistenceId, + FetchBlobSeqNo(filtered), + new DateTime(FetchBlobTimestamp(filtered))), + snapshot.Data); - return returnValue; - } + return result; } - - // TODO: see if there's ever a scenario where the most recent snapshots aren't in the beginning of the pagination list. - var result = await FilterAndFetch(results); - return result; } protected override async Task SaveAsync(SnapshotMetadata metadata, object snapshot) { - var blob = Container.GetBlockBlobReference(metadata.ToSnapshotBlobId()); + var blobClient = Container.GetBlockBlobClient(metadata.ToSnapshotBlobId()); var snapshotData = _serialization.SnapshotToBytes(new Serialization.Snapshot(snapshot)); - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) + using var cts = new CancellationTokenSource(_settings.RequestTimeout); + var blobMetadata = new Dictionary { - blob.Metadata.Add(TimeStampMetaDataKey, metadata.Timestamp.Ticks.ToString()); - + [TimeStampMetaDataKey] = metadata.Timestamp.Ticks.ToString(), /* * N.B. No need to convert the key into the Journal format we use here. * The blobs themselves don't have their sort order affected by * the presence of this metadata, so we should just save the SeqNo * in a format that can be easily deserialized later. */ - blob.Metadata.Add(SeqNoMetaDataKey, metadata.SequenceNr.ToString()); - - await blob.UploadFromByteArrayAsync( - snapshotData, - 0, - snapshotData.Length, - AccessCondition.GenerateEmptyCondition(), - GenerateOptions(), - new OperationContext(), - cts.Token); - } + [SeqNoMetaDataKey] = metadata.SequenceNr.ToString() + }; + + using var stream = new MemoryStream(snapshotData); + await blobClient.UploadAsync( + stream, + metadata: blobMetadata, + cancellationToken: cts.Token); } protected override async Task DeleteAsync(SnapshotMetadata metadata) { - var blob = Container.GetBlockBlobReference(metadata.ToSnapshotBlobId()); - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) - { - await blob.DeleteIfExistsAsync(DeleteSnapshotsOption.None, AccessCondition.GenerateEmptyCondition(), - GenerateOptions(), new OperationContext(), - cts.Token); - } + var blobClient = Container.GetBlobClient(metadata.ToSnapshotBlobId()); + + using var cts = new CancellationTokenSource(_settings.RequestTimeout); + await blobClient.DeleteIfExistsAsync(cancellationToken: cts.Token); } protected override async Task DeleteAsync(string persistenceId, SnapshotSelectionCriteria criteria) { - var requestOptions = GenerateOptions(); - BlobResultSegment results = null; - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) + using var cts = new CancellationTokenSource(_settings.RequestTimeout); + var items = Container.GetBlobsAsync( + prefix: SeqNoHelper.ToSnapshotSearchQuery(persistenceId), + traits: BlobTraits.Metadata, + cancellationToken: cts.Token); + + var filtered = items + .Where(x => FilterBlobSeqNo(criteria, x)) + .Where(x => FilterBlobTimestamp(criteria, x)); + + var deleteTasks = new List(); + await foreach (var blob in filtered.WithCancellation(cts.Token)) { - /* - * Query only the metadata - don't need to stream the entire blob back to us - * in order to delete it from storage in the next request. - */ - results = await Container.ListBlobsSegmentedAsync(SeqNoHelper.ToSnapshotSearchQuery(persistenceId), - true, - BlobListingDetails.Metadata, null, null, requestOptions, new OperationContext(), cts.Token); + var blobClient = Container.GetBlobClient(blob.Name); + deleteTasks.Add(blobClient.DeleteIfExistsAsync(cancellationToken: cts.Token)); } - // if we made it down here, the initial request succeeded. - - async Task FilterAndDelete(BlobResultSegment segment) - { - // apply filter criteria - var filtered = segment.Results.Where(x => x is CloudBlockBlob) - .Cast() - .Where(x => FilterBlobSeqNo(criteria, x)) - .Where(x => FilterBlobTimestamp(criteria, x)); - - var deleteTasks = new List(); - using (var cts = new CancellationTokenSource(_settings.RequestTimeout)) - { - foreach (var blob in filtered) - deleteTasks.Add(blob.DeleteIfExistsAsync(DeleteSnapshotsOption.None, - AccessCondition.GenerateEmptyCondition(), - GenerateOptions(), new OperationContext(), cts.Token)); - - await Task.WhenAll(deleteTasks); - } - } - - var continuationToken = results.ContinuationToken; - var deleteTask = FilterAndDelete(results); - - while (continuationToken != null) - { - // get the next round of results in parallel with the deletion of the previous - var nextResults = await Container.ListBlobsSegmentedAsync(continuationToken); - - // finish our previous delete tasks - await deleteTask; - - // start next round of deletes - deleteTask = FilterAndDelete(nextResults); - - // move the loop forward if there are more results to be processed still - continuationToken = nextResults.ContinuationToken; - } - - // wait for the final delete operation to complete - await deleteTask; + await Task.WhenAll(deleteTasks); } - private static bool FilterBlobSeqNo(SnapshotSelectionCriteria criteria, CloudBlob x) + private static bool FilterBlobSeqNo(SnapshotSelectionCriteria criteria, BlobItem x) { var seqNo = FetchBlobSeqNo(x); return seqNo <= criteria.MaxSequenceNr && seqNo >= criteria.MinSequenceNr; } - private static long FetchBlobSeqNo(CloudBlob x) + private static long FetchBlobSeqNo(BlobItem x) { return long.Parse(x.Metadata[SeqNoMetaDataKey]); } - private static bool FilterBlobTimestamp(SnapshotSelectionCriteria criteria, CloudBlob x) + private static bool FilterBlobTimestamp(SnapshotSelectionCriteria criteria, BlobItem x) { var ticks = FetchBlobTimestamp(x); return ticks <= criteria.MaxTimeStamp.Ticks && (!criteria.MinTimestamp.HasValue || ticks >= criteria.MinTimestamp.Value.Ticks); } - private static long FetchBlobTimestamp(CloudBlob x) + private static long FetchBlobTimestamp(BlobItem x) { return long.Parse(x.Metadata[TimeStampMetaDataKey]); } - - private BlobRequestOptions GenerateOptions() - { - return GenerateOptions(_settings); - } - - private static BlobRequestOptions GenerateOptions(AzureBlobSnapshotStoreSettings settings) - { - return new BlobRequestOptions { MaximumExecutionTime = settings.RequestTimeout }; - } } } \ No newline at end of file diff --git a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs index c75f978..8a8f086 100644 --- a/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs +++ b/src/Akka.Persistence.Azure/Snapshot/AzureBlobSnapshotStoreSettings.cs @@ -6,7 +6,7 @@ using System; using Akka.Configuration; -using Microsoft.WindowsAzure.Storage; +using Akka.Persistence.Azure.Util; namespace Akka.Persistence.Azure.Snapshot { diff --git a/src/Akka.Persistence.Azure/TableEntities/AllPersistenceIdsEntry.cs b/src/Akka.Persistence.Azure/TableEntities/AllPersistenceIdsEntry.cs index f2f5005..5c329ba 100644 --- a/src/Akka.Persistence.Azure/TableEntities/AllPersistenceIdsEntry.cs +++ b/src/Akka.Persistence.Azure/TableEntities/AllPersistenceIdsEntry.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.TableEntities { diff --git a/src/Akka.Persistence.Azure/TableEntities/EventTagEntry.cs b/src/Akka.Persistence.Azure/TableEntities/EventTagEntry.cs index 02b3c31..831ebbb 100644 --- a/src/Akka.Persistence.Azure/TableEntities/EventTagEntry.cs +++ b/src/Akka.Persistence.Azure/TableEntities/EventTagEntry.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using Akka.Persistence.Azure.Util; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.TableEntities { diff --git a/src/Akka.Persistence.Azure/TableEntities/HighestSequenceNrEntry.cs b/src/Akka.Persistence.Azure/TableEntities/HighestSequenceNrEntry.cs index 6c3f848..5c12e33 100644 --- a/src/Akka.Persistence.Azure/TableEntities/HighestSequenceNrEntry.cs +++ b/src/Akka.Persistence.Azure/TableEntities/HighestSequenceNrEntry.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.TableEntities { diff --git a/src/Akka.Persistence.Azure/TableEntities/PersistentJournalEntry.cs b/src/Akka.Persistence.Azure/TableEntities/PersistentJournalEntry.cs index c6bec5d..8ba2aa6 100644 --- a/src/Akka.Persistence.Azure/TableEntities/PersistentJournalEntry.cs +++ b/src/Akka.Persistence.Azure/TableEntities/PersistentJournalEntry.cs @@ -6,11 +6,10 @@ using Akka.Persistence.Azure.Journal; using Akka.Persistence.Azure.Util; -using Microsoft.WindowsAzure.Storage; -using Microsoft.WindowsAzure.Storage.Table; using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Azure.Cosmos.Table; namespace Akka.Persistence.Azure.TableEntities { diff --git a/src/Akka.Persistence.Azure/Util/NameValidator.cs b/src/Akka.Persistence.Azure/Util/NameValidator.cs new file mode 100644 index 0000000..ca1dd43 --- /dev/null +++ b/src/Akka.Persistence.Azure/Util/NameValidator.cs @@ -0,0 +1,192 @@ +// ----------------------------------------------------------------------------------------- +// +// Copyright 2013 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------------------- + +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Akka.Persistence.Azure.Util +{ + /// + /// Provides helpers to validate resource names across the Microsoft Azure Storage Services. + /// + public static class NameValidator + { + private const int BlobFileDirectoryMinLength = 1; + private const int ContainerShareQueueTableMinLength = 3; + private const int ContainerShareQueueTableMaxLength = 63; + private const int FileDirectoryMaxLength = 255; + private const int BlobMaxLength = 1024; + private static readonly string[] ReservedFileNames = { ".", "..", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "PRN", "AUX", "NUL", "CON", "CLOCK$" }; + private static readonly RegexOptions RegexOptions = RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant; + private static readonly Regex FileDirectoryRegex = new Regex(@"^[^""\\/:|<>*?]*\/{0,1}$", RegexOptions); + private static readonly Regex ShareContainerQueueRegex = new Regex("^[a-z0-9]+(-[a-z0-9]+)*$", RegexOptions); + private static readonly Regex TableRegex = new Regex("^[A-Za-z][A-Za-z0-9]*$", RegexOptions); + private static readonly Regex MetricsTableRegex = new Regex(@"^\$Metrics(HourPrimary|MinutePrimary|HourSecondary|MinuteSecondary)?(Transactions)(Blob|Queue|Table)$", RegexOptions); + + /// + /// Checks if a container name is valid. + /// + /// A string representing the container name to validate. + public static void ValidateContainerName(string containerName) + { + if (!("$root".Equals(containerName, StringComparison.Ordinal) || "$logs".Equals(containerName, StringComparison.Ordinal))) + { + ValidateShareContainerQueueHelper(containerName, "container"); + } + } + + /// + /// Checks if a queue name is valid. + /// + /// A string representing the queue name to validate. + public static void ValidateQueueName(string queueName) + { + ValidateShareContainerQueueHelper(queueName, "queue"); + } + + /// + /// Checks if a share name is valid. + /// + /// A string representing the share name to validate. + public static void ValidateShareName(string shareName) + { + ValidateShareContainerQueueHelper(shareName, "share"); + } + + private static void ValidateShareContainerQueueHelper(string resourceName, string resourceType) + { + if (string.IsNullOrWhiteSpace(resourceName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. The {0} name may not be null, empty, or whitespace only.", resourceType)); + } + + if (resourceName.Length < ContainerShareQueueTableMinLength || resourceName.Length > ContainerShareQueueTableMaxLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name length. The {0} name must be between {1} and {2} characters long.", resourceType, ContainerShareQueueTableMinLength, ContainerShareQueueTableMaxLength)); + } + + if (!ShareContainerQueueRegex.IsMatch(resourceName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. Check MSDN for more information about valid {0} naming.", resourceType)); + } + } + + /// + /// Checks if a blob name is valid. + /// + /// A string representing the blob name to validate. + public static void ValidateBlobName(string blobName) + { + if (string.IsNullOrWhiteSpace(blobName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. The {0} name may not be null, empty, or whitespace only.", "blob")); + } + + if (blobName.Length < BlobFileDirectoryMinLength || blobName.Length > BlobMaxLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name length. The {0} name must be between {1} and {2} characters long.", "blob", BlobFileDirectoryMinLength, BlobMaxLength)); + } + + int slashCount = 0; + foreach (char c in blobName) + { + if (c == '/') + { + slashCount++; + } + } + + // 254 slashes means 255 path segments; max 254 segments for blobs, 255 includes container. + if (slashCount >= 254) + { + throw new ArgumentException("The count of URL path segments (strings between '/' characters) as part of the blob name cannot exceed 254."); + } + } + + /// + /// Checks if a file name is valid. + /// + /// A string representing the file name to validate. + public static void ValidateFileName(string fileName) + { + ValidateFileDirectoryHelper(fileName, "file"); + + if (fileName.EndsWith("/", StringComparison.Ordinal)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. Check MSDN for more information about valid {0} naming.", "file")); + } + + foreach (string s in ReservedFileNames) + { + if (s.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. This {0} name is reserved.", "file")); + } + } + } + + /// + /// Checks if a directory name is valid. + /// + /// A string representing the directory name to validate. + public static void ValidateDirectoryName(string directoryName) + { + ValidateFileDirectoryHelper(directoryName, "directory"); + } + + private static void ValidateFileDirectoryHelper(string resourceName, string resourceType) + { + if (string.IsNullOrWhiteSpace(resourceName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. The {0} name may not be null, empty, or whitespace only.", resourceType)); + } + + if (resourceName.Length < BlobFileDirectoryMinLength || resourceName.Length > FileDirectoryMaxLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name length. The {0} name must be between {1} and {2} characters long.", resourceType, BlobFileDirectoryMinLength, FileDirectoryMaxLength)); + } + + if (!FileDirectoryRegex.IsMatch(resourceName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. Check MSDN for more information about valid {0} naming.", resourceType)); + } + } + + /// + /// Checks if a table name is valid. + /// + /// A string representing the table name to validate. + public static void ValidateTableName(string tableName) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. The {0} name may not be null, empty, or whitespace only.", "table")); + } + + if (tableName.Length < ContainerShareQueueTableMinLength || tableName.Length > ContainerShareQueueTableMaxLength) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name length. The {0} name must be between {1} and {2} characters long.", "table", ContainerShareQueueTableMinLength, ContainerShareQueueTableMaxLength)); + } + + if (!(TableRegex.IsMatch(tableName) || MetricsTableRegex.IsMatch(tableName) || tableName.Equals("$MetricsCapacityBlob", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Invalid {0} name. Check MSDN for more information about valid {0} naming.", "table")); + } + } + } +} \ No newline at end of file