diff --git a/src/Client/DataTransferUtility.cs b/src/Client/DataTransferUtility.cs new file mode 100644 index 0000000..a7ab2a0 --- /dev/null +++ b/src/Client/DataTransferUtility.cs @@ -0,0 +1,177 @@ +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 : IDataTransferUtility + { + private int BufferSize { get; set; } = 81920; + private readonly IMorphServerApiClient _morphServerApiClient; + private readonly ApiSession _apiSession; + + public DataTransferUtility(IMorphServerApiClient morphServerApiClient, ApiSession apiSession) + { + this._morphServerApiClient = morphServerApiClient ?? throw new ArgumentNullException(nameof(morphServerApiClient)); + this._apiSession = apiSession ?? throw new ArgumentNullException(nameof(apiSession)); + } + + public async Task SpaceUploadFileAsync(string localFilePath, string serverFolder, 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 SpaceUploadDataStreamRequest + { + DataStream = fsSource, + FileName = fileName, + FileSize = fileSize, + OverwriteExistingFile = overwriteExistingFile, + ServerFolder = serverFolder + }; + await _morphServerApiClient.SpaceUploadDataStreamAsync(_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)); + } + + + var localFolder = Path.GetDirectoryName(targetLocalFilePath); + var tempFile = Path.Combine(localFolder, Guid.NewGuid().ToString("D") + ".emtmp"); + + if (!overwriteExistingFile && File.Exists(targetLocalFilePath)) + { + throw new Exception($"Destination file '{targetLocalFilePath}' already exists."); + } + + + try + { + using (Stream tempFileStream = File.Open(tempFile, FileMode.Create)) + { + using (var serverStreamingData = await _morphServerApiClient.SpaceOpenStreamingDataAsync(_apiSession, remoteFilePath, cancellationToken)) + { + await serverStreamingData.Stream.CopyToAsync(tempFileStream, BufferSize, cancellationToken); + } + } + + if (File.Exists(targetLocalFilePath)) + { + File.Delete(targetLocalFilePath); + } + File.Move(tempFile, targetLocalFilePath); + + } + 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.SpaceOpenStreamingDataAsync(_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); + } + + } + + } + + 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 SpaceUploadDataStreamRequest + { + DataStream = fsSource, + FileName = destFileName, + FileSize = fileSize, + OverwriteExistingFile = overwriteExistingFile, + ServerFolder = serverFolder + }; + await _morphServerApiClient.SpaceUploadDataStreamAsync(_apiSession, request, cancellationToken); + return; + } + } + } + +} + + diff --git a/src/Client/IDataTransferUtility.cs b/src/Client/IDataTransferUtility.cs new file mode 100644 index 0000000..d198ade --- /dev/null +++ b/src/Client/IDataTransferUtility.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Client +{ + public interface IDataTransferUtility + { + 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); + } + +} \ No newline at end of file diff --git a/src/Client/ILowLevelApiClient.cs b/src/Client/ILowLevelApiClient.cs new file mode 100644 index 0000000..5bdadb3 --- /dev/null +++ b/src/Client/ILowLevelApiClient.cs @@ -0,0 +1,63 @@ +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; +using Morph.Server.Sdk.Events; + +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); + Task> TaskChangeModeAsync(ApiSession apiSession, Guid taskId, SpaceTaskChangeModeRequestDto requestDto, CancellationToken cancellationToken); + + // RUN-STOP Task + Task> GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken); + Task> StartTaskAsync(ApiSession apiSession, Guid taskId, TaskStartRequestDto taskStartRequestDto, CancellationToken cancellationToken); + 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, Action onReceiveProgress, 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); + + 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 b2392ac..bdb4b46 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; @@ -9,29 +12,49 @@ namespace Morph.Server.Sdk.Client { - public interface IMorphServerApiClient + + public interface IHasConfig { - event EventHandler FileProgress; + IClientConfiguration Config { get; } + } - Task BrowseSpaceAsync(ApiSession apiSession, string folderPath, CancellationToken cancellationToken); + internal interface ICanCloseSession: IHasConfig, IDisposable + { 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); + + } + + 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); - 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 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 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); Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken); Task GetTaskAsync(ApiSession apiSession, Guid taskId, 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 SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + Task SpaceOpenDataStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken); + + Task SpaceUploadDataStreamAsync(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/IRestClient.cs b/src/Client/IRestClient.cs new file mode 100644 index 0000000..e854894 --- /dev/null +++ b/src/Client/IRestClient.cs @@ -0,0 +1,43 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +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 +{ + public interface IRestClient : IDisposable + { + HttpClient HttpClient { get; set; } + 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> 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) + where TResult : new(); + + + } + + + +} \ No newline at end of file diff --git a/src/Client/LowLevelApiClient.cs b/src/Client/LowLevelApiClient.cs new file mode 100644 index 0000000..1949a4e --- /dev/null +++ b/src/Client/LowLevelApiClient.cs @@ -0,0 +1,312 @@ +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; +using Morph.Server.Sdk.Exceptions; +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 +{ + + internal class LowLevelApiClient : ILowLevelApiClient + { + private readonly IRestClient apiClient; + + public IRestClient RestClient => apiClient; + + public LowLevelApiClient(IRestClient 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> 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"; + return apiClient.GetAsync(url, null, new HeadersCollection(), cancellationToken); + } + + public Task> StartTaskAsync(ApiSession apiSession, Guid taskId, TaskStartRequestDto taskStartRequestDto, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); + } + + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); + + return apiClient.PostAsync(url, taskStartRequestDto, 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 serverFilePath, CancellationToken cancellationToken) + { + var spaceName = apiSession.SpaceName; + var url = UrlHelper.JoinUrl("space", spaceName, "files", serverFilePath); + + 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); + } + + public void Dispose() + { + apiClient.Dispose(); + } + + public Task> WebFilesDownloadFileAsync(ApiSession apiSession, string serverFilePath, Action onReceiveProgress, 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(), onReceiveProgress, 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); + + } + } + } + + public Task> WebFilesPutFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, 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(), onSendProgress, cancellationToken); + + } + + public Task> WebFilesPostFileStreamAsync(ApiSession apiSession, string serverFolder, SendFileStreamData sendFileStreamData, Action onSendProgress, 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(), onSendProgress, 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); + + 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 6018b5a..eda43d2 100644 --- a/src/Client/MorphServerApiClient.cs +++ b/src/Client/MorphServerApiClient.cs @@ -1,72 +1,118 @@ -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.Reflection; +using System.IO; +using Morph.Server.Sdk.Model.InternalModels; +using Morph.Server.Sdk.Dto; +using System.Collections.Concurrent; namespace Morph.Server.Sdk.Client { + + /// /// Morph Server api client V1 /// - public class MorphServerApiClient : IMorphServerApiClient, IDisposable + public class MorphServerApiClient : IMorphServerApiClient, IDisposable, ICanCloseSession { - protected readonly Uri _apiHost; - protected readonly string UserAgent = "MorphServerApiClient/1.3.5"; - protected HttpClient _httpClient; + + public event EventHandler OnDataDownloadProgress; + public event EventHandler OnDataUploadProgress; + + protected readonly string _userAgent = "MorphServerApiClient/next"; protected readonly string _api_v1 = "api/v1/"; + private readonly ILowLevelApiClient _lowLevelApiClient; + protected readonly IRestClient RestClient; + private ClientConfiguration clientConfiguration = new ClientConfiguration(); + + private bool _disposed = false; + private object _lock = new object(); + + + public IClientConfiguration Config => clientConfiguration; + + internal ILowLevelApiClient BuildApiClient(ClientConfiguration 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 = configuration.ServerCertificateCustomValidationCallback + }; +#elif NET45 + // 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); + } + /// /// Construct Api client /// /// Server url - public MorphServerApiClient(string apiHost) + public MorphServerApiClient(Uri apiHost) { - if (!apiHost.EndsWith("/")) - apiHost += "/"; - _apiHost = new Uri(apiHost); + if (apiHost == null) + { + throw new ArgumentNullException(nameof(apiHost)); + } + var defaultConfig = new ClientConfiguration + { + ApiUri = apiHost + }; + clientConfiguration = defaultConfig; + _lowLevelApiClient = BuildApiClient(clientConfiguration); + RestClient = _lowLevelApiClient.RestClient; } - protected HttpClient GetHttpClient() + public MorphServerApiClient(ClientConfiguration clientConfiguration) { - if (_httpClient == null) - { - // handler will be disposed automatically - HttpClientHandler aHandler = new HttpClientHandler() - { - ClientCertificateOptions = ClientCertificateOption.Automatic - }; + this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration)); + _lowLevelApiClient = BuildApiClient(clientConfiguration); + RestClient = _lowLevelApiClient.RestClient; + } + - _httpClient = ConstructHttpClient(_apiHost, aHandler); + protected virtual IRestClient ConstructRestApiClient(HttpClient httpClient) + { + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); } - return _httpClient; - } + return new MorphServerRestClient(httpClient); + } - public event EventHandler FileProgress; - protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClientHandler) + protected HttpClient BuildHttpClient(ClientConfiguration config, HttpClientHandler httpClientHandler) { if (httpClientHandler == null) { @@ -74,7 +120,7 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie } var client = new HttpClient(httpClientHandler, true); - client.BaseAddress = new Uri(apiHost, new Uri(_api_v1, UriKind.Relative)); + client.BaseAddress = new Uri(config.ApiUri, new Uri(_api_v1, UriKind.Relative)); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add( @@ -82,8 +128,13 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie { CharSet = "utf-8" }); - client.DefaultRequestHeaders.Add("User-Agent", UserAgent); - client.DefaultRequestHeaders.Add("X-Client-Type", "EMS-CMD"); + 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.DefaultRequestHeaders.Add("Connection", "Keep-Alive"); + client.DefaultRequestHeaders.Add("Keep-Alive", "timeout=120"); client.MaxResponseContentBufferSize = 100 * 1024; client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue @@ -94,497 +145,329 @@ protected HttpClient ConstructHttpClient(Uri apiHost, HttpClientHandler httpClie - client.Timeout = TimeSpan.FromMinutes(15); + client.Timeout = config.HttpClientTimeout; return client; } + - - - - protected static async Task HandleResponse(HttpResponseMessage response) + /// + /// Start Task like "fire and forget" + /// + public Task StartTaskAsync(ApiSession apiSession, StartTaskRequest startTaskRequest, CancellationToken cancellationToken) { - if (response.IsSuccessStatusCode) + if (apiSession == null) { - var content = await response.Content.ReadAsStringAsync(); - var result = JsonSerializationHelper.Deserialize(content); - return result; + throw new ArgumentNullException(nameof(apiSession)); } - else - { - await HandleErrorResponse(response); - return default(T); + if (startTaskRequest == null) + { + throw new ArgumentNullException(nameof(startTaskRequest)); } + return Wrapped(async (token) => + { + var requestDto = new TaskStartRequestDto(); + if (startTaskRequest.TaskParameters != null) + { + requestDto.TaskParameters = startTaskRequest.TaskParameters.Select(TaskParameterMapper.ToDto).ToList(); + } + + if (!startTaskRequest.TaskId.HasValue) + { + 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)); + + }, cancellationToken, OperationType.ShortOperation); } - protected async Task HandleResponse(HttpResponseMessage response) + protected virtual async Task Wrapped(Func> fun, CancellationToken orginalCancellationToken, OperationType operationType) { - if (!response.IsSuccessStatusCode) + if (_disposed) { - await HandleErrorResponse(response); + throw new ObjectDisposedException(nameof(MorphServerApiClient)); } - } - - private static async Task HandleErrorResponse(HttpResponseMessage response) - { + + TimeSpan maxExecutionTime; + switch (operationType) + { + case OperationType.FileTransfer: + maxExecutionTime = clientConfiguration.FileTransferTimeout; break; + case OperationType.ShortOperation: + maxExecutionTime = clientConfiguration.OperationTimeout; break; + case OperationType.SessionOpenAndRelated: + maxExecutionTime = clientConfiguration.SessionOpenTimeout; break; + default: throw new NotImplementedException(); + } - var content = await response.Content.ReadAsStringAsync(); - if (!string.IsNullOrWhiteSpace(content)) + + CancellationTokenSource derTokenSource =null; + try { - ErrorResponse errorResponse = null; - try - { - errorResponse = JsonSerializationHelper.Deserialize(content); - } - catch (Exception) + derTokenSource = CancellationTokenSource.CreateLinkedTokenSource(orginalCancellationToken); { - 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); + derTokenSource.CancelAfter(maxExecutionTime); + try + { + return await fun(derTokenSource.Token); + } - 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); + catch (OperationCanceledException) when (!orginalCancellationToken.IsCancellationRequested && derTokenSource.IsCancellationRequested) + { + if (operationType == OperationType.SessionOpenAndRelated) + { + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({maxExecutionTime})"); + } + else + { + throw new Exception($"Operation timeout ({maxExecutionTime}) when processing command to host {clientConfiguration.ApiUri}"); + } + } } } - - else + finally { - switch (response.StatusCode) + if (derTokenSource != null) { - 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); + if (operationType == OperationType.FileTransfer) + { + RegisterForDisposing(derTokenSource); + } + else + { + derTokenSource.Dispose(); + } } + } + + } + + private ConcurrentBag _ctsForDisposing = new ConcurrentBag(); + private void RegisterForDisposing(CancellationTokenSource derTokenSource) + { + if (derTokenSource == null) + { + throw new ArgumentNullException(nameof(derTokenSource)); } + + _ctsForDisposing.Add(derTokenSource); } - /// - /// Start Task like "fire and forget" - /// - /// api session - /// tast guid - /// - /// - /// - public async Task StartTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken, IEnumerable taskParameters = null) + protected virtual void FailIfError(ApiResult apiResult) { - if (apiSession == null) + if (!apiResult.IsSucceed) { - throw new ArgumentNullException(nameof(apiSession)); + throw apiResult.Error; } + } + + - var spaceName = apiSession.SpaceName; - var url = UrlHelper.JoinUrl("space", spaceName, "runningtasks", taskId.ToString("D"), "payload"); - var dto = new TaskStartRequestDto(); - if (taskParameters != null) + protected virtual TDataModel MapOrFail(ApiResult apiResult, Func maper) + { + if (apiResult.IsSucceed) { - dto.TaskParameters = taskParameters.Select(TaskParameterMapper.ToDto).ToList(); + return maper(apiResult.Data); } - var request = JsonSerializationHelper.SerializeAsStringContent(dto); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, request, apiSession), cancellationToken)) + else { - var info = await HandleResponse(response); - return RunningTaskStatusMapper.RunningTaskStatusFromDto(info); + throw apiResult.Error; } - } + /// - /// Gets status of the task (Running/Not running) and payload + /// Close opened session /// /// api session - /// task guid - /// cancellation token - /// Returns task status - private async Task GetRunningTaskStatusAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + /// + /// + Task ICanCloseSession.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); - 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.AuthLogoutAsync(apiSession, token); + // if task fail - do nothing. server will close this session after inactivity period + return Task.FromResult(0); + + }, cancellationToken, OperationType.ShortOperation); + } /// - /// Gets status of the task + /// Gets status of the task (Running/Not running) and payload /// /// api session /// task guid /// cancellation token /// Returns task status - public async Task GetTaskStatusAsync(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, "tasks", taskId.ToString("D")) + nvc.ToQueryString(); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Get, url, null, apiSession), cancellationToken)) + return Wrapped(async (token) => { - var dto = await HandleResponse(response); - var data = TaskStatusMapper.MapFromDto(dto); - return data; - } + var apiResult = await _lowLevelApiClient.GetRunningTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => RunningTaskStatusMapper.RunningTaskStatusFromDto(dto)); + + }, cancellationToken, OperationType.ShortOperation); + } + /// - /// Stops the Task + /// Gets status of the task /// /// api session - /// + /// task guid /// cancellation token - /// - public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + /// Returns task status + 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, "runningtasks", taskId.ToString("D")); - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Delete, url, null, apiSession), cancellationToken)) - { - await HandleResponse(response); - } - } - - /// - /// Returns server status. May raise exception if server is unreachable - /// - /// - public async 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.GetTaskStatusAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => TaskStatusMapper.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, OperationType.ShortOperation); - } - }, TimeSpan.FromSeconds(20), cancellationToken); } /// - /// Download file from server + /// Change task mode /// /// api session - /// Path to the remote file. Like /some/folder/file.txt - /// stream for writing. You should dispose the stream by yourself + /// task guid + /// /// cancellation token - /// returns file info - public async Task DownloadFileAsync(ApiSession apiSession, string remoteFilePath, Stream streamToWriteTo, CancellationToken cancellationToken) + /// Returns task status + public Task TaskChangeModeAsync(ApiSession apiSession, Guid taskId, TaskChangeModeRequest taskChangeModeRequest, 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) + if (taskChangeModeRequest is null) { - throw new ArgumentNullException(nameof(apiSession)); + throw new ArgumentNullException(nameof(taskChangeModeRequest)); } - 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 + return Wrapped(async (token) => + { + var request = new SpaceTaskChangeModeRequestDto { - // TODO: check - await HandleErrorResponse(response); - - } + TaskEnabled = taskChangeModeRequest.TaskEnabled + }; + var apiResult = await _lowLevelApiClient.TaskChangeModeAsync(apiSession, taskId, request, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + }, cancellationToken, OperationType.ShortOperation); } + /// - /// Uploads file to the server + /// Retrieves space status /// /// 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) + public Task GetSpaceStatusAsync(ApiSession apiSession, CancellationToken cancellationToken) { 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)) + return Wrapped(async (token) => { - await UploadFileAsync(apiSession, fsSource, fileName, fileSize, destFolderPath, cancellationToken, overwriteFileifExists); - return; - } + var apiResult = await _lowLevelApiClient.SpacesGetSpaceStatusAsync(apiSession, apiSession.SpaceName, token); + return MapOrFail(apiResult, (dto) => SpaceStatusMapper.MapFromDto(dto)); + + }, cancellationToken, OperationType.ShortOperation); } + /// - /// Uploads local file to the server folder. + /// Stops the Task /// /// 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) + public async Task StopTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) { 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 Wrapped(async (token) => { - await UploadFileAsync(apiSession, fsSource, fileName, fileSize, destFolderPath, cancellationToken, overwriteFileifExists); - return; - } - - } + var apiResult = await _lowLevelApiClient.StopTaskAsync(apiSession, taskId, token); + FailIfError(apiResult); + return Task.FromResult(0); + }, cancellationToken, OperationType.ShortOperation); - 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 + /// Returns server status. May raise exception if server is unreachable /// - /// 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) + public Task GetServerStatusAsync(CancellationToken cancellationToken) { - if (apiSession == null) - { - throw new ArgumentNullException(nameof(apiSession)); - } - - try + return Wrapped(async (token) => { - var spaceName = apiSession.SpaceName; - string boundary = "EasyMorphCommandClient--------" + Guid.NewGuid().ToString("N"); - string url = UrlHelper.JoinUrl("space", spaceName, "files", destFolderPath); + var apiResult = await _lowLevelApiClient.ServerGetStatusAsync(token); + return MapOrFail(apiResult, (dto) => ServerStatusMapper.MapFromDto(dto)); - 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"); - - } + }, cancellationToken, OperationType.SessionOpenAndRelated); } - private void DownloadProgress_StateChanged(object sender, FileEventArgs e) + public async Task GetSpacesListAsync(CancellationToken cancellationToken) { - if (FileProgress != null) + return await Wrapped(async (token) => { - FileProgress(this, e); - } + var apiResult = await _lowLevelApiClient.SpacesGetListAsync(token); + return MapOrFail(apiResult, (dto) => SpacesEnumerationMapper.MapFromDto(dto)); + }, cancellationToken, OperationType.SessionOpenAndRelated); } - - protected async Task GetDataWithCancelAfter(Func> action, TimeSpan timeout, CancellationToken cancellationToken) + private void DownloadProgress_StateChanged(object sender, FileTransferProgressEventArgs e) { - 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})"); - } - } + OnDataDownloadProgress?.Invoke(this, e); } - 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); - } + /// @@ -594,26 +477,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 SpaceBrowseAsync(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, OperationType.ShortOperation); } + /// /// Checks if file exists /// @@ -622,24 +501,26 @@ 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 SpaceFileExistsAsync(ApiSession apiSession, string serverFilePath, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - if (string.IsNullOrWhiteSpace(fileName)) - throw new ArgumentException(nameof(fileName)); - var browseResult = await this.BrowseSpaceAsync(apiSession, serverFolder, cancellationToken); + if (string.IsNullOrWhiteSpace(serverFilePath)) + { + throw new ArgumentException(nameof(serverFilePath)); + } - return browseResult.FileExists(fileName); + return Wrapped(async (token) => + { + var apiResult = await _lowLevelApiClient.WebFileExistsAsync(apiSession, serverFilePath, token); + return MapOrFail(apiResult, (dto) => dto); + }, cancellationToken, OperationType.ShortOperation); } - - - /// /// Performs file deletion /// @@ -648,48 +529,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 SpaceDeleteFileAsync(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", 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, serverFilePath, token); + FailIfError(apiResult); + return Task.FromResult(0); + + }, cancellationToken, OperationType.ShortOperation); } - /// - /// 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; - } - } /// @@ -699,7 +558,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) { @@ -707,102 +566,25 @@ 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; - - } - } - - 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 - }; + var request = new ValidateTasksRequestDto + { + SpaceName = apiSession.SpaceName, + ProjectPath = projectPath + }; + var apiResult = await _lowLevelApiClient.ValidateTasksAsync(apiSession, request, token); + return MapOrFail(apiResult, (dto) => ValidateTasksResponseMapper.MapFromDto(dto)); - 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 - }; + }, cancellationToken, OperationType.ShortOperation); - 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 - }; - } - } - /// /// Opens session based on required authentication mechanism @@ -823,161 +605,174 @@ 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 { - 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."); - } + var session = await MorphServerAuthenticator.OpenSessionMultiplexedAsync(desiredSpace, + new OpenSessionAuthenticatorContext( + _lowLevelApiClient, + this as ICanCloseSession, + (handler) => ConstructRestApiClient(BuildHttpClient(clientConfiguration, handler))), + openSessionRequest, cancellationToken); + + return session; } catch (OperationCanceledException) when (!ct.IsCancellationRequested && linkedTokenSource.IsCancellationRequested) { - throw new Exception($"Can't connect to host {_apiHost}. Operation timeout ({timeout})"); + throw new Exception($"Can't connect to host {clientConfiguration.ApiUri}. Operation timeout ({timeout})"); } } } - /// - /// 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); + 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)); - var token = await internalAuthLoginAsync(clientNonce, serverNonce, spaceName, allHash, cancellationToken); + }, cancellationToken, OperationType.ShortOperation); - return new ApiSession(this) + } + + public Task GetTaskAsync(ApiSession apiSession, Guid taskId, CancellationToken cancellationToken) + { + return Wrapped(async (token) => { - AuthToken = token, - IsAnonymous = false, - IsClosed = false, - SpaceName = spaceName - }; + var apiResult = await _lowLevelApiClient.GetTaskAsync(apiSession, taskId, token); + return MapOrFail(apiResult, (dto) => SpaceTaskMapper.MapFull(dto)); + + }, cancellationToken, OperationType.ShortOperation); } - /// - /// Close opened session - /// - /// api session - /// - /// - public async Task CloseSessionAsync(ApiSession apiSession, CancellationToken cancellationToken) + + public Task SpaceOpenStreamingDataAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) { if (apiSession == null) { throw new ArgumentNullException(nameof(apiSession)); } - if (apiSession.IsClosed) - return; - if (apiSession.IsAnonymous) - return; + return Wrapped(async (token) => + { + Action onReceiveProgress = (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) + ); - var url = "auth/logout"; + }, cancellationToken, OperationType.FileTransfer); + } - using (var response = await GetHttpClient().SendAsync(BuildHttpRequestMessage(HttpMethod.Post, url, null, apiSession), cancellationToken)) + public void Dispose() + { + lock (_lock) { + if (_disposed) + return; + if (_lowLevelApiClient != null) + _lowLevelApiClient.Dispose(); + + Array.ForEach(_ctsForDisposing.ToArray(), z => z.Dispose()); + _disposed = true; + } + } - await HandleResponse(response); - + public Task SpaceOpenDataStreamAsync(ApiSession apiSession, string remoteFilePath, CancellationToken cancellationToken) + { + if (apiSession == null) + { + throw new ArgumentNullException(nameof(apiSession)); } + return Wrapped(async (token) => + { + Action onReceiveProgress = (data) => + { + OnDataDownloadProgress?.Invoke(this, data); + }; + var apiResult = await _lowLevelApiClient.WebFilesDownloadFileAsync(apiSession, remoteFilePath, onReceiveProgress, token); + return MapOrFail(apiResult, (data) => data.Stream); + }, cancellationToken, OperationType.FileTransfer); } - public async Task GetTasksListAsync(ApiSession apiSession, CancellationToken cancellationToken) + public Task SpaceUploadContiniousStreamingAsync(ApiSession apiSession, SpaceUploadContiniousStreamRequest continiousStreamRequest, 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)) + if (apiSession == null) { - var dto = await HandleResponse(response); - return SpaceTasksListsMapper.MapFromDto(dto); + throw new ArgumentNullException(nameof(apiSession)); } - } - 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)) + if (continiousStreamRequest == null) { - var dto = await HandleResponse(response); - return SpaceTaskMapper.MapFull(dto); + throw new ArgumentNullException(nameof(continiousStreamRequest)); } + + return Wrapped(async (token) => + { + 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 void Dispose() + public Task SpaceUploadDataStreamAsync(ApiSession apiSession, SpaceUploadDataStreamRequest spaceUploadFileRequest, CancellationToken cancellationToken) { - if (_httpClient != null) + if (apiSession == null) { - _httpClient.Dispose(); - _httpClient = null; + throw new ArgumentNullException(nameof(apiSession)); + } + + if (spaceUploadFileRequest == null) + { + throw new ArgumentNullException(nameof(spaceUploadFileRequest)); } + + return Wrapped(async (token) => + { + Action onSendProgress = (data) => + { + OnDataUploadProgress?.Invoke(this, data); + }; + var sendStreamData = new SendFileStreamData( + spaceUploadFileRequest.DataStream, + spaceUploadFileRequest.FileName, + spaceUploadFileRequest.FileSize); + var apiResult = + spaceUploadFileRequest.OverwriteExistingFile ? + await _lowLevelApiClient.WebFilesPutFileStreamAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, token) : + await _lowLevelApiClient.WebFilesPostFileStreamAsync(apiSession, spaceUploadFileRequest.ServerFolder, sendStreamData, onSendProgress, 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..bb2d180 --- /dev/null +++ b/src/Client/MorphServerApiClientGlobalConfig.cs @@ -0,0 +1,74 @@ +using System; +using System.Net.Http; +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 + 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"; + + public static TimeSpan SessionOpenTimeout { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Default operation execution timeout + /// + public static TimeSpan OperationTimeout { get; set; } = TimeSpan.FromMinutes(2); + /// + /// 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; } + + /// + /// dispose client when session is closed + /// + public static bool AutoDisposeClientOnSessionClose { get; set; } = true; + + 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 new file mode 100644 index 0000000..77e31eb --- /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 Morph.Server.Sdk.Model.InternalModels; +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(context.MorphServerApiClient, 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(context.MorphServerApiClient, 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.Config.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(IRestClient 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(IRestClient 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 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 = passwordSha256 + serverNonce + clientNonce; + var composedHash = CryptographyHelper.CalculateSha256HEX(all); + + + var requestDto = new LoginRequestDto + { + ClientSeed = clientNonce, + Password = composedHash, + 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 new file mode 100644 index 0000000..fb9be12 --- /dev/null +++ b/src/Client/MorphServerRestClient.cs @@ -0,0 +1,426 @@ +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; +using System.IO; +using Morph.Server.Sdk.Model; +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 +{ + + + public class MorphServerRestClient : IRestClient + { + private HttpClient httpClient; + public HttpClient HttpClient { get => httpClient; set => httpClient = value; } + + 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) + { + 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) + 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) + { + var serialized = JsonSerializationHelper.Serialize(model); + stringContent = new StringContent(serialized, Encoding.UTF8, "application/json"); + } + + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); + var httpRequestMessage = BuildHttpRequestMessage(httpMethod, url, stringContent, headersCollection); + + // for model binding request read and buffer full server response + // but for HttpHead content reading is not necessary and might raise error. + //var httpCompletionOption = httpMethod != HttpMethod.Head ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead; + var httpCompletionOption = HttpCompletionOption.ResponseHeadersRead; + using (var response = await httpClient.SendAsync(httpRequestMessage, httpCompletionOption, + cancellationToken)) + { + return await HandleResponse(response); + } + } + + private async Task> HandleResponse(HttpResponseMessage response) + where TResult : new() + { + 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 async Task BuildExceptionFromResponse(HttpResponseMessage response) + { + + var rawContent = await response.Content.ReadAsStringAsync(); + if (!string.IsNullOrWhiteSpace(rawContent)) + { + ErrorResponse errorResponse = null; + try + { + errorResponse = DeserializeErrorResponse(rawContent); + } + catch (Exception) + { + 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", rawContent); + + 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 BuildCustomExceptionFromErrorResponse(rawContent, errorResponse); + } + } + + 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); + } + + } + } + + 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) + { + HttpClient.Dispose(); + HttpClient = null; + } + } + + + + + public Task> PushContiniousStreamingDataAsync( + HttpMethod httpMethod, string path, ContiniousStreamingRequest startContiniousStreamingRequest, NameValueCollection urlParameters, HeadersCollection headersCollection, + CancellationToken cancellationToken) + where TResult : new() + { + try + { + string boundary = "MorphRestClient-Streaming--------" + Guid.NewGuid().ToString("N"); + + var content = new MultipartFormDataContent(boundary); + + + 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 () => + { + try + { + try + { + 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)); + } + + requestMessage.Dispose(); + streamContent.Dispose(); + content.Dispose(); + } + catch (Exception) + { + // dd + } + + }).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, + Action onSendProgress, + CancellationToken cancellationToken) + where TResult : new() + { + try + { + string boundary = "MorphRestClient--------" + Guid.NewGuid().ToString("N"); + + 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)) + { + content.Add(streamContent, "files", Path.GetFileName(sendFileStreamData.FileName)); + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); + 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); + } + } + + protected async Task> RetrieveFileStreamAsync(HttpMethod httpMethod, string path, NameValueCollection urlParameters, HeadersCollection headersCollection, Action onReceiveProgress, CancellationToken cancellationToken) + { + var url = path + (urlParameters != null ? urlParameters.ToQueryString() : string.Empty); + HttpResponseMessage response = await httpClient.SendAsync( + BuildHttpRequestMessage(httpMethod, url, null, headersCollection), HttpCompletionOption.ResponseHeadersRead, cancellationToken); + { + if (response.IsSuccessStatusCode) + { + var contentDisposition = response.Content.Headers.ContentDisposition; + // 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; + if (!contentLength.HasValue) + { + throw new Exception("Response content length header is not set by the server."); + } + + FileProgress downloadProgress = null; + + if (contentLength.HasValue) + { + downloadProgress = new FileProgress(realFileName, contentLength.Value, onReceiveProgress); + } + downloadProgress?.ChangeState(FileProgressState.Starting); + long totalProcessedBytes = 0; + + { + // stream must be disposed by a caller + Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); + + + var streamWithProgress = new StreamWithProgress(streamToReadFrom, contentLength.Value, cancellationToken, + e => + { + // on read progress handler + if (downloadProgress != null) + { + totalProcessedBytes = e.TotalBytesRead; + downloadProgress.SetProcessedBytes(totalProcessedBytes); + } + }, + () => + { + // on disposed handler + if (downloadProgress != null && downloadProgress.ProcessedBytes != totalProcessedBytes) + { + downloadProgress.ChangeState(FileProgressState.Cancelled); + } + response.Dispose(); + }, + (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)); + + } + } + else + { + try + { + var error = await BuildExceptionFromResponse(response); + return ApiResult.Fail(error); + } + finally + { + response.Dispose(); + } + } + } + } + + + 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) + { + urlParameters = new NameValueCollection(); + } + urlParameters.Add("_", DateTime.Now.Ticks.ToString()); + return SendAsyncApiResult(HttpMethod.Head, url, null, urlParameters, headersCollection, cancellationToken); + } + } + + + +} \ No newline at end of file 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/Dto/NoContentRequest.cs b/src/Dto/NoContentRequest.cs new file mode 100644 index 0000000..58a6e48 --- /dev/null +++ b/src/Dto/NoContentRequest.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Morph.Server.Sdk.Dto +{ + [DataContractAttribute] + public 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..86cc3fb --- /dev/null +++ b/src/Dto/NoContentResult.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; + +namespace Morph.Server.Sdk.Dto +{ + + public sealed class NoContentResult + { + + } + + + +} \ No newline at end of file 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/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/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/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/ApiSessionExtension.cs b/src/Helper/ApiSessionExtension.cs new file mode 100644 index 0000000..19aea2f --- /dev/null +++ b/src/Helper/ApiSessionExtension.cs @@ -0,0 +1,23 @@ +using Morph.Server.Sdk.Model; +using Morph.Server.Sdk.Model.InternalModels; + +namespace Morph.Server.Sdk.Helper +{ + internal 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/Helper/ContiniousSteamingContent.cs b/src/Helper/ContiniousSteamingContent.cs new file mode 100644 index 0000000..222ebb4 --- /dev/null +++ b/src/Helper/ContiniousSteamingContent.cs @@ -0,0 +1,136 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +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 connection + /// + CloseConnection + } + + private const int DefBufferSize = 4096; + private readonly CancellationToken mainCancellation; + + + /// + /// 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; + /// + /// Message to process + /// + volatile MessageType _currentMessage = MessageType.None; + private 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._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); + + } + + internal void CloseConnetion() + { + // 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) + { + this.mainCancellation = mainCancellation; + + } + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + var buffer = new byte[DefBufferSize]; + bool canLoop = true; + while (canLoop) + { + + // await new data + await hasData.WaitAsync(Timeout.Infinite, mainCancellation); + // data has been arrived. check _currentMessage field + switch (this._currentMessage) { + // upload stream + 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); + } + + // set that data has been processed + dataProcessed.Release(1); + + }; break; + case MessageType.CloseConnection: + // close loop. dataProcessed flag will be triggered at the end of this function. + canLoop = false; + + break; + + } + this._currentMessage = MessageType.None; + + } + dataProcessed.Release(1); + + } + + protected override bool TryComputeLength(out long length) + { + // continuous stream length is unknown + length = 0; + return false; + } + } +} diff --git a/src/Helper/FileProgress.cs b/src/Helper/FileProgress.cs index 7b6f596..096d5f6 100644 --- a/src/Helper/FileProgress.cs +++ b/src/Helper/FileProgress.cs @@ -11,22 +11,22 @@ 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; } - 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 + onProgress?.Invoke(new FileTransferProgressEventArgs { ProcessedBytes = ProcessedBytes, State = state, - Guid = _guid, FileName = FileName, FileSize = FileSize @@ -35,13 +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, Guid? guid = null) + public FileProgress(string fileName, long fileSize, Action onProgress) { FileName = fileName; FileSize = fileSize; - _guid = guid; + this.onProgress = onProgress; } } } diff --git a/src/Helper/IFileProgress.cs b/src/Helper/IFileProgress.cs index f4155ee..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 d2ab970..93fb2fe 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; @@ -11,12 +12,18 @@ namespace Morph.Server.Sdk.Helper { - internal static class JsonSerializationHelper + public 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)) @@ -43,11 +50,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/Helper/ProgressStreamContent.cs b/src/Helper/ProgressStreamContent.cs index df3782b..283f91d 100644 --- a/src/Helper/ProgressStreamContent.cs +++ b/src/Helper/ProgressStreamContent.cs @@ -6,10 +6,12 @@ using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Morph.Server.Sdk.Helper { + internal class ProgressStreamContent : HttpContent { private const int DefBufferSize = 4096; @@ -32,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; @@ -48,10 +52,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/Helper/StreamProgressEventArgs.cs b/src/Helper/StreamProgressEventArgs.cs new file mode 100644 index 0000000..58b3da1 --- /dev/null +++ b/src/Helper/StreamProgressEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Morph.Server.Sdk.Helper +{ + internal class StreamProgressEventArgs : EventArgs + { + public long TotalBytesRead { get; } + public int BytesRead { get; } + + public StreamProgressEventArgs() + { + + } + public StreamProgressEventArgs(long totalBytesRead, int bytesRead) :this() + { + TotalBytesRead = totalBytesRead; + BytesRead = bytesRead; + } + } +} diff --git a/src/Helper/StreamWithProgress.cs b/src/Helper/StreamWithProgress.cs new file mode 100644 index 0000000..369e587 --- /dev/null +++ b/src/Helper/StreamWithProgress.cs @@ -0,0 +1,238 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +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 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 onDisposed = 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; + } + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => streamLength; + + public override long Position { get => _readPosition; set => throw new NotImplementedException(); } + + public override void Flush() + { + stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (httpTimeoutToken.IsCancellationRequested) + { + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); + } + var bytesRead = stream.Read(buffer, offset, count); + + IncrementReadProgress(bytesRead); + return bytesRead; + } + + private void IncrementReadProgress(int bytesRead) + { + _readPosition += bytesRead; + var totalBytesRead = _readPosition; + + if (onReadProgress != null) + { + 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); + //} + } + + 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 async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + byte[] buffer = new byte[bufferSize]; + int bytesRead; + while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + if (httpTimeoutToken.IsCancellationRequested) + { + if (cancellationToken.IsCancellationRequested) + { + onTokenCancelled(TokenCancellationReason.OperationCancellationToken, cancellationToken); + } + else + { + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); + } + } + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + } + } + 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) + { + if (httpTimeoutToken.IsCancellationRequested) + { + if (cancellationToken.IsCancellationRequested) + { + onTokenCancelled(TokenCancellationReason.OperationCancellationToken, cancellationToken); + } + else + { + onTokenCancelled(TokenCancellationReason.HttpTimeoutToken, httpTimeoutToken); + } + } + var bytesRead = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + IncrementReadProgress(bytesRead); + return bytesRead; + } + public override int ReadByte() + { + var @byte = stream.ReadByte(); + if (@byte != -1) + { + IncrementReadProgress(1); + } + return @byte; + } + 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) + { + if (!_disposed) + { + _disposed = true; + + stream.Dispose(); + if (onDisposed != null) + { + onDisposed(); + } + } + } + } + + + } +} diff --git a/src/Client/UrlHelper.cs b/src/Helper/UrlHelper.cs similarity index 88% rename from src/Client/UrlHelper.cs rename to src/Helper/UrlHelper.cs index 452f0fc..b6a173c 100644 --- a/src/Client/UrlHelper.cs +++ b/src/Helper/UrlHelper.cs @@ -1,11 +1,11 @@ using System; using System.Linq; -namespace Morph.Server.Sdk.Client +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/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/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/ApiSession.cs b/src/Model/ApiSession.cs index 2843671..23afa87 100644 --- a/src/Model/ApiSession.cs +++ b/src/Model/ApiSession.cs @@ -1,47 +1,62 @@ 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 { - - + /// + /// Disposable api session + /// 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 string SpaceName { get => - string.IsNullOrWhiteSpace(_spaceName) ? _defaultSpaceName : _spaceName.ToLower(); - internal set => _spaceName = value; } - internal string AuthToken { get; set; } - internal bool IsAnonymous { get; set; } + public bool IsClosed { get; internal set; } + public string SpaceName + { + get => +string.IsNullOrWhiteSpace(_spaceName) ? _defaultSpaceName : _spaceName.ToLower(); + internal set => _spaceName = value; + } + public string AuthToken { get; internal set; } + public bool IsAnonymous { get; internal set; } - MorphServerApiClient _client; + ICanCloseSession _client; private string _spaceName; + private SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - internal ApiSession(MorphServerApiClient client) + /// + /// 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, @@ -51,24 +66,84 @@ internal static ApiSession Anonymous(string spaceName) } + public async Task CloseSessionAsync(CancellationToken cancellationToken) + { - public void Dispose() + await _lock.WaitAsync(cancellationToken); + try + { + await _InternalCloseSessionAsync(cancellationToken); + } + finally + { + _lock.Release(); + } + } + + private async Task _InternalCloseSessionAsync(CancellationToken cancellationToken) { + // don't close if session is already closed or anon. + if(IsClosed || _client == null || IsAnonymous) + { + return; + } try { - if (!IsClosed && _client!=null) + + using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - Task.Run(async () => + 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) { - var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5)); - await _client.CloseSessionAsync(this, cts.Token); _client.Dispose(); - _client = null; } + _client = null; - ).Wait(TimeSpan.FromSeconds(5)); + IsClosed = true; + } + } + catch (Exception) + { + // + } + } - this.IsClosed = true; + public void Dispose() + { + try + { + if (_lock != null) + { + _lock.Wait(5000); + try + { + if (!IsClosed && _client != null) + { + Task.Run(async () => + { + try + { + await _InternalCloseSessionAsync(CancellationToken.None); + } + catch (Exception ex) + { + + } + }).Wait(TimeSpan.FromSeconds(5)); + + + } + + } + finally + { + _lock.Release(); + _lock.Dispose(); + _lock = null; + } } } catch (Exception) @@ -77,4 +152,4 @@ public void Dispose() } } } -} +} \ No newline at end of file diff --git a/src/Model/ClientConfiguration.cs b/src/Model/ClientConfiguration.cs new file mode 100644 index 0000000..79c117b --- /dev/null +++ b/src/Model/ClientConfiguration.cs @@ -0,0 +1,33 @@ +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 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 + public Func ServerCertificateCustomValidationCallback { get; set; } + = MorphServerApiClientGlobalConfig.ServerCertificateCustomValidationCallback; +#endif + + } + +} + + 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/DownloadFileInfo.cs b/src/Model/DownloadFileInfo.cs deleted file mode 100644 index 7f47ec4..0000000 --- a/src/Model/DownloadFileInfo.cs +++ /dev/null @@ -1,17 +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; } - - } -} diff --git a/src/Model/IClientConfiguration.cs b/src/Model/IClientConfiguration.cs new file mode 100644 index 0000000..e47563d --- /dev/null +++ b/src/Model/IClientConfiguration.cs @@ -0,0 +1,27 @@ +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; } + TimeSpan SessionOpenTimeout { get; } + + string ClientId { get; } + string ClientType { get; } + + bool AutoDisposeClientOnSessionClose { get; } + + Uri ApiUri { get; } +#if NETSTANDARD2_0 + Func ServerCertificateCustomValidationCallback { get; } +#endif + } +} + + diff --git a/src/Model/InternalModels/ApiResult.cs b/src/Model/InternalModels/ApiResult.cs new file mode 100644 index 0000000..1f9e0a9 --- /dev/null +++ b/src/Model/InternalModels/ApiResult.cs @@ -0,0 +1,44 @@ +using System; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + /// + /// Represents api result of DTO Model or Error (Exception) + /// + /// + 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 void ThrowIfFailed() + { + if (!IsSucceed && Error != null) + { + throw Error; + } + } + } + + +} + + diff --git a/src/Model/InternalModels/ContiniousStreamingRequest.cs b/src/Model/InternalModels/ContiniousStreamingRequest.cs new file mode 100644 index 0000000..94496a3 --- /dev/null +++ b/src/Model/InternalModels/ContiniousStreamingRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + public 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/InternalModels/FetchFileStreamData.cs b/src/Model/InternalModels/FetchFileStreamData.cs new file mode 100644 index 0000000..379dbca --- /dev/null +++ b/src/Model/InternalModels/FetchFileStreamData.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + 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; } + + } + + + +} \ No newline at end of file diff --git a/src/Model/InternalModels/HeadersCollection.cs b/src/Model/InternalModels/HeadersCollection.cs new file mode 100644 index 0000000..8bdd50a --- /dev/null +++ b/src/Model/InternalModels/HeadersCollection.cs @@ -0,0 +1,48 @@ +using System; +using System.Net.Http.Headers; +using System.Collections.Generic; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + 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/Model/InternalModels/OpenSessionAuthenticatorContext.cs b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs new file mode 100644 index 0000000..a4da307 --- /dev/null +++ b/src/Model/InternalModels/OpenSessionAuthenticatorContext.cs @@ -0,0 +1,32 @@ +using Morph.Server.Sdk.Client; +using System; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + internal class OpenSessionAuthenticatorContext + { + + public OpenSessionAuthenticatorContext + (ILowLevelApiClient lowLevelApiClient, + ICanCloseSession 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 ICanCloseSession MorphServerApiClient { 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..d8afa32 --- /dev/null +++ b/src/Model/InternalModels/OperationType.cs @@ -0,0 +1,11 @@ +namespace Morph.Server.Sdk.Model.InternalModels +{ + public enum OperationType + { + ShortOperation = 1, + FileTransfer = 2, + SessionOpenAndRelated = 3 + } +} + + diff --git a/src/Model/InternalModels/SendFileStreamData.cs b/src/Model/InternalModels/SendFileStreamData.cs new file mode 100644 index 0000000..c99587f --- /dev/null +++ b/src/Model/InternalModels/SendFileStreamData.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + 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; } + } + + + + + +} \ No newline at end of file diff --git a/src/Model/InternalModels/ServerPushStreaming.cs b/src/Model/InternalModels/ServerPushStreaming.cs new file mode 100644 index 0000000..ae0e788 --- /dev/null +++ b/src/Model/InternalModels/ServerPushStreaming.cs @@ -0,0 +1,93 @@ +using Morph.Server.Sdk.Helper; +using Morph.Server.Sdk.Model.InternalModels; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Morph.Server.Sdk.Model.InternalModels +{ + public class ServerPushStreaming : IDisposable + { + internal readonly ContiniousSteamingHttpContent steamingContent; + + private bool _closed = false; + volatile SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(0, 1); + + internal ServerPushStreaming(ContiniousSteamingHttpContent steamingContent) + { + this.steamingContent = steamingContent; + } + public void Dispose() + { + Close(); + } + + private void Close() + { + if (_closed) + return; + try + { + + this.steamingContent.CloseConnetion(); + + + SemaphoreSlim.Wait(10000); + if (DataException != null) + { + throw DataException; + } + + } + finally + { + SemaphoreSlim.Dispose(); + SemaphoreSlim = null; + _closed = true; + } + } + + public async Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken) + { + await steamingContent.WriteStreamAsync(stream, cancellationToken); + } + + public Exception DataException { get; private set; } + + private object dataResult = null; + + public TResult CloseAndGetData() + { + Close(); + + if (dataResult is TResult f) + { + return f; + } + else + { + return default(TResult); + } + } + + internal void SetApiResult(ApiResult apiResult) where TResult : new() + { + if (apiResult.IsSucceed) + { + dataResult = apiResult.Data; + + } + else + { + DataException = apiResult.Error; + } + + SemaphoreSlim.Release(); + } + } + + + + +} 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/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/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..3c7ac05 100644 --- a/src/Model/SpaceStatus.cs +++ b/src/Model/SpaceStatus.cs @@ -10,23 +10,6 @@ public class SpaceStatus { public string SpaceName { get; internal set; } public bool IsPublic { get; internal set; } - public IReadOnlyList SpacePermissions { get; internal set; } - } - - - public enum SpacePermission - { - TasksList, - TaskLogView, - TaskLogDeletion, - TaskCreate, - TaskModify, - TaskExecution, - TaskDeletion, - - FilesList, - FileUpload, - FileDownload, - FileDeletion, + public IReadOnlyList UserPermissions { get; internal set; } } } 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/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 diff --git a/src/Model/StartTaskRequest.cs b/src/Model/StartTaskRequest.cs new file mode 100644 index 0000000..a4157f7 --- /dev/null +++ b/src/Model/StartTaskRequest.cs @@ -0,0 +1,20 @@ +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; + } + public StartTaskRequest() + { + + } + } +} \ No newline at end of file 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/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 b39354d..588e79e 100644 --- a/src/Morph.Server.Sdk.csproj +++ b/src/Morph.Server.Sdk.csproj @@ -1,123 +1,36 @@ - - - + - 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 + netstandard2.0;net45 + false true - - Morph.Server.Sdk.snk - - - - False - - + + Morph.Server.Sdk.xml + + + Morph.Server.Sdk.xml + + + Morph.Server.Sdk.xml + + + Morph.Server.Sdk.xml + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - + + - \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index f101fa3..8f050e0 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.4.1.1")] +[assembly: AssemblyFileVersion("1.4.1.1")]