From 6f4dd85d3865bff9b3b0041d71315053d20a84c7 Mon Sep 17 00:00:00 2001 From: kev-le Date: Thu, 29 Feb 2024 17:13:43 -0800 Subject: [PATCH] Add docs for all methods, modify DownloadPhoto to thrown an exception on error --- StudioClient/DownloadHelper.cs | 59 ++++++++++++++++++++++++++++++---- StudioClient/Job.cs | 46 ++++++++++++++++++++++++++ StudioClient/Photo.cs | 54 +++++++++++++++++++++++++++++-- StudioClient/Profile.cs | 25 ++++++++++++++ StudioClient/StudioClient.cs | 34 ++++++++++++++++++-- 5 files changed, 207 insertions(+), 11 deletions(-) diff --git a/StudioClient/DownloadHelper.cs b/StudioClient/DownloadHelper.cs index e812886..4e08a90 100644 --- a/StudioClient/DownloadHelper.cs +++ b/StudioClient/DownloadHelper.cs @@ -8,11 +8,12 @@ namespace SkylabStudio public class PhotoOptions { public List? Bgs { get; set; } + public bool? ReturnOnError { get; set; } - // Default constructor (parameterless) public PhotoOptions() { Bgs = new List(); + ReturnOnError = false; } } @@ -30,6 +31,11 @@ public DownloadAllPhotosResult() } public partial class StudioClient { + /// + /// Downloads background images based on the provided profile. + /// + /// The profile associated to the job. + /// List of downloaded background images. private async Task?> DownloadBgImages(dynamic profile) { List tempBgs = new List(); @@ -45,6 +51,11 @@ public partial class StudioClient return tempBgs; } + /// + /// Downloads an image asynchronously from the specified URL. + /// + /// The URL of the image to download. + /// The downloaded image as a byte array. private static async Task DownloadImageAsync(string imageUrl) { if (!imageUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { @@ -71,6 +82,16 @@ public partial class StudioClient } } + /// + /// Downloads and replaces the background image in the input image. + /// Required: Profile should have a background uploaded and replace background toggled on + /// + /// The name of the file being processed. + /// The input image to process. + /// The path where the processed images will be saved. + /// The profile associated to the job. + /// List of background images. + /// True if the operation is successful; otherwise, false. private async Task DownloadReplacedBackgroundImage(string fileName, Image inputImage, string outputPath, dynamic? profile = null, List? bgs = null) { try @@ -98,11 +119,20 @@ private async Task DownloadReplacedBackgroundImage(string fileName, Image } catch (Exception ex) { - Console.Error.WriteLine($"Error downloading background image: {ex.Message}"); - return false; + string errorMsg = $"Error downloading background image: {ex.Message}"; + Console.Error.WriteLine(errorMsg); + + throw new Exception(errorMsg); } } + /// + /// Downloads all photos based on a list of photo IDs. + /// + /// List of photo objects with IDs. + /// The profile associated to the job. + /// The path where photos will be downloaded. + /// Download result containing lists of success and errored photo names. public async Task DownloadAllPhotos(JArray photosList, dynamic profile, string outputPath) { if (!Directory.Exists(outputPath)) @@ -127,6 +157,7 @@ public async Task DownloadAllPhotos(JArray photosList, List>> downloadTasks = new List>>(); PhotoOptions photoOptions = new PhotoOptions { + ReturnOnError = true, Bgs = bgs }; foreach (string photoId in photoIds) @@ -163,6 +194,20 @@ public async Task DownloadAllPhotos(JArray photosList, return downloadResults; } } + + /// + /// Downloads a photo based on the specified photo ID. + /// + /// The ID of the photo to download. + /// The path where the downloaded photo will be saved. Could either be + /// Optional: The profile containing photo processing options. + /// Optional: Additional options for photo processing. + /// Optional - *Used Interally with DownloadAllPhotos* : SemaphoreSlim for controlling concurrent photo downloads. + /// + /// A tuple containing the downloaded photo's filename and a boolean indicating + /// whether the download was successful. + /// + /// Thrown on any download error when DownloadPhoto is called without ReturnOnError option. public async Task> DownloadPhoto(long photoId, string outputPath, dynamic? profile = null, PhotoOptions? options = null, SemaphoreSlim? semaphore = null) { string fileName = ""; @@ -194,7 +239,7 @@ public async Task> DownloadPhoto(long photoId, string output // Load output image byte[] imageBuffer = await DownloadImageAsync(photo.retouchedUrl.Value); Image image = Image.NewFromBuffer(imageBuffer); - + if (isExtract) { // Output extract image string pngFileName = $"{Path.GetFileNameWithoutExtension(fileName)}.png"; @@ -218,8 +263,10 @@ public async Task> DownloadPhoto(long photoId, string output return new (fileName, true); } catch (Exception _e) { - Console.Error.WriteLine($"Failed to download photo id: {photoId}"); - Console.Error.WriteLine(_e); + string errorMsg = $"Failed to download photo id: {photoId} - ${_e}"; + if (options?.ReturnOnError == null) { + throw new Exception(errorMsg); + } return new (fileName, false); } finally diff --git a/StudioClient/Job.cs b/StudioClient/Job.cs index 0c4d000..5dfde8b 100644 --- a/StudioClient/Job.cs +++ b/StudioClient/Job.cs @@ -3,46 +3,92 @@ namespace SkylabStudio { public partial class StudioClient { + /// + /// Retrieves a list of all jobs. + /// + /// A dynamic object representing the list of jobs. public async Task ListJobs() { return await Request("jobs", RestSharp.Method.Get); } + /// + /// Creates a new job with the specified payload. + /// + /// The job payload to be sent in the request. + /// A dynamic object representing the created job. public async Task CreateJob(object payload) { return await Request("jobs", RestSharp.Method.Post, payload); } + /// + /// Retrieves information about a specific job based on its ID. + /// + /// The ID of the job to retrieve. + /// A dynamic object representing the requested job. public async Task GetJob(long jobId) { return await Request($"jobs/{jobId}", RestSharp.Method.Get); } + /// + /// Retrieves information about a job based on its name. + /// + /// The name of the job to retrieve. + /// A dynamic object representing the requested job. public async Task GetJobByName(string jobName) { return await Request($"jobs/find_by_name/?name={jobName}", RestSharp.Method.Get); } + /// + /// Updates a specific job with the provided payload. + /// + /// The ID of the job to update. + /// The job payload to be sent in the request. + /// A dynamic object representing the updated job. public async Task UpdateJob(long jobId, object payload) { return await Request($"jobs/{jobId}", RestSharp.Method.Put, payload); } + /// + /// Queues a specific job with the provided payload. + /// + /// The ID of the job to queue. + /// The job payload to be sent in the request. + /// A dynamic object representing the queued job. public async Task QueueJob(long jobId, object payload) { return await Request($"jobs/{jobId}/queue", RestSharp.Method.Post, payload); } + /// + /// Cancels a specific job based on its ID. + /// + /// The ID of the job to cancel. + /// A dynamic object representing the result of the cancellation. public async Task CancelJob(long jobId) { return await Request($"jobs/{jobId}/cancel", RestSharp.Method.Post); } + /// + /// Deletes a specific job based on its ID. + /// + /// The ID of the job to delete. + /// A dynamic object representing the result of the deletion. public async Task DeleteJob(long jobId) { return await Request($"jobs/{jobId}", RestSharp.Method.Delete); } + /// + /// Retrieves information about jobs that are in front of the specified job. + /// + /// The ID of the reference job. + /// A dynamic object representing jobs that are in front of the reference job. public async Task JobsInFront(long jobId) { return await Request($"jobs/{jobId}/jobs_in_front", RestSharp.Method.Get); diff --git a/StudioClient/Photo.cs b/StudioClient/Photo.cs index ded5e95..3e1172d 100644 --- a/StudioClient/Photo.cs +++ b/StudioClient/Photo.cs @@ -7,24 +7,53 @@ namespace SkylabStudio { public partial class StudioClient { - public static readonly string[] VALID_EXTENSIONS = { ".png", ".jpg", ".jpeg", "webp" }; + /// + /// Array of valid file extensions for photos. + /// + public static readonly string[] VALID_EXTENSIONS = { ".png", ".jpg", ".jpeg", ".webp" }; + + /// + /// Maximum allowed size for a photo in bytes. + /// public const int MAX_PHOTO_SIZE = 27 * 1024 * 1024; + /// + /// Creates a new photo with the specified payload. + /// + /// The photo payload to be sent in the request. + /// A dynamic object representing the created photo. public async Task CreatePhoto(object payload) { return await Request("photos", Method.Post, payload); } + /// + /// Retrieves information about a specific photo based on its ID. + /// + /// The ID of the photo to retrieve. + /// A dynamic object representing the requested photo. public async Task GetPhoto(long photoId) { return await Request($"photos/{photoId}", Method.Get); } + /// + /// Deletes a specific photo based on its ID. + /// + /// The ID of the photo to delete. + /// A dynamic object representing the result of the deletion. public async Task DeletePhoto(long photoId) { return await Request($"photos/{photoId}", Method.Delete); } + /// + /// Retrieves a presigned URL for uploading a photo. + /// + /// The ID of the photo for which to get the upload URL. + /// The MD5 hash of the photo data. + /// Flag indicating whether to use cache upload. + /// A dynamic object representing the presigned upload URL. 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}"; @@ -32,16 +61,35 @@ public async Task GetUploadUrl(long photoId, string md5 = "", bool useC return await Request($"photos/upload_url?{queryParams}", Method.Get); } + /// + /// Uploads a photo associated with a job. + /// + /// The path to the photo file. + /// The ID of the job associated with the photo. + /// A dynamic object representing the uploaded photo. public async Task UploadJobPhoto(string photoPath, long jobId) { return await UploadPhoto(photoPath, "job", jobId); } + /// + /// Uploads a photo associated with a profile. + /// + /// The path to the photo file. + /// The ID of the profile associated with the photo. + /// A dynamic object representing the uploaded photo. public async Task UploadProfilePhoto(string photoPath, long profileId) { return await UploadPhoto(photoPath, "profile", profileId); } + /// + /// Uploads a photo associated with a job or profile. + /// + /// The path to the photo file. + /// The name of the model (job or profile). + /// The ID of the model associated with the photo. + /// A dynamic object representing the uploaded photo. private async Task UploadPhoto(string photoPath, string modelName, long modelId) { string[] availableModels = { "job", "profile" }; @@ -51,7 +99,7 @@ private async Task UploadPhoto(string photoPath, string modelName, long string fileExtension = Path.GetExtension(photoBasename).ToLower(); if (!VALID_EXTENSIONS.Contains(fileExtension)) { - throw new Exception("Photo has invalid extension. Supported extensions (.jpg, .jpeg, .png, .webp)"); + throw new Exception("Photo has an invalid extension. Supported extensions (.jpg, .jpeg, .png, .webp)"); } var photoObject = new JObject @@ -82,7 +130,7 @@ private async Task UploadPhoto(string photoPath, string modelName, long 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"); + if (modelName == "job") request.AddHeader("X-Amz-Tagging", "job=photo&api=true"); // Upload image via PUT request to presigned url RestResponse response = await httpClient.ExecuteAsync(request); diff --git a/StudioClient/Profile.cs b/StudioClient/Profile.cs index d85a85a..1069623 100644 --- a/StudioClient/Profile.cs +++ b/StudioClient/Profile.cs @@ -2,26 +2,51 @@ namespace SkylabStudio { public partial class StudioClient { + /// + /// Creates a new profile with the specified payload. + /// + /// The payload containing profile information. + /// A dynamic object representing the created profile. public async Task CreateProfile(object payload) { return await Request("profiles", RestSharp.Method.Post, payload); } + /// + /// Retrieves a list of profiles. + /// + /// A dynamic object representing a list of profiles. public async Task ListProfiles() { return await Request("profiles", RestSharp.Method.Get); } + /// + /// Retrieves the profile with the specified ID. + /// + /// The ID of the profile to retrieve. + /// A dynamic object representing the retrieved profile. public async Task GetProfile(long profileId) { return await Request($"profiles/{profileId}", RestSharp.Method.Get); } + /// + /// Updates the profile with the specified ID using the provided payload. + /// + /// The ID of the profile to update. + /// The payload containing updated profile information. + /// A dynamic object representing the updated profile. public async Task UpdateProfile(long profileId, object payload) { return await Request($"profiles/{profileId}", RestSharp.Method.Put, payload); } + /// + /// Retrieves background photos associated with the profile identified by the given profile ID. + /// + /// The ID of the profile to retrieve background photos for. + /// A dynamic object representing background photos of the profile. public async Task GetProfileBgs(long profileId) { return await Request($"profiles/{profileId}/bg_photos", RestSharp.Method.Get); diff --git a/StudioClient/StudioClient.cs b/StudioClient/StudioClient.cs index 4807ace..93c9f5e 100644 --- a/StudioClient/StudioClient.cs +++ b/StudioClient/StudioClient.cs @@ -16,6 +16,11 @@ public partial class StudioClient private readonly string _apiKey; private readonly int _maxConcurrentDownloads = 5; + /// + /// Initializes a new instance of the class with the specified API key and options. + /// + /// The API key used for authentication. + /// Optional options for configuring the StudioClient. public StudioClient(string? apiKey = null, StudioOptions? options = null) { if (apiKey == null) throw new Exception("No API key provided"); @@ -26,6 +31,14 @@ public StudioClient(string? apiKey = null, StudioOptions? options = null) _maxConcurrentDownloads = options?.MaxConcurrentDownloads ?? 5; } + /// + /// Makes an HTTP request to the Skylab API endpoint with the specified parameters. + /// + /// The API endpoint to request. + /// The HTTP method (GET, POST, PUT, DELETE, etc.) to use for the request. + /// Optional payload data to include in the request. + /// The dynamic response data from the Skylab API. + /// Thrown when the HTTP request fails or when the response is not successful. private async Task Request(string endpoint, Method httpMethod, object? payload = null) { var apiEndpoint = $"api/public/v1/{endpoint}"; @@ -54,14 +67,19 @@ private async Task Request(string endpoint, Method httpMethod, object? if (jsonData != null) return jsonData; } - throw new Exception($"Failed to get successful response: {response?.Content}"); + throw new Exception($"{response?.Content}"); } catch (Exception ex) { - throw new Exception("An error occurred while making the HTTP request.", ex); + // Rethrow the exception to be caught by user + throw ex; } } + /// + /// Builds and returns a dictionary of headers to be included in the Skylab API request. + /// + /// A dictionary of headers with their corresponding values. private Dictionary BuildRequestHeaders() { var clientHeader = "1.1"; @@ -78,6 +96,18 @@ private Dictionary BuildRequestHeaders() return headers; } + + /// + /// Validates HMAC headers by comparing the computed HMAC signature with the provided signature. + /// Used to validate job json object in callback is from Skylab. + /// + /// The secret key used for HMAC hashing. + /// The JSON representation of the job. + /// The timestamp of the API request. + /// The provided HMAC signature to be validated. + /// + /// true if the computed HMAC signature matches the provided signature; otherwise, false. + /// public bool ValidateHmacHeaders(string secretKey, string jobJson, string requestTimestamp, string signature) { // Convert the secret key and message to byte arrays