diff --git a/Storage/Header.cs b/Storage/Header.cs
new file mode 100644
index 0000000..51c6034
--- /dev/null
+++ b/Storage/Header.cs
@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Supabase.Storage;
+
+///
+/// Represents a container for HTTP headers, providing functionality for adding and retrieving headers.
+///
+public class Header
+{
+ private readonly Dictionary _headers = [];
+
+ ///
+ /// Adds a new header to the collection or updates the value of an existing header.
+ /// Key will be lowercased
+ ///
+ /// The key of the header to add or update.
+ /// The value associated with the header key.
+ 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);
+ }
+
+ ///
+ /// Adds multiple headers to the collection or updates the values of existing headers.
+ /// Key will be lowercased
+ ///
+ ///
+ /// A dictionary containing the headers to add or update, where the key is the header name and the
+ /// value is the header value.
+ ///
+ public void Add(Dictionary headers)
+ {
+ foreach (var header in headers)
+ Add(header.Key, header.Value);
+ }
+
+ ///
+ /// Retrieves all the headers in the collection.
+ ///
+ /// A dictionary containing all headers, where the key is the header name and the value is the header value.
+ public Dictionary Get()
+ {
+ return _headers;
+ }
+}
diff --git a/Storage/SortBy.cs b/Storage/SortBy.cs
index 4e226cb..a67479b 100644
--- a/Storage/SortBy.cs
+++ b/Storage/SortBy.cs
@@ -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";
}
}
diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs
index a8b9f5c..4a66222 100644
--- a/Storage/StorageFileApi.cs
+++ b/Storage/StorageFileApi.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -24,6 +23,8 @@ public class StorageFileApi : IStorageFileApi
protected Dictionary Headers { get; set; }
protected string? BucketId { get; set; }
+ protected Header StorageHeader = new();
+
public StorageFileApi(
string url,
string bucketId,
@@ -45,6 +46,7 @@ public StorageFileApi(
BucketId = bucketId;
Options ??= new ClientOptions();
Headers = headers ?? new Dictionary();
+ StorageHeader.Add(Headers);
}
///
@@ -289,15 +291,12 @@ public async Task UploadToSignedUrl(
if (inferContentType)
options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(localFilePath);
- var headers = new Dictionary(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();
@@ -307,7 +306,7 @@ public async Task UploadToSignedUrl(
await Helpers.HttpUploadClient!.UploadFileAsync(
signedUrl.SignedUrl,
localFilePath,
- headers,
+ StorageHeader.Get(),
progress
);
@@ -336,16 +335,16 @@ public async Task UploadToSignedUrl(
if (inferContentType)
options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(signedUrl.Key);
- var headers = new Dictionary(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();
if (onProgress != null)
@@ -354,7 +353,7 @@ public async Task UploadToSignedUrl(
await Helpers.HttpUploadClient!.UploadBytesAsync(
signedUrl.SignedUrl,
data,
- headers,
+ StorageHeader.Get(),
progress
);
@@ -676,29 +675,26 @@ private async Task UploadOrUpdate(
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");
- var headers = new Dictionary(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();
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);
}
@@ -712,11 +708,8 @@ private async Task UploadOrContinue(
)
{
var uri = new Uri($"{Url}/upload/resumable");
-
- var headers = new Dictionary(Headers)
- {
- { "cache-control", $"max-age={options.CacheControl}" },
- };
+
+ StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
var metadata = new MetadataCollection
{
@@ -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();
@@ -745,7 +738,7 @@ private async Task UploadOrContinue(
uri,
localPath,
metadata,
- headers,
+ StorageHeader.Get(),
progress,
cancellationToken
);
@@ -761,10 +754,7 @@ private async Task UploadOrContinue(
{
var uri = new Uri($"{Url}/upload/resumable");
- var headers = new Dictionary(Headers)
- {
- { "cache-control", $"max-age={options.CacheControl}" },
- };
+ StorageHeader.Add("cache-control", $"max-age={options.CacheControl}");
var metadata = new MetadataCollection
{
@@ -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();
@@ -793,7 +783,7 @@ private async Task UploadOrContinue(
uri,
data,
metadata,
- headers,
+ StorageHeader.Get(),
progress,
cancellationToken
);
@@ -816,30 +806,27 @@ private async Task UploadOrUpdate(
)
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");
-
- var headers = new Dictionary(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();
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);
}
diff --git a/StorageTests/HeaderTests.cs b/StorageTests/HeaderTests.cs
new file mode 100644
index 0000000..0c3cb46
--- /dev/null
+++ b/StorageTests/HeaderTests.cs
@@ -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
+ {
+ { "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"]);
+ }
+}
diff --git a/StorageTests/SearchOptionsTests.cs b/StorageTests/SearchOptionsTests.cs
new file mode 100644
index 0000000..d336de5
--- /dev/null
+++ b/StorageTests/SearchOptionsTests.cs
@@ -0,0 +1,30 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Supabase.Storage;
+
+namespace StorageTests;
+
+[TestClass]
+public class SearchOptionsTests
+{
+ [TestMethod("SearchOptions: Test Default Sort Values")]
+ public void TestDefaultSortValues()
+ {
+ var options = new SearchOptions();
+
+ Assert.AreEqual(options.SortBy.Column, "name");
+ Assert.AreEqual(options.SortBy.Order, "asc");
+ }
+
+
+ [TestMethod("SearchOptions: Test Default Sort Column Value")]
+ public void TestDefaultSortColumnValue()
+ {
+ var options = new SearchOptions()
+ {
+
+ };
+
+ Assert.AreEqual(options.SortBy.Column, "name");
+ Assert.AreEqual(options.SortBy.Order, "asc");
+ }
+}
\ No newline at end of file
diff --git a/StorageTests/SortByTests.cs b/StorageTests/SortByTests.cs
new file mode 100644
index 0000000..90c7284
--- /dev/null
+++ b/StorageTests/SortByTests.cs
@@ -0,0 +1,55 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Supabase.Storage;
+
+namespace StorageTests;
+
+[TestClass]
+public class SortByTests
+{
+ [TestMethod("SortBy: Test Default Sort Values")]
+ public void TestDefaultSortValues()
+ {
+ var options = new SortBy();
+
+ Assert.AreEqual(options.Column, "name");
+ Assert.AreEqual(options.Order, "asc");
+ }
+
+
+ [TestMethod("SortBy: Test Default Sort Column Value")]
+ public void TestDefaultSortColumnValue()
+ {
+ var options = new SortBy()
+ {
+ Column = "status"
+ };
+
+ Assert.AreEqual(options.Column, "status");
+ Assert.AreEqual(options.Order, "asc");
+ }
+
+ [TestMethod("SortBy: Test Default Sort Order Value")]
+ public void TestDefaultSortOrderValue()
+ {
+ var options = new SortBy()
+ {
+ Order = "desc"
+ };
+
+ Assert.AreEqual(options.Column, "name");
+ Assert.AreEqual(options.Order, "desc");
+ }
+
+ [TestMethod("SortBy: Test SortBy")]
+ public void TestSortByValue()
+ {
+ var options = new SortBy()
+ {
+ Order = "desc",
+ Column = "updated_at"
+ };
+
+ Assert.AreEqual(options.Column, "updated_at");
+ Assert.AreEqual(options.Order, "desc");
+ }
+}
\ No newline at end of file
diff --git a/StorageTests/StorageFileTests.cs b/StorageTests/StorageFileTests.cs
index 3a80175..b1c78e3 100644
--- a/StorageTests/StorageFileTests.cs
+++ b/StorageTests/StorageFileTests.cs
@@ -664,5 +664,182 @@ public async Task CanCreateSignedUploadUrl()
var result = await _bucket.CreateUploadSignedUrl("test.png");
Assert.IsTrue(Uri.IsWellFormedUriString(result.SignedUrl.ToString(), UriKind.Absolute));
}
+
+ [TestMethod("File: Test List Default Values")]
+ public async Task GetListDefaultValues()
+ {
+ var tsc = new TaskCompletionSource();
+
+ var name1 = $"1-{Guid.NewGuid()}.bin";
+ var name2 = $"2-{Guid.NewGuid()}.bin";
+ var name3 = $"3-{Guid.NewGuid()}.bin";
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name1,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name2,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name3,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ var list = await _bucket.List();
+ Assert.IsNotNull(list);
+ Assert.AreEqual(3, list.Count);
+ Assert.AreEqual(name1, list[0].Name);
+ Assert.AreEqual(name2, list[1].Name);
+ Assert.AreEqual(name3, list[2].Name);
+ }
+
+ [TestMethod("File: Test List Ordered Values")]
+ public async Task GetListOrderedValues()
+ {
+ var tsc = new TaskCompletionSource();
+
+ var name1 = $"1-{Guid.NewGuid()}.bin";
+ var name2 = $"2-{Guid.NewGuid()}.bin";
+ var name3 = $"3-{Guid.NewGuid()}.bin";
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name1,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name2,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name3,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+ var sortBy = new SortBy()
+ {
+ Order = "desc"
+ };
+ var options = new SearchOptions
+ {
+ SortBy = sortBy
+ };
+
+ var list = await _bucket.List("", options);
+ Assert.IsNotNull(list);
+ Assert.AreEqual(3, list.Count);
+ Assert.AreEqual(name3, list[0].Name);
+ Assert.AreEqual(name2, list[1].Name);
+ Assert.AreEqual(name1, list[2].Name);
+ }
+
+ [TestMethod("File: Test List Column Values")]
+ public async Task GetListColumnValues()
+ {
+ var tsc = new TaskCompletionSource();
+
+ var name1 = $"1-{Guid.NewGuid()}.bin";
+ var name2 = $"2-{Guid.NewGuid()}.bin";
+ var name3 = $"3-{Guid.NewGuid()}.bin";
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name1,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name2,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name3,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+ var sortBy = new SortBy()
+ {
+ Column = "created_at",
+ };
+ var options = new SearchOptions
+ {
+ SortBy = sortBy
+ };
+
+ var list = await _bucket.List("", options);
+ Assert.IsNotNull(list);
+ Assert.AreEqual(3, list.Count);
+ Assert.AreEqual(name1, list[0].Name);
+ Assert.AreEqual(name2, list[1].Name);
+ Assert.AreEqual(name3, list[2].Name);
+ }
+
+ [TestMethod("File: Test List Override Sort Values")]
+ public async Task GetListOverrideSortValues()
+ {
+ var tsc = new TaskCompletionSource();
+
+ var name1 = $"1-{Guid.NewGuid()}.bin";
+ var name2 = $"2-{Guid.NewGuid()}.bin";
+ var name3 = $"3-{Guid.NewGuid()}.bin";
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name1,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name2,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+
+ await _bucket.Upload(
+ new Byte[] { 0x0, 0x0, 0x0 },
+ name3,
+ null,
+ (_, _) => tsc.TrySetResult(true)
+ );
+ var sortBy = new SortBy()
+ {
+ Column = "created_at",
+ Order = "desc"
+ };
+ var options = new SearchOptions
+ {
+ SortBy = sortBy
+ };
+
+ var list = await _bucket.List("", options);
+ Assert.IsNotNull(list);
+ Assert.AreEqual(3, list.Count);
+ Assert.AreEqual(name3, list[0].Name);
+ Assert.AreEqual(name2, list[1].Name);
+ Assert.AreEqual(name1, list[2].Name);
+ }
}
diff --git a/StorageTests/StorageTests.csproj b/StorageTests/StorageTests.csproj
index 5891e76..a45abf3 100644
--- a/StorageTests/StorageTests.csproj
+++ b/StorageTests/StorageTests.csproj
@@ -1,28 +1,29 @@
-
- net8.0
- false
- enable
-
+
+ net10.0
+ false
+ enable
+ default
+
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
-
-
-
+
+
+
-
-
- PreserveNewest
-
-
+
+
+ PreserveNewest
+
+