diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs
index 73a440f..31d8fe3 100644
--- a/Storage/StorageFileApi.cs
+++ b/Storage/StorageFileApi.cs
@@ -16,15 +16,36 @@
namespace Supabase.Storage
{
+ ///
public class StorageFileApi : IStorageFileApi
{
+ ///
public ClientOptions Options { get; protected set; }
+ ///
+ ///
+ ///
protected string Url { get; set; }
+ ///
+ ///
+ ///
protected Dictionary Headers { get; set; }
+ ///
+ ///
+ ///
protected string? BucketId { get; set; }
- protected Header StorageHeader = new();
+ ///
+ ///
+ ///
+ protected readonly Header StorageHeader = new();
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
public StorageFileApi(
string url,
string bucketId,
@@ -36,6 +57,12 @@ public StorageFileApi(
Options = options ?? new ClientOptions();
}
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
public StorageFileApi(
string url,
Dictionary? headers = null,
@@ -50,7 +77,7 @@ public StorageFileApi(
}
///
- /// A simple convenience function to get the URL for an asset in a public bucket.If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset.
+ /// A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset.
/// This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset.
///
///
@@ -68,7 +95,7 @@ public string GetPublicUrl(
if (downloadOptions != null)
queryParams.Add(downloadOptions.ToQueryCollection());
- if (transformOptions == null)
+ if (transformOptions is null or { IsEmpty: true })
{
var queryParamsString = queryParams.ToString();
return $"{Url}/object/public/{GetFinalPath(path)}?{queryParamsString}";
@@ -84,9 +111,9 @@ public string GetPublicUrl(
}
///
- /// Create signed url to download file without requiring permissions. This URL can be valid for a set number of seconds.
+ /// Create signed url to download a file without requiring permissions. This URL can be valid for a set number of seconds.
///
- /// The file path to be downloaded, including the current file name. For example `folder/image.png`.
+ /// The file path to be downloaded, including the current file name. For example, `folder/image.png`.
/// The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
///
///
@@ -101,7 +128,7 @@ public async Task CreateSignedUrl(
var body = new Dictionary { { "expiresIn", expiresIn } };
var url = $"{Url}/object/sign/{GetFinalPath(path)}";
- if (transformOptions != null)
+ if (transformOptions is { IsEmpty: false })
{
var transformOptionsJson = JsonConvert.SerializeObject(
transformOptions,
@@ -133,7 +160,7 @@ public async Task CreateSignedUrl(
///
/// Create signed URLs to download files without requiring permissions. These URLs can be valid for a set number of seconds.
///
- /// paths The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png'].
+ /// The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png'].
/// The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
///
///
@@ -224,6 +251,7 @@ public async Task CreateSignedUrl(
///
///
///
+ ///
///
public async Task Upload(
string localFilePath,
@@ -243,6 +271,26 @@ public async Task Upload(
return result;
}
+ ///
+ /// Uploads a byte array to an existing bucket.
+ ///
+ ///
+ /// The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
+ ///
+ ///
+ ///
+ ///
+ public Task Upload(
+ byte[] data,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ return Upload(data, supabasePath, options, onProgress, true, cancellationToken);
+ }
+
///
/// Uploads a byte array to an existing bucket.
///
@@ -251,6 +299,7 @@ public async Task Upload(
///
///
///
+ ///
///
public async Task Upload(
byte[] data,
@@ -508,7 +557,9 @@ await Helpers.MakeRequest(
}
///
- /// Downloads a file from a private bucket. For public buckets, use
+ /// Downloads a file from a private bucket. For public buckets, use
+ /// DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)
+ ///
///
///
///
@@ -527,14 +578,16 @@ public Task Download(
)
{
var url =
- transformOptions != null
+ transformOptions is { IsEmpty: false }
? $"{Url}/render/image/authenticated/{GetFinalPath(supabasePath)}"
: $"{Url}/object/{GetFinalPath(supabasePath)}";
return DownloadFile(url, localPath, transformOptions, onProgress, cancellationToken, cacheNonce);
}
///
- /// Downloads a file from a private bucket. For public buckets, use
+ /// Downloads a file from a private bucket. For public buckets, use
+ /// DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)
+ ///
///
///
///
@@ -551,7 +604,9 @@ public Task Download(
) => Download(supabasePath, localPath, null, onProgress: onProgress, cancellationToken, cacheNonce);
///
- /// Downloads a byte array from a private bucket to be used programmatically. For public buckets
+ /// Downloads a byte array from a private bucket to be used programmatically. For public buckets
+ /// DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)
+ ///
///
///
///
@@ -572,7 +627,9 @@ public Task Download(
}
///
- /// Downloads a byte array from a private bucket to be used programmatically. For public buckets
+ /// Downloads a byte array from a private bucket to be used programmatically. For public buckets
+ /// DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)
+ ///
///
///
///
@@ -627,20 +684,20 @@ public Task DownloadPublicFile(
}
///
- /// Deletes file within the same bucket
+ /// Deletes a file within the same bucket
///
- /// A path to delete, for example `folder/image.png`.
+ /// A path to delete, for example, `folder/image.png`.
///
public async Task Remove(string path)
{
- var result = await Remove(new List { path });
+ var result = await Remove([path]);
return result?.FirstOrDefault();
}
///
/// Deletes files within the same bucket
///
- /// An array of files to be deletes, including the path and file name. For example [`folder/image.png`].
+ /// An array of files to be deleted, including the path and file name. For example [`folder/image.png`].
///
public async Task?> Remove(List paths)
{
@@ -658,7 +715,7 @@ public Task DownloadPublicFile(
///
/// Creates an upload signed URL. Use it to upload a file straight to the bucket without credentials
///
- /// The file path, including the current file name. For example `folder/image.png`.
+ /// The file path, including the current file name. For example, `folder/image.png`.
///
public async Task CreateUploadSignedUrl(string supabasePath)
{
@@ -878,7 +935,7 @@ private async Task DownloadFile(
var builder = new UriBuilder(url);
var progress = new Progress();
- if (transformOptions != null)
+ if (transformOptions is { IsEmpty: false })
query.Add(transformOptions.ToQueryCollection());
if (cacheNonce != null)
@@ -919,7 +976,7 @@ private async Task DownloadBytes(
var builder = new UriBuilder(url);
var progress = new Progress();
- if (transformOptions != null)
+ if (transformOptions is { IsEmpty: false })
query.Add(transformOptions.ToQueryCollection());
if (cacheNonce != null)
diff --git a/Storage/TransformOptions.cs b/Storage/TransformOptions.cs
index 041f473..0287d89 100644
--- a/Storage/TransformOptions.cs
+++ b/Storage/TransformOptions.cs
@@ -57,5 +57,19 @@ public enum ResizeType
///
[JsonProperty("format")]
public string Format { get; set; } = "origin";
+
+ private static readonly TransformOptions Default = new();
+
+ ///
+ /// True when no transform has been requested — every field still holds its constructed default,
+ /// so the caller wants the original object, not a render. Callers use this to keep an
+ /// unconfigured instance off the image-render endpoint (issue #37).
+ ///
+ internal bool IsEmpty =>
+ Width == Default.Width
+ && Height == Default.Height
+ && Resize == Default.Resize
+ && Quality == Default.Quality
+ && Format == Default.Format;
}
}
diff --git a/StorageTests/Files/PublicUrlTests.cs b/StorageTests/Files/PublicUrlTests.cs
index 49c7076..e51dc5a 100644
--- a/StorageTests/Files/PublicUrlTests.cs
+++ b/StorageTests/Files/PublicUrlTests.cs
@@ -8,8 +8,9 @@ namespace StorageTests.Files;
///
/// Covers , the pure URL builder: it targets the public
-/// object path by default, switches to the image-render path when transform options are supplied,
-/// and appends the download attribute when download options ask for it.
+/// object path by default, switches to the image-render path only when a transform is actually
+/// requested (an empty options object is not), and appends the download attribute when download
+/// options ask for it.
///
[TestClass]
[TestCategory("Unit")]
@@ -34,6 +35,14 @@ public void GetPublicUrl_ShouldUseTheRenderPath_GivenTransformOptions()
url.Should().Contain("/render/image/public/bucket/a.png").And.Contain("width=100");
}
+ [TestMethod]
+ public void GetPublicUrl_ShouldTargetThePublicObjectPath_GivenEmptyTransformOptions()
+ {
+ var url = Bucket().GetPublicUrl("a.png", new TransformOptions());
+ url.Should().StartWith($"{BaseUrl}/object/public/bucket/a.png").And.NotContain("/render/image/",
+ "an empty TransformOptions requests no transform, so it must not route to the render endpoint (issue #37)");
+ }
+
[TestMethod]
public void GetPublicUrl_ShouldAppendDownloadName_GivenDownloadOptions()
{