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
5 changes: 5 additions & 0 deletions Storage/DownloadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ public class DownloadOptions
/// <p>When field is empty, the original file name will be used. Use <see cref="UseOriginalFileName"/> for quick initialized with original file names.</p>
/// </summary>
public string? FileName { get; set; }

/// <summary>
/// Append a cache nonce parameter to the URL to invalidate the cache
/// </summary>
public string? CacheNonce { get; set; }
}
}
10 changes: 7 additions & 3 deletions Storage/Extensions/DownloadOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ public static NameValueCollection ToQueryCollection(this DownloadOptions downloa
{
var query = HttpUtility.ParseQueryString(string.Empty);

if (download.FileName == null)
if (download.FileName != null)
{
return query;
query.Add("download", string.IsNullOrEmpty(download.FileName) ? "true" : download.FileName);
}

query.Add("download", string.IsNullOrEmpty(download.FileName) ? "true" : download.FileName);

if (download.CacheNonce != null)
{
query.Add("cacheNonce", download.CacheNonce);
}

return query;
}
Expand Down
17 changes: 11 additions & 6 deletions Storage/Interfaces/IStorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,43 @@ Task<string> CreateSignedUrl(
int expiresIn,
DownloadOptions? options = null
);
Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null, CancellationToken cancellationToken = default);
Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null, CancellationToken cancellationToken = default, string? cacheNonce = null);
Task<byte[]> Download(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
);
Task<string> Download(
string supabasePath,
string localPath,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
);
Task<string> Download(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
);
Task<byte[]> DownloadPublicFile(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
);
Task<string> DownloadPublicFile(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
);
string GetPublicUrl(
string path,
Expand Down
57 changes: 41 additions & 16 deletions Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -515,20 +515,22 @@ await Helpers.MakeRequest<GenericResponse>(
/// <param name="transformOptions"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<string> Download(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var url =
transformOptions != null
? $"{Url}/render/image/authenticated/{GetFinalPath(supabasePath)}"
: $"{Url}/object/{GetFinalPath(supabasePath)}";
return DownloadFile(url, localPath, transformOptions, onProgress, cancellationToken);
return DownloadFile(url, localPath, transformOptions, onProgress, cancellationToken, cacheNonce);
}

/// <summary>
Expand All @@ -538,13 +540,15 @@ public Task<string> Download(
/// <param name="localPath"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<string> Download(
string supabasePath,
string localPath,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
) => Download(supabasePath, localPath, null, onProgress: onProgress, cancellationToken);
CancellationToken cancellationToken = default,
string? cacheNonce = null
) => 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}?)"/>
Expand All @@ -553,16 +557,18 @@ public Task<string> Download(
/// <param name="transformOptions"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<byte[]> Download(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var url = $"{Url}/object/{GetFinalPath(supabasePath)}";
return DownloadBytes(url, transformOptions, onProgress, cancellationToken);
return DownloadBytes(url, transformOptions, onProgress, cancellationToken, cacheNonce);
}

/// <summary>
Expand All @@ -571,9 +577,10 @@ public Task<byte[]> Download(
/// <param name="supabasePath"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null, CancellationToken cancellationToken = default) =>
Download(supabasePath, transformOptions: null, onProgress: onProgress, cancellationToken);
public Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null, CancellationToken cancellationToken = default, string? cacheNonce = null) =>
Download(supabasePath, transformOptions: null, onProgress: onProgress, cancellationToken, cacheNonce);

/// <summary>
/// Downloads a public file to the filesystem. This method DOES NOT VERIFY that the file is actually public.
Expand All @@ -583,17 +590,19 @@ public Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgres
/// <param name="transformOptions"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<string> DownloadPublicFile(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var url = GetPublicUrl(supabasePath, transformOptions);
return DownloadFile(url, localPath, transformOptions, onProgress, cancellationToken);
return DownloadFile(url, localPath, transformOptions, onProgress, cancellationToken, cacheNonce);
}

