Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #15 and removes the unused bucket name from the S3 file factory. #16

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/Stowage.Test/Auth/S3AuthHandlerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ public async Task Get_first_10_bytes_of_a_file_in_a_bucket()
CheckHeader(message, "Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41");
}

[Fact]
public async Task Get_object_from_custom_endpoint()
{
string keyId = "AKIAIOSFODNN7EXAMPLE";
string secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";

var handler = new AuthHandlerWrapper(keyId, secret, "us-east-1");
HttpRequestMessage message = await handler.Exec(HttpMethod.Get, $"https://examplebucket.myprovider.com:9000/test.txt",
new DateTime(2013, 5, 24));

CheckHeader(message, "x-amz-date", "20130524T000000Z");
CheckHeader(message, "x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
CheckHeader(message, "Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=2244d60dfc04dde1f44b8b159357b6972dbe29b93d56b99ebbb89cc2e6c37380");
}

[Fact]
public async Task Delete_folder()
{
string keyId = "AKIAIOSFODNN7EXAMPLE";
string secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";

var handler = new AuthHandlerWrapper(keyId, secret, "us-east-1");
HttpRequestMessage message = await handler.Exec(HttpMethod.Delete, $"https://examplebucket.s3.amazonaws.com/test/",
new DateTime(2013, 5, 24));

CheckHeader(message, "x-amz-date", "20130524T000000Z");
CheckHeader(message, "x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
CheckHeader(message, "Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=3d74a056d3421e65f7357031be0efc8255ac2bd79bcfced92f7414bcd9ffec4a");
}

[Fact]
public async Task Put_object()
{
Expand Down Expand Up @@ -110,15 +140,18 @@ public async Task Get_bucket_lifecycle()
CheckHeader(message, "Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543");
}

[Fact]
public async Task List_objects()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task List_objects(bool explicitPort)
{
string keyId = "AKIAIOSFODNN7EXAMPLE";
string secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";

var handler = new AuthHandlerWrapper(keyId, secret, "us-east-1");

HttpRequestMessage message = await handler.Exec(HttpMethod.Get, $"https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J",
string port = explicitPort ? ":443" : string.Empty;
HttpRequestMessage message = await handler.Exec(HttpMethod.Get, $"https://examplebucket.s3.amazonaws.com{port}/?max-keys=2&prefix=J",
new DateTime(2013, 5, 24));

CheckHeader(message, "x-amz-date", "20130524T000000Z");
Expand Down
4 changes: 2 additions & 2 deletions src/Stowage/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ public static IFileStorage AzureBlobStorageWithLocalEmulator(this IFilesFactory

public static IFileStorage AmazonS3(this IFilesFactory _, string bucketName, string accessKeyId, string secretAccessKey, string region)
{
return AmazonS3(_, bucketName, accessKeyId, secretAccessKey, region, new Uri($"https://{bucketName}.s3.amazonaws.com"));
return AmazonS3(_, accessKeyId, secretAccessKey, region, new Uri($"https://{bucketName}.s3.amazonaws.com"));
}

public static IFileStorage AmazonS3(this IFilesFactory _, string bucketName, string accessKeyId, string secretAccessKey, string region, Uri endpoint)
public static IFileStorage AmazonS3(this IFilesFactory _, string accessKeyId, string secretAccessKey, string region, Uri endpoint)
{
return new AwsS3FileStorage(
endpoint,
Expand Down
18 changes: 12 additions & 6 deletions src/Stowage/IOPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,23 +254,29 @@ public static string NormalizePart(string part)
/// null if input path is null. Parent folder signatures are returned as a part of split, they are not removed.
/// If you want to get an absolute normalized path use <see cref="Normalize(string, bool)"/>
/// </summary>
public static string[] Split(string path)
public static string[] Split(string path, bool appendPathSeparatorIfFolder = true)
{
if(path == null)
return null;

bool isFolder = path.EndsWith(PathSeparatorString);

string[] parts = path.Split(new[] { PathSeparator }, StringSplitOptions.RemoveEmptyEntries).Select(NormalizePart).ToArray();

if(isFolder && parts.Length > 0)
if(appendPathSeparatorIfFolder && parts.Length > 0 && IsFolder(path))
{
parts[parts.Length - 1] = parts[parts.Length - 1] + PathSeparatorString;
}

return parts;
}

/// <summary>
/// Checks if path is folder path.
/// </summary>
public static bool IsFolder(string path)
{
return path.EndsWith(PathSeparatorString);
}

/// <summary>
/// Checks if path is root folder path, which can be an empty string, null, or the actual root path.
/// </summary>
Expand Down Expand Up @@ -338,12 +344,12 @@ public static string Rename(string path, string newFileName)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public bool IsFolder => _name.EndsWith(PathSeparatorString);
public bool IsFolderPath => _name.EndsWith(PathSeparatorString);

/// <summary>
/// Simply checks if kind of this item is not a folder.
/// </summary>
public bool IsFile => !IsFolder;
public bool IsFile => !IsFolderPath;

/// <summary>
/// Equality check
Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/Amazon/AwsS3FileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public AwsS3FileStorage(Uri endpoint, DelegatingHandler authHandler) :

public override async Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException("path needs to be a folder", nameof(path));

string delimiter = recurse ? null : "/";
Expand Down
12 changes: 7 additions & 5 deletions src/Stowage/Impl/Amazon/S3AuthHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
protected async Task<string> SignAsync(HttpRequestMessage request, DateTimeOffset? signDate = null)
{
// a very helpful article on S3 auth: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html

DateTimeOffset dateToUse = signDate ?? DateTimeOffset.UtcNow;
string nowDate = dateToUse.ToString("yyyyMMdd");
string amzNowDate = GetAmzDate(dateToUse);
Expand All @@ -69,7 +68,7 @@
* <SignedHeaders>\n
* <HashedPayload>
*/

string payloadHash = await AddPayloadHashHeader(request);

string canonicalRequest = request.Method + "\n" +
Expand Down Expand Up @@ -135,9 +134,12 @@
private string GetCanonicalUri(HttpRequestMessage request)
{
string path = request.RequestUri.AbsolutePath;
string[] ppts = IOPath.Split(path);
string[] ppts = IOPath.Split(path, appendPathSeparatorIfFolder: false);

return IOPath.Combine(ppts.Select(p => p.UrlEncode()));
string canonicalUri = IOPath.Combine(ppts.Select(p => p.UrlEncode()));
return IOPath.IsFolder(path) && !IOPath.IsRoot(canonicalUri)
? canonicalUri + IOPath.PathSeparatorString
: canonicalUri;
}

private string GetCanonicalQueryString(HttpRequestMessage request)
Expand Down Expand Up @@ -216,7 +218,7 @@
signedHeadersList.Add("date");
}

sb.Append("host:").Append(request.RequestUri.Host).Append("\n");
sb.Append("host:").Append(request.RequestUri.Authority).Append("\n");
signedHeadersList.Add("host");

if(request.Headers.Contains("range"))
Expand Down Expand Up @@ -247,10 +249,10 @@

/// <summary>
/// Hex(SHA256Hash(<payload>))
/// </summary>

Check warning on line 252 in src/Stowage/Impl/Amazon/S3AuthHandler.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'End tag 'summary' does not match the start tag 'payload'.'
/// <param name="request"></param>
/// <returns></returns>
private async Task<string> AddPayloadHashHeader(HttpRequestMessage request)

Check warning on line 255 in src/Stowage/Impl/Amazon/S3AuthHandler.cs

View workflow job for this annotation

GitHub Actions / build

XML comment has badly formed XML -- 'Expected an end tag for element 'summary'.'
{
string hash;

Expand Down
4 changes: 2 additions & 2 deletions src/Stowage/Impl/Databricks/DatabricksRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
/// <returns></returns>
public override async Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path = null, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException("path needs to be a folder", nameof(path));

var result = new List<IOEntry>();
Expand Down Expand Up @@ -120,7 +120,7 @@

if(recurse)
{
foreach(IOEntry folder in batch.Where(e => e.Path.IsFolder))
foreach(IOEntry folder in batch.Where(e => e.Path.IsFolderPath))
{
await Ls(folder.Path, container, recurse);
}
Expand Down Expand Up @@ -262,7 +262,7 @@

if(includeRuns)
{
RunsListResponse[] listOfListOfRuns = await Task.WhenAll(jobListResponse.Jobs.Select(rj => ListJobRuns(rj.Id, 2)));

Check warning on line 265 in src/Stowage/Impl/Databricks/DatabricksRestClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
return jobListResponse.Jobs.Zip(listOfListOfRuns, (job, runs) =>
{
if(runs?.Runs != null)
Expand All @@ -273,7 +273,7 @@
}).ToList();
}

return jobListResponse.Jobs.ToList();

Check warning on line 276 in src/Stowage/Impl/Databricks/DatabricksRestClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
}

public async Task<Job> GetJob(long jobId)
Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/Google/GoogleCloudStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public GoogleCloudStorage(string bucketName, DelegatingHandler authHandler)

public override async Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException("path needs to be a folder", nameof(path));

// https://cloud.google.com/storage/docs/json_api/v1/objects/list
Expand Down
4 changes: 2 additions & 2 deletions src/Stowage/Impl/InMemoryFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public override Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path, bool recurse
if(path == null)
path = IOPath.Root;

if(!path.IsFolder)
if(!path.IsFolderPath)
throw new ArgumentException("path needs to be a folder", nameof(path));

IEnumerable<KeyValuePair<IOPath, Tag>> query = _pathToTag;
Expand Down Expand Up @@ -118,7 +118,7 @@ public override Task Rm(IOPath path, bool recurse, CancellationToken cancellatio
_pathToTag.Remove(path);
}

if(path.IsFolder && recurse)
if(path.IsFolderPath && recurse)
{
List<IOPath> candidates = _pathToTag.Where(p => p.Key.Full.StartsWith(path.Full)).Select(p => p.Key).ToList();

Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/LocalDiskFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private IReadOnlyCollection<IOEntry> List(string path, bool recurse, bool addAtt

public override Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path = null, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException("path needs to be a folder", nameof(path));

return Task.FromResult(List(path, recurse, true));
Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/Microsoft/AzureBlobFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

public override Task<IReadOnlyCollection<IOEntry>> Ls(IOPath path, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException($"{nameof(path)} needs to be a folder", nameof(path));

return ListAsync(path, recurse, cancellationToken);
Expand Down Expand Up @@ -331,7 +331,7 @@
}
else
{
https://docs.microsoft.com/en-us/rest/api/storageservices/delete-blob

Check warning on line 334 in src/Stowage/Impl/Microsoft/AzureBlobFileStorage.cs

View workflow job for this annotation

GitHub Actions / build

This label has not been referenced
HttpResponseMessage response = await SendAsync(new HttpRequestMessage(HttpMethod.Delete, $"{_containerName}{path}"));
if(response.StatusCode != HttpStatusCode.NotFound)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/Microsoft/AzureTableFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private async Task<IReadOnlyCollection<IOEntry>> ListTablesAsync()
public override async Task<IReadOnlyCollection<IOEntry>> Ls(
IOPath path = null, bool recurse = false, CancellationToken cancellationToken = default)
{
if(path != null && !path.IsFolder)
if(path != null && !path.IsFolderPath)
throw new ArgumentException($"{nameof(path)} needs to be a folder", nameof(path));

if(IOPath.IsRoot(path))
Expand Down
2 changes: 1 addition & 1 deletion src/Stowage/Impl/VirtualStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Mount(IOPath path, IFileStorage storage)
throw new ArgumentNullException(nameof(path));
if(storage is null)
throw new ArgumentNullException(nameof(storage));
if(!path.IsFolder)
if(!path.IsFolderPath)
throw new ArgumentException($"only folders can be mounted", nameof(path));

_mps.Add(new MountPoint { Path = path, Storage = storage });
Expand Down
6 changes: 3 additions & 3 deletions src/Stowage/PolyfilledFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,19 @@ public virtual async Task Ren(IOPath oldPath, IOPath newPath, CancellationToken
throw new ArgumentNullException(nameof(newPath));

// when file moves to a folder
if(oldPath.IsFile && newPath.IsFolder)
if(oldPath.IsFile && newPath.IsFolderPath)
{
newPath = newPath.Combine(oldPath.Name);

// now it's a file-to-file rename

throw new NotImplementedException();
}
else if(oldPath.IsFolder && newPath.IsFile)
else if(oldPath.IsFolderPath && newPath.IsFile)
{
throw new ArgumentException($"attempted to rename folder to file", nameof(newPath));
}
else if(oldPath.IsFolder)
else if(oldPath.IsFolderPath)
{
// folder-to-folder ren
throw new NotImplementedException();
Expand Down
Loading