Skip to content
Merged
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
50 changes: 50 additions & 0 deletions Storage/Header.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;

namespace Supabase.Storage;

/// <summary>
/// Represents a container for HTTP headers, providing functionality for adding and retrieving headers.
/// </summary>
public class Header
{
private readonly Dictionary<string, string> _headers = [];

/// <summary>
/// Adds a new header to the collection or updates the value of an existing header.
/// Key will be lowercased
/// </summary>
/// <param name="key">The key of the header to add or update.</param>
/// <param name="value">The value associated with the header key.</param>
public void Add(string key, string value)
{
var newKey = key.ToLower();
foreach (var header in _headers.Where(header => header.Key.ToLower() == newKey))
_headers.Remove(header.Key);

_headers.Add(newKey, value);
}

/// <summary>
/// Adds multiple headers to the collection or updates the values of existing headers.
/// Key will be lowercased
/// </summary>
/// <param name="headers">
/// A dictionary containing the headers to add or update, where the key is the header name and the
/// value is the header value.
/// </param>
public void Add(Dictionary<string, string> headers)
{
foreach (var header in headers)
Add(header.Key, header.Value);
}

/// <summary>
/// Retrieves all the headers in the collection.
/// </summary>
/// <returns>A dictionary containing all headers, where the key is the header name and the value is the header value.</returns>
public Dictionary<string, string> Get()
{
return _headers;
}
}
4 changes: 2 additions & 2 deletions Storage/SortBy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ namespace Supabase.Storage
public class SortBy
{
[JsonProperty("column")]
public string? Column { get; set; }
public string? Column { get; set; } = "name";

[JsonProperty("order")]
public string? Order { get; set; }
public string? Order { get; set; } = "asc";
}
}
101 changes: 44 additions & 57 deletions Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand All @@ -24,6 +23,8 @@ public class StorageFileApi : IStorageFileApi<FileObject>
protected Dictionary<string, string> Headers { get; set; }
protected string? BucketId { get; set; }

protected Header StorageHeader = new();