/// <summary>
Expand All @@ -603,16 +612,18 @@ public Task<string> DownloadPublicFile(
/// <param name="transformOptions"></param>
/// <param name="onProgress"></param>
/// <param name="cancellationToken"></param>
/// <param name="cacheNonce"></param>
/// <returns></returns>
public Task<byte[]> DownloadPublicFile(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var url = GetPublicUrl(supabasePath, transformOptions);
return DownloadBytes(url, transformOptions, onProgress, cancellationToken);
return DownloadBytes(url, transformOptions, onProgress, cancellationToken, cacheNonce);
}

/// <summary>
Expand Down Expand Up @@ -859,14 +870,21 @@ private async Task<string> DownloadFile(
string localPath,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var query = HttpUtility.ParseQueryString(string.Empty);
var builder = new UriBuilder(url);
var progress = new Progress<float>();

if (transformOptions != null)
builder.Query = transformOptions.ToQueryCollection().ToString();
query.Add(transformOptions.ToQueryCollection());

if (cacheNonce != null)
query.Add("cacheNonce", cacheNonce);

builder.Query = query.ToString();

if (onProgress != null)
progress.ProgressChanged += onProgress;
Expand All @@ -893,14 +911,21 @@ private async Task<byte[]> DownloadBytes(
string url,
TransformOptions? transformOptions = null,
EventHandler<float>? onProgress = null,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
string? cacheNonce = null
)
{
var query = HttpUtility.ParseQueryString(string.Empty);
var builder = new UriBuilder(url);
var progress = new Progress<float>();

if (transformOptions != null)
builder.Query = transformOptions.ToQueryCollection().ToString();
query.Add(transformOptions.ToQueryCollection());

if (cacheNonce != null)
query.Add("cacheNonce", cacheNonce);

builder.Query = query.ToString();

if (onProgress != null)
progress.ProgressChanged += onProgress;
Expand Down
31 changes: 31 additions & 0 deletions StorageTests/Files/StorageFileApiContractTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Text;
Expand All @@ -13,6 +14,7 @@
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using FileOptions = Supabase.Storage.FileOptions;

namespace StorageTests.Files;

Expand Down Expand Up @@ -290,6 +292,35 @@ public async Task Download_ShouldGetBytesFromTheObjectPath()
}
}

[TestMethod]
public async Task Download_ShouldSendTheCacheNonceInTheQuery_GivenACacheNonce()
{
this.server.Given(Request.Create().WithPath($"/storage/v1/object/{Bucket}/a.bin").UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithBody(Encoding.UTF8.GetBytes("file-bytes")));
await this.client.From(Bucket).Download("a.bin", (EventHandler<float>?) null, cacheNonce: "nonce-123");
this.SingleRequest().Query!.Should().ContainKey("cacheNonce")
.WhoseValue.Should().Contain("nonce-123", "the nonce must ride the request so the CDN cache is bypassed");
}

[TestMethod]
public async Task Download_ShouldSendTheCacheNonceInTheQuery_GivenACacheNonceAndLocalPath()
{
this.server.Given(Request.Create().WithPath($"/storage/v1/object/{Bucket}/a.bin").UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithBody(Encoding.UTF8.GetBytes("file-bytes")));
var localPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.bin");
try
{
await this.client.From(Bucket).Download("a.bin", localPath, (EventHandler<float>?) null, cacheNonce: "nonce-123");
this.SingleRequest().Query!.Should().ContainKey("cacheNonce")
.WhoseValue.Should().Contain("nonce-123", "the to-disk path must carry the nonce just as the byte path does");
}
finally
{
if (File.Exists(localPath))
File.Delete(localPath);
}
}

[TestMethod]
public async Task PurgeCache_ShouldDeleteTheCdnObjectPathWithNoBodyAndReturnTheMessage()
{
Expand Down
7 changes: 7 additions & 0 deletions StorageTests/Options/DownloadOptionsExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ public void ToQueryCollection_ShouldEmitTheFileName_GivenAName()
var query = new DownloadOptions { FileName = "custom-file.png" }.ToQueryCollection();
query["download"].Should().Be("custom-file.png");
}

[TestMethod]
public void ToQueryCollection_ShouldEmitTheCacheNonce_GivenACacheNonce()
{
var query = new DownloadOptions { CacheNonce = "nonce-123" }.ToQueryCollection();
query["cacheNonce"].Should().Be("nonce-123");
}
}
Loading