diff --git a/Storage/Bucket.cs b/Storage/Bucket.cs
index b68308a..af381b1 100644
--- a/Storage/Bucket.cs
+++ b/Storage/Bucket.cs
@@ -1,31 +1,31 @@
using System;
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
public class Bucket
{
- [JsonProperty("id")]
+ [JsonPropertyName("id")]
public string? Id { get; set; }
- [JsonProperty("name")]
+ [JsonPropertyName("name")]
public string? Name { get; set; }
- [JsonProperty("owner")]
+ [JsonPropertyName("owner")]
public string? Owner { get; set; }
- [JsonProperty("created_at")]
+ [JsonPropertyName("created_at")]
public DateTime? CreatedAt { get; set; }
- [JsonProperty("updated_at")]
+ [JsonPropertyName("updated_at")]
public DateTime? UpdatedAt { get; set; }
///
/// The visibility of the bucket. Public buckets don't require an authorization token to download objects,
/// but still require a valid token for all other operations. By default, buckets are private.
///
- [JsonProperty("public")]
+ [JsonPropertyName("public")]
public bool Public { get; set; }
///
@@ -33,7 +33,7 @@ public class Bucket
///
/// Expects a string value following a format like: '1kb', '50mb', '150kb', etc.
///
- [JsonProperty("file_size_limit", NullValueHandling = NullValueHandling.Include)]
+ [JsonPropertyName("file_size_limit")]
public string? FileSizeLimit { get; set; }
///
@@ -41,7 +41,8 @@ public class Bucket
///
/// Expects a List of values such as: ['image/jpeg', 'image/png', etc]
///
- [JsonProperty("allowed_mime_types", NullValueHandling = NullValueHandling.Ignore)]
+ [JsonPropertyName("allowed_mime_types")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? AllowedMimes { get; set; }
}
}
diff --git a/Storage/BucketUpsertOptions.cs b/Storage/BucketUpsertOptions.cs
index 3faf8ba..3223c6b 100644
--- a/Storage/BucketUpsertOptions.cs
+++ b/Storage/BucketUpsertOptions.cs
@@ -1,5 +1,5 @@
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
@@ -9,7 +9,7 @@ public class BucketUpsertOptions
/// The visibility of the bucket. Public buckets don't require an authorization token to download objects,
/// but still require a valid token for all other operations. By default, buckets are private.
///
- [JsonProperty("public")]
+ [JsonPropertyName("public")]
public bool Public { get; set; } = false;
///
@@ -17,7 +17,7 @@ public class BucketUpsertOptions
///
/// Expects a string value following a format like: '1kb', '50mb', '150kb', etc.
///
- [JsonProperty("file_size_limit", NullValueHandling = NullValueHandling.Include)]
+ [JsonPropertyName("file_size_limit")]
public string? FileSizeLimit { get; set; }
///
@@ -25,7 +25,8 @@ public class BucketUpsertOptions
///
/// Expects a List of values such as: ['image/jpeg', 'image/png', etc]
///
- [JsonProperty("allowed_mime_types", NullValueHandling = NullValueHandling.Ignore)]
+ [JsonPropertyName("allowed_mime_types")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List? AllowedMimes { get; set; }
}
}
diff --git a/Storage/CreateSignedUrlResponse.cs b/Storage/CreateSignedUrlResponse.cs
index a6a0a44..b2fac28 100644
--- a/Storage/CreateSignedUrlResponse.cs
+++ b/Storage/CreateSignedUrlResponse.cs
@@ -1,16 +1,34 @@
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
+ ///
+ /// Represents the response received when creating a signed URL for file access through Supabase Storage.
+ ///
public class CreateSignedUrlResponse
{
- [JsonProperty("signedURL")]
+ ///
+ /// Represents the signed URL returned as part of a response when requesting access to a file
+ /// stored in Supabase Storage. This URL can be used to access the file directly with
+ /// the defined expiration and optional transformations or download options applied.
+ ///
+ [JsonPropertyName("signedURL")]
public string? SignedUrl { get; set; }
}
+ ///
+ /// Represents the extended response received when creating multiple signed URLs
+ /// for file access through Supabase Storage. In addition to the signed URL, it includes
+ /// the associated file path.
+ ///
public class CreateSignedUrlsResponse: CreateSignedUrlResponse
{
- [JsonProperty("path")]
+ ///
+ /// Represents the file path associated with a signed URL in the response.
+ /// This property indicates the specific file path for which the signed URL
+ /// was generated, allowing identification of the file within the storage bucket.
+ ///
+ [JsonPropertyName("path")]
public string? Path { get; set; }
}
}
diff --git a/Storage/DownloadOptions.cs b/Storage/DownloadOptions.cs
index 44f7c00..274b59d 100644
--- a/Storage/DownloadOptions.cs
+++ b/Storage/DownloadOptions.cs
@@ -1,7 +1,8 @@
-using Newtonsoft.Json;
-
namespace Supabase.Storage
{
+ ///
+ /// Represents options used when downloading files from storage.
+ ///
public class DownloadOptions
{
///
diff --git a/Storage/Extensions/HttpClientProgress.cs b/Storage/Extensions/HttpClientProgress.cs
index 6b768bb..5301e93 100644
--- a/Storage/Extensions/HttpClientProgress.cs
+++ b/Storage/Extensions/HttpClientProgress.cs
@@ -2,13 +2,13 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using BirdMessenger;
using BirdMessenger.Collections;
using BirdMessenger.Delegates;
using BirdMessenger.Infrastructure;
-using Newtonsoft.Json;
using Supabase.Storage.Exceptions;
namespace Supabase.Storage.Extensions
@@ -47,7 +47,10 @@ public static async Task DownloadDataAsync(
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
- var errorResponse = JsonConvert.DeserializeObject(content);
+ var errorResponse = JsonSerializer.Deserialize(
+ content,
+ Helpers.JsonOptions
+ );
var e = new SupabaseStorageException(errorResponse?.Message ?? content)
{
Content = content,
@@ -182,7 +185,10 @@ public static async Task UploadAsync(
if (!response.IsSuccessStatusCode)
{
var httpContent = await response.Content.ReadAsStringAsync();
- var errorResponse = JsonConvert.DeserializeObject(httpContent);
+ var errorResponse = JsonSerializer.Deserialize(
+ httpContent,
+ Helpers.JsonOptions
+ );
var e = new SupabaseStorageException(errorResponse?.Message ?? httpContent)
{
Content = httpContent,
@@ -343,7 +349,10 @@ HttpResponseMessage response
)
{
var httpContent = await response.Content.ReadAsStringAsync();
- var errorResponse = JsonConvert.DeserializeObject(httpContent);
+ var errorResponse = JsonSerializer.Deserialize(
+ httpContent,
+ Helpers.JsonOptions
+ );
var error = new SupabaseStorageException(errorResponse?.Message ?? httpContent)
{
Content = httpContent,
diff --git a/Storage/FileObject.cs b/Storage/FileObject.cs
index d907d13..0b18786 100644
--- a/Storage/FileObject.cs
+++ b/Storage/FileObject.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
@@ -10,32 +10,23 @@ public class FileObject
/// Flag representing if this object is a folder, all properties will be null but the name
///
public bool IsFolder => !string.IsNullOrEmpty(Name) && Id == null && CreatedAt == null && UpdatedAt == null;
-
- [JsonProperty("name")]
- public string? Name { get; set; }
- [JsonProperty("bucket_id")]
- public string? BucketId { get; set; }
+ [JsonPropertyName("name")] public string? Name { get; set; }
- [JsonProperty("owner")]
- public string? Owner { get; set; }
+ [JsonPropertyName("bucket_id")] public string? BucketId { get; set; }
- [JsonProperty("id")]
- public string? Id { get; set; }
+ [JsonPropertyName("owner")] public string? Owner { get; set; }
- [JsonProperty("updated_at")]
- public DateTime? UpdatedAt { get; set; }
+ [JsonPropertyName("id")] public string? Id { get; set; }
- [JsonProperty("created_at")]
- public DateTime? CreatedAt { get; set; }
+ [JsonPropertyName("updated_at")] public DateTime? UpdatedAt { get; set; }
- [JsonProperty("last_accessed_at")]
- public DateTime? LastAccessedAt { get; set; }
+ [JsonPropertyName("created_at")] public DateTime? CreatedAt { get; set; }
- [JsonProperty("metadata")]
- public Dictionary MetaData = new Dictionary();
+ [JsonPropertyName("last_accessed_at")] public DateTime? LastAccessedAt { get; set; }
- [JsonProperty("buckets")]
- public Bucket? Buckets { get; set; }
+ [JsonPropertyName("metadata")] public Dictionary MetaData = new Dictionary();
+
+ [JsonPropertyName("buckets")] public Bucket? Buckets { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/Storage/FileObjectV2.cs b/Storage/FileObjectV2.cs
index 7cbc181..337d174 100644
--- a/Storage/FileObjectV2.cs
+++ b/Storage/FileObjectV2.cs
@@ -1,49 +1,92 @@
using System;
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
+ ///
+ /// Represents a file object in Supabase Storage with its associated metadata and properties.
+ /// This class is used for version 2 of the Storage API.
+ ///
public class FileObjectV2
{
- [JsonProperty("id")]
+ ///
+ /// The unique identifier of the file.
+ ///
+ [JsonPropertyName("id")]
public string Id { get; set; }
-
- [JsonProperty("version")]
+
+ ///
+ /// The version of the file.
+ ///
+ [JsonPropertyName("version")]
public string Version { get; set; }
-
- [JsonProperty("name")]
+
+ ///
+ /// The name of the file.
+ ///
+ [JsonPropertyName("name")]
public string? Name { get; set; }
- [JsonProperty("bucket_id")]
+ ///
+ /// The identifier of the bucket containing the file.
+ ///
+ [JsonPropertyName("bucket_id")]
public string? BucketId { get; set; }
- [JsonProperty("updated_at")]
+ ///
+ /// The timestamp when the file was last updated.
+ ///
+ [JsonPropertyName("updated_at")]
public DateTime? UpdatedAt { get; set; }
- [JsonProperty("created_at")]
+ ///
+ /// The timestamp when the file was created.
+ ///
+ [JsonPropertyName("created_at")]
public DateTime? CreatedAt { get; set; }
- [JsonProperty("last_accessed_at")]
+ ///
+ /// The timestamp when the file was last accessed.
+ ///
+ [JsonPropertyName("last_accessed_at")]
public DateTime? LastAccessedAt { get; set; }
-
- [JsonProperty("size")]
+
+ ///
+ /// The size of the file in bytes.
+ ///
+ [JsonPropertyName("size")]
public int? Size { get; set; }
-
- [JsonProperty("cache_control")]
+
+ ///
+ /// The cache control directives for the file.
+ ///
+ [JsonPropertyName("cache_control")]
public string? CacheControl { get; set; }
-
- [JsonProperty("content_type")]
+
+ ///
+ /// The MIME type of the file.
+ ///
+ [JsonPropertyName("content_type")]
public string? ContentType { get; set; }
-
- [JsonProperty("etag")]
+
+ ///
+ /// The ETag of the file for caching purposes.
+ ///
+ [JsonPropertyName("etag")]
public string? Etag { get; set; }
-
- [JsonProperty("last_modified")]
+
+ ///
+ /// The timestamp when the file was last modified.
+ ///
+ [JsonPropertyName("last_modified")]
public DateTime? LastModified { get; set; }
-
- [JsonProperty("metadata")]
+
+ ///
+ /// The custom metadata associated with the file.
+ ///
+ [JsonPropertyName("metadata")]
public Dictionary? Metadata { get; set; }
}
}
diff --git a/Storage/FileOptions.cs b/Storage/FileOptions.cs
index f633da3..93654ce 100644
--- a/Storage/FileOptions.cs
+++ b/Storage/FileOptions.cs
@@ -1,26 +1,47 @@
using System.Collections.Generic;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
+ ///
+ /// Represents configuration options for file operations in Supabase Storage.
+ ///
public class FileOptions
{
- [JsonProperty("cacheControl")]
+ ///
+ /// Controls caching behavior for the file. Default value is "3600".
+ ///
+ [JsonPropertyName("cacheControl")]
public string CacheControl { get; set; } = "3600";
- [JsonProperty("contentType")]
+ ///
+ /// Specifies the content type of the file. Default value is "text/plain;charset=UTF-8".
+ ///
+ [JsonPropertyName("contentType")]
public string ContentType { get; set; } = "text/plain;charset=UTF-8";
- [JsonProperty("upsert")]
+ ///
+ /// Determines whether to perform an upsert operation (update if exists, insert if not).
+ ///
+ [JsonPropertyName("upsert")]
public bool Upsert { get; set; }
-
- [JsonProperty("duplex")]
+
+ ///
+ /// Specifies the duplex mode for the file operation.
+ ///
+ [JsonPropertyName("duplex")]
public string? Duplex { get; set; }
-
- [JsonProperty("metadata")]
+
+ ///
+ /// Additional metadata associated with the file.
+ ///
+ [JsonPropertyName("metadata")]
public Dictionary? Metadata { get; set; }
-
- [JsonProperty("headers")]
+
+ ///
+ /// Custom headers to be included with the file operation.
+ ///
+ [JsonPropertyName("headers")]
public Dictionary? Headers { get; set; }
}
}
diff --git a/Storage/Helpers.cs b/Storage/Helpers.cs
index 18eb3ef..e22d668 100644
--- a/Storage/Helpers.cs
+++ b/Storage/Helpers.cs
@@ -1,125 +1,165 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
+using System.Runtime.CompilerServices;
using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
using System.Threading.Tasks;
using System.Web;
-using Newtonsoft.Json;
-using System.Runtime.CompilerServices;
using Supabase.Storage.Exceptions;
-using System.Threading;
[assembly: InternalsVisibleTo("StorageTests")]
+
namespace Supabase.Storage
{
- internal static class Helpers
- {
- internal static HttpClient? HttpRequestClient;
-
- internal static HttpClient? HttpUploadClient;
-
- internal static HttpClient? HttpDownloadClient;
-
- ///
- /// Initializes HttpClients with their appropriate timeouts. Called at the initialization of StorageBucketApi.
- ///
- ///
- internal static void Initialize(ClientOptions options)
- {
- HttpRequestClient = new HttpClient { Timeout = options.HttpRequestTimeout };
- HttpDownloadClient = new HttpClient { Timeout = options.HttpDownloadTimeout };
- HttpUploadClient = new HttpClient { Timeout = options.HttpUploadTimeout };
- }
-
- ///
- /// Helper to make a request using the defined parameters to an API Endpoint and coerce into a model.
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public static async Task MakeRequest(HttpMethod method, string url, object? data = null,
- Dictionary? headers = null) where T : class
- {
- var response = await MakeRequest(method, url, data, headers);
- var content = await response.Content.ReadAsStringAsync();
-
- return JsonConvert.DeserializeObject(content);
- }
-
- ///
- /// Helper to make a request using the defined parameters to an API Endpoint.
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public static async Task MakeRequest(HttpMethod method, string url, object? data = null, Dictionary? headers = null, CancellationToken cancellationToken = default)
- {
- var builder = new UriBuilder(url);
- var query = HttpUtility.ParseQueryString(builder.Query);
-
- if (data != null && method != HttpMethod.Get)
- {
- // Case if it's a Get request the data object is a dictionary
- if (data is Dictionary reqParams)
- {
- foreach (var param in reqParams)
- query[param.Key] = param.Value;
- }
- }
-
- builder.Query = query.ToString();
-
- using var requestMessage = new HttpRequestMessage(method, builder.Uri);
-
- if (data != null && method != HttpMethod.Get)
- requestMessage.Content = new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
-
- if (headers != null)
- {
- foreach (var kvp in headers)
- requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
- }
-
- var response = await HttpRequestClient!.SendAsync(requestMessage, cancellationToken);
-
- var content = await response.Content.ReadAsStringAsync();
-
- if (!response.IsSuccessStatusCode)
- {
- var errorResponse = JsonConvert.DeserializeObject(content);
- var e = new SupabaseStorageException(errorResponse?.Message ?? content)
- {
- Content = content,
- Response = response,
- StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode
- };
-
- e.AddReason();
- throw e;
- }
-
- return response;
- }
- }
-
- public class GenericResponse
- {
- [JsonProperty("message")]
- public string? Message { get; set; }
- }
-
- public class ErrorResponse
- {
- [JsonProperty("statusCode")]
- public int StatusCode { get; set; }
-
- [JsonProperty("message")]
- public string? Message { get; set; }
- }
-}
\ No newline at end of file
+ internal static class Helpers
+ {
+ internal static HttpClient? HttpRequestClient;
+
+ internal static HttpClient? HttpUploadClient;
+
+ internal static HttpClient? HttpDownloadClient;
+
+ internal static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
+ };
+
+ ///
+ /// Initializes HttpClients with their appropriate timeouts. Called at the initialization of StorageBucketApi.
+ ///
+ ///
+ internal static void Initialize(ClientOptions options)
+ {
+ HttpRequestClient = new HttpClient { Timeout = options.HttpRequestTimeout };
+ HttpDownloadClient = new HttpClient { Timeout = options.HttpDownloadTimeout };
+ HttpUploadClient = new HttpClient { Timeout = options.HttpUploadTimeout };
+ }
+
+ ///
+ /// Helper to make a request using the defined parameters to an API Endpoint and coerce into a model.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task MakeRequest(
+ HttpMethod method,
+ string url,
+ object? data = null,
+ Dictionary? headers = null
+ )
+ where T : class
+ {
+ var response = await MakeRequest(method, url, data, headers);
+ var content = await response.Content.ReadAsStringAsync();
+
+ return JsonSerializer.Deserialize(content, JsonOptions);
+ }
+
+ ///
+ /// Helper to make a request using the defined parameters to an API Endpoint.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async Task MakeRequest(
+ HttpMethod method,
+ string url,
+ object? data = null,
+ Dictionary? headers = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var builder = new UriBuilder(url);
+ var query = HttpUtility.ParseQueryString(builder.Query);
+
+ if (data != null && method != HttpMethod.Get)
+ {
+ // Case if it's a Get request the data object is a dictionary
+ if (data is Dictionary reqParams)
+ {
+ foreach (var param in reqParams)
+ query[param.Key] = param.Value;
+ }
+ }
+
+ builder.Query = query.ToString();
+
+ using var requestMessage = new HttpRequestMessage(method, builder.Uri);
+
+ if (data != null && method != HttpMethod.Get)
+ requestMessage.Content = new StringContent(
+ JsonSerializer.Serialize(data, JsonOptions),
+ Encoding.UTF8,
+ "application/json"
+ );
+
+ if (headers != null)
+ {
+ foreach (var kvp in headers)
+ requestMessage.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
+ }
+
+ var response = await HttpRequestClient!.SendAsync(requestMessage, cancellationToken);
+
+ var content = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var errorResponse = JsonSerializer.Deserialize(content, JsonOptions);
+ var e = new SupabaseStorageException(errorResponse?.Message ?? content)
+ {
+ Content = content,
+ Response = response,
+ StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode,
+ };
+
+ e.AddReason();
+ throw e;
+ }
+
+ return response;
+ }
+ }
+
+ ///
+ /// Represents a generic response returned by certain API operations.
+ ///
+ public class GenericResponse
+ {
+ ///
+ /// Gets or sets the message associated with the response.
+ ///
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+ }
+
+ ///
+ /// Represents an error response returned by the Supabase Storage service.
+ ///
+ public class ErrorResponse
+ {
+ ///
+ /// Gets or sets the status code associated with the response.
+ ///
+ [JsonPropertyName("statusCode")]
+ public int StatusCode { get; set; }
+
+ ///
+ /// Gets or sets the error message associated with the response.
+ ///
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+ }
+}
diff --git a/Storage/Interfaces/IStorageFileApi.cs b/Storage/Interfaces/IStorageFileApi.cs
index 5cb3a17..cbef9f0 100644
--- a/Storage/Interfaces/IStorageFileApi.cs
+++ b/Storage/Interfaces/IStorageFileApi.cs
@@ -118,4 +118,3 @@ Task UploadToSignedUrl(
Task CreateUploadSignedUrl(string supabasePath);
}
}
-
diff --git a/Storage/Responses/CreatedUploadSignedUrlResponse.cs b/Storage/Responses/CreatedUploadSignedUrlResponse.cs
index 70db004..ab8fedd 100644
--- a/Storage/Responses/CreatedUploadSignedUrlResponse.cs
+++ b/Storage/Responses/CreatedUploadSignedUrlResponse.cs
@@ -1,10 +1,10 @@
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage.Responses
{
internal class CreatedUploadSignedUrlResponse
{
- [JsonProperty("url")]
+ [JsonPropertyName("url")]
public string? Url { get; set; }
}
}
diff --git a/Storage/SearchOptions.cs b/Storage/SearchOptions.cs
index dfc29af..796bc73 100644
--- a/Storage/SearchOptions.cs
+++ b/Storage/SearchOptions.cs
@@ -1,4 +1,4 @@
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
@@ -7,25 +7,25 @@ public class SearchOptions
///
/// Number of files to be returned
///
- [JsonProperty("limit")]
+ [JsonPropertyName("limit")]
public int Limit { get; set; } = 100;
///
/// Starting position of query
///
- [JsonProperty("offset")]
+ [JsonPropertyName("offset")]
public int Offset { get; set; } = 0;
-
+
///
/// The search string to filter files by
///
- [JsonProperty("search")]
+ [JsonPropertyName("search")]
public string Search { get; set; } = string.Empty;
///
/// Column to sort by. Can be any colum inside of a
///
- [JsonProperty("sortBy")]
+ [JsonPropertyName("sortBy")]
public SortBy SortBy { get; set; } = new SortBy { Column = "name", Order = "asc" };
}
}
diff --git a/Storage/SortBy.cs b/Storage/SortBy.cs
index 4e226cb..ad10d05 100644
--- a/Storage/SortBy.cs
+++ b/Storage/SortBy.cs
@@ -1,13 +1,22 @@
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
namespace Supabase.Storage
{
+ ///
+ /// Represents sorting configuration for Storage queries.
+ ///
public class SortBy
{
- [JsonProperty("column")]
+ ///
+ /// The column name to sort by.
+ ///
+ [JsonPropertyName("column")]
public string? Column { get; set; }
- [JsonProperty("order")]
+ ///
+ /// The sort order direction.
+ ///
+ [JsonPropertyName("order")]
public string? Order { get; set; }
}
}
diff --git a/Storage/Storage.csproj b/Storage/Storage.csproj
index 7753bf6..e85c856 100644
--- a/Storage/Storage.csproj
+++ b/Storage/Storage.csproj
@@ -41,9 +41,9 @@
-
+
diff --git a/Storage/StorageApiConstants.cs b/Storage/StorageApiConstants.cs
new file mode 100644
index 0000000..7d29282
--- /dev/null
+++ b/Storage/StorageApiConstants.cs
@@ -0,0 +1,48 @@
+namespace Supabase.Storage
+{
+ ///
+ /// Internal constants used by the Storage API.
+ ///
+ internal static class StorageConstants
+ {
+ ///
+ /// HTTP header names used in Storage API requests.
+ ///
+ public static class Headers
+ {
+ public const string Authorization = "Authorization";
+ public const string CacheControl = "cache-control";
+ public const string ContentType = "content-type";
+ public const string Upsert = "x-upsert";
+ public const string Metadata = "x-metadata";
+ public const string Duplex = "x-duplex";
+ }
+
+ ///
+ /// API endpoint paths.
+ ///
+ public static class Endpoints
+ {
+ public const string Object = "/object";
+ public const string ObjectPublic = "/object/public";
+ public const string ObjectSign = "/object/sign";
+ public const string ObjectList = "/object/list";
+ public const string ObjectInfo = "/object/info";
+ public const string ObjectMove = "/object/move";
+ public const string ObjectCopy = "/object/copy";
+ public const string RenderImageAuthenticated = "/render/image/authenticated";
+ public const string RenderImagePublic = "/render/image/public";
+ public const string UploadResumable = "/upload/resumable";
+ public const string UploadSign = "/object/upload/sign";
+ }
+
+ ///
+ /// Default values.
+ ///
+ public static class Defaults
+ {
+ public const int CacheControlMaxAge = 3600;
+ public const int UploadChunkSize = 6 * 1024 * 1024; // 6MB
+ }
+ }
+}
diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs
index a8b9f5c..0f3fea1 100644
--- a/Storage/StorageFileApi.cs
+++ b/Storage/StorageFileApi.cs
@@ -4,12 +4,11 @@
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using BirdMessenger.Collections;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
using Supabase.Storage.Exceptions;
using Supabase.Storage.Extensions;
using Supabase.Storage.Interfaces;
@@ -17,13 +16,38 @@
namespace Supabase.Storage
{
+ ///
+ /// Provides API methods for interacting with Supabase Storage files.
+ ///
public class StorageFileApi : IStorageFileApi
{
+ ///
+ /// Gets or sets the client options for the Storage API.
+ ///
public ClientOptions Options { get; protected set; }
+
+ ///
+ /// Gets or sets the base URL for the Storage API.
+ ///
protected string Url { get; set; }
+
+ ///
+ /// Gets or sets the HTTP headers used for API requests.
+ ///
protected Dictionary Headers { get; set; }
+
+ ///
+ /// Gets or sets the current bucket identifier.
+ ///
protected string? BucketId { get; set; }
+ ///
+ /// Initializes a new instance of the StorageFileApi class with specified URL, bucket ID, options, and headers.
+ ///
+ /// The base URL for the Storage API.
+ /// The identifier of the bucket to operate on.
+ /// Client options for configuring the API behavior.
+ /// Optional HTTP headers to include with requests.
public StorageFileApi(
string url,
string bucketId,
@@ -35,6 +59,12 @@ public StorageFileApi(
Options = options ?? new ClientOptions();
}
+ ///
+ /// Initializes a new instance of the StorageFileApi class with specified URL, optional headers, and bucket ID.
+ ///
+ /// The base URL for the Storage API.
+ /// Optional HTTP headers to include with requests.
+ /// Optional bucket identifier to operate on.
public StorageFileApi(
string url,
Dictionary? headers = null,
@@ -101,12 +131,13 @@ public async Task CreateSignedUrl(
if (transformOptions != null)
{
- var transformOptionsJson = JsonConvert.SerializeObject(
+ var transformOptionsJson = JsonSerializer.Serialize(
transformOptions,
- new StringEnumConverter()
+ Helpers.JsonOptions
);
- var transformOptionsObj = JsonConvert.DeserializeObject>(
- transformOptionsJson
+ var transformOptionsObj = JsonSerializer.Deserialize>(
+ transformOptionsJson,
+ Helpers.JsonOptions
);
body.Add("transform", transformOptionsObj);
}
@@ -180,11 +211,10 @@ public async Task CreateSignedUrl(
{
options ??= new SearchOptions();
- var json = JsonConvert.SerializeObject(options);
- var body = JsonConvert.DeserializeObject>(json);
+ var json = JsonSerializer.Serialize(options, Helpers.JsonOptions);
+ var body = JsonSerializer.Deserialize>(json, Helpers.JsonOptions);
- if (body != null)
- body.Add("prefix", string.IsNullOrEmpty(path) ? "" : path);
+ body?.Add("prefix", string.IsNullOrEmpty(path) ? "" : path);
var response = await Helpers.MakeRequest>(
HttpMethod.Post,
@@ -221,6 +251,7 @@ public async Task CreateSignedUrl(
///
///
///
+ ///
///
public async Task Upload(
string localFilePath,
@@ -248,6 +279,7 @@ public async Task Upload(
///
///
///
+ ///
///
public async Task Upload(
byte[] data,
@@ -291,13 +323,13 @@ public async Task UploadToSignedUrl(
var headers = new Dictionary(Headers)
{
- ["Authorization"] = $"Bearer {signedUrl.Token}",
- ["cache-control"] = $"max-age={options.CacheControl}",
- ["content-type"] = options.ContentType,
+ [StorageConstants.Headers.Authorization] = $"Bearer {signedUrl.Token}",
+ [StorageConstants.Headers.CacheControl] = $"max-age={options.CacheControl}",
+ [StorageConstants.Headers.ContentType] = options.ContentType,
};
if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+ headers.Add(StorageConstants.Headers.Upsert, options.Upsert.ToString().ToLower());
var progress = new Progress();
@@ -338,13 +370,13 @@ public async Task UploadToSignedUrl(
var headers = new Dictionary(Headers)
{
- ["Authorization"] = $"Bearer {signedUrl.Token}",
- ["cache-control"] = $"max-age={options.CacheControl}",
- ["content-type"] = options.ContentType,
+ [StorageConstants.Headers.Authorization] = $"Bearer {signedUrl.Token}",
+ [StorageConstants.Headers.CacheControl] = $"max-age={options.CacheControl}",
+ [StorageConstants.Headers.ContentType] = options.ContentType,
};
if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+ headers.Add(StorageConstants.Headers.Upsert, options.Upsert.ToString().ToLower());
var progress = new Progress();
@@ -676,22 +708,7 @@ 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 },
- };
-
- if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
-
- if (options.Metadata != null)
- headers.Add("x-metadata", ParseMetadata(options.Metadata));
-
- options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
-
- if (options.Duplex != null)
- headers.Add("x-duplex", options.Duplex.ToLower());
+ var headers = BuildUploadHeaders(options);
var progress = new Progress();
@@ -715,7 +732,7 @@ private async Task UploadOrContinue(
var headers = new Dictionary(Headers)
{
- { "cache-control", $"max-age={options.CacheControl}" },
+ { StorageConstants.Headers.CacheControl, $"max-age={options.CacheControl}" },
};
var metadata = new MetadataCollection
@@ -726,15 +743,15 @@ private async Task UploadOrContinue(
};
if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+ headers.Add(StorageConstants.Headers.Upsert, options.Upsert.ToString().ToLower());
if (options.Metadata != null)
- headers.Add("x-metadata", ParseMetadata(options.Metadata));
+ headers.Add(StorageConstants.Headers.Metadata, ParseMetadata(options.Metadata));
options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
if (options.Duplex != null)
- headers.Add("x-duplex", options.Duplex.ToLower());
+ headers.Add(StorageConstants.Headers.Duplex, options.Duplex.ToLower());
var progress = new Progress();
@@ -763,7 +780,7 @@ private async Task UploadOrContinue(
var headers = new Dictionary(Headers)
{
- { "cache-control", $"max-age={options.CacheControl}" },
+ { StorageConstants.Headers.CacheControl, $"max-age={options.CacheControl}" },
};
var metadata = new MetadataCollection
@@ -774,15 +791,15 @@ private async Task UploadOrContinue(
};
if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+ headers.Add(StorageConstants.Headers.Upsert, options.Upsert.ToString().ToLower());
if (options.Metadata != null)
- metadata["metadata"] = JsonConvert.SerializeObject(options.Metadata);
+ metadata["metadata"] = JsonSerializer.Serialize(options.Metadata, Helpers.JsonOptions);
options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
if (options.Duplex != null)
- headers.Add("x-duplex", options.Duplex.ToLower());
+ headers.Add(StorageConstants.Headers.Duplex, options.Duplex.ToLower());
var progress = new Progress();
@@ -801,7 +818,7 @@ private async Task UploadOrContinue(
private static string ParseMetadata(Dictionary metadata)
{
- var json = JsonConvert.SerializeObject(metadata);
+ var json = JsonSerializer.Serialize(metadata, Helpers.JsonOptions);
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(json));
return base64;
@@ -817,22 +834,7 @@ 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 },
- };
-
- if (options.Upsert)
- headers.Add("x-upsert", options.Upsert.ToString().ToLower());
-
- if (options.Metadata != null)
- headers.Add("x-metadata", ParseMetadata(options.Metadata));
-
- options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
-
- if (options.Duplex != null)
- headers.Add("x-duplex", options.Duplex.ToLower());
+ var headers = BuildUploadHeaders(options);
var progress = new Progress();
@@ -902,6 +904,27 @@ private async Task DownloadBytes(
}
private string GetFinalPath(string path) => $"{BucketId}/{path}";
- }
-}
+ private Dictionary BuildUploadHeaders(FileOptions options)
+ {
+ var headers = new Dictionary(Headers)
+ {
+ { StorageConstants.Headers.CacheControl, $"max-age={options.CacheControl}" },
+ { StorageConstants.Headers.ContentType, options.ContentType },
+ };
+
+ if (options.Upsert)
+ headers.Add(StorageConstants.Headers.Upsert, options.Upsert.ToString().ToLower());
+
+ if (options.Metadata != null)
+ headers.Add(StorageConstants.Headers.Metadata, ParseMetadata(options.Metadata));
+
+ options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
+
+ if (options.Duplex != null)
+ headers.Add(StorageConstants.Headers.Duplex, options.Duplex.ToLower());
+
+ return headers;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Storage/TransformOptions.cs b/Storage/TransformOptions.cs
index 041f473..d6afe1f 100644
--- a/Storage/TransformOptions.cs
+++ b/Storage/TransformOptions.cs
@@ -1,5 +1,5 @@
using System.Runtime.Serialization;
-using Newtonsoft.Json;
+using System.Text.Json.Serialization;
using Supabase.Core.Attributes;
namespace Supabase.Storage
@@ -25,13 +25,13 @@ public enum ResizeType
///
/// The width of the image in pixels.
///
- [JsonProperty("width")]
+ [JsonPropertyName("width")]
public int? Width { get; set; }
///
/// The height of the image in pixels.
///
- [JsonProperty("height")]
+ [JsonPropertyName("height")]
public int? Height { get; set; }
///
@@ -40,13 +40,13 @@ public enum ResizeType
/// - Contain resizes the image to maintain it's aspect ratio while fitting the entire image within the width and height.
/// - Fill resizes the image to fill the entire width and height.If the object's aspect ratio does not match the width and height, the image will be stretched to fit.
///
- [JsonProperty("resize")]
+ [JsonPropertyName("resize")]
public ResizeType Resize { get; set; } = ResizeType.Cover;
///
/// Set the quality of the returned image, this is percentage based, default 80
///
- [JsonProperty("quality")]
+ [JsonPropertyName("quality")]
public int Quality { get; set; } = 80;
///
@@ -55,7 +55,7 @@ public enum ResizeType
/// When using 'origin' we force the format to be the same as the original image,
/// bypassing automatic browser optimisation such as webp conversion
///
- [JsonProperty("format")]
+ [JsonPropertyName("format")]
public string Format { get; set; } = "origin";
}
}
diff --git a/StorageTests/StorageFileTests.cs b/StorageTests/StorageFileTests.cs
index 3a80175..8299ca7 100644
--- a/StorageTests/StorageFileTests.cs
+++ b/StorageTests/StorageFileTests.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Supabase.Storage;
+using Supabase.Storage.Exceptions;
using Supabase.Storage.Interfaces;
using FileOptions = Supabase.Storage.FileOptions;
@@ -94,7 +95,7 @@ public async Task UploadResumableFile()
var name = $"{Guid.NewGuid()}.png";
var tempFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.png");
- var data = new byte[2 * 1024 * 1024];
+ var data = new byte[2 * 1024 * 1024];
var rng = new Random();
rng.NextBytes(data);
await File.WriteAllBytesAsync(tempFilePath, data);
@@ -195,6 +196,66 @@ await _bucket.UploadOrResume(
await _bucket.Remove([name]);
}
+ [TestMethod("File Resume Upload File as Byte Not Override Existing")]
+ public async Task UploadResumableByteNotOverrideExisting()
+ {
+ var didTriggerProgress = new TaskCompletionSource();
+ var data = new byte[1 * 1024 * 1024];
+ var rng = new Random();
+ rng.NextBytes(data);
+ var name = $"{Guid.NewGuid()}.png";
+ var metadata = new Dictionary
+ {
+ ["custom"] = "metadata",
+ ["local_file"] = "local_file",
+ };
+
+ var headers = new Dictionary { ["x-version"] = "123" };
+
+ var options = new FileOptions
+ {
+ Duplex = "duplex",
+ Metadata = metadata,
+ Headers = headers,
+ };
+
+ await _bucket.UploadOrResume(
+ data,
+ name,
+ options,
+ (x, y) =>
+ {
+ didTriggerProgress.TrySetResult(true);
+ }
+ );
+
+ var action = async () =>
+ {
+ await _bucket.UploadOrResume(
+ data,
+ name,
+ options,
+ (x, y) =>
+ {
+ didTriggerProgress.TrySetResult(true);
+ }
+ );
+ };
+
+ await Assert.ThrowsExceptionAsync(action);
+
+ var list = await _bucket.List();
+ Assert.IsNotNull(list);
+
+ var existing = list.Find(item => item.Name == name);
+ Assert.IsNotNull(existing);
+
+ var sentProgressEvent = await didTriggerProgress.Task;
+ Assert.IsTrue(sentProgressEvent);
+
+ await _bucket.Remove([name]);
+ }
+
[TestMethod("File: Resume Upload as Byte override existing one")]
public async Task UploadResumableByteDuplicate()
{
@@ -257,6 +318,12 @@ public async Task UploadOrResumeByteWithInterruptionAndResume()
var firstUploadProgressTriggered = new TaskCompletionSource();
var resumeUploadProgressTriggered = new TaskCompletionSource();
+ var assetFileName = "jetbrains.dmg";
+ var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)?.Replace("file:", "");
+ Assert.IsNotNull(basePath);
+
+ var imagePath = Path.Combine(basePath!, "Assets", assetFileName);
+
var data = new byte[200 * 1024 * 1024];
var rng = new Random();
rng.NextBytes(data);
@@ -275,7 +342,7 @@ public async Task UploadOrResumeByteWithInterruptionAndResume()
try
{
await _bucket.UploadOrResume(
- data,
+ imagePath,
name,
options,
(_, progress) =>
@@ -284,6 +351,11 @@ await _bucket.UploadOrResume(
cts.Cancel();
Console.WriteLine($"First upload progress: {progress}");
+ if (progress >= 30)
+ {
+ cts.Cancel();
+ Console.WriteLine("Cancelling first upload");
+ }
firstUploadProgressTriggered.TrySetResult(true);
},
cts.Token
@@ -311,7 +383,7 @@ await Task.WhenAny(
);
await _bucket.UploadOrResume(
- data,
+ imagePath,
name,
options,
(_, progress) =>
@@ -409,7 +481,7 @@ await _bucket.Upload(
await _bucket.Remove(new List { name });
}
-
+
[TestMethod("File: Cancel Upload Arbitrary Byte Array")]
public async Task UploadArbitraryByteArrayCanceled()
{
@@ -423,7 +495,14 @@ public async Task UploadArbitraryByteArrayCanceled()
var action = async () =>
{
- await _bucket.Upload(data, name, null, (_, _) => tsc.TrySetResult(true), true, ctk.Token);
+ await _bucket.Upload(
+ data,
+ name,
+ null,
+ (_, _) => tsc.TrySetResult(true),
+ true,
+ ctk.Token
+ );
};
await Assert.ThrowsExceptionAsync(action);
@@ -665,4 +744,3 @@ public async Task CanCreateSignedUploadUrl()
Assert.IsTrue(Uri.IsWellFormedUriString(result.SignedUrl.ToString(), UriKind.Absolute));
}
}
-