diff --git a/Storage/DownloadOptions.cs b/Storage/DownloadOptions.cs new file mode 100644 index 0000000..44f7c00 --- /dev/null +++ b/Storage/DownloadOptions.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Supabase.Storage +{ + public class DownloadOptions + { + /// + ///

Use the original file name when downloading

+ ///
+ public static readonly DownloadOptions UseOriginalFileName = new DownloadOptions { FileName = "" }; + + /// + ///

The name of the file to be downloaded

+ ///

When field is null, no download attribute will be added.

+ ///

When field is empty, the original file name will be used. Use for quick initialized with original file names.

+ ///
+ public string? FileName { get; set; } + } +} \ No newline at end of file diff --git a/Storage/Extensions/DownloadOptionsExtension.cs b/Storage/Extensions/DownloadOptionsExtension.cs new file mode 100644 index 0000000..6911924 --- /dev/null +++ b/Storage/Extensions/DownloadOptionsExtension.cs @@ -0,0 +1,27 @@ +using System.Collections.Specialized; +using System.Web; + +namespace Supabase.Storage.Extensions +{ + public static class DownloadOptionsExtension + { + /// + /// Transforms options into a NameValueCollection to be used with a + /// + /// + /// + 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; + } + } +} \ No newline at end of file diff --git a/Storage/Interfaces/IStorageFileApi.cs b/Storage/Interfaces/IStorageFileApi.cs index 3aaaecc..13eabdf 100644 --- a/Storage/Interfaces/IStorageFileApi.cs +++ b/Storage/Interfaces/IStorageFileApi.cs @@ -8,15 +8,15 @@ public interface IStorageFileApi where TFileObject : FileObject { ClientOptions Options { get; } - Task CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null); - Task?> CreateSignedUrls(List paths, int expiresIn); + Task CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? options = null); + Task?> CreateSignedUrls(List paths, int expiresIn, DownloadOptions? options = null); Task Download(string supabasePath, EventHandler? onProgress = null); Task Download(string supabasePath, TransformOptions? transformOptions = null, EventHandler? onProgress = null); Task Download(string supabasePath, string localPath, EventHandler? onProgress = null); Task Download(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler? onProgress = null); Task DownloadPublicFile(string supabasePath, TransformOptions? transformOptions = null, EventHandler? onProgress = null); Task DownloadPublicFile(string supabasePath, string localPath, TransformOptions? transformOptions = null, EventHandler? onProgress = null); - string GetPublicUrl(string path, TransformOptions? transformOptions = null); + string GetPublicUrl(string path, TransformOptions? transformOptions = null, DownloadOptions? options = null); Task?> List(string path = "", SearchOptions? options = null); Task Move(string fromPath, string toPath, DestinationOptions? options = null); Task Copy(string fromPath, string toPath, DestinationOptions? options = null); diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs index f14320f..f234741 100644 --- a/Storage/StorageFileApi.cs +++ b/Storage/StorageFileApi.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Net.Http; @@ -41,15 +42,25 @@ public StorageFileApi(string url, Dictionary? headers = null, st /// /// /// + /// /// - 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(); @@ -61,8 +72,9 @@ public string GetPublicUrl(string path, TransformOptions? transformOptions) /// The file path to be downloaded, including the current file name. For example `folder/image.png`. /// The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute. /// + /// /// - public async Task CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null) + public async Task CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? downloadOptions = null) { var body = new Dictionary { { "expiresIn", expiresIn } }; var url = $"{Url}/object/sign/{GetFinalPath(path)}"; @@ -79,8 +91,10 @@ public async Task 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}"; } /// @@ -88,13 +102,15 @@ public async Task CreateSignedUrl(string path, int expiresIn, TransformO /// /// paths The file paths to be downloaded, including the current file names. For example [`folder/image.png`, 'folder2/image2.png']. /// The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute. + /// /// - public async Task?> CreateSignedUrls(List paths, int expiresIn) + public async Task?> CreateSignedUrls(List paths, int expiresIn, DownloadOptions? downloadOptions = null) { var body = new Dictionary { { "expiresIn", expiresIn }, { "paths", paths } }; var response = await Helpers.MakeRequest>(HttpMethod.Post, $"{Url}/object/sign/{BucketId}", body, Headers); + var downloadQueryParams = downloadOptions?.ToQueryCollection().ToString(); if (response != null) { foreach (var item in response) @@ -103,7 +119,7 @@ public async Task 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}"; } } diff --git a/StorageTests/StorageFileTests.cs b/StorageTests/StorageFileTests.cs index e56e16f..a313c94 100644 --- a/StorageTests/StorageFileTests.cs +++ b/StorageTests/StorageFileTests.cs @@ -208,6 +208,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 { 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 { name }); + + Assert.IsNotNull(url); + StringAssert.Contains(url, "download=true"); + } [TestMethod("File: Get Signed Link")] public async Task GetSignedLink() @@ -232,6 +256,19 @@ public async Task GetSignedLinkWithTransformOptions() await _bucket.Remove(new List { 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 { name }); + } [TestMethod("File: Get Multiple Signed Links")] public async Task GetMultipleSignedLinks() @@ -242,13 +279,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 { name1, name2 }, 3600); + var urls = await _bucket.CreateSignedUrls(new List { 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 { name1 });