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