diff --git a/Storage/DownloadOptions.cs b/Storage/DownloadOptions.cs
index 44f7c00..883e95a 100644
--- a/Storage/DownloadOptions.cs
+++ b/Storage/DownloadOptions.cs
@@ -15,5 +15,10 @@ public class DownloadOptions
///
When field is empty, the original file name will be used. Use for quick initialized with original file names.
///
public string? FileName { get; set; }
+
+ ///
+ /// Append a cache nonce parameter to the URL to invalidate the cache
+ ///
+ public string? CacheNonce { get; set; }
}
}
\ No newline at end of file
diff --git a/Storage/Extensions/DownloadOptionsExtension.cs b/Storage/Extensions/DownloadOptionsExtension.cs
index 6911924..9d6135c 100644
--- a/Storage/Extensions/DownloadOptionsExtension.cs
+++ b/Storage/Extensions/DownloadOptionsExtension.cs
@@ -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;
}
diff --git a/Storage/Interfaces/IStorageFileApi.cs b/Storage/Interfaces/IStorageFileApi.cs
index 8c7df70..f78e78e 100644
--- a/Storage/Interfaces/IStorageFileApi.cs
+++ b/Storage/Interfaces/IStorageFileApi.cs
@@ -20,38 +20,43 @@ Task CreateSignedUrl(
int expiresIn,
DownloadOptions? options = null
);
- Task Download(string supabasePath, EventHandler? onProgress = null, CancellationToken cancellationToken = default);
+ Task Download(string supabasePath, EventHandler? onProgress = null, CancellationToken cancellationToken = default, string? cacheNonce = null);
Task Download(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler? onProgress = null,
- CancellationToken cancellationToken = default
+ CancellationToken cancellationToken = default,
+ string? cacheNonce = null
);
Task Download(
string supabasePath,
string localPath,
EventHandler? onProgress = null,
- CancellationToken cancellationToken = default
+ CancellationToken cancellationToken = default,
+ string? cacheNonce = null
);
Task Download(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler? onProgress = null,
- CancellationToken cancellationToken = default
+ CancellationToken cancellationToken = default,
+ string? cacheNonce = null
);
Task DownloadPublicFile(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler? onProgress = null,
- CancellationToken cancellationToken = default
+ CancellationToken cancellationToken = default,
+ string? cacheNonce = null
);
Task DownloadPublicFile(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler? onProgress = null,
- CancellationToken cancellationToken = default
+ CancellationToken cancellationToken = default,
+ string? cacheNonce = null
);
string GetPublicUrl(
string path,
diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs
index a0c29c0..73a440f 100644
--- a/Storage/StorageFileApi.cs
+++ b/Storage/StorageFileApi.cs
@@ -515,20 +515,22 @@ await Helpers.MakeRequest(
///
///
///
+ ///
///
public Task Download(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler? 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);
}
///
@@ -538,13 +540,15 @@ public Task Download(
///
///
///
+ ///
///
public Task Download(
string supabasePath,
string localPath,
EventHandler? 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);
///
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets
@@ -553,16 +557,18 @@ public Task Download(
///
///
///
+ ///
///
public Task Download(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler? 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);
}
///
@@ -571,9 +577,10 @@ public Task Download(
///
///
///
+ ///
///
- public Task Download(string supabasePath, EventHandler? onProgress = null, CancellationToken cancellationToken = default) =>
- Download(supabasePath, transformOptions: null, onProgress: onProgress, cancellationToken);
+ public Task Download(string supabasePath, EventHandler? onProgress = null, CancellationToken cancellationToken = default, string? cacheNonce = null) =>
+ Download(supabasePath, transformOptions: null, onProgress: onProgress, cancellationToken, cacheNonce);
///
/// Downloads a public file to the filesystem. This method DOES NOT VERIFY that the file is actually public.
@@ -583,17 +590,19 @@ public Task Download(string supabasePath, EventHandler? onProgres
///
///
///
+ ///
///
public Task DownloadPublicFile(
string supabasePath,
string localPath,
TransformOptions? transformOptions = null,
EventHandler? 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);
}
///
@@ -603,16 +612,18 @@ public Task DownloadPublicFile(
///
///
///
+ ///
///
public Task DownloadPublicFile(
string supabasePath,
TransformOptions? transformOptions = null,
EventHandler? 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);
}
///
@@ -859,14 +870,21 @@ private async Task DownloadFile(
string localPath,
TransformOptions? transformOptions = null,
EventHandler? 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();
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;
@@ -893,14 +911,21 @@ private async Task DownloadBytes(
string url,
TransformOptions? transformOptions = null,
EventHandler? 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();
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;
diff --git a/StorageTests/Files/StorageFileApiContractTests.cs b/StorageTests/Files/StorageFileApiContractTests.cs
index 7c41628..7d7f9cb 100644
--- a/StorageTests/Files/StorageFileApiContractTests.cs
+++ b/StorageTests/Files/StorageFileApiContractTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Text;
@@ -13,6 +14,7 @@
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
+using FileOptions = Supabase.Storage.FileOptions;
namespace StorageTests.Files;
@@ -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?) 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?) 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()
{
diff --git a/StorageTests/Options/DownloadOptionsExtensionTests.cs b/StorageTests/Options/DownloadOptionsExtensionTests.cs
index c6e7217..5e4c350 100644
--- a/StorageTests/Options/DownloadOptionsExtensionTests.cs
+++ b/StorageTests/Options/DownloadOptionsExtensionTests.cs
@@ -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");
+ }
}