Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 76 additions & 19 deletions Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,36 @@

namespace Supabase.Storage
{
/// <inheritdoc />
public class StorageFileApi : IStorageFileApi<FileObject>
{
/// <inheritdoc />
public ClientOptions Options { get; protected set; }
/// <summary>
///
/// </summary>
protected string Url { get; set; }
/// <summary>
///
/// </summary>
protected Dictionary<string, string> Headers { get; set; }
/// <summary>
///
/// </summary>
protected string? BucketId { get; set; }

protected Header StorageHeader = new();
/// <summary>
///
/// </summary>
protected readonly Header StorageHeader = new();

/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="bucketId"></param>
/// <param name="options"></param>
/// <param name="headers"></param>
public StorageFileApi(
string url,
string bucketId,
Expand All @@ -36,6 +57,12 @@ public StorageFileApi(
Options = options ?? new ClientOptions();
}

/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="headers"></param>
/// <param name="bucketId"></param>
public StorageFileApi(
string url,
Dictionary<string, string>? headers = null,
Expand All @@ -50,7 +77,7 @@ public StorageFileApi(
}

/// <summary>
/// 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.
/// </summary>
/// <param name="path"></param>
Expand All @@ -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}";
Expand All @@ -84,9 +111,9 @@ public string GetPublicUrl(
}

/// <summary>
/// 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.
/// </summary>
/// <param name="path">The file path to be downloaded, including the current file name. For example `folder/image.png`.</param>
/// <param name="path">The file path to be downloaded, including the current file name. For example, `folder/image.png`.</param>
/// <param name="expiresIn">The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.</param>
/// <param name="transformOptions"></param>
/// <param name="downloadOptions"></param>
Expand All @@ -101,7 +128,7 @@ public async Task<string> CreateSignedUrl(
var body = new Dictionary<string, object?> { { "expiresIn", expiresIn } };
var url = $"{Url}/object/sign/{GetFinalPath(path)}";

if (transformOptions != null)
if (transformOptions is { IsEmpty: false })
{
var transformOptionsJson = JsonConvert.SerializeObject(
transformOptions,
Expand Down Expand Up @@ -133,7 +160,7 @@ public async Task<string> CreateSignedUrl(
/// <summary>
/// Create signed URLs to download files without requiring permissions. These URLs can be valid for a set number of seconds.
/// </summary>
/// <param name="paths">paths The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png'].</param>
/// <param name="paths">The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png'].</param>
/// <param name="expiresIn">The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.</param>
/// <param name="downloadOptions"></param>
/// <returns></returns>
Expand Down Expand Up @@ -224,6 +251,7 @@ public async Task<string> CreateSignedUrl(
/// <param name="options"></param>
/// <param name="onProgress"></param>
/// <param name="inferContentType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> Upload(
string localFilePath,
Expand All @@ -243,6 +271,26 @@ public async Task<string> Upload(
return result;
}

/// <summary>
/// Uploads a byte array to an existing bucket.
/// </summary>
/// <param name="data"></param>
/// <param name="supabasePath">The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.</param>
/// <param name="options"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task<string> Upload(
byte[] data,
string supabasePath,
FileOptions? options = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
)
{
return Upload(data, supabasePath, options, onProgress, true, cancellationToken);
}

/// <summary>
/// Uploads a byte array to an existing bucket.
/// </summary>
Expand All @@ -251,6 +299,7 @@ public async Task<string> Upload(
/// <param name="options"></param>
/// <param name="onProgress"></param>
/// <param name="inferContentType"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<string> Upload(
byte[] data,
Expand Down Expand Up @@ -508,7 +557,9 @@ await Helpers.MakeRequest<GenericResponse>(
}

/// <summary>
/// Downloads a file from a private bucket. For public buckets, use <see cref="DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)"/>
/// Downloads a file from a private bucket. For public buckets, use <see>
/// <cref>DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)</cref>
/// </see>
/// </summary>
/// <param name="supabasePath"></param>
/// <param name="localPath"></param>
Expand All @@ -527,14 +578,16 @@ public Task<string> 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);
}

/// <summary>
/// Downloads a file from a private bucket. For public buckets, use <see cref="DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)"/>
/// Downloads a file from a private bucket. For public buckets, use <see>
/// <cref>DownloadPublicFile(string, string, TransformOptions?, EventHandler{float}?)</cref>
/// </see>
/// </summary>
/// <param name="supabasePath"></param>
/// <param name="localPath"></param>
Expand All @@ -551,7 +604,9 @@ public Task<string> Download(
) => Download(supabasePath, localPath, null, onProgress: onProgress, cancellationToken, cacheNonce);

/// <summary>
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets <see cref="DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)"/>
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets <see>
/// <cref>DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)</cref>
/// </see>
/// </summary>
/// <param name="supabasePath"></param>
/// <param name="transformOptions"></param>
Expand All @@ -572,7 +627,9 @@ public Task<byte[]> Download(
}

/// <summary>
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets <see cref="DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)"/>
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets <see>
/// <cref>DownloadPublicFile(string, TransformOptions?, EventHandler{float}?)</cref>
/// </see>
/// </summary>
/// <param name="supabasePath"></param>
/// <param name="onProgress"></param>
Expand Down Expand Up @@ -627,20 +684,20 @@ public Task<byte[]> DownloadPublicFile(
}

/// <summary>
/// Deletes file within the same bucket
/// Deletes a file within the same bucket
/// </summary>
/// <param name="path">A path to delete, for example `folder/image.png`.</param>
/// <param name="path">A path to delete, for example, `folder/image.png`.</param>
/// <returns></returns>
public async Task<FileObject?> Remove(string path)
{
var result = await Remove(new List<string> { path });
var result = await Remove([path]);
return result?.FirstOrDefault();
}

/// <summary>
/// Deletes files within the same bucket
/// </summary>
/// <param name="paths">An array of files to be deletes, including the path and file name. For example [`folder/image.png`].</param>
/// <param name="paths">An array of files to be deleted, including the path and file name. For example [`folder/image.png`].</param>
/// <returns></returns>
public async Task<List<FileObject>?> Remove(List<string> paths)
{
Expand All @@ -658,7 +715,7 @@ public Task<byte[]> DownloadPublicFile(
/// <summary>
/// Creates an upload signed URL. Use it to upload a file straight to the bucket without credentials
/// </summary>
/// <param name="supabasePath">The file path, including the current file name. For example `folder/image.png`.</param>
/// <param name="supabasePath">The file path, including the current file name. For example, `folder/image.png`.</param>
/// <returns></returns>
public async Task<UploadSignedUrl> CreateUploadSignedUrl(string supabasePath)
{
Expand Down Expand Up @@ -878,7 +935,7 @@ private async Task<string> DownloadFile(
var builder = new UriBuilder(url);
var progress = new Progress<float>();

if (transformOptions != null)
if (transformOptions is { IsEmpty: false })
query.Add(transformOptions.ToQueryCollection());

if (cacheNonce != null)
Expand Down Expand Up @@ -919,7 +976,7 @@ private async Task<byte[]> DownloadBytes(
var builder = new UriBuilder(url);
var progress = new Progress<float>();

if (transformOptions != null)
if (transformOptions is { IsEmpty: false })
query.Add(transformOptions.ToQueryCollection());

if (cacheNonce != null)
Expand Down
14 changes: 14 additions & 0 deletions Storage/TransformOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,19 @@ public enum ResizeType
/// </summary>
[JsonProperty("format")]
public string Format { get; set; } = "origin";

private static readonly TransformOptions Default = new();

/// <summary>
/// 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).
/// </summary>
internal bool IsEmpty =>
Width == Default.Width
&& Height == Default.Height
&& Resize == Default.Resize
&& Quality == Default.Quality
&& Format == Default.Format;
}
}
13 changes: 11 additions & 2 deletions StorageTests/Files/PublicUrlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ namespace StorageTests.Files;

/// <summary>
/// Covers <see cref="StorageFileApi.GetPublicUrl"/>, 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.
/// </summary>
[TestClass]
[TestCategory("Unit")]
Expand All @@ -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()
{
Expand Down
Loading