From 678379ef16d8661fa2d556a8cf77bcffa7ac28ff Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 15:23:30 +0200 Subject: [PATCH 1/6] Support file upload --- SynologyDotNet.Core/Helpers/RequestBuilder.cs | 68 +++++++++++++++++-- .../Responses/ApiListRessponse.cs | 2 +- SynologyDotNet.Core/SynoClient.cs | 58 ++++++++++++---- 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/SynologyDotNet.Core/Helpers/RequestBuilder.cs b/SynologyDotNet.Core/Helpers/RequestBuilder.cs index 5a87d88..af85926 100644 --- a/SynologyDotNet.Core/Helpers/RequestBuilder.cs +++ b/SynologyDotNet.Core/Helpers/RequestBuilder.cs @@ -1,8 +1,10 @@ -using System; +using SynologyDotNet.Core.Model; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; -using SynologyDotNet.Core.Model; +using System.Threading.Tasks; namespace SynologyDotNet.Core.Helpers { @@ -26,6 +28,15 @@ public class RequestBuilder /// The parameters in this collection will be always serialized into the query string regardless of the request type /// public Dictionary ExplicitQueryStringParams { get; } = new Dictionary(); + + /// + /// File to upload with its filename + /// + /// + /// The file. + /// + public (Stream, string)? File { get; set; } + #endregion #region Constructor @@ -203,6 +214,23 @@ public RequestBuilder SetParams(params (string, object)[] parameters) return this; } + /// + /// Sets the file to upload + /// + /// The file. + /// The filename. + /// + public RequestBuilder SetFile(Stream file, string filename) + { + if (file == null || file.Length == 0) + throw new ArgumentNullException(nameof(file)); + if (string.IsNullOrWhiteSpace(filename)) + throw new ArgumentNullException(nameof(filename)); + + File = (file, filename); + return this; + } + /// /// Explicit query string parameters are always appended to the final URL, regardless the chosen HTTP method. /// @@ -226,17 +254,45 @@ public RequestBuilder SetExplicitQueryStringParams(params (string, object)[] par /// Converts to HTTP POST request. /// /// - public HttpRequestMessage ToPostRequest() + public async Task ToPostRequest() { // Only ExplicitQueryStringParams are going to the query string string url = GetBaseUrl(); if (ExplicitQueryStringParams.Count > 0) url += "?" + string.Join("&", ExplicitQueryStringParams.Select(x => $"{x.Key}={System.Web.HttpUtility.UrlEncode(x.Value)}")); // All the other parameters are serialized into the request body - var msg = new HttpRequestMessage(HttpMethod.Post, url) + var msg = new HttpRequestMessage(HttpMethod.Post, url); + var paramContent = new FormUrlEncodedContent(Params); + if (File == null) + { + msg.Content = paramContent; + } + // If a file is attached, create a Multipart + else { - Content = new FormUrlEncodedContent(Params) - }; + var multiPart = new MultipartFormDataContent(Guid.NewGuid().ToString()); + // Add all parameters as separate form data + foreach (var param in Params.Where(p => p.Value != null)) + { + var stringContent = new StringContent(param.Value); + // Content type is automatically set to text/plain, but this makes the API return an error. Set to null + stringContent.Headers.ContentType = null; + multiPart.Add(stringContent, $"\"{param.Key}\""); + } + // Add the file + multiPart.Add(new StreamContent(File.Value.Item1), "\"file\"", $"\"{File.Value.Item2}\""); + // Problem with the .NET framework: the boundary is set between quotes (""). The API will return a 101 error because it cannot process this. + // Therefore, remove the quotes + var boundaryParameter = multiPart.Headers.ContentType.Parameters.Single(p => p.Name == "boundary"); + boundaryParameter.Value = boundaryParameter.Value.Replace("\"", ""); + + // As a result, Multipart does not calculate the size anymore (set to 0), which leaves the final request body empty. Calculate manually + var size = (await multiPart.ReadAsStreamAsync()).Length; + multiPart.Headers.ContentLength = size; + + msg.Content = multiPart; + } + return msg; } diff --git a/SynologyDotNet.Core/Responses/ApiListRessponse.cs b/SynologyDotNet.Core/Responses/ApiListRessponse.cs index ba5f198..c5ba9fc 100644 --- a/SynologyDotNet.Core/Responses/ApiListRessponse.cs +++ b/SynologyDotNet.Core/Responses/ApiListRessponse.cs @@ -8,7 +8,7 @@ namespace SynologyDotNet.Core.Responses /// /// /// - public class ApiListRessponse : ApiResponse, IApiListResponse + public class ApiListResponse : ApiResponse, IApiListResponse where T : ListResponseBase { ListResponseBase IApiListResponse.Data => Data; diff --git a/SynologyDotNet.Core/SynoClient.cs b/SynologyDotNet.Core/SynoClient.cs index b715d2f..c74b7c6 100644 --- a/SynologyDotNet.Core/SynoClient.cs +++ b/SynologyDotNet.Core/SynoClient.cs @@ -1,15 +1,16 @@ -using System; +using Newtonsoft.Json; +using SynologyDotNet.Core.Exceptions; +using SynologyDotNet.Core.Helpers; +using SynologyDotNet.Core.Model; +using SynologyDotNet.Core.Responses; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; -using SynologyDotNet.Core.Exceptions; -using SynologyDotNet.Core.Helpers; -using SynologyDotNet.Core.Model; -using SynologyDotNet.Core.Responses; namespace SynologyDotNet { @@ -264,7 +265,7 @@ public async Task LoginAsync(string username, string password, Canc req["enable_syno_token"] = "yes"; req.SetExplicitQueryStringParam("enable_syno_token", "yes"); // This is necessary to get a valid synotoken, this has to be present in the query string as well (even if it's a POST!) - var response = await _httpClient.SendAsync(req.ToPostRequest(), cancellationToken).ConfigureAwait(false); + var response = await _httpClient.SendAsync(await req.ToPostRequest(), cancellationToken).ConfigureAwait(false); ThrowIfNotSuccessfulHttpResponse(response); var json = Encoding.UTF8.GetString(await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); var loginResult = JsonConvert.DeserializeObject(json); @@ -384,12 +385,11 @@ public async Task QueryListAsync(string apiName, string method, int limit, /// /// The parameters. /// - public async Task QueryListAsync(string apiName, string method, int limit, int offset, CancellationToken cancellationToken, params (string, object)[] parameters) + public Task QueryListAsync(string apiName, string method, int limit, int offset, CancellationToken cancellationToken, params (string, object)[] parameters) where T : IApiListResponse { var req = new RequestBuilder(GetApiInfo(apiName)).Method(method).Limit(limit).Offset(offset).SetParams(parameters); - var result = await QueryObjectAsync(req, cancellationToken).ConfigureAwait(false); - return result; + return QueryObjectAsync(req, cancellationToken); } /// @@ -403,6 +403,19 @@ public async Task QueryListAsync(string apiName, string method, int limit, public async Task QueryObjectAsync(string apiName, string method, params (string, object)[] parameters) => await QueryObjectAsync(apiName, method, CancellationToken.None, parameters).ConfigureAwait(false); + /// + /// Queries an entity from the specified endpoint with a file attached. + /// + /// + /// Name of the API. + /// The method. + /// The stream. + /// The filename. + /// The parameters. + /// + public Task QueryObjectAsync(string apiName, string method, Stream data, string filename, params (string, object)[] parameters) + => QueryObjectAsync(apiName, method, CancellationToken.None, data, filename, parameters); + /// /// Queries an entity from the specified endpoint. /// @@ -419,6 +432,23 @@ public async Task QueryObjectAsync(string apiName, string method, Cancella return result; } + /// + /// Queries an entity from the specified endpoint with a file attached + /// + /// + /// Name of the API. + /// The method. + /// The cancellation token. + /// The stream. + /// The filename. + /// The parameters. + /// + public Task QueryObjectAsync(string apiName, string method, CancellationToken cancellationToken, Stream data, string filename, params (string, object)[] parameters) + { + var req = new RequestBuilder(GetApiInfo(apiName)).Method(method).SetParams(parameters).SetFile(data, filename); + return QueryObjectAsync(req, cancellationToken); + } + /// /// Queries image data from the specified endpoint. /// @@ -461,7 +491,7 @@ public async Task QueryStringAsync(RequestBuilder req) /// public async Task QueryStringAsync(RequestBuilder req, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(req.ToPostRequest(), cancellationToken).ConfigureAwait(false); + var response = await _httpClient.SendAsync(await req.ToPostRequest(), cancellationToken).ConfigureAwait(false); ThrowIfNotSuccessfulHttpResponse(response); var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); // Do not use ReadAsStringAsync here var text = Encoding.UTF8.GetString(bytes); @@ -486,7 +516,7 @@ public async Task QueryObjectAsync(RequestBuilder req) /// public async Task QueryObjectAsync(RequestBuilder req, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(req.ToPostRequest(), cancellationToken).ConfigureAwait(false); + var response = await _httpClient.SendAsync(await req.ToPostRequest(), cancellationToken).ConfigureAwait(false); ThrowIfNotSuccessfulHttpResponse(response); var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); // Do not use ReadAsStringAsync here var text = Encoding.UTF8.GetString(bytes); @@ -512,7 +542,7 @@ public async Task QueryByteArrayAsync(RequestBuilder req) /// public async Task QueryByteArrayAsync(RequestBuilder req, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(req.ToPostRequest(), cancellationToken).ConfigureAwait(false); + var response = await _httpClient.SendAsync(await req.ToPostRequest(), cancellationToken).ConfigureAwait(false); ThrowIfNotSuccessfulHttpResponse(response); var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); return new ByteArrayData() @@ -546,7 +576,7 @@ public async Task QueryStreamAsync(RequestBuilder req, Action read /// public async Task QueryStreamAsync(RequestBuilder req, Action readStreamAction, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(req.ToPostRequest(), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var response = await _httpClient.SendAsync(await req.ToPostRequest(), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); ThrowIfNotSuccessfulHttpResponse(response); if (response.Content is null) throw new NullReferenceException("No content."); From 19e13128e7c1e1333ad7c8cd4a9b4933f1e1dca0 Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 15:24:10 +0200 Subject: [PATCH 2/6] Updated version --- SynologyDotNet.Core/SynologyDotNet.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynologyDotNet.Core/SynologyDotNet.Core.csproj b/SynologyDotNet.Core/SynologyDotNet.Core.csproj index c9b7b83..552fc8a 100644 --- a/SynologyDotNet.Core/SynologyDotNet.Core.csproj +++ b/SynologyDotNet.Core/SynologyDotNet.Core.csproj @@ -8,7 +8,7 @@ SynologyDotNet.Core SynologyDotNet.Core - 0.4.3 + 0.4.4 Base library to develop .NET clients for Synology DSM. https://github.com/geloczigeri/synologydotnet-core Gergő Gelóczi From 4d769fe2106ab0f8bee2fab18b6024e8211f6004 Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 15:27:18 +0200 Subject: [PATCH 3/6] Renamed ApiListRessponse --- .../Responses/{ApiListRessponse.cs => ApiListResponse.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SynologyDotNet.Core/Responses/{ApiListRessponse.cs => ApiListResponse.cs} (100%) diff --git a/SynologyDotNet.Core/Responses/ApiListRessponse.cs b/SynologyDotNet.Core/Responses/ApiListResponse.cs similarity index 100% rename from SynologyDotNet.Core/Responses/ApiListRessponse.cs rename to SynologyDotNet.Core/Responses/ApiListResponse.cs From ed1db0659e909d17d61a61d2e3603b1d77c1e656 Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 15:29:51 +0200 Subject: [PATCH 4/6] Remove 0 check for stream --- SynologyDotNet.Core/Helpers/RequestBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynologyDotNet.Core/Helpers/RequestBuilder.cs b/SynologyDotNet.Core/Helpers/RequestBuilder.cs index af85926..dc8f1a6 100644 --- a/SynologyDotNet.Core/Helpers/RequestBuilder.cs +++ b/SynologyDotNet.Core/Helpers/RequestBuilder.cs @@ -222,7 +222,7 @@ public RequestBuilder SetParams(params (string, object)[] parameters) /// public RequestBuilder SetFile(Stream file, string filename) { - if (file == null || file.Length == 0) + if (file == null) throw new ArgumentNullException(nameof(file)); if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullException(nameof(filename)); From 618bf936e4a0b7b282b72f2068b077efd2060795 Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 17:34:47 +0200 Subject: [PATCH 5/6] Added datetime extensions for unix timestamp --- .../Extensions/DateTimeExtensions.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 SynologyDotNet.Core/Extensions/DateTimeExtensions.cs diff --git a/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs b/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs new file mode 100644 index 0000000..b2dbaf1 --- /dev/null +++ b/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs @@ -0,0 +1,56 @@ +using System; + +namespace SynologyDotNet.Core.Extensions +{ + public static class DateTimeExtensions + { + #region Unix seconds + + public static DateTime FromUnixSecondsToDateTimeUtc(this long unixTime) + { + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(unixTime); + } + + public static long FromDateTimeUtcToUnixSeconds(this DateTime dateTime) + { + return ((DateTimeOffset)dateTime).ToUnixTimeSeconds(); + } + + public static DateTime? FromUnixSecondsToDateTimeUtc(this long? unixTime) + { + return unixTime?.FromUnixSecondsToDateTimeUtc(); + } + + public static long? FromDateTimeUtcToUnix(this DateTime? dateTime) + { + return dateTime?.FromDateTimeUtcToUnixSeconds(); + } + + #endregion + + #region Milliseconds + + public static DateTime FromUnixMillisecondsToDateTimeUtc(this long unixTime) + { + return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(unixTime); + } + + public static long FromDateTimeUtcToUnixMilliseconds(this DateTime dateTime) + { + return ((DateTimeOffset)dateTime).ToUnixTimeMilliseconds(); + } + + public static DateTime? FromUnixMillisecondsToDateTimeUtc(this long? unixTime) + { + return unixTime?.FromUnixMillisecondsToDateTimeUtc(); + } + + public static long? FromDateTimeUtcToUnixMilliseconds(this DateTime? dateTime) + { + return dateTime?.FromDateTimeUtcToUnixMilliseconds(); + } + + #endregion + + } +} From dac2518a4d46d8a67d0fe522e4b171e6a999b7ef Mon Sep 17 00:00:00 2001 From: Peter Butzelaar Date: Sun, 31 Jul 2022 17:38:50 +0200 Subject: [PATCH 6/6] Added comment --- .../Extensions/DateTimeExtensions.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs b/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs index b2dbaf1..00ece19 100644 --- a/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs +++ b/SynologyDotNet.Core/Extensions/DateTimeExtensions.cs @@ -2,25 +2,48 @@ namespace SynologyDotNet.Core.Extensions { + /// + /// Extension methods to convert from and to Unix timestamps. Be aware that DSM sometimes returns Unix timestamps in seconds, and sometimes in milliseconds + /// public static class DateTimeExtensions { #region Unix seconds + /// + /// Converts Unix time in seconds to a DateTime + /// + /// The unix time. + /// public static DateTime FromUnixSecondsToDateTimeUtc(this long unixTime) { return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(unixTime); } + /// + /// Converts from DateTime to a Unix time in seconds + /// + /// The date time. + /// public static long FromDateTimeUtcToUnixSeconds(this DateTime dateTime) { return ((DateTimeOffset)dateTime).ToUnixTimeSeconds(); } + /// + /// Converts Unix time in seconds to a DateTime + /// + /// The unix time. + /// public static DateTime? FromUnixSecondsToDateTimeUtc(this long? unixTime) { return unixTime?.FromUnixSecondsToDateTimeUtc(); } + /// + /// Converts from DateTime to a Unix time in seconds + /// + /// The date time. + /// public static long? FromDateTimeUtcToUnix(this DateTime? dateTime) { return dateTime?.FromDateTimeUtcToUnixSeconds(); @@ -30,21 +53,41 @@ public static long FromDateTimeUtcToUnixSeconds(this DateTime dateTime) #region Milliseconds + /// + /// Converts Unix time in milliseconds to a DateTime + /// + /// The unix time. + /// public static DateTime FromUnixMillisecondsToDateTimeUtc(this long unixTime) { return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(unixTime); } + /// + /// Converts from DateTime to a Unix time in milliseconds + /// + /// The date time. + /// public static long FromDateTimeUtcToUnixMilliseconds(this DateTime dateTime) { return ((DateTimeOffset)dateTime).ToUnixTimeMilliseconds(); } + /// + /// Converts Unix time in milliseconds to a DateTime + /// + /// The unix time. + /// public static DateTime? FromUnixMillisecondsToDateTimeUtc(this long? unixTime) { return unixTime?.FromUnixMillisecondsToDateTimeUtc(); } + /// + /// Converts from DateTime to a Unix time in milliseconds + /// + /// The date time. + /// public static long? FromDateTimeUtcToUnixMilliseconds(this DateTime? dateTime) { return dateTime?.FromDateTimeUtcToUnixMilliseconds();