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() {