Skip to content

Commit

Permalink
Merge pull request #1 from PeterButzelaar/feature/SupportFileUpload
Browse files Browse the repository at this point in the history
Feature/support file upload
  • Loading branch information
geloczi authored Jul 31, 2022
2 parents 3c5f686 + dac2518 commit 32bbba8
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 22 deletions.
99 changes: 99 additions & 0 deletions SynologyDotNet.Core/Extensions/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;

namespace SynologyDotNet.Core.Extensions
{
/// <summary>
/// Extension methods to convert from and to Unix timestamps. Be aware that DSM sometimes returns Unix timestamps in seconds, and sometimes in milliseconds
/// </summary>
public static class DateTimeExtensions
{
#region Unix seconds

/// <summary>
/// Converts Unix time in seconds to a DateTime
/// </summary>
/// <param name="unixTime">The unix time.</param>
/// <returns></returns>
public static DateTime FromUnixSecondsToDateTimeUtc(this long unixTime)
{
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(unixTime);
}

/// <summary>
/// Converts from DateTime to a Unix time in seconds
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <returns></returns>
public static long FromDateTimeUtcToUnixSeconds(this DateTime dateTime)
{
return ((DateTimeOffset)dateTime).ToUnixTimeSeconds();
}

/// <summary>
/// Converts Unix time in seconds to a DateTime
/// </summary>
/// <param name="unixTime">The unix time.</param>
/// <returns></returns>
public static DateTime? FromUnixSecondsToDateTimeUtc(this long? unixTime)
{
return unixTime?.FromUnixSecondsToDateTimeUtc();
}

/// <summary>
/// Converts from DateTime to a Unix time in seconds
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <returns></returns>
public static long? FromDateTimeUtcToUnix(this DateTime? dateTime)
{
return dateTime?.FromDateTimeUtcToUnixSeconds();
}

#endregion

#region Milliseconds

/// <summary>
/// Converts Unix time in milliseconds to a DateTime
/// </summary>
/// <param name="unixTime">The unix time.</param>
/// <returns></returns>
public static DateTime FromUnixMillisecondsToDateTimeUtc(this long unixTime)
{
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(unixTime);
}

/// <summary>
/// Converts from DateTime to a Unix time in milliseconds
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <returns></returns>
public static long FromDateTimeUtcToUnixMilliseconds(this DateTime dateTime)
{
return ((DateTimeOffset)dateTime).ToUnixTimeMilliseconds();
}

/// <summary>
/// Converts Unix time in milliseconds to a DateTime
/// </summary>
/// <param name="unixTime">The unix time.</param>
/// <returns></returns>
public static DateTime? FromUnixMillisecondsToDateTimeUtc(this long? unixTime)
{
return unixTime?.FromUnixMillisecondsToDateTimeUtc();
}

/// <summary>
/// Converts from DateTime to a Unix time in milliseconds
/// </summary>
/// <param name="dateTime">The date time.</param>
/// <returns></returns>
public static long? FromDateTimeUtcToUnixMilliseconds(this DateTime? dateTime)
{
return dateTime?.FromDateTimeUtcToUnixMilliseconds();
}

#endregion

}
}
68 changes: 62 additions & 6 deletions SynologyDotNet.Core/Helpers/RequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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
/// </summary>
public Dictionary<string, string> ExplicitQueryStringParams { get; } = new Dictionary<string, string>();

/// <summary>
/// File to upload with its filename
/// </summary>
/// <value>
/// The file.
/// </value>
public (Stream, string)? File { get; set; }

#endregion

#region Constructor
Expand Down Expand Up @@ -203,6 +214,23 @@ public RequestBuilder SetParams(params (string, object)[] parameters)
return this;
}

/// <summary>
/// Sets the file to upload
/// </summary>
/// <param name="file">The file.</param>
/// <param name="filename">The filename.</param>
/// <returns></returns>
public RequestBuilder SetFile(Stream file, string filename)
{
if (file == null)
throw new ArgumentNullException(nameof(file));
if (string.IsNullOrWhiteSpace(filename))
throw new ArgumentNullException(nameof(filename));

File = (file, filename);
return this;
}

