Skip to content

Commit 06dd7f3

Browse files
committed
feat: add support for download options
1 parent a66944f commit 06dd7f3

File tree

5 files changed

+112
-12
lines changed

5 files changed

+112
-12
lines changed

Diff for: Storage/DownloadOptions.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using Newtonsoft.Json;
2+
3+
namespace Supabase.Storage
4+
{
5+
public class DownloadOptions
6+
{
7+
/// <summary>
8+
/// <p>Use the original file name when downloading</p>
9+
/// </summary>
10+
public static readonly DownloadOptions UseOriginalFileName = new DownloadOptions { FileName = "" };
11+
12+
/// <summary>
13+
/// <p>The name of the file to be downloaded</p>
14+
/// <p>When field is null, no download attribute will be added.</p>
15+
/// <p>When field is empty, the original file name will be used. Use <see cref="UseOriginalFileName"/> for quick initialized with original file names.</p>
16+
/// </summary>
17+
public string? FileName { get; set; }
18+
}
19+
}

Diff for: Storage/Extensions/DownloadOptionsExtension.cs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Specialized;
2+
using System.Web;
3+
4+
namespace Supabase.Storage.Extensions
5+
{
6+
public static class DownloadOptionsExtension
7+
{
8+
/// <summary>
9+
/// Transforms options into a NameValueCollection to be used with a <see cref="UriBuilder"/>
10+
/// </summary>
11+
/// <param name="download"></param>
12+
/// <returns></returns>
13+
public static NameValueCollection ToQueryCollection(this DownloadOptions download)
14+
{
15+
var query = HttpUtility.ParseQueryString(string.Empty);
16+
17+
if (download.FileName == null)
18+
{
19+
return query;
20+
}
21+
22+
query.Add("download", string.IsNullOrEmpty(download.FileName) ? "true" : download.FileName);
23+
24+
return query;
25+
}
26+
}
27+
}

Diff for: Storage/Interfaces/IStorageFileApi.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ public interface IStorageFileApi<TFileObject>
88
where TFileObject : FileObject
99
{
1010
ClientOptions Options { get; }
11-
Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null);
12-
Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn);
11+
Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? options = null);
12+
Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn, DownloadOptions? options = null);
1313
Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null);
1414
Task<byte[]> Download(string supabasePath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
1515
Task<string> Download(string supabasePath, string localPath, EventHandler<float>? onProgress = null);
1616
Task<string> Download(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
1717
Task<byte[]> DownloadPublicFile(string supabasePath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
1818
Task<string> DownloadPublicFile(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
19-
string GetPublicUrl(string path, TransformOptions? transformOptions = null);
19+
string GetPublicUrl(string path, TransformOptions? transformOptions = null, DownloadOptions? options = null);
2020
Task<List<TFileObject>?> List(string path = "", SearchOptions? options = null);
2121
Task<bool> Move(string fromPath, string toPath);
2222
Task<TFileObject?> Remove(string path);

Diff for: Storage/StorageFileApi.cs

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Specialized;
34
using System.IO;
45
using System.Linq;
56
using System.Net.Http;
@@ -41,15 +42,25 @@ public StorageFileApi(string url, Dictionary<string, string>? headers = null, st
4142
/// </summary>
4243
/// <param name="path"></param>
4344
/// <param name="transformOptions"></param>
45+
/// <param name="downloadOptions"></param>
4446
/// <returns></returns>
45-
public string GetPublicUrl(string path, TransformOptions? transformOptions)
47+
public string GetPublicUrl(string path, TransformOptions? transformOptions, DownloadOptions? downloadOptions = null)
4648
{
49+
var queryParams = HttpUtility.ParseQueryString(string.Empty);
50+
51+
if (downloadOptions != null)
52+
queryParams.Add(downloadOptions.ToQueryCollection());
53+
4754
if (transformOptions == null)
48-
return $"{Url}/object/public/{GetFinalPath(path)}";
55+
{
56+
var queryParamsString = queryParams.ToString();
57+
return $"{Url}/object/public/{GetFinalPath(path)}?{queryParamsString}";
58+
}
4959

60+
queryParams.Add(transformOptions.ToQueryCollection());
5061
var builder = new UriBuilder($"{Url}/render/image/public/{GetFinalPath(path)}")
5162
{
52-
Query = transformOptions.ToQueryCollection().ToString()
63+
Query = queryParams.ToString()
5364
};
5465

5566
return builder.ToString();
@@ -61,8 +72,9 @@ public string GetPublicUrl(string path, TransformOptions? transformOptions)
6172
/// <param name="path">The file path to be downloaded, including the current file name. For example `folder/image.png`.</param>
6273
/// <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>
6374
/// <param name="transformOptions"></param>
75+
/// <param name="downloadOptions"></param>
6476
/// <returns></returns>
65-
public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null)
77+
public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? downloadOptions = null)
6678
{
6779
var body = new Dictionary<string, object?> { { "expiresIn", expiresIn } };
6880
var url = $"{Url}/object/sign/{GetFinalPath(path)}";
@@ -79,22 +91,26 @@ public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformO
7991
if (response == null || string.IsNullOrEmpty(response.SignedUrl))
8092
throw new SupabaseStorageException(
8193
$"Signed Url for {path} returned empty, do you have permission?");
94+
95+
var downloadQueryParams = downloadOptions?.ToQueryCollection().ToString();
8296

83-
return $"{Url}{response?.SignedUrl}";
97+
return $"{Url}{response.SignedUrl}?{downloadQueryParams}";
8498
}
8599

86100
/// <summary>
87101
/// Create signed URLs to download files without requiring permissions. These URLs can be valid for a set number of seconds.
88102
/// </summary>
89103
/// <param name="paths">paths The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png'].</param>
90104
/// <param name="expiresIn">The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.</param>
105+
/// <param name="downloadOptions"></param>
91106
/// <returns></returns>
92-
public async Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn)
107+
public async Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn, DownloadOptions? downloadOptions = null)
93108
{
94109
var body = new Dictionary<string, object> { { "expiresIn", expiresIn }, { "paths", paths } };
95110
var response = await Helpers.MakeRequest<List<CreateSignedUrlsResponse>>(HttpMethod.Post,
96111
$"{Url}/object/sign/{BucketId}", body, Headers);
97112

113+
var downloadQueryParams = downloadOptions?.ToQueryCollection().ToString();
98114
if (response != null)
99115
{
100116
foreach (var item in response)
@@ -103,7 +119,7 @@ public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformO
103119
throw new SupabaseStorageException(
104120
$"Signed Url for {item.Path} returned empty, do you have permission?");
105121

106-
item.SignedUrl = $"{Url}{item.SignedUrl}";
122+
item.SignedUrl = $"{Url}{item.SignedUrl}?{downloadQueryParams}";
107123
}
108124
}
109125

Diff for: StorageTests/StorageFileTests.cs

+40-2
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ public async Task GetPublicLink()
166166

167167
Assert.IsNotNull(url);
168168
}
169+
170+
[TestMethod("File: Get Public Link with download options")]
171+
public async Task GetPublicLinkWithDownloadOptions()
172+
{
173+
var name = $"{Guid.NewGuid()}.bin";
174+
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);
175+
var url = _bucket.GetPublicUrl(name, null, new DownloadOptions { FileName = "custom-file.png"});
176+
await _bucket.Remove(new List<string> { name });
177+
178+
Assert.IsNotNull(url);
179+
StringAssert.Contains(url, "download=custom-file.png");
180+
}
181+
182+
[TestMethod("File: Get Public Link with download and transform options")]
183+
public async Task GetPublicLinkWithDownloadAndTransformOptions()
184+
{
185+
var name = $"{Guid.NewGuid()}.bin";
186+
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);
187+
var url = _bucket.GetPublicUrl(name, new TransformOptions { Height = 100, Width = 100}, DownloadOptions.UseOriginalFileName);
188+
await _bucket.Remove(new List<string> { name });
189+
190+
Assert.IsNotNull(url);
191+
StringAssert.Contains(url, "download=true");
192+
}
169193

