From 49f6a0b5f79d20465f29fc4aa8a70be48aa004e9 Mon Sep 17 00:00:00 2001 From: kev-le Date: Wed, 28 Feb 2024 15:08:05 -0800 Subject: [PATCH] Add ability to download photo with output path + photo name, migrate to use RestSharp for REST api calls --- README.md | 3 ++- StudioClient.Tests/Job.Test.cs | 12 ------------ StudioClient/DownloadHelper.cs | 29 +++++++++++++++++++---------- StudioClient/Job.cs | 18 +++++++++--------- StudioClient/Photo.cs | 24 +++++++++++++----------- StudioClient/Profile.cs | 10 +++++----- StudioClient/StudioClient.cs | 26 ++++++++++++++------------ StudioClient/StudioClient.csproj | 3 ++- 8 files changed, 64 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 3afbc3f..364d36a 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,8 @@ Erorred photos: [4.jpg] OR ```dotnet -api.DownloadPhoto(photoId, "/path/to/photo"); +api.DownloadPhoto(photoId, "/path/to/photo.jpg"); # accepts full path OR +api.DownloadPhoto(photoId, "/path/to/output/folder"); # accepts directory ``` #### Delete photo diff --git a/StudioClient.Tests/Job.Test.cs b/StudioClient.Tests/Job.Test.cs index b30f69b..9effca9 100644 --- a/StudioClient.Tests/Job.Test.cs +++ b/StudioClient.Tests/Job.Test.cs @@ -68,16 +68,4 @@ public async Task Call_CancelJob_ReturnsJob() Assert.NotNull(cancelledJob); } - - [Fact] - public async Task Call_JobsInFront_ReturnsJob() - { - Guid randomUuid = Guid.NewGuid(); - var jobName = $"test-job-{randomUuid}"; - dynamic job = await apiClient.CreateJob(new { name = jobName, profile_id = profile?.id.Value }); - - dynamic jobsInFrontMessage = await apiClient.JobsInFront(job.id.Value); - - Assert.NotNull(jobsInFrontMessage.message); - } } \ No newline at end of file diff --git a/StudioClient/DownloadHelper.cs b/StudioClient/DownloadHelper.cs index 2a79594..683acf4 100644 --- a/StudioClient/DownloadHelper.cs +++ b/StudioClient/DownloadHelper.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using NetVips; +using RestSharp; namespace SkylabStudio @@ -20,8 +21,6 @@ public partial class StudioClient { private async Task?> DownloadBgImages(dynamic profile) { - var httpClient = new HttpClient(); - List tempBgs = new List(); List bgPhotos = ((JArray) profile!.photos).Where(photo => photo["jobId"] != null).ToList(); @@ -43,10 +42,13 @@ public partial class StudioClient try { - using (HttpClient httpClient = new HttpClient()) + using (RestClient httpClient = new RestClient()) { + RestRequest request = new RestRequest(imageUrl, Method.Get); // Download the image into a byte array - byte[] imageBuffer = await httpClient.GetByteArrayAsync(imageUrl); + RestResponse response = await httpClient.ExecuteAsync(request); + + byte[] imageBuffer = response.RawBytes ?? Array.Empty(); return imageBuffer; } @@ -99,8 +101,6 @@ public async Task DownloadAllPhotos(JArray photosList, profile = await GetProfile(profile.id.Value); List bgs = await DownloadBgImages(profile); - var httpClient = new HttpClient(); - List photoIds = photosList.Select(photo => photo?["id"]?.ToString() ?? "").ToList() ?? new List(); // Use a semaphore to control access to the download operation @@ -142,13 +142,22 @@ public async Task DownloadAllPhotos(JArray photosList, } public async Task> DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, dynamic? options = null, SemaphoreSlim? semaphore = null) { - dynamic photo = await GetPhoto(photoId); - long profileId = photo.job.profileId; + string fileName = ""; - string fileName = photo.name.Value; + if (!Directory.Exists(outputPath)) + { + // Must be a file path - separate outputPath and fileName + fileName = Path.GetFileName(outputPath); + outputPath = Path.GetDirectoryName(outputPath) ?? ""; + } + + if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available try { - if (semaphore != null) await semaphore.WaitAsync(); // Wait until a slot is available + var photo = await GetPhoto(photoId); + long profileId = photo.job.profileId; + + if (fileName.Length <= 0) fileName = photo.name.Value; if (profile == null) { profile = await GetProfile(profileId); diff --git a/StudioClient/Job.cs b/StudioClient/Job.cs index 0ac1688..0c4d000 100644 --- a/StudioClient/Job.cs +++ b/StudioClient/Job.cs @@ -5,47 +5,47 @@ public partial class StudioClient { public async Task ListJobs() { - return await Request("jobs", HttpMethod.Get); + return await Request("jobs", RestSharp.Method.Get); } public async Task CreateJob(object payload) { - return await Request("jobs", HttpMethod.Post, payload); + return await Request("jobs", RestSharp.Method.Post, payload); } public async Task GetJob(long jobId) { - return await Request($"jobs/{jobId}", HttpMethod.Get); + return await Request($"jobs/{jobId}", RestSharp.Method.Get); } public async Task GetJobByName(string jobName) { - return await Request($"jobs/find_by_name/?name={jobName}", HttpMethod.Get); + return await Request($"jobs/find_by_name/?name={jobName}", RestSharp.Method.Get); } public async Task UpdateJob(long jobId, object payload) { - return await Request($"jobs/{jobId}", HttpMethod.Put, payload); + return await Request($"jobs/{jobId}", RestSharp.Method.Put, payload); } public async Task QueueJob(long jobId, object payload) { - return await Request($"jobs/{jobId}/queue", HttpMethod.Post, payload); + return await Request($"jobs/{jobId}/queue", RestSharp.Method.Post, payload); } public async Task CancelJob(long jobId) { - return await Request($"jobs/{jobId}/cancel", HttpMethod.Post); + return await Request($"jobs/{jobId}/cancel", RestSharp.Method.Post); } public async Task DeleteJob(long jobId) { - return await Request($"jobs/{jobId}", HttpMethod.Delete); + return await Request($"jobs/{jobId}", RestSharp.Method.Delete); } public async Task JobsInFront(long jobId) { - return await Request($"jobs/{jobId}/jobs_in_front", HttpMethod.Get); + return await Request($"jobs/{jobId}/jobs_in_front", RestSharp.Method.Get); } } } \ No newline at end of file diff --git a/StudioClient/Photo.cs b/StudioClient/Photo.cs index 121bb7d..ded5e95 100644 --- a/StudioClient/Photo.cs +++ b/StudioClient/Photo.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using Newtonsoft.Json.Linq; +using RestSharp; namespace SkylabStudio @@ -11,24 +12,24 @@ public partial class StudioClient public async Task CreatePhoto(object payload) { - return await Request("photos", HttpMethod.Post, payload); + return await Request("photos", Method.Post, payload); } public async Task GetPhoto(long photoId) { - return await Request($"photos/{photoId}", HttpMethod.Get); + return await Request($"photos/{photoId}", Method.Get); } public async Task DeletePhoto(long photoId) { - return await Request($"photos/{photoId}", HttpMethod.Delete); + return await Request($"photos/{photoId}", Method.Delete); } public async Task GetUploadUrl(long photoId, string md5 = "", bool useCacheUpload = false) { string queryParams = $"use_cache_upload={useCacheUpload.ToString().ToLower()}&photo_id={photoId}&content_md5={md5}"; - return await Request($"photos/upload_url?{queryParams}", HttpMethod.Get); + return await Request($"photos/upload_url?{queryParams}", Method.Get); } public async Task UploadJobPhoto(string photoPath, long jobId) @@ -74,16 +75,17 @@ private async Task UploadPhoto(string photoPath, string modelName, long dynamic uploadObj = await GetUploadUrl(photo.id.Value, md5Base64); string presignedUrl = uploadObj.url.Value; - using (var client = new HttpClient()) + using (RestClient httpClient = new RestClient()) { - var content = new ByteArrayContent(photoData); - var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl); - request.Content = content; - request.Content.Headers.ContentMD5 = MD5.Create().ComputeHash(photoData); - if (modelName == "job") request.Headers.Add("X-Amz-Tagging", "job=photo&api=true"); + byte[] fileBytes = File.ReadAllBytes(photoPath); + RestRequest request = new RestRequest(presignedUrl, Method.Put); + + request.AddParameter("application/octet-stream", fileBytes, ParameterType.RequestBody); + request.AddHeader("Content-MD5", Convert.ToBase64String(MD5.Create().ComputeHash(fileBytes))); + if (modelName == "job") request.AddHeader("X-Amz-Tagging", "job=photo&api=true"); // Upload image via PUT request to presigned url - HttpResponseMessage response = await client.SendAsync(request); + RestResponse response = await httpClient.ExecuteAsync(request); if (!response.IsSuccessStatusCode) { diff --git a/StudioClient/Profile.cs b/StudioClient/Profile.cs index 7a60530..d85a85a 100644 --- a/StudioClient/Profile.cs +++ b/StudioClient/Profile.cs @@ -4,27 +4,27 @@ public partial class StudioClient { public async Task CreateProfile(object payload) { - return await Request("profiles", HttpMethod.Post, payload); + return await Request("profiles", RestSharp.Method.Post, payload); } public async Task ListProfiles() { - return await Request("profiles", HttpMethod.Get); + return await Request("profiles", RestSharp.Method.Get); } public async Task GetProfile(long profileId) { - return await Request($"profiles/{profileId}", HttpMethod.Get); + return await Request($"profiles/{profileId}", RestSharp.Method.Get); } public async Task UpdateProfile(long profileId, object payload) { - return await Request($"profiles/{profileId}", HttpMethod.Put, payload); + return await Request($"profiles/{profileId}", RestSharp.Method.Put, payload); } public async Task GetProfileBgs(long profileId) { - return await Request($"profiles/{profileId}/bg_photos", HttpMethod.Get); + return await Request($"profiles/{profileId}/bg_photos", RestSharp.Method.Get); } } } diff --git a/StudioClient/StudioClient.cs b/StudioClient/StudioClient.cs index ee46e47..4807ace 100644 --- a/StudioClient/StudioClient.cs +++ b/StudioClient/StudioClient.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; using System.Text; using Newtonsoft.Json; +using RestSharp; namespace SkylabStudio { @@ -11,7 +12,7 @@ public class StudioOptions public partial class StudioClient { - private readonly HttpClient _httpClient; + private readonly RestClient _httpClient; private readonly string _apiKey; private readonly int _maxConcurrentDownloads = 5; @@ -20,39 +21,40 @@ public StudioClient(string? apiKey = null, StudioOptions? options = null) if (apiKey == null) throw new Exception("No API key provided"); string baseUrl = Environment.GetEnvironmentVariable("SKYLAB_API_URL") ?? "https://studio.skylabtech.ai"; - _httpClient = new HttpClient(); - _httpClient.BaseAddress = new Uri(baseUrl); + _httpClient = new RestClient(baseUrl); _apiKey = apiKey; _maxConcurrentDownloads = options?.MaxConcurrentDownloads ?? 5; } - private async Task Request(string endpoint, HttpMethod httpMethod, object? payload = null) + private async Task Request(string endpoint, Method httpMethod, object? payload = null) { var apiEndpoint = $"api/public/v1/{endpoint}"; - var request = new HttpRequestMessage(httpMethod, apiEndpoint); + RestRequest request = new RestRequest(apiEndpoint, httpMethod); var headers = BuildRequestHeaders(); foreach (var header in headers) { - request.Headers.Add(header.Key, header.Value); + request.AddHeader(header.Key, header.Value); } if (payload != null) { var jsonObj = JsonConvert.SerializeObject(payload); - request.Content = new StringContent(jsonObj, Encoding.UTF8, "application/json"); + request.AddJsonBody(jsonObj); } try { // Send the request and get the response - HttpResponseMessage response = await _httpClient.SendAsync(request); + RestResponse response = await _httpClient.ExecuteAsync(request); - string responseContent = await response.Content.ReadAsStringAsync(); - dynamic? jsonData = JsonConvert.DeserializeObject(responseContent); - if (jsonData != null) return jsonData; + string responseContent = response?.Content ?? ""; + if (response?.IsSuccessStatusCode ?? false) { + dynamic? jsonData = JsonConvert.DeserializeObject(responseContent); + if (jsonData != null) return jsonData; + } - throw new Exception("Failed to get response from server."); + throw new Exception($"Failed to get successful response: {response?.Content}"); } catch (Exception ex) { diff --git a/StudioClient/StudioClient.csproj b/StudioClient/StudioClient.csproj index c79af6d..f76e5e0 100644 --- a/StudioClient/StudioClient.csproj +++ b/StudioClient/StudioClient.csproj @@ -11,7 +11,7 @@ https://github.com/skylab-tech/studio_client_dotnet https://avatars.githubusercontent.com/u/45469060 studio skylab skylabtech api library .net nuget - net5.0;net6.0;net7.0;netstandard2.1;netcoreapp3.0;netcoreapp3.1 + net47;net471;net472;net48;net481;net5.0;net6.0;net7.0;netstandard2.1;netcoreapp3.1 enable enable README.md @@ -24,6 +24,7 @@ +