public StorageFileApi(
string url,
string bucketId,
Expand All @@ -45,6 +46,7 @@ public StorageFileApi(
BucketId = bucketId;
Options ??= new ClientOptions();
Headers = headers ?? new Dictionary<string, string>();
StorageHeader.Add(Headers);
}

/// <summary>
Expand Down Expand Up @@ -289,15 +291,12 @@ public async Task<string> UploadToSignedUrl(
if (inferContentType)
options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(localFilePath);

var headers = new Dictionary<string, string>(Headers)
{
["Authorization"] = $"Bearer {signedUrl.Token}",
["cache-control"] = $"max-age={options.CacheControl}",
["content-type"] = options.ContentType,
};
StorageHeader.Add("Authorization", $"Bearer {signedUrl.Token}");
StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
StorageHeader.Add("content-type", options.ContentType);

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

var progress = new Progress<float>();

Expand All @@ -307,7 +306,7 @@ public async Task<string> UploadToSignedUrl(
await Helpers.HttpUploadClient!.UploadFileAsync(
signedUrl.SignedUrl,
localFilePath,
headers,
StorageHeader.Get(),
progress
);

Expand Down Expand Up @@ -336,16 +335,16 @@ public async Task<string> UploadToSignedUrl(
if (inferContentType)
options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(signedUrl.Key);

var headers = new Dictionary<string, string>(Headers)
{
["Authorization"] = $"Bearer {signedUrl.Token}",
["cache-control"] = $"max-age={options.CacheControl}",
["content-type"] = options.ContentType,
};
StorageHeader.Add("Authorization", $"Bearer {signedUrl.Token}");
StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
StorageHeader.Add("content-type", options.ContentType);

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

if (options.Metadata != null)
StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata));

var progress = new Progress<float>();

if (onProgress != null)
Expand All @@ -354,7 +353,7 @@ public async Task<string> UploadToSignedUrl(
await Helpers.HttpUploadClient!.UploadBytesAsync(
signedUrl.SignedUrl,
data,
headers,
StorageHeader.Get(),
progress
);

Expand Down Expand Up @@ -676,29 +675,26 @@ private async Task<string> UploadOrUpdate(
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");

var headers = new Dictionary<string, string>(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
{ "content-type", options.ContentType },
};
StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
StorageHeader.Add("content-type", options.ContentType);

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

if (options.Metadata != null)
headers.Add("x-metadata", ParseMetadata(options.Metadata));
StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata));

options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
options.Headers?.ToList().ForEach(x => StorageHeader.Add(x.Key, x.Value));

if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
StorageHeader.Add("x-duplex", options.Duplex.ToLower());

var progress = new Progress<float>();

if (onProgress != null)
progress.ProgressChanged += onProgress;

await Helpers.HttpUploadClient!.UploadFileAsync(uri, localPath, headers, progress, cancellationToken);
await Helpers.HttpUploadClient!.UploadFileAsync(uri, localPath, StorageHeader.Get(), progress, cancellationToken);

return GetFinalPath(supabasePath);
}
Expand All @@ -712,11 +708,8 @@ private async Task UploadOrContinue(
)
{
var uri = new Uri($"{Url}/upload/resumable");

var headers = new Dictionary<string, string>(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
};

StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");

var metadata = new MetadataCollection
{
Expand All @@ -726,15 +719,15 @@ private async Task UploadOrContinue(
};

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

if (options.Metadata != null)
headers.Add("x-metadata", ParseMetadata(options.Metadata));
StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata));

options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
options.Headers?.ToList().ForEach(x => StorageHeader.Add(x.Key, x.Value));

if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
StorageHeader.Add("x-duplex", options.Duplex.ToLower());

var progress = new Progress<float>();

Expand All @@ -745,7 +738,7 @@ private async Task UploadOrContinue(
uri,
localPath,
metadata,
headers,
StorageHeader.Get(),
progress,
cancellationToken
);
Expand All @@ -761,10 +754,7 @@ private async Task UploadOrContinue(
{
var uri = new Uri($"{Url}/upload/resumable");

var headers = new Dictionary<string, string>(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
};
StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");

var metadata = new MetadataCollection
{
Expand All @@ -774,15 +764,15 @@ private async Task UploadOrContinue(
};

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

if (options.Metadata != null)
metadata["metadata"] = JsonConvert.SerializeObject(options.Metadata);

options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
options.Headers?.ToList().ForEach(x => StorageHeader.Add(x.Key, x.Value));

if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
StorageHeader.Add("x-duplex", options.Duplex.ToLower());

var progress = new Progress<float>();

Expand All @@ -793,7 +783,7 @@ private async Task UploadOrContinue(
uri,
data,
metadata,
headers,
StorageHeader.Get(),
progress,
cancellationToken
);
Expand All @@ -816,30 +806,27 @@ private async Task<string> UploadOrUpdate(
)
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");

var headers = new Dictionary<string, string>(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
{ "content-type", options.ContentType },
};


StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
StorageHeader.Add("content-type", options.ContentType);

if (options.Upsert)
headers.Add("x-upsert", options.Upsert.ToString().ToLower());
StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower());

if (options.Metadata != null)
headers.Add("x-metadata", ParseMetadata(options.Metadata));
StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata));

options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
options.Headers?.ToList().ForEach(x => StorageHeader.Add(x.Key, x.Value));

if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
StorageHeader.Add("x-duplex", options.Duplex.ToLower());

var progress = new Progress<float>();

if (onProgress != null)
progress.ProgressChanged += onProgress;

await Helpers.HttpUploadClient!.UploadBytesAsync(uri, data, headers, progress, cancellationToken);
await Helpers.HttpUploadClient!.UploadBytesAsync(uri, data, StorageHeader.Get(), progress, cancellationToken);

return GetFinalPath(supabasePath);
}
Expand Down
66 changes: 66 additions & 0 deletions StorageTests/HeaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Supabase.Storage;

namespace StorageTests;

[TestClass]
public class HeaderTests
{
[TestMethod("Header: Add Single Header")]
public void TestHeaderAddSingle()
{
var header = new Header();
header.Add("Content-Type", "application/json");

var headers = header.Get();
Assert.AreEqual(1, headers.Count);
Assert.IsTrue(headers.ContainsKey("content-type"));
Assert.AreEqual("application/json", headers["content-type"]);
}

[TestMethod("Header: Add Multiple Headers")]
public void TestHeaderAddMultiple()
{
var header = new Header();
var dictionary = new Dictionary<string, string>
{
{ "Content-Type", "application/json" },
{ "X-Custom-Header", "Value" },
};

header.Add(dictionary);

var headers = header.Get();
Assert.AreEqual(2, headers.Count);
Assert.IsTrue(headers.ContainsKey("content-type"));
Assert.IsTrue(headers.ContainsKey("x-custom-header"));
Assert.AreEqual("application/json", headers["content-type"]);
Assert.AreEqual("Value", headers["x-custom-header"]);
}

[TestMethod("Header: Update Existing Header")]
public void TestHeaderUpdateExisting()
{
var header = new Header();
header.Add("Content-Type", "application/json");
header.Add("CONTENT-TYPE", "text/plain");

var headers = header.Get();
Assert.AreEqual(1, headers.Count);
Assert.IsTrue(headers.ContainsKey("content-type"));
Assert.AreEqual("text/plain", headers["content-type"]);
}

[TestMethod("Header: Update Existing Header with Different Case")]
public void TestHeaderUpdateExistingDifferentCase()
{
var header = new Header();
header.Add("X-Custom", "value1");
header.Add("x-custom", "value2");

var headers = header.Get();
Assert.AreEqual(1, headers.Count);
Assert.AreEqual("value2", headers["x-custom"]);
}
}
Loading
Loading