170194
[TestMethod("File: Get Signed Link")]
171195
public async Task GetSignedLink()
@@ -190,6 +214,19 @@ public async Task GetSignedLinkWithTransformOptions()
190214

191215
await _bucket.Remove(new List<string> { name });
192216
}
217+
218+
[TestMethod("File: Get Signed Link with download options")]
219+
public async Task GetSignedLinkWithDownloadOptions()
220+
{
221+
var name = $"{Guid.NewGuid()}.bin";
222+
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);
223+
224+
var url = await _bucket.CreateSignedUrl(name, 3600, null, new DownloadOptions { FileName = "custom-file.png"});
225+
Assert.IsTrue(Uri.IsWellFormedUriString(url, UriKind.Absolute));
226+
StringAssert.Contains(url, "download=custom-file.png");
227+
228+
await _bucket.Remove(new List<string> { name });
229+
}
193230

194231
[TestMethod("File: Get Multiple Signed Links")]
195232
public async Task GetMultipleSignedLinks()
@@ -200,13 +237,14 @@ public async Task GetMultipleSignedLinks()
200237
var name2 = $"{Guid.NewGuid()}.bin";
201238
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name2);
202239

203-
var urls = await _bucket.CreateSignedUrls(new List<string> { name1, name2 }, 3600);
240+
var urls = await _bucket.CreateSignedUrls(new List<string> { name1, name2 }, 3600, DownloadOptions.UseOriginalFileName);
204241

205242
Assert.IsNotNull(urls);
206243

207244
foreach (var response in urls)
208245
{
209-
Assert.IsTrue(Uri.IsWellFormedUriString(response.SignedUrl, UriKind.Absolute));
246+
Assert.IsTrue(Uri.IsWellFormedUriString($"{response.SignedUrl}", UriKind.Absolute));
247+
StringAssert.Contains(response.SignedUrl, "download=true");
210248
}
211249

212250
await _bucket.Remove(new List<string> { name1 });

0 commit comments

Comments
 (0)