diff --git a/src/Ramstack.FileSystem.Abstractions/Internal/ThrowHelper.cs b/src/Ramstack.FileSystem.Abstractions/Internal/ThrowHelper.cs
index b8ea490..7ef8725 100644
--- a/src/Ramstack.FileSystem.Abstractions/Internal/ThrowHelper.cs
+++ b/src/Ramstack.FileSystem.Abstractions/Internal/ThrowHelper.cs
@@ -21,8 +21,8 @@ public static void ChangesNotSupported() =>
[DoesNotReturn]
public static void PathMappingFailed()
{
- const string message = "The virtual path could not be mapped to a physical path. The parent directory may not exist or be accessible.";
- throw new InvalidOperationException(message);
+ const string Message = "The virtual path could not be mapped to a physical path. The parent directory may not exist or be accessible.";
+ throw new InvalidOperationException(Message);
}
///
diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs
index 2ffcac9..3d5f8cd 100644
--- a/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs
+++ b/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs
@@ -196,4 +196,58 @@ protected virtual async ValueTask CopyCoreAsync(string destinationPath, bool ove
await using var source = await OpenReadAsync(cancellationToken).ConfigureAwait(false);
await FileSystem.WriteFileAsync(destinationPath, source, overwrite, cancellationToken).ConfigureAwait(false);
}
+
+ ///
+ /// Asynchronously copies the contents of the current to the specified destination .
+ ///
+ /// The destination where the contents will be copied to.
+ /// to overwrite an existing file; to throw an exception if the file already exists.
+ /// An optional cancellation token to cancel the operation.
+ ///
+ /// A that represents the asynchronous copy operation.
+ ///
+ ///
+ ///
+ /// - If the file does not exist, it will be created.
+ /// - If it exists and is , the existing file will be overwritten.
+ /// - If is and the file exists, an exception will be thrown.
+ ///
+ ///
+ public ValueTask CopyToAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken = default)
+ {
+ if (destination.IsReadOnly)
+ ThrowHelper.ChangesNotSupported();
+
+ return CopyToCoreAsync(destination, overwrite, cancellationToken);
+ }
+
+ ///
+ /// Core implementation for asynchronously copying the contents of the current to the specified destination .
+ ///
+ /// The destination where the contents will be copied to.
+ /// to overwrite an existing file; to throw an exception if the file already exists.
+ /// An optional cancellation token to cancel the operation.
+ ///
+ /// A that represents the asynchronous copy operation.
+ ///
+ ///
+ ///
+ /// - If the file does not exist, it will be created.
+ /// - If it exists and is , the existing file will be overwritten.
+ /// - If is and the file exists, an exception will be thrown.
+ ///
+ ///
+ protected virtual async ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken)
+ {
+ if (FileSystem == destination.FileSystem)
+ {
+ await CopyAsync(destination.FullName, overwrite, cancellationToken).ConfigureAwait(false);
+ destination.Refresh();
+ }
+ else
+ {
+ await using var stream = await OpenReadAsync(cancellationToken).ConfigureAwait(false);
+ await destination.WriteAsync(stream, overwrite, cancellationToken).ConfigureAwait(false);
+ }
+ }
}
diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs
index 9bf0860..1065130 100644
--- a/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs
+++ b/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs
@@ -61,37 +61,6 @@ public static ValueTask WriteAsync(this VirtualFile file, Stream stream, Cancell
public static ValueTask CopyToAsync(this VirtualFile file, VirtualFile destination, CancellationToken cancellationToken = default) =>
file.CopyToAsync(destination, overwrite: false, cancellationToken);
- ///
- /// Asynchronously copies the contents of the current to the specified destination .
- ///
- /// The source to copy from.
- /// The destination where the contents will be copied to.
- /// to overwrite an existing file; to throw an exception if the file already exists.
- /// A token to cancel the operation. Defaults to .
- ///
- /// A that represents the asynchronous copy operation.
- ///
- ///
- ///
- /// - If the file does not exist, it will be created.
- /// - If it exists and is , the existing file will be overwritten.
- /// - If is and the file exists, an exception will be thrown.
- ///
- ///
- public static async ValueTask CopyToAsync(this VirtualFile file, VirtualFile destination, bool overwrite, CancellationToken cancellationToken = default)
- {
- if (file.FileSystem == destination.FileSystem)
- {
- await file.CopyAsync(destination.FullName, overwrite, cancellationToken).ConfigureAwait(false);
- destination.Refresh();
- }
- else
- {
- await using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false);
- await destination.WriteAsync(stream, overwrite, cancellationToken).ConfigureAwait(false);
- }
- }
-
///
/// Asynchronously returns the size of the specified file in bytes.
///
diff --git a/src/Ramstack.FileSystem.Amazon/AccessControl.cs b/src/Ramstack.FileSystem.Amazon/AccessControl.cs
new file mode 100644
index 0000000..cfdb0ff
--- /dev/null
+++ b/src/Ramstack.FileSystem.Amazon/AccessControl.cs
@@ -0,0 +1,66 @@
+namespace Ramstack.FileSystem.Amazon;
+
+///
+/// An enumeration of all possible CannedACLs that can be used
+/// for S3 Buckets or S3 Objects. For more information about CannedACLs, refer to
+/// .
+///
+public enum AccessControl
+{
+ ///
+ /// Owner gets FULL_CONTROL.
+ /// No one else has access rights (default).
+ ///
+ NoAcl,
+
+ ///
+ /// Owner gets FULL_CONTROL.
+ /// No one else has access rights (default).
+ ///
+ Private,
+
+ ///
+ /// Owner gets FULL_CONTROL and the anonymous principal is granted READ access.
+ /// If this policy is used on an object, it can be read from a browser with no authentication.
+ ///
+ PublicRead,
+
+ ///
+ /// Owner gets FULL_CONTROL, the anonymous principal is granted READ and WRITE access.
+ /// This can be a useful policy to apply to a bucket, but is generally not recommended.
+ ///
+ PublicReadWrite,
+
+ ///
+ /// Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon
+ /// S3 user is granted READ access.
+ ///
+ AuthenticatedRead,
+
+ ///
+ /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an
+ /// Amazon Machine Image (AMI) bundle from Amazon S3.
+ ///
+ AwsExecRead,
+
+ ///
+ /// Object Owner gets FULL_CONTROL, Bucket Owner gets READ
+ /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket.
+ /// You use this ACL to let someone other than the bucket owner write content (get full control)
+ /// in the bucket but still grant the bucket owner read access to the objects.
+ ///
+ BucketOwnerRead,
+
+ ///
+ /// Object Owner gets FULL_CONTROL, Bucket Owner gets FULL_CONTROL.
+ /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket.
+ /// You use this ACL to let someone other than the bucket owner write content (get full control)
+ /// in the bucket but still grant the bucket owner full rights over the objects.
+ ///
+ BucketOwnerFullControl,
+
+ ///
+ /// The LogDelivery group gets WRITE and READ_ACP permissions on the bucket.
+ ///
+ LogDeliveryWrite
+}
diff --git a/src/Ramstack.FileSystem.Amazon/AmazonFile.cs b/src/Ramstack.FileSystem.Amazon/AmazonFile.cs
deleted file mode 100644
index 58b0c35..0000000
--- a/src/Ramstack.FileSystem.Amazon/AmazonFile.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using System.Net;
-
-using Amazon.S3;
-using Amazon.S3.Model;
-
-namespace Ramstack.FileSystem.Amazon;
-
-///
-/// Represents an implementation of that maps a file to an object in Amazon S3.
-///
-internal sealed class AmazonFile : VirtualFile
-{
- private readonly AmazonS3FileSystem _fs;
- private readonly string _key;
-
- ///
- public override IVirtualFileSystem FileSystem => _fs;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The file system associated with this file.
- /// The path to the file.
- public AmazonFile(AmazonS3FileSystem fileSystem, string path) : base(path) =>
- (_fs, _key) = (fileSystem, path[1..]);
-
- ///
- protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken)
- {
- try
- {
- var metadata = await _fs.AmazonClient
- .GetObjectMetadataAsync(
- new GetObjectMetadataRequest { BucketName = _fs.BucketName, Key = _key },
- cancellationToken)
- .ConfigureAwait(false);
-
- return VirtualNodeProperties.CreateFileProperties(
- creationTime: default,
- lastAccessTime: default,
- lastWriteTime: metadata.LastModified,
- length: metadata.ContentLength);
- }
- catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound)
- {
- return null;
- }
- }
-
- ///
- protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken)
- {
- var response = await _fs.AmazonClient
- .GetObjectAsync(
- new GetObjectRequest { BucketName = _fs.BucketName, Key = _key },
- cancellationToken)
- .ConfigureAwait(false);
-
- return response.ResponseStream;
- }
-
- ///
- protected override async ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken)
- {
- var response = await _fs.AmazonClient
- .InitiateMultipartUploadAsync(_fs.BucketName, _key, cancellationToken)
- .ConfigureAwait(false);
-
- return new AmazonS3UploadStream(_fs.AmazonClient, _fs.BucketName, _key, response.UploadId);
- }
-
- ///
- protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken)
- {
- var request = new PutObjectRequest
- {
- BucketName = _fs.BucketName,
- Key = _key,
- InputStream = stream,
- AutoCloseStream = false
- };
-
- if (!overwrite)
- request.IfNoneMatch = "*";
-
- await _fs.AmazonClient
- .PutObjectAsync(request, cancellationToken)
- .ConfigureAwait(false);
- }
-
- ///
- protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken)
- {
- try
- {
- await _fs.AmazonClient
- .DeleteObjectAsync(
- new DeleteObjectRequest { BucketName = _fs.BucketName, Key = _key },
- cancellationToken)
- .ConfigureAwait(false);
- }
- catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
- {
- }
- }
-}
diff --git a/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs b/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs
index 5ea59ad..cb3c295 100644
--- a/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs
+++ b/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs
@@ -81,14 +81,14 @@ public AmazonS3FileSystem(string awsAccessKeyId, string awsSecretAccessKey, Regi
public VirtualFile GetFile(string path)
{
path = VirtualPath.GetFullPath(path);
- return new AmazonFile(this, path);
+ return new S3File(this, path);
}
///
public VirtualDirectory GetDirectory(string path)
{
path = VirtualPath.GetFullPath(path);
- return new AmazonDirectory(this, path);
+ return new S3Directory(this, path);
}
///
@@ -102,7 +102,18 @@ public void Dispose() =>
///
/// A representing the asynchronous operation.
///
- public async ValueTask CreateBucketAsync(CancellationToken cancellationToken = default)
+ public ValueTask CreateBucketAsync(CancellationToken cancellationToken = default) =>
+ CreateBucketAsync(AccessControl.NoAcl, cancellationToken);
+
+ ///
+ /// Creates the S3 bucket if it does not already exist.
+ ///
+ /// The ACL to apply to the bucket.
+ /// A cancellation token to cancel the operation.
+ ///
+ /// A representing the asynchronous operation.
+ ///
+ public async ValueTask CreateBucketAsync(AccessControl accessControl, CancellationToken cancellationToken = default)
{
var exists = AmazonS3Util
.DoesS3BucketExistV2Async(AmazonClient, BucketName)
@@ -114,7 +125,20 @@ public async ValueTask CreateBucketAsync(CancellationToken cancellationToken = d
var request = new PutBucketRequest
{
BucketName = BucketName,
- UseClientRegion = true
+ UseClientRegion = true,
+ CannedACL = accessControl switch
+ {
+ AccessControl.NoAcl => S3CannedACL.NoACL,
+ AccessControl.Private => S3CannedACL.Private,
+ AccessControl.PublicRead => S3CannedACL.PublicRead,
+ AccessControl.PublicReadWrite => S3CannedACL.PublicReadWrite,
+ AccessControl.AuthenticatedRead => S3CannedACL.AuthenticatedRead,
+ AccessControl.AwsExecRead => S3CannedACL.AWSExecRead,
+ AccessControl.BucketOwnerRead => S3CannedACL.BucketOwnerRead,
+ AccessControl.BucketOwnerFullControl => S3CannedACL.BucketOwnerFullControl,
+ AccessControl.LogDeliveryWrite => S3CannedACL.LogDeliveryWrite,
+ _ => throw new ArgumentOutOfRangeException(nameof(accessControl))
+ }
};
await AmazonClient
diff --git a/src/Ramstack.FileSystem.Amazon/AmazonDirectory.cs b/src/Ramstack.FileSystem.Amazon/S3Directory.cs
similarity index 87%
rename from src/Ramstack.FileSystem.Amazon/AmazonDirectory.cs
rename to src/Ramstack.FileSystem.Amazon/S3Directory.cs
index 9197505..acae6a1 100644
--- a/src/Ramstack.FileSystem.Amazon/AmazonDirectory.cs
+++ b/src/Ramstack.FileSystem.Amazon/S3Directory.cs
@@ -10,7 +10,7 @@ namespace Ramstack.FileSystem.Amazon;
/// Represents an implementation of that maps a directory
/// to a path within a specified Amazon S3 bucket.
///
-internal sealed class AmazonDirectory : VirtualDirectory
+internal sealed class S3Directory : VirtualDirectory
{
private readonly AmazonS3FileSystem _fs;
private readonly string _prefix;
@@ -19,11 +19,11 @@ internal sealed class AmazonDirectory : VirtualDirectory
public override IVirtualFileSystem FileSystem => _fs;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The file system associated with this directory.
/// The path to the directory within the specified Amazon S3 bucket.
- public AmazonDirectory(AmazonS3FileSystem fileSystem, string path) : base(path)
+ public S3Directory(AmazonS3FileSystem fileSystem, string path) : base(path)
{
_fs = fileSystem;
_prefix = FullName == "/" ? "" : $"{FullName[1..]}/";
@@ -96,10 +96,10 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En
.ConfigureAwait(false);
foreach (var prefix in response.CommonPrefixes)
- yield return new AmazonDirectory(_fs, VirtualPath.Normalize(prefix));
+ yield return new S3Directory(_fs, VirtualPath.Normalize(prefix));
foreach (var obj in response.S3Objects)
- yield return new AmazonFile(_fs, VirtualPath.Normalize(obj.Key));
+ yield return new S3File(_fs, VirtualPath.Normalize(obj.Key));
request.ContinuationToken = response.NextContinuationToken;
}
diff --git a/src/Ramstack.FileSystem.Amazon/S3File.cs b/src/Ramstack.FileSystem.Amazon/S3File.cs
new file mode 100644
index 0000000..8247759
--- /dev/null
+++ b/src/Ramstack.FileSystem.Amazon/S3File.cs
@@ -0,0 +1,165 @@
+using System.Net;
+
+using Amazon.S3;
+using Amazon.S3.Model;
+
+namespace Ramstack.FileSystem.Amazon;
+
+///
+/// Represents an implementation of that maps a file to an object in Amazon S3.
+///
+internal sealed class S3File : VirtualFile
+{
+ private readonly AmazonS3FileSystem _fs;
+ private readonly string _key;
+
+ ///
+ public override IVirtualFileSystem FileSystem => _fs;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system associated with this file.
+ /// The path to the file.
+ public S3File(AmazonS3FileSystem fileSystem, string path) : base(path) =>
+ (_fs, _key) = (fileSystem, path[1..]);
+
+ ///
+ protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var metadata = await _fs.AmazonClient
+ .GetObjectMetadataAsync(
+ new GetObjectMetadataRequest { BucketName = _fs.BucketName, Key = _key },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ return VirtualNodeProperties.CreateFileProperties(
+ creationTime: default,
+ lastAccessTime: default,
+ lastWriteTime: metadata.LastModified,
+ length: metadata.ContentLength);
+ }
+ catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+
+ ///
+ protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken)
+ {
+ var response = await _fs.AmazonClient
+ .GetObjectAsync(
+ new GetObjectRequest { BucketName = _fs.BucketName, Key = _key },
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ return response.ResponseStream;
+ }
+
+ ///
+ protected override async ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken)
+ {
+ var response = await _fs.AmazonClient
+ .InitiateMultipartUploadAsync(_fs.BucketName, _key, cancellationToken)
+ .ConfigureAwait(false);
+
+ return new S3UploadStream(_fs.AmazonClient, _fs.BucketName, _key, response.UploadId);
+ }
+
+ ///
+ protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken)
+ {
+ var request = new PutObjectRequest
+ {
+ BucketName = _fs.BucketName,
+ Key = _key,
+ InputStream = stream,
+ AutoCloseStream = false
+ };
+
+ if (!overwrite)
+ request.IfNoneMatch = "*";
+
+ await _fs.AmazonClient
+ .PutObjectAsync(request, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await _fs.AmazonClient
+ .DeleteObjectAsync(
+ new DeleteObjectRequest { BucketName = _fs.BucketName, Key = _key },
+ cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
+ {
+ }
+ }
+
+ ///
+ protected override ValueTask CopyCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) =>
+ CopyObjectAsync(_fs.BucketName, _key, _fs.BucketName, destinationPath, overwrite, cancellationToken);
+
+ ///
+ protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken)
+ {
+ return destination switch
+ {
+ S3File destinationFile => CopyObjectAsync(_fs.BucketName, _key, destinationFile._fs.BucketName, destinationFile._key, overwrite, cancellationToken),
+ _ => base.CopyToCoreAsync(destination, overwrite, cancellationToken)
+ };
+ }
+
+ ///
+ /// Asynchronously copies an object from the source bucket and key to the destination bucket and key.
+ ///
+ /// The name of the source S3 bucket.
+ /// The key of the source object in the S3 bucket.
+ /// The name of the destination S3 bucket.
+ /// The key of the destination object in the S3 bucket.
+ /// A boolean value indicating whether to overwrite the destination object if it already exists.
+ /// A cancellation token to cancel the operation.
+ ///
+ /// A that represents the asynchronous copy operation.
+ ///
+ private async ValueTask CopyObjectAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, bool overwrite, CancellationToken cancellationToken)
+ {
+ // Unfortunately, Amazon S3 does not support destination conditions,
+ // so we make a separate request to check for the destination object existence.
+
+ if (!overwrite)
+ {
+ try
+ {
+ await _fs.AmazonClient
+ .GetObjectMetadataAsync(destinationBucket, destinationKey, cancellationToken)
+ .ConfigureAwait(false);
+
+ throw new AmazonS3Exception($"An object already exists at destination: {destinationKey}");
+ }
+ catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound)
+ {
+ }
+ }
+
+ var request = new CopyObjectRequest
+ {
+ SourceBucket = sourceBucket,
+ SourceKey = sourceKey,
+ DestinationBucket = destinationBucket,
+ DestinationKey = destinationKey
+ };
+
+ await _fs.AmazonClient
+ .CopyObjectAsync(request, cancellationToken)
+ .ConfigureAwait(false);
+ }
+}
diff --git a/src/Ramstack.FileSystem.Amazon/AmazonS3UploadStream.cs b/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs
similarity index 63%
rename from src/Ramstack.FileSystem.Amazon/AmazonS3UploadStream.cs
rename to src/Ramstack.FileSystem.Amazon/S3UploadStream.cs
index 453a895..c4089db 100644
--- a/src/Ramstack.FileSystem.Amazon/AmazonS3UploadStream.cs
+++ b/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics.CodeAnalysis;
using Amazon.S3;
using Amazon.S3.Model;
@@ -12,7 +12,7 @@ namespace Ramstack.FileSystem.Amazon;
/// This stream accumulates data in a temporary buffer and uploads it to S3 in parts
/// once the buffer reaches a predefined size.
///
-internal sealed class AmazonS3UploadStream : Stream
+internal sealed class S3UploadStream : Stream
{
private const long PartSize = 5 * 1024 * 1024;
@@ -57,37 +57,30 @@ public override long Position
}
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Amazon S3 client used for uploading parts.
/// The name of the S3 bucket where the data will be uploaded.
/// The key (path) in the S3 bucket where the data will be stored.
- /// The ID of the multipart upload session.
- public AmazonS3UploadStream(IAmazonS3 client, string bucketName, string key, string uploadId)
+ /// The multipart upload session identifier.
+ public S3UploadStream(IAmazonS3 client, string bucketName, string key, string uploadId)
{
_client = client;
_bucketName = bucketName;
_key = key;
_uploadId = uploadId;
- _stream = CreateTempStream();
_partETags = [];
- static FileStream CreateTempStream()
- {
- const FileOptions Options =
- FileOptions.DeleteOnClose |
- FileOptions.Asynchronous;
-
- return new FileStream(
- Path.Combine(
- Path.GetTempPath(),
- Path.GetRandomFileName()),
- FileMode.CreateNew,
- FileAccess.ReadWrite,
- FileShare.None,
- bufferSize: 4096,
- Options);
- }
+ _stream = new FileStream(
+ Path.Combine(
+ Path.GetTempPath(),
+ Path.GetRandomFileName()),
+ FileMode.CreateNew,
+ FileAccess.ReadWrite,
+ FileShare.None,
+ bufferSize: 4096,
+ FileOptions.DeleteOnClose
+ | FileOptions.Asynchronous);
}
///
@@ -118,12 +111,8 @@ public override void Write(ReadOnlySpan buffer)
}
///
- public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
- if (_stream.Length >= PartSize)
- await UploadPartAsync(cancellationToken).ConfigureAwait(false);
- }
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
+ WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
///
public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = new CancellationToken())
@@ -185,19 +174,9 @@ await _client
.CompleteMultipartUploadAsync(request)
.ConfigureAwait(false);
}
- catch (Exception)
+ catch
{
- var request = new AbortMultipartUploadRequest
- {
- BucketName = _bucketName,
- Key = _key,
- UploadId = _uploadId
- };
-
- await _client
- .AbortMultipartUploadAsync(request)
- .ConfigureAwait(false);
-
+ await AbortAsync(CancellationToken.None).ConfigureAwait(false);
throw;
}
finally
@@ -232,35 +211,71 @@ private async ValueTask UploadPartAsync(CancellationToken cancellationToken)
if (_stream.Length != 0 || _partETags.Count == 0)
{
- _stream.Position = 0;
-
- // TODO: Check limit
- // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
- // The maximum allowed part size is 5 gigabytes.
- // Perhaps we should split a file exceeding this limit
- // into smaller parts and upload them separately.
-
- var request = new UploadPartRequest
+ try
{
- BucketName = _bucketName,
- Key = _key,
- UploadId = _uploadId,
- PartNumber = _partETags.Count + 1,
- InputStream = _stream,
- PartSize = _stream.Length
- };
-
- var response = await _client
- .UploadPartAsync(request, cancellationToken)
- .ConfigureAwait(false);
-
- _partETags.Add(new PartETag(response));
-
- _stream.Position = 0;
- _stream.SetLength(0);
+ _stream.Position = 0;
+
+ // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html
+ // The maximum allowed part size is 5 gigabytes.
+
+ var request = new UploadPartRequest
+ {
+ BucketName = _bucketName,
+ Key = _key,
+ UploadId = _uploadId,
+ PartNumber = _partETags.Count + 1,
+ InputStream = _stream,
+ PartSize = _stream.Length
+ };
+
+ var response = await _client
+ .UploadPartAsync(request, cancellationToken)
+ .ConfigureAwait(false);
+
+ _partETags.Add(new PartETag(response));
+
+ _stream.Position = 0;
+ _stream.SetLength(0);
+ }
+ catch
+ {
+ await AbortAsync(cancellationToken).ConfigureAwait(false);
+ throw;
+ }
}
}
+ ///
+ /// Aborts the multipart upload session.
+ ///
+ /// A cancellation token to cancel the operation.
+ ///
+ /// A that represents the asynchronous abort operation.
+ ///
+ ///
+ /// This method sends a request to Amazon S3 to abort the multipart upload identified by the
+ /// . Once aborted, the upload cannot be resumed, and any uploaded parts
+ /// will be deleted.
+ ///
+ private async ValueTask AbortAsync(CancellationToken cancellationToken)
+ {
+ var request = new AbortMultipartUploadRequest
+ {
+ BucketName = _bucketName,
+ Key = _key,
+ UploadId = _uploadId
+ };
+
+ await _client
+ .AbortMultipartUploadAsync(request, cancellationToken)
+ .ConfigureAwait(false);
+
+ // Prevent subsequent writes to the stream.
+ await _stream
+ .DisposeAsync()
+ .ConfigureAwait(false);
+ }
+
///
/// Throws a .
///
diff --git a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs
index 31d904c..db27fc2 100644
--- a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs
+++ b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs
@@ -46,12 +46,12 @@ protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken
///
protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken)
{
- var collection = _fs.Container
+ var collection = _fs.AzureClient
.GetBlobsAsync(
prefix: GetPrefix(FullName),
cancellationToken: cancellationToken);
- var client = _fs.Container.GetBlobBatchClient();
+ var client = _fs.AzureClient.GetBlobBatchClient();
var batch = client.CreateBatch();
// https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch#remarks
@@ -62,7 +62,7 @@ protected override async ValueTask DeleteCoreAsync(CancellationToken cancellatio
{
foreach (var blob in page.Values)
{
- batch.DeleteBlob(_fs.Container.Name, blob.Name, DeleteSnapshotsOption.IncludeSnapshots, conditions: null);
+ batch.DeleteBlob(_fs.AzureClient.Name, blob.Name, DeleteSnapshotsOption.IncludeSnapshots, conditions: null);
if (batch.RequestCount != MaxSubRequests)
continue;
@@ -103,7 +103,7 @@ static bool Processed(ReadOnlyCollection exceptions)
///
protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
- var collection = _fs.Container
+ var collection = _fs.AzureClient
.GetBlobsByHierarchyAsync(
delimiter: "/",
prefix: GetPrefix(FullName),
@@ -119,7 +119,7 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En
///
protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
- var collection = _fs.Container
+ var collection = _fs.AzureClient
.GetBlobsByHierarchyAsync(
delimiter: "/",
prefix: GetPrefix(FullName),
@@ -134,7 +134,7 @@ protected override async IAsyncEnumerable GetFilesCoreAsync([Enumer
///
protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
- var collection = _fs.Container
+ var collection = _fs.AzureClient
.GetBlobsByHierarchyAsync(
delimiter: "/",
prefix: GetPrefix(FullName),
diff --git a/src/Ramstack.FileSystem.Azure/AzureFile.cs b/src/Ramstack.FileSystem.Azure/AzureFile.cs
index 45920bc..f1dd7ba 100644
--- a/src/Ramstack.FileSystem.Azure/AzureFile.cs
+++ b/src/Ramstack.FileSystem.Azure/AzureFile.cs
@@ -70,10 +70,7 @@ protected override ValueTask OpenWriteCoreAsync(CancellationToken cancel
///
protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken)
{
- var options = new BlobUploadOptions
- {
- HttpHeaders = _fs.GetBlobHeaders(FullName)
- };
+ var options = new BlobUploadOptions();
if (!overwrite)
{
@@ -83,7 +80,6 @@ protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, Cance
};
}
-
var task = GetBlobClient().UploadAsync(stream, options, cancellationToken);
return new ValueTask(task);
}
@@ -99,16 +95,42 @@ protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken
}
///
- protected override async ValueTask CopyCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken)
+ protected override ValueTask CopyCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken)
+ {
+ var source = GetBlobClient();
+ var destination = _fs.CreateBlobClient(destinationPath);
+ return CopyBlobAsync(source, destination, overwrite, cancellationToken);
+ }
+
+ ///
+ protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken)
+ {
+ return destination switch
+ {
+ AzureFile destinationFile => CopyBlobAsync(GetBlobClient(), destinationFile.GetBlobClient(), overwrite, cancellationToken),
+ _ => base.CopyToCoreAsync(destination, overwrite, cancellationToken)
+ };
+ }
+
+ ///
+ /// Asynchronously copies a source blob to the specified destination.
+ ///
+ /// The source blob client.
+ /// The destination blob client.
+ /// A boolean value indicating whether to overwrite the destination blob if it already exists.
+ /// A cancellation token to cancel the operation.
+ ///
+ /// A representing the asynchronous operation.
+ ///
+ private static async ValueTask CopyBlobAsync(BlobClient source, BlobClient destination, bool overwrite, CancellationToken cancellationToken)
{
- var destClient = _fs.CreateBlobClient(destinationPath);
var conditions = !overwrite
? new BlobRequestConditions { IfNoneMatch = new ETag("*") }
: null;
- var operation = await destClient
+ var operation = await destination
.StartCopyFromUriAsync(
- GetBlobClient().Uri,
+ source.Uri,
destinationConditions: conditions,
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -119,23 +141,15 @@ await operation
cancellationToken)
.ConfigureAwait(false);
- var properties = (
- await destClient
- .GetPropertiesAsync(cancellationToken: cancellationToken)
- .ConfigureAwait(false)
- ).Value;
+ BlobProperties properties = await destination
+ .GetPropertiesAsync(cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
if (properties.CopyStatus != CopyStatus.Success)
{
var message = $"Error while copying file. {properties.CopyStatus}: {properties.CopyStatusDescription}";
throw new InvalidOperationException(message);
}
-
- await destClient
- .SetHttpHeadersAsync(
- _fs.GetBlobHeaders(destinationPath),
- cancellationToken: cancellationToken)
- .ConfigureAwait(false);
}
///
diff --git a/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs b/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs
index 9d987db..9d03a3d 100644
--- a/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs
+++ b/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs
@@ -1,5 +1,7 @@
using System.Diagnostics;
+using Azure.Core;
+using Azure.Storage;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
@@ -18,28 +20,79 @@ public sealed class AzureFileSystem : IVirtualFileSystem
public bool IsReadOnly { get; init; }
///
- /// Gets the options used to configure the file system.
+ /// Gets the instance.
///
- public AzureFileSystemOptions Options { get; }
+ internal BlobContainerClient AzureClient { get; }
///
- /// Gets the instance.
+ /// Initializes a new instance of the class.
///
- internal BlobContainerClient Container { get; }
+ /// A connection string includes the authentication information required for your application
+ /// to access data in an Azure Storage account at runtime.
+ ///
+ /// For more information,
+ /// Configure Azure Storage connection strings
+ ///
+ /// The name of the blob container in the storage account to reference.
+ public AzureFileSystem(string connectionString, string containerName) =>
+ AzureClient = new BlobContainerClient(connectionString, containerName);
///
- /// Initializes a new instance of the class using the specified container name and configuration options.
+ /// Initializes a new instance of the class.
///
- /// The name of the container in Azure Blob Storage.
- /// The used to configure the file system.
- public AzureFileSystem(string containerName, AzureFileSystemOptions options)
- {
- Options = options;
- Container = new BlobContainerClient(options.ConnectionString, containerName);
+ /// A connection string includes the authentication information required for your application
+ /// to access data in an Azure Storage account at runtime.
+ ///
+ /// For more information,
+ /// Configure Azure Storage connection strings
+ ///
+ /// The name of the blob container in the storage account to reference.
+ /// Optional client options that define the transport pipeline policies
+ /// for authentication, retries, etc., that are applied to every request.
+ public AzureFileSystem(string connectionString, string containerName, BlobClientOptions? options) =>
+ AzureClient = new BlobContainerClient(connectionString, containerName, options);
- Container.CreateIfNotExists();
- Container.SetAccessPolicy(options.Public ? PublicAccessType.Blob : PublicAccessType.None);
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A referencing the blob container
+ /// that includes the name of the account and the name of the container. This is likely to be similar
+ /// to https://{account_name}.blob.core.windows.net/{container_name}.
+ /// Optional client options that define the transport pipeline policies
+ /// for authentication, retries, etc., that are applied to every request.
+ public AzureFileSystem(Uri containerUri, BlobClientOptions? options = null) =>
+ AzureClient = new BlobContainerClient(containerUri, options);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A referencing the blob container
+ /// that includes the name of the account and the name of the container. This is likely to be similar
+ /// to https://{account_name}.blob.core.windows.net/{container_name}.
+ /// The shared key credential used to sign requests.
+ /// Optional client options that define the transport pipeline policies
+ /// for authentication, retries, etc., that are applied to every request.
+ public AzureFileSystem(Uri containerUri, StorageSharedKeyCredential credential, BlobClientOptions? options = null) =>
+ AzureClient = new BlobContainerClient(containerUri, credential, options);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A referencing the blob container
+ /// that includes the name of the account and the name of the container. This is likely to be similar
+ /// to https://{account_name}.blob.core.windows.net/{container_name}.
+ /// The token credential used to sign requests.
+ /// Optional client options that define the transport pipeline policies
+ /// for authentication, retries, etc., that are applied to every request.
+ public AzureFileSystem(Uri containerUri, TokenCredential credential, BlobClientOptions? options = null) =>
+ AzureClient = new BlobContainerClient(containerUri, credential, options);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance used to interact with the Azure Blob storage container.
+ public AzureFileSystem(BlobContainerClient client) =>
+ AzureClient = client;
///
public VirtualDirectory GetDirectory(string path) =>
@@ -49,6 +102,50 @@ public VirtualDirectory GetDirectory(string path) =>
public VirtualFile GetFile(string path) =>
new AzureFile(this, VirtualPath.GetFullPath(path));
+ ///
+ /// Asynchronously creates the container in Azure Blob Storage if it does not already exist.
+ ///
+ /// An optional cancellation token to cancel the operation.
+ ///
+ /// A representing the asynchronous operation.
+ ///
+ public ValueTask CreateContainerAsync(CancellationToken cancellationToken = default) =>
+ CreateContainerAsync(PublicAccessType.None, cancellationToken);
+
+ ///
+ /// Asynchronously creates the container in Azure Blob Storage if it does not already exist.
+ ///
+ /// Specifies whether data in the container may be accessed publicly and the level of access.
+ ///
+ /// -
+ ///
+ /// : Specifies full public read access for both the container and blob data.
+ /// Clients can enumerate blobs within the container via anonymous requests, but cannot enumerate containers within the storage account.
+ ///
+ ///
+ /// -
+ ///
+ /// : Specifies public read access for blobs only. Blob data within this container can be
+ /// read via anonymous requests, but container data is not available. Clients cannot enumerate blobs within the container via anonymous requests.
+ ///
+ ///
+ /// -
+ ///
+ /// : Specifies that the container data is private to the account owner.
+ ///
+ ///
+ ///
+ ///
+ /// An optional cancellation token to cancel the operation.
+ ///
+ /// A representing the asynchronous operation.
+ ///
+ public ValueTask CreateContainerAsync(PublicAccessType accessType, CancellationToken cancellationToken = default)
+ {
+ var task = AzureClient.CreateIfNotExistsAsync(accessType, cancellationToken: cancellationToken);
+ return new ValueTask(task);
+ }
+
///
void IDisposable.Dispose()
{
@@ -64,20 +161,6 @@ void IDisposable.Dispose()
internal BlobClient CreateBlobClient(string path)
{
Debug.Assert(path == VirtualPath.GetFullPath(path));
- return Container.GetBlobClient(path[1..]);
- }
-
- ///
- /// Returns the for the blob located at the specified path.
- ///
- /// The path to the blob.
- ///
- /// The associated with the blob at the specified path.
- ///
- internal BlobHttpHeaders GetBlobHeaders(string path)
- {
- var headers = new BlobHttpHeaders();
- Options.HeadersUpdate(this, new HeadersUpdateEventArgs(path, headers));
- return headers;
+ return AzureClient.GetBlobClient(path[1..]);
}
}
diff --git a/src/Ramstack.FileSystem.Azure/AzureFileSystemOptions.cs b/src/Ramstack.FileSystem.Azure/AzureFileSystemOptions.cs
deleted file mode 100644
index 62c7a85..0000000
--- a/src/Ramstack.FileSystem.Azure/AzureFileSystemOptions.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Azure.Storage.Blobs.Models;
-
-namespace Ramstack.FileSystem.Azure;
-
-///
-/// Represents options for configuring an Azure Blob file system.
-///
-public sealed class AzureFileSystemOptions
-{
- ///
- /// Gets or sets the connection string for Azure Blob storage.
- ///
- public string? ConnectionString { get; init; }
-
- ///
- /// Gets or sets a value indicating whether blobs should be publicly accessible.
- ///
- public bool Public { get; init; }
-
- ///
- /// Occurs when blob HTTP headers need to be updated, allowing users to modify headers before AzureBlob is uploaded to Azure.
- ///
- public event EventHandler? OnHeadersUpdate;
-
- ///
- /// Raises the event, notifying subscribers to update blob HTTP headers.
- ///
- /// The source of the event.
- /// The containing the headers to update.
- internal void HeadersUpdate(object sender, HeadersUpdateEventArgs e) =>
- OnHeadersUpdate?.Invoke(sender, e);
-}
diff --git a/src/Ramstack.FileSystem.Azure/HeadersUpdateEventArgs.cs b/src/Ramstack.FileSystem.Azure/HeadersUpdateEventArgs.cs
deleted file mode 100644
index 5aec5d9..0000000
--- a/src/Ramstack.FileSystem.Azure/HeadersUpdateEventArgs.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Azure.Storage.Blobs.Models;
-
-namespace Ramstack.FileSystem.Azure;
-
-///
-/// Provides event arguments containing the path and blob HTTP headers.
-///
-/// The path associated with the blob.
-/// The blob HTTP headers to update.
-public sealed class HeadersUpdateEventArgs(string path, BlobHttpHeaders headers) : EventArgs
-{
- ///
- /// Gets the path associated with the blob.
- ///
- public string Path => path;
-
- ///
- /// Gets the blob HTTP headers to update.
- ///
- public BlobHttpHeaders Headers => headers;
-}
diff --git a/src/Ramstack.FileSystem.Physical/PhysicalFile.cs b/src/Ramstack.FileSystem.Physical/PhysicalFile.cs
index 1ef4b45..9e895e6 100644
--- a/src/Ramstack.FileSystem.Physical/PhysicalFile.cs
+++ b/src/Ramstack.FileSystem.Physical/PhysicalFile.cs
@@ -43,8 +43,13 @@ internal PhysicalFile(PhysicalFileSystem fileSystem, string path, string physica
///
protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken)
{
- const FileOptions Options = FileOptions.Asynchronous | FileOptions.SequentialScan;
- var stream = new FileStream(_physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, Options);
+ // SequentialScan is a performance hint that requires extra sys-call on non-Windows systems.
+ // https://github.com/dotnet/runtime/blob/46c9a4fff83f35ec659e6659050440aadccf3201/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L694
+ var options = Path.DirectorySeparatorChar == '\\'
+ ? FileOptions.Asynchronous | FileOptions.SequentialScan
+ : FileOptions.Asynchronous;
+
+ var stream = new FileStream(_physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, options);
return new ValueTask(stream);
}
@@ -54,9 +59,11 @@ protected override ValueTask OpenWriteCoreAsync(CancellationToken cancel
{
EnsureDirectoryExists();
- const FileOptions Options = FileOptions.Asynchronous | FileOptions.SequentialScan;
+ var options = Path.DirectorySeparatorChar == '\\'
+ ? FileOptions.Asynchronous | FileOptions.SequentialScan
+ : FileOptions.Asynchronous;
- var stream = new FileStream(_physicalPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, DefaultBufferSize, Options);
+ var stream = new FileStream(_physicalPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, DefaultBufferSize, options);
// Since, FileMode.OpenOrCreate doesn't truncate the file, we manually
// set the file length to zero to remove any leftover data.
@@ -70,14 +77,16 @@ protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite,
{
EnsureDirectoryExists();
- const FileOptions Options = FileOptions.Asynchronous | FileOptions.SequentialScan;
+ var options = Path.DirectorySeparatorChar == '\\'
+ ? FileOptions.Asynchronous | FileOptions.SequentialScan
+ : FileOptions.Asynchronous;
// To overwrite the file, we use FileMode.OpenOrCreate instead of FileMode.Create.
// This avoids a System.UnauthorizedAccessException: Access to the path is denied,
// which can occur if the file has the FileAttributes.Hidden attribute.
var fileMode = overwrite ? FileMode.OpenOrCreate : FileMode.CreateNew;
- await using var fs = new FileStream(_physicalPath, fileMode, FileAccess.Write, FileShare.None, DefaultBufferSize, Options);
+ await using var fs = new FileStream(_physicalPath, fileMode, FileAccess.Write, FileShare.None, DefaultBufferSize, options);
// Since, FileMode.OpenOrCreate doesn't truncate the file, we manually
// set the file length to zero to remove any leftover data.
diff --git a/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs b/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs
index ad3003e..91e922f 100644
--- a/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs
+++ b/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs
@@ -120,7 +120,7 @@ protected override AmazonS3FileSystem GetFileSystem()
{
RegionEndpoint = RegionEndpoint.USEast1,
ServiceURL = "http://localhost:9000",
- ForcePathStyle = true,
+ ForcePathStyle = true
},
bucketName: "storage");
}
diff --git a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs
similarity index 64%
rename from tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs
index 82f12f3..1084ff2 100644
--- a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs
@@ -5,7 +5,7 @@ namespace Ramstack.FileSystem.Azure;
[TestFixture]
[Category("Cloud:Azure")]
-public class ReadonlyAzureFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class ReadonlyAzureFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
@@ -14,6 +14,8 @@ public async Task Setup()
{
using var fs = CreateFileSystem(isReadonly: false);
+ await fs.CreateContainerAsync();
+
foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories))
{
await using var stream = File.OpenRead(path);
@@ -36,15 +38,11 @@ protected override IVirtualFileSystem GetFileSystem() =>
protected override DirectoryInfo GetDirectoryInfo() =>
new DirectoryInfo(_storage.Root);
- private static IVirtualFileSystem CreateFileSystem(bool isReadonly)
+ private static AzureFileSystem CreateFileSystem(bool isReadonly)
{
- var options = new AzureFileSystemOptions
- {
- ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
- Public = false
- };
+ const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;";
- return new AzureFileSystem("storage", options)
+ return new AzureFileSystem(ConnectionString, "storage")
{
IsReadOnly = isReadonly
};
diff --git a/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs
similarity index 68%
rename from tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs
index 0fd7d9d..1b1dff0 100644
--- a/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs
@@ -5,7 +5,7 @@ namespace Ramstack.FileSystem.Azure;
[TestFixture]
[Category("Cloud:Azure")]
-public class WritableAzureFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class WritableAzureFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
@@ -14,6 +14,8 @@ public async Task Setup()
{
using var fs = GetFileSystem();
+ await fs.CreateContainerAsync();
+
foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories))
{
await using var stream = File.OpenRead(path);
@@ -44,23 +46,24 @@ public async Task Directory_BatchDeleting()
await fs.WriteFileAsync($"/temp/{i:0000}", Stream.Null);
Assert.That(
- await fs.GetFilesAsync("/temp", "**").CountAsync(),
+ await fs.GetFilesAsync("/temp").CountAsync(),
Is.EqualTo(Count));
await fs.DeleteDirectoryAsync("/temp");
Assert.That(
- await fs.GetFilesAsync("/temp", "**").CountAsync(),
+ await fs.GetFilesAsync("/temp").CountAsync(),
Is.EqualTo(0));
}
- protected override IVirtualFileSystem GetFileSystem()
+ protected override AzureFileSystem GetFileSystem()
{
- return new AzureFileSystem("storage", new AzureFileSystemOptions
+ const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;";
+
+ return new AzureFileSystem(ConnectionString, "storage")
{
- ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
- Public = false
- });
+ IsReadOnly = false
+ };
}
protected override DirectoryInfo GetDirectoryInfo() =>
diff --git a/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterSpecificationTests.cs b/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs
similarity index 78%
rename from tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterSpecificationTests.cs
rename to tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs
index 7bd118c..7209d70 100644
--- a/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs
@@ -7,9 +7,9 @@
namespace Ramstack.FileSystem.Adapters;
[TestFixture]
-public class VirtualFileSystemAdapterSpecificationTests : VirtualFileSystemSpecificationTests
+public class VirtualFileSystemAdapterTests : VirtualFileSystemSpecificationTests
{
- private readonly TempFileStorage _storage = new();
+ private readonly TempFileStorage _storage = new TempFileStorage();
[OneTimeTearDown]
public void Cleanup() =>
diff --git a/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs
similarity index 90%
rename from tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs
index f382c35..66a1276 100644
--- a/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs
@@ -5,7 +5,7 @@
namespace Ramstack.FileSystem.Globbing;
[TestFixture]
-public class GlobingFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class GlobingFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
diff --git a/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs
similarity index 93%
rename from tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs
index 321f21b..8ff8e58 100644
--- a/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs
@@ -5,7 +5,7 @@
namespace Ramstack.FileSystem.Physical;
[TestFixture]
-public class ReadonlyPhysicalFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class ReadonlyPhysicalFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
diff --git a/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs
similarity index 83%
rename from tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs
index f42bef8..ebd0624 100644
--- a/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs
@@ -4,7 +4,7 @@
namespace Ramstack.FileSystem.Physical;
[TestFixture]
-public class WriteablePhysicalFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class WriteablePhysicalFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
diff --git a/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs
similarity index 84%
rename from tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs
index 5a71de9..5d451bd 100644
--- a/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs
@@ -5,7 +5,7 @@
namespace Ramstack.FileSystem.Prefixed;
[TestFixture]
-public class PrefixedFileSystemSpecificationTests() : VirtualFileSystemSpecificationTests(Prefix)
+public class PrefixedFileSystemTests() : VirtualFileSystemSpecificationTests(Prefix)
{
private const string Prefix = "solution/app";
diff --git a/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs
similarity index 85%
rename from tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs
index fbde606..b402798 100644
--- a/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs
@@ -5,7 +5,7 @@
namespace Ramstack.FileSystem.Readonly;
[TestFixture]
-public class ReadOnlyFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class ReadOnlyFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
diff --git a/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs
similarity index 85%
rename from tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs
index 31211f1..d0e343a 100644
--- a/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs
@@ -5,7 +5,7 @@
namespace Ramstack.FileSystem.Sub;
[TestFixture]
-public class SubFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class SubFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly TempFileStorage _storage = new TempFileStorage();
diff --git a/tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemTests.cs
similarity index 87%
rename from tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemSpecificationTests.cs
rename to tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemTests.cs
index a5b4886..9092723 100644
--- a/tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemSpecificationTests.cs
+++ b/tests/Ramstack.FileSystem.Zip.Tests/ZipFileSystemTests.cs
@@ -6,7 +6,7 @@
namespace Ramstack.FileSystem.Zip;
[TestFixture]
-public class ZipFileSystemSpecificationTests : VirtualFileSystemSpecificationTests
+public class ZipFileSystemTests : VirtualFileSystemSpecificationTests
{
private readonly string _path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
private readonly TempFileStorage _storage = new();