Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for download options #20

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions Storage/DownloadOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;

namespace Supabase.Storage
{
public class DownloadOptions
{
/// <summary>
/// <p>Use the original file name when downloading</p>
/// </summary>
public static readonly DownloadOptions UseOriginalFileName = new DownloadOptions { FileName = "" };

/// <summary>
/// <p>The name of the file to be downloaded</p>
/// <p>When field is null, no download attribute will be added.</p>
/// <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; }
}
}
27 changes: 27 additions & 0 deletions Storage/Extensions/DownloadOptionsExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Specialized;
using System.Web;

namespace Supabase.Storage.Extensions
{
public static class DownloadOptionsExtension
{
/// <summary>
/// Transforms options into a NameValueCollection to be used with a <see cref="UriBuilder"/>
/// </summary>
/// <param name="download"></param>
/// <returns></returns>
public static NameValueCollection ToQueryCollection(this DownloadOptions download)
{
var query = HttpUtility.ParseQueryString(string.Empty);

if (download.FileName == null)
{
return query;
}

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

return query;
}
}
}
6 changes: 3 additions & 3 deletions Storage/Interfaces/IStorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ public interface IStorageFileApi<TFileObject>
where TFileObject : FileObject
{
ClientOptions Options { get; }
Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null);
Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn);
Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? options = null);
Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn, DownloadOptions? options = null);
Task<byte[]> Download(string supabasePath, EventHandler<float>? onProgress = null);
Task<byte[]> Download(string supabasePath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
Task<string> Download(string supabasePath, string localPath, EventHandler<float>? onProgress = null);
Task<string> Download(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
Task<byte[]> DownloadPublicFile(string supabasePath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
Task<string> DownloadPublicFile(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler<float>? onProgress = null);
string GetPublicUrl(string path, TransformOptions? transformOptions = null);
string GetPublicUrl(string path, TransformOptions? transformOptions = null, DownloadOptions? options = null);
Task<List<TFileObject>?> List(string path = "", SearchOptions? options = null);
Task<bool> Move(string fromPath, string toPath);
Task<TFileObject?> Remove(string path);
Expand Down
30 changes: 23 additions & 7 deletions Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -41,15 +42,25 @@ public StorageFileApi(string url, Dictionary<string, string>? headers = null, st
/// </summary>
/// <param name="path"></param>
/// <param name="transformOptions"></param>
/// <param name="downloadOptions"></param>
/// <returns></returns>
public string GetPublicUrl(string path, TransformOptions? transformOptions)
public string GetPublicUrl(string path, TransformOptions? transformOptions, DownloadOptions? downloadOptions = null)
{
var queryParams = HttpUtility.ParseQueryString(string.Empty);

if (downloadOptions != null)
queryParams.Add(downloadOptions.ToQueryCollection());

if (transformOptions == null)
return $"{Url}/object/public/{GetFinalPath(path)}";
{
var queryParamsString = queryParams.ToString();
return $"{Url}/object/public/{GetFinalPath(path)}?{queryParamsString}";
}

queryParams.Add(transformOptions.ToQueryCollection());
var builder = new UriBuilder($"{Url}/render/image/public/{GetFinalPath(path)}")
{
Query = transformOptions.ToQueryCollection().ToString()
Query = queryParams.ToString()
};

return builder.ToString();
Expand All @@ -61,8 +72,9 @@ public string GetPublicUrl(string path, TransformOptions? transformOptions)
/// <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>
/// <returns></returns>
public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null)
public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? downloadOptions = null)
{
var body = new Dictionary<string, object?> { { "expiresIn", expiresIn } };
var url = $"{Url}/object/sign/{GetFinalPath(path)}";
Expand All @@ -79,22 +91,26 @@ public async Task<string> CreateSignedUrl(string path, int expiresIn, TransformO
if (response == null || string.IsNullOrEmpty(response.SignedUrl))
throw new SupabaseStorageException(
$"Signed Url for {path} returned empty, do you have permission?");

var downloadQueryParams = downloadOptions?.ToQueryCollection().ToString();

return $"{Url}{response?.SignedUrl}";
return $"{Url}{response.SignedUrl}?{downloadQueryParams}";
}

/// <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="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>
public async Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn)
public async Task<List<CreateSignedUrlsResponse>?> CreateSignedUrls(List<string> paths, int expiresIn, DownloadOptions? downloadOptions = null)
{
var body = new Dictionary<string, object> { { "expiresIn", expiresIn }, { "paths", paths } };
var response = await Helpers.MakeRequest<List<CreateSignedUrlsResponse>>(HttpMethod.Post,
$"{Url}/object/sign/{BucketId}", body, Headers);

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

item.SignedUrl = $"{Url}{item.SignedUrl}";
item.SignedUrl = $"{Url}{item.SignedUrl}?{downloadQueryParams}";
}
}

Expand Down
42 changes: 40 additions & 2 deletions StorageTests/StorageFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,30 @@ public async Task GetPublicLink()

Assert.IsNotNull(url);
}

[TestMethod("File: Get Public Link with download options")]
public async Task GetPublicLinkWithDownloadOptions()
{
var name = $"{Guid.NewGuid()}.bin";
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);
var url = _bucket.GetPublicUrl(name, null, new DownloadOptions { FileName = "custom-file.png"});
await _bucket.Remove(new List<string> { name });

Assert.IsNotNull(url);
StringAssert.Contains(url, "download=custom-file.png");
}

[TestMethod("File: Get Public Link with download and transform options")]
public async Task GetPublicLinkWithDownloadAndTransformOptions()
{
var name = $"{Guid.NewGuid()}.bin";
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);
var url = _bucket.GetPublicUrl(name, new TransformOptions { Height = 100, Width = 100}, DownloadOptions.UseOriginalFileName);
await _bucket.Remove(new List<string> { name });

Assert.IsNotNull(url);
StringAssert.Contains(url, "download=true");
}

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

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

[TestMethod("File: Get Signed Link with download options")]
public async Task GetSignedLinkWithDownloadOptions()
{
var name = $"{Guid.NewGuid()}.bin";
await _bucket.Upload(new Byte[] { 0x0, 0x1 }, name);

var url = await _bucket.CreateSignedUrl(name, 3600, null, new DownloadOptions { FileName = "custom-file.png"});
Assert.IsTrue(Uri.IsWellFormedUriString(url, UriKind.Absolute));
StringAssert.Contains(url, "download=custom-file.png");

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

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

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

Assert.IsNotNull(urls);

foreach (var response in urls)
{
Assert.IsTrue(Uri.IsWellFormedUriString(response.SignedUrl, UriKind.Absolute));
Assert.IsTrue(Uri.IsWellFormedUriString($"{response.SignedUrl}", UriKind.Absolute));
StringAssert.Contains(response.SignedUrl, "download=true");
}

await _bucket.Remove(new List<string> { name1 });
Expand Down
Loading