diff --git a/Storage/Extensions/HttpClientProgress.cs b/Storage/Extensions/HttpClientProgress.cs
index 6410bbc..15d0061 100644
--- a/Storage/Extensions/HttpClientProgress.cs
+++ b/Storage/Extensions/HttpClientProgress.cs
@@ -4,139 +4,312 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using BirdMessenger;
+using BirdMessenger.Collections;
using Newtonsoft.Json;
using Supabase.Storage.Exceptions;
namespace Supabase.Storage.Extensions
{
- ///
- /// Adapted from: https://gist.github.com/dalexsoto/9fd3c5bdbe9f61a717d47c5843384d11
- ///
- internal static class HttpClientProgress
- {
- public static async Task DownloadDataAsync(this HttpClient client, Uri uri, Dictionary? headers = null, IProgress? progress = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- var destination = new MemoryStream();
- var message = new HttpRequestMessage(HttpMethod.Get, uri);
-
- if (headers != null)
- {
- foreach (var header in headers)
- {
- message.Headers.Add(header.Key, header.Value);
- }
- }
-
- using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseHeadersRead))
- {
- if (!response.IsSuccessStatusCode)
- {
- var content = await response.Content.ReadAsStringAsync();
- var errorResponse = JsonConvert.DeserializeObject(content);
- var e = new SupabaseStorageException(errorResponse?.Message ?? content)
- {
- Content = content,
- Response = response,
- StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode
- };
-
- e.AddReason();
- throw e;
- }
-
- var contentLength = response.Content.Headers.ContentLength;
- using (var download = await response.Content.ReadAsStreamAsync())
- {
- // no progress... no contentLength... very sad
- if (progress is null || !contentLength.HasValue)
- {
- await download.CopyToAsync(destination);
- return destination;
- }
-
- // Such progress and contentLength much reporting Wow!
- var progressWrapper = new Progress(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
- await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
- }
- }
-
- float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
-
- return destination;
- }
-
- static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress? progress = null, CancellationToken cancellationToken = default(CancellationToken))
- {
- if (bufferSize < 0)
- throw new ArgumentOutOfRangeException(nameof(bufferSize));
- if (source is null)
- throw new ArgumentNullException(nameof(source));
- if (!source.CanRead)
- throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
- if (destination == null)
- throw new ArgumentNullException(nameof(destination));
- if (!destination.CanWrite)
- throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
-
- var buffer = new byte[bufferSize];
- long totalBytesRead = 0;
- int bytesRead;
-
- while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
- {
- await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
- totalBytesRead += bytesRead;
- progress?.Report(totalBytesRead);
- }
- }
-
- public static Task UploadFileAsync(this HttpClient client, Uri uri, string filePath, Dictionary? headers = null, Progress? progress = null)
- {
- var fileStream = new FileStream(filePath, mode: FileMode.Open, FileAccess.Read);
- return UploadAsync(client, uri, fileStream, headers, progress);
- }
-
- public static Task UploadBytesAsync(this HttpClient client, Uri uri, byte[] data, Dictionary? headers = null, Progress? progress = null)
- {
- var stream = new MemoryStream(data);
- return UploadAsync(client, uri, stream, headers, progress);
- }
-
- public static async Task UploadAsync(this HttpClient client, Uri uri, Stream stream, Dictionary? headers = null, Progress? progress = null)
- {
- var content = new ProgressableStreamContent(stream, 4096, progress);
-
- if (headers != null)
- {
- client.DefaultRequestHeaders.Clear();
-
- foreach (var header in headers)
- {
- if (header.Key.Contains("content"))
- content.Headers.Add(header.Key, header.Value);
- else
- client.DefaultRequestHeaders.Add(header.Key, header.Value);
- }
- }
-
- var response = await client.PostAsync(uri, content);
-
- if (!response.IsSuccessStatusCode)
- {
- var httpContent = await response.Content.ReadAsStringAsync();
- var errorResponse = JsonConvert.DeserializeObject(httpContent);
- var e = new SupabaseStorageException(errorResponse?.Message ?? httpContent)
- {
- Content = httpContent,
- Response = response,
- StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode
- };
-
- e.AddReason();
- throw e;
- }
-
- return response;
- }
- }
+ ///
+ /// Adapted from: https://gist.github.com/dalexsoto/9fd3c5bdbe9f61a717d47c5843384d11
+ ///
+ internal static class HttpClientProgress
+ {
+ public static async Task DownloadDataAsync(
+ this HttpClient client,
+ Uri uri,
+ Dictionary? headers = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default(CancellationToken)
+ )
+ {
+ var destination = new MemoryStream();
+ var message = new HttpRequestMessage(HttpMethod.Get, uri);
+
+ if (headers != null)
+ {
+ foreach (var header in headers)
+ {
+ message.Headers.Add(header.Key, header.Value);
+ }
+ }
+
+ using (
+ var response = await client.SendAsync(
+ message,
+ HttpCompletionOption.ResponseHeadersRead
+ )
+ )
+ {
+ if (!response.IsSuccessStatusCode)
+ {
+ var content = await response.Content.ReadAsStringAsync();
+ var errorResponse = JsonConvert.DeserializeObject(content);
+ var e = new SupabaseStorageException(errorResponse?.Message ?? content)
+ {
+ Content = content,
+ Response = response,
+ StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode,
+ };
+
+ e.AddReason();
+ throw e;
+ }
+
+ var contentLength = response.Content.Headers.ContentLength;
+ using (var download = await response.Content.ReadAsStreamAsync())
+ {
+ // no progress... no contentLength... very sad
+ if (progress is null || !contentLength.HasValue)
+ {
+ await download.CopyToAsync(destination);
+ return destination;
+ }
+
+ // Such progress and contentLength much reporting Wow!
+ var progressWrapper = new Progress(totalBytes =>
+ progress.Report(GetProgressPercentage(totalBytes, contentLength.Value))
+ );
+ await download.CopyToAsync(
+ destination,
+ 81920,
+ progressWrapper,
+ cancellationToken
+ );
+ }
+ }
+
+ float GetProgressPercentage(float totalBytes, float currentBytes) =>
+ (totalBytes / currentBytes) * 100f;
+
+ return destination;
+ }
+
+ static async Task CopyToAsync(
+ this Stream source,
+ Stream destination,
+ int bufferSize,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default(CancellationToken)
+ )
+ {
+ if (bufferSize < 0)
+ throw new ArgumentOutOfRangeException(nameof(bufferSize));
+ if (source is null)
+ throw new ArgumentNullException(nameof(source));
+ if (!source.CanRead)
+ throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
+ if (destination == null)
+ throw new ArgumentNullException(nameof(destination));
+ if (!destination.CanWrite)
+ throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
+
+ var buffer = new byte[bufferSize];
+ long totalBytesRead = 0;
+ int bytesRead;
+
+ while (
+ (
+ bytesRead = await source
+ .ReadAsync(buffer, 0, buffer.Length, cancellationToken)
+ .ConfigureAwait(false)
+ ) != 0
+ )
+ {
+ await destination
+ .WriteAsync(buffer, 0, bytesRead, cancellationToken)
+ .ConfigureAwait(false);
+ totalBytesRead += bytesRead;
+ progress?.Report(totalBytesRead);
+ }
+ }
+
+ public static Task UploadFileAsync(
+ this HttpClient client,
+ Uri uri,
+ string filePath,
+ Dictionary? headers = null,
+ Progress? progress = null
+ )
+ {
+ var fileStream = new FileStream(filePath, mode: FileMode.Open, FileAccess.Read);
+ return UploadAsync(client, uri, fileStream, headers, progress);
+ }
+
+ public static Task UploadBytesAsync(
+ this HttpClient client,
+ Uri uri,
+ byte[] data,
+ Dictionary? headers = null,
+ Progress? progress = null
+ )
+ {
+ var stream = new MemoryStream(data);
+ return UploadAsync(client, uri, stream, headers, progress);
+ }
+
+ public static async Task UploadAsync(
+ this HttpClient client,
+ Uri uri,
+ Stream stream,
+ Dictionary? headers = null,
+ Progress? progress = null
+ )
+ {
+ var content = new ProgressableStreamContent(stream, 4096, progress);
+
+ if (headers != null)
+ {
+ client.DefaultRequestHeaders.Clear();
+
+ foreach (var header in headers)
+ {
+ if (header.Key.Contains("content"))
+ content.Headers.Add(header.Key, header.Value);
+ else
+ client.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
+
+ var response = await client.PostAsync(uri, content);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var httpContent = await response.Content.ReadAsStringAsync();
+ var errorResponse = JsonConvert.DeserializeObject(httpContent);
+ var e = new SupabaseStorageException(errorResponse?.Message ?? httpContent)
+ {
+ Content = httpContent,
+ Response = response,
+ StatusCode = errorResponse?.StatusCode ?? (int)response.StatusCode,
+ };
+
+ e.AddReason();
+ throw e;
+ }
+
+ return response;
+ }
+
+ public static Task UploadOrContinueFileAsync(
+ this HttpClient client,
+ Uri uri,
+ string filePath,
+ Dictionary? headers = null,
+ MetadataCollection? metadata = null,
+ Progress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var fileStream = new FileStream(filePath, mode: FileMode.Open, FileAccess.Read);
+ return ResumableUploadAsync(
+ client,
+ uri,
+ fileStream,
+ headers,
+ metadata,
+ progress,
+ cancellationToken
+ );
+ }
+
+ public static Task UploadOrContinueByteAsync(
+ this HttpClient client,
+ Uri uri,
+ byte[] data,
+ Dictionary? headers = null,
+ MetadataCollection? metadata = null,
+ Progress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var stream = new MemoryStream(data);
+ return ResumableUploadAsync(
+ client,
+ uri,
+ stream,
+ headers,
+ metadata,
+ progress,
+ cancellationToken
+ );
+ }
+
+ private static async Task ResumableUploadAsync(
+ this HttpClient client,
+ Uri uri,
+ Stream fileStream,
+ Dictionary? headers = null,
+ MetadataCollection? metadata = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (fileStream == null)
+ throw new ArgumentNullException(nameof(fileStream));
+
+ if (fileStream.Position != 0 && fileStream.CanSeek)
+ {
+ fileStream.Seek(0, SeekOrigin.Begin);
+ }
+
+ if (headers != null)
+ {
+ client.DefaultRequestHeaders.Clear();
+ foreach (var header in headers)
+ {
+ client.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ }
+
+ var createOption = new TusCreateRequestOption()
+ {
+ Endpoint = uri,
+ Metadata = metadata,
+ UploadLength = fileStream.Length,
+ };
+
+ var responseCreate = await client.TusCreateAsync(createOption, cancellationToken);
+
+ var patchOption = new TusPatchRequestOption
+ {
+ FileLocation = responseCreate.FileLocation,
+ Stream = fileStream,
+ UploadBufferSize = 6 * 1024 * 1024,
+ UploadType = UploadType.Chunk,
+ OnProgressAsync = x =>
+ {
+ if (progress == null)
+ return Task.CompletedTask;
+
+ var uploadedProgress = (float)x.UploadedSize / x.TotalSize * 100f;
+ progress.Report(uploadedProgress);
+
+ return Task.CompletedTask;
+ },
+ OnCompletedAsync = _ => Task.CompletedTask,
+ OnFailedAsync = _ => Task.CompletedTask,
+ };
+
+ var responsePatch = await client.TusPatchAsync(patchOption, cancellationToken);
+
+ if (responsePatch.OriginResponseMessage.IsSuccessStatusCode)
+ return responsePatch.OriginResponseMessage;
+
+ var httpContent = await responsePatch.OriginResponseMessage.Content.ReadAsStringAsync();
+ var errorResponse = JsonConvert.DeserializeObject(httpContent);
+ var e = new SupabaseStorageException(errorResponse?.Message ?? httpContent)
+ {
+ Content = httpContent,
+ Response = responsePatch.OriginResponseMessage,
+ StatusCode =
+ errorResponse?.StatusCode
+ ?? (int)responsePatch.OriginResponseMessage.StatusCode,
+ };
+
+ e.AddReason();
+ throw e;
+ }
+ }
}
diff --git a/Storage/Interfaces/IStorageFileApi.cs b/Storage/Interfaces/IStorageFileApi.cs
index 4af9dcc..c0a5d2a 100644
--- a/Storage/Interfaces/IStorageFileApi.cs
+++ b/Storage/Interfaces/IStorageFileApi.cs
@@ -1,34 +1,119 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
namespace Supabase.Storage.Interfaces
{
- public interface IStorageFileApi
- where TFileObject : FileObject
- {
- ClientOptions Options { get; }
- 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, DownloadOptions? options = null);
- Task?> List(string path = "", SearchOptions? options = null);
- Task Info(string path);
- Task Move(string fromPath, string toPath, DestinationOptions? options = null);
- Task Copy(string fromPath, string toPath, DestinationOptions? options = null);
+ public interface IStorageFileApi
+ where TFileObject : FileObject
+ {
+ ClientOptions Options { get; }
+ 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,
+ DownloadOptions? options = null
+ );
+ Task?> List(string path = "", SearchOptions? options = null);
+ Task Info(string path);
+ Task Move(string fromPath, string toPath, DestinationOptions? options = null);
+ Task Copy(string fromPath, string toPath, DestinationOptions? options = null);
Task Remove(string path);
- Task?> Remove(List paths);
- Task Update(byte[] data, string supabasePath, FileOptions? options = null, EventHandler? onProgress = null);
- Task Update(string localFilePath, string supabasePath, FileOptions? options = null, EventHandler? onProgress = null);
- Task Upload(byte[] data, string supabasePath, FileOptions? options = null, EventHandler? onProgress = null, bool inferContentType = true);
- Task Upload(string localFilePath, string supabasePath, FileOptions? options = null, EventHandler? onProgress = null, bool inferContentType = true);
- Task UploadToSignedUrl(byte[] data, UploadSignedUrl url, FileOptions? options = null, EventHandler? onProgress = null, bool inferContentType = true);
- Task UploadToSignedUrl(string localFilePath, UploadSignedUrl url, FileOptions? options = null, EventHandler? onProgress = null, bool inferContentType = true);
- Task CreateUploadSignedUrl(string supabasePath);
- }
-}
\ No newline at end of file
+ Task?> Remove(List paths);
+ Task Update(
+ byte[] data,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null
+ );
+ Task Update(
+ string localFilePath,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null
+ );
+ Task Upload(
+ byte[] data,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ );
+ Task Upload(
+ string localFilePath,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ );
+ Task UploadOrResume(
+ string localPath,
+ string fileName,
+ FileOptions options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ );
+ Task UploadOrResume(
+ byte[] data,
+ string fileName,
+ FileOptions options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ );
+ Task UploadToSignedUrl(
+ byte[] data,
+ UploadSignedUrl url,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ );
+ Task UploadToSignedUrl(
+ string localFilePath,
+ UploadSignedUrl url,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ );
+ Task CreateUploadSignedUrl(string supabasePath);
+ }
+}
+
diff --git a/Storage/Storage.csproj b/Storage/Storage.csproj
index ae6d100..712d3c3 100644
--- a/Storage/Storage.csproj
+++ b/Storage/Storage.csproj
@@ -40,6 +40,7 @@
+
diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs
index 2ab8091..758c46b 100644
--- a/Storage/StorageFileApi.cs
+++ b/Storage/StorageFileApi.cs
@@ -4,8 +4,10 @@
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Threading;
using System.Threading.Tasks;
using System.Web;
+using BirdMessenger.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Supabase.Storage.Exceptions;
@@ -22,13 +24,22 @@ public class StorageFileApi : IStorageFileApi
protected Dictionary Headers { get; set; }
protected string? BucketId { get; set; }
- public StorageFileApi(string url, string bucketId, ClientOptions? options,
- Dictionary? headers = null) : this(url, headers, bucketId)
+ public StorageFileApi(
+ string url,
+ string bucketId,
+ ClientOptions? options,
+ Dictionary? headers = null
+ )
+ : this(url, headers, bucketId)
{
Options = options ?? new ClientOptions();
}
- public StorageFileApi(string url, Dictionary? headers = null, string? bucketId = null)
+ public StorageFileApi(
+ string url,
+ Dictionary? headers = null,
+ string? bucketId = null
+ )
{
Url = url;
BucketId = bucketId;
@@ -44,11 +55,15 @@ public StorageFileApi(string url, Dictionary? headers = null, st
///
///
///
- public string GetPublicUrl(string path, TransformOptions? transformOptions, DownloadOptions? downloadOptions = null)
+ public string GetPublicUrl(
+ string path,
+ TransformOptions? transformOptions,
+ DownloadOptions? downloadOptions = null
+ )
{
var queryParams = HttpUtility.ParseQueryString(string.Empty);
-
- if (downloadOptions != null)
+
+ if (downloadOptions != null)
queryParams.Add(downloadOptions.ToQueryCollection());
if (transformOptions == null)
@@ -60,7 +75,7 @@ public string GetPublicUrl(string path, TransformOptions? transformOptions, Down
queryParams.Add(transformOptions.ToQueryCollection());
var builder = new UriBuilder($"{Url}/render/image/public/{GetFinalPath(path)}")
{
- Query = queryParams.ToString()
+ Query = queryParams.ToString(),
};
return builder.ToString();
@@ -74,24 +89,40 @@ public string GetPublicUrl(string path, TransformOptions? transformOptions, Down
///
///
///
- public async Task CreateSignedUrl(string path, int expiresIn, TransformOptions? transformOptions = null, DownloadOptions? downloadOptions = 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)}";
if (transformOptions != null)
{
- var transformOptionsJson = JsonConvert.SerializeObject(transformOptions, new StringEnumConverter());
- var transformOptionsObj = JsonConvert.DeserializeObject>(transformOptionsJson);
+ var transformOptionsJson = JsonConvert.SerializeObject(
+ transformOptions,
+ new StringEnumConverter()
+ );
+ var transformOptionsObj = JsonConvert.DeserializeObject>(
+ transformOptionsJson
+ );
body.Add("transform", transformOptionsObj);
}
- var response = await Helpers.MakeRequest(HttpMethod.Post, url, body, Headers);
+ var response = await Helpers.MakeRequest(
+ HttpMethod.Post,
+ url,
+ body,
+ Headers
+ );
if (response == null || string.IsNullOrEmpty(response.SignedUrl))
throw new SupabaseStorageException(
- $"Signed Url for {path} returned empty, do you have permission?");
-
+ $"Signed Url for {path} returned empty, do you have permission?"
+ );
+
var downloadQueryParams = downloadOptions?.ToQueryCollection().ToString();
return $"{Url}{response.SignedUrl}?{downloadQueryParams}";
@@ -104,11 +135,23 @@ public async Task CreateSignedUrl(string path, int expiresIn, TransformO
/// 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, DownloadOptions? downloadOptions = null)
+ 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 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)
@@ -117,7 +160,8 @@ public async Task CreateSignedUrl(string path, int expiresIn, TransformO
{
if (string.IsNullOrEmpty(item.SignedUrl))
throw new SupabaseStorageException(
- $"Signed Url for {item.Path} returned empty, do you have permission?");
+ $"Signed Url for {item.Path} returned empty, do you have permission?"
+ );
item.SignedUrl = $"{Url}{item.SignedUrl}?{downloadQueryParams}";
}
@@ -142,13 +186,16 @@ public async Task CreateSignedUrl(string path, int expiresIn, TransformO
if (body != null)
body.Add("prefix", string.IsNullOrEmpty(path) ? "" : path);
- var response =
- await Helpers.MakeRequest>(HttpMethod.Post, $"{Url}/object/list/{BucketId}", body,
- Headers);
+ var response = await Helpers.MakeRequest>(
+ HttpMethod.Post,
+ $"{Url}/object/list/{BucketId}",
+ body,
+ Headers
+ );
return response;
}
-
+
///
/// Retrieves the details of an existing file.
///
@@ -156,8 +203,12 @@ await Helpers.MakeRequest>(HttpMethod.Post, $"{Url}/object/list
///
public async Task Info(string path)
{
- var response =
- await Helpers.MakeRequest(HttpMethod.Get, $"{Url}/object/info/{BucketId}/{path}", null, Headers);
+ var response = await Helpers.MakeRequest(
+ HttpMethod.Get,
+ $"{Url}/object/info/{BucketId}/{path}",
+ null,
+ Headers
+ );
return response;
}
@@ -171,8 +222,13 @@ await Helpers.MakeRequest>(HttpMethod.Post, $"{Url}/object/list
///
///
///
- public async Task Upload(string localFilePath, string supabasePath, FileOptions? options = null,
- EventHandler? onProgress = null, bool inferContentType = true)
+ public async Task Upload(
+ string localFilePath,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ )
{
options ??= new FileOptions();
@@ -192,8 +248,13 @@ public async Task Upload(string localFilePath, string supabasePath, File
///
///
///
- public async Task Upload(byte[] data, string supabasePath, FileOptions? options = null,
- EventHandler? onProgress = null, bool inferContentType = true)
+ public async Task Upload(
+ byte[] data,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ )
{
options ??= new FileOptions();
@@ -213,8 +274,13 @@ public async Task Upload(byte[] data, string supabasePath, FileOptions?
///
///
///
- public async Task UploadToSignedUrl(string localFilePath, UploadSignedUrl signedUrl,
- FileOptions? options = null, EventHandler? onProgress = null, bool inferContentType = true)
+ public async Task UploadToSignedUrl(
+ string localFilePath,
+ UploadSignedUrl signedUrl,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ )
{
options ??= new FileOptions();
@@ -225,7 +291,7 @@ public async Task UploadToSignedUrl(string localFilePath, UploadSignedUr
{
["Authorization"] = $"Bearer {signedUrl.Token}",
["cache-control"] = $"max-age={options.CacheControl}",
- ["content-type"] = options.ContentType
+ ["content-type"] = options.ContentType,
};
if (options.Upsert)
@@ -236,7 +302,12 @@ public async Task UploadToSignedUrl(string localFilePath, UploadSignedUr
if (onProgress != null)
progress.ProgressChanged += onProgress;
- await Helpers.HttpUploadClient!.UploadFileAsync(signedUrl.SignedUrl, localFilePath, headers, progress);
+ await Helpers.HttpUploadClient!.UploadFileAsync(
+ signedUrl.SignedUrl,
+ localFilePath,
+ headers,
+ progress
+ );
return GetFinalPath(signedUrl.Key);
}
@@ -250,8 +321,13 @@ public async Task UploadToSignedUrl(string localFilePath, UploadSignedUr
///
///
///
- public async Task UploadToSignedUrl(byte[] data, UploadSignedUrl signedUrl, FileOptions? options = null,
- EventHandler? onProgress = null, bool inferContentType = true)
+ public async Task UploadToSignedUrl(
+ byte[] data,
+ UploadSignedUrl signedUrl,
+ FileOptions? options = null,
+ EventHandler? onProgress = null,
+ bool inferContentType = true
+ )
{
options ??= new FileOptions();
@@ -262,7 +338,7 @@ public async Task UploadToSignedUrl(byte[] data, UploadSignedUrl signedU
{
["Authorization"] = $"Bearer {signedUrl.Token}",
["cache-control"] = $"max-age={options.CacheControl}",
- ["content-type"] = options.ContentType
+ ["content-type"] = options.ContentType,
};
if (options.Upsert)
@@ -273,12 +349,16 @@ public async Task UploadToSignedUrl(byte[] data, UploadSignedUrl signedU
if (onProgress != null)
progress.ProgressChanged += onProgress;
- await Helpers.HttpUploadClient!.UploadBytesAsync(signedUrl.SignedUrl, data, headers, progress);
+ await Helpers.HttpUploadClient!.UploadBytesAsync(
+ signedUrl.SignedUrl,
+ data,
+ headers,
+ progress
+ );
return GetFinalPath(signedUrl.Key);
}
-
///
/// Replaces an existing file at the specified path with a new one.
///
@@ -287,8 +367,12 @@ public async Task UploadToSignedUrl(byte[] data, UploadSignedUrl signedU
/// HTTP headers.
///
///
- public Task Update(string localFilePath, string supabasePath, FileOptions? options = null,
- EventHandler? onProgress = null)
+ public Task Update(
+ string localFilePath,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null
+ )
{
options ??= new FileOptions();
return UploadOrUpdate(localFilePath, supabasePath, options, onProgress);
@@ -302,13 +386,66 @@ public Task Update(string localFilePath, string supabasePath, FileOption
/// HTTP headers.
///
///
- public Task Update(byte[] data, string supabasePath, FileOptions? options = null,
- EventHandler? onProgress = null)
+ public Task Update(
+ byte[] data,
+ string supabasePath,
+ FileOptions? options = null,
+ EventHandler? onProgress = null
+ )
{
options ??= new FileOptions();
return UploadOrUpdate(data, supabasePath, options, onProgress);
}
+ ///
+ /// Attempts to upload a file to Supabase storage. If the upload process is interrupted or incomplete, it will attempt to resume the upload.
+ ///
+ /// The local file path of the file to be uploaded.
+ /// The destination path in Supabase Storage where the file will be stored.
+ /// Optional file options to specify metadata or other upload configurations.
+ /// An optional event handler for tracking and reporting upload progress as a percentage.
+ /// Cancellation token to observe while waiting for the task to complete.
+ /// Returns a task that resolves to a string representing the URL or path of the uploaded file in the storage.
+ public Task UploadOrResume(
+ string localPath,
+ string fileName,
+ FileOptions? options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ options ??= new FileOptions();
+ return UploadOrContinue(
+ localPath,
+ fileName,
+ options,
+ onProgress,
+ cancellationToken
+ );
+ }
+
+ ///
+ /// Uploads a file to the specified path in Supabase storage or resumes an interrupted upload process.
+ /// Allows customization through provided file options and supports tracking upload progress via an event handler.
+ ///
+ /// The byte array containing the file data to upload.
+ /// The destination path within Supabase storage where the file should be stored.
+ /// Optional configuration settings for the upload process.
+ /// An optional event handler for monitoring the upload progress, reporting it as a percentage.
+ /// A cancellation token to observe while awaiting the task, allowing the operation to be canceled.
+ /// A task representing the asynchronous operation, resolving to the path of the uploaded file upon successful completion.
+ public Task UploadOrResume(
+ byte[] data,
+ string fileName,
+ FileOptions? options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ options ??= new FileOptions();
+ return UploadOrContinue(data, fileName, options, onProgress, cancellationToken);
+ }
+
///
/// Moves an existing file to a new location, optionally allowing renaming.
///
@@ -316,16 +453,25 @@ public Task Update(byte[] data, string supabasePath, FileOptions? option
/// The target file path, including the new file name (e.g., `folder/image-copy.png`).
/// Optional parameters for specifying the destination bucket and other settings.
/// Returns a boolean value indicating whether the operation was successful.
- public async Task Move(string fromPath, string toPath, DestinationOptions? options = null)
+ public async Task Move(
+ string fromPath,
+ string toPath,
+ DestinationOptions? options = null
+ )
{
var body = new Dictionary
{
{ "bucketId", BucketId },
{ "sourceKey", fromPath },
{ "destinationKey", toPath },
- { "destinationBucket", options?.DestinationBucket }
+ { "destinationBucket", options?.DestinationBucket },
};
- await Helpers.MakeRequest(HttpMethod.Post, $"{Url}/object/move", body, Headers);
+ await Helpers.MakeRequest(
+ HttpMethod.Post,
+ $"{Url}/object/move",
+ body,
+ Headers
+ );
return true;
}
@@ -336,17 +482,26 @@ public async Task Move(string fromPath, string toPath, DestinationOptions?
/// The destination path for the copied file/object.
/// Optional parameters such as the destination bucket.
/// True if the copy operation was successful.
- public async Task Copy(string fromPath, string toPath, DestinationOptions? options = null)
+ public async Task Copy(
+ string fromPath,
+ string toPath,
+ DestinationOptions? options = null
+ )
{
var body = new Dictionary
{
{ "bucketId", BucketId },
{ "sourceKey", fromPath },
{ "destinationKey", toPath },
- { "destinationBucket", options?.DestinationBucket }
+ { "destinationBucket", options?.DestinationBucket },
};
- await Helpers.MakeRequest(HttpMethod.Post, $"{Url}/object/copy", body, Headers);
+ await Helpers.MakeRequest(
+ HttpMethod.Post,
+ $"{Url}/object/copy",
+ body,
+ Headers
+ );
return true;
}
@@ -358,12 +513,17 @@ public async Task Copy(string fromPath, string toPath, DestinationOptions?
///
///
///
- public Task Download(string supabasePath, string localPath, TransformOptions? transformOptions = null,
- EventHandler? onProgress = null)
+ public Task Download(
+ string supabasePath,
+ string localPath,
+ TransformOptions? transformOptions = null,
+ EventHandler? onProgress = null
+ )
{
- var url = transformOptions != null
- ? $"{Url}/render/image/authenticated/{GetFinalPath(supabasePath)}"
- : $"{Url}/object/{GetFinalPath(supabasePath)}";
+ var url =
+ transformOptions != null
+ ? $"{Url}/render/image/authenticated/{GetFinalPath(supabasePath)}"
+ : $"{Url}/object/{GetFinalPath(supabasePath)}";
return DownloadFile(url, localPath, transformOptions, onProgress);
}
@@ -374,8 +534,11 @@ public Task Download(string supabasePath, string localPath, TransformOpt
///
///
///
- public Task Download(string supabasePath, string localPath, EventHandler? onProgress = null) =>
- Download(supabasePath, localPath, null, onProgress: onProgress);
+ public Task Download(
+ string supabasePath,
+ string localPath,
+ EventHandler? onProgress = null
+ ) => Download(supabasePath, localPath, null, onProgress: onProgress);
///
/// Downloads a byte array from a private bucket to be used programmatically. For public buckets
@@ -384,8 +547,11 @@ public Task Download(string supabasePath, string localPath, EventHandler
///
///
///
- public Task Download(string supabasePath, TransformOptions? transformOptions = null,
- EventHandler? onProgress = null)
+ public Task Download(
+ string supabasePath,
+ TransformOptions? transformOptions = null,
+ EventHandler? onProgress = null
+ )
{
var url = $"{Url}/object/{GetFinalPath(supabasePath)}";
return DownloadBytes(url, transformOptions, onProgress);
@@ -408,8 +574,12 @@ public Task Download(string supabasePath, EventHandler? onProgres
///
///
///
- public Task DownloadPublicFile(string supabasePath, string localPath,
- TransformOptions? transformOptions = null, EventHandler? onProgress = null)
+ public Task DownloadPublicFile(
+ string supabasePath,
+ string localPath,
+ TransformOptions? transformOptions = null,
+ EventHandler? onProgress = null
+ )
{
var url = GetPublicUrl(supabasePath, transformOptions);
return DownloadFile(url, localPath, transformOptions, onProgress);
@@ -422,8 +592,11 @@ public Task DownloadPublicFile(string supabasePath, string localPath,
///
///
///
- public Task DownloadPublicFile(string supabasePath, TransformOptions? transformOptions = null,
- EventHandler? onProgress = null)
+ public Task DownloadPublicFile(
+ string supabasePath,
+ TransformOptions? transformOptions = null,
+ EventHandler? onProgress = null
+ )
{
var url = GetPublicUrl(supabasePath, transformOptions);
return DownloadBytes(url, transformOptions, onProgress);
@@ -448,9 +621,12 @@ public Task DownloadPublicFile(string supabasePath, TransformOptions? tr
public async Task?> Remove(List paths)
{
var data = new Dictionary { { "prefixes", paths } };
- var response =
- await Helpers.MakeRequest>(HttpMethod.Delete, $"{Url}/object/{BucketId}", data,
- Headers);
+ var response = await Helpers.MakeRequest>(
+ HttpMethod.Delete,
+ $"{Url}/object/{BucketId}",
+ data,
+ Headers
+ );
return response;
}
@@ -465,12 +641,21 @@ public async Task CreateUploadSignedUrl(string supabasePath)
var path = GetFinalPath(supabasePath);
var url = $"{Url}/object/upload/sign/{path}";
- var response =
- await Helpers.MakeRequest(HttpMethod.Post, url, null, Headers);
-
- if (response == null || string.IsNullOrEmpty(response.Url) || !response.Url!.Contains("token"))
+ var response = await Helpers.MakeRequest(
+ HttpMethod.Post,
+ url,
+ null,
+ Headers
+ );
+
+ if (
+ response == null
+ || string.IsNullOrEmpty(response.Url)
+ || !response.Url!.Contains("token")
+ )
throw new SupabaseStorageException(
- "Response did not return with expected data. Does this token have proper permission to generate a url?");
+ "Response did not return with expected data. Does this token have proper permission to generate a url?"
+ );
var generatedUri = new Uri($"{Url}{response.Url}");
var query = HttpUtility.ParseQueryString(generatedUri.Query);
@@ -479,15 +664,19 @@ public async Task CreateUploadSignedUrl(string supabasePath)
return new UploadSignedUrl(generatedUri, token, supabasePath);
}
- private async Task UploadOrUpdate(string localPath, string supabasePath, FileOptions options,
- EventHandler? onProgress = null)
+ private async Task UploadOrUpdate(
+ string localPath,
+ string supabasePath,
+ FileOptions options,
+ EventHandler? onProgress = null
+ )
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");
var headers = new Dictionary(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
- { "content-type", options.ContentType }
+ { "content-type", options.ContentType },
};
if (options.Upsert)
@@ -495,12 +684,12 @@ private async Task UploadOrUpdate(string localPath, string supabasePath,
if (options.Metadata != null)
headers.Add("x-metadata", ParseMetadata(options.Metadata));
-
+
options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
-
+
if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
-
+
var progress = new Progress();
if (onProgress != null)
@@ -511,23 +700,123 @@ private async Task UploadOrUpdate(string localPath, string supabasePath,
return GetFinalPath(supabasePath);
}
+ private async Task UploadOrContinue(
+ string localPath,
+ string fileName,
+ FileOptions options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var uri = new Uri($"{Url}/upload/resumable");
+
+ var headers = new Dictionary(Headers)
+ {
+ { "cache-control", $"max-age={options.CacheControl}" },
+ };
+
+ var metadata = new MetadataCollection
+ {
+ ["bucketName"] = BucketId,
+ ["objectName"] = fileName,
+ ["contentType"] = options.ContentType,
+ };
+
+ if (options.Upsert)
+ headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+
+ if (options.Metadata != null)
+ headers.Add("x-metadata", ParseMetadata(options.Metadata));
+
+ options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
+
+ if (options.Duplex != null)
+ headers.Add("x-duplex", options.Duplex.ToLower());
+
+ var progress = new Progress();
+
+ if (onProgress != null)
+ progress.ProgressChanged += onProgress;
+
+ await Helpers.HttpUploadClient!.UploadOrContinueFileAsync(
+ uri,
+ localPath,
+ headers,
+ metadata,
+ progress,
+ cancellationToken
+ );
+ }
+
+ private async Task UploadOrContinue(
+ byte[] data,
+ string fileName,
+ FileOptions options,
+ EventHandler? onProgress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var uri = new Uri($"{Url}/upload/resumable");
+
+ var headers = new Dictionary(Headers)
+ {
+ { "cache-control", $"max-age={options.CacheControl}" },
+ };
+
+ var metadata = new MetadataCollection
+ {
+ ["bucketName"] = BucketId,
+ ["objectName"] = fileName,
+ ["contentType"] = options.ContentType,
+ };
+
+ if (options.Upsert)
+ headers.Add("x-upsert", options.Upsert.ToString().ToLower());
+
+ if (options.Metadata != null)
+ metadata["metadata"] = JsonConvert.SerializeObject(options.Metadata);
+
+ options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
+
+ if (options.Duplex != null)
+ headers.Add("x-duplex", options.Duplex.ToLower());
+
+ var progress = new Progress();
+
+ if (onProgress != null)
+ progress.ProgressChanged += onProgress;
+
+ await Helpers.HttpUploadClient!.UploadOrContinueByteAsync(
+ uri,
+ data,
+ headers,
+ metadata,
+ progress,
+ cancellationToken
+ );
+ }
+
private static string ParseMetadata(Dictionary metadata)
{
var json = JsonConvert.SerializeObject(metadata);
var base64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(json));
-
+
return base64;
}
- private async Task UploadOrUpdate(byte[] data, string supabasePath, FileOptions options,
- EventHandler? onProgress = null)
+ private async Task UploadOrUpdate(
+ byte[] data,
+ string supabasePath,
+ FileOptions options,
+ EventHandler? onProgress = null
+ )
{
Uri uri = new Uri($"{Url}/object/{GetFinalPath(supabasePath)}");
var headers = new Dictionary(Headers)
{
{ "cache-control", $"max-age={options.CacheControl}" },
- { "content-type", options.ContentType }
+ { "content-type", options.ContentType },
};
if (options.Upsert)
@@ -535,12 +824,12 @@ private async Task UploadOrUpdate(byte[] data, string supabasePath, File
if (options.Metadata != null)
headers.Add("x-metadata", ParseMetadata(options.Metadata));
-
+
options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value));
-
+
if (options.Duplex != null)
headers.Add("x-duplex", options.Duplex.ToLower());
-
+
var progress = new Progress();
if (onProgress != null)
@@ -551,8 +840,12 @@ private async Task