From 57cdbcbcb1098e6c19c0e7acf37279b7cf47c640 Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Tue, 30 Jun 2026 21:08:04 -0300 Subject: [PATCH 1/2] feat(): add feature based on parity pr --- Storage/Header.cs | 50 +++++++++++++++ Storage/StorageFileApi.cs | 101 ++++++++++++++----------------- StorageTests/HeaderTests.cs | 66 ++++++++++++++++++++ StorageTests/StorageTests.csproj | 45 +++++++------- 4 files changed, 183 insertions(+), 79 deletions(-) create mode 100644 Storage/Header.cs create mode 100644 StorageTests/HeaderTests.cs 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/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/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 + + From 9363677ffdbc13ec54b8fd4723d91a0ee98c480a Mon Sep 17 00:00:00 2001 From: Diego Santo Date: Tue, 30 Jun 2026 21:48:09 -0300 Subject: [PATCH 2/2] feat(): add default value for sort by --- Storage/SortBy.cs | 4 +- StorageTests/SearchOptionsTests.cs | 30 +++++ StorageTests/SortByTests.cs | 55 +++++++++ StorageTests/StorageFileTests.cs | 177 +++++++++++++++++++++++++++++ 4 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 StorageTests/SearchOptionsTests.cs create mode 100644 StorageTests/SortByTests.cs 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/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); + } }