From 49a38918fae19c094d704088ce91e84d2679f397 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 22 Jan 2019 20:43:55 +0200 Subject: [PATCH 01/37] initial 1.3.6 Version Added InstanceRunId to ServerStatus response. --- src/Dto/ServerStatusDto.cs | 2 ++ src/Mappers/ServerStatusMapper.cs | 3 ++- src/Model/ServerStatus.cs | 5 +++++ src/Properties/AssemblyInfo.cs | 6 +++--- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Dto/ServerStatusDto.cs b/src/Dto/ServerStatusDto.cs index 1766705..0dcf123 100644 --- a/src/Dto/ServerStatusDto.cs +++ b/src/Dto/ServerStatusDto.cs @@ -16,5 +16,7 @@ internal class ServerStatusDto public string StatusMessage { get; set; } [DataMember(Name = "version")] public string Version { get; set; } + [DataMember(Name = "instanceRunId")] + public string InstanceRunId { get; set; } } } diff --git a/src/Mappers/ServerStatusMapper.cs b/src/Mappers/ServerStatusMapper.cs index ab3e8f6..788f18b 100644 --- a/src/Mappers/ServerStatusMapper.cs +++ b/src/Mappers/ServerStatusMapper.cs @@ -16,7 +16,8 @@ public static ServerStatus MapFromDto(ServerStatusDto dto) { StatusMessage = dto.StatusMessage, Version = Version.Parse(dto.Version), - StatusCode = Parse(dto.StatusCode) + StatusCode = Parse(dto.StatusCode), + InstanceRunId = !String.IsNullOrWhiteSpace(dto.InstanceRunId)? Guid.Parse(dto.InstanceRunId) : new Guid?() }; } diff --git a/src/Model/ServerStatus.cs b/src/Model/ServerStatus.cs index b9bcb74..6563b19 100644 --- a/src/Model/ServerStatus.cs +++ b/src/Model/ServerStatus.cs @@ -20,6 +20,11 @@ public class ServerStatus /// Server version /// public Version Version { get; set; } + + /// + /// Server Instance RunId + /// + public Guid? InstanceRunId { get; set; } } diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index f101fa3..6c80c38 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("EasyMorph Inc.")] [assembly: AssemblyProduct("Morph.Server.Sdk")] -[assembly: AssemblyCopyright("Copyright © EasyMorph Inc. 2017-2018")] +[assembly: AssemblyCopyright("Copyright © EasyMorph Inc. 2017-2019")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.3.5.3")] -[assembly: AssemblyFileVersion("1.3.5.3")] +[assembly: AssemblyVersion("1.3.6.0")] +[assembly: AssemblyFileVersion("1.3.6.0")] From ac61ce572d0d9d867d343c2196b53d404bf89f24 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 23 Jan 2019 18:59:36 +0200 Subject: [PATCH 02/37] initial 1.4.0 multiple targets : net45, netstandard2.0 api client redesign in progress --- src/Client/MorphServerApiClient.cs | 426 ++++++++++++++++++++------ src/Helper/JsonSerializationHelper.cs | 7 +- src/Morph.Server.Sdk.csproj | 122 +------- 3 files changed, 335 insertions(+), 220 deletions(-) diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 6018b5a..dff6b01 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -20,9 +20,287 @@ using System.Linq; using Morph.Server.Sdk.Dto.Errors; using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; namespace Morph.Server.Sdk.Client { + + public class ApiResult + { + public T Data { get; set; } = default(T); + public Exception Error { get; set; } = default(Exception); + public bool IsSucceed { get { return Error == null; } } + public static ApiResult Fail(Exception exception) + { + return new ApiResult() + { + Data = default(T), + Error = exception + }; + } + + public static ApiResult Ok(T data) + { + return new ApiResult() + { + Data = data, + Error = null + }; + } + } + + public static class ApiSessionExtension + { + public static HeadersCollection ToHeadersCollection(this ApiSession apiSession) + { + var collection = new HeadersCollection(); + if (apiSession != null && !apiSession.IsAnonymous && !apiSession.IsClosed) + { + collection.Add(ApiSession.AuthHeaderName, apiSession.AuthToken); + } + return collection; + } + } + + + + public class HeadersCollection + { + private Dictionary _headers = new Dictionary(); + public HeadersCollection() + { + + } + + + + public void Add(string header, string value) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _headers[header] = value; + } + + public void Fill(HttpRequestHeaders reqestHeaders) + { + if (reqestHeaders == null) + { + throw new ArgumentNullException(nameof(reqestHeaders)); + } + foreach(var item in _headers) + { + reqestHeaders.Add(item.Key, item.Value); + } + } + } + + + public interface IApiClient + { + Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostAsync(string url,TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + } + + public sealed class NoContentResult + { + + } + + public sealed class NoContentRequest + { + + } + + public class MorphServerRestClient : IApiClient + { + private readonly HttpClient httpClient; + + public MorphServerRestClient(HttpClient httpClient) + { + this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + public Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Delete, url, null, urlParameters, headersCollection, cancellationToken); + } + + public Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + if(urlParameters == null) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return SendAsyncApiResult(HttpMethod.Get, url, null, urlParameters, headersCollection, cancellationToken); + } + + public Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Post, url, model, urlParameters, headersCollection, cancellationToken); + } + + public Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Put, url, model, urlParameters, headersCollection, cancellationToken); + } + + protected virtual async Task> SendAsyncApiResult(HttpMethod httpMethod, string path, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + StringContent stringContent = null; + if (model != null) + { + var serialized = JsonSerializationHelper.Serialize(model); + stringContent = new StringContent(serialized, Encoding.UTF8, "application/json"); + } + + var url = path + urlParameters.ToQueryString(); + var httpRequestMessage = BuildHttpRequestMessage(httpMethod, url, stringContent, headersCollection); + + using (var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var result = JsonSerializationHelper.Deserialize(content); + return ApiResult.Ok(result); + } + else + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); + } + } + } + + protected HttpRequestMessage BuildHttpRequestMessage(HttpMethod httpMethod, string url, HttpContent content, HeadersCollection headersCollection) + { + var requestMessage = new HttpRequestMessage() + { + Content = content, + Method = httpMethod, + RequestUri = new Uri(url, UriKind.Relative) + }; + if(headersCollection != null) + { + headersCollection.Fill(requestMessage.Headers); + } + return requestMessage; + } + + + + private static async Task BuildExceptionFromResponse(HttpResponseMessage response) + { + + var content = await response.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(content)) + { + ErrorResponse errorResponse = null; + try + { + errorResponse = JsonSerializationHelper.Deserialize(content); + } + catch (Exception) + { + return new ResponseParseException("An error occurred while deserializing the response", content); + } + if (errorResponse.error == null) + return new ResponseParseException("An error occurred while deserializing the response", content); + + switch (errorResponse.error.code) + { + case ReadableErrorTopCode.Conflict: return new MorphApiConflictException(errorResponse.error.message); + case ReadableErrorTopCode.NotFound: return new MorphApiNotFoundException(errorResponse.error.message); + case ReadableErrorTopCode.Forbidden: return new MorphApiForbiddenException(errorResponse.error.message); + case ReadableErrorTopCode.Unauthorized: return new MorphApiUnauthorizedException(errorResponse.error.message); + case ReadableErrorTopCode.BadArgument: return new MorphApiBadArgumentException(FieldErrorsMapper.MapFromDto(errorResponse.error), errorResponse.error.message); + default: return new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); + } + } + + else + { + switch (response.StatusCode) + { + case HttpStatusCode.Conflict: return new MorphApiConflictException(response.ReasonPhrase ?? "Conflict"); + case HttpStatusCode.NotFound: return new MorphApiNotFoundException(response.ReasonPhrase ?? "Not found"); + case HttpStatusCode.Forbidden: return new MorphApiForbiddenException(response.ReasonPhrase ?? "Forbidden"); + case HttpStatusCode.Unauthorized: return new MorphApiUnauthorizedException(response.ReasonPhrase ?? "Unauthorized"); + case HttpStatusCode.BadRequest: return new MorphClientGeneralException("Unknown", response.ReasonPhrase ?? "Unknown error"); + default: return new ResponseParseException(response.ReasonPhrase, null); + } + + } + } + + + } + + + + internal interface ILowLevelApiClient + { + // TASKS + Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); + Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); + Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + } + + internal class LowLevelApiClient : ILowLevelApiClient + { + private readonly IApiClient apiClient; + + public LowLevelApiClient(IApiClient apiClient) + { + this.apiClient = apiClient; + } + public Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "tasks", taskId.ToString("D")); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + { + throw new NotImplementedException(); + } + + public Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + /// /// Morph Server api client V1 /// @@ -34,6 +312,10 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable protected readonly string _api_v1 = "api/v1/"; + //private IApiClient apiClient; + private ILowLevelApiClient lowLevelApiClient; + + /// /// Construct Api client @@ -51,11 +333,23 @@ protected HttpClient GetHttpClient() { if (_httpClient == null) { +#if NETSTANDARD2_0 // handler will be disposed automatically HttpClientHandler aHandler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Automatic, + ServerCertificateCustomValidationCallback = new Func( + (request, certificate, chain, sslPolicyErrors) => true) + + }; +#elif NET45 + // handler will be disposed automatically + HttpClientHandler aHandler = new HttpClientHandler() { ClientCertificateOptions = ClientCertificateOption.Automatic + }; +#endif _httpClient = ConstructHttpClient(_apiHost, aHandler); } @@ -102,75 +396,6 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie - protected static async Task HandleResponse(HttpResponseMessage response) - { - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - var result = JsonSerializationHelper.Deserialize(content); - return result; - } - else - { - await HandleErrorResponse(response); - return default(T); - - } - - } - - protected async Task HandleResponse(HttpResponseMessage response) - { - if (!response.IsSuccessStatusCode) - { - await HandleErrorResponse(response); - } - - } - - private static async Task HandleErrorResponse(HttpResponseMessage response) - { - - var content = await response.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) - { - ErrorResponse errorResponse = null; - try - { - errorResponse = JsonSerializationHelper.Deserialize(content); - } - catch (Exception) - { - throw new ResponseParseException("An error occurred while deserializing the response", content); - } - if (errorResponse.error == null) - throw new ResponseParseException("An error occurred while deserializing the response", content); - - switch (errorResponse.error.code) - { - case ReadableErrorTopCode.Conflict: throw new MorphApiConflictException(errorResponse.error.message); - case ReadableErrorTopCode.NotFound: throw new MorphApiNotFoundException(errorResponse.error.message); - case ReadableErrorTopCode.Forbidden: throw new MorphApiForbiddenException(errorResponse.error.message); - case ReadableErrorTopCode.Unauthorized: throw new MorphApiUnauthorizedException(errorResponse.error.message); - case ReadableErrorTopCode.BadArgument: throw new MorphApiBadArgumentException(FieldErrorsMapper.MapFromDto(errorResponse.error), errorResponse.error.message); - default: throw new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); - } - } - - else - { - switch (response.StatusCode) - { - case HttpStatusCode.Conflict: throw new MorphApiConflictException(response.ReasonPhrase ?? "Conflict"); - case HttpStatusCode.NotFound: throw new MorphApiNotFoundException(response.ReasonPhrase ?? "Not found"); - case HttpStatusCode.Forbidden: throw new MorphApiForbiddenException(response.ReasonPhrase ?? "Forbidden"); - case HttpStatusCode.Unauthorized: throw new MorphApiUnauthorizedException(response.ReasonPhrase ?? "Unauthorized"); - case HttpStatusCode.BadRequest: throw new MorphClientGeneralException("Unknown", response.ReasonPhrase ?? "Unknown error"); - default: throw new ResponseParseException(response.ReasonPhrase, null); - } - - } - } /// /// Start Task like "fire and forget" @@ -186,7 +411,7 @@ public async Task StartTaskAsync(ApiSession apiSession, Guid { throw new ArgumentNullException(nameof(apiSession)); } - + var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); var dto = new TaskStartRequestDto(); @@ -194,7 +419,11 @@ public async Task StartTaskAsync(ApiSession apiSession, Guid { dto.TaskParameters = taskParameters.Select(TaskParameterMapper.ToDto).ToList(); } - var request = JsonSerializationHelper.SerializeAsStringContent(dto); + var result = await apiClient.PostAsync(url, dto, new NameValueCollection(), apiSession.ToHeadersCollection(), cancellationToken); + + + + using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, request, apiSession), cancellationToken)) { var info = await HandleResponse(response); @@ -203,6 +432,25 @@ public async Task StartTaskAsync(ApiSession apiSession, Guid } + protected Task WrappedShort(Func> fun, CancellationToken orginalCancellationToken) + { + return fun(orginalCancellationToken); + } + + protected TDataModel MapOrFail(ApiResult apiResult, Func maper) + { + if (apiResult.IsSucceed) + { + return maper(apiResult.Data); + } + else + { + throw apiResult.Error; + } + } + + + /// /// Gets status of the task (Running/Not running) and payload /// @@ -221,7 +469,7 @@ private async Task GetRunningTaskStatusAsync(ApiSession apiSe var nvc = new NameValueCollection(); nvc.Add("_", DateTime.Now.Ticks.ToString()); var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D")) + nvc.ToQueryString(); - + using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) { var info = await HandleResponse(response); @@ -237,23 +485,19 @@ private async Task GetRunningTaskStatusAsync(ApiSession apiSe /// task guid /// cancellation token /// Returns task status - public async Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - var spaceName = apiSession.SpaceName; - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = UrlHelper.JoinUrl("space", spaceName, "tasks", taskId.ToString("D")) + nvc.ToQueryString(); - - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) + return WrappedShort(async (token) => { - var dto = await HandleResponse(response); - var data = TaskStatusMapper.MapFromDto(dto); - return data; - } + var apiResult = await lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); + + },cancellationToken); + } /// @@ -471,21 +715,7 @@ public async Task UploadFileAsync(ApiSession apiSession, string localFilePath, s } - protected HttpRequestMessage BuildHttpRequestMessage(HttpMethod httpMethod, string url, HttpContent content, ApiSession apiSession) - { - var requestMessage = new HttpRequestMessage() - { - Content = content, - Method = httpMethod, - RequestUri = new Uri(url, UriKind.Relative) - }; - if (apiSession != null && !apiSession.IsAnonymous && !apiSession.IsClosed) - { - requestMessage.Headers.Add(ApiSession.AuthHeaderName, apiSession.AuthToken); - } - return requestMessage; - } - + /// /// Upload file stream to the server diff --git a/src/Helper/JsonSerializationHelper.cs b/src/Helper/JsonSerializationHelper.cs index d2ab970..d125dcd 100644 --- a/src/Helper/JsonSerializationHelper.cs +++ b/src/Helper/JsonSerializationHelper.cs @@ -43,11 +43,6 @@ public static string Serialize(T obj) } - public static StringContent SerializeAsStringContent(T obj) - { - var serialized = Serialize(obj); - var result = new StringContent(serialized, Encoding.UTF8, "application/json"); - return result; - } + } } diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index b39354d..d089de4 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -1,123 +1,13 @@ - - - + - Debug - AnyCPU - {72ECC66F-62FE-463F-AFAD-E1FF5CC19CD9} - Library - Properties - Morph.Server.Sdk - Morph.Server.Sdk - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\Morph.Server.Sdk.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\Morph.Server.Sdk.xml - - - true + netstandard2.0;net45 - Morph.Server.Sdk.snk + false - - - - False - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - \ No newline at end of file From ee2a8c1fef44b174f850978b411d12ea1face513 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 24 Jan 2019 17:34:40 +0200 Subject: [PATCH 03/37] code refactoring - work in progress --- src/Client/ApiResult.cs | 33 ++ src/Client/ApiSessionExtension.cs | 22 + src/Client/HeadersCollection.cs | 49 ++ src/Client/IMorphServerApiClient.cs | 2 +- src/Client/LowLevelApiClient.cs | 248 +++++++++ src/Client/MorphServerApiClient.cs | 836 ++++++++++------------------ src/Client/MorphServerRestClient.cs | 164 ++++++ src/Model/ApiSession.cs | 7 +- 8 files changed, 822 insertions(+), 539 deletions(-) create mode 100644 src/Client/ApiResult.cs create mode 100644 src/Client/ApiSessionExtension.cs create mode 100644 src/Client/HeadersCollection.cs create mode 100644 src/Client/LowLevelApiClient.cs create mode 100644 src/Client/MorphServerRestClient.cs diff --git a/src/Client/ApiResult.cs b/src/Client/ApiResult.cs new file mode 100644 index 0000000..143e843 --- /dev/null +++ b/src/Client/ApiResult.cs @@ -0,0 +1,33 @@ +using System; + +namespace Morph.Server.Sdk.Client +{ + public class ApiResult + { + public T Data { get; set; } = default(T); + public Exception Error { get; set; } = default(Exception); + public bool IsSucceed { get { return Error == null; } } + public static ApiResult Fail(Exception exception) + { + return new ApiResult() + { + Data = default(T), + Error = exception + }; + } + + public static ApiResult Ok(T data) + { + return new ApiResult() + { + Data = data, + Error = null + }; + } + } + + + } + + +} diff --git a/src/Client/ApiSessionExtension.cs b/src/Client/ApiSessionExtension.cs new file mode 100644 index 0000000..346bda7 --- /dev/null +++ b/src/Client/ApiSessionExtension.cs @@ -0,0 +1,22 @@ +using Morph.Server.Sdk.Model; + +namespace Morph.Server.Sdk.Client +{ + public static class ApiSessionExtension + { + public static HeadersCollection ToHeadersCollection(this ApiSession apiSession) + { + var collection = new HeadersCollection(); + if (apiSession != null && !apiSession.IsAnonymous && !apiSession.IsClosed) + { + collection.Add(ApiSession.AuthHeaderName, apiSession.AuthToken); + } + return collection; + } + } + + + } + + +} diff --git a/src/Client/HeadersCollection.cs b/src/Client/HeadersCollection.cs new file mode 100644 index 0000000..2a44aaa --- /dev/null +++ b/src/Client/HeadersCollection.cs @@ -0,0 +1,49 @@ +using System; +using System.Net.Http.Headers; +using System.Collections.Generic; + +namespace Morph.Server.Sdk.Client +{ + public class HeadersCollection + { + private Dictionary _headers = new Dictionary(); + public HeadersCollection() + { + + } + + + + public void Add(string header, string value) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _headers[header] = value; + } + + public void Fill(HttpRequestHeaders reqestHeaders) + { + if (reqestHeaders == null) + { + throw new ArgumentNullException(nameof(reqestHeaders)); + } + foreach (var item in _headers) + { + reqestHeaders.Add(item.Key, item.Value); + } + } + } + + + } + + +} diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index b2392ac..9860f29 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -9,7 +9,7 @@ namespace Morph.Server.Sdk.Client { - public interface IMorphServerApiClient + public interface IMorphServerApiClient:IDisposable { event EventHandler FileProgress; diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs new file mode 100644 index 0000000..d9ff432 --- /dev/null +++ b/src/Client/LowLevelApiClient.cs @@ -0,0 +1,248 @@ +using Morph.Server.Sdk.Dto; +using Morph.Server.Sdk.Model; +using System; +using System.Threading; +using System.Threading.Tasks; +using Morph.Server.Sdk.Dto.Commands; +using Morph.Server.Sdk.Mappers; +using System.Linq; +using System.Collections.Generic; +using System.IO; + +namespace Morph.Server.Sdk.Client +{ + + + + internal interface ILowLevelApiClient + { + // TASKS + Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); + Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + + // RUN-STOP Task + Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); + Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + + // Tasks validation + Task> ValidateTasksAsync(ApiSession apiSession, ValidateTasksRequestDto validateTasksRequestDto, CancellationToken cancellationToken); + + + // Auth and sessions + Task> AuthLogoutAsync(ApiSession apiSession, CancellationToken cancellationToken); + Task> AuthLoginPasswordAsync(LoginRequestDto loginRequestDto, CancellationToken cancellationToken); + Task> AuthGenerateNonce(CancellationToken cancellationToken); + + + // Server interaction + Task> ServerGetStatusAsync(CancellationToken cancellationToken); + + + // spaces + + Task> SpacesGetListAsync(CancellationToken cancellationToken); + Task> SpacesGetSpaceStatusAsync(ApiSession apiSession, string spaceName, CancellationToken cancellationToken); + + // WEB FILES + Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); + Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + + } + + + public interface IWebFilesLowLevelApiClient + { + + + Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken); + Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken); + + + + Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); + Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false); + Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); + + + } + + + + internal class LowLevelApiClient : ILowLevelApiClient + { + private readonly IApiClient apiClient; + + public LowLevelApiClient(IApiClient apiClient) + { + this.apiClient = apiClient; + } + + public Task> AuthLogoutAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + var url = "auth/logout"; + return apiClient.PostAsync(url, null, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D")); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> SpacesGetListAsync(CancellationToken cancellationToken) + { + var url = "spaces/list"; + return apiClient.GetAsync(url, null, new HeadersCollection(), cancellationToken); + + } + + public Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var url = UrlHelper.JoinUrl("space", apiSession.SpaceName, "tasks", taskId.ToString("D")); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var url = UrlHelper.JoinUrl("space", apiSession.SpaceName, "tasks"); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + + public Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "tasks", taskId.ToString("D")); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> ServerGetStatusAsync(CancellationToken cancellationToken) + { + var url = "server/status"; + return apiClient.GetAsync(url, null, new HeadersCollection(), cancellationToken); + } + + public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); + var dto = new TaskStartRequestDto(); + if (taskParameters != null) + { + dto.TaskParameters = taskParameters.Select(TaskParameterMapper.ToDto).ToList(); + } + + return apiClient.PostAsync(url, dto, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D")); + return apiClient.DeleteAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> ValidateTasksAsync(ApiSession apiSession, ValidateTasksRequestDto validateTasksRequestDto, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = "commands/validatetasks"; + + return apiClient.PostAsync(url, validateTasksRequestDto, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> SpacesGetSpaceStatusAsync(ApiSession apiSession, string spaceName, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + if (spaceName == null) + { + throw new ArgumentNullException(nameof(spaceName)); + } + + spaceName = spaceName.Trim(); + var url = UrlHelper.JoinUrl("spaces", spaceName, "status"); + + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) + { + var spaceName = apiSession.SpaceName; + + var url = UrlHelper.JoinUrl("space", spaceName, "browse", folderPath); + return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + { + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder, fileName); + + return apiClient.DeleteAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } + + public Task> AuthLoginPasswordAsync(LoginRequestDto loginRequestDto, CancellationToken cancellationToken) + { + var url = "auth/login"; + return apiClient.PostAsync(url, loginRequestDto, null, new HeadersCollection(), cancellationToken); + } + + public Task> AuthGenerateNonce(CancellationToken cancellationToken) + { + var url = "auth/nonce"; + return apiClient.PostAsync(url, new GenerateNonceRequestDto(), null, new HeadersCollection(), cancellationToken); + } + + } +} + + + diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index dff6b01..54d4b3c 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -26,279 +26,173 @@ namespace Morph.Server.Sdk.Client { - public class ApiResult + + + public class MorphServerAuthenticator { - public T Data { get; set; } = default(T); - public Exception Error { get; set; } = default(Exception); - public bool IsSucceed { get { return Error == null; } } - public static ApiResult Fail(Exception exception) - { - return new ApiResult() - { - Data = default(T), - Error = exception - }; - } + private readonly IApiClient apiClient; + private readonly IMorphServerApiClient morphServerApiClient; - public static ApiResult Ok(T data) + public MorphServerAuthenticator(IApiClient apiClient, IMorphServerApiClient morphServerApiClient) { - return new ApiResult() - { - Data = data, - Error = null - }; + this.apiClient = apiClient; + this.morphServerApiClient = morphServerApiClient; } - } - public static class ApiSessionExtension - { - public static HeadersCollection ToHeadersCollection(this ApiSession apiSession) + + public async Task OpenSessionAsync(SpaceEnumerationItem desiredSpace, OpenSessionRequest openSessionRequest, CancellationToken cancellationToken) { - var collection = new HeadersCollection(); - if (apiSession != null && !apiSession.IsAnonymous && !apiSession.IsClosed) + // space access restriction is supported since server 3.9.2 + // for previous versions api will return SpaceAccessRestriction.NotSupported + // a special fall-back mechanize need to be used to open session in such case + switch (desiredSpace.SpaceAccessRestriction) { - collection.Add(ApiSession.AuthHeaderName, apiSession.AuthToken); - } - return collection; - } - } + // anon space + case SpaceAccessRestriction.None: + return ApiSession.Anonymous(openSessionRequest.SpaceName); + // password protected space + case SpaceAccessRestriction.BasicPassword: + return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + // windows authentication + case SpaceAccessRestriction.WindowsAuthentication: + return await OpenSessionViaWindowsAuthenticationAsync(openSessionRequest.SpaceName, cancellationToken); - public class HeadersCollection - { - private Dictionary _headers = new Dictionary(); - public HeadersCollection() - { - - } + // fallback + case SpaceAccessRestriction.NotSupported: - + // if space is public or password is not set - open anon session + if (desiredSpace.IsPublic || string.IsNullOrWhiteSpace(openSessionRequest.Password)) + { + return ApiSession.Anonymous(openSessionRequest.SpaceName); + } + // otherwise open session via space password + else + { + return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + } - public void Add(string header, string value) - { - if (header == null) - { - throw new ArgumentNullException(nameof(header)); + default: + throw new Exception("Space access restriction method is not supported by this client."); } - - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _headers[header] = value; } - public void Fill(HttpRequestHeaders reqestHeaders) + + protected async Task OpenSessionViaWindowsAuthenticationAsync(string spaceName, CancellationToken cancellationToken) { - if (reqestHeaders == null) + if (string.IsNullOrEmpty(spaceName)) { - throw new ArgumentNullException(nameof(reqestHeaders)); + throw new ArgumentException("Space name is not set", nameof(spaceName)); } - foreach(var item in _headers) + // handler will be disposed automatically + HttpClientHandler aHandler = new HttpClientHandler() { - reqestHeaders.Add(item.Key, item.Value); - } - } - } - - - public interface IApiClient - { - Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostAsync(string url,TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - } - - public sealed class NoContentResult - { - - } - - public sealed class NoContentRequest - { - - } + ClientCertificateOptions = ClientCertificateOption.Automatic, + UseDefaultCredentials = true + }; - public class MorphServerRestClient : IApiClient - { - private readonly HttpClient httpClient; + using (var httpClient = ConstructHttpClient(_apiHost, aHandler)) + { - public MorphServerRestClient(HttpClient httpClient) - { - this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); - } - public Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - return SendAsyncApiResult(HttpMethod.Delete, url, null, urlParameters, headersCollection, cancellationToken); - } + var serverNonce = await internalGetAuthNonceAsync(httpClient, cancellationToken); + var token = await internalAuthExternalWindowAsync(httpClient, spaceName, serverNonce, cancellationToken); - public Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - if(urlParameters == null) - { - urlParameters = new NameValueCollection(); + return new ApiSession(morphServerApiClient) + { + AuthToken = token, + IsAnonymous = false, + IsClosed = false, + SpaceName = spaceName + }; } - urlParameters.Add("_", DateTime.Now.Ticks.ToString()); - return SendAsyncApiResult(HttpMethod.Get, url, null, urlParameters, headersCollection, cancellationToken); } - - public Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + protected static async Task internalGetAuthNonceAsync(HttpClient httpClient, CancellationToken cancellationToken) { - return SendAsyncApiResult(HttpMethod.Post, url, model, urlParameters, headersCollection, cancellationToken); - } + var url = "auth/nonce"; + using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(new GenerateNonceRequestDto()), cancellationToken)) + { + var dto = await HandleResponse(response); + return dto.Nonce; - public Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - return SendAsyncApiResult(HttpMethod.Put, url, model, urlParameters, headersCollection, cancellationToken); + } } - protected virtual async Task> SendAsyncApiResult(HttpMethod httpMethod, string path, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + protected static async Task internalAuthExternalWindowAsync(HttpClient httpClient, string spaceName, string serverNonce, CancellationToken cancellationToken) { - StringContent stringContent = null; - if (model != null) + var url = "auth/external/windows"; + var requestDto = new WindowsExternalLoginRequestDto { - var serialized = JsonSerializationHelper.Serialize(model); - stringContent = new StringContent(serialized, Encoding.UTF8, "application/json"); - } - - var url = path + urlParameters.ToQueryString(); - var httpRequestMessage = BuildHttpRequestMessage(httpMethod, url, stringContent, headersCollection); + RequestToken = serverNonce, + SpaceName = spaceName + }; - using (var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(requestDto), cancellationToken)) { - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - var result = JsonSerializationHelper.Deserialize(content); - return ApiResult.Ok(result); - } - else - { - var error = await BuildExceptionFromResponse(response); - return ApiResult.Fail(error); - } + var responseDto = await HandleResponse(response); + return responseDto.Token; } } - protected HttpRequestMessage BuildHttpRequestMessage(HttpMethod httpMethod, string url, HttpContent content, HeadersCollection headersCollection) + protected async Task internalAuthLoginAsync(string clientNonce, string serverNonce, string spaceName, string passwordHash, CancellationToken cancellationToken) { - var requestMessage = new HttpRequestMessage() + + var requestDto = new LoginRequestDto { - Content = content, - Method = httpMethod, - RequestUri = new Uri(url, UriKind.Relative) + ClientSeed = clientNonce, + Password = passwordHash, + Provider = "Space", + UserName = spaceName, + RequestToken = serverNonce }; - if(headersCollection != null) - { - headersCollection.Fill(requestMessage.Headers); - } - return requestMessage; - } - + var apiResult = await lowLevelApiClient.AuthLoginPasswordAsync(requestDto, cancellationToken); + FailIfError(apiResult); + return apiResult.Data.Token; + } - private static async Task BuildExceptionFromResponse(HttpResponseMessage response) + /// + /// Open a new authenticated session via password + /// + /// space name + /// space password + /// + /// + public async Task OpenSessionViaSpacePasswordAsync(string spaceName, string password, CancellationToken cancellationToken) { - - var content = await response.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) + if (string.IsNullOrEmpty(spaceName)) { - ErrorResponse errorResponse = null; - try - { - errorResponse = JsonSerializationHelper.Deserialize(content); - } - catch (Exception) - { - return new ResponseParseException("An error occurred while deserializing the response", content); - } - if (errorResponse.error == null) - return new ResponseParseException("An error occurred while deserializing the response", content); - - switch (errorResponse.error.code) - { - case ReadableErrorTopCode.Conflict: return new MorphApiConflictException(errorResponse.error.message); - case ReadableErrorTopCode.NotFound: return new MorphApiNotFoundException(errorResponse.error.message); - case ReadableErrorTopCode.Forbidden: return new MorphApiForbiddenException(errorResponse.error.message); - case ReadableErrorTopCode.Unauthorized: return new MorphApiUnauthorizedException(errorResponse.error.message); - case ReadableErrorTopCode.BadArgument: return new MorphApiBadArgumentException(FieldErrorsMapper.MapFromDto(errorResponse.error), errorResponse.error.message); - default: return new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); - } + throw new ArgumentException("Space name is not set.", nameof(spaceName)); } - else + if (password == null) { - switch (response.StatusCode) - { - case HttpStatusCode.Conflict: return new MorphApiConflictException(response.ReasonPhrase ?? "Conflict"); - case HttpStatusCode.NotFound: return new MorphApiNotFoundException(response.ReasonPhrase ?? "Not found"); - case HttpStatusCode.Forbidden: return new MorphApiForbiddenException(response.ReasonPhrase ?? "Forbidden"); - case HttpStatusCode.Unauthorized: return new MorphApiUnauthorizedException(response.ReasonPhrase ?? "Unauthorized"); - case HttpStatusCode.BadRequest: return new MorphClientGeneralException("Unknown", response.ReasonPhrase ?? "Unknown error"); - default: return new ResponseParseException(response.ReasonPhrase, null); - } - + throw new ArgumentNullException(nameof(password)); } - } - - - } - + var passwordHash = CryptographyHelper.CalculateSha256HEX(password); + var serverNonce = await internalGetAuthNonceAsync(GetHttpClient(), cancellationToken); + var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); + var all = passwordHash + serverNonce + clientNonce; + var allHash = CryptographyHelper.CalculateSha256HEX(all); - internal interface ILowLevelApiClient - { - // TASKS - Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); - Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); - Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - } - internal class LowLevelApiClient : ILowLevelApiClient - { - private readonly IApiClient apiClient; + var token = await internalAuthLoginAsync(clientNonce, serverNonce, spaceName, allHash, cancellationToken); - public LowLevelApiClient(IApiClient apiClient) - { - this.apiClient = apiClient; - } - public Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); + return new ApiSession(this) + { + AuthToken = token, + IsAnonymous = false, + IsClosed = false, + SpaceName = spaceName + }; } - public Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - public Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "tasks", taskId.ToString("D")); - return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); - } + } - public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) - { - throw new NotImplementedException(); - } - public Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } /// @@ -315,7 +209,8 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable //private IApiClient apiClient; private ILowLevelApiClient lowLevelApiClient; - + public TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); /// /// Construct Api client @@ -333,7 +228,7 @@ protected HttpClient GetHttpClient() { if (_httpClient == null) { -#if NETSTANDARD2_0 +#if NETSTANDARD2_0 // handler will be disposed automatically HttpClientHandler aHandler = new HttpClientHandler() { @@ -349,11 +244,13 @@ protected HttpClient GetHttpClient() ClientCertificateOptions = ClientCertificateOption.Automatic }; -#endif +#endif _httpClient = ConstructHttpClient(_apiHost, aHandler); } return _httpClient; + + } @@ -405,38 +302,49 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie /// /// /// - public async Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); - var dto = new TaskStartRequestDto(); - if (taskParameters != null) + return Wrapped(async (token) => { - dto.TaskParameters = taskParameters.Select(TaskParameterMapper.ToDto).ToList(); - } - var result = await apiClient.PostAsync(url, dto, new NameValueCollection(), apiSession.ToHeadersCollection(), cancellationToken); - + var apiResult = await lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + }, cancellationToken, OperationTimeout); + } - - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, request, apiSession), cancellationToken)) + protected Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, TimeSpan maxExecutionTime) + { + using (var derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken)) { - var info = await HandleResponse(response); - return RunningTaskStatusMapper.RunningTaskStatusFromDto(info); - } + derTokenSource.CancelAfter(maxExecutionTime); + try + { + return fun(derTokenSource.Token); + } + catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) + { + throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({maxExecutionTime})"); + } + + } } - protected Task WrappedShort(Func> fun, CancellationToken orginalCancellationToken) + + protected void FailIfError(ApiResult apiResult) { - return fun(orginalCancellationToken); + if (!apiResult.IsSucceed) + { + throw apiResult.Error; + } } + + protected TDataModel MapOrFail(ApiResult apiResult, Func maper) { if (apiResult.IsSucceed) @@ -450,6 +358,33 @@ protected TDataModel MapOrFail(ApiResult apiResult, Func } + /// + /// Close opened session + /// + /// api session + /// + /// + public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + if (apiSession.IsClosed) + return Task.FromResult(0); + if (apiSession.IsAnonymous) + return Task.FromResult(0); + + return Wrapped(async (token) => + { + var apiResult = await lowLevelApiClient.AuthLogoutAsync(apiSession, token); + // if task fail - do nothing. server will close this session after inactivity period + return Task.FromResult(0); + + }, cancellationToken, OperationTimeout); + + } + /// /// Gets status of the task (Running/Not running) and payload @@ -458,23 +393,20 @@ protected TDataModel MapOrFail(ApiResult apiResult, Func /// task guid /// cancellation token /// Returns task status - private async Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + private Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - var spaceName = apiSession.SpaceName; - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D")) + nvc.ToQueryString(); - - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { - var info = await HandleResponse(response); - return RunningTaskStatusMapper.RunningTaskStatusFromDto(info); - } + var apiResult = await lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + + }, cancellationToken, OperationTimeout); + } @@ -485,21 +417,46 @@ private async Task GetRunningTaskStatusAsync(ApiSession apiSe /// task guid /// cancellation token /// Returns task status - public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - return WrappedShort(async (token) => + return Wrapped(async (token) => { var apiResult = await lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); - },cancellationToken); + }, cancellationToken, OperationTimeout); + + } + + + /// + /// Retrieves space status + /// + /// api session + /// + /// + public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + return Wrapped(async (token) => + { + var apiResult = await lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); + return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); + + }, cancellationToken, OperationTimeout); } + + /// /// Stops the Task /// @@ -514,34 +471,38 @@ public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, Cancellation throw new ArgumentNullException(nameof(apiSession)); } - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D")); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Delete, url, null, apiSession), cancellationToken)) + await Wrapped(async (token) => { - await HandleResponse(response); - } + var apiResult = await lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); + FailIfError(apiResult); + return Task.FromResult(0); + + }, cancellationToken, OperationTimeout); + } /// /// Returns server status. May raise exception if server is unreachable /// /// - public async Task GetServerStatusAsync(CancellationToken cancellationToken) + public Task GetServerStatusAsync(CancellationToken cancellationToken) { - return await GetDataWithCancelAfter(async (token) => + return Wrapped(async (token) => { - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); + var apiResult = await lowLevelApiClient.ServerGetStatusAsync(token); + return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); - var url = "server/status" + nvc.ToQueryString(); - using (var response = await GetHttpClient().GetAsync(url, token)) - { - var dto = await HandleResponse(response); - var result = ServerStatusMapper.MapFromDto(dto); - return result; + }, cancellationToken, OperationTimeout); + } - } - }, TimeSpan.FromSeconds(20), cancellationToken); + public Task GetSpacesListAsync(CancellationToken cancellationToken) + { + return Wrapped(async (token) => + { + var apiResult = await lowLevelApiClient.SpacesGetListAsync(token); + return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + + }, cancellationToken, OperationTimeout); } /// @@ -715,7 +676,7 @@ public async Task UploadFileAsync(ApiSession apiSession, string localFilePath, s } - + /// /// Upload file stream to the server @@ -783,38 +744,24 @@ private void DownloadProgress_StateChanged(object sender, FileEventArgs e) } - protected async Task GetDataWithCancelAfter(Func> action, TimeSpan timeout, CancellationToken cancellationToken) - { - using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - { - linkedTokenSource.CancelAfter(timeout); - try - { - return await action(linkedTokenSource.Token); - } + //protected async Task GetDataWithCancelAfter(Func> action, TimeSpan timeout, CancellationToken cancellationToken) + //{ + // using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + // { + // linkedTokenSource.CancelAfter(timeout); + // try + // { + // return await action(linkedTokenSource.Token); + // } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) - { - throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({timeout})"); - } - } - } + // catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) + // { + // throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({timeout})"); + // } + // } + //} - public async Task GetSpacesListAsync(CancellationToken cancellationToken) - { - return await GetDataWithCancelAfter(async (token) => - { - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = "spaces/list" + nvc.ToQueryString(); - using (var response = await GetHttpClient().GetAsync(url, token)) - { - var dto = await HandleResponse(response); - return SpacesEnumerationMapper.MapFromDto(dto); - } - }, TimeSpan.FromSeconds(20), cancellationToken); - } /// @@ -824,26 +771,22 @@ public async Task GetSpacesListAsync(CancellationToken ca /// folder path like /path/to/folder /// /// - public async Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) + public Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - var spaceName = apiSession.SpaceName; - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - - var url = UrlHelper.JoinUrl("space", spaceName, "browse", folderPath) + nvc.ToQueryString(); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { - var dto = await HandleResponse(response); - return SpaceBrowsingMapper.MapFromDto(dto); + var apiResult = await lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, folderPath, token); + return MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); - } + }, cancellationToken, OperationTimeout); } + /// /// Checks if file exists /// @@ -852,7 +795,7 @@ public async Task BrowseSpaceAsync(ApiSession apiSession, str /// file name /// /// Returns true if file exists. - public async Task FileExistsAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + public Task FileExistsAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) { if (apiSession == null) { @@ -860,10 +803,17 @@ public async Task FileExistsAsync(ApiSession apiSession, string serverFold } if (string.IsNullOrWhiteSpace(fileName)) + { throw new ArgumentException(nameof(fileName)); - var browseResult = await this.BrowseSpaceAsync(apiSession, serverFolder, cancellationToken); + } + + return Wrapped(async (token) => + { + var apiResult = await lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, serverFolder, token); + var browseResult = MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); + return browseResult.FileExists(fileName); - return browseResult.FileExists(fileName); + }, cancellationToken, OperationTimeout); } @@ -878,48 +828,26 @@ public async Task FileExistsAsync(ApiSession apiSession, string serverFold /// file name /// /// - public async Task DeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + public Task DeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder, fileName); - - using (HttpResponseMessage response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Delete, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { - await HandleResponse(response); - } + var apiResult = await lowLevelApiClient.WebFilesDeleteFileAsync(apiSession, serverFolder, fileName, token); + FailIfError(apiResult); + return Task.FromResult(0); - } + }, cancellationToken, OperationTimeout); + } - /// - /// Retrieves space status - /// - /// api session - /// - /// - public async Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("spaces", spaceName, "status"); - using (HttpResponseMessage response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) - { - var dto = await HandleResponse(response); - var entity = SpaceStatusMapper.MapFromDto(dto); - return entity; - } - } /// @@ -929,7 +857,7 @@ public async Task GetSpaceStatusAsync(ApiSession apiSession, Cancel /// project path like /path/to/project.morph /// /// - public async Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) + public Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -937,101 +865,34 @@ public async Task ValidateTasksAsync(ApiSession apiSession, } if (string.IsNullOrWhiteSpace(projectPath)) - throw new ArgumentException(nameof(projectPath)); - var spaceName = apiSession.SpaceName; - var url = "commands/validatetasks"; - var request = new ValidateTasksRequestDto { - SpaceName = spaceName, - ProjectPath = projectPath - }; - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, JsonSerializationHelper.SerializeAsStringContent(request), apiSession), cancellationToken)) - { - - var dto = await HandleResponse(response); - var entity = ValidateTasksResponseMapper.MapFromDto(dto); - return entity; - + throw new ArgumentException("projectPath is empty", nameof(projectPath)); } - } - - - - protected static async Task internalGetAuthNonceAsync(HttpClient httpClient, CancellationToken cancellationToken) - { - var url = "auth/nonce"; - using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(new GenerateNonceRequestDto()), cancellationToken)) + return Wrapped(async (token) => { - var dto = await HandleResponse(response); - return dto.Nonce; - - } - } + var request = new ValidateTasksRequestDto + { + SpaceName = apiSession.SpaceName, + ProjectPath = projectPath + }; + var apiResult = await lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); + return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); - protected async Task internalAuthLoginAsync(string clientNonce, string serverNonce, string spaceName, string passwordHash, CancellationToken cancellationToken) - { - var url = "auth/login"; - var requestDto = new LoginRequestDto - { - ClientSeed = clientNonce, - Password = passwordHash, - Provider = "Space", - UserName = spaceName, - RequestToken = serverNonce - }; + }, cancellationToken, OperationTimeout); - using (var response = await GetHttpClient().PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(requestDto), cancellationToken)) - { - var responseDto = await HandleResponse(response); - return responseDto.Token; - } } - protected static async Task internalAuthExternalWindowAsync(HttpClient httpClient, string spaceName, string serverNonce, CancellationToken cancellationToken) - { - var url = "auth/external/windows"; - var requestDto = new WindowsExternalLoginRequestDto - { - RequestToken = serverNonce, - SpaceName = spaceName - }; - using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(requestDto), cancellationToken)) - { - var responseDto = await HandleResponse(response); - return responseDto.Token; - } - } + - protected async Task OpenSessionViaWindowsAuthenticationAsync(string spaceName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(spaceName)) - { - throw new ArgumentException("Space name is not set", nameof(spaceName)); - } - // handler will be disposed automatically - HttpClientHandler aHandler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Automatic, - UseDefaultCredentials = true - }; - using (var httpClient = ConstructHttpClient(_apiHost, aHandler)) - { + - var serverNonce = await internalGetAuthNonceAsync(httpClient, cancellationToken); - var token = await internalAuthExternalWindowAsync(httpClient, spaceName, serverNonce, cancellationToken); + - return new ApiSession(this) - { - AuthToken = token, - IsAnonymous = false, - IsClosed = false, - SpaceName = spaceName - }; - } - } + + /// @@ -1059,46 +920,16 @@ public async Task OpenSessionAsync(OpenSessionRequest openSessionReq var cancellationToken = linkedTokenSource.Token; try { - var spacesListResult = await GetSpacesListAsync(cancellationToken); + var spacesListApiResult = await lowLevelApiClient.SpacesGetListAsync(cancellationToken); + var spacesListResult = MapOrFail(spacesListApiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + var desiredSpace = spacesListResult.Items.FirstOrDefault(x => x.SpaceName.Equals(openSessionRequest.SpaceName, StringComparison.OrdinalIgnoreCase)); if (desiredSpace == null) { throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); } - // space access restriction is supported since server 3.9.2 - // for previous versions api will return SpaceAccessRestriction.NotSupported - // a special fall-back mechanize need to be used to open session in such case - switch (desiredSpace.SpaceAccessRestriction) - { - // anon space - case SpaceAccessRestriction.None: - return ApiSession.Anonymous(openSessionRequest.SpaceName); - - // password protected space - case SpaceAccessRestriction.BasicPassword: - return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); - - // windows authentication - case SpaceAccessRestriction.WindowsAuthentication: - return await OpenSessionViaWindowsAuthenticationAsync(openSessionRequest.SpaceName, cancellationToken); - - // fallback - case SpaceAccessRestriction.NotSupported: - - // if space is public or password is not set - open anon session - if (desiredSpace.IsPublic || string.IsNullOrWhiteSpace(openSessionRequest.Password)) - { - return ApiSession.Anonymous(openSessionRequest.SpaceName); - } - // otherwise open session via space password - else - { - return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); - } - default: - throw new Exception("Space access restriction method is not supported by this client."); - } + } catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) { @@ -1108,95 +939,28 @@ public async Task OpenSessionAsync(OpenSessionRequest openSessionReq } - /// - /// Open a new authenticated session via password - /// - /// space name - /// space password - /// - /// - public async Task OpenSessionViaSpacePasswordAsync(string spaceName, string password, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(spaceName)) - { - throw new ArgumentException("Space name is not set.", nameof(spaceName)); - } - - if (password == null) - { - throw new ArgumentNullException(nameof(password)); - } - - var passwordHash = CryptographyHelper.CalculateSha256HEX(password); - var serverNonce = await internalGetAuthNonceAsync(GetHttpClient(), cancellationToken); - var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); - var all = passwordHash + serverNonce + clientNonce; - var allHash = CryptographyHelper.CalculateSha256HEX(all); - - - var token = await internalAuthLoginAsync(clientNonce, serverNonce, spaceName, allHash, cancellationToken); + - return new ApiSession(this) - { - AuthToken = token, - IsAnonymous = false, - IsClosed = false, - SpaceName = spaceName - }; - } - /// - /// Close opened session - /// - /// api session - /// - /// - public async Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + public Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - if (apiSession.IsClosed) - return; - if (apiSession.IsAnonymous) - return; - - - var url = "auth/logout"; - - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { + var apiResult = await lowLevelApiClient.GetTasksListAsync(apiSession, token); + return MapOrFail(apiResult, (dto) => SpaceTasksListsMapper.MapFromDto(dto)); - await HandleResponse(response); - - } - + }, cancellationToken, OperationTimeout); } - public async Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = UrlHelper.JoinUrl("space", apiSession.SpaceName, "tasks"); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { - var dto = await HandleResponse(response); - return SpaceTasksListsMapper.MapFromDto(dto); - } - } + var apiResult = await lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); - public async Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = UrlHelper.JoinUrl("space", apiSession.SpaceName, "tasks", taskId.ToString("D")); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) - { - var dto = await HandleResponse(response); - return SpaceTaskMapper.MapFull(dto); - } + }, cancellationToken, OperationTimeout); } public void Dispose() @@ -1207,7 +971,9 @@ public void Dispose() _httpClient = null; } } - } + } } + + diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs new file mode 100644 index 0000000..269b454 --- /dev/null +++ b/src/Client/MorphServerRestClient.cs @@ -0,0 +1,164 @@ +using Morph.Server.Sdk.Exceptions; +using Morph.Server.Sdk.Helper; +using System; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using System.Collections.Specialized; +using Morph.Server.Sdk.Mappers; +using Morph.Server.Sdk.Dto.Errors; + +namespace Morph.Server.Sdk.Client +{ + + public interface IApiClient + { + Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + } + + public sealed class NoContentResult + { + + } + + public sealed class NoContentRequest + { + + } + + + public class MorphServerRestClient : IApiClient + { + private readonly HttpClient httpClient; + + public MorphServerRestClient(HttpClient httpClient) + { + this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + } + public Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Delete, url, null, urlParameters, headersCollection, cancellationToken); + } + + public Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + if (urlParameters == null) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return SendAsyncApiResult(HttpMethod.Get, url, null, urlParameters, headersCollection, cancellationToken); + } + + public Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Post, url, model, urlParameters, headersCollection, cancellationToken); + } + + public Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendAsyncApiResult(HttpMethod.Put, url, model, urlParameters, headersCollection, cancellationToken); + } + + protected virtual async Task> SendAsyncApiResult(HttpMethod httpMethod, string path, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + StringContent stringContent = null; + if (model != null) + { + var serialized = JsonSerializationHelper.Serialize(model); + stringContent = new StringContent(serialized, Encoding.UTF8, "application/json"); + } + + var url = path + urlParameters.ToQueryString(); + var httpRequestMessage = BuildHttpRequestMessage(httpMethod, url, stringContent, headersCollection); + + using (var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var result = JsonSerializationHelper.Deserialize(content); + return ApiResult.Ok(result); + } + else + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); + } + } + } + + protected HttpRequestMessage BuildHttpRequestMessage(HttpMethod httpMethod, string url, HttpContent content, HeadersCollection headersCollection) + { + var requestMessage = new HttpRequestMessage() + { + Content = content, + Method = httpMethod, + RequestUri = new Uri(url, UriKind.Relative) + }; + if (headersCollection != null) + { + headersCollection.Fill(requestMessage.Headers); + } + return requestMessage; + } + + + + private static async Task BuildExceptionFromResponse(HttpResponseMessage response) + { + + var content = await response.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(content)) + { + ErrorResponse errorResponse = null; + try + { + errorResponse = JsonSerializationHelper.Deserialize(content); + } + catch (Exception) + { + return new ResponseParseException("An error occurred while deserializing the response", content); + } + if (errorResponse.error == null) + return new ResponseParseException("An error occurred while deserializing the response", content); + + switch (errorResponse.error.code) + { + case ReadableErrorTopCode.Conflict: return new MorphApiConflictException(errorResponse.error.message); + case ReadableErrorTopCode.NotFound: return new MorphApiNotFoundException(errorResponse.error.message); + case ReadableErrorTopCode.Forbidden: return new MorphApiForbiddenException(errorResponse.error.message); + case ReadableErrorTopCode.Unauthorized: return new MorphApiUnauthorizedException(errorResponse.error.message); + case ReadableErrorTopCode.BadArgument: return new MorphApiBadArgumentException(FieldErrorsMapper.MapFromDto(errorResponse.error), errorResponse.error.message); + default: return new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); + } + } + + else + { + switch (response.StatusCode) + { + case HttpStatusCode.Conflict: return new MorphApiConflictException(response.ReasonPhrase ?? "Conflict"); + case HttpStatusCode.NotFound: return new MorphApiNotFoundException(response.ReasonPhrase ?? "Not found"); + case HttpStatusCode.Forbidden: return new MorphApiForbiddenException(response.ReasonPhrase ?? "Forbidden"); + case HttpStatusCode.Unauthorized: return new MorphApiUnauthorizedException(response.ReasonPhrase ?? "Unauthorized"); + case HttpStatusCode.BadRequest: return new MorphClientGeneralException("Unknown", response.ReasonPhrase ?? "Unknown error"); + default: return new ResponseParseException(response.ReasonPhrase, null); + } + + } + } + + + } + + +} + + + diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index 2843671..f1a21e3 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -21,10 +21,10 @@ public class ApiSession : IDisposable internal string AuthToken { get; set; } internal bool IsAnonymous { get; set; } - MorphServerApiClient _client; + IMorphServerApiClient _client; private string _spaceName; - internal ApiSession(MorphServerApiClient client) + internal ApiSession(IMorphServerApiClient client) { _client = client; IsClosed = false; @@ -62,7 +62,8 @@ public void Dispose() { var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5)); await _client.CloseSessionAsync(this, cts.Token); - _client.Dispose(); + // don't dispose client implicitly, just remove link to client + //_client.Dispose(); _client = null; } From b50d6591bfe2922a7d74aa8a54bc43a675ef94f8 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 24 Jan 2019 18:18:51 +0200 Subject: [PATCH 04/37] code refactoring - work in progress --- src/Client/ApiResult.cs | 8 ++ src/Client/LowLevelApiClient.cs | 7 +- src/Client/MorphServerApiClient.cs | 133 +++++++++++++++++----------- src/Client/MorphServerRestClient.cs | 17 +++- 4 files changed, 106 insertions(+), 59 deletions(-) diff --git a/src/Client/ApiResult.cs b/src/Client/ApiResult.cs index 143e843..1de1d03 100644 --- a/src/Client/ApiResult.cs +++ b/src/Client/ApiResult.cs @@ -24,6 +24,14 @@ public static ApiResult Ok(T data) Error = null }; } + + public void ThrowIfFailed() + { + if(!IsSucceed && Error != null) + { + throw Error; + } + } } diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index d9ff432..c582168 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -14,7 +14,7 @@ namespace Morph.Server.Sdk.Client - internal interface ILowLevelApiClient + internal interface ILowLevelApiClient: IDisposable { // TASKS Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); @@ -34,6 +34,7 @@ internal interface ILowLevelApiClient Task> AuthLogoutAsync(ApiSession apiSession, CancellationToken cancellationToken); Task> AuthLoginPasswordAsync(LoginRequestDto loginRequestDto, CancellationToken cancellationToken); Task> AuthGenerateNonce(CancellationToken cancellationToken); + // Server interaction @@ -241,6 +242,10 @@ public Task> AuthGenerateNonce(CancellationT return apiClient.PostAsync(url, new GenerateNonceRequestDto(), null, new HeadersCollection(), cancellationToken); } + public void Dispose() + { + apiClient.Dispose(); + } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 54d4b3c..eccaf28 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -26,21 +26,33 @@ namespace Morph.Server.Sdk.Client { - - - public class MorphServerAuthenticator + internal class OpenSessionAuthenticatorContext { - private readonly IApiClient apiClient; - private readonly IMorphServerApiClient morphServerApiClient; - - public MorphServerAuthenticator(IApiClient apiClient, IMorphServerApiClient morphServerApiClient) + + public OpenSessionAuthenticatorContext + (ILowLevelApiClient lowLevelApiClient, + IMorphServerApiClient morphServerApiClient, + Func buildApiClient) { - this.apiClient = apiClient; - this.morphServerApiClient = morphServerApiClient; + LowLevelApiClient = lowLevelApiClient ?? throw new ArgumentNullException(nameof(lowLevelApiClient)); + MorphServerApiClient = morphServerApiClient ?? throw new ArgumentNullException(nameof(morphServerApiClient)); + BuildApiClient = buildApiClient ?? throw new ArgumentNullException(nameof(buildApiClient)); } + public ILowLevelApiClient LowLevelApiClient { get; } + public IMorphServerApiClient MorphServerApiClient { get; } + public Func BuildApiClient { get; } + } + - public async Task OpenSessionAsync(SpaceEnumerationItem desiredSpace, OpenSessionRequest openSessionRequest, CancellationToken cancellationToken) + internal static class MorphServerAuthenticator + { + + public static async Task OpenSessionMultiplexedAsync( + SpaceEnumerationItem desiredSpace, + OpenSessionAuthenticatorContext context, + OpenSessionRequest openSessionRequest, + CancellationToken cancellationToken) { // space access restriction is supported since server 3.9.2 // for previous versions api will return SpaceAccessRestriction.NotSupported @@ -53,11 +65,11 @@ public async Task OpenSessionAsync(SpaceEnumerationItem desiredSpace // password protected space case SpaceAccessRestriction.BasicPassword: - return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); // windows authentication case SpaceAccessRestriction.WindowsAuthentication: - return await OpenSessionViaWindowsAuthenticationAsync(openSessionRequest.SpaceName, cancellationToken); + return await OpenSessionViaWindowsAuthenticationAsync(context, openSessionRequest.SpaceName, cancellationToken); // fallback case SpaceAccessRestriction.NotSupported: @@ -70,7 +82,7 @@ public async Task OpenSessionAsync(SpaceEnumerationItem desiredSpace // otherwise open session via space password else { - return await OpenSessionViaSpacePasswordAsync(openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); } default: @@ -79,7 +91,7 @@ public async Task OpenSessionAsync(SpaceEnumerationItem desiredSpace } - protected async Task OpenSessionViaWindowsAuthenticationAsync(string spaceName, CancellationToken cancellationToken) + static async Task OpenSessionViaWindowsAuthenticationAsync(OpenSessionAuthenticatorContext context, string spaceName, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(spaceName)) { @@ -92,13 +104,13 @@ protected async Task OpenSessionViaWindowsAuthenticationAsync(string UseDefaultCredentials = true }; - using (var httpClient = ConstructHttpClient(_apiHost, aHandler)) + // build a new low level client based on specified handler + using (var ntmlRestApiClient = context.BuildApiClient(aHandler)) { + var serverNonce = await internalGetAuthNonceAsync(ntmlRestApiClient, cancellationToken); + var token = await internalAuthExternalWindowAsync(ntmlRestApiClient, spaceName, serverNonce, cancellationToken); - var serverNonce = await internalGetAuthNonceAsync(httpClient, cancellationToken); - var token = await internalAuthExternalWindowAsync(httpClient, spaceName, serverNonce, cancellationToken); - - return new ApiSession(morphServerApiClient) + return new ApiSession(context.MorphServerApiClient) { AuthToken = token, IsAnonymous = false, @@ -107,18 +119,16 @@ protected async Task OpenSessionViaWindowsAuthenticationAsync(string }; } } - protected static async Task internalGetAuthNonceAsync(HttpClient httpClient, CancellationToken cancellationToken) + static async Task internalGetAuthNonceAsync(IApiClient apiClient, CancellationToken cancellationToken) { var url = "auth/nonce"; - using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(new GenerateNonceRequestDto()), cancellationToken)) - { - var dto = await HandleResponse(response); - return dto.Nonce; - - } + var response = await apiClient.PostAsync + (url, new GenerateNonceRequestDto(), null, new HeadersCollection(), cancellationToken); + response.ThrowIfFailed(); + return response.Data.Nonce; } - protected static async Task internalAuthExternalWindowAsync(HttpClient httpClient, string spaceName, string serverNonce, CancellationToken cancellationToken) + static async Task internalAuthExternalWindowAsync(IApiClient apiClient, string spaceName, string serverNonce, CancellationToken cancellationToken) { var url = "auth/external/windows"; var requestDto = new WindowsExternalLoginRequestDto @@ -127,29 +137,13 @@ protected static async Task internalAuthExternalWindowAsync(HttpClient h SpaceName = spaceName }; - using (var response = await httpClient.PostAsync(url, JsonSerializationHelper.SerializeAsStringContent(requestDto), cancellationToken)) - { - var responseDto = await HandleResponse(response); - return responseDto.Token; - } + var apiResult = await apiClient.PostAsync(url, requestDto, null, new HeadersCollection(), cancellationToken); + apiResult.ThrowIfFailed(); + return apiResult.Data.Token; + } - protected async Task internalAuthLoginAsync(string clientNonce, string serverNonce, string spaceName, string passwordHash, CancellationToken cancellationToken) - { - var requestDto = new LoginRequestDto - { - ClientSeed = clientNonce, - Password = passwordHash, - Provider = "Space", - UserName = spaceName, - RequestToken = serverNonce - }; - var apiResult = await lowLevelApiClient.AuthLoginPasswordAsync(requestDto, cancellationToken); - FailIfError(apiResult); - return apiResult.Data.Token; - - } /// /// Open a new authenticated session via password @@ -158,7 +152,7 @@ protected async Task internalAuthLoginAsync(string clientNonce, string s /// space password /// /// - public async Task OpenSessionViaSpacePasswordAsync(string spaceName, string password, CancellationToken cancellationToken) + static async Task OpenSessionViaSpacePasswordAsync(OpenSessionAuthenticatorContext context, string spaceName, string password, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(spaceName)) { @@ -170,16 +164,30 @@ public async Task OpenSessionViaSpacePasswordAsync(string spaceName, throw new ArgumentNullException(nameof(password)); } - var passwordHash = CryptographyHelper.CalculateSha256HEX(password); - var serverNonce = await internalGetAuthNonceAsync(GetHttpClient(), cancellationToken); + var passwordHash = CryptographyHelper.CalculateSha256HEX(password); + var serverNonceApiResult = await context.LowLevelApiClient.AuthGenerateNonce(cancellationToken); + serverNonceApiResult.ThrowIfFailed(); + var serverNonce = serverNonceApiResult.Data.Nonce; var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); var all = passwordHash + serverNonce + clientNonce; var allHash = CryptographyHelper.CalculateSha256HEX(all); - var token = await internalAuthLoginAsync(clientNonce, serverNonce, spaceName, allHash, cancellationToken); - return new ApiSession(this) + var requestDto = new LoginRequestDto + { + ClientSeed = clientNonce, + Password = passwordHash, + Provider = "Space", + UserName = spaceName, + RequestToken = serverNonce + }; + var authApiResult = await context.LowLevelApiClient.AuthLoginPasswordAsync(requestDto, cancellationToken); + authApiResult.ThrowIfFailed(); + var token = authApiResult.Data.Token; + + + return new ApiSession(context.MorphServerApiClient) { AuthToken = token, IsAnonymous = false, @@ -257,6 +265,17 @@ protected HttpClient GetHttpClient() public event EventHandler FileProgress; + protected IApiClient ConstructRestApiClient(HttpClient httpClient) + { + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); + } + + return new MorphServerRestClient(httpClient); + } + + protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClientHandler) { if (httpClientHandler == null) @@ -928,8 +947,14 @@ public async Task OpenSessionAsync(OpenSessionRequest openSessionReq { throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); } - - + var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, + new OpenSessionAuthenticatorContext( + lowLevelApiClient, + this, + (handler) => ConstructRestApiClient(ConstructHttpClient(_apiHost, handler))), + openSessionRequest, cancellationToken); + + return session; } catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) { diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 269b454..d095368 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -13,8 +13,9 @@ namespace Morph.Server.Sdk.Client { - public interface IApiClient + public interface IApiClient:IDisposable { + HttpClient HttpClient { get; set; } Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); @@ -34,11 +35,12 @@ public sealed class NoContentRequest public class MorphServerRestClient : IApiClient { - private readonly HttpClient httpClient; + private HttpClient httpClient; + public HttpClient HttpClient { get => httpClient; set => httpClient = value; } public MorphServerRestClient(HttpClient httpClient) { - this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + HttpClient = httpClient; } public Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) { @@ -154,7 +156,14 @@ private static async Task BuildExceptionFromResponse(HttpResponseMess } } - + public void Dispose() + { + if(HttpClient!= null) + { + HttpClient.Dispose(); + HttpClient = null; + } + } } From eaebeeb080406bbeee36b0ed2f90c4d3b20c340d Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 24 Jan 2019 21:31:19 +0200 Subject: [PATCH 05/37] code refactoring - in progress --- src/Client/IMorphServerApiClient.cs | 6 + src/Client/MorphServerApiClient.cs | 220 ++---------------- src/Client/MorphServerAuthenticator.cs | 173 ++++++++++++++ src/Client/MorphServerRestClient.cs | 113 +++++++-- src/Client/OpenSessionAuthenticatorContext.cs | 31 +++ 5 files changed, 326 insertions(+), 217 deletions(-) create mode 100644 src/Client/MorphServerAuthenticator.cs create mode 100644 src/Client/OpenSessionAuthenticatorContext.cs diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index 9860f29..e0f9928 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Morph.Server.Sdk.Events; @@ -12,6 +15,9 @@ namespace Morph.Server.Sdk.Client public interface IMorphServerApiClient:IDisposable { event EventHandler FileProgress; +#if NETSTANDARD2_0 + Func ServerCertificateCustomValidationCallback { get; set; } +#endif Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken); diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index eccaf28..8b3867d 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -26,180 +26,6 @@ namespace Morph.Server.Sdk.Client { - internal class OpenSessionAuthenticatorContext - { - - public OpenSessionAuthenticatorContext - (ILowLevelApiClient lowLevelApiClient, - IMorphServerApiClient morphServerApiClient, - Func buildApiClient) - { - LowLevelApiClient = lowLevelApiClient ?? throw new ArgumentNullException(nameof(lowLevelApiClient)); - MorphServerApiClient = morphServerApiClient ?? throw new ArgumentNullException(nameof(morphServerApiClient)); - BuildApiClient = buildApiClient ?? throw new ArgumentNullException(nameof(buildApiClient)); - } - - public ILowLevelApiClient LowLevelApiClient { get; } - public IMorphServerApiClient MorphServerApiClient { get; } - public Func BuildApiClient { get; } - } - - - internal static class MorphServerAuthenticator - { - - public static async Task OpenSessionMultiplexedAsync( - SpaceEnumerationItem desiredSpace, - OpenSessionAuthenticatorContext context, - OpenSessionRequest openSessionRequest, - CancellationToken cancellationToken) - { - // space access restriction is supported since server 3.9.2 - // for previous versions api will return SpaceAccessRestriction.NotSupported - // a special fall-back mechanize need to be used to open session in such case - switch (desiredSpace.SpaceAccessRestriction) - { - // anon space - case SpaceAccessRestriction.None: - return ApiSession.Anonymous(openSessionRequest.SpaceName); - - // password protected space - case SpaceAccessRestriction.BasicPassword: - return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); - - // windows authentication - case SpaceAccessRestriction.WindowsAuthentication: - return await OpenSessionViaWindowsAuthenticationAsync(context, openSessionRequest.SpaceName, cancellationToken); - - // fallback - case SpaceAccessRestriction.NotSupported: - - // if space is public or password is not set - open anon session - if (desiredSpace.IsPublic || string.IsNullOrWhiteSpace(openSessionRequest.Password)) - { - return ApiSession.Anonymous(openSessionRequest.SpaceName); - } - // otherwise open session via space password - else - { - return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); - } - - default: - throw new Exception("Space access restriction method is not supported by this client."); - } - } - - - static async Task OpenSessionViaWindowsAuthenticationAsync(OpenSessionAuthenticatorContext context, string spaceName, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(spaceName)) - { - throw new ArgumentException("Space name is not set", nameof(spaceName)); - } - // handler will be disposed automatically - HttpClientHandler aHandler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Automatic, - UseDefaultCredentials = true - }; - - // build a new low level client based on specified handler - using (var ntmlRestApiClient = context.BuildApiClient(aHandler)) - { - var serverNonce = await internalGetAuthNonceAsync(ntmlRestApiClient, cancellationToken); - var token = await internalAuthExternalWindowAsync(ntmlRestApiClient, spaceName, serverNonce, cancellationToken); - - return new ApiSession(context.MorphServerApiClient) - { - AuthToken = token, - IsAnonymous = false, - IsClosed = false, - SpaceName = spaceName - }; - } - } - static async Task internalGetAuthNonceAsync(IApiClient apiClient, CancellationToken cancellationToken) - { - var url = "auth/nonce"; - var response = await apiClient.PostAsync - (url, new GenerateNonceRequestDto(), null, new HeadersCollection(), cancellationToken); - response.ThrowIfFailed(); - return response.Data.Nonce; - } - - static async Task internalAuthExternalWindowAsync(IApiClient apiClient, string spaceName, string serverNonce, CancellationToken cancellationToken) - { - var url = "auth/external/windows"; - var requestDto = new WindowsExternalLoginRequestDto - { - RequestToken = serverNonce, - SpaceName = spaceName - }; - - var apiResult = await apiClient.PostAsync(url, requestDto, null, new HeadersCollection(), cancellationToken); - apiResult.ThrowIfFailed(); - return apiResult.Data.Token; - - } - - - - /// - /// Open a new authenticated session via password - /// - /// space name - /// space password - /// - /// - static async Task OpenSessionViaSpacePasswordAsync(OpenSessionAuthenticatorContext context, string spaceName, string password, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(spaceName)) - { - throw new ArgumentException("Space name is not set.", nameof(spaceName)); - } - - if (password == null) - { - throw new ArgumentNullException(nameof(password)); - } - - var passwordHash = CryptographyHelper.CalculateSha256HEX(password); - var serverNonceApiResult = await context.LowLevelApiClient.AuthGenerateNonce(cancellationToken); - serverNonceApiResult.ThrowIfFailed(); - var serverNonce = serverNonceApiResult.Data.Nonce; - var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); - var all = passwordHash + serverNonce + clientNonce; - var allHash = CryptographyHelper.CalculateSha256HEX(all); - - - - var requestDto = new LoginRequestDto - { - ClientSeed = clientNonce, - Password = passwordHash, - Provider = "Space", - UserName = spaceName, - RequestToken = serverNonce - }; - var authApiResult = await context.LowLevelApiClient.AuthLoginPasswordAsync(requestDto, cancellationToken); - authApiResult.ThrowIfFailed(); - var token = authApiResult.Data.Token; - - - return new ApiSession(context.MorphServerApiClient) - { - AuthToken = token, - IsAnonymous = false, - IsClosed = false, - SpaceName = spaceName - }; - } - - - - } - @@ -210,7 +36,7 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable { protected readonly Uri _apiHost; protected readonly string UserAgent = "MorphServerApiClient/1.3.5"; - protected HttpClient _httpClient; + protected readonly string _api_v1 = "api/v1/"; @@ -219,6 +45,9 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable public TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); public TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); +#if NETSTANDARD2_0 + public Func ServerCertificateCustomValidationCallback { get; set; } +#endif /// /// Construct Api client @@ -229,20 +58,14 @@ public MorphServerApiClient(string apiHost) if (!apiHost.EndsWith("/")) apiHost += "/"; _apiHost = new Uri(apiHost); - - } - - protected HttpClient GetHttpClient() - { - if (_httpClient == null) - { + + #if NETSTANDARD2_0 // handler will be disposed automatically HttpClientHandler aHandler = new HttpClientHandler() { ClientCertificateOptions = ClientCertificateOption.Automatic, - ServerCertificateCustomValidationCallback = new Func( - (request, certificate, chain, sslPolicyErrors) => true) + ServerCertificateCustomValidationCallback = this.ServerCertificateCustomValidationCallback }; #elif NET45 @@ -254,18 +77,16 @@ protected HttpClient GetHttpClient() }; #endif - _httpClient = ConstructHttpClient(_apiHost, aHandler); - } - return _httpClient; + var httpClient = ConstructHttpClient(_apiHost, aHandler); + var restClient = ConstructRestApiClient(httpClient); + this.lowLevelApiClient = new LowLevelApiClient(restClient); - } - public event EventHandler FileProgress; - protected IApiClient ConstructRestApiClient(HttpClient httpClient) + private IApiClient ConstructRestApiClient(HttpClient httpClient) { if (httpClient == null) { @@ -278,6 +99,7 @@ protected IApiClient ConstructRestApiClient(HttpClient httpClient) protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClientHandler) { + if (httpClientHandler == null) { throw new ArgumentNullException(nameof(httpClientHandler)); @@ -304,7 +126,7 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie - client.Timeout = TimeSpan.FromMinutes(15); + client.Timeout = TimeSpan.FromHours(24); return client; } @@ -901,16 +723,6 @@ public Task ValidateTasksAsync(ApiSession apiSession, strin }, cancellationToken, OperationTimeout); } - - - - - - - - - - @@ -990,10 +802,10 @@ public Task GetTaskAsync(ApiSession apiSession, Guid taskId, Cancella public void Dispose() { - if (_httpClient != null) + if (lowLevelApiClient != null) { - _httpClient.Dispose(); - _httpClient = null; + lowLevelApiClient.Dispose(); + lowLevelApiClient = null; } } diff --git a/src/Client/MorphServerAuthenticator.cs b/src/Client/MorphServerAuthenticator.cs new file mode 100644 index 0000000..1f87b62 --- /dev/null +++ b/src/Client/MorphServerAuthenticator.cs @@ -0,0 +1,173 @@ +using Morph.Server.Sdk.Dto; +using Morph.Server.Sdk.Helper; +using Morph.Server.Sdk.Model; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Client +{ + internal static class MorphServerAuthenticator + { + + public static async Task OpenSessionMultiplexedAsync( + SpaceEnumerationItem desiredSpace, + OpenSessionAuthenticatorContext context, + OpenSessionRequest openSessionRequest, + CancellationToken cancellationToken) + { + // space access restriction is supported since server 3.9.2 + // for previous versions api will return SpaceAccessRestriction.NotSupported + // a special fall-back mechanize need to be used to open session in such case + switch (desiredSpace.SpaceAccessRestriction) + { + // anon space + case SpaceAccessRestriction.None: + return ApiSession.Anonymous(openSessionRequest.SpaceName); + + // password protected space + case SpaceAccessRestriction.BasicPassword: + return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + + // windows authentication + case SpaceAccessRestriction.WindowsAuthentication: + return await OpenSessionViaWindowsAuthenticationAsync(context, openSessionRequest.SpaceName, cancellationToken); + + // fallback + case SpaceAccessRestriction.NotSupported: + + // if space is public or password is not set - open anon session + if (desiredSpace.IsPublic || string.IsNullOrWhiteSpace(openSessionRequest.Password)) + { + return ApiSession.Anonymous(openSessionRequest.SpaceName); + } + // otherwise open session via space password + else + { + return await OpenSessionViaSpacePasswordAsync(context, openSessionRequest.SpaceName, openSessionRequest.Password, cancellationToken); + } + + default: + throw new Exception("Space access restriction method is not supported by this client."); + } + } + + + static async Task OpenSessionViaWindowsAuthenticationAsync(OpenSessionAuthenticatorContext context, string spaceName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(spaceName)) + { + throw new ArgumentException("Space name is not set", nameof(spaceName)); + } + // handler will be disposed automatically + HttpClientHandler aHandler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Automatic, + // required for automatic NTML/Negotiate challenge + UseDefaultCredentials = true, +#if NETSTANDARD2_0 + ServerCertificateCustomValidationCallback = context.MorphServerApiClient.ServerCertificateCustomValidationCallback +#endif + + + }; + + // build a new low level client based on specified handler + using (var ntmlRestApiClient = context.BuildApiClient(aHandler)) + { + var serverNonce = await internalGetAuthNonceAsync(ntmlRestApiClient, cancellationToken); + var token = await internalAuthExternalWindowAsync(ntmlRestApiClient, spaceName, serverNonce, cancellationToken); + + return new ApiSession(context.MorphServerApiClient) + { + AuthToken = token, + IsAnonymous = false, + IsClosed = false, + SpaceName = spaceName + }; + } + } + static async Task internalGetAuthNonceAsync(IApiClient apiClient, CancellationToken cancellationToken) + { + var url = "auth/nonce"; + var response = await apiClient.PostAsync + (url, new GenerateNonceRequestDto(), null, new HeadersCollection(), cancellationToken); + response.ThrowIfFailed(); + return response.Data.Nonce; + } + + static async Task internalAuthExternalWindowAsync(IApiClient apiClient, string spaceName, string serverNonce, CancellationToken cancellationToken) + { + var url = "auth/external/windows"; + var requestDto = new WindowsExternalLoginRequestDto + { + RequestToken = serverNonce, + SpaceName = spaceName + }; + + var apiResult = await apiClient.PostAsync(url, requestDto, null, new HeadersCollection(), cancellationToken); + apiResult.ThrowIfFailed(); + return apiResult.Data.Token; + + } + + + + /// + /// Open a new authenticated session via password + /// + /// space name + /// space password + /// + /// + static async Task OpenSessionViaSpacePasswordAsync(OpenSessionAuthenticatorContext context, string spaceName, string password, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(spaceName)) + { + throw new ArgumentException("Space name is not set.", nameof(spaceName)); + } + + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + + var passwordHash = CryptographyHelper.CalculateSha256HEX(password); + var serverNonceApiResult = await context.LowLevelApiClient.AuthGenerateNonce(cancellationToken); + serverNonceApiResult.ThrowIfFailed(); + var serverNonce = serverNonceApiResult.Data.Nonce; + var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); + var all = passwordHash + serverNonce + clientNonce; + var allHash = CryptographyHelper.CalculateSha256HEX(all); + + + + var requestDto = new LoginRequestDto + { + ClientSeed = clientNonce, + Password = passwordHash, + Provider = "Space", + UserName = spaceName, + RequestToken = serverNonce + }; + var authApiResult = await context.LowLevelApiClient.AuthLoginPasswordAsync(requestDto, cancellationToken); + authApiResult.ThrowIfFailed(); + var token = authApiResult.Data.Token; + + + return new ApiSession(context.MorphServerApiClient) + { + AuthToken = token, + IsAnonymous = false, + IsClosed = false, + SpaceName = spaceName + }; + } + + + + } +} + + diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index d095368..6882a79 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -9,19 +9,42 @@ using System.Collections.Specialized; using Morph.Server.Sdk.Mappers; using Morph.Server.Sdk.Dto.Errors; +using System.IO; +using Morph.Server.Sdk.Model; namespace Morph.Server.Sdk.Client { - public interface IApiClient:IDisposable + public interface IApiClient : IDisposable { HttpClient HttpClient { get; set; } Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + + + } + + public sealed class SendFileStreamData + { + public SendFileStreamData(Stream stream, string fileName, long fileSize) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + FileSize = fileSize; + } + + public Stream Stream { get; } + public string FileName { get; } + public long FileSize { get; } + } + + public sealed class NoContentResult { @@ -81,17 +104,22 @@ protected virtual async Task> SendAsyncApiResult(content); - return ApiResult.Ok(result); - } - else - { - var error = await BuildExceptionFromResponse(response); - return ApiResult.Fail(error); - } + return await HandleResponse(response); + } + } + + private static async Task> HandleResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var result = JsonSerializationHelper.Deserialize(content); + return ApiResult.Ok(result); + } + else + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); } } @@ -158,12 +186,71 @@ private static async Task BuildExceptionFromResponse(HttpResponseMess public void Dispose() { - if(HttpClient!= null) + if (HttpClient != null) { HttpClient.Dispose(); HttpClient = null; } } + + public async Task> SendFileStreamAsync( + HttpMethod httpMethod, string path, SendFileStreamData sendFileStreamData, + NameValueCollection urlParameters, HeadersCollection headersCollection, + CancellationToken cancellationToken) + { + try + { + string boundary = "MorphRestClient--------" + Guid.NewGuid().ToString("N"); + + using (var content = new MultipartFormDataContent(boundary)) + { + var downloadProgress = new FileProgress(sendFileStreamData.FileName, sendFileStreamData.FileSize); + downloadProgress.StateChanged += DownloadProgress_StateChanged; + using (cancellationToken.Register(() => downloadProgress.ChangeState(FileProgressState.Cancelled))) + { + using (var streamContent = new ProgressStreamContent(sendFileStreamData.Stream, downloadProgress)) + { + content.Add(streamContent, "files", Path.GetFileName(sendFileStreamData.FileName)); + var url = path + urlParameters.ToQueryString(); + var requestMessage = BuildHttpRequestMessage(httpMethod, url, content, headersCollection); + using (requestMessage) + { + using (var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + return await HandleResponse(response); + } + } + } + } + } + } + catch (Exception ex) when (ex.InnerException != null && + ex.InnerException is WebException web && + web.Status == WebExceptionStatus.ConnectionClosed) + { + return ApiResult.Fail(new MorphApiNotFoundException("Specified folder not found")); + } + catch (Exception e) + { + return ApiResult.Fail(e); + } + } + + private void DownloadProgress_StateChanged(object sender, Events.FileEventArgs e) + { + // TODO: add handler + //throw new NotImplementedException(); + } + + public Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendFileStreamAsync(HttpMethod.Put, url, sendFileStreamData, urlParameters, headersCollection, cancellationToken); + } + + public Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + return SendFileStreamAsync(HttpMethod.Post, url, sendFileStreamData, urlParameters, headersCollection, cancellationToken); + } } diff --git a/src/Client/OpenSessionAuthenticatorContext.cs b/src/Client/OpenSessionAuthenticatorContext.cs new file mode 100644 index 0000000..a699f57 --- /dev/null +++ b/src/Client/OpenSessionAuthenticatorContext.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Morph.Server.Sdk.Client +{ + internal class OpenSessionAuthenticatorContext + { + + public OpenSessionAuthenticatorContext + (ILowLevelApiClient lowLevelApiClient, + IMorphServerApiClient morphServerApiClient, + Func buildApiClient + + ) + { + LowLevelApiClient = lowLevelApiClient ?? throw new ArgumentNullException(nameof(lowLevelApiClient)); + MorphServerApiClient = morphServerApiClient ?? throw new ArgumentNullException(nameof(morphServerApiClient)); + BuildApiClient = buildApiClient ?? throw new ArgumentNullException(nameof(buildApiClient)); + + } + + public ILowLevelApiClient LowLevelApiClient { get; } + public IMorphServerApiClient MorphServerApiClient { get; } + public Func BuildApiClient { get; } + + } +} + + From 7a7d0b6158652f8b2d1bc9151e9de9688b124ef2 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 25 Jan 2019 18:28:31 +0200 Subject: [PATCH 06/37] sdk refactoring - in progress --- src/Client/IMorphServerApiClient.cs | 2 + src/Client/LowLevelApiClient.cs | 34 +++- src/Client/MorphServerApiClient.cs | 296 +++++----------------------- src/Client/MorphServerRestClient.cs | 83 +++++++- src/Helper/ProgressStreamContent.cs | 20 ++ src/Helper/StreamWithProgress.cs | 158 +++++++++++++++ src/Model/DownloadFileInfo.cs | 1 + 7 files changed, 334 insertions(+), 260 deletions(-) create mode 100644 src/Helper/StreamWithProgress.cs diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index e0f9928..ca25bc9 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -39,5 +39,7 @@ public interface IMorphServerApiClient:IDisposable Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken); Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + + Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index c582168..e96579b 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -16,6 +16,8 @@ namespace Morph.Server.Sdk.Client internal interface ILowLevelApiClient: IDisposable { + IApiClient ApiClient { get; } + // TASKS Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); @@ -49,25 +51,27 @@ internal interface ILowLevelApiClient: IDisposable // WEB FILES Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); + } - public interface IWebFilesLowLevelApiClient - { + //public interface IWebFilesLowLevelApiClient + //{ - Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken); - Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken); + // Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken); + // Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken); - Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); - Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false); - Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); + // Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); + // Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false); + // Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); - } + //} @@ -75,6 +79,8 @@ internal class LowLevelApiClient : ILowLevelApiClient { private readonly IApiClient apiClient; + public IApiClient ApiClient => apiClient; + public LowLevelApiClient(IApiClient apiClient) { this.apiClient = apiClient; @@ -246,6 +252,18 @@ public void Dispose() { apiClient.Dispose(); } + + public Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); + return apiClient.RetrieveFileGetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 8b3867d..df51b93 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -27,6 +27,16 @@ namespace Morph.Server.Sdk.Client { + public static class MorphServerApiClientConfig + { +#if NETSTANDARD2_0 + public static Func ServerCertificateCustomValidationCallback { get; set; } +#endif + + public static string ClientId { get; set; } = string.Empty; + + } + /// @@ -346,235 +356,6 @@ public Task GetSpacesListAsync(CancellationToken cancella }, cancellationToken, OperationTimeout); } - /// - /// Download file from server - /// - /// api session - /// Path to the remote file. Like /some/folder/file.txt - /// stream for writing. You should dispose the stream by yourself - /// cancellation token - /// returns file info - public async Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - DownloadFileInfo fileInfo = null; - await DownloadFileAsync(apiSession, remoteFilePath, (fi) => { fileInfo = fi; return true; }, streamToWriteTo, cancellationToken); - return fileInfo; - } - /// - /// Download file from server - /// - /// api session - /// Path to the remote file. Like /some/folder/file.txt - /// delegate to check file info before accessing to the file stream - /// stream for writing. Writing will be executed only if handleFile delegate returns true - /// - /// - public async Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - var spaceName = apiSession.SpaceName; - var nvc = new NameValueCollection(); - nvc.Add("_", DateTime.Now.Ticks.ToString()); - var url = UrlHelper.JoinUrl("space", spaceName, "files", remoteFilePath) + nvc.ToQueryString(); - // it's necessary to add HttpCompletionOption.ResponseHeadersRead to disable caching - using (HttpResponseMessage response = await GetHttpClient() - .SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), HttpCompletionOption.ResponseHeadersRead, cancellationToken)) - if (response.IsSuccessStatusCode) - { - using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) - { - var contentDisposition = response.Content.Headers.ContentDisposition; - DownloadFileInfo dfi = null; - if (contentDisposition != null) - { - dfi = new DownloadFileInfo - { - // need to fix double quotes, that may come from server response - // FileNameStar contains file name encoded in UTF8 - FileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"') - }; - } - var contentLength = response.Content.Headers.ContentLength; - var fileProgress = new FileProgress(dfi.FileName, contentLength.Value); - fileProgress.StateChanged += DownloadProgress_StateChanged; - - var bufferSize = 4096; - if (handleFile(dfi)) - { - - var buffer = new byte[bufferSize]; - var size = contentLength.Value; - var processed = 0; - var lastUpdate = DateTime.MinValue; - - fileProgress.ChangeState(FileProgressState.Starting); - - while (true) - { - // cancel download if cancellation token triggered - if (cancellationToken.IsCancellationRequested) - { - fileProgress.ChangeState(FileProgressState.Cancelled); - throw new OperationCanceledException(); - } - - var length = await streamToReadFrom.ReadAsync(buffer, 0, buffer.Length); - if (length <= 0) break; - await streamToWriteTo.WriteAsync(buffer, 0, length); - processed += length; - if (DateTime.Now - lastUpdate > TimeSpan.FromMilliseconds(250)) - { - fileProgress.SetProcessedBytes(processed); - fileProgress.ChangeState(FileProgressState.Processing); - lastUpdate = DateTime.Now; - } - - } - - fileProgress.ChangeState(FileProgressState.Finishing); - - } - - } - } - else - { - // TODO: check - await HandleErrorResponse(response); - - } - - - } - - /// - /// Uploads file to the server - /// - /// api session - /// path to the local file - /// detination folder like /path/to/folder - /// cancellation token - /// overwrite file - /// - public async Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - if (!File.Exists(localFilePath)) - throw new FileNotFoundException(string.Format("File '{0}' not found", localFilePath)); - var fileSize = new System.IO.FileInfo(localFilePath).Length; - var fileName = Path.GetFileName(localFilePath); - using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) - { - await UploadFileAsync(apiSession, fsSource, fileName, fileSize, destFolderPath, cancellationToken, overwriteFileifExists); - return; - } - - } - - - /// - /// Uploads local file to the server folder. - /// - /// api session - /// path to the local file - /// destination folder like /path/to/folder - /// destination filename. If it's empty then original file name will be used - /// cancellation token - /// overwrite file - /// - public async Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - if (!File.Exists(localFilePath)) - { - throw new FileNotFoundException(string.Format("File '{0}' not found", localFilePath)); - } - var fileName = String.IsNullOrWhiteSpace(destFileName) ? Path.GetFileName(localFilePath) : destFileName; - var fileSize = new FileInfo(localFilePath).Length; - using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) - { - await UploadFileAsync(apiSession, fsSource, fileName, fileSize, destFolderPath, cancellationToken, overwriteFileifExists); - return; - } - - } - - - - - /// - /// Upload file stream to the server - /// - /// api session - /// stream for read from - /// file name - /// file size in bytes - /// destination folder like /path/to/folder - /// cancellation tokern - /// - /// - public async Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - try - { - var spaceName = apiSession.SpaceName; - string boundary = "EasyMorphCommandClient--------" + Guid.NewGuid().ToString("N"); - string url = UrlHelper.JoinUrl("space", spaceName, "files", destFolderPath); - - using (var content = new MultipartFormDataContent(boundary)) - { - var downloadProgress = new FileProgress(fileName, fileSize); - downloadProgress.StateChanged += DownloadProgress_StateChanged; - using (cancellationToken.Register(() => downloadProgress.ChangeState(FileProgressState.Cancelled))) - { - using (var streamContent = new ProgressStreamContent(inputStream, downloadProgress)) - { - content.Add(streamContent, "files", Path.GetFileName(fileName)); - - var requestMessage = BuildHttpRequestMessage(overwriteFileifExists ? HttpMethod.Put : HttpMethod.Post, url, content, apiSession); - using (requestMessage) - { - using (var response = await GetHttpClient().SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) - { - await HandleResponse(response); - } - } - } - } - } - } - catch (Exception ex) - when (ex.InnerException != null && ex.InnerException is WebException) - { - var einner = ex.InnerException as WebException; - if (einner.Status == WebExceptionStatus.ConnectionClosed) - throw new MorphApiNotFoundException("Specified folder not found"); - - } - } - private void DownloadProgress_StateChanged(object sender, FileEventArgs e) { if (FileProgress != null) @@ -585,23 +366,6 @@ private void DownloadProgress_StateChanged(object sender, FileEventArgs e) } - //protected async Task GetDataWithCancelAfter(Func> action, TimeSpan timeout, CancellationToken cancellationToken) - //{ - // using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - // { - // linkedTokenSource.CancelAfter(timeout); - // try - // { - // return await action(linkedTokenSource.Token); - // } - - // catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) - // { - // throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({timeout})"); - // } - // } - //} - @@ -810,6 +574,46 @@ public void Dispose() } + public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + return Wrapped(async (token) => + { + var apiResult = await lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, cancellationToken); + return MapOrFail(apiResult, (data) => data); + + }, cancellationToken, FileTransferTimeout); + } + + + public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) + { + throw new NotImplementedException(); + } + + public Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false) + { + throw new NotImplementedException(); + } + + public Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) + { + throw new NotImplementedException(); + } } } diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 6882a79..b24676b 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -22,14 +22,29 @@ public interface IApiClient : IDisposable Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); } + public sealed class FetchFileStreamData + { + public FetchFileStreamData(Stream stream, string fileName, long? fileSize) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + FileSize = fileSize; + } + + public Stream Stream { get; } + public string FileName { get; } + public long? FileSize { get; } + } + public sealed class SendFileStreamData { public SendFileStreamData(Stream stream, string fileName, long fileSize) @@ -224,8 +239,8 @@ public async Task> SendFileStreamAsync( } } } - catch (Exception ex) when (ex.InnerException != null && - ex.InnerException is WebException web && + catch (Exception ex) when (ex.InnerException != null && + ex.InnerException is WebException web && web.Status == WebExceptionStatus.ConnectionClosed) { return ApiResult.Fail(new MorphApiNotFoundException("Specified folder not found")); @@ -251,10 +266,66 @@ public Task> PostFileStreamAsync(string url, SendFil { return SendFileStreamAsync(HttpMethod.Post, url, sendFileStreamData, urlParameters, headersCollection, cancellationToken); } - } -} + public Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + if (urlParameters == null) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return RetrieveFileStreamAsync(HttpMethod.Get, url, urlParameters, headersCollection, cancellationToken); + } + + + + protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + var url = path + urlParameters.ToQueryString(); + using (HttpResponseMessage response = await httpClient.SendAsync( + BuildHttpRequestMessage(httpMethod, url, null, headersCollection), HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + { + if (response.IsSuccessStatusCode) + { + var contentDisposition = response.Content.Headers.ContentDisposition; + DownloadFileInfo dfi = null; + if (contentDisposition != null) + { + dfi = new DownloadFileInfo + { + // need to fix double quotes, that may come from server response + // FileNameStar contains file name encoded in UTF8 + FileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"') + }; + } + var contentLength = response.Content.Headers.ContentLength; + + using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) + { + var streamWithProgress = new StreamWithProgress(streamToReadFrom, + e => + { + + }, + e => + { + + }); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, dfi.FileName, contentLength)); + } + } + else + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); + } + } + } + + + } +} \ No newline at end of file diff --git a/src/Helper/ProgressStreamContent.cs b/src/Helper/ProgressStreamContent.cs index df3782b..c97ec18 100644 --- a/src/Helper/ProgressStreamContent.cs +++ b/src/Helper/ProgressStreamContent.cs @@ -6,10 +6,30 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Morph.Server.Sdk.Helper { + internal class StreamProgressEventArgs : EventArgs + { + public int BytesMoved { get; } + public long StreamTotalLength { get; } + public long StreamCurrentPosition { get; } + + public StreamProgressEventArgs() + { + + } + public StreamProgressEventArgs(int bytesMoved, long streamTotalLength, long streamCurrentPosition):this() + { + BytesMoved = bytesMoved; + StreamTotalLength = streamTotalLength; + StreamCurrentPosition = streamCurrentPosition; + } + } + + internal class ProgressStreamContent : HttpContent { private const int DefBufferSize = 4096; diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs new file mode 100644 index 0000000..4e6ce58 --- /dev/null +++ b/src/Helper/StreamWithProgress.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Helper +{ + internal class StreamWithProgress : Stream + { + private readonly Stream stream; + private readonly Action onReadProgress; + private readonly Action onWriteProgress; + + public StreamWithProgress(Stream stream, + Action onReadProgress = null, + Action onWriteProgress = null) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this.onReadProgress = onReadProgress; + this.onWriteProgress = onWriteProgress; + } + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position { get => stream.Position; set => stream.Position = value; } + + public override void Flush() + { + stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesRead = stream.Read(buffer, offset, count); + RaiseOnReadProgress(bytesRead); + return bytesRead; + } + + private void RaiseOnReadProgress(int bytesRead) + { + if (onReadProgress != null) + { + var args = new StreamProgressEventArgs(bytesRead , stream.Length, stream.Position); + onReadProgress(args); + } + } + private void RaiseOnWriteProgress(int bytesWrittens) + { + if (onWriteProgress != null) + { + var args = new StreamProgressEventArgs(bytesWrittens, stream.Length, stream.Position); + onWriteProgress(args); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + return stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + stream.Write(buffer, offset, count); + RaiseOnWriteProgress(count); + } + public override bool CanTimeout => stream.CanTimeout; + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotImplementedException(); + // return stream.BeginRead(buffer, offset, count, callback, state); + } + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + throw new NotImplementedException(); + //return stream.BeginWrite(buffer, offset, count, callback, state); + } + public override void Close() + { + stream.Close(); + base.Close(); + } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return stream.CopyToAsync(destination, bufferSize, cancellationToken); + } + public override int EndRead(IAsyncResult asyncResult) + { + return stream.EndRead(asyncResult); + } + public override void EndWrite(IAsyncResult asyncResult) + { + stream.EndWrite(asyncResult); + } + public override bool Equals(object obj) + { + return stream.Equals(obj); + } + public override Task FlushAsync(CancellationToken cancellationToken) + { + return stream.FlushAsync(cancellationToken); + } + public override int GetHashCode() + { + return stream.GetHashCode(); + } + public override object InitializeLifetimeService() + { + return stream.InitializeLifetimeService(); + } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken); + RaiseOnReadProgress(bytesRead); + return bytesRead; + } + public override int ReadByte() + { + return stream.ReadByte(); + } + public override int ReadTimeout { get => stream.ReadTimeout; set => stream.ReadTimeout = value; } + public override string ToString() + { + return stream.ToString(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await stream.WriteAsync(buffer, offset, count, cancellationToken); + RaiseOnWriteProgress(count); + } + public override void WriteByte(byte value) + { + stream.WriteByte(value); + RaiseOnWriteProgress(1); + } + public override int WriteTimeout { get => stream.WriteTimeout; set => stream.WriteTimeout = value; } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + stream.Dispose(); + } + } + + + } +} diff --git a/src/Model/DownloadFileInfo.cs b/src/Model/DownloadFileInfo.cs index 7f47ec4..2b94498 100644 --- a/src/Model/DownloadFileInfo.cs +++ b/src/Model/DownloadFileInfo.cs @@ -13,5 +13,6 @@ public class DownloadFileInfo /// public string FileName { get; set; } + } } From 010c7f70fec29cedfbbae51c967416d5bac733c7 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 25 Jan 2019 19:55:50 +0200 Subject: [PATCH 07/37] sdk refactoring - in progress --- src/Client/ApiResult.cs | 7 ++--- src/Client/ApiSessionExtension.cs | 6 ++-- src/Client/HeadersCollection.cs | 6 ++-- src/Client/MorphServerAuthenticator.cs | 11 ++++--- src/Client/MorphServerRestClient.cs | 41 ++++++++++++++++---------- src/Helper/ProgressStreamContent.cs | 12 +++----- src/Helper/StreamWithProgress.cs | 27 ++++++++++++----- 7 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/Client/ApiResult.cs b/src/Client/ApiResult.cs index 1de1d03..dd8678b 100644 --- a/src/Client/ApiResult.cs +++ b/src/Client/ApiResult.cs @@ -27,15 +27,14 @@ public static ApiResult Ok(T data) public void ThrowIfFailed() { - if(!IsSucceed && Error != null) + if (!IsSucceed && Error != null) { throw Error; } } } - - } - } + + diff --git a/src/Client/ApiSessionExtension.cs b/src/Client/ApiSessionExtension.cs index 346bda7..1dde893 100644 --- a/src/Client/ApiSessionExtension.cs +++ b/src/Client/ApiSessionExtension.cs @@ -15,8 +15,8 @@ public static HeadersCollection ToHeadersCollection(this ApiSession apiSession) } } - - } - } + + + diff --git a/src/Client/HeadersCollection.cs b/src/Client/HeadersCollection.cs index 2a44aaa..9465fd8 100644 --- a/src/Client/HeadersCollection.cs +++ b/src/Client/HeadersCollection.cs @@ -42,8 +42,8 @@ public void Fill(HttpRequestHeaders reqestHeaders) } } - - } - } + + + diff --git a/src/Client/MorphServerAuthenticator.cs b/src/Client/MorphServerAuthenticator.cs index 1f87b62..88bf8f1 100644 --- a/src/Client/MorphServerAuthenticator.cs +++ b/src/Client/MorphServerAuthenticator.cs @@ -133,20 +133,19 @@ static async Task OpenSessionViaSpacePasswordAsync(OpenSessionAuthen throw new ArgumentNullException(nameof(password)); } - var passwordHash = CryptographyHelper.CalculateSha256HEX(password); + var passwordSha256 = CryptographyHelper.CalculateSha256HEX(password); var serverNonceApiResult = await context.LowLevelApiClient.AuthGenerateNonce(cancellationToken); serverNonceApiResult.ThrowIfFailed(); var serverNonce = serverNonceApiResult.Data.Nonce; var clientNonce = ConvertHelper.ByteArrayToHexString(CryptographyHelper.GenerateRandomSequence(16)); - var all = passwordHash + serverNonce + clientNonce; - var allHash = CryptographyHelper.CalculateSha256HEX(all); - - + var all = passwordSha256 + serverNonce + clientNonce; + var composedHash = CryptographyHelper.CalculateSha256HEX(all); + var requestDto = new LoginRequestDto { ClientSeed = clientNonce, - Password = passwordHash, + Password = composedHash, Provider = "Space", UserName = spaceName, RequestToken = serverNonce diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index b24676b..73f2870 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -31,7 +31,7 @@ public interface IApiClient : IDisposable } - public sealed class FetchFileStreamData + public sealed class FetchFileStreamData : IDisposable { public FetchFileStreamData(Stream stream, string fileName, long? fileSize) { @@ -40,9 +40,18 @@ public FetchFileStreamData(Stream stream, string fileName, long? fileSize) FileSize = fileSize; } - public Stream Stream { get; } + public Stream Stream { get; private set; } public string FileName { get; } public long? FileSize { get; } + + public void Dispose() + { + if (Stream != null) + { + Stream.Dispose(); + Stream = null; + } + } } public sealed class SendFileStreamData @@ -114,10 +123,10 @@ protected virtual async Task> SendAsyncApiResult(response); } @@ -283,8 +292,8 @@ public Task> RetrieveFileGetAsync(string url, Nam protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) { var url = path + urlParameters.ToQueryString(); - using (HttpResponseMessage response = await httpClient.SendAsync( - BuildHttpRequestMessage(httpMethod, url, null, headersCollection), HttpCompletionOption.ResponseHeadersRead, cancellationToken)) + HttpResponseMessage response = await httpClient.SendAsync( + BuildHttpRequestMessage(httpMethod, url, null, headersCollection), HttpCompletionOption.ResponseHeadersRead, cancellationToken); { if (response.IsSuccessStatusCode) { @@ -301,19 +310,19 @@ protected async Task> RetrieveFileStreamAsync(Htt } var contentLength = response.Content.Headers.ContentLength; - using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) - { - var streamWithProgress = new StreamWithProgress(streamToReadFrom, - e => - { - - }, + // stream must be disposed by a caller + Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); + var streamWithProgress = new StreamWithProgress(streamToReadFrom, e => { - }); - return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, dfi.FileName, contentLength)); - } + }, + () => + { + response.Dispose(); + }); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, dfi.FileName, contentLength)); + } else { diff --git a/src/Helper/ProgressStreamContent.cs b/src/Helper/ProgressStreamContent.cs index c97ec18..50321ad 100644 --- a/src/Helper/ProgressStreamContent.cs +++ b/src/Helper/ProgressStreamContent.cs @@ -13,19 +13,15 @@ namespace Morph.Server.Sdk.Helper { internal class StreamProgressEventArgs : EventArgs { - public int BytesMoved { get; } - public long StreamTotalLength { get; } - public long StreamCurrentPosition { get; } - + public int BytesProcessed { get; } + public StreamProgressEventArgs() { } - public StreamProgressEventArgs(int bytesMoved, long streamTotalLength, long streamCurrentPosition):this() + public StreamProgressEventArgs(int bytesProcessed):this() { - BytesMoved = bytesMoved; - StreamTotalLength = streamTotalLength; - StreamCurrentPosition = streamCurrentPosition; + BytesProcessed = bytesProcessed; } } diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index 4e6ce58..0a05ebe 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -10,14 +10,17 @@ internal class StreamWithProgress : Stream private readonly Stream stream; private readonly Action onReadProgress; private readonly Action onWriteProgress; + private readonly Action onDisposed; public StreamWithProgress(Stream stream, - Action onReadProgress = null, - Action onWriteProgress = null) + Action onReadProgress = null, + Action onDisposed = null + ) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.onReadProgress = onReadProgress; this.onWriteProgress = onWriteProgress; + this.onDisposed = onDisposed; } public override bool CanRead => stream.CanRead; @@ -45,7 +48,7 @@ private void RaiseOnReadProgress(int bytesRead) { if (onReadProgress != null) { - var args = new StreamProgressEventArgs(bytesRead , stream.Length, stream.Position); + var args = new StreamProgressEventArgs(bytesRead ); onReadProgress(args); } } @@ -53,7 +56,7 @@ private void RaiseOnWriteProgress(int bytesWrittens) { if (onWriteProgress != null) { - var args = new StreamProgressEventArgs(bytesWrittens, stream.Length, stream.Position); + var args = new StreamProgressEventArgs(bytesWrittens); onWriteProgress(args); } } @@ -89,9 +92,14 @@ public override void Close() stream.Close(); base.Close(); } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - return stream.CopyToAsync(destination, bufferSize, cancellationToken); + byte[] buffer = new byte[bufferSize]; + int bytesRead; + while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + } } public override int EndRead(IAsyncResult asyncResult) { @@ -117,9 +125,10 @@ public override object InitializeLifetimeService() { return stream.InitializeLifetimeService(); } + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken); + var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); RaiseOnReadProgress(bytesRead); return bytesRead; } @@ -150,6 +159,10 @@ protected override void Dispose(bool disposing) if (disposing) { stream.Dispose(); + if (onDisposed != null) + { + onDisposed(); + } } } From 9dbcacf5186482b9b328d12d2e946a3175944157 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 29 Jan 2019 12:30:35 +0200 Subject: [PATCH 08/37] morph sdk code refactoring - in progress --- src/Client/IMorphServerApiClient.cs | 36 +- src/Client/LowLevelApiClient.cs | 55 +- src/Client/MorphServerApiClient.cs | 916 +++++++++++++------------ src/Client/MorphServerAuthenticator.cs | 2 +- src/Client/MorphServerRestClient.cs | 11 +- src/Helper/StreamWithProgress.cs | 4 +- 6 files changed, 567 insertions(+), 457 deletions(-) diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index ca25bc9..a5048c9 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -12,34 +12,46 @@ namespace Morph.Server.Sdk.Client { + + public class SpaceUploadFileRequest + { + public Stream DataStream { get; set; } + public string FileName { get; set; } + public long? FileSize { get; set; } + public bool OverwriteExistingFile { get; set; } = false; + } + + public interface IMorphServerApiClient:IDisposable { event EventHandler FileProgress; -#if NETSTANDARD2_0 - Func ServerCertificateCustomValidationCallback { get; set; } -#endif - Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); + IMorphApiClientConfiguration Config { get; } + Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken); - Task DeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); - Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken); - Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken); - Task FileExistsAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + Task GetServerStatusAsync(CancellationToken cancellationToken); Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken cancellationToken); Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); - Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false); - Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); + Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken); Task GetSpacesListAsync(CancellationToken cancellationToken); Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken); Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceBrowseAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); + Task SpaceDeleteFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceFileExistsAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + + Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + + Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); + + } } \ No newline at end of file diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index e96579b..58dda0a 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Collections.Generic; using System.IO; +using Morph.Server.Sdk.Exceptions; namespace Morph.Server.Sdk.Client { @@ -50,28 +51,14 @@ internal interface ILowLevelApiClient: IDisposable // WEB FILES Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); - Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); + Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); } - - //public interface IWebFilesLowLevelApiClient - //{ - - - // Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken); - // Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken); - - - - // Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); - // Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false); - // Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false); - - - //} + @@ -228,10 +215,10 @@ public Task> WebFilesBrowseSpaceAsync(ApiSes return apiClient.GetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); } - public Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + public Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) { var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder, fileName); + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); return apiClient.DeleteAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); } @@ -264,6 +251,36 @@ public Task> WebFilesDownloadFileAsync(ApiSession var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); return apiClient.RetrieveFileGetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); } + + public async Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); + var apiResult = await apiClient.HeadAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + // http ok or http no content means that file exists + if (apiResult.IsSucceed) + { + return ApiResult.Ok(true); + } + else + { + // if not found, return Ok with false result + if(apiResult.Error is MorphApiNotFoundException) + { + return ApiResult.Ok(false); + } + else + { + // some error occured - return internal error from api result + return ApiResult.Fail(apiResult.Error); + + } + } + } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index df51b93..eb5d5aa 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -1,618 +1,690 @@ -using Morph.Server.Sdk.Dto; -using Morph.Server.Sdk.Exceptions; -using Morph.Server.Sdk.Helper; -using Morph.Server.Sdk.Model; +using Morph.Server.Sdk.Model; using System; -using System.IO; using System.Net.Http; using System.Net.Http.Headers; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Net; using Morph.Server.Sdk.Events; -using System.Collections.Specialized; using Morph.Server.Sdk.Dto.Commands; -using Morph.Server.Sdk.Model.Errors; using Morph.Server.Sdk.Mappers; using Morph.Server.Sdk.Model.Commands; using System.Linq; -using Morph.Server.Sdk.Dto.Errors; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Net.Security; +using System.Reflection; +using System.IO; namespace Morph.Server.Sdk.Client { + public enum OperationType + { + ShortOperation = 1, + FileTransfer = 2 + } + - public static class MorphServerApiClientConfig + public static class MorphServerApiClientGlobalConfig { #if NETSTANDARD2_0 public static Func ServerCertificateCustomValidationCallback { get; set; } #endif + private const string DefaultClientType = "EMS-SDK"; + + /// + /// Default operation execution timeout + /// + public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// Default File transfer operation timeout + /// + public static TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); + + /// + /// HttpClient Timeout + /// + public static TimeSpan HttpClientTimeout { get; set; } = TimeSpan.FromHours(24); + + // additional parameter for client identification public static string ClientId { get; set; } = string.Empty; + public static string ClientType { get; set; } = DefaultClientType; + + // "Morph.Server.Sdk/x.x.x.x" + internal static string SDKVersionString { get; } + + + static MorphServerApiClientGlobalConfig() + { + // set sdk version string + // "Morph.Server.Sdk/x.x.x.x" + Assembly thisAssem = typeof(MorphServerApiClientGlobalConfig).Assembly; + var assemblyVersion = thisAssem.GetName().Version; + SDKVersionString = "Morph.Server.Sdk/" + assemblyVersion.ToString(); + + } + + } + public interface IMorphApiClientConfiguration + { + TimeSpan OperationTimeout { get; } + TimeSpan FileTransferTimeout { get; } + TimeSpan HttpClientTimeout { get; } + + Uri ApiUri { get; } +#if NETSTANDARD2_0 + Func ServerCertificateCustomValidationCallback { get; } +#endif + } - /// - /// Morph Server api client V1 - /// - public class MorphServerApiClient : IMorphServerApiClient, IDisposable + public class MorphApiClientConfiguration : IMorphApiClientConfiguration { - protected readonly Uri _apiHost; - protected readonly string UserAgent = "MorphServerApiClient/1.3.5"; - - protected readonly string _api_v1 = "api/v1/"; + public TimeSpan OperationTimeout { get; set; } = MorphServerApiClientGlobalConfig.OperationTimeout; + public TimeSpan FileTransferTimeout { get; set; } = MorphServerApiClientGlobalConfig.FileTransferTimeout; + public TimeSpan HttpClientTimeout { get; set; } = MorphServerApiClientGlobalConfig.HttpClientTimeout; - //private IApiClient apiClient; - private ILowLevelApiClient lowLevelApiClient; + public string ClientId { get; set; } = MorphServerApiClientGlobalConfig.ClientId; + public string ClientType { get; set; } = MorphServerApiClientGlobalConfig.ClientType; - public TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); - public TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); + public Uri ApiUri { get; set; } + internal string SDKVersionString { get; set; } = MorphServerApiClientGlobalConfig.SDKVersionString; #if NETSTANDARD2_0 public Func ServerCertificateCustomValidationCallback { get; set; } + = MorphServerApiClientGlobalConfig.ServerCertificateCustomValidationCallback; #endif + + /// - /// Construct Api client + /// Morph Server api client V1 /// - /// Server url - public MorphServerApiClient(string apiHost) + public class MorphServerApiClient : IMorphServerApiClient, IDisposable { - if (!apiHost.EndsWith("/")) - apiHost += "/"; - _apiHost = new Uri(apiHost); - - + public event EventHandler FileProgress; + + protected readonly string _userAgent = "MorphServerApiClient/next"; + protected readonly string _api_v1 = "api/v1/"; + + private readonly ILowLevelApiClient _lowLevelApiClient; + private MorphApiClientConfiguration clientConfiguration = new MorphApiClientConfiguration(); + + public IMorphApiClientConfiguration Config => clientConfiguration; + + internal ILowLevelApiClient BuildApiClient(MorphApiClientConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + #if NETSTANDARD2_0 // handler will be disposed automatically HttpClientHandler aHandler = new HttpClientHandler() { ClientCertificateOptions = ClientCertificateOption.Automatic, - ServerCertificateCustomValidationCallback = this.ServerCertificateCustomValidationCallback - + ServerCertificateCustomValidationCallback = configuration.ServerCertificateCustomValidationCallback }; #elif NET45 - // handler will be disposed automatically - HttpClientHandler aHandler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Automatic - - }; + // handler will be disposed automatically + HttpClientHandler aHandler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Automatic + }; +#else + Not implemented #endif + var httpClient = BuildHttpClient(configuration, aHandler); + var restClient = ConstructRestApiClient(httpClient); + return new LowLevelApiClient(restClient); + } - var httpClient = ConstructHttpClient(_apiHost, aHandler); - var restClient = ConstructRestApiClient(httpClient); - this.lowLevelApiClient = new LowLevelApiClient(restClient); - } - public event EventHandler FileProgress; + /// + /// Construct Api client + /// + /// Server url + public MorphServerApiClient(string apiHost) + { + if (apiHost == null) + { + throw new ArgumentNullException(nameof(apiHost)); + } + var defaultConfig = new MorphApiClientConfiguration + { + ApiUri = new Uri(apiHost) + }; + clientConfiguration = defaultConfig; + _lowLevelApiClient = BuildApiClient(clientConfiguration); + } - private IApiClient ConstructRestApiClient(HttpClient httpClient) - { - if (httpClient == null) + public MorphServerApiClient(MorphApiClientConfiguration clientConfiguration) { - throw new ArgumentNullException(nameof(httpClient)); + this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration)); + _lowLevelApiClient = BuildApiClient(clientConfiguration); } - return new MorphServerRestClient(httpClient); - } - - protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClientHandler) - { - - if (httpClientHandler == null) + private IApiClient ConstructRestApiClient(HttpClient httpClient) { - throw new ArgumentNullException(nameof(httpClientHandler)); + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); + } + + return new MorphServerRestClient(httpClient); } - var client = new HttpClient(httpClientHandler, true); - client.BaseAddress = new Uri(apiHost, new Uri(_api_v1, UriKind.Relative)); - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue("application/json") - { - CharSet = "utf-8" - }); - client.DefaultRequestHeaders.Add("User-Agent", UserAgent); - client.DefaultRequestHeaders.Add("X-Client-Type", "EMS-CMD"); - client.MaxResponseContentBufferSize = 100 * 1024; - client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + protected HttpClient BuildHttpClient(MorphApiClientConfiguration config, HttpClientHandler httpClientHandler) { - NoCache = true, - NoStore = true - }; + if (httpClientHandler == null) + { + throw new ArgumentNullException(nameof(httpClientHandler)); + } + var client = new HttpClient(httpClientHandler, true); + client.BaseAddress = new Uri(config.ApiUri, new Uri(_api_v1, UriKind.Relative)); + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json") + { + CharSet = "utf-8" + }); + client.DefaultRequestHeaders.Add("User-Agent", _userAgent); + client.DefaultRequestHeaders.Add("X-Client-Type", config.ClientType); + client.DefaultRequestHeaders.Add("X-Client-Id", config.ClientId); + client.DefaultRequestHeaders.Add("X-Client-Sdk", config.SDKVersionString); + + client.MaxResponseContentBufferSize = 100 * 1024; + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + NoStore = true + }; - client.Timeout = TimeSpan.FromHours(24); - return client; - } + client.Timeout = config.HttpClientTimeout; + return client; + } - /// - /// Start Task like "fire and forget" - /// - /// api session - /// tast guid - /// - /// - /// - public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); - }, cancellationToken, OperationTimeout); - } - protected Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, TimeSpan maxExecutionTime) - { - using (var derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken)) + /// + /// Start Task like "fire and forget" + /// + /// api session + /// tast guid + /// + /// + /// + public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) { - derTokenSource.CancelAfter(maxExecutionTime); - try + if (apiSession == null) { - return fun(derTokenSource.Token); + throw new ArgumentNullException(nameof(apiSession)); } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + + }, cancellationToken, OperationType.ShortOperation); + } - catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) + protected Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) + { + TimeSpan maxExecutionTime; + switch (operationType) { - throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({maxExecutionTime})"); + case OperationType.FileTransfer: + maxExecutionTime = clientConfiguration.FileTransferTimeout; break; + case OperationType.ShortOperation: + maxExecutionTime = clientConfiguration.OperationTimeout; break; + default: throw new NotImplementedException(); } + using (var derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken)) + { + derTokenSource.CancelAfter(maxExecutionTime); + try + { + return fun(derTokenSource.Token); + } + catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) + { + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({maxExecutionTime})"); + } + + } } - } - protected void FailIfError(ApiResult apiResult) - { - if (!apiResult.IsSucceed) + protected void FailIfError(ApiResult apiResult) { - throw apiResult.Error; + if (!apiResult.IsSucceed) + { + throw apiResult.Error; + } } - } - protected TDataModel MapOrFail(ApiResult apiResult, Func maper) - { - if (apiResult.IsSucceed) + protected TDataModel MapOrFail(ApiResult apiResult, Func maper) { - return maper(apiResult.Data); - } - else - { - throw apiResult.Error; + if (apiResult.IsSucceed) + { + return maper(apiResult.Data); + } + else + { + throw apiResult.Error; + } } - } - /// - /// Close opened session - /// - /// api session - /// - /// - public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Close opened session + /// + /// api session + /// + /// + public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } - if (apiSession.IsClosed) - return Task.FromResult(0); - if (apiSession.IsAnonymous) - return Task.FromResult(0); + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + if (apiSession.IsClosed) + return Task.FromResult(0); + if (apiSession.IsAnonymous) + return Task.FromResult(0); - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.AuthLogoutAsync(apiSession, token); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.AuthLogoutAsync(apiSession, token); // if task fail - do nothing. server will close this session after inactivity period return Task.FromResult(0); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Gets status of the task (Running/Not running) and payload - /// - /// api session - /// task guid - /// cancellation token - /// Returns task status - private Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Gets status of the task (Running/Not running) and payload + /// + /// api session + /// task guid + /// cancellation token + /// Returns task status + private Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Gets status of the task - /// - /// api session - /// task guid - /// cancellation token - /// Returns task status - public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - return Wrapped(async (token) => + /// + /// Gets status of the task + /// + /// api session + /// task guid + /// cancellation token + /// Returns task status + public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Retrieves space status - /// - /// api session - /// - /// - public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Retrieves space status + /// + /// api session + /// + /// + public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); - return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); + return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Stops the Task - /// - /// api session - /// - /// cancellation token - /// - public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Stops the Task + /// + /// api session + /// + /// cancellation token + /// + public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - await Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); - FailIfError(apiResult); - return Task.FromResult(0); + await Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); + FailIfError(apiResult); + return Task.FromResult(0); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Returns server status. May raise exception if server is unreachable - /// - /// - public Task GetServerStatusAsync(CancellationToken cancellationToken) - { - return Wrapped(async (token) => + /// + /// Returns server status. May raise exception if server is unreachable + /// + /// + public Task GetServerStatusAsync(CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.ServerGetStatusAsync(token); - return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.ServerGetStatusAsync(token); + return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); - } + }, cancellationToken, OperationType.ShortOperation); + } - public Task GetSpacesListAsync(CancellationToken cancellationToken) - { - return Wrapped(async (token) => + public Task GetSpacesListAsync(CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.SpacesGetListAsync(token); - return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); + return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); - } + }, cancellationToken, OperationType.ShortOperation); + } - private void DownloadProgress_StateChanged(object sender, FileEventArgs e) - { - if (FileProgress != null) + private void DownloadProgress_StateChanged(object sender, FileEventArgs e) { - FileProgress(this, e); + FileProgress?.Invoke(this, e); } - } - - /// - /// Prerforms browsing the Space - /// - /// api session - /// folder path like /path/to/folder - /// - /// - public Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - return Wrapped(async (token) => + /// + /// Prerforms browsing the Space + /// + /// api session + /// folder path like /path/to/folder + /// + /// + public Task SpaceBrowseAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, folderPath, token); - return MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); - - }, cancellationToken, OperationTimeout); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, folderPath, token); + return MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); - /// - /// Checks if file exists - /// - /// api session - /// server folder like /path/to/folder - /// file name - /// - /// Returns true if file exists. - public Task FileExistsAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); + }, cancellationToken, OperationType.ShortOperation); } - if (string.IsNullOrWhiteSpace(fileName)) - { - throw new ArgumentException(nameof(fileName)); - } - return Wrapped(async (token) => + /// + /// Checks if file exists + /// + /// api session + /// server folder like /path/to/folder + /// file name + /// + /// Returns true if file exists. + public Task SpaceFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, serverFolder, token); - var browseResult = MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); - return browseResult.FileExists(fileName); - - }, cancellationToken, OperationTimeout); - } - + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + if (string.IsNullOrWhiteSpace(serverFilePath)) + { + throw new ArgumentException(nameof(serverFilePath)); + } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFileExistsAsync(apiSession, serverFilePath, token); + return MapOrFail(apiResult, (dto) => dto); + }, cancellationToken, OperationType.ShortOperation); + } - /// - /// Performs file deletion - /// - /// api session - /// Path to server folder like /path/to/folder - /// file name - /// - /// - public Task DeleteFileAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Performs file deletion + /// + /// api session + /// Path to server folder like /path/to/folder + /// file name + /// + /// + public Task SpaceDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.WebFilesDeleteFileAsync(apiSession, serverFolder, fileName, token); - FailIfError(apiResult); - return Task.FromResult(0); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFilesDeleteFileAsync(apiSession, serverFilePath, token); + FailIfError(apiResult); + return Task.FromResult(0); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Validate tasks. Checks that there are no missing parameters in the tasks. - /// - /// api session - /// project path like /path/to/project.morph - /// - /// - public Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) - { - if (apiSession == null) + /// + /// Validate tasks. Checks that there are no missing parameters in the tasks. + /// + /// api session + /// project path like /path/to/project.morph + /// + /// + public Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); - } + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - if (string.IsNullOrWhiteSpace(projectPath)) - { - throw new ArgumentException("projectPath is empty", nameof(projectPath)); - } + if (string.IsNullOrWhiteSpace(projectPath)) + { + throw new ArgumentException("projectPath is empty", nameof(projectPath)); + } - return Wrapped(async (token) => - { - var request = new ValidateTasksRequestDto + return Wrapped(async (token) => { - SpaceName = apiSession.SpaceName, - ProjectPath = projectPath - }; - var apiResult = await lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); - return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); + var request = new ValidateTasksRequestDto + { + SpaceName = apiSession.SpaceName, + ProjectPath = projectPath + }; + var apiResult = await _lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); + return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); + }, cancellationToken, OperationType.ShortOperation); - } - + } - /// - /// Opens session based on required authentication mechanism - /// - /// - /// - /// - public async Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken ct) - { - if (openSessionRequest == null) - { - throw new ArgumentNullException(nameof(openSessionRequest)); - } - if (string.IsNullOrWhiteSpace(openSessionRequest.SpaceName)) - { - throw new ArgumentException("Space name is not set.", nameof(openSessionRequest.SpaceName)); - } - using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct)) + /// + /// Opens session based on required authentication mechanism + /// + /// + /// + /// + public async Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken ct) { - // no more than 20 sec for session opening - var timeout = TimeSpan.FromSeconds(20); - linkedTokenSource.CancelAfter(timeout); - var cancellationToken = linkedTokenSource.Token; - try - { - var spacesListApiResult = await lowLevelApiClient.SpacesGetListAsync(cancellationToken); - var spacesListResult = MapOrFail(spacesListApiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - - var desiredSpace = spacesListResult.Items.FirstOrDefault(x => x.SpaceName.Equals(openSessionRequest.SpaceName, StringComparison.OrdinalIgnoreCase)); - if (desiredSpace == null) - { - throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); - } - var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, - new OpenSessionAuthenticatorContext( - lowLevelApiClient, - this, - (handler) => ConstructRestApiClient(ConstructHttpClient(_apiHost, handler))), - openSessionRequest, cancellationToken); - - return session; + if (openSessionRequest == null) + { + throw new ArgumentNullException(nameof(openSessionRequest)); } - catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) + if (string.IsNullOrWhiteSpace(openSessionRequest.SpaceName)) { - throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({timeout})"); + throw new ArgumentException("Space name is not set.", nameof(openSessionRequest.SpaceName)); } - } - - } - + using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct)) + { + // no more than 20 sec for session opening + var timeout = TimeSpan.FromSeconds(20); + linkedTokenSource.CancelAfter(timeout); + var cancellationToken = linkedTokenSource.Token; + try + { + var spacesListApiResult = await _lowLevelApiClient.SpacesGetListAsync(cancellationToken); + var spacesListResult = MapOrFail(spacesListApiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + + var desiredSpace = spacesListResult.Items.FirstOrDefault(x => x.SpaceName.Equals(openSessionRequest.SpaceName, StringComparison.OrdinalIgnoreCase)); + if (desiredSpace == null) + { + throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); + } + var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, + new OpenSessionAuthenticatorContext( + _lowLevelApiClient, + this, + (handler) => ConstructRestApiClient(BuildHttpClient(clientConfiguration, handler))), + openSessionRequest, cancellationToken); + + return session; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) + { + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({timeout})"); + } + } + } - public Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) - { - return Wrapped(async (token) => - { - var apiResult = await lowLevelApiClient.GetTasksListAsync(apiSession, token); - return MapOrFail(apiResult, (dto) => SpaceTasksListsMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); - } - public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) - { - return Wrapped(async (token) => + public Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetTasksListAsync(apiSession, token); + return MapOrFail(apiResult, (dto) => SpaceTasksListsMapper.MapFromDto(dto)); - }, cancellationToken, OperationTimeout); - } + }, cancellationToken, OperationType.ShortOperation); - public void Dispose() - { - if (lowLevelApiClient != null) - { - lowLevelApiClient.Dispose(); - lowLevelApiClient = null; } - } - - public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) - { - if (apiSession == null) + public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { - throw new ArgumentNullException(nameof(apiSession)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + + }, cancellationToken, OperationType.ShortOperation); } - return Wrapped(async (token) => + + public Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { - var apiResult = await lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, cancellationToken); - return MapOrFail(apiResult, (data) => data); + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - }, cancellationToken, FileTransferTimeout); - } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + return MapOrFail(apiResult, (data) => data); + }, cancellationToken, OperationType.FileTransfer); + } - public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Func handleFile, Stream streamToWriteTo, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public void Dispose() + { + if (_lowLevelApiClient != null) + { + _lowLevelApiClient.Dispose(); + } + } - public Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - public Task UploadFileAsync(ApiSession apiSession, Stream inputStream, string fileName, long fileSize, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - throw new NotImplementedException(); - } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + return MapOrFail(apiResult, (data) => data.Stream); - public Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, string destFileName, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - throw new NotImplementedException(); - } + }, cancellationToken, OperationType.FileTransfer); + } - public Task UploadFileAsync(ApiSession apiSession, string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteFileifExists = false) - { - throw new NotImplementedException(); + public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } } diff --git a/src/Client/MorphServerAuthenticator.cs b/src/Client/MorphServerAuthenticator.cs index 88bf8f1..38d07e6 100644 --- a/src/Client/MorphServerAuthenticator.cs +++ b/src/Client/MorphServerAuthenticator.cs @@ -67,7 +67,7 @@ static async Task OpenSessionViaWindowsAuthenticationAsync(OpenSessi // required for automatic NTML/Negotiate challenge UseDefaultCredentials = true, #if NETSTANDARD2_0 - ServerCertificateCustomValidationCallback = context.MorphServerApiClient.ServerCertificateCustomValidationCallback + ServerCertificateCustomValidationCallback = context.MorphServerApiClient.Config.ServerCertificateCustomValidationCallback #endif diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 73f2870..1ee9543 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -19,6 +19,7 @@ public interface IApiClient : IDisposable { HttpClient HttpClient { get; set; } Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); @@ -332,7 +333,15 @@ protected async Task> RetrieveFileStreamAsync(Htt } } - + public Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + { + if (urlParameters == null) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return SendAsyncApiResult(HttpMethod.Head, url, null, urlParameters, headersCollection, cancellationToken); + } } diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index 0a05ebe..899d536 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -9,7 +9,7 @@ internal class StreamWithProgress : Stream { private readonly Stream stream; private readonly Action onReadProgress; - private readonly Action onWriteProgress; + private readonly Action onWriteProgress = null; private readonly Action onDisposed; public StreamWithProgress(Stream stream, @@ -19,7 +19,7 @@ public StreamWithProgress(Stream stream, { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.onReadProgress = onReadProgress; - this.onWriteProgress = onWriteProgress; + this.onDisposed = onDisposed; } public override bool CanRead => stream.CanRead; From 5aea633768ab9345a91190b1d0cb288b84245ecc Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 29 Jan 2019 18:47:00 +0200 Subject: [PATCH 09/37] code refactoring - in progress --- src/Client/ILowLevelApiClient.cs | 58 ++ src/Client/IMorphServerApiClient.cs | 15 +- src/Client/IRestClient.cs | 28 + src/Client/LowLevelApiClient.cs | 94 +- src/Client/MorphServerApiClient.cs | 936 ++++++++---------- .../MorphServerApiClientGlobalConfig.cs | 54 + src/Client/MorphServerAuthenticator.cs | 5 +- src/Client/MorphServerRestClient.cs | 85 +- src/Dto/NoContentRequest.cs | 10 + src/Dto/NoContentResult.cs | 10 + src/{Client => Helper}/ApiSessionExtension.cs | 5 +- src/{Client => Helper}/UrlHelper.cs | 2 +- src/Model/ClientConfiguration.cs | 30 + src/Model/DownloadFileInfo.cs | 14 +- src/Model/IClientConfiguration.cs | 21 + .../InternalModels}/ApiResult.cs | 4 +- .../InternalModels/FetchFileStreamData.cs | 23 + .../InternalModels}/HeadersCollection.cs | 7 +- .../OpenSessionAuthenticatorContext.cs | 9 +- src/Model/InternalModels/OperationType.cs | 10 + .../InternalModels/SendFileStreamData.cs | 22 + src/Model/ServerStreamingData.cs | 27 + src/Model/SpaceUploadFileRequest.cs | 13 + 23 files changed, 821 insertions(+), 661 deletions(-) create mode 100644 src/Client/ILowLevelApiClient.cs create mode 100644 src/Client/IRestClient.cs create mode 100644 src/Client/MorphServerApiClientGlobalConfig.cs create mode 100644 src/Dto/NoContentRequest.cs create mode 100644 src/Dto/NoContentResult.cs rename src/{Client => Helper}/ApiSessionExtension.cs (78%) rename src/{Client => Helper}/UrlHelper.cs (97%) create mode 100644 src/Model/ClientConfiguration.cs create mode 100644 src/Model/IClientConfiguration.cs rename src/{Client => Model/InternalModels}/ApiResult.cs (90%) create mode 100644 src/Model/InternalModels/FetchFileStreamData.cs rename src/{Client => Model/InternalModels}/HeadersCollection.cs (91%) rename src/{Client => Model/InternalModels}/OpenSessionAuthenticatorContext.cs (78%) create mode 100644 src/Model/InternalModels/OperationType.cs create mode 100644 src/Model/InternalModels/SendFileStreamData.cs create mode 100644 src/Model/ServerStreamingData.cs create mode 100644 src/Model/SpaceUploadFileRequest.cs diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs new file mode 100644 index 0000000..c78f1bd --- /dev/null +++ b/src/Client/ILowLevelApiClient.cs @@ -0,0 +1,58 @@ +using Morph.Server.Sdk.Dto; +using Morph.Server.Sdk.Model; +using System; +using System.Threading; +using System.Threading.Tasks; +using Morph.Server.Sdk.Dto.Commands; +using System.Collections.Generic; +using Morph.Server.Sdk.Model.InternalModels; + +namespace Morph.Server.Sdk.Client +{ + internal interface ILowLevelApiClient: IDisposable + { + IRestClient RestClient { get; } + + // TASKS + Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); + Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + + // RUN-STOP Task + Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); + Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + + // Tasks validation + Task> ValidateTasksAsync(ApiSession apiSession, ValidateTasksRequestDto validateTasksRequestDto, CancellationToken cancellationToken); + + + // Auth and sessions + Task> AuthLogoutAsync(ApiSession apiSession, CancellationToken cancellationToken); + Task> AuthLoginPasswordAsync(LoginRequestDto loginRequestDto, CancellationToken cancellationToken); + Task> AuthGenerateNonce(CancellationToken cancellationToken); + + + + // Server interaction + Task> ServerGetStatusAsync(CancellationToken cancellationToken); + + + // spaces + + Task> SpacesGetListAsync(CancellationToken cancellationToken); + Task> SpacesGetSpaceStatusAsync(ApiSession apiSession, string spaceName, CancellationToken cancellationToken); + + // WEB FILES + Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); + Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); + Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); + Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); + Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken); + Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken); + + } +} + + + diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index a5048c9..80d73f0 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -12,21 +12,12 @@ namespace Morph.Server.Sdk.Client { - - public class SpaceUploadFileRequest - { - public Stream DataStream { get; set; } - public string FileName { get; set; } - public long? FileSize { get; set; } - public bool OverwriteExistingFile { get; set; } = false; - } - - + public interface IMorphServerApiClient:IDisposable { event EventHandler FileProgress; - IMorphApiClientConfiguration Config { get; } + IClientConfiguration Config { get; } Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken); @@ -47,7 +38,7 @@ public interface IMorphServerApiClient:IDisposable Task SpaceDeleteFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceFileExistsAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); diff --git a/src/Client/IRestClient.cs b/src/Client/IRestClient.cs new file mode 100644 index 0000000..ceb145b --- /dev/null +++ b/src/Client/IRestClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Specialized; +using Morph.Server.Sdk.Model.InternalModels; + +namespace Morph.Server.Sdk.Client +{ + internal interface IRestClient : IDisposable + { + HttpClient HttpClient { get; set; } + Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + + Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + + Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + + } + + + +} \ No newline at end of file diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 58dda0a..589264f 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -9,66 +9,19 @@ using System.Collections.Generic; using System.IO; using Morph.Server.Sdk.Exceptions; +using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Helper; namespace Morph.Server.Sdk.Client { - - - internal interface ILowLevelApiClient: IDisposable - { - IApiClient ApiClient { get; } - - // TASKS - Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); - Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - - // RUN-STOP Task - Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); - Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - - // Tasks validation - Task> ValidateTasksAsync(ApiSession apiSession, ValidateTasksRequestDto validateTasksRequestDto, CancellationToken cancellationToken); - - - // Auth and sessions - Task> AuthLogoutAsync(ApiSession apiSession, CancellationToken cancellationToken); - Task> AuthLoginPasswordAsync(LoginRequestDto loginRequestDto, CancellationToken cancellationToken); - Task> AuthGenerateNonce(CancellationToken cancellationToken); - - - - // Server interaction - Task> ServerGetStatusAsync(CancellationToken cancellationToken); - - - // spaces - - Task> SpacesGetListAsync(CancellationToken cancellationToken); - Task> SpacesGetSpaceStatusAsync(ApiSession apiSession, string spaceName, CancellationToken cancellationToken); - - // WEB FILES - Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); - Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); - Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); - Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); - - - } - - - - - internal class LowLevelApiClient : ILowLevelApiClient { - private readonly IApiClient apiClient; + private readonly IRestClient apiClient; - public IApiClient ApiClient => apiClient; + public IRestClient RestClient => apiClient; - public LowLevelApiClient(IApiClient apiClient) + public LowLevelApiClient(IRestClient apiClient) { this.apiClient = apiClient; } @@ -281,6 +234,43 @@ public async Task> WebFileExistsAsync(ApiSession apiSession, str } } } + + public Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + if (sendFileStreamData == null) + { + throw new ArgumentNullException(nameof(sendFileStreamData)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); + + return apiClient.PutFileStreamAsync(url,sendFileStreamData, null, apiSession.ToHeadersCollection(), cancellationToken); + + } + + public Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + if (sendFileStreamData == null) + { + throw new ArgumentNullException(nameof(sendFileStreamData)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); + + return apiClient.PostFileStreamAsync(url, sendFileStreamData, null, apiSession.ToHeadersCollection(), cancellationToken); + } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index eb5d5aa..36a4385 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -10,126 +10,42 @@ using Morph.Server.Sdk.Model.Commands; using System.Linq; using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using System.Net.Security; using System.Reflection; using System.IO; +using Morph.Server.Sdk.Model.InternalModels; namespace Morph.Server.Sdk.Client { - public enum OperationType + /// + /// Morph Server api client V1 + /// + public class MorphServerApiClient : IMorphServerApiClient, IDisposable { - ShortOperation = 1, - FileTransfer = 2 - } - - - public static class MorphServerApiClientGlobalConfig - { -#if NETSTANDARD2_0 - public static Func ServerCertificateCustomValidationCallback { get; set; } -#endif - - private const string DefaultClientType = "EMS-SDK"; - - /// - /// Default operation execution timeout - /// - public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); - /// - /// Default File transfer operation timeout - /// - public static TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); - - /// - /// HttpClient Timeout - /// - public static TimeSpan HttpClientTimeout { get; set; } = TimeSpan.FromHours(24); - - // additional parameter for client identification - public static string ClientId { get; set; } = string.Empty; - public static string ClientType { get; set; } = DefaultClientType; - - // "Morph.Server.Sdk/x.x.x.x" - internal static string SDKVersionString { get; } - - - static MorphServerApiClientGlobalConfig() - { - // set sdk version string - // "Morph.Server.Sdk/x.x.x.x" - Assembly thisAssem = typeof(MorphServerApiClientGlobalConfig).Assembly; - var assemblyVersion = thisAssem.GetName().Version; - SDKVersionString = "Morph.Server.Sdk/" + assemblyVersion.ToString(); - - } - - - - } - - - public interface IMorphApiClientConfiguration - { - TimeSpan OperationTimeout { get; } - TimeSpan FileTransferTimeout { get; } - TimeSpan HttpClientTimeout { get; } - - Uri ApiUri { get; } -#if NETSTANDARD2_0 - Func ServerCertificateCustomValidationCallback { get; } -#endif - } - - public class MorphApiClientConfiguration : IMorphApiClientConfiguration - { - - public TimeSpan OperationTimeout { get; set; } = MorphServerApiClientGlobalConfig.OperationTimeout; - public TimeSpan FileTransferTimeout { get; set; } = MorphServerApiClientGlobalConfig.FileTransferTimeout; - public TimeSpan HttpClientTimeout { get; set; } = MorphServerApiClientGlobalConfig.HttpClientTimeout; + public event EventHandler FileProgress; - public string ClientId { get; set; } = MorphServerApiClientGlobalConfig.ClientId; - public string ClientType { get; set; } = MorphServerApiClientGlobalConfig.ClientType; - - public Uri ApiUri { get; set; } - internal string SDKVersionString { get; set; } = MorphServerApiClientGlobalConfig.SDKVersionString; -#if NETSTANDARD2_0 - public Func ServerCertificateCustomValidationCallback { get; set; } - = MorphServerApiClientGlobalConfig.ServerCertificateCustomValidationCallback; -#endif + protected readonly string _userAgent = "MorphServerApiClient/next"; + protected readonly string _api_v1 = "api/v1/"; + private readonly ILowLevelApiClient _lowLevelApiClient; + private ClientConfiguration clientConfiguration = new ClientConfiguration(); + public IClientConfiguration Config => clientConfiguration; - /// - /// Morph Server api client V1 - /// - public class MorphServerApiClient : IMorphServerApiClient, IDisposable + internal ILowLevelApiClient BuildApiClient(ClientConfiguration configuration) { - public event EventHandler FileProgress; - - protected readonly string _userAgent = "MorphServerApiClient/next"; - protected readonly string _api_v1 = "api/v1/"; - - private readonly ILowLevelApiClient _lowLevelApiClient; - private MorphApiClientConfiguration clientConfiguration = new MorphApiClientConfiguration(); - - public IMorphApiClientConfiguration Config => clientConfiguration; - - internal ILowLevelApiClient BuildApiClient(MorphApiClientConfiguration configuration) + if (configuration == null) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } + throw new ArgumentNullException(nameof(configuration)); + } #if NETSTANDARD2_0 - // handler will be disposed automatically - HttpClientHandler aHandler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Automatic, - ServerCertificateCustomValidationCallback = configuration.ServerCertificateCustomValidationCallback - }; + // handler will be disposed automatically + HttpClientHandler aHandler = new HttpClientHandler() + { + ClientCertificateOptions = ClientCertificateOption.Automatic, + ServerCertificateCustomValidationCallback = configuration.ServerCertificateCustomValidationCallback + }; #elif NET45 // handler will be disposed automatically HttpClientHandler aHandler = new HttpClientHandler() @@ -139,554 +55,578 @@ internal ILowLevelApiClient BuildApiClient(MorphApiClientConfiguration configura #else Not implemented #endif - var httpClient = BuildHttpClient(configuration, aHandler); - var restClient = ConstructRestApiClient(httpClient); - return new LowLevelApiClient(restClient); - } + var httpClient = BuildHttpClient(configuration, aHandler); + var restClient = ConstructRestApiClient(httpClient); + return new LowLevelApiClient(restClient); + } - /// - /// Construct Api client - /// - /// Server url - public MorphServerApiClient(string apiHost) + /// + /// Construct Api client + /// + /// Server url + public MorphServerApiClient(string apiHost) + { + if (apiHost == null) { - if (apiHost == null) - { - throw new ArgumentNullException(nameof(apiHost)); - } - - var defaultConfig = new MorphApiClientConfiguration - { - ApiUri = new Uri(apiHost) - }; - clientConfiguration = defaultConfig; - _lowLevelApiClient = BuildApiClient(clientConfiguration); + throw new ArgumentNullException(nameof(apiHost)); } - public MorphServerApiClient(MorphApiClientConfiguration clientConfiguration) + var defaultConfig = new ClientConfiguration { - this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration)); - _lowLevelApiClient = BuildApiClient(clientConfiguration); - } + ApiUri = new Uri(apiHost) + }; + clientConfiguration = defaultConfig; + _lowLevelApiClient = BuildApiClient(clientConfiguration); + } + public MorphServerApiClient(ClientConfiguration clientConfiguration) + { + this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration)); + _lowLevelApiClient = BuildApiClient(clientConfiguration); + } - private IApiClient ConstructRestApiClient(HttpClient httpClient) - { - if (httpClient == null) - { - throw new ArgumentNullException(nameof(httpClient)); - } - return new MorphServerRestClient(httpClient); + private IRestClient ConstructRestApiClient(HttpClient httpClient) + { + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); } + return new MorphServerRestClient(httpClient); + } - protected HttpClient BuildHttpClient(MorphApiClientConfiguration config, HttpClientHandler httpClientHandler) + + protected HttpClient BuildHttpClient(ClientConfiguration config, HttpClientHandler httpClientHandler) + { + if (httpClientHandler == null) { - if (httpClientHandler == null) - { - throw new ArgumentNullException(nameof(httpClientHandler)); - } + throw new ArgumentNullException(nameof(httpClientHandler)); + } - var client = new HttpClient(httpClientHandler, true); - client.BaseAddress = new Uri(config.ApiUri, new Uri(_api_v1, UriKind.Relative)); + var client = new HttpClient(httpClientHandler, true); + client.BaseAddress = new Uri(config.ApiUri, new Uri(_api_v1, UriKind.Relative)); - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue("application/json") - { - CharSet = "utf-8" - }); - client.DefaultRequestHeaders.Add("User-Agent", _userAgent); - client.DefaultRequestHeaders.Add("X-Client-Type", config.ClientType); - client.DefaultRequestHeaders.Add("X-Client-Id", config.ClientId); - client.DefaultRequestHeaders.Add("X-Client-Sdk", config.SDKVersionString); - - client.MaxResponseContentBufferSize = 100 * 1024; - client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json") { - NoCache = true, - NoStore = true - }; + CharSet = "utf-8" + }); + client.DefaultRequestHeaders.Add("User-Agent", _userAgent); + client.DefaultRequestHeaders.Add("X-Client-Type", config.ClientType); + client.DefaultRequestHeaders.Add("X-Client-Id", config.ClientId); + client.DefaultRequestHeaders.Add("X-Client-Sdk", config.SDKVersionString); + client.MaxResponseContentBufferSize = 100 * 1024; + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + NoStore = true + }; - client.Timeout = config.HttpClientTimeout; - return client; - } + client.Timeout = config.HttpClientTimeout; + return client; + } - /// - /// Start Task like "fire and forget" - /// - /// api session - /// tast guid - /// - /// - /// - public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) - { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + /// + /// Start Task like "fire and forget" + /// + /// api session + /// tast guid + /// + /// + /// + public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); - protected Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) + }, cancellationToken, OperationType.ShortOperation); + } + + internal virtual Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) + { + TimeSpan maxExecutionTime; + switch (operationType) + { + case OperationType.FileTransfer: + maxExecutionTime = clientConfiguration.FileTransferTimeout; break; + case OperationType.ShortOperation: + maxExecutionTime = clientConfiguration.OperationTimeout; break; + default: throw new NotImplementedException(); + } + using (var derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken)) { - TimeSpan maxExecutionTime; - switch (operationType) + derTokenSource.CancelAfter(maxExecutionTime); + try { - case OperationType.FileTransfer: - maxExecutionTime = clientConfiguration.FileTransferTimeout; break; - case OperationType.ShortOperation: - maxExecutionTime = clientConfiguration.OperationTimeout; break; - default: throw new NotImplementedException(); + return fun(derTokenSource.Token); } - using (var derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken)) - { - derTokenSource.CancelAfter(maxExecutionTime); - try - { - return fun(derTokenSource.Token); - } - - catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) - { - throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({maxExecutionTime})"); - } + catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) + { + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({maxExecutionTime})"); } + } + } - protected void FailIfError(ApiResult apiResult) + internal virtual void FailIfError(ApiResult apiResult) + { + if (!apiResult.IsSucceed) { - if (!apiResult.IsSucceed) - { - throw apiResult.Error; - } + throw apiResult.Error; } + } - protected TDataModel MapOrFail(ApiResult apiResult, Func maper) + internal virtual TDataModel MapOrFail(ApiResult apiResult, Func maper) + { + if (apiResult.IsSucceed) { - if (apiResult.IsSucceed) - { - return maper(apiResult.Data); - } - else - { - throw apiResult.Error; - } + return maper(apiResult.Data); + } + else + { + throw apiResult.Error; } + } - /// - /// Close opened session - /// - /// api session - /// - /// - public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + /// + /// Close opened session + /// + /// api session + /// + /// + public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - if (apiSession.IsClosed) - return Task.FromResult(0); - if (apiSession.IsAnonymous) - return Task.FromResult(0); + throw new ArgumentNullException(nameof(apiSession)); + } + if (apiSession.IsClosed) + return Task.FromResult(0); + if (apiSession.IsAnonymous) + return Task.FromResult(0); - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.AuthLogoutAsync(apiSession, token); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.AuthLogoutAsync(apiSession, token); // if task fail - do nothing. server will close this session after inactivity period return Task.FromResult(0); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Gets status of the task (Running/Not running) and payload - /// - /// api session - /// task guid - /// cancellation token - /// Returns task status - private Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + /// + /// Gets status of the task (Running/Not running) and payload + /// + /// api session + /// task guid + /// cancellation token + /// Returns task status + private Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Gets status of the task - /// - /// api session - /// task guid - /// cancellation token - /// Returns task status - public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + /// + /// Gets status of the task + /// + /// api session + /// task guid + /// cancellation token + /// Returns task status + public Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); + throw new ArgumentNullException(nameof(apiSession)); + } + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.GetTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => TaskStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Retrieves space status - /// - /// api session - /// - /// - public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) + /// + /// Retrieves space status + /// + /// api session + /// + /// + public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); - return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); + return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Stops the Task - /// - /// api session - /// - /// cancellation token - /// - public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + /// + /// Stops the Task + /// + /// api session + /// + /// cancellation token + /// + public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + throw new ArgumentNullException(nameof(apiSession)); + } - await Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); - FailIfError(apiResult); - return Task.FromResult(0); + await Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); + FailIfError(apiResult); + return Task.FromResult(0); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - /// - /// Returns server status. May raise exception if server is unreachable - /// - /// - public Task GetServerStatusAsync(CancellationToken cancellationToken) + /// + /// Returns server status. May raise exception if server is unreachable + /// + /// + public Task GetServerStatusAsync(CancellationToken cancellationToken) + { + return Wrapped(async (token) => { - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.ServerGetStatusAsync(token); - return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); + var apiResult = await _lowLevelApiClient.ServerGetStatusAsync(token); + return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); - } + }, cancellationToken, OperationType.ShortOperation); + } - public Task GetSpacesListAsync(CancellationToken cancellationToken) + public Task GetSpacesListAsync(CancellationToken cancellationToken) + { + return Wrapped(async (token) => { - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); - return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); + return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); - } + }, cancellationToken, OperationType.ShortOperation); + } + + private void DownloadProgress_StateChanged(object sender, FileEventArgs e) + { + FileProgress?.Invoke(this, e); + } - private void DownloadProgress_StateChanged(object sender, FileEventArgs e) - { - FileProgress?.Invoke(this, e); - } + /// + /// Prerforms browsing the Space + /// + /// api session + /// folder path like /path/to/folder + /// + /// + public Task SpaceBrowseAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - /// - /// Prerforms browsing the Space - /// - /// api session - /// folder path like /path/to/folder - /// - /// - public Task SpaceBrowseAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken) + return Wrapped(async (token) => { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + var apiResult = await _lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, folderPath, token); + return MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.WebFilesBrowseSpaceAsync(apiSession, folderPath, token); - return MapOrFail(apiResult, (dto) => SpaceBrowsingMapper.MapFromDto(dto)); + }, cancellationToken, OperationType.ShortOperation); + } - }, cancellationToken, OperationType.ShortOperation); + + /// + /// Checks if file exists + /// + /// api session + /// server folder like /path/to/folder + /// file name + /// + /// Returns true if file exists. + public Task SpaceFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); } + if (string.IsNullOrWhiteSpace(serverFilePath)) + { + throw new ArgumentException(nameof(serverFilePath)); + } - /// - /// Checks if file exists - /// - /// api session - /// server folder like /path/to/folder - /// file name - /// - /// Returns true if file exists. - public Task SpaceFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + return Wrapped(async (token) => { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + var apiResult = await _lowLevelApiClient.WebFileExistsAsync(apiSession, serverFilePath, token); + return MapOrFail(apiResult, (dto) => dto); + }, cancellationToken, OperationType.ShortOperation); + } - if (string.IsNullOrWhiteSpace(serverFilePath)) - { - throw new ArgumentException(nameof(serverFilePath)); - } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.WebFileExistsAsync(apiSession, serverFilePath, token); - return MapOrFail(apiResult, (dto) => dto); - }, cancellationToken, OperationType.ShortOperation); + /// + /// Performs file deletion + /// + /// api session + /// Path to server folder like /path/to/folder + /// file name + /// + /// + public Task SpaceDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); } - - /// - /// Performs file deletion - /// - /// api session - /// Path to server folder like /path/to/folder - /// file name - /// - /// - public Task SpaceDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + return Wrapped(async (token) => { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + var apiResult = await _lowLevelApiClient.WebFilesDeleteFileAsync(apiSession, serverFilePath, token); + FailIfError(apiResult); + return Task.FromResult(0); - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.WebFilesDeleteFileAsync(apiSession, serverFilePath, token); - FailIfError(apiResult); - return Task.FromResult(0); + }, cancellationToken, OperationType.ShortOperation); - }, cancellationToken, OperationType.ShortOperation); + } - } + /// + /// Validate tasks. Checks that there are no missing parameters in the tasks. + /// + /// api session + /// project path like /path/to/project.morph + /// + /// + public Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } - /// - /// Validate tasks. Checks that there are no missing parameters in the tasks. - /// - /// api session - /// project path like /path/to/project.morph - /// - /// - public Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken) + if (string.IsNullOrWhiteSpace(projectPath)) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + throw new ArgumentException("projectPath is empty", nameof(projectPath)); + } - if (string.IsNullOrWhiteSpace(projectPath)) + return Wrapped(async (token) => + { + var request = new ValidateTasksRequestDto { - throw new ArgumentException("projectPath is empty", nameof(projectPath)); - } + SpaceName = apiSession.SpaceName, + ProjectPath = projectPath + }; + var apiResult = await _lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); + return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); - return Wrapped(async (token) => - { - var request = new ValidateTasksRequestDto - { - SpaceName = apiSession.SpaceName, - ProjectPath = projectPath - }; - var apiResult = await _lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); - return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); + }, cancellationToken, OperationType.ShortOperation); - }, cancellationToken, OperationType.ShortOperation); + } - } + /// + /// Opens session based on required authentication mechanism + /// + /// + /// + /// + public async Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken ct) + { + if (openSessionRequest == null) + { + throw new ArgumentNullException(nameof(openSessionRequest)); + } + if (string.IsNullOrWhiteSpace(openSessionRequest.SpaceName)) + { + throw new ArgumentException("Space name is not set.", nameof(openSessionRequest.SpaceName)); + } - /// - /// Opens session based on required authentication mechanism - /// - /// - /// - /// - public async Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken ct) + using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - if (openSessionRequest == null) - { - throw new ArgumentNullException(nameof(openSessionRequest)); - } - if (string.IsNullOrWhiteSpace(openSessionRequest.SpaceName)) + // no more than 20 sec for session opening + var timeout = TimeSpan.FromSeconds(20); + linkedTokenSource.CancelAfter(timeout); + var cancellationToken = linkedTokenSource.Token; + try { - throw new ArgumentException("Space name is not set.", nameof(openSessionRequest.SpaceName)); - } + var spacesListApiResult = await _lowLevelApiClient.SpacesGetListAsync(cancellationToken); + var spacesListResult = MapOrFail(spacesListApiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct)) - { - // no more than 20 sec for session opening - var timeout = TimeSpan.FromSeconds(20); - linkedTokenSource.CancelAfter(timeout); - var cancellationToken = linkedTokenSource.Token; - try + var desiredSpace = spacesListResult.Items.FirstOrDefault(x => x.SpaceName.Equals(openSessionRequest.SpaceName, StringComparison.OrdinalIgnoreCase)); + if (desiredSpace == null) { - var spacesListApiResult = await _lowLevelApiClient.SpacesGetListAsync(cancellationToken); - var spacesListResult = MapOrFail(spacesListApiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - - var desiredSpace = spacesListResult.Items.FirstOrDefault(x => x.SpaceName.Equals(openSessionRequest.SpaceName, StringComparison.OrdinalIgnoreCase)); - if (desiredSpace == null) - { - throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); - } - var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, - new OpenSessionAuthenticatorContext( - _lowLevelApiClient, - this, - (handler) => ConstructRestApiClient(BuildHttpClient(clientConfiguration, handler))), - openSessionRequest, cancellationToken); - - return session; - } - catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) - { - throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({timeout})"); + throw new Exception($"Server has no space '{openSessionRequest.SpaceName}'"); } + var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, + new OpenSessionAuthenticatorContext( + _lowLevelApiClient, + this, + (handler) => ConstructRestApiClient(BuildHttpClient(clientConfiguration, handler))), + openSessionRequest, cancellationToken); + + return session; + } + catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) + { + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({timeout})"); } - } + } + - public Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + public Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + { + return Wrapped(async (token) => { - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.GetTasksListAsync(apiSession, token); - return MapOrFail(apiResult, (dto) => SpaceTasksListsMapper.MapFromDto(dto)); + var apiResult = await _lowLevelApiClient.GetTasksListAsync(apiSession, token); + return MapOrFail(apiResult, (dto) => SpaceTasksListsMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.ShortOperation); - } + } - public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + return Wrapped(async (token) => { - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); - return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + var apiResult = await _lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); - }, cancellationToken, OperationType.ShortOperation); - } + }, cancellationToken, OperationType.ShortOperation); + } - public Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + throw new ArgumentNullException(nameof(apiSession)); + } - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); - return MapOrFail(apiResult, (data) => data); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + return MapOrFail(apiResult, (data) => new ServerStreamingData(data.Stream, data.FileName, data.FileSize) + ); + + }, cancellationToken, OperationType.FileTransfer); + } - }, cancellationToken, OperationType.FileTransfer); + public void Dispose() + { + if (_lowLevelApiClient != null) + { + _lowLevelApiClient.Dispose(); } + } - public void Dispose() + public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) { - if (_lowLevelApiClient != null) - { - _lowLevelApiClient.Dispose(); - } + throw new ArgumentNullException(nameof(apiSession)); } - public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + return Wrapped(async (token) => { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + return MapOrFail(apiResult, (data) => data.Stream); - return Wrapped(async (token) => - { - var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); - return MapOrFail(apiResult, (data) => data.Stream); + }, cancellationToken, OperationType.FileTransfer); + } - }, cancellationToken, OperationType.FileTransfer); + public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); } - public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) + if (spaceUploadFileRequest == null) { - throw new NotImplementedException(); + throw new ArgumentNullException(nameof(spaceUploadFileRequest)); } + + return Wrapped(async (token) => + { + var sendStreamData = new SendFileStreamData( + spaceUploadFileRequest.DataStream, + spaceUploadFileRequest.FileName, + spaceUploadFileRequest.FileSize); + var apiResult = + spaceUploadFileRequest.OverwriteExistingFile ? + await _lowLevelApiClient.WebFilesPutFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, token) : + await _lowLevelApiClient.WebFilesPostFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, token); + FailIfError(apiResult); + return Task.FromResult(0); + + }, cancellationToken, OperationType.FileTransfer); } } + } diff --git a/src/Client/MorphServerApiClientGlobalConfig.cs b/src/Client/MorphServerApiClientGlobalConfig.cs new file mode 100644 index 0000000..8abb817 --- /dev/null +++ b/src/Client/MorphServerApiClientGlobalConfig.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; +using System.Reflection; + +namespace Morph.Server.Sdk.Client +{ + public static class MorphServerApiClientGlobalConfig + { +#if NETSTANDARD2_0 + public static Func ServerCertificateCustomValidationCallback { get; set; } +#endif + + private const string DefaultClientType = "EMS-SDK"; + + /// + /// Default operation execution timeout + /// + public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// Default File transfer operation timeout + /// + public static TimeSpan FileTransferTimeout { get; set; } = TimeSpan.FromHours(3); + + /// + /// HttpClient Timeout + /// + public static TimeSpan HttpClientTimeout { get; set; } = TimeSpan.FromHours(24); + + // additional parameter for client identification + public static string ClientId { get; set; } = string.Empty; + public static string ClientType { get; set; } = DefaultClientType; + + // "Morph.Server.Sdk/x.x.x.x" + internal static string SDKVersionString { get; } + + + static MorphServerApiClientGlobalConfig() + { + // set sdk version string + // "Morph.Server.Sdk/x.x.x.x" + Assembly thisAssem = typeof(MorphServerApiClientGlobalConfig).Assembly; + var assemblyVersion = thisAssem.GetName().Version; + SDKVersionString = "Morph.Server.Sdk/" + assemblyVersion.ToString(); + + } + + + + } +} + + diff --git a/src/Client/MorphServerAuthenticator.cs b/src/Client/MorphServerAuthenticator.cs index 38d07e6..9ecd3ba 100644 --- a/src/Client/MorphServerAuthenticator.cs +++ b/src/Client/MorphServerAuthenticator.cs @@ -1,6 +1,7 @@ using Morph.Server.Sdk.Dto; using Morph.Server.Sdk.Helper; using Morph.Server.Sdk.Model; +using Morph.Server.Sdk.Model.InternalModels; using System; using System.Net.Http; using System.Threading; @@ -88,7 +89,7 @@ static async Task OpenSessionViaWindowsAuthenticationAsync(OpenSessi }; } } - static async Task internalGetAuthNonceAsync(IApiClient apiClient, CancellationToken cancellationToken) + static async Task internalGetAuthNonceAsync(IRestClient apiClient, CancellationToken cancellationToken) { var url = "auth/nonce"; var response = await apiClient.PostAsync @@ -97,7 +98,7 @@ static async Task internalGetAuthNonceAsync(IApiClient apiClient, Cancel return response.Data.Nonce; } - static async Task internalAuthExternalWindowAsync(IApiClient apiClient, string spaceName, string serverNonce, CancellationToken cancellationToken) + static async Task internalAuthExternalWindowAsync(IRestClient apiClient, string spaceName, string serverNonce, CancellationToken cancellationToken) { var url = "auth/external/windows"; var requestDto = new WindowsExternalLoginRequestDto diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 1ee9543..b70ea3e 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -11,77 +11,14 @@ using Morph.Server.Sdk.Dto.Errors; using System.IO; using Morph.Server.Sdk.Model; +using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Dto; namespace Morph.Server.Sdk.Client { - public interface IApiClient : IDisposable - { - HttpClient HttpClient { get; set; } - Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - - Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - - Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - - } - - - public sealed class FetchFileStreamData : IDisposable - { - public FetchFileStreamData(Stream stream, string fileName, long? fileSize) - { - Stream = stream ?? throw new ArgumentNullException(nameof(stream)); - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - FileSize = fileSize; - } - - public Stream Stream { get; private set; } - public string FileName { get; } - public long? FileSize { get; } - - public void Dispose() - { - if (Stream != null) - { - Stream.Dispose(); - Stream = null; - } - } - } - - public sealed class SendFileStreamData - { - public SendFileStreamData(Stream stream, string fileName, long fileSize) - { - Stream = stream ?? throw new ArgumentNullException(nameof(stream)); - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); - FileSize = fileSize; - } - - public Stream Stream { get; } - public string FileName { get; } - public long FileSize { get; } - } - - - public sealed class NoContentResult - { - - } - public sealed class NoContentRequest - { - - } - - - public class MorphServerRestClient : IApiClient + internal class MorphServerRestClient : IRestClient { private HttpClient httpClient; public HttpClient HttpClient { get => httpClient; set => httpClient = value; } @@ -299,16 +236,10 @@ protected async Task> RetrieveFileStreamAsync(Htt if (response.IsSuccessStatusCode) { var contentDisposition = response.Content.Headers.ContentDisposition; - DownloadFileInfo dfi = null; - if (contentDisposition != null) - { - dfi = new DownloadFileInfo - { - // need to fix double quotes, that may come from server response - // FileNameStar contains file name encoded in UTF8 - FileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"') - }; - } + // need to fix double quotes, that may come from server response + // FileNameStar contains file name encoded in UTF8 + var realFileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"'); + var contentLength = response.Content.Headers.ContentLength; // stream must be disposed by a caller @@ -322,7 +253,7 @@ protected async Task> RetrieveFileStreamAsync(Htt { response.Dispose(); }); - return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, dfi.FileName, contentLength)); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); } else diff --git a/src/Dto/NoContentRequest.cs b/src/Dto/NoContentRequest.cs new file mode 100644 index 0000000..666758f --- /dev/null +++ b/src/Dto/NoContentRequest.cs @@ -0,0 +1,10 @@ +namespace Morph.Server.Sdk.Dto +{ + internal sealed class NoContentRequest + { + + } + + + +} \ No newline at end of file diff --git a/src/Dto/NoContentResult.cs b/src/Dto/NoContentResult.cs new file mode 100644 index 0000000..6e35445 --- /dev/null +++ b/src/Dto/NoContentResult.cs @@ -0,0 +1,10 @@ +namespace Morph.Server.Sdk.Dto +{ + internal sealed class NoContentResult + { + + } + + + +} \ No newline at end of file diff --git a/src/Client/ApiSessionExtension.cs b/src/Helper/ApiSessionExtension.cs similarity index 78% rename from src/Client/ApiSessionExtension.cs rename to src/Helper/ApiSessionExtension.cs index 1dde893..19aea2f 100644 --- a/src/Client/ApiSessionExtension.cs +++ b/src/Helper/ApiSessionExtension.cs @@ -1,8 +1,9 @@ using Morph.Server.Sdk.Model; +using Morph.Server.Sdk.Model.InternalModels; -namespace Morph.Server.Sdk.Client +namespace Morph.Server.Sdk.Helper { - public static class ApiSessionExtension + internal static class ApiSessionExtension { public static HeadersCollection ToHeadersCollection(this ApiSession apiSession) { diff --git a/src/Client/UrlHelper.cs b/src/Helper/UrlHelper.cs similarity index 97% rename from src/Client/UrlHelper.cs rename to src/Helper/UrlHelper.cs index 452f0fc..0aea043 100644 --- a/src/Client/UrlHelper.cs +++ b/src/Helper/UrlHelper.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -namespace Morph.Server.Sdk.Client +namespace Morph.Server.Sdk.Helper { internal static class UrlHelper { diff --git a/src/Model/ClientConfiguration.cs b/src/Model/ClientConfiguration.cs new file mode 100644 index 0000000..19f4d55 --- /dev/null +++ b/src/Model/ClientConfiguration.cs @@ -0,0 +1,30 @@ +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; +using Morph.Server.Sdk.Client; + +namespace Morph.Server.Sdk.Model +{ + public class ClientConfiguration : IClientConfiguration + { + + public TimeSpan OperationTimeout { get; set; } = MorphServerApiClientGlobalConfig.OperationTimeout; + public TimeSpan FileTransferTimeout { get; set; } = MorphServerApiClientGlobalConfig.FileTransferTimeout; + public TimeSpan HttpClientTimeout { get; set; } = MorphServerApiClientGlobalConfig.HttpClientTimeout; + + public string ClientId { get; set; } = MorphServerApiClientGlobalConfig.ClientId; + public string ClientType { get; set; } = MorphServerApiClientGlobalConfig.ClientType; + + public Uri ApiUri { get; set; } + internal string SDKVersionString { get; set; } = MorphServerApiClientGlobalConfig.SDKVersionString; +#if NETSTANDARD2_0 + public Func ServerCertificateCustomValidationCallback { get; set; } + = MorphServerApiClientGlobalConfig.ServerCertificateCustomValidationCallback; +#endif + + } + +} + + diff --git a/src/Model/DownloadFileInfo.cs b/src/Model/DownloadFileInfo.cs index 2b94498..261bd41 100644 --- a/src/Model/DownloadFileInfo.cs +++ b/src/Model/DownloadFileInfo.cs @@ -6,13 +6,13 @@ namespace Morph.Server.Sdk.Model { - public class DownloadFileInfo - { - /// - /// File name - /// - public string FileName { get; set; } + //public class DownloadFileInfo + //{ + // /// + // /// File name + // /// + // public string FileName { get; set; } - } + //} } diff --git a/src/Model/IClientConfiguration.cs b/src/Model/IClientConfiguration.cs new file mode 100644 index 0000000..db849ef --- /dev/null +++ b/src/Model/IClientConfiguration.cs @@ -0,0 +1,21 @@ +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Net.Security; + +namespace Morph.Server.Sdk.Model +{ + public interface IClientConfiguration + { + TimeSpan OperationTimeout { get; } + TimeSpan FileTransferTimeout { get; } + TimeSpan HttpClientTimeout { get; } + + Uri ApiUri { get; } +#if NETSTANDARD2_0 + Func ServerCertificateCustomValidationCallback { get; } +#endif + } +} + + diff --git a/src/Client/ApiResult.cs b/src/Model/InternalModels/ApiResult.cs similarity index 90% rename from src/Client/ApiResult.cs rename to src/Model/InternalModels/ApiResult.cs index dd8678b..22f6159 100644 --- a/src/Client/ApiResult.cs +++ b/src/Model/InternalModels/ApiResult.cs @@ -1,8 +1,8 @@ using System; -namespace Morph.Server.Sdk.Client +namespace Morph.Server.Sdk.Model.InternalModels { - public class ApiResult + internal class ApiResult { public T Data { get; set; } = default(T); public Exception Error { get; set; } = default(Exception); diff --git a/src/Model/InternalModels/FetchFileStreamData.cs b/src/Model/InternalModels/FetchFileStreamData.cs new file mode 100644 index 0000000..2ae879c --- /dev/null +++ b/src/Model/InternalModels/FetchFileStreamData.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + internal sealed class FetchFileStreamData + { + public FetchFileStreamData(Stream stream, string fileName, long? fileSize) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + FileSize = fileSize; + } + + public Stream Stream { get;} + public string FileName { get; } + public long? FileSize { get; } + + } + + + +} \ No newline at end of file diff --git a/src/Client/HeadersCollection.cs b/src/Model/InternalModels/HeadersCollection.cs similarity index 91% rename from src/Client/HeadersCollection.cs rename to src/Model/InternalModels/HeadersCollection.cs index 9465fd8..ff32baa 100644 --- a/src/Client/HeadersCollection.cs +++ b/src/Model/InternalModels/HeadersCollection.cs @@ -2,17 +2,16 @@ using System.Net.Http.Headers; using System.Collections.Generic; -namespace Morph.Server.Sdk.Client +namespace Morph.Server.Sdk.Model.InternalModels { - public class HeadersCollection + internal class HeadersCollection { private Dictionary _headers = new Dictionary(); public HeadersCollection() { } - - + public void Add(string header, string value) { diff --git a/src/Client/OpenSessionAuthenticatorContext.cs b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs similarity index 78% rename from src/Client/OpenSessionAuthenticatorContext.cs rename to src/Model/InternalModels/OpenSessionAuthenticatorContext.cs index a699f57..c4162e1 100644 --- a/src/Client/OpenSessionAuthenticatorContext.cs +++ b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs @@ -1,9 +1,10 @@ -using System; +using Morph.Server.Sdk.Client; +using System; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace Morph.Server.Sdk.Client +namespace Morph.Server.Sdk.Model.InternalModels { internal class OpenSessionAuthenticatorContext { @@ -11,7 +12,7 @@ internal class OpenSessionAuthenticatorContext public OpenSessionAuthenticatorContext (ILowLevelApiClient lowLevelApiClient, IMorphServerApiClient morphServerApiClient, - Func buildApiClient + Func buildApiClient ) { @@ -23,7 +24,7 @@ Func buildApiClient public ILowLevelApiClient LowLevelApiClient { get; } public IMorphServerApiClient MorphServerApiClient { get; } - public Func BuildApiClient { get; } + public Func BuildApiClient { get; } } } diff --git a/src/Model/InternalModels/OperationType.cs b/src/Model/InternalModels/OperationType.cs new file mode 100644 index 0000000..0ff2c5b --- /dev/null +++ b/src/Model/InternalModels/OperationType.cs @@ -0,0 +1,10 @@ +namespace Morph.Server.Sdk.Model.InternalModels +{ + internal enum OperationType + { + ShortOperation = 1, + FileTransfer = 2 + } +} + + diff --git a/src/Model/InternalModels/SendFileStreamData.cs b/src/Model/InternalModels/SendFileStreamData.cs new file mode 100644 index 0000000..08648e7 --- /dev/null +++ b/src/Model/InternalModels/SendFileStreamData.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + internal sealed class SendFileStreamData + { + public SendFileStreamData(Stream stream, string fileName, long fileSize) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + FileSize = fileSize; + } + + public Stream Stream { get; } + public string FileName { get; } + public long FileSize { get; } + } + + + +} \ No newline at end of file diff --git a/src/Model/ServerStreamingData.cs b/src/Model/ServerStreamingData.cs new file mode 100644 index 0000000..a6a727c --- /dev/null +++ b/src/Model/ServerStreamingData.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; + +namespace Morph.Server.Sdk.Model +{ + public sealed class ServerStreamingData : IDisposable + { + public ServerStreamingData(Stream stream, string fileName, long? fileSize) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + FileSize = fileSize; + } + + public Stream Stream { get; private set; } + public string FileName { get; } + public long? FileSize { get; } + public void Dispose() + { + if (Stream != null) + { + Stream.Dispose(); + Stream = null; + } + } + } +} \ No newline at end of file diff --git a/src/Model/SpaceUploadFileRequest.cs b/src/Model/SpaceUploadFileRequest.cs new file mode 100644 index 0000000..fbc4239 --- /dev/null +++ b/src/Model/SpaceUploadFileRequest.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace Morph.Server.Sdk.Model +{ + public sealed class SpaceUploadFileRequest + { + public string ServerFolder { get; set; } + public Stream DataStream { get; set; } + public string FileName { get; set; } + public long FileSize { get; set; } + public bool OverwriteExistingFile { get; set; } = false; + } +} \ No newline at end of file From b25cfec1ee983c423b2291f6646acb8ff413078a Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 30 Jan 2019 10:43:47 +0200 Subject: [PATCH 10/37] sdk code refactoring - in progress --- src/Client/DataTransferUtility.cs | 19 ++++++++++ src/Client/ILowLevelApiClient.cs | 2 +- src/Client/IMorphServerApiClient.cs | 5 +-- src/Client/LowLevelApiClient.cs | 12 ++----- src/Client/MorphServerApiClient.cs | 35 +++++++++++-------- src/Client/MorphServerRestClient.cs | 2 +- ...gs.cs => FileTransferProgressEventArgs.cs} | 10 ++---- src/Helper/FileProgress.cs | 13 +++---- src/Helper/IFileProgress.cs | 2 +- src/Model/SpaceUploadFileRequest.cs | 4 ++- src/Model/StartTaskRequest.cs | 16 +++++++++ 11 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 src/Client/DataTransferUtility.cs rename src/Events/{FileEventArgs.cs => FileTransferProgressEventArgs.cs} (71%) create mode 100644 src/Model/StartTaskRequest.cs diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs new file mode 100644 index 0000000..460a170 --- /dev/null +++ b/src/Client/DataTransferUtility.cs @@ -0,0 +1,19 @@ +namespace Morph.Server.Sdk.Client +{ + /// + /// Transfer file from/to server to/from local file + /// + public class DataTransferUtility + { + private readonly IMorphServerApiClient morphServerApiClient; + + public DataTransferUtility(IMorphServerApiClient morphServerApiClient) + { + this.morphServerApiClient = morphServerApiClient; + } + + } + +} + + diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index c78f1bd..01ecb84 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -20,7 +20,7 @@ internal interface ILowLevelApiClient: IDisposable // RUN-STOP Task Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); + Task> StartTaskAsync(ApiSession apiSession, Guid taskId, TaskStartRequestDto taskStartRequestDto, CancellationToken cancellationToken); Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); // Tasks validation diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index 80d73f0..ba2531d 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -15,7 +15,8 @@ namespace Morph.Server.Sdk.Client public interface IMorphServerApiClient:IDisposable { - event EventHandler FileProgress; + event EventHandler OnFileDownloadProgress; + event EventHandler OnFileUploadProgress; IClientConfiguration Config { get; } @@ -25,7 +26,7 @@ public interface IMorphServerApiClient:IDisposable Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken cancellationToken); - Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null); + Task StartTaskAsync(ApiSession apiSession, StartTaskRequest startTaskRequest, CancellationToken cancellationToken); Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken); diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 589264f..6233eba 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -96,7 +96,7 @@ public Task> ServerGetStatusAsync(CancellationToken c return apiClient.GetAsync(url, null, new HeadersCollection(), cancellationToken); } - public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, TaskStartRequestDto taskStartRequestDto, CancellationToken cancellationToken) { if (apiSession == null) { @@ -105,14 +105,8 @@ public Task> StartTaskAsync(ApiSession apiSessio var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); - var dto = new TaskStartRequestDto(); - if (taskParameters != null) - { - dto.TaskParameters = taskParameters.Select(TaskParameterMapper.ToDto).ToList(); - } - - return apiClient.PostAsync(url, dto, null, apiSession.ToHeadersCollection(), cancellationToken); - + + return apiClient.PostAsync(url, taskStartRequestDto, null, apiSession.ToHeadersCollection(), cancellationToken); } public Task> StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 36a4385..a08d012 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -13,16 +13,19 @@ using System.Reflection; using System.IO; using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Dto; namespace Morph.Server.Sdk.Client { + /// /// Morph Server api client V1 /// public class MorphServerApiClient : IMorphServerApiClient, IDisposable { - public event EventHandler FileProgress; + public event EventHandler OnFileDownloadProgress; + public event EventHandler OnFileUploadProgress; protected readonly string _userAgent = "MorphServerApiClient/next"; protected readonly string _api_v1 = "api/v1/"; @@ -134,28 +137,32 @@ protected HttpClient BuildHttpClient(ClientConfiguration config, HttpClientHandl return client; } - - - - + /// /// Start Task like "fire and forget" /// - /// api session - /// tast guid - /// - /// - /// - public Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + public Task StartTaskAsync(ApiSession apiSession, StartTaskRequest startTaskRequest, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } + + if (startTaskRequest == null) + { + throw new ArgumentNullException(nameof(startTaskRequest)); + } + return Wrapped(async (token) => { - var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, taskId, token); + var requestDto = new TaskStartRequestDto(); + if (startTaskRequest.TaskParameters != null) + { + requestDto.TaskParameters = startTaskRequest.TaskParameters.Select(TaskParameterMapper.ToDto).ToList(); + } + + var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, startTaskRequest.TaskId, requestDto, token); return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); }, cancellationToken, OperationType.ShortOperation); @@ -359,9 +366,9 @@ public Task GetSpacesListAsync(CancellationToken cancella }, cancellationToken, OperationType.ShortOperation); } - private void DownloadProgress_StateChanged(object sender, FileEventArgs e) + private void DownloadProgress_StateChanged(object sender, FileTransferProgressEventArgs e) { - FileProgress?.Invoke(this, e); + OnFileDownloadProgress?.Invoke(this, e); } diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index b70ea3e..abd6524 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -198,7 +198,7 @@ ex.InnerException is WebException web && } } - private void DownloadProgress_StateChanged(object sender, Events.FileEventArgs e) + private void DownloadProgress_StateChanged(object sender, Events.FileTransferProgressEventArgs e) { // TODO: add handler //throw new NotImplementedException(); diff --git a/src/Events/FileEventArgs.cs b/src/Events/FileTransferProgressEventArgs.cs similarity index 71% rename from src/Events/FileEventArgs.cs rename to src/Events/FileTransferProgressEventArgs.cs index 97fb432..effe53b 100644 --- a/src/Events/FileEventArgs.cs +++ b/src/Events/FileTransferProgressEventArgs.cs @@ -1,18 +1,14 @@ using Morph.Server.Sdk.Model; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Morph.Server.Sdk.Events { - public class FileEventArgs : EventArgs + public class FileTransferProgressEventArgs : EventArgs { public FileProgressState State { get; set; } public long ProcessedBytes { get; set; } public long FileSize { get; set; } - public Guid? Guid { get; set; } + //public Guid? Guid { get; set; } public string FileName { get; set; } public double Percent { @@ -23,7 +19,7 @@ public double Percent return Math.Round((ProcessedBytes * 100.0 / FileSize), 2); } } - public FileEventArgs() + public FileTransferProgressEventArgs() { diff --git a/src/Helper/FileProgress.cs b/src/Helper/FileProgress.cs index 7b6f596..422244a 100644 --- a/src/Helper/FileProgress.cs +++ b/src/Helper/FileProgress.cs @@ -11,22 +11,20 @@ namespace Morph.Server.Sdk.Helper internal class FileProgress : IFileProgress { - public event EventHandler StateChanged; + public event EventHandler StateChanged; public long FileSize { get; private set; } - public string FileName { get; private set; } - private Guid? _guid; + public string FileName { get; private set; } public long ProcessedBytes { get; private set; } public FileProgressState State { get; private set; } public void ChangeState(FileProgressState state) { State = state; - StateChanged?.Invoke(this, new FileEventArgs + StateChanged?.Invoke(this, new FileTransferProgressEventArgs { ProcessedBytes = ProcessedBytes, State = state, - Guid = _guid, FileName = FileName, FileSize = FileSize @@ -37,11 +35,10 @@ public void SetProcessedBytes(long np) ProcessedBytes = np; } - public FileProgress(string fileName, long fileSize, Guid? guid = null) + public FileProgress(string fileName, long fileSize) { FileName = fileName; - FileSize = fileSize; - _guid = guid; + FileSize = fileSize; } } } diff --git a/src/Helper/IFileProgress.cs b/src/Helper/IFileProgress.cs index f4155ee..cc19c39 100644 --- a/src/Helper/IFileProgress.cs +++ b/src/Helper/IFileProgress.cs @@ -10,7 +10,7 @@ namespace Morph.Server.Sdk.Helper { internal interface IFileProgress { - event EventHandler StateChanged; + event EventHandler StateChanged; FileProgressState State { get; } long FileSize { get; } string FileName { get; } diff --git a/src/Model/SpaceUploadFileRequest.cs b/src/Model/SpaceUploadFileRequest.cs index fbc4239..89c2146 100644 --- a/src/Model/SpaceUploadFileRequest.cs +++ b/src/Model/SpaceUploadFileRequest.cs @@ -1,4 +1,6 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; namespace Morph.Server.Sdk.Model { diff --git a/src/Model/StartTaskRequest.cs b/src/Model/StartTaskRequest.cs new file mode 100644 index 0000000..da3f09f --- /dev/null +++ b/src/Model/StartTaskRequest.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Morph.Server.Sdk.Model +{ + public sealed class StartTaskRequest + { + public Guid TaskId { get; set; } + public IEnumerable TaskParameters { get; set; } + + public StartTaskRequest(Guid taskId) + { + TaskId = taskId; + } + } +} \ No newline at end of file From 084aaf75864f465f191884c24acac6ae6e04e931 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 30 Jan 2019 13:59:50 +0200 Subject: [PATCH 11/37] sdk code refactoring - in Progress working on progress indicator and streams cancellation --- src/Client/ILowLevelApiClient.cs | 7 +- src/Client/IRestClient.cs | 26 ++++-- src/Client/LowLevelApiClient.cs | 13 +-- src/Client/MorphServerApiClient.cs | 33 +++++-- src/Client/MorphServerRestClient.cs | 126 ++++++++++++++++---------- src/Dto/NoContentRequest.cs | 5 +- src/Dto/NoContentResult.cs | 5 +- src/Helper/FileProgress.cs | 19 +++- src/Helper/IFileProgress.cs | 2 +- src/Helper/JsonSerializationHelper.cs | 9 +- src/Helper/ProgressStreamContent.cs | 5 +- src/Model/ApiSession.cs | 2 +- src/Model/StartTaskRequest.cs | 8 +- 13 files changed, 175 insertions(+), 85 deletions(-) diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index 01ecb84..1cdf593 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -6,6 +6,7 @@ using Morph.Server.Sdk.Dto.Commands; using System.Collections.Generic; using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Events; namespace Morph.Server.Sdk.Client { @@ -47,9 +48,9 @@ internal interface ILowLevelApiClient: IDisposable Task> WebFilesBrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); - Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); - Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken); - Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken); + Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, Action onReceiveProgress, CancellationToken cancellationToken); + Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); + Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); } } diff --git a/src/Client/IRestClient.cs b/src/Client/IRestClient.cs index ceb145b..bc43040 100644 --- a/src/Client/IRestClient.cs +++ b/src/Client/IRestClient.cs @@ -4,22 +4,30 @@ using System.Threading.Tasks; using System.Collections.Specialized; using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Events; namespace Morph.Server.Sdk.Client { internal interface IRestClient : IDisposable { HttpClient HttpClient { get; set; } - Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new(); + Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new(); + Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new(); + Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new(); + Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new(); + Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) + where TResult : new(); + Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) + where TResult : new(); - Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); - - Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken); + Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken); + } diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 6233eba..bc02e69 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -11,6 +11,7 @@ using Morph.Server.Sdk.Exceptions; using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Helper; +using Morph.Server.Sdk.Events; namespace Morph.Server.Sdk.Client { @@ -187,7 +188,7 @@ public void Dispose() apiClient.Dispose(); } - public Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) + public Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, Action onReceiveProgress, CancellationToken cancellationToken) { if (apiSession == null) { @@ -196,7 +197,7 @@ public Task> WebFilesDownloadFileAsync(ApiSession var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); - return apiClient.RetrieveFileGetAsync(url, null, apiSession.ToHeadersCollection(), cancellationToken); + return apiClient.RetrieveFileGetAsync(url, null, apiSession.ToHeadersCollection(), onReceiveProgress, cancellationToken); } public async Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) @@ -229,7 +230,7 @@ public async Task> WebFileExistsAsync(ApiSession apiSession, str } } - public Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken) + public Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) { if (apiSession == null) { @@ -244,11 +245,11 @@ public Task> WebFilesPutFileAsync(ApiSession apiSessi var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); - return apiClient.PutFileStreamAsync(url,sendFileStreamData, null, apiSession.ToHeadersCollection(), cancellationToken); + return apiClient.PutFileStreamAsync(url,sendFileStreamData, null, apiSession.ToHeadersCollection(), onSendProgress, cancellationToken); } - public Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, CancellationToken cancellationToken) + public Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) { if (apiSession == null) { @@ -263,7 +264,7 @@ public Task> WebFilesPostFileAsync(ApiSession apiSess var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); - return apiClient.PostFileStreamAsync(url, sendFileStreamData, null, apiSession.ToHeadersCollection(), cancellationToken); + return apiClient.PostFileStreamAsync(url, sendFileStreamData, null, apiSession.ToHeadersCollection(), onSendProgress, cancellationToken); } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index a08d012..1e20801 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -161,8 +161,12 @@ public Task StartTaskAsync(ApiSession apiSession, StartTaskRe { requestDto.TaskParameters = startTaskRequest.TaskParameters.Select(TaskParameterMapper.ToDto).ToList(); } - - var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, startTaskRequest.TaskId, requestDto, token); + + if (!startTaskRequest.TaskId.HasValue) + { + throw new Exception("TaskId mus be set."); + } + var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, startTaskRequest.TaskId.Value, requestDto, token); return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); }, cancellationToken, OperationType.ShortOperation); @@ -181,7 +185,7 @@ internal virtual Task Wrapped(Func Wrapped(Func SpaceDownloadFileAsync(ApiSession apiSession, s return Wrapped(async (token) => { - var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + Action onReceiveProgress = (data) => + { + OnFileDownloadProgress?.Invoke(this, data); + }; + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, onReceiveProgress, token); return MapOrFail(apiResult, (data) => new ServerStreamingData(data.Stream, data.FileName, data.FileSize) ); @@ -599,7 +610,11 @@ public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string r return Wrapped(async (token) => { - var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, token); + Action onReceiveProgress = (data) => + { + OnFileDownloadProgress?.Invoke(this, data); + }; + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, onReceiveProgress, token); return MapOrFail(apiResult, (data) => data.Stream); }, cancellationToken, OperationType.FileTransfer); @@ -619,14 +634,18 @@ public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest s return Wrapped(async (token) => { + Action onSendProgress = (data) => + { + OnFileUploadProgress?.Invoke(this, data); + }; var sendStreamData = new SendFileStreamData( spaceUploadFileRequest.DataStream, spaceUploadFileRequest.FileName, spaceUploadFileRequest.FileSize); var apiResult = spaceUploadFileRequest.OverwriteExistingFile ? - await _lowLevelApiClient.WebFilesPutFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, token) : - await _lowLevelApiClient.WebFilesPostFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, token); + await _lowLevelApiClient.WebFilesPutFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token) : + await _lowLevelApiClient.WebFilesPostFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token); FailIfError(apiResult); return Task.FromResult(0); diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index abd6524..e7b12dd 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -13,6 +13,7 @@ using Morph.Server.Sdk.Model; using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Dto; +using Morph.Server.Sdk.Events; namespace Morph.Server.Sdk.Client { @@ -28,11 +29,13 @@ public MorphServerRestClient(HttpClient httpClient) HttpClient = httpClient; } public Task> DeleteAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { return SendAsyncApiResult(HttpMethod.Delete, url, null, urlParameters, headersCollection, cancellationToken); } public Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { if (urlParameters == null) { @@ -43,16 +46,19 @@ public Task> GetAsync(string url, NameValueCollectio } public Task> PostAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { return SendAsyncApiResult(HttpMethod.Post, url, model, urlParameters, headersCollection, cancellationToken); } public Task> PutAsync(string url, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { return SendAsyncApiResult(HttpMethod.Put, url, model, urlParameters, headersCollection, cancellationToken); } protected virtual async Task> SendAsyncApiResult(HttpMethod httpMethod, string path, TModel model, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { StringContent stringContent = null; if (model != null) @@ -71,6 +77,7 @@ protected virtual async Task> SendAsyncApiResult> HandleResponse(HttpResponseMessage response) + where TResult : new() { if (response.IsSuccessStatusCode) { @@ -158,7 +165,9 @@ public void Dispose() public async Task> SendFileStreamAsync( HttpMethod httpMethod, string path, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, + Action onSendProgress, CancellationToken cancellationToken) + where TResult : new() { try { @@ -166,11 +175,11 @@ public async Task> SendFileStreamAsync( using (var content = new MultipartFormDataContent(boundary)) { - var downloadProgress = new FileProgress(sendFileStreamData.FileName, sendFileStreamData.FileSize); - downloadProgress.StateChanged += DownloadProgress_StateChanged; - using (cancellationToken.Register(() => downloadProgress.ChangeState(FileProgressState.Cancelled))) + var uploadProgress = new FileProgress(sendFileStreamData.FileName, sendFileStreamData.FileSize, onSendProgress); + + using (cancellationToken.Register(() => uploadProgress.ChangeState(FileProgressState.Cancelled))) { - using (var streamContent = new ProgressStreamContent(sendFileStreamData.Stream, downloadProgress)) + using (var streamContent = new ProgressStreamContent(sendFileStreamData.Stream, uploadProgress)) { content.Add(streamContent, "files", Path.GetFileName(sendFileStreamData.FileName)); var url = path + urlParameters.ToQueryString(); @@ -198,36 +207,7 @@ ex.InnerException is WebException web && } } - private void DownloadProgress_StateChanged(object sender, Events.FileTransferProgressEventArgs e) - { - // TODO: add handler - //throw new NotImplementedException(); - } - - public Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - return SendFileStreamAsync(HttpMethod.Put, url, sendFileStreamData, urlParameters, headersCollection, cancellationToken); - } - - public Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - return SendFileStreamAsync(HttpMethod.Post, url, sendFileStreamData, urlParameters, headersCollection, cancellationToken); - } - - - public Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) - { - if (urlParameters == null) - { - urlParameters = new NameValueCollection(); - } - urlParameters.Add("_", DateTime.Now.Ticks.ToString()); - return RetrieveFileStreamAsync(HttpMethod.Get, url, urlParameters, headersCollection, cancellationToken); - } - - - - protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken) { var url = path + urlParameters.ToQueryString(); HttpResponseMessage response = await httpClient.SendAsync( @@ -239,22 +219,47 @@ protected async Task> RetrieveFileStreamAsync(Htt // need to fix double quotes, that may come from server response // FileNameStar contains file name encoded in UTF8 var realFileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"'); - var contentLength = response.Content.Headers.ContentLength; - // stream must be disposed by a caller - Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); - var streamWithProgress = new StreamWithProgress(streamToReadFrom, - e => - { + FileProgress downloadProgress = null; - }, - () => + if (contentLength.HasValue) { - response.Dispose(); - }); - return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); - + downloadProgress = new FileProgress(realFileName, contentLength.Value, onReceiveProgress); + } + downloadProgress?.ChangeState(FileProgressState.Starting); + var totalProcessedBytes = 0; + + { + // stream must be disposed by a caller + Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); + DateTime _lastUpdate = DateTime.MinValue; + + var streamWithProgress = new StreamWithProgress(streamToReadFrom, + e => + { + if (downloadProgress != null) + { + totalProcessedBytes += e.BytesProcessed; + if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) + { + downloadProgress.SetProcessedBytes(totalProcessedBytes); + _lastUpdate = DateTime.Now; + } + + } + }, + () => + { + + if (downloadProgress != null && downloadProgress.ProcessedBytes!= totalProcessedBytes) + { + downloadProgress.ChangeState(FileProgressState.Cancelled); + } + response.Dispose(); + }); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); + } } else { @@ -264,7 +269,36 @@ protected async Task> RetrieveFileStreamAsync(Htt } } + + public Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) + where TResult : new() + { + return SendFileStreamAsync(HttpMethod.Put, url, sendFileStreamData, urlParameters, headersCollection, onSendProgress, cancellationToken); + } + + public Task> PostFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) + where TResult : new() + { + return SendFileStreamAsync(HttpMethod.Post, url, sendFileStreamData, urlParameters, headersCollection, onSendProgress, cancellationToken); + } + + + public Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken) + { + if (urlParameters == null) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return RetrieveFileStreamAsync(HttpMethod.Get, url, urlParameters, headersCollection, onReceiveProgress, cancellationToken); + } + + + + + public Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) + where TResult : new() { if (urlParameters == null) { diff --git a/src/Dto/NoContentRequest.cs b/src/Dto/NoContentRequest.cs index 666758f..6cd7609 100644 --- a/src/Dto/NoContentRequest.cs +++ b/src/Dto/NoContentRequest.cs @@ -1,5 +1,8 @@ -namespace Morph.Server.Sdk.Dto +using System.Runtime.Serialization; + +namespace Morph.Server.Sdk.Dto { + [DataContractAttribute] internal sealed class NoContentRequest { diff --git a/src/Dto/NoContentResult.cs b/src/Dto/NoContentResult.cs index 6e35445..8d1043b 100644 --- a/src/Dto/NoContentResult.cs +++ b/src/Dto/NoContentResult.cs @@ -1,5 +1,8 @@ -namespace Morph.Server.Sdk.Dto +using System.Runtime.Serialization; + +namespace Morph.Server.Sdk.Dto { + internal sealed class NoContentResult { diff --git a/src/Helper/FileProgress.cs b/src/Helper/FileProgress.cs index 422244a..096d5f6 100644 --- a/src/Helper/FileProgress.cs +++ b/src/Helper/FileProgress.cs @@ -11,7 +11,9 @@ namespace Morph.Server.Sdk.Helper internal class FileProgress : IFileProgress { - public event EventHandler StateChanged; + private readonly Action onProgress; + + //public event EventHandler StateChanged; public long FileSize { get; private set; } public string FileName { get; private set; } @@ -21,7 +23,7 @@ internal class FileProgress : IFileProgress public void ChangeState(FileProgressState state) { State = state; - StateChanged?.Invoke(this, new FileTransferProgressEventArgs + onProgress?.Invoke(new FileTransferProgressEventArgs { ProcessedBytes = ProcessedBytes, State = state, @@ -33,12 +35,21 @@ public void ChangeState(FileProgressState state) public void SetProcessedBytes(long np) { ProcessedBytes = np; + if(ProcessedBytes!= FileSize) + { + ChangeState(FileProgressState.Processing); + } + if(ProcessedBytes == FileSize && State !=FileProgressState.Finishing) + { + ChangeState(FileProgressState.Finishing); + } } - public FileProgress(string fileName, long fileSize) + public FileProgress(string fileName, long fileSize, Action onProgress) { FileName = fileName; - FileSize = fileSize; + FileSize = fileSize; + this.onProgress = onProgress; } } } diff --git a/src/Helper/IFileProgress.cs b/src/Helper/IFileProgress.cs index cc19c39..5f441c0 100644 --- a/src/Helper/IFileProgress.cs +++ b/src/Helper/IFileProgress.cs @@ -10,7 +10,7 @@ namespace Morph.Server.Sdk.Helper { internal interface IFileProgress { - event EventHandler StateChanged; + // event EventHandler StateChanged; FileProgressState State { get; } long FileSize { get; } string FileName { get; } diff --git a/src/Helper/JsonSerializationHelper.cs b/src/Helper/JsonSerializationHelper.cs index d125dcd..9f2cd24 100644 --- a/src/Helper/JsonSerializationHelper.cs +++ b/src/Helper/JsonSerializationHelper.cs @@ -1,4 +1,5 @@ -using Morph.Server.Sdk.Dto.Commands; +using Morph.Server.Sdk.Dto; +using Morph.Server.Sdk.Dto.Commands; using Morph.Server.Sdk.Exceptions; using System; using System.Collections.Generic; @@ -14,9 +15,15 @@ namespace Morph.Server.Sdk.Helper internal static class JsonSerializationHelper { public static T Deserialize(string input) + where T: new() { try { + var tType = typeof(T); + if ( tType == typeof(NoContentResult)) + { + return new T(); + } var serializer = new DataContractJsonSerializer(typeof(T)); var d = Encoding.UTF8.GetBytes(input); using (var ms = new MemoryStream(d)) diff --git a/src/Helper/ProgressStreamContent.cs b/src/Helper/ProgressStreamContent.cs index 50321ad..0a2ba3f 100644 --- a/src/Helper/ProgressStreamContent.cs +++ b/src/Helper/ProgressStreamContent.cs @@ -64,10 +64,9 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon await stream.WriteAsync(buffer, 0, length); processed += length; - if (DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(250)) + if (DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) { - _fileProgress.SetProcessedBytes(processed); - _fileProgress.ChangeState(FileProgressState.Processing); + _fileProgress.SetProcessedBytes(processed); _lastUpdate = DateTime.Now; } } diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index f1a21e3..3db5898 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -72,7 +72,7 @@ public void Dispose() this.IsClosed = true; } } - catch (Exception) + catch (Exception ex) { } diff --git a/src/Model/StartTaskRequest.cs b/src/Model/StartTaskRequest.cs index da3f09f..a4157f7 100644 --- a/src/Model/StartTaskRequest.cs +++ b/src/Model/StartTaskRequest.cs @@ -5,12 +5,16 @@ namespace Morph.Server.Sdk.Model { public sealed class StartTaskRequest { - public Guid TaskId { get; set; } + public Guid? TaskId { get; set; } public IEnumerable TaskParameters { get; set; } public StartTaskRequest(Guid taskId) { TaskId = taskId; - } + } + public StartTaskRequest() + { + + } } } \ No newline at end of file From 1ee7e807c52806d3ed20687069f8f36ef6401816 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 30 Jan 2019 20:11:50 +0200 Subject: [PATCH 12/37] ems-sdk 1.4.0 - work in progress --- src/Client/DataTransferUtility.cs | 41 ++++++++++++++++++--- src/Client/IDataTransferUtility.cs | 10 +++++ src/Client/MorphServerApiClient.cs | 19 +++++----- src/Client/MorphServerRestClient.cs | 57 ++++++++++++++++++----------- src/Dto/SpaceBrowsingResponseDto.cs | 6 +-- src/Helper/StreamWithProgress.cs | 23 +++++++++++- src/Mappers/SpaceBrowsingMapper.cs | 16 +------- src/Mappers/SpaceStatusMapper.cs | 6 +-- src/Model/SpaceBrowsingInfo.cs | 18 +-------- src/Model/SpaceStatus.cs | 4 +- 10 files changed, 121 insertions(+), 79 deletions(-) create mode 100644 src/Client/IDataTransferUtility.cs diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 460a170..f168224 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -1,15 +1,46 @@ -namespace Morph.Server.Sdk.Client +using Morph.Server.Sdk.Model; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Client { /// /// Transfer file from/to server to/from local file /// - public class DataTransferUtility + public class DataTransferUtility : IDataTransferUtility { - private readonly IMorphServerApiClient morphServerApiClient; + private readonly IMorphServerApiClient _morphServerApiClient; + private readonly ApiSession _apiSession; - public DataTransferUtility(IMorphServerApiClient morphServerApiClient) + public DataTransferUtility(IMorphServerApiClient morphServerApiClient, ApiSession apiSession) { - this.morphServerApiClient = morphServerApiClient; + this._morphServerApiClient = morphServerApiClient ?? throw new ArgumentNullException(nameof(morphServerApiClient)); + this._apiSession = apiSession ?? throw new ArgumentNullException(nameof(apiSession)); + } + + public async Task SpaceUploadFileAsync(string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteExistingFile = false) + { + if (!File.Exists(localFilePath)) + { + throw new FileNotFoundException(string.Format("File '{0}' not found", localFilePath)); + } + var fileSize = new FileInfo(localFilePath).Length; + var fileName = Path.GetFileName(localFilePath); + using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) + { + var request = new SpaceUploadFileRequest + { + DataStream = fsSource, + FileName = fileName, + FileSize = fileSize, + OverwriteExistingFile = overwriteExistingFile, + ServerFolder = destFolderPath + }; + await _morphServerApiClient.SpaceUploadFileAsync(_apiSession, request, cancellationToken); + return; + } } } diff --git a/src/Client/IDataTransferUtility.cs b/src/Client/IDataTransferUtility.cs new file mode 100644 index 0000000..9117a6e --- /dev/null +++ b/src/Client/IDataTransferUtility.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Client +{ + public interface IDataTransferUtility + { + Task SpaceUploadFileAsync(string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteExistingFile = false); + } +} \ No newline at end of file diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 1e20801..a306378 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -172,7 +172,7 @@ public Task StartTaskAsync(ApiSession apiSession, StartTaskRe }, cancellationToken, OperationType.ShortOperation); } - internal virtual Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) + internal virtual async Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) { TimeSpan maxExecutionTime; switch (operationType) @@ -183,22 +183,21 @@ internal virtual Task Wrapped(Func GetServerStatusAsync(CancellationToken cancellationTok }, cancellationToken, OperationType.ShortOperation); } - public Task GetSpacesListAsync(CancellationToken cancellationToken) + public async Task GetSpacesListAsync(CancellationToken cancellationToken) { - return Wrapped(async (token) => + return await Wrapped(async (token) => { var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index e7b12dd..c37ead6 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -182,7 +182,7 @@ public async Task> SendFileStreamAsync( using (var streamContent = new ProgressStreamContent(sendFileStreamData.Stream, uploadProgress)) { content.Add(streamContent, "files", Path.GetFileName(sendFileStreamData.FileName)); - var url = path + urlParameters.ToQueryString(); + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); var requestMessage = BuildHttpRequestMessage(httpMethod, url, content, headersCollection); using (requestMessage) { @@ -209,7 +209,7 @@ ex.InnerException is WebException web && protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken) { - var url = path + urlParameters.ToQueryString(); + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); HttpResponseMessage response = await httpClient.SendAsync( BuildHttpRequestMessage(httpMethod, url, null, headersCollection), HttpCompletionOption.ResponseHeadersRead, cancellationToken); { @@ -235,36 +235,49 @@ protected async Task> RetrieveFileStreamAsync(Htt Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); DateTime _lastUpdate = DateTime.MinValue; - var streamWithProgress = new StreamWithProgress(streamToReadFrom, - e => - { - if (downloadProgress != null) + + var streamWithProgress = new StreamWithProgress(streamToReadFrom, cancellationToken, + e => { - totalProcessedBytes += e.BytesProcessed; - if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) + if (downloadProgress != null) { - downloadProgress.SetProcessedBytes(totalProcessedBytes); - _lastUpdate = DateTime.Now; + totalProcessedBytes += e.BytesProcessed; + if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) + { + downloadProgress.SetProcessedBytes(totalProcessedBytes); + _lastUpdate = DateTime.Now; + } + } - + }, + () => + { + + if (downloadProgress != null && downloadProgress.ProcessedBytes != totalProcessedBytes) + { + downloadProgress.ChangeState(FileProgressState.Cancelled); } + response.Dispose(); }, - () => - { + ()=> { + throw new Exception("Timeout"); - if (downloadProgress != null && downloadProgress.ProcessedBytes!= totalProcessedBytes) - { - downloadProgress.ChangeState(FileProgressState.Cancelled); - } - response.Dispose(); - }); - return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); + }); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); + } } else { - var error = await BuildExceptionFromResponse(response); - return ApiResult.Fail(error); + try + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); + } + finally + { + response.Dispose(); + } } } } diff --git a/src/Dto/SpaceBrowsingResponseDto.cs b/src/Dto/SpaceBrowsingResponseDto.cs index 57e2a3d..9224578 100644 --- a/src/Dto/SpaceBrowsingResponseDto.cs +++ b/src/Dto/SpaceBrowsingResponseDto.cs @@ -17,14 +17,10 @@ internal sealed class SpaceBrowsingResponseDto [DataMember(Name = "navigationChain")] public List NavigationChain { get; set; } [DataMember(Name = "freeSpaceBytes")] - public ulong FreeSpaceBytes { get; set; } - [DataMember(Name = "webFilesAccesMode")] - public string WebFilesAccesMode { get; set; } + public ulong FreeSpaceBytes { get; set; } [DataMember(Name = "spaceName")] public string SpaceName { get; set; } - - public SpaceBrowsingResponseDto() { Folders = new List(); diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index 899d536..2e644f0 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -11,16 +11,23 @@ internal class StreamWithProgress : Stream private readonly Action onReadProgress; private readonly Action onWriteProgress = null; private readonly Action onDisposed; + private readonly Action onTokenCancelled; + private readonly CancellationToken mainTokem; public StreamWithProgress(Stream stream, + CancellationToken mainTokem, Action onReadProgress = null, - Action onDisposed = null + Action onDisposed = null, + Action onTokenCancelled = null + ) { this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); this.onReadProgress = onReadProgress; this.onDisposed = onDisposed; + this.onTokenCancelled = onTokenCancelled; + this.mainTokem = mainTokem; } public override bool CanRead => stream.CanRead; @@ -39,6 +46,10 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { + if (mainTokem.IsCancellationRequested) + { + onTokenCancelled(); + } var bytesRead = stream.Read(buffer, offset, count); RaiseOnReadProgress(bytesRead); return bytesRead; @@ -97,7 +108,11 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) - { + { + if (mainTokem.IsCancellationRequested) + { + onTokenCancelled(); + } await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); } } @@ -128,6 +143,10 @@ public override object InitializeLifetimeService() public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + if (mainTokem.IsCancellationRequested) + { + onTokenCancelled(); + } var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); RaiseOnReadProgress(bytesRead); return bytesRead; diff --git a/src/Mappers/SpaceBrowsingMapper.cs b/src/Mappers/SpaceBrowsingMapper.cs index bea89f0..7111909 100644 --- a/src/Mappers/SpaceBrowsingMapper.cs +++ b/src/Mappers/SpaceBrowsingMapper.cs @@ -18,23 +18,11 @@ public static SpaceBrowsingInfo MapFromDto(SpaceBrowsingResponseDto dto) Folders = dto.Folders?.Select(Map).ToList(), NavigationChain = dto.NavigationChain?.Select(Map).ToList(), FreeSpaceBytes = dto.FreeSpaceBytes, - SpaceName = dto.SpaceName, - WebFilesAccesMode = Parse(dto.WebFilesAccesMode) + SpaceName = dto.SpaceName }; } - private static WebFilesAccesMode Parse(string value) - { - if (value == null) - return WebFilesAccesMode.Unknown; - WebFilesAccesMode webFilesAccesMode; - if(Enum.TryParse(value, out webFilesAccesMode)) - { - return webFilesAccesMode; - } - return WebFilesAccesMode.Unknown; - } - + private static SpaceFileInfo Map(SpaceFileItemDto dto) { return new SpaceFileInfo diff --git a/src/Mappers/SpaceStatusMapper.cs b/src/Mappers/SpaceStatusMapper.cs index 9aa0fb2..971f640 100644 --- a/src/Mappers/SpaceStatusMapper.cs +++ b/src/Mappers/SpaceStatusMapper.cs @@ -16,14 +16,14 @@ public static SpaceStatus MapFromDto(SpaceStatusDto dto) { IsPublic = dto.IsPublic, SpaceName = dto.SpaceName, - SpacePermissions = dto.UserPermissions?.Select(MapPermission)?.Where(x => x.HasValue)?.Select(x => x.Value)?.ToList().AsReadOnly() + UserPermissions = dto.UserPermissions?.Select(MapPermission)?.Where(x => x.HasValue)?.Select(x => x.Value)?.ToList().AsReadOnly() }; } - private static SpacePermission? MapPermission(string permission) + private static UserSpacePermission? MapPermission(string permission) { - if (Enum.TryParse(permission, true, out var p)) + if (Enum.TryParse(permission, true, out var p)) { return p; } diff --git a/src/Model/SpaceBrowsingInfo.cs b/src/Model/SpaceBrowsingInfo.cs index b492da4..c266294 100644 --- a/src/Model/SpaceBrowsingInfo.cs +++ b/src/Model/SpaceBrowsingInfo.cs @@ -13,8 +13,7 @@ public class SpaceBrowsingInfo { public ulong FreeSpaceBytes { get; set; } public string SpaceName { get; set; } - public WebFilesAccesMode WebFilesAccesMode { get; set; } - + public List Folders { get; set; } public List Files { get; set; } public List NavigationChain { get; set; } @@ -31,20 +30,7 @@ public bool FileExists(string fileName) return Files.Any(x => String.Equals(fileName, x.Name, StringComparison.OrdinalIgnoreCase)); } - public bool CanDownloadFiles - { - get - { - return WebFilesAccesMode == WebFilesAccesMode.FullAccess || WebFilesAccesMode == WebFilesAccesMode.OnlyDownload; - } - } - public bool CanUploadFiles - { - get - { - return WebFilesAccesMode == WebFilesAccesMode.FullAccess || WebFilesAccesMode == WebFilesAccesMode.OnlyUpload; - } - } + } diff --git a/src/Model/SpaceStatus.cs b/src/Model/SpaceStatus.cs index a5a6ae4..40a6559 100644 --- a/src/Model/SpaceStatus.cs +++ b/src/Model/SpaceStatus.cs @@ -10,11 +10,11 @@ public class SpaceStatus { public string SpaceName { get; internal set; } public bool IsPublic { get; internal set; } - public IReadOnlyList SpacePermissions { get; internal set; } + public IReadOnlyList UserPermissions { get; internal set; } } - public enum SpacePermission + public enum UserSpacePermission { TasksList, TaskLogView, From 7dd2c03b88e085a372cd5e339c6d9a1e8088c446 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 30 Jan 2019 20:13:28 +0200 Subject: [PATCH 13/37] DownloadFileInfo has been removed --- src/Model/DownloadFileInfo.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/Model/DownloadFileInfo.cs diff --git a/src/Model/DownloadFileInfo.cs b/src/Model/DownloadFileInfo.cs deleted file mode 100644 index 261bd41..0000000 --- a/src/Model/DownloadFileInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Morph.Server.Sdk.Model -{ - //public class DownloadFileInfo - //{ - // /// - // /// File name - // /// - // public string FileName { get; set; } - - - //} -} From bf3bc2c15e94159ad92b4270464971037c03b321 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 31 Jan 2019 17:10:27 +0200 Subject: [PATCH 14/37] TransferUtility now supports downloading file from space internal StreamWithProgress now takes file size as StreamLength and supports Position marker. Position will be updated on every read operation --- src/Client/DataTransferUtility.cs | 109 +++++++++++++++++++++++++++- src/Client/IDataTransferUtility.cs | 3 + src/Client/MorphServerApiClient.cs | 3 + src/Client/MorphServerRestClient.cs | 9 ++- src/Helper/StreamWithProgress.cs | 34 ++++++--- 5 files changed, 142 insertions(+), 16 deletions(-) diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index f168224..3f8d2fb 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -11,6 +11,7 @@ namespace Morph.Server.Sdk.Client /// public class DataTransferUtility : IDataTransferUtility { + private int BufferSize { get; set; } = 81920; private readonly IMorphServerApiClient _morphServerApiClient; private readonly ApiSession _apiSession; @@ -20,7 +21,7 @@ public DataTransferUtility(IMorphServerApiClient morphServerApiClient, ApiSessio this._apiSession = apiSession ?? throw new ArgumentNullException(nameof(apiSession)); } - public async Task SpaceUploadFileAsync(string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteExistingFile = false) + public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder, CancellationToken cancellationToken, bool overwriteExistingFile = false) { if (!File.Exists(localFilePath)) { @@ -36,13 +37,117 @@ public async Task SpaceUploadFileAsync(string localFilePath, string destFolderPa FileName = fileName, FileSize = fileSize, OverwriteExistingFile = overwriteExistingFile, - ServerFolder = destFolderPath + ServerFolder = serverFolder }; await _morphServerApiClient.SpaceUploadFileAsync(_apiSession, request, cancellationToken); return; } } + + + + public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string targetLocalFilePath, CancellationToken cancellationToken, bool overwriteExistingFile = false) + { + if (remoteFilePath == null) + { + throw new ArgumentNullException(nameof(remoteFilePath)); + } + + if (targetLocalFilePath == null) + { + throw new ArgumentNullException(nameof(targetLocalFilePath)); + } + + string destFileName = Path.GetFileName(targetLocalFilePath); + var localFolder = Path.GetDirectoryName(targetLocalFilePath); + var tempFile = Path.Combine(localFolder, Guid.NewGuid().ToString("D") + ".emtmp"); + + if (!overwriteExistingFile && File.Exists(destFileName)) + { + throw new Exception($"Destination file '{destFileName}' already exists."); + } + + + try + { + using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) + { + using (var serverStreamingData = await _morphServerApiClient.SpaceDownloadFileAsync(_apiSession, remoteFilePath, cancellationToken)) + { + await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); + } + } + + if (File.Exists(destFileName)) + { + File.Delete(destFileName); + } + File.Move(tempFile, destFileName); + + } + finally + { + //drop file + if (tempFile != null && File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + } + } + + public async Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string targetLocalFolder, CancellationToken cancellationToken, bool overwriteExistingFile = false) + { + if (remoteFilePath == null) + { + throw new ArgumentNullException(nameof(remoteFilePath)); + } + + if (targetLocalFolder == null) + { + throw new ArgumentNullException(nameof(targetLocalFolder)); + } + + string destFileName = null; + var tempFile = Path.Combine(targetLocalFolder, Guid.NewGuid().ToString("D") + ".emtmp"); + try + { + using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) + { + + using (var serverStreamingData = await _morphServerApiClient.SpaceDownloadFileAsync(_apiSession, remoteFilePath, cancellationToken)) + { + destFileName = Path.Combine(targetLocalFolder, serverStreamingData.FileName); + + if (!overwriteExistingFile && File.Exists(destFileName)) + { + throw new Exception($"Destination file '{destFileName}' already exists."); + } + + await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); + } + } + + if (File.Exists(destFileName)) + { + File.Delete(destFileName); + } + File.Move(tempFile, destFileName); + + } + finally + { + //drop file + if (tempFile != null && File.Exists(tempFile)) + { + File.Delete(tempFile); + } + + } + + } + } } diff --git a/src/Client/IDataTransferUtility.cs b/src/Client/IDataTransferUtility.cs index 9117a6e..b26e53e 100644 --- a/src/Client/IDataTransferUtility.cs +++ b/src/Client/IDataTransferUtility.cs @@ -6,5 +6,8 @@ namespace Morph.Server.Sdk.Client public interface IDataTransferUtility { Task SpaceUploadFileAsync(string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteExistingFile = false); + Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string targetLocalFilePath, CancellationToken cancellationToken, bool overwriteExistingFile = false); + Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string targetLocalFolder, CancellationToken cancellationToken, bool overwriteExistingFile = false); } + } \ No newline at end of file diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index a306378..1c7ca96 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -124,6 +124,9 @@ protected HttpClient BuildHttpClient(ClientConfiguration config, HttpClientHandl client.DefaultRequestHeaders.Add("X-Client-Id", config.ClientId); client.DefaultRequestHeaders.Add("X-Client-Sdk", config.SDKVersionString); + client.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); + client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=60"); + client.MaxResponseContentBufferSize = 100 * 1024; client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index c37ead6..d716aa0 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -220,6 +220,10 @@ protected async Task> RetrieveFileStreamAsync(Htt // FileNameStar contains file name encoded in UTF8 var realFileName = (contentDisposition.FileNameStar ?? contentDisposition.FileName).TrimStart('\"').TrimEnd('\"'); var contentLength = response.Content.Headers.ContentLength; + if (!contentLength.HasValue) + { + throw new Exception("Response content length header is not set by the server."); + } FileProgress downloadProgress = null; @@ -233,10 +237,9 @@ protected async Task> RetrieveFileStreamAsync(Htt { // stream must be disposed by a caller Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); - DateTime _lastUpdate = DateTime.MinValue; - + DateTime _lastUpdate = DateTime.MinValue; - var streamWithProgress = new StreamWithProgress(streamToReadFrom, cancellationToken, + var streamWithProgress = new StreamWithProgress(streamToReadFrom, contentLength.Value, cancellationToken , e => { if (downloadProgress != null) diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index 2e644f0..a00eeb2 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -8,13 +8,18 @@ namespace Morph.Server.Sdk.Helper internal class StreamWithProgress : Stream { private readonly Stream stream; + private readonly long streamLength; private readonly Action onReadProgress; private readonly Action onWriteProgress = null; private readonly Action onDisposed; private readonly Action onTokenCancelled; - private readonly CancellationToken mainTokem; + private readonly CancellationToken httpTimeoutToken; - public StreamWithProgress(Stream stream, + private long _readPosition = 0; + + + public StreamWithProgress(Stream httpStream, + long streamLength, CancellationToken mainTokem, Action onReadProgress = null, Action onDisposed = null, @@ -22,12 +27,13 @@ public StreamWithProgress(Stream stream, ) { - this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + this.stream = httpStream ?? throw new ArgumentNullException(nameof(httpStream)); + this.streamLength = streamLength; this.onReadProgress = onReadProgress; this.onDisposed = onDisposed; this.onTokenCancelled = onTokenCancelled; - this.mainTokem = mainTokem; + this.httpTimeoutToken = mainTokem; } public override bool CanRead => stream.CanRead; @@ -35,9 +41,9 @@ public StreamWithProgress(Stream stream, public override bool CanWrite => stream.CanWrite; - public override long Length => stream.Length; + public override long Length => streamLength; - public override long Position { get => stream.Position; set => stream.Position = value; } + public override long Position { get => _readPosition; set => throw new NotImplementedException(); } public override void Flush() { @@ -46,17 +52,18 @@ public override void Flush() public override int Read(byte[] buffer, int offset, int count) { - if (mainTokem.IsCancellationRequested) + if (httpTimeoutToken.IsCancellationRequested) { onTokenCancelled(); } - var bytesRead = stream.Read(buffer, offset, count); + var bytesRead = stream.Read(buffer, offset, count); RaiseOnReadProgress(bytesRead); return bytesRead; } private void RaiseOnReadProgress(int bytesRead) { + _readPosition += bytesRead; if (onReadProgress != null) { var args = new StreamProgressEventArgs(bytesRead ); @@ -109,7 +116,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance int bytesRead; while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { - if (mainTokem.IsCancellationRequested) + if (httpTimeoutToken.IsCancellationRequested) { onTokenCancelled(); } @@ -143,7 +150,7 @@ public override object InitializeLifetimeService() public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - if (mainTokem.IsCancellationRequested) + if (httpTimeoutToken.IsCancellationRequested) { onTokenCancelled(); } @@ -153,7 +160,12 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, } public override int ReadByte() { - return stream.ReadByte(); + var @byte = stream.ReadByte(); + if (@byte != -1) + { + RaiseOnReadProgress(1); + } + return @byte; } public override int ReadTimeout { get => stream.ReadTimeout; set => stream.ReadTimeout = value; } public override string ToString() From 5667002514303d5d466a5bf4080ce1f845d487fc Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 31 Jan 2019 18:46:13 +0200 Subject: [PATCH 15/37] code refactoring - in progress --- src/Client/DataTransferUtility.cs | 28 +++++++++++++++++++++++++--- src/Client/IDataTransferUtility.cs | 5 +++-- src/Client/ILowLevelApiClient.cs | 4 ++-- src/Client/IMorphServerApiClient.cs | 10 +++++----- src/Client/LowLevelApiClient.cs | 4 ++-- src/Client/MorphServerApiClient.cs | 22 +++++++++++----------- src/Model/ApiSession.cs | 2 +- src/Model/SpaceStatus.cs | 17 ----------------- 8 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 3f8d2fb..5004aa6 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -39,7 +39,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder OverwriteExistingFile = overwriteExistingFile, ServerFolder = serverFolder }; - await _morphServerApiClient.SpaceUploadFileAsync(_apiSession, request, cancellationToken); + await _morphServerApiClient.SpaceUploadStreamAsync(_apiSession, request, cancellationToken); return; } } @@ -73,7 +73,7 @@ public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string t { using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceDownloadFileAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); } @@ -116,7 +116,7 @@ public async Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceDownloadFileAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { destFileName = Path.Combine(targetLocalFolder, serverStreamingData.FileName); @@ -148,6 +148,28 @@ public async Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string } + public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder, string destFileName, CancellationToken cancellationToken, bool overwriteExistingFile = false) + { + if (!File.Exists(localFilePath)) + { + throw new FileNotFoundException(string.Format("File '{0}' not found", localFilePath)); + } + var fileSize = new FileInfo(localFilePath).Length; + + using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) + { + var request = new SpaceUploadFileRequest + { + DataStream = fsSource, + FileName = destFileName, + FileSize = fileSize, + OverwriteExistingFile = overwriteExistingFile, + ServerFolder = serverFolder + }; + await _morphServerApiClient.SpaceUploadStreamAsync(_apiSession, request, cancellationToken); + return; + } + } } } diff --git a/src/Client/IDataTransferUtility.cs b/src/Client/IDataTransferUtility.cs index b26e53e..d198ade 100644 --- a/src/Client/IDataTransferUtility.cs +++ b/src/Client/IDataTransferUtility.cs @@ -4,8 +4,9 @@ namespace Morph.Server.Sdk.Client { public interface IDataTransferUtility - { - Task SpaceUploadFileAsync(string localFilePath, string destFolderPath, CancellationToken cancellationToken, bool overwriteExistingFile = false); + { + Task SpaceUploadFileAsync(string localFilePath, string serverFolder, CancellationToken cancellationToken, bool overwriteExistingFile = false); + Task SpaceUploadFileAsync(string localFilePath, string serverFolder, string destFileName, CancellationToken cancellationToken, bool overwriteExistingFile = false); Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string targetLocalFilePath, CancellationToken cancellationToken, bool overwriteExistingFile = false); Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string targetLocalFolder, CancellationToken cancellationToken, bool overwriteExistingFile = false); } diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index 1cdf593..d752543 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -49,8 +49,8 @@ internal interface ILowLevelApiClient: IDisposable Task> WebFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); Task> WebFilesDeleteFileAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken); Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, Action onReceiveProgress, CancellationToken cancellationToken); - Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); - Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); + Task> WebFilesPutFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); + Task> WebFilesPostFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); } } diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index ba2531d..67e3186 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -15,8 +15,8 @@ namespace Morph.Server.Sdk.Client public interface IMorphServerApiClient:IDisposable { - event EventHandler OnFileDownloadProgress; - event EventHandler OnFileUploadProgress; + event EventHandler OnDataDownloadProgress; + event EventHandler OnDataUploadProgress; IClientConfiguration Config { get; } @@ -39,10 +39,10 @@ public interface IMorphServerApiClient:IDisposable Task SpaceDeleteFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceFileExistsAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); + Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); } diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index bc02e69..b43e339 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -230,7 +230,7 @@ public async Task> WebFileExistsAsync(ApiSession apiSession, str } } - public Task> WebFilesPutFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) + public Task> WebFilesPutFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) { if (apiSession == null) { @@ -249,7 +249,7 @@ public Task> WebFilesPutFileAsync(ApiSession apiSessi } - public Task> WebFilesPostFileAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) + public Task> WebFilesPostFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken) { if (apiSession == null) { diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 1c7ca96..750364d 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -24,8 +24,8 @@ namespace Morph.Server.Sdk.Client /// public class MorphServerApiClient : IMorphServerApiClient, IDisposable { - public event EventHandler OnFileDownloadProgress; - public event EventHandler OnFileUploadProgress; + public event EventHandler OnDataDownloadProgress; + public event EventHandler OnDataUploadProgress; protected readonly string _userAgent = "MorphServerApiClient/next"; protected readonly string _api_v1 = "api/v1/"; @@ -377,7 +377,7 @@ public async Task GetSpacesListAsync(CancellationToken ca private void DownloadProgress_StateChanged(object sender, FileTransferProgressEventArgs e) { - OnFileDownloadProgress?.Invoke(this, e); + OnDataDownloadProgress?.Invoke(this, e); } @@ -575,7 +575,7 @@ public Task GetTaskAsync(ApiSession apiSession, Guid taskId, Cancella } - public Task SpaceDownloadFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -586,7 +586,7 @@ public Task SpaceDownloadFileAsync(ApiSession apiSession, s { Action onReceiveProgress = (data) => { - OnFileDownloadProgress?.Invoke(this, data); + OnDataDownloadProgress?.Invoke(this, data); }; var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, onReceiveProgress, token); return MapOrFail(apiResult, (data) => new ServerStreamingData(data.Stream, data.FileName, data.FileSize) @@ -603,7 +603,7 @@ public void Dispose() } } - public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -614,7 +614,7 @@ public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string r { Action onReceiveProgress = (data) => { - OnFileDownloadProgress?.Invoke(this, data); + OnDataDownloadProgress?.Invoke(this, data); }; var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, onReceiveProgress, token); return MapOrFail(apiResult, (data) => data.Stream); @@ -622,7 +622,7 @@ public Task SpaceDownloadFileStreamAsync(ApiSession apiSession, string r }, cancellationToken, OperationType.FileTransfer); } - public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) + public Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) { if (apiSession == null) { @@ -638,7 +638,7 @@ public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest s { Action onSendProgress = (data) => { - OnFileUploadProgress?.Invoke(this, data); + OnDataUploadProgress?.Invoke(this, data); }; var sendStreamData = new SendFileStreamData( spaceUploadFileRequest.DataStream, @@ -646,8 +646,8 @@ public Task SpaceUploadFileAsync(ApiSession apiSession, SpaceUploadFileRequest s spaceUploadFileRequest.FileSize); var apiResult = spaceUploadFileRequest.OverwriteExistingFile ? - await _lowLevelApiClient.WebFilesPutFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token) : - await _lowLevelApiClient.WebFilesPostFileAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token); + await _lowLevelApiClient.WebFilesPutFileStreamAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token) : + await _lowLevelApiClient.WebFilesPostFileStreamAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token); FailIfError(apiResult); return Task.FromResult(0); diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index 3db5898..f1a21e3 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -72,7 +72,7 @@ public void Dispose() this.IsClosed = true; } } - catch (Exception ex) + catch (Exception) { } diff --git a/src/Model/SpaceStatus.cs b/src/Model/SpaceStatus.cs index 40a6559..3c7ac05 100644 --- a/src/Model/SpaceStatus.cs +++ b/src/Model/SpaceStatus.cs @@ -12,21 +12,4 @@ public class SpaceStatus public bool IsPublic { get; internal set; } public IReadOnlyList UserPermissions { get; internal set; } } - - - public enum UserSpacePermission - { - TasksList, - TaskLogView, - TaskLogDeletion, - TaskCreate, - TaskModify, - TaskExecution, - TaskDeletion, - - FilesList, - FileUpload, - FileDownload, - FileDeletion, - } } From 3cee470b70e1e72dd353daa0304188d2fa956f47 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 1 Feb 2019 12:47:20 +0200 Subject: [PATCH 16/37] sending multiple streams in a single request -in progress sdk code refactoring - in progress --- src/Client/DataTransferUtility.cs | 4 +- src/Client/ILowLevelApiClient.cs | 2 + src/Client/IMorphServerApiClient.cs | 4 +- src/Client/IRestClient.cs | 7 +- src/Client/LowLevelApiClient.cs | 14 +++ src/Client/MorphServerApiClient.cs | 24 +++- .../MorphServerApiClientGlobalConfig.cs | 16 ++- src/Client/MorphServerRestClient.cs | 115 +++++++++++++----- src/Helper/ContiniousSteamingContent.cs | 111 +++++++++++++++++ src/Helper/ProgressStreamContent.cs | 20 +-- src/Helper/StreamProgressEventArgs.cs | 18 +++ src/Model/ApiSession.cs | 3 +- .../ContiniousStreamingRequest.cs | 19 +++ src/Model/ServerWritableStreaming.cs | 43 +++++++ src/Model/UserSpacePermission.cs | 18 +++ src/Morph.Server.Sdk.csproj | 2 + 16 files changed, 367 insertions(+), 53 deletions(-) create mode 100644 src/Helper/ContiniousSteamingContent.cs create mode 100644 src/Helper/StreamProgressEventArgs.cs create mode 100644 src/Model/InternalModels/ContiniousStreamingRequest.cs create mode 100644 src/Model/ServerWritableStreaming.cs create mode 100644 src/Model/UserSpacePermission.cs diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 5004aa6..271f6a8 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -73,7 +73,7 @@ public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string t { using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenReadStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); } @@ -116,7 +116,7 @@ public async Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenReadStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { destFileName = Path.Combine(targetLocalFolder, serverStreamingData.FileName); diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index d752543..c937c7a 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -52,6 +52,8 @@ internal interface ILowLevelApiClient: IDisposable Task> WebFilesPutFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); Task> WebFilesPostFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); + Task> WebFilesPushPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + } } diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index 67e3186..a426fa3 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -39,11 +39,11 @@ public interface IMorphServerApiClient:IDisposable Task SpaceDeleteFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceFileExistsAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenReadStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); - + Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, string folder, string fileName, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Client/IRestClient.cs b/src/Client/IRestClient.cs index bc43040..9ae0b98 100644 --- a/src/Client/IRestClient.cs +++ b/src/Client/IRestClient.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Events; +using Morph.Server.Sdk.Model; namespace Morph.Server.Sdk.Client { @@ -27,7 +28,11 @@ Task> PostFileStreamAsync(string url, SendFileStream where TResult : new(); Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken); - + + + Task> PushContiniousStreamingDataAsync( + HttpMethod httpMethod, string path, ContiniousStreamingRequest startContiniousStreamingRequest, NameValueCollection urlParameters, HeadersCollection headersCollection, + CancellationToken cancellationToken); } diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index b43e339..51f7e45 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -12,6 +12,7 @@ using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Helper; using Morph.Server.Sdk.Events; +using System.Net.Http; namespace Morph.Server.Sdk.Client { @@ -266,6 +267,19 @@ public Task> WebFilesPostFileStreamAsync(ApiSession a return apiClient.PostFileStreamAsync(url, sendFileStreamData, null, apiSession.ToHeadersCollection(), onSendProgress, cancellationToken); } + public Task> WebFilesPushPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); + + return apiClient.PushContiniousStreamingDataAsync(HttpMethod.Put, url, new ContiniousStreamingRequest(fileName), null, apiSession.ToHeadersCollection(), cancellationToken); + } } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 750364d..7683497 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -167,7 +167,7 @@ public Task StartTaskAsync(ApiSession apiSession, StartTaskRe if (!startTaskRequest.TaskId.HasValue) { - throw new Exception("TaskId mus be set."); + throw new Exception("TaskId must be set."); } var apiResult = await _lowLevelApiClient.StartTaskAsync(apiSession, startTaskRequest.TaskId.Value, requestDto, token); return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); @@ -575,7 +575,7 @@ public Task GetTaskAsync(ApiSession apiSession, Guid taskId, Cancella } - public Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceOpenReadStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -622,6 +622,26 @@ public Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remot }, cancellationToken, OperationType.FileTransfer); } + public Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, string folder, string fileName, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + return Wrapped(async (token) => + { + Action onSendProgress = (data) => + { + OnDataUploadProgress?.Invoke(this, data); + }; + var apiResult = await _lowLevelApiClient.WebFilesPushPostStreamAsync(apiSession, folder, fileName, token); + return MapOrFail(apiResult, x => x); + + }, cancellationToken, OperationType.FileTransfer); + + } + public Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) { if (apiSession == null) diff --git a/src/Client/MorphServerApiClientGlobalConfig.cs b/src/Client/MorphServerApiClientGlobalConfig.cs index 8abb817..1670d7c 100644 --- a/src/Client/MorphServerApiClientGlobalConfig.cs +++ b/src/Client/MorphServerApiClientGlobalConfig.cs @@ -3,13 +3,27 @@ using System.Security.Cryptography.X509Certificates; using System.Net.Security; using System.Reflection; +using System.Net; namespace Morph.Server.Sdk.Client { public static class MorphServerApiClientGlobalConfig { + #if NETSTANDARD2_0 - public static Func ServerCertificateCustomValidationCallback { get; set; } + private static object obj = new object(); + public static Func ServerCertificateCustomValidationCallback { get; set; } = + (httpRequestMessage, xcert, xchain, sslerror) => + { + if (ServicePointManager.ServerCertificateValidationCallback != null) + { + return ServicePointManager.ServerCertificateValidationCallback(obj, xcert, xchain, sslerror); + } + else + { + return false; + } + }; #endif private const string DefaultClientType = "EMS-SDK"; diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index d716aa0..426ca3a 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -162,6 +162,64 @@ public void Dispose() } } + + + + public Task> PushContiniousStreamingDataAsync( + HttpMethod httpMethod, string path, ContiniousStreamingRequest startContiniousStreamingRequest, NameValueCollection urlParameters, HeadersCollection headersCollection, + CancellationToken cancellationToken) + { + try + { + string boundary = "MorphRestClient-Streaming--------" + Guid.NewGuid().ToString("N"); + + var content = new MultipartFormDataContent(boundary); + + + var streamContent = new ContiniousSteamingContent(cancellationToken); + var serverPushStreaming = new ServerPushStreaming(streamContent); + content.Add(streamContent, "files", Path.GetFileName(startContiniousStreamingRequest.FileName)); + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); + var requestMessage = BuildHttpRequestMessage(httpMethod, url, content, headersCollection); + //using (requestMessage) + { + new Task(async () => + { + // TODO: dispose + serverPushStreaming.RegisterOnClose(() => + { + streamContent.CloseConnetion(); + requestMessage.Dispose(); + content.Dispose(); + streamContent.Dispose(); + + + + }); + var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.Dispose(); + + }).Start(); + return Task.FromResult(ApiResult.Ok(serverPushStreaming)); + + + } + + + } + catch (Exception ex) when (ex.InnerException != null && + ex.InnerException is WebException web && + web.Status == WebExceptionStatus.ConnectionClosed) + { + return Task.FromResult(ApiResult.Fail(new MorphApiNotFoundException("Specified folder not found"))); + } + catch (Exception e) + { + return Task.FromResult(ApiResult.Fail(e)); + } + } + + public async Task> SendFileStreamAsync( HttpMethod httpMethod, string path, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, @@ -176,7 +234,7 @@ public async Task> SendFileStreamAsync( using (var content = new MultipartFormDataContent(boundary)) { var uploadProgress = new FileProgress(sendFileStreamData.FileName, sendFileStreamData.FileSize, onSendProgress); - + using (cancellationToken.Register(() => uploadProgress.ChangeState(FileProgressState.Cancelled))) { using (var streamContent = new ProgressStreamContent(sendFileStreamData.Stream, uploadProgress)) @@ -233,41 +291,42 @@ protected async Task> RetrieveFileStreamAsync(Htt } downloadProgress?.ChangeState(FileProgressState.Starting); var totalProcessedBytes = 0; - + { // stream must be disposed by a caller Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); - DateTime _lastUpdate = DateTime.MinValue; - - var streamWithProgress = new StreamWithProgress(streamToReadFrom, contentLength.Value, cancellationToken , - e => + DateTime _lastUpdate = DateTime.MinValue; + + var streamWithProgress = new StreamWithProgress(streamToReadFrom, contentLength.Value, cancellationToken, + e => + { + if (downloadProgress != null) { - if (downloadProgress != null) + totalProcessedBytes += e.BytesProcessed; + if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) { - totalProcessedBytes += e.BytesProcessed; - if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) - { - downloadProgress.SetProcessedBytes(totalProcessedBytes); - _lastUpdate = DateTime.Now; - } - + downloadProgress.SetProcessedBytes(totalProcessedBytes); + _lastUpdate = DateTime.Now; } - }, - () => - { - if (downloadProgress != null && downloadProgress.ProcessedBytes != totalProcessedBytes) - { - downloadProgress.ChangeState(FileProgressState.Cancelled); } - response.Dispose(); }, - ()=> { - throw new Exception("Timeout"); + () => + { + + if (downloadProgress != null && downloadProgress.ProcessedBytes != totalProcessedBytes) + { + downloadProgress.ChangeState(FileProgressState.Cancelled); + } + response.Dispose(); + }, + () => + { + throw new Exception("Timeout"); + + }); + return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); - }); - return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); - } } else @@ -286,7 +345,7 @@ protected async Task> RetrieveFileStreamAsync(Htt } - public Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) + public Task> PutFileStreamAsync(string url, SendFileStreamData sendFileStreamData, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onSendProgress, CancellationToken cancellationToken) where TResult : new() { return SendFileStreamAsync(HttpMethod.Put, url, sendFileStreamData, urlParameters, headersCollection, onSendProgress, cancellationToken); @@ -311,7 +370,7 @@ public Task> RetrieveFileGetAsync(string url, Nam - + public Task> HeadAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) where TResult : new() diff --git a/src/Helper/ContiniousSteamingContent.cs b/src/Helper/ContiniousSteamingContent.cs new file mode 100644 index 0000000..9af4bba --- /dev/null +++ b/src/Helper/ContiniousSteamingContent.cs @@ -0,0 +1,111 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Helper +{ + internal class ContiniousSteamingContent : HttpContent + { + internal enum MessageType + { + None, + NewStream, + Close + } + + private const int DefBufferSize = 4096; + private readonly CancellationToken mainCancellation; + + //ManualResetEventSlim resetEventSlim = new ManualResetEventSlim(true); + volatile SemaphoreSlim hasData = new SemaphoreSlim(0, 1); + volatile SemaphoreSlim dataProcessed = new SemaphoreSlim(0, 1); + + volatile Stream _stream; + volatile MessageType _messageType = MessageType.None; + private CancellationToken cancellationToken; + + internal async Task WriteStream( Stream stream, CancellationToken cancellationToken) + { + + this._stream = stream; + this.cancellationToken = cancellationToken; + this._messageType = MessageType.NewStream; + hasData.Release(1); //has data->1 + + + await dataProcessed.WaitAsync(Timeout.Infinite, cancellationToken); + // dataProcessed.Release(); + } + + internal void CloseConnetion() + { + this._messageType = MessageType.Close; + hasData.Release(); + dataProcessed.Wait(5000); + //dataProcessed.Release(); + } + + public ContiniousSteamingContent(CancellationToken mainCancellation) + { + this.mainCancellation = mainCancellation; + + } + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + var buffer = new byte[DefBufferSize]; + bool canLoop = true; + while (canLoop) + { + //TODO: add cancellation token + await hasData.WaitAsync(Timeout.Infinite, mainCancellation); + // resetEventSlim.Wait(); + switch (this._messageType) { + case MessageType.NewStream: + using (this._stream) + { + int bytesRead; + while ((bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await stream.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + } + + + dataProcessed.Release(1); + + + + //var length = await _stream.ReadAsync(buffer, 0, buffer.Length, mainCancellation); + + //if (length <= 0) { + // dataProcessed.Release(); ; + // // hasData.Release(); + // // resetEventSlim.Reset(); + // break; + //} + + //await stream.WriteAsync(buffer, 0, length, mainCancellation); + }; break; + case MessageType.Close: + canLoop = false; + + break; + + } + this._messageType = MessageType.None; + + // this._stream = null; + + } + dataProcessed.Release(1); + + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + } +} diff --git a/src/Helper/ProgressStreamContent.cs b/src/Helper/ProgressStreamContent.cs index 0a2ba3f..283f91d 100644 --- a/src/Helper/ProgressStreamContent.cs +++ b/src/Helper/ProgressStreamContent.cs @@ -11,21 +11,7 @@ namespace Morph.Server.Sdk.Helper { - internal class StreamProgressEventArgs : EventArgs - { - public int BytesProcessed { get; } - - public StreamProgressEventArgs() - { - - } - public StreamProgressEventArgs(int bytesProcessed):this() - { - BytesProcessed = bytesProcessed; - } - } - - + internal class ProgressStreamContent : HttpContent { private const int DefBufferSize = 4096; @@ -48,7 +34,9 @@ public ProgressStreamContent(Stream stream, IFileProgress downloader) : this(str protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { - var buffer = new byte[_bufSize]; + + + var buffer = new byte[_bufSize]; var size = _stream.Length; var processed = 0; diff --git a/src/Helper/StreamProgressEventArgs.cs b/src/Helper/StreamProgressEventArgs.cs new file mode 100644 index 0000000..8b89c68 --- /dev/null +++ b/src/Helper/StreamProgressEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Morph.Server.Sdk.Helper +{ + internal class StreamProgressEventArgs : EventArgs + { + public int BytesProcessed { get; } + + public StreamProgressEventArgs() + { + + } + public StreamProgressEventArgs(int bytesProcessed):this() + { + BytesProcessed = bytesProcessed; + } + } +} diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index f1a21e3..2888fad 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -1,14 +1,15 @@ using Morph.Server.Sdk.Client; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Morph.Server.Sdk.Model { - public class ApiSession : IDisposable { protected readonly string _defaultSpaceName = "default"; diff --git a/src/Model/InternalModels/ContiniousStreamingRequest.cs b/src/Model/InternalModels/ContiniousStreamingRequest.cs new file mode 100644 index 0000000..f881c9c --- /dev/null +++ b/src/Model/InternalModels/ContiniousStreamingRequest.cs @@ -0,0 +1,19 @@ +using System; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + internal class ContiniousStreamingRequest + { + public ContiniousStreamingRequest(string fileName) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + } + + public string FileName { get; } + + + } + + + +} \ No newline at end of file diff --git a/src/Model/ServerWritableStreaming.cs b/src/Model/ServerWritableStreaming.cs new file mode 100644 index 0000000..658ab4b --- /dev/null +++ b/src/Model/ServerWritableStreaming.cs @@ -0,0 +1,43 @@ +using Morph.Server.Sdk.Helper; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Model +{ + public class ServerPushStreaming:IDisposable + { + internal readonly ContiniousSteamingContent steamingContent; + Action onClose = null; + + //private Func onWriteStream { get; set; } + //private Action onClose { get; set; } + + internal ServerPushStreaming(ContiniousSteamingContent steamingContent ) + { + this.steamingContent = steamingContent; + } + public void Dispose() + { + if (onClose != null) + { + onClose(); + } + } + + public async Task WriteStream(Stream stream, CancellationToken cancellationToken) + { + //return onWriteStream(stream, cancellationToken); + await steamingContent.WriteStream(stream, cancellationToken); + } + + internal void RegisterOnClose(Action onClose ) + { + this.onClose = onClose; + + } + } + + +} diff --git a/src/Model/UserSpacePermission.cs b/src/Model/UserSpacePermission.cs new file mode 100644 index 0000000..3ba7fd4 --- /dev/null +++ b/src/Model/UserSpacePermission.cs @@ -0,0 +1,18 @@ +namespace Morph.Server.Sdk.Model +{ + public enum UserSpacePermission + { + TasksList, + TaskLogView, + TaskLogDeletion, + TaskCreate, + TaskModify, + TaskExecution, + TaskDeletion, + + FilesList, + FileUpload, + FileDownload, + FileDeletion, + } +} diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index d089de4..8333f4e 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -4,6 +4,8 @@ false + true + Morph.Server.Sdk.snk From 7f7d813b6db6634b25df6692e35d31d83e4bdd09 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 1 Feb 2019 19:04:06 +0200 Subject: [PATCH 17/37] continuous streaming --- src/Client/IRestClient.cs | 6 +- src/Client/LowLevelApiClient.cs | 2 +- src/Client/MorphServerRestClient.cs | 76 ++++++++++++++-------- src/Helper/ContiniousSteamingContent.cs | 4 +- src/Model/ServerWritableStreaming.cs | 85 +++++++++++++++++++++---- 5 files changed, 127 insertions(+), 46 deletions(-) diff --git a/src/Client/IRestClient.cs b/src/Client/IRestClient.cs index 9ae0b98..eadff79 100644 --- a/src/Client/IRestClient.cs +++ b/src/Client/IRestClient.cs @@ -30,9 +30,11 @@ Task> PostFileStreamAsync(string url, SendFileStream Task> RetrieveFileGetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken); - Task> PushContiniousStreamingDataAsync( + Task> PushContiniousStreamingDataAsync( HttpMethod httpMethod, string path, ContiniousStreamingRequest startContiniousStreamingRequest, NameValueCollection urlParameters, HeadersCollection headersCollection, - CancellationToken cancellationToken); + CancellationToken cancellationToken) + where TResult : new(); + } diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 51f7e45..77c8732 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -278,7 +278,7 @@ public Task> WebFilesPushPostStreamAsync(ApiSessi var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); - return apiClient.PushContiniousStreamingDataAsync(HttpMethod.Put, url, new ContiniousStreamingRequest(fileName), null, apiSession.ToHeadersCollection(), cancellationToken); + return apiClient.PushContiniousStreamingDataAsync(HttpMethod.Put, url, new ContiniousStreamingRequest(fileName), null, apiSession.ToHeadersCollection(), cancellationToken); } } } diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 426ca3a..884895e 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -165,47 +165,69 @@ public void Dispose() - public Task> PushContiniousStreamingDataAsync( + public Task> PushContiniousStreamingDataAsync( HttpMethod httpMethod, string path, ContiniousStreamingRequest startContiniousStreamingRequest, NameValueCollection urlParameters, HeadersCollection headersCollection, - CancellationToken cancellationToken) + CancellationToken cancellationToken) + where TResult : new() { try { string boundary = "MorphRestClient-Streaming--------" + Guid.NewGuid().ToString("N"); - var content = new MultipartFormDataContent(boundary); + var content = new MultipartFormDataContent(boundary); - var streamContent = new ContiniousSteamingContent(cancellationToken); - var serverPushStreaming = new ServerPushStreaming(streamContent); - content.Add(streamContent, "files", Path.GetFileName(startContiniousStreamingRequest.FileName)); - var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); - var requestMessage = BuildHttpRequestMessage(httpMethod, url, content, headersCollection); - //using (requestMessage) - { - new Task(async () => + var streamContent = new ContiniousSteamingHttpContent(cancellationToken); + var serverPushStreaming = new ServerPushStreaming(streamContent); + content.Add(streamContent, "files", Path.GetFileName(startContiniousStreamingRequest.FileName)); + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); + var requestMessage = BuildHttpRequestMessage(httpMethod, url, content, headersCollection); + //using (requestMessage) + { + new Task(async () => + { + //// TODO: dispose + //serverPushStreaming.RegisterOnClose(() => + //{ + // streamContent.CloseConnetion(); + //}); + try + { + try { - // TODO: dispose - serverPushStreaming.RegisterOnClose(() => - { - streamContent.CloseConnetion(); - requestMessage.Dispose(); - content.Dispose(); - streamContent.Dispose(); - - - - }); var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + var result = await HandleResponse(response); + serverPushStreaming.SetApiResult(result); response.Dispose(); + } + catch (Exception ex) when (ex.InnerException != null && + ex.InnerException is WebException web && + web.Status == WebExceptionStatus.ConnectionClosed) + { + serverPushStreaming.SetApiResult(ApiResult.Fail(new MorphApiNotFoundException("Specified folder not found"))); + } + catch (Exception e) + { + serverPushStreaming.SetApiResult(ApiResult.Fail(e)); + } - }).Start(); - return Task.FromResult(ApiResult.Ok(serverPushStreaming)); + requestMessage.Dispose(); + streamContent.Dispose(); + content.Dispose(); + } + catch (Exception ex) + { + // dd + } + + }).Start(); + return Task.FromResult(ApiResult.Ok(serverPushStreaming)); + + + } - - } - } catch (Exception ex) when (ex.InnerException != null && ex.InnerException is WebException web && diff --git a/src/Helper/ContiniousSteamingContent.cs b/src/Helper/ContiniousSteamingContent.cs index 9af4bba..483cf1d 100644 --- a/src/Helper/ContiniousSteamingContent.cs +++ b/src/Helper/ContiniousSteamingContent.cs @@ -6,7 +6,7 @@ namespace Morph.Server.Sdk.Helper { - internal class ContiniousSteamingContent : HttpContent + internal class ContiniousSteamingHttpContent : HttpContent { internal enum MessageType { @@ -47,7 +47,7 @@ internal void CloseConnetion() //dataProcessed.Release(); } - public ContiniousSteamingContent(CancellationToken mainCancellation) + public ContiniousSteamingHttpContent(CancellationToken mainCancellation) { this.mainCancellation = mainCancellation; diff --git a/src/Model/ServerWritableStreaming.cs b/src/Model/ServerWritableStreaming.cs index 658ab4b..bbd6290 100644 --- a/src/Model/ServerWritableStreaming.cs +++ b/src/Model/ServerWritableStreaming.cs @@ -1,4 +1,5 @@ using Morph.Server.Sdk.Helper; +using Morph.Server.Sdk.Model.InternalModels; using System; using System.IO; using System.Threading; @@ -6,23 +7,46 @@ namespace Morph.Server.Sdk.Model { - public class ServerPushStreaming:IDisposable + public class ServerPushStreaming : IDisposable { - internal readonly ContiniousSteamingContent steamingContent; - Action onClose = null; + internal readonly ContiniousSteamingHttpContent steamingContent; + //Action onClose = null; + private bool _closed = false; + volatile SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(0, 1); - //private Func onWriteStream { get; set; } - //private Action onClose { get; set; } - - internal ServerPushStreaming(ContiniousSteamingContent steamingContent ) + internal ServerPushStreaming(ContiniousSteamingHttpContent steamingContent) { this.steamingContent = steamingContent; - } + } public void Dispose() { - if (onClose != null) + Close(); + } + + private void Close() + { + if (_closed) + return; + try + { + + this.steamingContent.CloseConnetion(); + //if (onClose != null) + //{ + + // onClose(); + SemaphoreSlim.Wait(10000); + if (DataException != null) + { + throw DataException; + } + //} + } + finally { - onClose(); + SemaphoreSlim.Dispose(); + SemaphoreSlim = null; + _closed = true; } } @@ -32,12 +56,45 @@ public async Task WriteStream(Stream stream, CancellationToken cancellationToken await steamingContent.WriteStream(stream, cancellationToken); } - internal void RegisterOnClose(Action onClose ) + //internal void RegisterOnClose(Action onClose) + //{ + // this.onClose = onClose; + + //} + + public Exception DataException { get; private set; } + + private object dataResult = null; + + public TResult GetData() + { + Close(); + + if (dataResult is TResult f) + { + return f; + } + else + { + return default(TResult); + } + } + + internal void SetApiResult(ApiResult apiResult) where TResult : new() { - this.onClose = onClose; - + if (apiResult.IsSucceed) + { + dataResult = apiResult.Data; + + } + else + { + DataException = apiResult.Error; + } + + SemaphoreSlim.Release(); } } - + } From 69a6084e7584fd7bc008435e022ce78cbbbd4d01 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 4 Feb 2019 18:23:21 +0200 Subject: [PATCH 18/37] Streaming upload cancellation fix --- src/Client/MorphServerRestClient.cs | 7 +- src/Helper/ContiniousSteamingContent.cs | 89 ++++++++++++++++--------- src/Model/ServerWritableStreaming.cs | 23 ++----- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 884895e..3118547 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -186,17 +186,12 @@ public Task> PushContiniousStreamingDataAsync { - //// TODO: dispose - //serverPushStreaming.RegisterOnClose(() => - //{ - // streamContent.CloseConnetion(); - //}); try { try { var response = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - + var result = await HandleResponse(response); serverPushStreaming.SetApiResult(result); response.Dispose(); diff --git a/src/Helper/ContiniousSteamingContent.cs b/src/Helper/ContiniousSteamingContent.cs index 483cf1d..222ebb4 100644 --- a/src/Helper/ContiniousSteamingContent.cs +++ b/src/Helper/ContiniousSteamingContent.cs @@ -8,43 +8,78 @@ namespace Morph.Server.Sdk.Helper { internal class ContiniousSteamingHttpContent : HttpContent { + /// + /// message type for processing + /// internal enum MessageType { + /// + /// no new messages + /// None, + /// + /// push new stream to server + /// NewStream, - Close + /// + /// Close connection + /// + CloseConnection } private const int DefBufferSize = 4096; private readonly CancellationToken mainCancellation; - //ManualResetEventSlim resetEventSlim = new ManualResetEventSlim(true); + + /// + /// cross-thread flag, that new message need to be processed + /// volatile SemaphoreSlim hasData = new SemaphoreSlim(0, 1); + /// + /// cross-thread flag that new message has been processed + /// volatile SemaphoreSlim dataProcessed = new SemaphoreSlim(0, 1); + /// + /// stream to process + /// volatile Stream _stream; - volatile MessageType _messageType = MessageType.None; + /// + /// Message to process + /// + volatile MessageType _currentMessage = MessageType.None; private CancellationToken cancellationToken; - internal async Task WriteStream( Stream stream, CancellationToken cancellationToken) + internal async Task WriteStreamAsync( Stream stream, CancellationToken cancellationToken) { + if(_currentMessage != MessageType.None) + { + throw new System.Exception("Another message is processing by the ContiniousSteamingHttpContent handler. "); + } this._stream = stream; this.cancellationToken = cancellationToken; - this._messageType = MessageType.NewStream; - hasData.Release(1); //has data->1 + this._currentMessage = MessageType.NewStream; - + // set flag that new data has been arrived + hasData.Release(1); //has data->1 + // await till all data will be send by another thread. Another thread will trigger dataProcessed semaphore await dataProcessed.WaitAsync(Timeout.Infinite, cancellationToken); - // dataProcessed.Release(); + } internal void CloseConnetion() { - this._messageType = MessageType.Close; - hasData.Release(); - dataProcessed.Wait(5000); - //dataProcessed.Release(); + // if cancellation token has been requested, it is not necessary to send message CloseConnection + if (!mainCancellation.IsCancellationRequested) + { + this._currentMessage = MessageType.CloseConnection; + // send message that data is ready + hasData.Release(); + // wait until it has been processed + dataProcessed.Wait(5000); + } + } public ContiniousSteamingHttpContent(CancellationToken mainCancellation) @@ -58,10 +93,12 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon bool canLoop = true; while (canLoop) { - //TODO: add cancellation token + + // await new data await hasData.WaitAsync(Timeout.Infinite, mainCancellation); - // resetEventSlim.Wait(); - switch (this._messageType) { + // data has been arrived. check _currentMessage field + switch (this._currentMessage) { + // upload stream case MessageType.NewStream: using (this._stream) { @@ -71,31 +108,18 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon await stream.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); } - + // set that data has been processed dataProcessed.Release(1); - - - //var length = await _stream.ReadAsync(buffer, 0, buffer.Length, mainCancellation); - - //if (length <= 0) { - // dataProcessed.Release(); ; - // // hasData.Release(); - // // resetEventSlim.Reset(); - // break; - //} - - //await stream.WriteAsync(buffer, 0, length, mainCancellation); }; break; - case MessageType.Close: + case MessageType.CloseConnection: + // close loop. dataProcessed flag will be triggered at the end of this function. canLoop = false; break; } - this._messageType = MessageType.None; - - // this._stream = null; + this._currentMessage = MessageType.None; } dataProcessed.Release(1); @@ -104,6 +128,7 @@ protected override async Task SerializeToStreamAsync(Stream stream, TransportCon protected override bool TryComputeLength(out long length) { + // continuous stream length is unknown length = 0; return false; } diff --git a/src/Model/ServerWritableStreaming.cs b/src/Model/ServerWritableStreaming.cs index bbd6290..c096c42 100644 --- a/src/Model/ServerWritableStreaming.cs +++ b/src/Model/ServerWritableStreaming.cs @@ -10,7 +10,7 @@ namespace Morph.Server.Sdk.Model public class ServerPushStreaming : IDisposable { internal readonly ContiniousSteamingHttpContent steamingContent; - //Action onClose = null; + private bool _closed = false; volatile SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(0, 1); @@ -31,16 +31,14 @@ private void Close() { this.steamingContent.CloseConnetion(); - //if (onClose != null) - //{ + - // onClose(); SemaphoreSlim.Wait(10000); if (DataException != null) { throw DataException; } - //} + } finally { @@ -50,23 +48,16 @@ private void Close() } } - public async Task WriteStream(Stream stream, CancellationToken cancellationToken) - { - //return onWriteStream(stream, cancellationToken); - await steamingContent.WriteStream(stream, cancellationToken); + public async Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken) + { + await steamingContent.WriteStreamAsync(stream, cancellationToken); } - //internal void RegisterOnClose(Action onClose) - //{ - // this.onClose = onClose; - - //} - public Exception DataException { get; private set; } private object dataResult = null; - public TResult GetData() + public TResult CloseAndGetData() { Close(); From b5a266a4f10e1b6a0a7e650cb2dbc74a8a3ae4a8 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 4 Feb 2019 18:56:54 +0200 Subject: [PATCH 19/37] server streaming, some comments --- src/Client/DataTransferUtility.cs | 8 ++--- src/Client/ILowLevelApiClient.cs | 3 +- src/Client/IMorphServerApiClient.cs | 4 +-- src/Client/LowLevelApiClient.cs | 12 ++++++- src/Client/MorphServerApiClient.cs | 22 ++++++++----- src/Model/ContiniousStreamingConnection.cs | 33 +++++++++++++++++++ src/Model/InternalModels/ApiResult.cs | 4 +++ .../ContiniousStreamingRequest.cs | 5 --- .../InternalModels/SendFileStreamData.cs | 2 ++ .../ServerPushStreaming.cs} | 6 ++-- .../SpaceUploadContiniosStreamRequest.cs | 13 ++++++++ src/Model/SpaceUploadFileRequest.cs | 15 --------- src/Model/SpaceUploadFileStreamRequest.cs | 31 +++++++++++++++++ 13 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 src/Model/ContiniousStreamingConnection.cs rename src/Model/{ServerWritableStreaming.cs => InternalModels/ServerPushStreaming.cs} (95%) create mode 100644 src/Model/SpaceUploadContiniosStreamRequest.cs delete mode 100644 src/Model/SpaceUploadFileRequest.cs create mode 100644 src/Model/SpaceUploadFileStreamRequest.cs diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 271f6a8..6f2d81e 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -31,7 +31,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder var fileName = Path.GetFileName(localFilePath); using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) { - var request = new SpaceUploadFileRequest + var request = new SpaceUploadDataStreamRequest { DataStream = fsSource, FileName = fileName, @@ -39,7 +39,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder OverwriteExistingFile = overwriteExistingFile, ServerFolder = serverFolder }; - await _morphServerApiClient.SpaceUploadStreamAsync(_apiSession, request, cancellationToken); + await _morphServerApiClient.SpaceUploadFileStreamAsync(_apiSession, request, cancellationToken); return; } } @@ -158,7 +158,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder using (var fsSource = new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) { - var request = new SpaceUploadFileRequest + var request = new SpaceUploadDataStreamRequest { DataStream = fsSource, FileName = destFileName, @@ -166,7 +166,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder OverwriteExistingFile = overwriteExistingFile, ServerFolder = serverFolder }; - await _morphServerApiClient.SpaceUploadStreamAsync(_apiSession, request, cancellationToken); + await _morphServerApiClient.SpaceUploadFileStreamAsync(_apiSession, request, cancellationToken); return; } } diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index c937c7a..83a43f5 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -52,7 +52,8 @@ internal interface ILowLevelApiClient: IDisposable Task> WebFilesPutFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); Task> WebFilesPostFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, CancellationToken cancellationToken); - Task> WebFilesPushPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + Task> WebFilesOpenContiniousPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); + Task> WebFilesOpenContiniousPutStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken); } } diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index a426fa3..7d8ad3f 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -42,8 +42,8 @@ public interface IMorphServerApiClient:IDisposable Task SpaceOpenReadStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken); - Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, string folder, string fileName, CancellationToken cancellationToken); + Task SpaceUploadFileStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken); + Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, SpaceUploadContiniousStreamRequest continiousStreamRequest, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 77c8732..581d455 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -267,14 +267,24 @@ public Task> WebFilesPostFileStreamAsync(ApiSession a return apiClient.PostFileStreamAsync(url, sendFileStreamData, null, apiSession.ToHeadersCollection(), onSendProgress, cancellationToken); } - public Task> WebFilesPushPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + public Task> WebFilesOpenContiniousPostStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); + return apiClient.PushContiniousStreamingDataAsync(HttpMethod.Post, url, new ContiniousStreamingRequest(fileName), null, apiSession.ToHeadersCollection(), cancellationToken); + } + public Task> WebFilesOpenContiniousPutStreamAsync(ApiSession apiSession, string serverFolder, string fileName, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } var spaceName = apiSession.SpaceName; var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFolder); diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 7683497..da328fc 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -622,27 +622,33 @@ public Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remot }, cancellationToken, OperationType.FileTransfer); } - public Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, string folder, string fileName, CancellationToken cancellationToken) + public Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, SpaceUploadContiniousStreamRequest continiousStreamRequest, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } + if (continiousStreamRequest == null) + { + throw new ArgumentNullException(nameof(continiousStreamRequest)); + } + return Wrapped(async (token) => { - Action onSendProgress = (data) => - { - OnDataUploadProgress?.Invoke(this, data); - }; - var apiResult = await _lowLevelApiClient.WebFilesPushPostStreamAsync(apiSession, folder, fileName, token); - return MapOrFail(apiResult, x => x); + var apiResult = + continiousStreamRequest.OverwriteExistingFile ? + await _lowLevelApiClient.WebFilesOpenContiniousPutStreamAsync(apiSession, continiousStreamRequest.ServerFolder, continiousStreamRequest.FileName, token) : + await _lowLevelApiClient.WebFilesOpenContiniousPostStreamAsync(apiSession, continiousStreamRequest.ServerFolder, continiousStreamRequest.FileName, token); + + var connection = MapOrFail(apiResult, c => c); + return new ContiniousStreamingConnection(connection); }, cancellationToken, OperationType.FileTransfer); } - public Task SpaceUploadStreamAsync(ApiSession apiSession, SpaceUploadFileRequest spaceUploadFileRequest, CancellationToken cancellationToken) + public Task SpaceUploadFileStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken) { if (apiSession == null) { diff --git a/src/Model/ContiniousStreamingConnection.cs b/src/Model/ContiniousStreamingConnection.cs new file mode 100644 index 0000000..3aaf68d --- /dev/null +++ b/src/Model/ContiniousStreamingConnection.cs @@ -0,0 +1,33 @@ +using Morph.Server.Sdk.Model.InternalModels; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Model +{ + public class ContiniousStreamingConnection: IDisposable + { + private readonly ServerPushStreaming serverPushStreaming; + + internal ContiniousStreamingConnection(ServerPushStreaming serverPushStreaming) + { + this.serverPushStreaming = serverPushStreaming; + } + + public void Dispose() + { + serverPushStreaming.Dispose(); + } + + public async Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken) + { + await serverPushStreaming.WriteStreamAsync(stream, cancellationToken); + } + + } + + + + +} diff --git a/src/Model/InternalModels/ApiResult.cs b/src/Model/InternalModels/ApiResult.cs index 22f6159..105831b 100644 --- a/src/Model/InternalModels/ApiResult.cs +++ b/src/Model/InternalModels/ApiResult.cs @@ -2,6 +2,10 @@ namespace Morph.Server.Sdk.Model.InternalModels { + /// + /// Represents api result of DTO Model or Error (Exception) + /// + /// internal class ApiResult { public T Data { get; set; } = default(T); diff --git a/src/Model/InternalModels/ContiniousStreamingRequest.cs b/src/Model/InternalModels/ContiniousStreamingRequest.cs index f881c9c..f0d84be 100644 --- a/src/Model/InternalModels/ContiniousStreamingRequest.cs +++ b/src/Model/InternalModels/ContiniousStreamingRequest.cs @@ -10,10 +10,5 @@ public ContiniousStreamingRequest(string fileName) } public string FileName { get; } - - } - - - } \ No newline at end of file diff --git a/src/Model/InternalModels/SendFileStreamData.cs b/src/Model/InternalModels/SendFileStreamData.cs index 08648e7..6f21427 100644 --- a/src/Model/InternalModels/SendFileStreamData.cs +++ b/src/Model/InternalModels/SendFileStreamData.cs @@ -17,6 +17,8 @@ public SendFileStreamData(Stream stream, string fileName, long fileSize) public long FileSize { get; } } + + } \ No newline at end of file diff --git a/src/Model/ServerWritableStreaming.cs b/src/Model/InternalModels/ServerPushStreaming.cs similarity index 95% rename from src/Model/ServerWritableStreaming.cs rename to src/Model/InternalModels/ServerPushStreaming.cs index c096c42..38eeeed 100644 --- a/src/Model/ServerWritableStreaming.cs +++ b/src/Model/InternalModels/ServerPushStreaming.cs @@ -5,9 +5,9 @@ using System.Threading; using System.Threading.Tasks; -namespace Morph.Server.Sdk.Model +namespace Morph.Server.Sdk.Model.InternalModels { - public class ServerPushStreaming : IDisposable + internal class ServerPushStreaming : IDisposable { internal readonly ContiniousSteamingHttpContent steamingContent; @@ -88,4 +88,6 @@ public TResult CloseAndGetData() } + + } diff --git a/src/Model/SpaceUploadContiniosStreamRequest.cs b/src/Model/SpaceUploadContiniosStreamRequest.cs new file mode 100644 index 0000000..51b8a0c --- /dev/null +++ b/src/Model/SpaceUploadContiniosStreamRequest.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Morph.Server.Sdk.Model +{ + public sealed class SpaceUploadContiniousStreamRequest + { + public string ServerFolder { get; set; } + public string FileName { get; set; } + public bool OverwriteExistingFile { get; set; } = false; + } +} \ No newline at end of file diff --git a/src/Model/SpaceUploadFileRequest.cs b/src/Model/SpaceUploadFileRequest.cs deleted file mode 100644 index 89c2146..0000000 --- a/src/Model/SpaceUploadFileRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Morph.Server.Sdk.Model -{ - public sealed class SpaceUploadFileRequest - { - public string ServerFolder { get; set; } - public Stream DataStream { get; set; } - public string FileName { get; set; } - public long FileSize { get; set; } - public bool OverwriteExistingFile { get; set; } = false; - } -} \ No newline at end of file diff --git a/src/Model/SpaceUploadFileStreamRequest.cs b/src/Model/SpaceUploadFileStreamRequest.cs new file mode 100644 index 0000000..8e713cb --- /dev/null +++ b/src/Model/SpaceUploadFileStreamRequest.cs @@ -0,0 +1,31 @@ +using System.IO; + +namespace Morph.Server.Sdk.Model +{ + /// + /// Uploads specified data stream to the server space + /// + public sealed class SpaceUploadDataStreamRequest + { + /// + /// Server folder to place data file + /// + public string ServerFolder { get; set; } + /// + /// Stream to send to + /// + public Stream DataStream { get; set; } + /// + /// Destination server file name + /// + public string FileName { get; set; } + /// + /// File size. required for process indication + /// + public long FileSize { get; set; } + /// + /// A flag to overwrite existing file. If flag is not set and file exists api will raise an exception + /// + public bool OverwriteExistingFile { get; set; } = false; + } +} \ No newline at end of file From 4da4f272deee74ecc645cb44ebac8eae8f3cb7af Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 5 Feb 2019 17:35:22 +0200 Subject: [PATCH 20/37] some changes related to session open/close, method names change --- src/Client/DataTransferUtility.cs | 8 +- src/Client/IMorphServerApiClient.cs | 25 +++-- src/Client/MorphServerApiClient.cs | 104 +++++++++++++----- .../MorphServerApiClientGlobalConfig.cs | 8 +- src/Client/MorphServerRestClient.cs | 2 +- src/Model/ApiSession.cs | 78 ++++++++++--- src/Model/ClientConfiguration.cs | 3 + src/Model/IClientConfiguration.cs | 6 + .../OpenSessionAuthenticatorContext.cs | 4 +- src/Model/InternalModels/OperationType.cs | 3 +- 10 files changed, 183 insertions(+), 58 deletions(-) diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 6f2d81e..584761d 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -39,7 +39,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder OverwriteExistingFile = overwriteExistingFile, ServerFolder = serverFolder }; - await _morphServerApiClient.SpaceUploadFileStreamAsync(_apiSession, request, cancellationToken); + await _morphServerApiClient.SpaceUploadDataStreamAsync(_apiSession, request, cancellationToken); return; } } @@ -73,7 +73,7 @@ public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string t { using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceOpenReadStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); } @@ -116,7 +116,7 @@ public async Task SpaceDownloadFileIntoFolderAsync(string remoteFilePath, string using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) { - using (var serverStreamingData = await _morphServerApiClient.SpaceOpenReadStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) { destFileName = Path.Combine(targetLocalFolder, serverStreamingData.FileName); @@ -166,7 +166,7 @@ public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder OverwriteExistingFile = overwriteExistingFile, ServerFolder = serverFolder }; - await _morphServerApiClient.SpaceUploadFileStreamAsync(_apiSession, request, cancellationToken); + await _morphServerApiClient.SpaceUploadDataStreamAsync(_apiSession, request, cancellationToken); return; } } diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index 7d8ad3f..d433352 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -12,16 +12,25 @@ namespace Morph.Server.Sdk.Client { - - public interface IMorphServerApiClient:IDisposable - { - event EventHandler OnDataDownloadProgress; - event EventHandler OnDataUploadProgress; + public interface IHasConfig + { IClientConfiguration Config { get; } + } + internal interface ICanCloseSession: IHasConfig, IDisposable + { Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken); + } + + public interface IMorphServerApiClient: IHasConfig, IDisposable + { + event EventHandler OnDataDownloadProgress; + event EventHandler OnDataUploadProgress; + + + Task GetServerStatusAsync(CancellationToken cancellationToken); Task GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task OpenSessionAsync(OpenSessionRequest openSessionRequest, CancellationToken cancellationToken); @@ -39,10 +48,10 @@ public interface IMorphServerApiClient:IDisposable Task SpaceDeleteFileAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); Task SpaceFileExistsAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceOpenReadStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenDataStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); - Task SpaceUploadFileStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken); + Task SpaceUploadDataStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken); Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, SpaceUploadContiniousStreamRequest continiousStreamRequest, CancellationToken cancellationToken); } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index da328fc..87c55bd 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -14,6 +14,7 @@ using System.IO; using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Dto; +using System.Collections.Concurrent; namespace Morph.Server.Sdk.Client { @@ -22,8 +23,9 @@ namespace Morph.Server.Sdk.Client /// /// Morph Server api client V1 /// - public class MorphServerApiClient : IMorphServerApiClient, IDisposable + public class MorphServerApiClient : IMorphServerApiClient, IDisposable, ICanCloseSession { + public event EventHandler OnDataDownloadProgress; public event EventHandler OnDataUploadProgress; @@ -33,6 +35,10 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable private readonly ILowLevelApiClient _lowLevelApiClient; private ClientConfiguration clientConfiguration = new ClientConfiguration(); + private bool _disposed = false; + private object _lock = new object(); + + public IClientConfiguration Config => clientConfiguration; internal ILowLevelApiClient BuildApiClient(ClientConfiguration configuration) @@ -69,7 +75,7 @@ Not implemented /// Construct Api client /// /// Server url - public MorphServerApiClient(string apiHost) + public MorphServerApiClient(Uri apiHost) { if (apiHost == null) { @@ -78,7 +84,7 @@ public MorphServerApiClient(string apiHost) var defaultConfig = new ClientConfiguration { - ApiUri = new Uri(apiHost) + ApiUri = apiHost }; clientConfiguration = defaultConfig; _lowLevelApiClient = BuildApiClient(clientConfiguration); @@ -125,7 +131,7 @@ protected HttpClient BuildHttpClient(ClientConfiguration config, HttpClientHandl client.DefaultRequestHeaders.Add("X-Client-Sdk", config.SDKVersionString); client.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); - client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=60"); + client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=120"); client.MaxResponseContentBufferSize = 100 * 1024; client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue @@ -177,6 +183,11 @@ public Task StartTaskAsync(ApiSession apiSession, StartTaskRe internal virtual async Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) { + if (_disposed) + { + throw new ObjectDisposedException(nameof(MorphServerApiClient)); + } + TimeSpan maxExecutionTime; switch (operationType) { @@ -184,26 +195,63 @@ internal virtual async Task Wrapped(Func _ctsForDisposing = new ConcurrentBag(); + + private void RegisterForDisposing(CancellationTokenSource derTokenSource) + { + if (derTokenSource == null) + { + throw new ArgumentNullException(nameof(derTokenSource)); + } + + _ctsForDisposing.Add(derTokenSource); + } internal virtual void FailIfError(ApiResult apiResult) { @@ -234,7 +282,7 @@ internal virtual TDataModel MapOrFail(ApiResult apiResul /// api session /// /// - public Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + Task ICanCloseSession.CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) { if (apiSession == null) { @@ -362,7 +410,7 @@ public Task GetServerStatusAsync(CancellationToken cancellationTok var apiResult = await _lowLevelApiClient.ServerGetStatusAsync(token); return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.SessionOpenAndRelated); } public async Task GetSpacesListAsync(CancellationToken cancellationToken) @@ -372,7 +420,7 @@ public async Task GetSpacesListAsync(CancellationToken ca var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); - }, cancellationToken, OperationType.ShortOperation); + }, cancellationToken, OperationType.SessionOpenAndRelated); } private void DownloadProgress_StateChanged(object sender, FileTransferProgressEventArgs e) @@ -519,8 +567,8 @@ public async Task OpenSessionAsync(OpenSessionRequest openSessionReq using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct)) { - // no more than 20 sec for session opening - var timeout = TimeSpan.FromSeconds(20); + + var timeout = clientConfiguration.SessionOpenTimeout; linkedTokenSource.CancelAfter(timeout); var cancellationToken = linkedTokenSource.Token; try @@ -536,7 +584,7 @@ public async Task OpenSessionAsync(OpenSessionRequest openSessionReq var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, new OpenSessionAuthenticatorContext( _lowLevelApiClient, - this, + this as ICanCloseSession, (handler) => ConstructRestApiClient(BuildHttpClient(clientConfiguration, handler))), openSessionRequest, cancellationToken); @@ -575,7 +623,7 @@ public Task GetTaskAsync(ApiSession apiSession, Guid taskId, Cancella } - public Task SpaceOpenReadStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -597,13 +645,19 @@ public Task SpaceOpenReadStreamingDataAsync(ApiSession apiS public void Dispose() { - if (_lowLevelApiClient != null) + lock (_lock) { - _lowLevelApiClient.Dispose(); + if (_disposed) + return; + if (_lowLevelApiClient != null) + _lowLevelApiClient.Dispose(); + + Array.ForEach(_ctsForDisposing.ToArray(), z => z.Dispose()); + _disposed = true; } } - public Task SpaceOpenFileStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + public Task SpaceOpenDataStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { @@ -648,7 +702,7 @@ await _lowLevelApiClient.WebFilesOpenContiniousPutStreamAsync(apiSession, contin } - public Task SpaceUploadFileStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken) + public Task SpaceUploadDataStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken) { if (apiSession == null) { diff --git a/src/Client/MorphServerApiClientGlobalConfig.cs b/src/Client/MorphServerApiClientGlobalConfig.cs index 1670d7c..bb2d180 100644 --- a/src/Client/MorphServerApiClientGlobalConfig.cs +++ b/src/Client/MorphServerApiClientGlobalConfig.cs @@ -28,10 +28,12 @@ public static class MorphServerApiClientGlobalConfig private const string DefaultClientType = "EMS-SDK"; + public static TimeSpan SessionOpenTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// /// Default operation execution timeout /// - public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromSeconds(30); + public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromMinutes(2); /// /// Default File transfer operation timeout /// @@ -49,6 +51,10 @@ public static class MorphServerApiClientGlobalConfig // "Morph.Server.Sdk/x.x.x.x" internal static string SDKVersionString { get; } + /// + /// dispose client when session is closed + /// + public static bool AutoDisposeClientOnSessionClose { get; set; } = true; static MorphServerApiClientGlobalConfig() { diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 3118547..a87553f 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -211,7 +211,7 @@ ex.InnerException is WebException web && streamContent.Dispose(); content.Dispose(); } - catch (Exception ex) + catch (Exception) { // dd } diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index 2888fad..a4b5432 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -16,16 +16,20 @@ public class ApiSession : IDisposable internal const string AuthHeaderName = "X-EasyMorph-Auth"; internal bool IsClosed { get; set; } - public string SpaceName { get => - string.IsNullOrWhiteSpace(_spaceName) ? _defaultSpaceName : _spaceName.ToLower(); - internal set => _spaceName = value; } + public string SpaceName + { + get => +string.IsNullOrWhiteSpace(_spaceName) ? _defaultSpaceName : _spaceName.ToLower(); + internal set => _spaceName = value; + } internal string AuthToken { get; set; } internal bool IsAnonymous { get; set; } - IMorphServerApiClient _client; + ICanCloseSession _client; private string _spaceName; + private SemaphoreSlim _lock = new SemaphoreSlim(0, 1); - internal ApiSession(IMorphServerApiClient client) + internal ApiSession(ICanCloseSession client) { _client = client; IsClosed = false; @@ -52,25 +56,67 @@ internal static ApiSession Anonymous(string spaceName) } + public async Task CloseSessionAsync(CancellationToken cancellationToken) + { + + await _lock.WaitAsync(cancellationToken); + try + { + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); + await _client.CloseSessionAsync(this, linkedCts.Token); + + // don't dispose client implicitly, just remove link to client + if (_client.Config.AutoDisposeClientOnSessionClose) + { + _client.Dispose(); + } + _client = null; + + IsClosed = true; + } + } + finally + { + _lock.Release(); + } + } + public void Dispose() { try { - if (!IsClosed && _client!=null) + if (_lock != null) { - Task.Run(async () => + _lock.Wait(5000); + try { - var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5)); - await _client.CloseSessionAsync(this, cts.Token); - // don't dispose client implicitly, just remove link to client - //_client.Dispose(); - _client = null; - } + if (!IsClosed && _client != null) + { + Task.Run(async () => + { + try + { + await this.CloseSessionAsync(CancellationToken.None); + } + catch (Exception) + { - ).Wait(TimeSpan.FromSeconds(5)); + } + }).Wait(TimeSpan.FromSeconds(5)); - this.IsClosed = true; + + } + + } + finally + { + _lock.Release(); + _lock.Dispose(); + _lock = null; + } } } catch (Exception) @@ -79,4 +125,4 @@ public void Dispose() } } } -} +} \ No newline at end of file diff --git a/src/Model/ClientConfiguration.cs b/src/Model/ClientConfiguration.cs index 19f4d55..79c117b 100644 --- a/src/Model/ClientConfiguration.cs +++ b/src/Model/ClientConfiguration.cs @@ -12,10 +12,13 @@ public class ClientConfiguration : IClientConfiguration public TimeSpan OperationTimeout { get; set; } = MorphServerApiClientGlobalConfig.OperationTimeout; public TimeSpan FileTransferTimeout { get; set; } = MorphServerApiClientGlobalConfig.FileTransferTimeout; public TimeSpan HttpClientTimeout { get; set; } = MorphServerApiClientGlobalConfig.HttpClientTimeout; + public TimeSpan SessionOpenTimeout { get; set; } = MorphServerApiClientGlobalConfig.SessionOpenTimeout; public string ClientId { get; set; } = MorphServerApiClientGlobalConfig.ClientId; public string ClientType { get; set; } = MorphServerApiClientGlobalConfig.ClientType; + public bool AutoDisposeClientOnSessionClose { get; set; } = MorphServerApiClientGlobalConfig.AutoDisposeClientOnSessionClose; + public Uri ApiUri { get; set; } internal string SDKVersionString { get; set; } = MorphServerApiClientGlobalConfig.SDKVersionString; #if NETSTANDARD2_0 diff --git a/src/Model/IClientConfiguration.cs b/src/Model/IClientConfiguration.cs index db849ef..e47563d 100644 --- a/src/Model/IClientConfiguration.cs +++ b/src/Model/IClientConfiguration.cs @@ -10,6 +10,12 @@ public interface IClientConfiguration TimeSpan OperationTimeout { get; } TimeSpan FileTransferTimeout { get; } TimeSpan HttpClientTimeout { get; } + TimeSpan SessionOpenTimeout { get; } + + string ClientId { get; } + string ClientType { get; } + + bool AutoDisposeClientOnSessionClose { get; } Uri ApiUri { get; } #if NETSTANDARD2_0 diff --git a/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs index c4162e1..a4da307 100644 --- a/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs +++ b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs @@ -11,7 +11,7 @@ internal class OpenSessionAuthenticatorContext public OpenSessionAuthenticatorContext (ILowLevelApiClient lowLevelApiClient, - IMorphServerApiClient morphServerApiClient, + ICanCloseSession morphServerApiClient, Func buildApiClient ) @@ -23,7 +23,7 @@ Func buildApiClient } public ILowLevelApiClient LowLevelApiClient { get; } - public IMorphServerApiClient MorphServerApiClient { get; } + public ICanCloseSession MorphServerApiClient { get; } public Func BuildApiClient { get; } } diff --git a/src/Model/InternalModels/OperationType.cs b/src/Model/InternalModels/OperationType.cs index 0ff2c5b..05a7bdc 100644 --- a/src/Model/InternalModels/OperationType.cs +++ b/src/Model/InternalModels/OperationType.cs @@ -3,7 +3,8 @@ internal enum OperationType { ShortOperation = 1, - FileTransfer = 2 + FileTransfer = 2, + SessionOpenAndRelated = 3 } } From 0ac6ec14230639ea9bbdb18002aebabc7163c316 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 5 Feb 2019 17:52:38 +0200 Subject: [PATCH 21/37] transfer utility fix --- src/Client/DataTransferUtility.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs index 584761d..a7ab2a0 100644 --- a/src/Client/DataTransferUtility.cs +++ b/src/Client/DataTransferUtility.cs @@ -59,13 +59,13 @@ public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string t throw new ArgumentNullException(nameof(targetLocalFilePath)); } - string destFileName = Path.GetFileName(targetLocalFilePath); + var localFolder = Path.GetDirectoryName(targetLocalFilePath); var tempFile = Path.Combine(localFolder, Guid.NewGuid().ToString("D") + ".emtmp"); - if (!overwriteExistingFile && File.Exists(destFileName)) + if (!overwriteExistingFile && File.Exists(targetLocalFilePath)) { - throw new Exception($"Destination file '{destFileName}' already exists."); + throw new Exception($"Destination file '{targetLocalFilePath}' already exists."); } @@ -79,11 +79,11 @@ public async Task SpaceDownloadFileIntoFileAsync(string remoteFilePath, string t } } - if (File.Exists(destFileName)) + if (File.Exists(targetLocalFilePath)) { - File.Delete(destFileName); + File.Delete(targetLocalFilePath); } - File.Move(tempFile, destFileName); + File.Move(tempFile, targetLocalFilePath); } finally From cfc662a6d6c33bf0f5d97f37d155c92676c586c3 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 5 Feb 2019 17:53:32 +0200 Subject: [PATCH 22/37] some performance optimizations --- src/Helper/StreamWithProgress.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index a00eeb2..d6ade11 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -56,14 +56,14 @@ public override int Read(byte[] buffer, int offset, int count) { onTokenCancelled(); } - var bytesRead = stream.Read(buffer, offset, count); + var bytesRead = stream.Read(buffer, offset, count); + _readPosition += bytesRead; RaiseOnReadProgress(bytesRead); return bytesRead; } private void RaiseOnReadProgress(int bytesRead) - { - _readPosition += bytesRead; + { if (onReadProgress != null) { var args = new StreamProgressEventArgs(bytesRead ); @@ -155,6 +155,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, onTokenCancelled(); } var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + _readPosition += bytesRead; RaiseOnReadProgress(bytesRead); return bytesRead; } @@ -163,7 +164,8 @@ public override int ReadByte() var @byte = stream.ReadByte(); if (@byte != -1) { - RaiseOnReadProgress(1); + _readPosition += 1; + // RaiseOnReadProgress(1); } return @byte; } From f93bb6e29e574f43997e0c472867ec24ac53ae90 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 5 Feb 2019 18:24:40 +0200 Subject: [PATCH 23/37] session closing fix --- src/Model/ApiSession.cs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index a4b5432..dd0b0cc 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -27,7 +27,7 @@ public string SpaceName ICanCloseSession _client; private string _spaceName; - private SemaphoreSlim _lock = new SemaphoreSlim(0, 1); + private SemaphoreSlim _lock = new SemaphoreSlim(1, 1); internal ApiSession(ICanCloseSession client) { @@ -62,20 +62,7 @@ public async Task CloseSessionAsync(CancellationToken cancellationToken) await _lock.WaitAsync(cancellationToken); try { - using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - { - linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); - await _client.CloseSessionAsync(this, linkedCts.Token); - - // don't dispose client implicitly, just remove link to client - if (_client.Config.AutoDisposeClientOnSessionClose) - { - _client.Dispose(); - } - _client = null; - - IsClosed = true; - } + await _InternalCloseSessionAsync(cancellationToken); } finally { @@ -83,6 +70,23 @@ public async Task CloseSessionAsync(CancellationToken cancellationToken) } } + private async Task _InternalCloseSessionAsync(CancellationToken cancellationToken) + { + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); + await _client.CloseSessionAsync(this, linkedCts.Token); + + // don't dispose client implicitly, just remove link to client + if (_client.Config.AutoDisposeClientOnSessionClose) + { + _client.Dispose(); + } + _client = null; + + IsClosed = true; + } + } public void Dispose() { @@ -99,7 +103,7 @@ public void Dispose() { try { - await this.CloseSessionAsync(CancellationToken.None); + await _InternalCloseSessionAsync(CancellationToken.None); } catch (Exception) { From 54cbf977339219236e9f7d37fd3e307b1f7f599c Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 5 Feb 2019 22:30:13 +0200 Subject: [PATCH 24/37] fix server response reading for http head request --- src/Client/MorphServerRestClient.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index a87553f..afd138b 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -70,7 +70,11 @@ protected virtual async Task> SendAsyncApiResult(response); } From 6e8e094d819e4eead99b81ec5fda71f85268789a Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 8 Feb 2019 17:56:34 +0200 Subject: [PATCH 25/37] StreamWithProgress performance optimizations --- src/Client/MorphServerRestClient.cs | 25 ++++---- src/Helper/StreamProgressEventArgs.cs | 10 +-- src/Helper/StreamWithProgress.cs | 90 ++++++++++++++++++--------- src/Model/ApiSession.cs | 2 +- 4 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index afd138b..5fca1e6 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -14,6 +14,7 @@ using Morph.Server.Sdk.Model.InternalModels; using Morph.Server.Sdk.Dto; using Morph.Server.Sdk.Events; +using static Morph.Server.Sdk.Helper.StreamWithProgress; namespace Morph.Server.Sdk.Client { @@ -311,39 +312,39 @@ protected async Task> RetrieveFileStreamAsync(Htt downloadProgress = new FileProgress(realFileName, contentLength.Value, onReceiveProgress); } downloadProgress?.ChangeState(FileProgressState.Starting); - var totalProcessedBytes = 0; + long totalProcessedBytes = 0; { // stream must be disposed by a caller Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); - DateTime _lastUpdate = DateTime.MinValue; + var streamWithProgress = new StreamWithProgress(streamToReadFrom, contentLength.Value, cancellationToken, e => { + // on read progress handler if (downloadProgress != null) { - totalProcessedBytes += e.BytesProcessed; - if ((DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500)) || e.BytesProcessed == 0) - { - downloadProgress.SetProcessedBytes(totalProcessedBytes); - _lastUpdate = DateTime.Now; - } - + totalProcessedBytes = e.TotalBytesRead; + downloadProgress.SetProcessedBytes(totalProcessedBytes); } }, () => { - + // on disposed handler if (downloadProgress != null && downloadProgress.ProcessedBytes != totalProcessedBytes) { downloadProgress.ChangeState(FileProgressState.Cancelled); } response.Dispose(); }, - () => + (tokenCancellationReason) => { - throw new Exception("Timeout"); + // on tokenCancelled + if (tokenCancellationReason == TokenCancellationReason.HttpTimeoutToken) + { + throw new Exception("Timeout"); + } }); return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); diff --git a/src/Helper/StreamProgressEventArgs.cs b/src/Helper/StreamProgressEventArgs.cs index 8b89c68..58b3da1 100644 --- a/src/Helper/StreamProgressEventArgs.cs +++ b/src/Helper/StreamProgressEventArgs.cs @@ -4,15 +4,17 @@ namespace Morph.Server.Sdk.Helper { internal class StreamProgressEventArgs : EventArgs { - public int BytesProcessed { get; } - + public long TotalBytesRead { get; } + public int BytesRead { get; } + public StreamProgressEventArgs() { } - public StreamProgressEventArgs(int bytesProcessed):this() + public StreamProgressEventArgs(long totalBytesRead, int bytesRead) :this() { - BytesProcessed = bytesProcessed; + TotalBytesRead = totalBytesRead; + BytesRead = bytesRead; } } } diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index d6ade11..242afa8 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -7,30 +7,38 @@ namespace Morph.Server.Sdk.Helper { internal class StreamWithProgress : Stream { + internal enum TokenCancellationReason + { + HttpTimeoutToken, + OperationCancellationToken + } + private readonly Stream stream; private readonly long streamLength; private readonly Action onReadProgress; private readonly Action onWriteProgress = null; private readonly Action onDisposed; - private readonly Action onTokenCancelled; + private readonly Action onTokenCancelled; private readonly CancellationToken httpTimeoutToken; - + private DateTime _lastUpdate = DateTime.MinValue; private long _readPosition = 0; + private bool _disposed = false; + public StreamWithProgress(Stream httpStream, long streamLength, CancellationToken mainTokem, - Action onReadProgress = null, + Action onReadProgress = null, Action onDisposed = null, - Action onTokenCancelled = null + Action onTokenCancelled = null ) { this.stream = httpStream ?? throw new ArgumentNullException(nameof(httpStream)); this.streamLength = streamLength; this.onReadProgress = onReadProgress; - + this.onDisposed = onDisposed; this.onTokenCancelled = onTokenCancelled; this.httpTimeoutToken = mainTokem; @@ -54,29 +62,37 @@ public override int Read(byte[] buffer, int offset, int count) { if (httpTimeoutToken.IsCancellationRequested) { - onTokenCancelled(); + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); } var bytesRead = stream.Read(buffer, offset, count); - _readPosition += bytesRead; - RaiseOnReadProgress(bytesRead); - return bytesRead; + + IncrementReadProgress(bytesRead); + return bytesRead; } - private void RaiseOnReadProgress(int bytesRead) - { + private void IncrementReadProgress(int bytesRead) + { + _readPosition += bytesRead; + var totalBytesRead = _readPosition; + if (onReadProgress != null) { - var args = new StreamProgressEventArgs(bytesRead ); - onReadProgress(args); + if (DateTime.Now - _lastUpdate > TimeSpan.FromMilliseconds(500) || bytesRead == 0) + { + _lastUpdate = DateTime.Now; + var args = new StreamProgressEventArgs(totalBytesRead, bytesRead); + onReadProgress(args); + + } } } private void RaiseOnWriteProgress(int bytesWrittens) { - if (onWriteProgress != null) - { - var args = new StreamProgressEventArgs(bytesWrittens); - onWriteProgress(args); - } + //if (onWriteProgress != null) + //{ + // var args = new StreamProgressEventArgs(bytesWrittens); + // onWriteProgress(args); + //} } public override long Seek(long offset, SeekOrigin origin) @@ -118,7 +134,14 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance { if (httpTimeoutToken.IsCancellationRequested) { - onTokenCancelled(); + if (cancellationToken.IsCancellationRequested) + { + onTokenCancelled(TokenCancellationReason.OperationCancellationToken); + } + else + { + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); + } } await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); } @@ -147,16 +170,22 @@ public override object InitializeLifetimeService() { return stream.InitializeLifetimeService(); } - + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (httpTimeoutToken.IsCancellationRequested) { - onTokenCancelled(); + if (cancellationToken.IsCancellationRequested) + { + onTokenCancelled(TokenCancellationReason.OperationCancellationToken); + } + else + { + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); + } } var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - _readPosition += bytesRead; - RaiseOnReadProgress(bytesRead); + IncrementReadProgress(bytesRead); return bytesRead; } public override int ReadByte() @@ -164,8 +193,7 @@ public override int ReadByte() var @byte = stream.ReadByte(); if (@byte != -1) { - _readPosition += 1; - // RaiseOnReadProgress(1); + IncrementReadProgress(1); } return @byte; } @@ -189,12 +217,18 @@ public override void WriteByte(byte value) protected override void Dispose(bool disposing) { + if (disposing) { - stream.Dispose(); - if (onDisposed != null) + if (!_disposed) { - onDisposed(); + _disposed = true; + + stream.Dispose(); + if (onDisposed != null) + { + onDisposed(); + } } } } diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index dd0b0cc..69df3db 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -105,7 +105,7 @@ public void Dispose() { await _InternalCloseSessionAsync(CancellationToken.None); } - catch (Exception) + catch (Exception ex) { } From 7903071d0a4e7916d1e689a4e6a8855ef5b0c887 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Fri, 8 Feb 2019 20:04:42 +0200 Subject: [PATCH 26/37] Stream download cancellation fix --- src/Client/MorphServerRestClient.cs | 6 +++++- src/Helper/StreamWithProgress.cs | 14 +++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 5fca1e6..60f7c27 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -338,13 +338,17 @@ protected async Task> RetrieveFileStreamAsync(Htt } response.Dispose(); }, - (tokenCancellationReason) => + (tokenCancellationReason, token) => { // on tokenCancelled if (tokenCancellationReason == TokenCancellationReason.HttpTimeoutToken) { throw new Exception("Timeout"); } + if(tokenCancellationReason == TokenCancellationReason.OperationCancellationToken) + { + throw new OperationCanceledException(token); + } }); return ApiResult.Ok(new FetchFileStreamData(streamWithProgress, realFileName, contentLength)); diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs index 242afa8..369e587 100644 --- a/src/Helper/StreamWithProgress.cs +++ b/src/Helper/StreamWithProgress.cs @@ -18,7 +18,7 @@ internal enum TokenCancellationReason private readonly Action onReadProgress; private readonly Action onWriteProgress = null; private readonly Action onDisposed; - private readonly Action onTokenCancelled; + private readonly Action onTokenCancelled; private readonly CancellationToken httpTimeoutToken; private DateTime _lastUpdate = DateTime.MinValue; private long _readPosition = 0; @@ -31,7 +31,7 @@ public StreamWithProgress(Stream httpStream, CancellationToken mainTokem, Action onReadProgress = null, Action onDisposed = null, - Action onTokenCancelled = null + Action onTokenCancelled = null ) { @@ -62,7 +62,7 @@ public override int Read(byte[] buffer, int offset, int count) { if (httpTimeoutToken.IsCancellationRequested) { - onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); } var bytesRead = stream.Read(buffer, offset, count); @@ -136,11 +136,11 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance { if (cancellationToken.IsCancellationRequested) { - onTokenCancelled(TokenCancellationReason.OperationCancellationToken); + onTokenCancelled(TokenCancellationReason.OperationCancellationToken, cancellationToken); } else { - onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); } } await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); @@ -177,11 +177,11 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { if (cancellationToken.IsCancellationRequested) { - onTokenCancelled(TokenCancellationReason.OperationCancellationToken); + onTokenCancelled(TokenCancellationReason.OperationCancellationToken, cancellationToken); } else { - onTokenCancelled(TokenCancellationReason.HttpTimeoutToken); + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); } } var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); From f9c258a0522c69a08029c6b50463ffbe35d07e63 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 12 Feb 2019 16:36:10 +0200 Subject: [PATCH 27/37] xml doc --- src/Morph.Server.Sdk.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index 8333f4e..b0df20c 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -7,6 +7,12 @@ true Morph.Server.Sdk.snk + + Morph.Server.Sdk.xml + + + Morph.Server.Sdk.xml + From bfc2a64dd1e952dcbfce9b7a44d3053bfd14aded Mon Sep 17 00:00:00 2001 From: constantin_k Date: Tue, 12 Feb 2019 16:41:32 +0200 Subject: [PATCH 28/37] xml doc generation --- src/Morph.Server.Sdk.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index b0df20c..6bdfaec 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -13,6 +13,12 @@ Morph.Server.Sdk.xml + + Morph.Server.Sdk.xml + + + Morph.Server.Sdk.xml + From b2f4247add02ddd3ca24831cca6a3258fa9bb73b Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 13 Feb 2019 12:37:31 +0200 Subject: [PATCH 29/37] AssemblyInfo.cs updated to version 1.4.0.0 --- src/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 6c80c38..974f961 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.3.6.0")] -[assembly: AssemblyFileVersion("1.3.6.0")] +[assembly: AssemblyVersion("1.4.0.0")] +[assembly: AssemblyFileVersion("1.4.0.0")] From 54c249bd025e7775a948b126f5025ed7fa0cf1ae Mon Sep 17 00:00:00 2001 From: constantin_k Date: Wed, 13 Feb 2019 15:19:39 +0200 Subject: [PATCH 30/37] trying to fix some dependencies related to ns2.0 --- src/Morph.Server.Sdk.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index 6bdfaec..a66302e 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -24,4 +24,8 @@ + + + + \ No newline at end of file From 826c2356f5cca6e4c1aa9452b0f649e79c865b59 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 15 Apr 2019 15:46:44 +0300 Subject: [PATCH 31/37] 1.4.0.1 - some changes required to allow SDK extending by 3rd parties --- src/Client/IRestClient.cs | 2 +- src/Client/MorphServerApiClient.cs | 11 +++++++---- src/Client/MorphServerRestClient.cs | 2 +- src/Dto/NoContentRequest.cs | 2 +- src/Dto/NoContentResult.cs | 2 +- src/Helper/UrlHelper.cs | 4 ++-- src/Model/ApiSession.cs | 8 ++++---- src/Model/InternalModels/ApiResult.cs | 2 +- .../InternalModels/ContiniousStreamingRequest.cs | 2 +- src/Model/InternalModels/FetchFileStreamData.cs | 2 +- src/Model/InternalModels/HeadersCollection.cs | 2 +- src/Model/InternalModels/OperationType.cs | 2 +- src/Model/InternalModels/SendFileStreamData.cs | 2 +- src/Model/InternalModels/ServerPushStreaming.cs | 2 +- src/Morph.Server.Sdk.csproj | 5 +++++ src/Properties/AssemblyInfo.cs | 4 ++-- 16 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/Client/IRestClient.cs b/src/Client/IRestClient.cs index eadff79..e854894 100644 --- a/src/Client/IRestClient.cs +++ b/src/Client/IRestClient.cs @@ -9,7 +9,7 @@ namespace Morph.Server.Sdk.Client { - internal interface IRestClient : IDisposable + public interface IRestClient : IDisposable { HttpClient HttpClient { get; set; } Task> GetAsync(string url, NameValueCollection urlParameters, HeadersCollection headersCollection, CancellationToken cancellationToken) diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 87c55bd..786e198 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -33,6 +33,7 @@ public class MorphServerApiClient : IMorphServerApiClient, IDisposable, ICanClos protected readonly string _api_v1 = "api/v1/"; private readonly ILowLevelApiClient _lowLevelApiClient; + protected readonly IRestClient RestClient; private ClientConfiguration clientConfiguration = new ClientConfiguration(); private bool _disposed = false; @@ -88,12 +89,14 @@ public MorphServerApiClient(Uri apiHost) }; clientConfiguration = defaultConfig; _lowLevelApiClient = BuildApiClient(clientConfiguration); + RestClient = _lowLevelApiClient.RestClient; } public MorphServerApiClient(ClientConfiguration clientConfiguration) { this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration)); _lowLevelApiClient = BuildApiClient(clientConfiguration); + RestClient = _lowLevelApiClient.RestClient; } @@ -181,7 +184,7 @@ public Task StartTaskAsync(ApiSession apiSession, StartTaskRe }, cancellationToken, OperationType.ShortOperation); } - internal virtual async Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) + protected virtual async Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) { if (_disposed) { @@ -198,7 +201,7 @@ internal virtual async Task Wrapped(Func(ApiResult apiResult) + protected virtual void FailIfError(ApiResult apiResult) { if (!apiResult.IsSucceed) { @@ -263,7 +266,7 @@ internal virtual void FailIfError(ApiResult apiResult) - internal virtual TDataModel MapOrFail(ApiResult apiResult, Func maper) + protected virtual TDataModel MapOrFail(ApiResult apiResult, Func maper) { if (apiResult.IsSucceed) { diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 60f7c27..f1d2750 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -20,7 +20,7 @@ namespace Morph.Server.Sdk.Client { - internal class MorphServerRestClient : IRestClient + public class MorphServerRestClient : IRestClient { private HttpClient httpClient; public HttpClient HttpClient { get => httpClient; set => httpClient = value; } diff --git a/src/Dto/NoContentRequest.cs b/src/Dto/NoContentRequest.cs index 6cd7609..58a6e48 100644 --- a/src/Dto/NoContentRequest.cs +++ b/src/Dto/NoContentRequest.cs @@ -3,7 +3,7 @@ namespace Morph.Server.Sdk.Dto { [DataContractAttribute] - internal sealed class NoContentRequest + public sealed class NoContentRequest { } diff --git a/src/Dto/NoContentResult.cs b/src/Dto/NoContentResult.cs index 8d1043b..86cc3fb 100644 --- a/src/Dto/NoContentResult.cs +++ b/src/Dto/NoContentResult.cs @@ -3,7 +3,7 @@ namespace Morph.Server.Sdk.Dto { - internal sealed class NoContentResult + public sealed class NoContentResult { } diff --git a/src/Helper/UrlHelper.cs b/src/Helper/UrlHelper.cs index 0aea043..b6a173c 100644 --- a/src/Helper/UrlHelper.cs +++ b/src/Helper/UrlHelper.cs @@ -3,9 +3,9 @@ namespace Morph.Server.Sdk.Helper { - internal static class UrlHelper + public static class UrlHelper { - internal static string JoinUrl(params string[] urlParts) + public static string JoinUrl(params string[] urlParts) { var result = string.Empty; for (var i = 0; i < urlParts.Length; i++) diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index 69df3db..fbb8088 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -13,17 +13,17 @@ namespace Morph.Server.Sdk.Model public class ApiSession : IDisposable { protected readonly string _defaultSpaceName = "default"; - internal const string AuthHeaderName = "X-EasyMorph-Auth"; + public const string AuthHeaderName = "X-EasyMorph-Auth"; - internal bool IsClosed { get; set; } + public bool IsClosed { get; internal set; } public string SpaceName { get => string.IsNullOrWhiteSpace(_spaceName) ? _defaultSpaceName : _spaceName.ToLower(); internal set => _spaceName = value; } - internal string AuthToken { get; set; } - internal bool IsAnonymous { get; set; } + public string AuthToken { get; internal set; } + public bool IsAnonymous { get; internal set; } ICanCloseSession _client; private string _spaceName; diff --git a/src/Model/InternalModels/ApiResult.cs b/src/Model/InternalModels/ApiResult.cs index 105831b..1f9e0a9 100644 --- a/src/Model/InternalModels/ApiResult.cs +++ b/src/Model/InternalModels/ApiResult.cs @@ -6,7 +6,7 @@ namespace Morph.Server.Sdk.Model.InternalModels /// Represents api result of DTO Model or Error (Exception) /// /// - internal class ApiResult + public class ApiResult { public T Data { get; set; } = default(T); public Exception Error { get; set; } = default(Exception); diff --git a/src/Model/InternalModels/ContiniousStreamingRequest.cs b/src/Model/InternalModels/ContiniousStreamingRequest.cs index f0d84be..94496a3 100644 --- a/src/Model/InternalModels/ContiniousStreamingRequest.cs +++ b/src/Model/InternalModels/ContiniousStreamingRequest.cs @@ -2,7 +2,7 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal class ContiniousStreamingRequest + public class ContiniousStreamingRequest { public ContiniousStreamingRequest(string fileName) { diff --git a/src/Model/InternalModels/FetchFileStreamData.cs b/src/Model/InternalModels/FetchFileStreamData.cs index 2ae879c..379dbca 100644 --- a/src/Model/InternalModels/FetchFileStreamData.cs +++ b/src/Model/InternalModels/FetchFileStreamData.cs @@ -3,7 +3,7 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal sealed class FetchFileStreamData + public sealed class FetchFileStreamData { public FetchFileStreamData(Stream stream, string fileName, long? fileSize) { diff --git a/src/Model/InternalModels/HeadersCollection.cs b/src/Model/InternalModels/HeadersCollection.cs index ff32baa..8bdd50a 100644 --- a/src/Model/InternalModels/HeadersCollection.cs +++ b/src/Model/InternalModels/HeadersCollection.cs @@ -4,7 +4,7 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal class HeadersCollection + public class HeadersCollection { private Dictionary _headers = new Dictionary(); public HeadersCollection() diff --git a/src/Model/InternalModels/OperationType.cs b/src/Model/InternalModels/OperationType.cs index 05a7bdc..d8afa32 100644 --- a/src/Model/InternalModels/OperationType.cs +++ b/src/Model/InternalModels/OperationType.cs @@ -1,6 +1,6 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal enum OperationType + public enum OperationType { ShortOperation = 1, FileTransfer = 2, diff --git a/src/Model/InternalModels/SendFileStreamData.cs b/src/Model/InternalModels/SendFileStreamData.cs index 6f21427..c99587f 100644 --- a/src/Model/InternalModels/SendFileStreamData.cs +++ b/src/Model/InternalModels/SendFileStreamData.cs @@ -3,7 +3,7 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal sealed class SendFileStreamData + public sealed class SendFileStreamData { public SendFileStreamData(Stream stream, string fileName, long fileSize) { diff --git a/src/Model/InternalModels/ServerPushStreaming.cs b/src/Model/InternalModels/ServerPushStreaming.cs index 38eeeed..ae0e788 100644 --- a/src/Model/InternalModels/ServerPushStreaming.cs +++ b/src/Model/InternalModels/ServerPushStreaming.cs @@ -7,7 +7,7 @@ namespace Morph.Server.Sdk.Model.InternalModels { - internal class ServerPushStreaming : IDisposable + public class ServerPushStreaming : IDisposable { internal readonly ContiniousSteamingHttpContent steamingContent; diff --git a/src/Morph.Server.Sdk.csproj b/src/Morph.Server.Sdk.csproj index a66302e..588e79e 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -28,4 +28,9 @@ + + + + + \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 974f961..c6bad0b 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.4.0.0")] -[assembly: AssemblyFileVersion("1.4.0.0")] +[assembly: AssemblyVersion("1.4.0.1")] +[assembly: AssemblyFileVersion("1.4.0.1")] From 50784e47d195c45cef4d38290c88ca0f530a2e38 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 23 May 2019 14:03:18 +0300 Subject: [PATCH 32/37] 1.4.0.2 custom error codes --- src/Client/MorphServerRestClient.cs | 26 ++++++++++++++++++-------- src/Dto/Errors/Error.cs | 2 +- src/Dto/Errors/ErrorResponse.cs | 2 +- src/Dto/Errors/InnerError.cs | 2 +- src/Helper/JsonSerializationHelper.cs | 2 +- src/Properties/AssemblyInfo.cs | 4 ++-- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index f1d2750..50a316f 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -81,7 +81,7 @@ protected virtual async Task> SendAsyncApiResult> HandleResponse(HttpResponseMessage response) + private async Task> HandleResponse(HttpResponseMessage response) where TResult : new() { if (response.IsSuccessStatusCode) @@ -114,23 +114,23 @@ protected HttpRequestMessage BuildHttpRequestMessage(HttpMethod httpMethod, stri - private static async Task BuildExceptionFromResponse(HttpResponseMessage response) + private async Task BuildExceptionFromResponse(HttpResponseMessage response) { - var content = await response.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) + var rawContent = await response.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(rawContent)) { ErrorResponse errorResponse = null; try { - errorResponse = JsonSerializationHelper.Deserialize(content); + errorResponse = DeserializeErrorResponse(rawContent); } catch (Exception) { - return new ResponseParseException("An error occurred while deserializing the response", content); + return new ResponseParseException("An error occurred while deserializing the response", rawContent); } if (errorResponse.error == null) - return new ResponseParseException("An error occurred while deserializing the response", content); + return new ResponseParseException("An error occurred while deserializing the response", rawContent); switch (errorResponse.error.code) { @@ -139,7 +139,7 @@ private static async Task BuildExceptionFromResponse(HttpResponseMess case ReadableErrorTopCode.Forbidden: return new MorphApiForbiddenException(errorResponse.error.message); case ReadableErrorTopCode.Unauthorized: return new MorphApiUnauthorizedException(errorResponse.error.message); case ReadableErrorTopCode.BadArgument: return new MorphApiBadArgumentException(FieldErrorsMapper.MapFromDto(errorResponse.error), errorResponse.error.message); - default: return new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); + default: return BuildCustomExceptionFromErrorResponse(rawContent, errorResponse); } } @@ -158,6 +158,16 @@ private static async Task BuildExceptionFromResponse(HttpResponseMess } } + protected virtual ErrorResponse DeserializeErrorResponse(string rawContent) + { + return JsonSerializationHelper.Deserialize(rawContent); + } + + protected virtual Exception BuildCustomExceptionFromErrorResponse(string rawContent, ErrorResponse errorResponse) + { + return new MorphClientGeneralException(errorResponse.error.code, errorResponse.error.message); + } + public void Dispose() { if (HttpClient != null) diff --git a/src/Dto/Errors/Error.cs b/src/Dto/Errors/Error.cs index 39e0f36..c04b5b8 100644 --- a/src/Dto/Errors/Error.cs +++ b/src/Dto/Errors/Error.cs @@ -10,7 +10,7 @@ namespace Morph.Server.Sdk.Dto.Errors { [DataContract] - internal class Error + public class Error { /// diff --git a/src/Dto/Errors/ErrorResponse.cs b/src/Dto/Errors/ErrorResponse.cs index 19fe371..8767789 100644 --- a/src/Dto/Errors/ErrorResponse.cs +++ b/src/Dto/Errors/ErrorResponse.cs @@ -9,7 +9,7 @@ namespace Morph.Server.Sdk.Dto.Errors { [DataContract] - internal class ErrorResponse + public class ErrorResponse { [DataMember] public Error error { get; set; } diff --git a/src/Dto/Errors/InnerError.cs b/src/Dto/Errors/InnerError.cs index 06f979e..b984b44 100644 --- a/src/Dto/Errors/InnerError.cs +++ b/src/Dto/Errors/InnerError.cs @@ -9,7 +9,7 @@ namespace Morph.Server.Sdk.Dto.Errors { [DataContract] - internal class InnerError + public class InnerError { [DataMember] public string code { get; set; } diff --git a/src/Helper/JsonSerializationHelper.cs b/src/Helper/JsonSerializationHelper.cs index 9f2cd24..93fb2fe 100644 --- a/src/Helper/JsonSerializationHelper.cs +++ b/src/Helper/JsonSerializationHelper.cs @@ -12,7 +12,7 @@ namespace Morph.Server.Sdk.Helper { - internal static class JsonSerializationHelper + public static class JsonSerializationHelper { public static T Deserialize(string input) where T: new() diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index c6bad0b..e3a6842 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.4.0.1")] -[assembly: AssemblyFileVersion("1.4.0.1")] +[assembly: AssemblyVersion("1.4.0.2")] +[assembly: AssemblyFileVersion("1.4.0.2")] From 5580210274564e4b3d089349a51d513759ea819d Mon Sep 17 00:00:00 2001 From: constantin_k Date: Thu, 23 May 2019 14:11:27 +0300 Subject: [PATCH 33/37] uncommited changes --- src/Client/MorphServerApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 786e198..5ab7ec0 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -100,7 +100,7 @@ public MorphServerApiClient(ClientConfiguration clientConfiguration) } - private IRestClient ConstructRestApiClient(HttpClient httpClient) + protected virtual IRestClient ConstructRestApiClient(HttpClient httpClient) { if (httpClient == null) { From a3dcce8dafa77646034024ef6362f6e7c8501f99 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 30 Sep 2019 12:32:00 +0300 Subject: [PATCH 34/37] 1.4.0.3 fix NRE on Anonymous session closing --- src/Client/MorphServerAuthenticator.cs | 4 +-- src/Model/ApiSession.cs | 49 +++++++++++++++++++------- src/Properties/AssemblyInfo.cs | 4 +-- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/Client/MorphServerAuthenticator.cs b/src/Client/MorphServerAuthenticator.cs index 9ecd3ba..77e31eb 100644 --- a/src/Client/MorphServerAuthenticator.cs +++ b/src/Client/MorphServerAuthenticator.cs @@ -25,7 +25,7 @@ public static async Task OpenSessionMultiplexedAsync( { // anon space case SpaceAccessRestriction.None: - return ApiSession.Anonymous(openSessionRequest.SpaceName); + return ApiSession.Anonymous(context.MorphServerApiClient, openSessionRequest.SpaceName); // password protected space case SpaceAccessRestriction.BasicPassword: @@ -41,7 +41,7 @@ public static async Task OpenSessionMultiplexedAsync( // if space is public or password is not set - open anon session if (desiredSpace.IsPublic || string.IsNullOrWhiteSpace(openSessionRequest.Password)) { - return ApiSession.Anonymous(openSessionRequest.SpaceName); + return ApiSession.Anonymous(context.MorphServerApiClient, openSessionRequest.SpaceName); } // otherwise open session via space password else diff --git a/src/Model/ApiSession.cs b/src/Model/ApiSession.cs index fbb8088..23afa87 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -9,7 +9,9 @@ namespace Morph.Server.Sdk.Model { - + /// + /// Disposable api session + /// public class ApiSession : IDisposable { protected readonly string _defaultSpaceName = "default"; @@ -29,24 +31,32 @@ public string SpaceName private string _spaceName; private SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + /// + /// Api session constructor + /// + /// reference to client internal ApiSession(ICanCloseSession client) { - _client = client; + _client = client ?? throw new ArgumentNullException(nameof(client)); IsClosed = false; IsAnonymous = false; } - internal static ApiSession Anonymous(string spaceName) + internal static ApiSession Anonymous(ICanCloseSession client, string spaceName) { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } if (string.IsNullOrWhiteSpace(spaceName)) { throw new ArgumentException("Value is empty {0}", nameof(spaceName)); } - return new ApiSession(null) + return new ApiSession(client) { IsAnonymous = true, IsClosed = false, @@ -72,19 +82,32 @@ public async Task CloseSessionAsync(CancellationToken cancellationToken) private async Task _InternalCloseSessionAsync(CancellationToken cancellationToken) { - using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + // don't close if session is already closed or anon. + if(IsClosed || _client == null || IsAnonymous) + { + return; + } + try { - linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); - await _client.CloseSessionAsync(this, linkedCts.Token); - // don't dispose client implicitly, just remove link to client - if (_client.Config.AutoDisposeClientOnSessionClose) + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - _client.Dispose(); - } - _client = null; + linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); + await _client.CloseSessionAsync(this, linkedCts.Token); - IsClosed = true; + // don't dispose client implicitly, just remove link to client + if (_client.Config.AutoDisposeClientOnSessionClose) + { + _client.Dispose(); + } + _client = null; + + IsClosed = true; + } + } + catch (Exception) + { + // } } diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index e3a6842..988da7d 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.4.0.2")] -[assembly: AssemblyFileVersion("1.4.0.2")] +[assembly: AssemblyVersion("1.4.0.3")] +[assembly: AssemblyFileVersion("1.4.0.3")] From a9c5c19e08d101695262ae9c5373b129ea4c0c8f Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 27 Jan 2020 17:49:02 +0200 Subject: [PATCH 35/37] version 1.4.1.0 changetaskmode : enabled/disabled --- src/Client/ILowLevelApiClient.cs | 1 + src/Client/IMorphServerApiClient.cs | 4 ++- src/Client/LowLevelApiClient.cs | 15 ++++++++++ src/Client/MorphServerApiClient.cs | 37 +++++++++++++++++++++++- src/Dto/SpaceTaskChangeModeRequestDto.cs | 11 +++++++ src/Model/TaskChangeModeRequest.cs | 7 +++++ src/Properties/AssemblyInfo.cs | 4 +-- 7 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/Dto/SpaceTaskChangeModeRequestDto.cs create mode 100644 src/Model/TaskChangeModeRequest.cs diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs index 83a43f5..5bdadb3 100644 --- a/src/Client/ILowLevelApiClient.cs +++ b/src/Client/ILowLevelApiClient.cs @@ -18,6 +18,7 @@ internal interface ILowLevelApiClient: IDisposable Task> GetTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); Task> GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); Task> GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> TaskChangeModeAsync(ApiSession apiSession, Guid taskId, SpaceTaskChangeModeRequestDto requestDto, CancellationToken cancellationToken); // RUN-STOP Task Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); diff --git a/src/Client/IMorphServerApiClient.cs b/src/Client/IMorphServerApiClient.cs index d433352..bdb4b46 100644 --- a/src/Client/IMorphServerApiClient.cs +++ b/src/Client/IMorphServerApiClient.cs @@ -37,7 +37,9 @@ public interface IMorphServerApiClient: IHasConfig, IDisposable Task StartTaskAsync(ApiSession apiSession, StartTaskRequest startTaskRequest, CancellationToken cancellationToken); Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); - + + + Task TaskChangeModeAsync(ApiSession apiSession, Guid taskId, TaskChangeModeRequest taskChangeModeRequest, CancellationToken cancellationToken); Task ValidateTasksAsync(ApiSession apiSession, string projectPath, CancellationToken cancellationToken); Task GetSpacesListAsync(CancellationToken cancellationToken); Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken); diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs index 581d455..1949a4e 100644 --- a/src/Client/LowLevelApiClient.cs +++ b/src/Client/LowLevelApiClient.cs @@ -92,6 +92,19 @@ public Task> GetTaskStatusAsync(ApiSession apiSession, } + public Task> TaskChangeModeAsync(ApiSession apiSession, Guid taskId, SpaceTaskChangeModeRequestDto requestDto, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "tasks", taskId.ToString("D"), "changeMode"); + + return apiClient.PostAsync(url, requestDto, null, apiSession.ToHeadersCollection(), cancellationToken); + } + public Task> ServerGetStatusAsync(CancellationToken cancellationToken) { var url = "server/status"; @@ -290,6 +303,8 @@ public Task> WebFilesOpenContiniousPutStreamAsync return apiClient.PushContiniousStreamingDataAsync(HttpMethod.Put, url, new ContiniousStreamingRequest(fileName), null, apiSession.ToHeadersCollection(), cancellationToken); } + + } } diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index 5ab7ec0..bd9229f 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -191,6 +191,7 @@ protected virtual async Task Wrapped(Func Wrapped(Func GetRunningTaskStatusAsync(ApiSession apiSession, } + /// + /// Change task mode + /// + /// api session + /// task guid + /// + /// cancellation token + /// Returns task status + public Task TaskChangeModeAsync(ApiSession apiSession, Guid taskId, TaskChangeModeRequest taskChangeModeRequest, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + if (taskChangeModeRequest is null) + { + throw new ArgumentNullException(nameof(taskChangeModeRequest)); + } + + return Wrapped(async (token) => + { + var request = new SpaceTaskChangeModeRequestDto + { + TaskEnabled = taskChangeModeRequest.TaskEnabled + }; + var apiResult = await _lowLevelApiClient.TaskChangeModeAsync(apiSession, taskId, request, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + + }, cancellationToken, OperationType.ShortOperation); + + } + /// /// Retrieves space status diff --git a/src/Dto/SpaceTaskChangeModeRequestDto.cs b/src/Dto/SpaceTaskChangeModeRequestDto.cs new file mode 100644 index 0000000..35d25fd --- /dev/null +++ b/src/Dto/SpaceTaskChangeModeRequestDto.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Morph.Server.Sdk.Dto +{ + [DataContract] + internal class SpaceTaskChangeModeRequestDto + { + [DataMember(Name = "taskEnabled")] + public bool? TaskEnabled { get; set; } = null; + } +} diff --git a/src/Model/TaskChangeModeRequest.cs b/src/Model/TaskChangeModeRequest.cs new file mode 100644 index 0000000..c8573f9 --- /dev/null +++ b/src/Model/TaskChangeModeRequest.cs @@ -0,0 +1,7 @@ +namespace Morph.Server.Sdk.Model +{ + public sealed class TaskChangeModeRequest + { + public bool? TaskEnabled { get; set; } + } +} \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 988da7d..9c91b60 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -21,5 +21,5 @@ [assembly: Guid("72ecc66f-62fe-463f-afad-e1ff5cc19cd9")] -[assembly: AssemblyVersion("1.4.0.3")] -[assembly: AssemblyFileVersion("1.4.0.3")] +[assembly: AssemblyVersion("1.4.1.0")] +[assembly: AssemblyFileVersion("1.4.1.0")] From 505785a175dcaa0c61b72aebcf1256fcd1f37227 Mon Sep 17 00:00:00 2001 From: constantin_k Date: Mon, 27 Jan 2020 17:55:13 +0200 Subject: [PATCH 36/37] ... --- src/Client/MorphServerApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/MorphServerApiClient.cs b/src/Client/MorphServerApiClient.cs index bd9229f..eda43d2 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -204,7 +204,7 @@ protected virtual async Task Wrapped(Func Date: Tue, 3 Mar 2020 14:18:51 +0200 Subject: [PATCH 37/37] 1.4.1.0: content processing ResponseHeadersRead --- src/Client/MorphServerRestClient.cs | 3 ++- src/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Client/MorphServerRestClient.cs b/src/Client/MorphServerRestClient.cs index 50a316f..fb9be12 100644 --- a/src/Client/MorphServerRestClient.cs +++ b/src/Client/MorphServerRestClient.cs @@ -73,7 +73,8 @@ protected virtual async Task> SendAsyncApiResult