/// <summary>
/// Explicit query string parameters are always appended to the final URL, regardless the chosen HTTP method.
/// </summary>
Expand All @@ -226,17 +254,45 @@ public RequestBuilder SetExplicitQueryStringParams(params (string, object)[] par
/// Converts to HTTP POST request.
/// </summary>
/// <returns></returns>
public HttpRequestMessage ToPostRequest()
public async Task<HttpRequestMessage> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace SynologyDotNet.Core.Responses
/// <typeparam name="T"></typeparam>
/// <seealso cref="SynologyDotNet.Core.Responses.ApiResponse" />
/// <seealso cref="SynologyDotNet.Core.Responses.IApiListResponse" />
public class ApiListRessponse<T> : ApiResponse, IApiListResponse
public class ApiListResponse<T> : ApiResponse, IApiListResponse
where T : ListResponseBase
{
ListResponseBase IApiListResponse.Data => Data;
Expand Down
58 changes: 44 additions & 14 deletions SynologyDotNet.Core/SynoClient.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -264,7 +265,7 @@ public async Task<SynoSession> 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<LoginResult>(json);
Expand Down Expand Up @@ -384,12 +385,11 @@ public async Task<T> QueryListAsync<T>(string apiName, string method, int limit,
/// <param name="cancellationToken"></param>
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
public async Task<T> QueryListAsync<T>(string apiName, string method, int limit, int offset, CancellationToken cancellationToken, params (string, object)[] parameters)
public Task<T> QueryListAsync<T>(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<T>(req, cancellationToken).ConfigureAwait(false);
return result;
return QueryObjectAsync<T>(req, cancellationToken);
}

/// <summary>
Expand All @@ -403,6 +403,19 @@ public async Task<T> QueryListAsync<T>(string apiName, string method, int limit,
public async Task<T> QueryObjectAsync<T>(string apiName, string method, params (string, object)[] parameters)
=> await QueryObjectAsync<T>(apiName, method, CancellationToken.None, parameters).ConfigureAwait(false);

/// <summary>
/// Queries an entity from the specified endpoint with a file attached.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="apiName">Name of the API.</param>
/// <param name="method">The method.</param>
/// <param name="data">The stream.</param>
/// <param name="filename">The filename.</param>
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
public Task<T> QueryObjectAsync<T>(string apiName, string method, Stream data, string filename, params (string, object)[] parameters)
=> QueryObjectAsync<T>(apiName, method, CancellationToken.None, data, filename, parameters);

/// <summary>
/// Queries an entity from the specified endpoint.
/// </summary>
Expand All @@ -419,6 +432,23 @@ public async Task<T> QueryObjectAsync<T>(string apiName, string method, Cancella
return result;
}

/// <summary>
/// Queries an entity from the specified endpoint with a file attached
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="apiName">Name of the API.</param>
/// <param name="method">The method.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="data">The stream.</param>
/// <param name="filename">The filename.</param>
/// <param name="parameters">The parameters.</param>
/// <returns></returns>
public Task<T> QueryObjectAsync<T>(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<T>(req, cancellationToken);
}

/// <summary>
/// Queries image data from the specified endpoint.
/// </summary>
Expand Down Expand Up @@ -461,7 +491,7 @@ public async Task<string> QueryStringAsync(RequestBuilder req)
/// <returns></returns>
public async Task<string> 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);
Expand All @@ -486,7 +516,7 @@ public async Task<T> QueryObjectAsync<T>(RequestBuilder req)
/// <returns></returns>
public async Task<T> QueryObjectAsync<T>(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);
Expand All @@ -512,7 +542,7 @@ public async Task<ByteArrayData> QueryByteArrayAsync(RequestBuilder req)
/// <exception cref="SynologyDotNet.Core.Exceptions.SynoHttpException"></exception>
public async Task<ByteArrayData> 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()
Expand Down Expand Up @@ -546,7 +576,7 @@ public async Task QueryStreamAsync(RequestBuilder req, Action<StreamResult> read
/// <exception cref="NotSupportedException"></exception>
public async Task QueryStreamAsync(RequestBuilder req, Action<StreamResult> 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.");
Expand Down
2 changes: 1 addition & 1 deletion SynologyDotNet.Core/SynologyDotNet.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<PropertyGroup>
<Title>SynologyDotNet.Core</Title>
<Product>SynologyDotNet.Core</Product>
<Version>0.4.3</Version>
<Version>0.4.4</Version>
<Description>Base library to develop .NET clients for Synology DSM.</Description>
<RepositoryUrl>https://github.com/geloczigeri/synologydotnet-core</RepositoryUrl>
<Authors>Gergő Gelóczi</Authors>
Expand Down

0 comments on commit 32bbba8

Please sign in to comment.