From fc64db14bf7954a8f25786954c001308811e9195 Mon Sep 17 00:00:00 2001 From: "mika@stocksharp.com" Date: Wed, 20 Nov 2024 00:31:28 +0300 Subject: [PATCH] YandexDisk.Client fork --- Backup.Yandex/Backup.Yandex.csproj | 4 +- Ecng.sln | 6 + YandexDisk.Client/AboutInfo.cs | 46 +++ .../Clients/CommandsClientExtensions.cs | 152 ++++++++ .../Clients/FilesClientExtension.cs | 113 ++++++ YandexDisk.Client/Clients/ICommandsClient.cs | 58 +++ YandexDisk.Client/Clients/IFilesClient.cs | 44 +++ YandexDisk.Client/Clients/IMetaInfoClient.cs | 56 +++ YandexDisk.Client/Http/ApiContext.cs | 13 + .../Http/Clients/CommandsClient.cs | 66 ++++ YandexDisk.Client/Http/Clients/FilesClient.cs | 53 +++ .../Http/Clients/MetaInfoClient.cs | 56 +++ YandexDisk.Client/Http/DiskClientBase.cs | 299 ++++++++++++++++ YandexDisk.Client/Http/DiskHttpApi.cs | 113 ++++++ YandexDisk.Client/Http/IHttpClient.cs | 20 ++ YandexDisk.Client/Http/ILogSaver.cs | 78 ++++ YandexDisk.Client/Http/Logger.cs | 119 +++++++ .../Http/RealHttpClientWrapper.cs | 27 ++ .../Http/Serialization/DateTimeSerializer.cs | 15 + .../Http/Serialization/PropertyResolver.cs | 85 +++++ .../Serialization/QueryParamsSerializer.cs | 337 ++++++++++++++++++ .../Serialization/SnakeCaseEnumConverter.cs | 73 ++++ .../SnakeCasePropertyNamesContractResolver.cs | 12 + .../Http/Serialization/ValueSerializer.cs | 28 ++ YandexDisk.Client/IDiskApi.cs | 31 ++ YandexDisk.Client/Protocol/CopyFileRequest.cs | 29 ++ .../Protocol/DeleteFileRequest.cs | 23 ++ YandexDisk.Client/Protocol/Disk.cs | 45 +++ .../Protocol/ErrorDescription.cs | 18 + .../Protocol/FilesResourceList.cs | 25 ++ .../Protocol/FilesResourceRequest.cs | 26 ++ .../Protocol/LastUploadedResourceList.cs | 20 ++ .../Protocol/LastUploadedResourceRequest.cs | 20 ++ YandexDisk.Client/Protocol/Link.cs | 25 ++ YandexDisk.Client/Protocol/MediaType.cs | 103 ++++++ YandexDisk.Client/Protocol/MoveFileRequest.cs | 29 ++ YandexDisk.Client/Protocol/Operation.cs | 35 ++ .../Protocol/ProtocolObjectResponse.cs | 15 + .../Protocol/PublicResourcesList.cs | 30 ++ YandexDisk.Client/Protocol/Resource.cs | 107 ++++++ YandexDisk.Client/Protocol/ResourceList.cs | 47 +++ YandexDisk.Client/Protocol/ResourceRequest.cs | 37 ++ .../Protocol/RestoreFromTrashRequest.cs | 29 ++ YandexDisk.Client/README.md | 3 + YandexDisk.Client/YandexApiException.cs | 62 ++++ YandexDisk.Client/YandexDisk.Client.csproj | 12 + 46 files changed, 2641 insertions(+), 3 deletions(-) create mode 100644 YandexDisk.Client/AboutInfo.cs create mode 100644 YandexDisk.Client/Clients/CommandsClientExtensions.cs create mode 100644 YandexDisk.Client/Clients/FilesClientExtension.cs create mode 100644 YandexDisk.Client/Clients/ICommandsClient.cs create mode 100644 YandexDisk.Client/Clients/IFilesClient.cs create mode 100644 YandexDisk.Client/Clients/IMetaInfoClient.cs create mode 100644 YandexDisk.Client/Http/ApiContext.cs create mode 100644 YandexDisk.Client/Http/Clients/CommandsClient.cs create mode 100644 YandexDisk.Client/Http/Clients/FilesClient.cs create mode 100644 YandexDisk.Client/Http/Clients/MetaInfoClient.cs create mode 100644 YandexDisk.Client/Http/DiskClientBase.cs create mode 100644 YandexDisk.Client/Http/DiskHttpApi.cs create mode 100644 YandexDisk.Client/Http/IHttpClient.cs create mode 100644 YandexDisk.Client/Http/ILogSaver.cs create mode 100644 YandexDisk.Client/Http/Logger.cs create mode 100644 YandexDisk.Client/Http/RealHttpClientWrapper.cs create mode 100644 YandexDisk.Client/Http/Serialization/DateTimeSerializer.cs create mode 100644 YandexDisk.Client/Http/Serialization/PropertyResolver.cs create mode 100644 YandexDisk.Client/Http/Serialization/QueryParamsSerializer.cs create mode 100644 YandexDisk.Client/Http/Serialization/SnakeCaseEnumConverter.cs create mode 100644 YandexDisk.Client/Http/Serialization/SnakeCasePropertyNamesContractResolver.cs create mode 100644 YandexDisk.Client/Http/Serialization/ValueSerializer.cs create mode 100644 YandexDisk.Client/IDiskApi.cs create mode 100644 YandexDisk.Client/Protocol/CopyFileRequest.cs create mode 100644 YandexDisk.Client/Protocol/DeleteFileRequest.cs create mode 100644 YandexDisk.Client/Protocol/Disk.cs create mode 100644 YandexDisk.Client/Protocol/ErrorDescription.cs create mode 100644 YandexDisk.Client/Protocol/FilesResourceList.cs create mode 100644 YandexDisk.Client/Protocol/FilesResourceRequest.cs create mode 100644 YandexDisk.Client/Protocol/LastUploadedResourceList.cs create mode 100644 YandexDisk.Client/Protocol/LastUploadedResourceRequest.cs create mode 100644 YandexDisk.Client/Protocol/Link.cs create mode 100644 YandexDisk.Client/Protocol/MediaType.cs create mode 100644 YandexDisk.Client/Protocol/MoveFileRequest.cs create mode 100644 YandexDisk.Client/Protocol/Operation.cs create mode 100644 YandexDisk.Client/Protocol/ProtocolObjectResponse.cs create mode 100644 YandexDisk.Client/Protocol/PublicResourcesList.cs create mode 100644 YandexDisk.Client/Protocol/Resource.cs create mode 100644 YandexDisk.Client/Protocol/ResourceList.cs create mode 100644 YandexDisk.Client/Protocol/ResourceRequest.cs create mode 100644 YandexDisk.Client/Protocol/RestoreFromTrashRequest.cs create mode 100644 YandexDisk.Client/README.md create mode 100644 YandexDisk.Client/YandexApiException.cs create mode 100644 YandexDisk.Client/YandexDisk.Client.csproj diff --git a/Backup.Yandex/Backup.Yandex.csproj b/Backup.Yandex/Backup.Yandex.csproj index 09fe242d..4a04eb4f 100644 --- a/Backup.Yandex/Backup.Yandex.csproj +++ b/Backup.Yandex/Backup.Yandex.csproj @@ -1,10 +1,8 @@  - - - + \ No newline at end of file diff --git a/Ecng.sln b/Ecng.sln index ae076e72..c39c05b3 100644 --- a/Ecng.sln +++ b/Ecng.sln @@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MathLight", "MathLight\Math EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backup.Mega", "Backup.Mega\Backup.Mega.csproj", "{6202135C-F945-4047-828B-284E1E00BBEA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YandexDisk.Client", "YandexDisk.Client\YandexDisk.Client.csproj", "{5BA2E8AB-3569-4780-BDC6-7920B93A770D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,10 @@ Global {6202135C-F945-4047-828B-284E1E00BBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6202135C-F945-4047-828B-284E1E00BBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6202135C-F945-4047-828B-284E1E00BBEA}.Release|Any CPU.Build.0 = Release|Any CPU + {5BA2E8AB-3569-4780-BDC6-7920B93A770D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BA2E8AB-3569-4780-BDC6-7920B93A770D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BA2E8AB-3569-4780-BDC6-7920B93A770D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BA2E8AB-3569-4780-BDC6-7920B93A770D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/YandexDisk.Client/AboutInfo.cs b/YandexDisk.Client/AboutInfo.cs new file mode 100644 index 00000000..e63ea822 --- /dev/null +++ b/YandexDisk.Client/AboutInfo.cs @@ -0,0 +1,46 @@ +using System.Linq; +using System.Reflection; +using JetBrains.Annotations; + +namespace YandexDisk.Client +{ + /// + /// Class provided assymbly and file description + /// + [PublicAPI] + public class AboutInfo + { + private readonly Assembly _assembly; + private string _productTitle; + private static string _version; + + /// Assembly for information providing + public AboutInfo(Assembly assembly) + { + _assembly = assembly; + } + + /// + /// Return product title from AssemblyTitleAttribute + /// + [PublicAPI, NotNull] + public string ProductTitle => _productTitle ?? (_productTitle = GetAttribute().Title); + + /// + /// Return version of assembly + /// + [PublicAPI, NotNull] + public string Version => _version ?? (_version = _assembly.GetName().Version.ToString()); + + private TAttr GetAttribute() + { + return (TAttr)_assembly.GetCustomAttributes(typeof(TAttr), true).First(); + } + + /// + /// Default Info for IDiskApi + /// + [PublicAPI, NotNull] + public static readonly AboutInfo Client = new AboutInfo(typeof(IDiskApi).Assembly); + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Clients/CommandsClientExtensions.cs b/YandexDisk.Client/Clients/CommandsClientExtensions.cs new file mode 100644 index 00000000..db8a6296 --- /dev/null +++ b/YandexDisk.Client/Clients/CommandsClientExtensions.cs @@ -0,0 +1,152 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Clients +{ + /// + /// Extended file commands + /// + [PublicAPI] + public static class CommandsClientExtensions + { + /// + /// Default pull period for waiting operation. + /// + public static TimeSpan DefaultPullPeriod = TimeSpan.FromSeconds(3); + + private static async Task WaitOperationAsync([NotNull] this ICommandsClient client, [NotNull] Link operationLink, CancellationToken cancellationToken, TimeSpan? pullPeriod) + { + if (pullPeriod == null) + { + pullPeriod = DefaultPullPeriod; + } + + Operation operation; + do + { + Thread.Sleep(pullPeriod.Value); + operation = await client.GetOperationStatus(operationLink, cancellationToken).ConfigureAwait(false); + } while (operation.Status == OperationStatus.InProgress && + !cancellationToken.IsCancellationRequested); + } + + /// + /// Copy file or folder on Disk from one path to another and wait until operation is done + /// + public static async Task CopyAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] CopyFileRequest request, CancellationToken cancellationToken = default(CancellationToken), TimeSpan? pullPeriod = null) + { + var link = await client.CopyAsync(request, cancellationToken).ConfigureAwait(false); + + if (link.HttpStatusCode == HttpStatusCode.Accepted) + { + await client.WaitOperationAsync(link, cancellationToken, pullPeriod).ConfigureAwait(false); + } + } + + /// + /// Move file or folder on Disk from one path to another and wait until operation is done + /// + public static async Task MoveAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] MoveFileRequest request, CancellationToken cancellationToken = default(CancellationToken), TimeSpan? pullPeriod = null) + { + var link = await client.MoveAsync(request, cancellationToken).ConfigureAwait(false); + + if (link.HttpStatusCode == HttpStatusCode.Accepted) + { + await client.WaitOperationAsync(link, cancellationToken, pullPeriod).ConfigureAwait(false); + } + } + + /// + /// Delete file or folder on Disk and wait until operation is done + /// + public static async Task DeleteAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] DeleteFileRequest request, CancellationToken cancellationToken = default(CancellationToken), TimeSpan? pullPeriod = null) + { + var link = await client.DeleteAsync(request, cancellationToken).ConfigureAwait(false); + + if (link.HttpStatusCode == HttpStatusCode.Accepted) + { + await client.WaitOperationAsync(link, cancellationToken, pullPeriod).ConfigureAwait(false); + } + } + + /// + /// Empty trash + /// + public static async Task EmptyTrashAndWaitAsyncAsync([NotNull] this ICommandsClient client, [NotNull] string path, CancellationToken cancellationToken = default(CancellationToken), TimeSpan? pullPeriod = null) + { + var link = await client.EmptyTrashAsync(path, cancellationToken).ConfigureAwait(false); + + if (link.HttpStatusCode == HttpStatusCode.Accepted) + { + await client.WaitOperationAsync(link, cancellationToken, pullPeriod).ConfigureAwait(false); + } + } + + /// + /// Restore files from trash + /// + public static async Task RestoreFromTrashAndWaitAsyncAsync([NotNull] this ICommandsClient client, [NotNull] RestoreFromTrashRequest request, CancellationToken cancellationToken = default(CancellationToken), TimeSpan? pullPeriod = null) + { + var link = await client.RestoreFromTrashAsync(request, cancellationToken).ConfigureAwait(false); + + if (link.HttpStatusCode == HttpStatusCode.Accepted) + { + await client.WaitOperationAsync(link, cancellationToken, pullPeriod).ConfigureAwait(false); + } + } + + + #region Obsoleted + /// + /// Copy file or folder on Disk from one path to another and wait until operation is done + /// + [Obsolete("Method is obsolete. Please use CopyAndWaitAsync(ICommandsClient, CopyFileRequest, CancellationToken, TimeSpan) instead.")] + public static Task CopyAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] CopyFileRequest request, CancellationToken cancellationToken, int pullPeriod) + { + return CopyAndWaitAsync(client, request, cancellationToken, TimeSpan.FromSeconds(pullPeriod)); + } + + + /// + /// Move file or folder on Disk from one path to another and wait until operation is done + /// + [Obsolete("Method is obsolete. Please use MoveAndWaitAsync(ICommandsClient, MoveFileRequest, CancellationToken, TimeSpan) instead.")] + public static Task MoveAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] MoveFileRequest request, CancellationToken cancellationToken, int pullPeriod) + { + return MoveAndWaitAsync(client, request, cancellationToken, TimeSpan.FromSeconds(pullPeriod)); + } + + /// + /// Delete file or folder on Disk and wait until operation is done + /// + [Obsolete("Method is obsolete. Please use DeleteAndWaitAsync(ICommandsClient, DeleteFileRequest, CancellationToken, TimeSpan) instead.")] + public static Task DeleteAndWaitAsync([NotNull] this ICommandsClient client, [NotNull] DeleteFileRequest request, CancellationToken cancellationToken, int pullPeriod) + { + return DeleteAndWaitAsync(client, request, cancellationToken, TimeSpan.FromSeconds(pullPeriod)); + } + + /// + /// Empty trash + /// + [Obsolete("Method is obsolete. Please use EmptyTrashAndWaitAsyncAsync(ICommandsClient, string, CancellationToken, TimeSpan) instead.")] + public static Task EmptyTrashAndWaitAsyncAsync([NotNull] this ICommandsClient client, [NotNull] string path, CancellationToken cancellationToken, int pullPeriod) + { + return EmptyTrashAndWaitAsyncAsync(client, path, cancellationToken, TimeSpan.FromSeconds(pullPeriod)); + } + + /// + /// Restore files from trash + /// + [Obsolete("Method is obsolete. Please use RestoreFromTrashAndWaitAsyncAsync(ICommandsClient, RestoreFromTrashRequest, CancellationToken, TimeSpan) instead.")] + public static Task RestoreFromTrashAndWaitAsyncAsync([NotNull] this ICommandsClient client, [NotNull] RestoreFromTrashRequest request, CancellationToken cancellationToken, int pullPeriod) + { + return RestoreFromTrashAndWaitAsyncAsync(client, request, cancellationToken, TimeSpan.FromSeconds(pullPeriod)); + } + + #endregion + } +} diff --git a/YandexDisk.Client/Clients/FilesClientExtension.cs b/YandexDisk.Client/Clients/FilesClientExtension.cs new file mode 100644 index 00000000..bcc70544 --- /dev/null +++ b/YandexDisk.Client/Clients/FilesClientExtension.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Clients +{ + /// + /// Extended helpers from uploading and downloading files + /// + public static class FilesClientExtension + { + /// + /// Just upload stream data to Yandex Disk + /// + [PublicAPI] + public static async Task UploadFileAsync([NotNull] this IFilesClient client, [NotNull] string path, bool overwrite, [NotNull] Stream file, CancellationToken cancellationToken = default(CancellationToken)) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + if (String.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + Link link = await client.GetUploadLinkAsync(path, overwrite, cancellationToken).ConfigureAwait(false); + + await client.UploadAsync(link, file, cancellationToken).ConfigureAwait(false); + } + + /// + /// Just upload file from local disk to Yandex Disk + /// + [PublicAPI] + public static async Task UploadFileAsync([NotNull] this IFilesClient client, [NotNull] string path, bool overwrite, [NotNull] string localFile, CancellationToken cancellationToken) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + if (String.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + if (String.IsNullOrWhiteSpace(localFile)) + { + throw new ArgumentNullException(nameof(localFile)); + } + + Link link = await client.GetUploadLinkAsync(path, overwrite, cancellationToken).ConfigureAwait(false); + + using (var file = new FileStream(localFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + await client.UploadAsync(link, file, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Get downloaded file from Yandex Disk as stream + /// + [PublicAPI] + public static async Task DownloadFileAsync([NotNull] this IFilesClient client, [NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + if (String.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + Link link = await client.GetDownloadLinkAsync(path, cancellationToken).ConfigureAwait(false); + + return await client.DownloadAsync(link, cancellationToken).ConfigureAwait(false); + } + + /// + /// Downloaded data from Yandex Disk to local file + /// + [PublicAPI] + public static async Task DownloadFileAsync([NotNull] this IFilesClient client, [NotNull] string path, [NotNull] string localFile, CancellationToken cancellationToken = default(CancellationToken)) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + if (String.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException(nameof(path)); + } + if (String.IsNullOrWhiteSpace(localFile)) + { + throw new ArgumentNullException(nameof(localFile)); + } + + Stream data = await DownloadFileAsync(client, path, cancellationToken).ConfigureAwait(false); + + using (var file = new FileStream(localFile, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite)) + { + await data.CopyToAsync(file, bufferSize: 81920/*keep default*/, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/YandexDisk.Client/Clients/ICommandsClient.cs b/YandexDisk.Client/Clients/ICommandsClient.cs new file mode 100644 index 00000000..e8ef81ab --- /dev/null +++ b/YandexDisk.Client/Clients/ICommandsClient.cs @@ -0,0 +1,58 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Clients +{ + /// + /// Disk file operations + /// + [PublicAPI] + public interface ICommandsClient + { + /// + /// Create folder on Disk + /// + [PublicAPI, ItemNotNull] + Task CreateDictionaryAsync([NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Copy fileor folder on Disk from one path to another + /// + [PublicAPI, ItemNotNull] + Task CopyAsync([NotNull] CopyFileRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Move file or folder on Disk from one path to another + /// + [PublicAPI, ItemNotNull] + Task MoveAsync([NotNull] MoveFileRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Delete file or folder on Disk + /// + [PublicAPI, ItemNotNull] + Task DeleteAsync([NotNull] DeleteFileRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Delete files in trash + /// + [PublicAPI, ItemNotNull] + Task EmptyTrashAsync([NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Delete files in trash + /// + [PublicAPI, ItemNotNull] + Task RestoreFromTrashAsync([NotNull] RestoreFromTrashRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Return status of operation + /// + [PublicAPI, ItemNotNull] + Task GetOperationStatus([NotNull] Link link, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/YandexDisk.Client/Clients/IFilesClient.cs b/YandexDisk.Client/Clients/IFilesClient.cs new file mode 100644 index 00000000..67f0002c --- /dev/null +++ b/YandexDisk.Client/Clients/IFilesClient.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Clients +{ + /// + /// Files operation client + /// + [PublicAPI] + public interface IFilesClient + { + /// + /// Return link for file upload + /// + /// Path on Disk for uploading file + /// If file exists it will be overwritten + /// + [PublicAPI, NotNull] + Task GetUploadLinkAsync([NotNull] string path, bool overwrite, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Upload file to Disk on link recivied by + /// + [PublicAPI, NotNull] + Task UploadAsync([NotNull] Link link, [NotNull] Stream file, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Return link for file download + /// + /// Path to downloading fileon Disk + /// + [PublicAPI, NotNull] + Task GetDownloadLinkAsync([NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Download file from Disk on link recivied by + /// + [PublicAPI, NotNull] + Task DownloadAsync([NotNull] Link link, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/YandexDisk.Client/Clients/IMetaInfoClient.cs b/YandexDisk.Client/Clients/IMetaInfoClient.cs new file mode 100644 index 00000000..0f30e869 --- /dev/null +++ b/YandexDisk.Client/Clients/IMetaInfoClient.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Clients +{ + /// + /// Files and folder metadata client + /// + public interface IMetaInfoClient + { + /// + /// Returns information about disk + /// + Task GetDiskInfoAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Return files or folder metadata + /// + Task GetInfoAsync([NotNull] ResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Return files or folder metadata in the trash + /// + Task GetTrashInfoAsync([NotNull] ResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Flat file list on Disk + /// + /// + Task GetFilesInfoAsync(FilesResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Last uploaded file list on Disk + /// + Task GetLastUploadedInfoAsync([NotNull] LastUploadedResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Append custom properties to resource + /// + Task AppendCustomProperties([NotNull] string path, [NotNull] IDictionary properties, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Publish resource + /// + Task PublishFolderAsync([NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Unpublish resource + /// + Task UnpublishFolderAsync([NotNull] string path, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/YandexDisk.Client/Http/ApiContext.cs b/YandexDisk.Client/Http/ApiContext.cs new file mode 100644 index 00000000..526dfb2f --- /dev/null +++ b/YandexDisk.Client/Http/ApiContext.cs @@ -0,0 +1,13 @@ +using System; + +namespace YandexDisk.Client.Http +{ + internal class ApiContext + { + public IHttpClient HttpClient { get; set; } + + public Uri BaseUrl { get; set; } + + public ILogSaver LogSaver { get; set; } + } +} diff --git a/YandexDisk.Client/Http/Clients/CommandsClient.cs b/YandexDisk.Client/Http/Clients/CommandsClient.cs new file mode 100644 index 00000000..092f683f --- /dev/null +++ b/YandexDisk.Client/Http/Clients/CommandsClient.cs @@ -0,0 +1,66 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using YandexDisk.Client.Clients; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Http.Clients +{ + internal class CommandsClient : DiskClientBase, ICommandsClient + { + internal CommandsClient(ApiContext apiContext) + : base(apiContext) + { } + + public Task CreateDictionaryAsync(string path, CancellationToken cancellationToken = default) + { + return PutAsync("resources", new { path }, /*requestBody*/ null, cancellationToken); + } + + public Task CopyAsync(CopyFileRequest request, CancellationToken cancellationToken = default) + { + return PostAsync("resources/copy", request, /*requestBody*/ null, cancellationToken); + } + + public Task MoveAsync(MoveFileRequest request, CancellationToken cancellationToken = default) + { + return PostAsync("resources/move", request, /*requestBody*/ null, cancellationToken); + } + + public Task DeleteAsync(DeleteFileRequest request, CancellationToken cancellationToken = default) + { + return DeleteAsync("resources", request, /*requestBody*/ null, cancellationToken); + } + + public Task EmptyTrashAsync(string path, CancellationToken cancellationToken = default) + { + return DeleteAsync("trash/resources", new { path }, /*requestBody*/ null, cancellationToken); + } + + public Task RestoreFromTrashAsync(RestoreFromTrashRequest request, CancellationToken cancellationToken = default) + { + return PutAsync("trash/resources", request, /*requestBody*/ null, cancellationToken); + } + + public async Task GetOperationStatus(Link link, CancellationToken cancellationToken = default) + { + var url = new Uri(link.Href); + + var method = new HttpMethod(link.Method); + + var requestMessage = new HttpRequestMessage(method, url); + + HttpResponseMessage responseMessage = await SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); + + Operation operation = await ReadResponse(responseMessage, cancellationToken).ConfigureAwait(false); + + if (operation == null) + { + throw new Exception("Unexpected empty result."); + } + + return operation; + } + } +} diff --git a/YandexDisk.Client/Http/Clients/FilesClient.cs b/YandexDisk.Client/Http/Clients/FilesClient.cs new file mode 100644 index 00000000..857731e3 --- /dev/null +++ b/YandexDisk.Client/Http/Clients/FilesClient.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using YandexDisk.Client.Clients; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Http.Clients +{ + internal class FilesClient : DiskClientBase, IFilesClient + { + internal FilesClient(ApiContext apiContext) + : base(apiContext) + { } + + public Task GetUploadLinkAsync(string path, bool overwrite, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("resources/upload", new { path, overwrite }, cancellationToken); + } + + public Task UploadAsync(Link link, Stream file, CancellationToken cancellationToken = default(CancellationToken)) + { + var url = new Uri(link.Href); + + var method = new HttpMethod(link.Method); + + var content = new StreamContent(file); + + var requestMessage = new HttpRequestMessage(method, url) { Content = content }; + + return SendAsync(requestMessage, cancellationToken); + } + + public Task GetDownloadLinkAsync(string path, CancellationToken cancellationToken) + { + return GetAsync("resources/download", new { path }, cancellationToken); + } + + public async Task DownloadAsync(Link link, CancellationToken cancellationToken = default(CancellationToken)) + { + var url = new Uri(link.Href); + + var method = new HttpMethod(link.Method); + + var requestMessage = new HttpRequestMessage(method, url); + + HttpResponseMessage responseMessage = await SendAsync(requestMessage, cancellationToken).ConfigureAwait(false); + + return await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + } +} diff --git a/YandexDisk.Client/Http/Clients/MetaInfoClient.cs b/YandexDisk.Client/Http/Clients/MetaInfoClient.cs new file mode 100644 index 00000000..409c9a2e --- /dev/null +++ b/YandexDisk.Client/Http/Clients/MetaInfoClient.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using YandexDisk.Client.Clients; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Http.Clients +{ + internal class MetaInfoClient : DiskClientBase, IMetaInfoClient + { + internal MetaInfoClient(ApiContext apiContext) + : base(apiContext) + { } + + public Task GetDiskInfoAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("", /*params*/ null, cancellationToken); + } + + public Task GetInfoAsync(ResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("resources", request, cancellationToken); + } + + public Task GetTrashInfoAsync(ResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("trash/resources", request, cancellationToken); + } + + public Task GetFilesInfoAsync(FilesResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("resources/files", request, cancellationToken); + } + + public Task GetLastUploadedInfoAsync(LastUploadedResourceRequest request, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetAsync("resources/last-uploaded", request, cancellationToken); + } + + public Task AppendCustomProperties(string path, IDictionary customProperties, CancellationToken cancellationToken = default(CancellationToken)) + { + return PatchAsync("resources", new { path }, new { customProperties }, cancellationToken); + } + + public Task PublishFolderAsync(string path, CancellationToken cancellationToken = default) + { + return PutAsync("resources/publish", new { path }, /*requestBody*/ null, cancellationToken); + } + + public Task UnpublishFolderAsync(string path, CancellationToken cancellationToken = default) + { + return PutAsync("resources/unpublish", new { path }, /*requestBody*/ null, cancellationToken); + } + } +} diff --git a/YandexDisk.Client/Http/DiskClientBase.cs b/YandexDisk.Client/Http/DiskClientBase.cs new file mode 100644 index 00000000..7ab8122b --- /dev/null +++ b/YandexDisk.Client/Http/DiskClientBase.cs @@ -0,0 +1,299 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Newtonsoft.Json; +using YandexDisk.Client.Http.Serialization; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client.Http +{ + internal abstract class DiskClientBase + { + private static readonly QueryParamsSerializer MvcSerializer = new QueryParamsSerializer(); + + private readonly MediaTypeFormatter[] _defaultFormatters = + { + new JsonMediaTypeFormatter + { + SerializerSettings = + { + ContractResolver = new SnakeCasePropertyNamesContractResolver(), + Converters = { new SnakeCaseEnumConverter() }, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateParseHandling = DateParseHandling.DateTime + } + } + }; + + private readonly IHttpClient _httpClient; + private readonly ILogSaver _logSaver; + private readonly Uri _baseUrl; + + protected DiskClientBase([NotNull] ApiContext apiContext) + { + if (apiContext == null) + { + throw new ArgumentNullException(nameof(apiContext)); + } + if (apiContext.HttpClient == null) + { + throw new ArgumentNullException(nameof(apiContext.HttpClient)); + } + if (apiContext.BaseUrl == null) + { + throw new ArgumentNullException(nameof(apiContext.BaseUrl)); + } + + _httpClient = apiContext.HttpClient; + _logSaver = apiContext.LogSaver; + _baseUrl = apiContext.BaseUrl; + } + + [NotNull] + private Uri GetUrl([NotNull] string relativeUrl, [CanBeNull] object request = null) + { + var uriBuilder = new UriBuilder(_baseUrl); + uriBuilder.Path += relativeUrl ?? throw new ArgumentNullException(nameof(relativeUrl)); + + if (request != null) + { + uriBuilder.Query = MvcSerializer.Serialize(request); + } + + return uriBuilder.Uri; + } + + [CanBeNull] + private HttpContent GetContent([CanBeNull] TRequest request) + where TRequest : class + { + if (request == null) + { + return null; + } + + if (typeof(TRequest) == typeof(string)) + { + return new StringContent(request as string); + } + if (typeof(TRequest) == typeof(byte[])) + { + return new ByteArrayContent(request as byte[]); + } + if (typeof(Stream).IsAssignableFrom(typeof(TRequest))) + { + return new StreamContent(request as Stream); + } + + return new ObjectContent(request, _defaultFormatters.First()); + } + + [NotNull] + private ILogger GetLogger() + { + return LoggerFactory.GetLogger(_logSaver); + } + + [ItemCanBeNull] + private async Task SendAsync([NotNull] HttpRequestMessage request, CancellationToken cancellationToken) + where TResponse : class, new() + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + HttpResponseMessage responseMessage = await SendAsync(request, cancellationToken).ConfigureAwait(false); + + TResponse response = await ReadResponse(responseMessage, cancellationToken).ConfigureAwait(false); + + //If response body is null but ProtocolObjectResponse was requested, + //create empty object + if (response == null && + typeof (ProtocolObjectResponse).IsAssignableFrom(typeof (TResponse))) + { + response = new TResponse(); + } + + //If response is ProtocolObjectResponse, + //add HttpStatusCode to response + if (response is ProtocolObjectResponse protocolObject) + { + protocolObject.HttpStatusCode = responseMessage.StatusCode; + } + + return response; + } + + [ItemNotNull] + protected async Task SendAsync([NotNull] HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + using (ILogger logger = GetLogger()) + { + await logger.SetRequestAsync(request).ConfigureAwait(false); + + try + { + HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + + await logger.SetResponseAsync(response).ConfigureAwait(false); + + await EnsureSuccessStatusCode(response).ConfigureAwait(false); + + logger.EndWithSuccess(); + + return response; + } + catch (Exception e) + { + logger.EndWithError(e); + + throw; + } + } + } + + [ItemCanBeNull] + protected async Task ReadResponse([NotNull] HttpResponseMessage responseMessage, CancellationToken cancellationToken) + where TResponse : class + { + if (responseMessage == null) + { + throw new ArgumentNullException(nameof(responseMessage)); + } + + if (responseMessage.StatusCode == HttpStatusCode.NoContent) + { + return null; + } + if (typeof(TResponse) == typeof(string)) + { + return await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false) as TResponse; + } + if (typeof(TResponse) == typeof(byte[])) + { + return await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false) as TResponse; + } + if (typeof(Stream).IsAssignableFrom(typeof(TResponse))) + { + return await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false) as TResponse; + } + + return await responseMessage.Content.ReadAsAsync(_defaultFormatters, cancellationToken).ConfigureAwait(false); + } + + [ItemCanBeNull] + protected Task GetAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, CancellationToken cancellationToken) + where TParams : class + where TResponse : class, new() + { + if (relativeUrl == null) + { + throw new ArgumentNullException(nameof(relativeUrl)); + } + + Uri url = GetUrl(relativeUrl, parameters); + + var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); + + return SendAsync(requestMessage, cancellationToken); + } + + [ItemCanBeNull] + private Task RequestAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, [CanBeNull] TRequest request, [NotNull] HttpMethod httpMethod, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class, new() + where TParams : class + { + Uri url = GetUrl(relativeUrl, parameters); + + HttpContent content = GetContent(request); + + var requestMessage = new HttpRequestMessage(httpMethod, url) { Content = content }; + + return SendAsync(requestMessage, cancellationToken); + } + + [ItemCanBeNull] + protected Task PostAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, [CanBeNull] TRequest request, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class, new() + where TParams : class + { + return RequestAsync(relativeUrl, parameters, request, HttpMethod.Post, cancellationToken); + } + + [ItemCanBeNull] + protected Task PutAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, [CanBeNull] TRequest request, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class, new() + where TParams : class + { + return RequestAsync(relativeUrl, parameters, request, HttpMethod.Put, cancellationToken); + } + + [ItemCanBeNull] + protected Task DeleteAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, [CanBeNull] TRequest request, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class, new() + where TParams : class + { + return RequestAsync(relativeUrl, parameters, request, HttpMethod.Delete, cancellationToken); + } + + [ItemCanBeNull] + protected Task PatchAsync([NotNull] string relativeUrl, [CanBeNull] TParams parameters, [CanBeNull] TRequest request, CancellationToken cancellationToken) + where TRequest : class + where TResponse : class, new() + where TParams : class + { + return RequestAsync(relativeUrl, parameters, request, new HttpMethod("PATCH"), cancellationToken); + } + + private async Task EnsureSuccessStatusCode([NotNull] HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + var error = await TryGetErrorDescriptionAsync(response).ConfigureAwait(false); + + response.Content?.Dispose(); + + if (response.StatusCode == HttpStatusCode.Unauthorized || + response.StatusCode == HttpStatusCode.Forbidden) + { + throw new NotAuthorizedException(response.ReasonPhrase, error); + } + + throw new YandexApiException(response.StatusCode, response.ReasonPhrase, error); + } + } + + [ItemCanBeNull] + private async Task TryGetErrorDescriptionAsync([NotNull] HttpResponseMessage response) + { + try + { + return response.Content != null + ? await response.Content.ReadAsAsync().ConfigureAwait(false) + : null; + } + catch (SerializationException) //unexpected data in content + { + return null; + } + } + } +} diff --git a/YandexDisk.Client/Http/DiskHttpApi.cs b/YandexDisk.Client/Http/DiskHttpApi.cs new file mode 100644 index 00000000..eb52b317 --- /dev/null +++ b/YandexDisk.Client/Http/DiskHttpApi.cs @@ -0,0 +1,113 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using JetBrains.Annotations; +using YandexDisk.Client.Clients; +using YandexDisk.Client.Http.Clients; + +namespace YandexDisk.Client.Http +{ + /// + /// This object is thread safe. You should cache it between requests. + /// + [PublicAPI] + public class DiskHttpApi : IDiskApi + { + private readonly IHttpClient _httpClient; + + /// + /// Default base url to Yandex Disk API + /// + public string BaseUrl { get; } = "https://cloud-api.yandex.net/v1/disk/"; + + /// + /// Create new instance of DiskHttpApi. Keep one instance for all requests. + /// + /// + /// OAuth Key for authorization on API + /// + /// + /// Instance of custom logger. It noticed on each request-response API operation. + [PublicAPI] + public DiskHttpApi([NotNull] string oauthKey, [CanBeNull] ILogSaver logSaver = null) + { + var clientHandler = new HttpClientHandler(); + + var httpClient = new HttpClient(clientHandler, disposeHandler: true); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", oauthKey); + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(AboutInfo.Client.ProductTitle, AboutInfo.Client.Version)); + httpClient.Timeout = TimeSpan.FromHours(24); //For support large file uploading and downloading + + _httpClient = new RealHttpClientWrapper(httpClient); + + var apiContext = new ApiContext + { + HttpClient = _httpClient, + BaseUrl = new Uri(BaseUrl), + LogSaver = logSaver + }; + + Files = new FilesClient(apiContext); + MetaInfo = new MetaInfoClient(apiContext); + Commands = new CommandsClient(apiContext); + } + + /// + /// Create new instance of DiskHttpApi. Keep one instance for all requests. + /// + /// Base url to Yandex Disk API. + /// + /// OAuth Key for authorization on API + /// + /// + /// Instance of custom logger. + /// + public DiskHttpApi([NotNull] string baseUrl, [NotNull] string oauthKey, [CanBeNull] ILogSaver logSaver, [NotNull] IHttpClient httpClient) + { + BaseUrl = baseUrl; + _httpClient = httpClient; + + var apiContext = new ApiContext + { + HttpClient = httpClient, + BaseUrl = new Uri(baseUrl), + LogSaver = logSaver + }; + + Files = new FilesClient(apiContext); + MetaInfo = new MetaInfoClient(apiContext); + Commands = new CommandsClient(apiContext); + } + + #region Clients + + /// + /// Uploading and downloading file operation + /// + [PublicAPI] + public IFilesClient Files { get; } + + /// + /// Getting files and folders metadata + /// + [PublicAPI] + public IMetaInfoClient MetaInfo { get; } + + /// + /// Manipulating with existing files and folders + /// + [PublicAPI] + public ICommandsClient Commands { get; } + + #endregion + + /// + /// Dispose + /// + [PublicAPI] + public void Dispose() + { + _httpClient.Dispose(); + } + } +} diff --git a/YandexDisk.Client/Http/IHttpClient.cs b/YandexDisk.Client/Http/IHttpClient.cs new file mode 100644 index 00000000..2577ee2b --- /dev/null +++ b/YandexDisk.Client/Http/IHttpClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace YandexDisk.Client.Http +{ + /// + /// Abstract request sender for testing purpose + /// + public interface IHttpClient: IDisposable + { + /// + /// Send http-request to API + /// + [ItemNotNull] + Task SendAsync([NotNull] HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken)); + } +} diff --git a/YandexDisk.Client/Http/ILogSaver.cs b/YandexDisk.Client/Http/ILogSaver.cs new file mode 100644 index 00000000..60daf3a2 --- /dev/null +++ b/YandexDisk.Client/Http/ILogSaver.cs @@ -0,0 +1,78 @@ +using System; +using System.Net; +using JetBrains.Annotations; + +namespace YandexDisk.Client.Http +{ + /// + /// Logger noticed on each request-response API operation. + /// + [PublicAPI] + public interface ILogSaver + { + /// + /// Trigered on each request + /// + [PublicAPI] + void SaveLog([NotNull] RequestLog requestLog, [NotNull] ResponseLog responseLog); + } + + /// + /// Describe output data of http request + /// + [PublicAPI] + public class RequestLog + { + /// + /// Request Url + /// + public string Uri { get; set; } + + /// + /// Sent headers to Yandex + /// + public string Headers { get; set; } + + /// + /// Sent request body to Yandex + /// + public byte[] Body { get; set; } + + /// + /// Time of request started + /// + public DateTime StartedAt { get; set; } + } + + /// + /// Describe input data of http request + /// + [PublicAPI] + public class ResponseLog + { + /// + /// Response body from Yandex + /// + public byte[] Body { get; set; } + + /// + /// Response headers from Yandex + /// + public string Headers { get; set; } + + /// + /// Exception text if it have been risen + /// + public string Exception { get; set; } + + /// + /// Http status code from Yandex + /// + public HttpStatusCode StatusCode { get; set; } + + /// + /// Request-response time duration in milliseconds + /// + public long Duration { get; set; } + } +} diff --git a/YandexDisk.Client/Http/Logger.cs b/YandexDisk.Client/Http/Logger.cs new file mode 100644 index 00000000..9e9a8297 --- /dev/null +++ b/YandexDisk.Client/Http/Logger.cs @@ -0,0 +1,119 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace YandexDisk.Client.Http +{ + internal interface ILogger: IDisposable + { + Task SetRequestAsync([NotNull] HttpRequestMessage request); + + Task SetResponseAsync([NotNull] HttpResponseMessage httpResponseMessage); + + void EndWithSuccess(); + + void EndWithError([NotNull] Exception e); + } + + internal class Logger : ILogger + { + private readonly ILogSaver _log; + private readonly RequestLog _requestLog = new RequestLog(); + private readonly ResponseLog _responseLog = new ResponseLog(); + private readonly Stopwatch _stopwatch = new Stopwatch(); + + internal Logger(ILogSaver log) + { + _log = log; + } + + public async Task SetRequestAsync(HttpRequestMessage request) + { + _requestLog.Headers = request.ToString(); + if (request.Content != null) + { + _requestLog.Body = await request.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + + _requestLog.StartedAt = DateTime.Now; + _stopwatch.Start(); + } + + public async Task SetResponseAsync(HttpResponseMessage httpResponseMessage) + { + _stopwatch.Stop(); + + _responseLog.Headers = httpResponseMessage.ToString(); + _responseLog.StatusCode = httpResponseMessage.StatusCode; + + if (httpResponseMessage.Content != null) + { + _responseLog.Body = await httpResponseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + } + } + + public void EndWithSuccess() + { + _responseLog.Duration = _stopwatch.ElapsedMilliseconds; + + SaveLog(); + } + + public void EndWithError(Exception e) + { + _responseLog.Exception = e.Message; + _responseLog.Duration = _stopwatch.ElapsedMilliseconds; + + SaveLog(); + } + + private void SaveLog() + { + _log?.SaveLog(_requestLog, _responseLog); + + _isDisposed = true; + } + + private bool _isDisposed; + + public void Dispose() + { + if (!_isDisposed) + { + EndWithError(new Exception("Log object is never ended. You should end log befor disposing.")); + } + } + } + + internal class DummyLogger : ILogger + { + public void Dispose() + { } + + public Task SetRequestAsync(HttpRequestMessage request) + { + return Task.CompletedTask; + } + + public Task SetResponseAsync(HttpResponseMessage httpResponseMessage) + { + return Task.CompletedTask; + } + + public void EndWithSuccess() + { } + + public void EndWithError(Exception e) + { } + } + + internal static class LoggerFactory + { + public static ILogger GetLogger([CanBeNull] ILogSaver saver) + { + return saver != null ? (ILogger)new Logger(saver) : new DummyLogger(); + } + } +} diff --git a/YandexDisk.Client/Http/RealHttpClientWrapper.cs b/YandexDisk.Client/Http/RealHttpClientWrapper.cs new file mode 100644 index 00000000..79eef513 --- /dev/null +++ b/YandexDisk.Client/Http/RealHttpClientWrapper.cs @@ -0,0 +1,27 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace YandexDisk.Client.Http +{ + internal class RealHttpClientWrapper: IHttpClient + { + public RealHttpClientWrapper(HttpMessageInvoker httpMessageInvoker) + { + HttpMessageInvoker = httpMessageInvoker; + } + + private HttpMessageInvoker HttpMessageInvoker { get; } + + + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken)) + { + return HttpMessageInvoker.SendAsync(request, cancellationToken); + } + + public void Dispose() + { + HttpMessageInvoker.Dispose(); + } + } +} diff --git a/YandexDisk.Client/Http/Serialization/DateTimeSerializer.cs b/YandexDisk.Client/Http/Serialization/DateTimeSerializer.cs new file mode 100644 index 00000000..58470402 --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/DateTimeSerializer.cs @@ -0,0 +1,15 @@ +using System; +using System.Globalization; + +namespace YandexDisk.Client.Http.Serialization +{ + internal class DateTimeSerializer : IObjectSerializer + { + public string Serialize(object obj, Type type) + { + return ((DateTime)obj).ToString(Format, CultureInfo.InvariantCulture); + } + + public string Format { get; set; } = @"dd.MM.yyyy'T'HH:mm:ss"; + } +} diff --git a/YandexDisk.Client/Http/Serialization/PropertyResolver.cs b/YandexDisk.Client/Http/Serialization/PropertyResolver.cs new file mode 100644 index 00000000..cc160142 --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/PropertyResolver.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Text; + +namespace YandexDisk.Client.Http.Serialization +{ + internal abstract class PropertyResolver + { + public abstract string GetSerializedName(string propertyName); + } + + internal class DefaultPropertyResolver : PropertyResolver + { + public override string GetSerializedName(string propertyName) + { + return propertyName; + } + } + + internal class CamelCasePropertyResolver : PropertyResolver + { + public override string GetSerializedName(string propertyName) + { + return ToCamelCase(propertyName); + } + + /// + /// From Newtonsoft.Json + /// + public static string ToCamelCase(string propertyName) + { + if (string.IsNullOrEmpty(propertyName)) + return propertyName; + + if (!char.IsUpper(propertyName[0])) + return propertyName; + + char[] chars = propertyName.ToCharArray(); + + for (int i = 0; i < chars.Length; i++) + { + bool hasNext = (i + 1 < chars.Length); + if (i > 0 && hasNext && !char.IsUpper(chars[i + 1])) + break; + + chars[i] = char.ToLower(chars[i]); + } + + return new string(chars); + } + } + + internal class SnakeCasePropertyResolver : PropertyResolver + { + public override string GetSerializedName(string propertyName) + { + return ToSnakeCase(propertyName); + } + + /// + /// From https://gist.github.com/roryf/1042502 + /// + public static string ToSnakeCase(string propertyName) + { + var parts = new List(); + var currentWord = new StringBuilder(); + + foreach (var c in propertyName) + { + if (char.IsUpper(c) && currentWord.Length > 0) + { + parts.Add(currentWord.ToString()); + currentWord.Clear(); + } + currentWord.Append(char.ToLower(c)); + } + + if (currentWord.Length > 0) + { + parts.Add(currentWord.ToString()); + } + + return string.Join("_", parts.ToArray()); + } + } +} diff --git a/YandexDisk.Client/Http/Serialization/QueryParamsSerializer.cs b/YandexDisk.Client/Http/Serialization/QueryParamsSerializer.cs new file mode 100644 index 00000000..57f3c0e7 --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/QueryParamsSerializer.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace YandexDisk.Client.Http.Serialization +{ + /// + /// Интерфейс настраиваемого сереализатора + /// + internal interface IObjectSerializer + { + string Serialize(object obj, Type type); + } + + + /// + /// Сериализатор объекта в строку запроса в стиле MVC Framework + /// + internal class QueryParamsSerializer + { + public QueryParamsSerializer() + { + //Создаем общий серивализатор для всех простых типов + DefaultSerializer = new ValueSerializer(); + + //Сериализатор для строки + RegisterSerializer(typeof(string), DefaultSerializer); + //Сериализатор для времени + RegisterSerializer(typeof(DateTime), new DateTimeSerializer()); + //Сериализатор для TimeSpan + RegisterSerializer(typeof(TimeSpan), new FunctionSerializer((obj, type) => + { + var timeSpan = (TimeSpan) obj; + return $"{timeSpan.Days}.{timeSpan.Hours}:{timeSpan.Minutes}:{timeSpan.Seconds}"; + })); + } + + public PropertyResolver PropertyResolver { get; set; } = new SnakeCasePropertyResolver(); + + #region Serialize + public string Serialize(T obj) + { + return Serialize(obj, typeof(T)); + } + + public string Serialize(T obj, string prefix) + { + return Serialize(obj, typeof(T), prefix); + } + + public string Serialize(object obj) + { + if (obj == null) + { + return String.Empty; + } + return Serialize(obj, obj.GetType()); + } + + public string Serialize(object obj, string prefix) + { + if (obj == null) + { + return String.Empty; + } + return Serialize(obj, obj.GetType(), prefix); + } + + public string Serialize(object obj, Type type) + { + return Serialize(obj, type, String.Empty); + } + + public string Serialize(object obj, Type type, string prefix) + { + var stringParams = SerializeToDictionary(obj, type, prefix); + + return DictionarySerialize(stringParams); + } + + public Dictionary SerializeToDictionary(object obj) + { + if (obj == null) + { + return null; + } + return SerializeToDictionary(obj, obj.GetType(), String.Empty); + } + + public Dictionary SerializeToDictionary(object obj, Type type) + { + return SerializeToDictionary(obj, type, String.Empty); + } + + public Dictionary SerializeToDictionary(object obj, Type type, string prefix) + { + var parameters = new ParamBuilder(obj, type, prefix, _serializers.Keys); + + return ParameterSerialize(parameters); + } + + private Dictionary ParameterSerialize(IEnumerable> parameters) + { + var dic = new Dictionary(); + + foreach (var param in parameters.Where(param => param.Value != null)) + { + var type = param.Value.GetType(); + + var value = Serialize(type, param.Value); + + dic.Add(PropertyResolver.GetSerializedName(param.Key), value); + } + + return dic; + } + + private string Serialize(Type type, object value) + { + if (type.IsArray) + { + var enumerableType = type.GetElementType(); + return SerializeArray((IEnumerable)value, enumerableType); + } + + return SerializeValue(type, value); + } + + private string SerializeValue(Type type, object value) + { + IObjectSerializer serializer; + + if (!_serializers.TryGetValue(type, out serializer)) + { + serializer = DefaultSerializer; + } + + return serializer.Serialize(value, type); + } + + private string SerializeArray(IEnumerable enumerable, Type enumerableType) + { + var values = from object item in enumerable select SerializeValue(enumerableType, item); + + return String.Join(",", values); + } + #endregion + + + #region Custom Serializers + private readonly Dictionary _serializers = new Dictionary(); + + public void RegisterSerializer(Type type, IObjectSerializer serializer) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (_serializers.ContainsKey(type)) + { + _serializers[type] = serializer; + } + else + { + _serializers.Add(type, serializer); + } + } + + public void RegisterSerializer(Type type, Func serializerFunc) + { + var serializer = new FunctionSerializer(serializerFunc); + RegisterSerializer(type, serializer); + } + + public void UnregisterSerializer(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (_serializers.ContainsKey(type)) + { + _serializers.Remove(type); + } + } + + public IObjectSerializer DefaultSerializer { get; set; } + #endregion + + + #region Concatenate List + protected string DictionarySerialize(IEnumerable> dic) + { + var parameters = from param in dic select + $"{Uri.EscapeDataString(param.Key)}={Uri.EscapeDataString(param.Value)}"; + return String.Join("&", parameters); + } + #endregion + + + #region Private Classes + /// + /// Строит список параметров по именам + /// + private class ParamBuilder : Dictionary + { + /// + /// Постройка дерева + /// + /// Корневой объект для построения дерева + /// Тип объекта + /// Корневой префикс + /// Типы, разбор членов которых не требуется + public ParamBuilder(object obj, Type type, string prefix, IEnumerable simpleTypes) + { + SimpleTypes = new HashSet(simpleTypes ?? new Type[0]); + BuildParameters(prefix, obj, type); + } + + /// + /// Список типов не требующих сериализации + /// + public HashSet SimpleTypes { get; } + + /// + /// Построение списка параметров + /// + private void BuildParameters(string prefix, object obj, Type type) + { + if (obj != null && !SimpleTypes.Contains(type)) + { + if (typeof(IDictionary).IsAssignableFrom(type)) + { + AddDictionary(prefix, obj); + return; + } + + if (type.IsArray) + { + var genericType = type.GetElementType(); + + if (genericType.IsValueType || + SimpleTypes.Contains(genericType)) + { + Add(prefix, obj); + return; + } + } + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + AddEnumerable(prefix, obj); + return; + } + + if (type.IsClass) + { + AddClass(prefix, obj, type); + return; + } + } + + Add(prefix, obj); + } + + + private void AddClass(string prefix, object obj, Type type) + { + var properties = type.GetProperties(); + + foreach (var property in properties) + { + var name = property.Name; + if (!String.IsNullOrEmpty(prefix)) + { + name = $"{prefix}.{name}"; + } + + BuildParameters(name, property.GetValue(obj, null), property.PropertyType); + } + } + + private void AddEnumerable(string prefix, object obj) + { + var enumerable = (IEnumerable)obj; + + var counter = 0; + foreach (var item in enumerable) + { + var name = $"{prefix}[{counter}]"; + + BuildParameters(name, item, item?.GetType() ?? typeof(object)); + + counter++; + } + } + + private void AddDictionary(string prefix, object obj) + { + var dictionary = (IDictionary)obj; + + foreach (var itemKey in dictionary.Keys) + { + var name = $"{prefix}[{itemKey}]"; + + var item = dictionary[itemKey]; + + BuildParameters(name, item, item?.GetType() ?? typeof(object)); + } + } + } + + /// + /// Функциональный сериалайзер + /// + private class FunctionSerializer : IObjectSerializer + { + public FunctionSerializer(Func serializerFunc) + { + _serializerFunc = serializerFunc; + } + + private readonly Func _serializerFunc; + + public string Serialize(object obj, Type type) + { + return _serializerFunc(obj, type); + } + } + + + #endregion + } +} diff --git a/YandexDisk.Client/Http/Serialization/SnakeCaseEnumConverter.cs b/YandexDisk.Client/Http/Serialization/SnakeCaseEnumConverter.cs new file mode 100644 index 00000000..0180ab48 --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/SnakeCaseEnumConverter.cs @@ -0,0 +1,73 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace YandexDisk.Client.Http.Serialization +{ + internal class SnakeCaseEnumConverter: JsonConverter + { + private readonly StringEnumConverter _stringEnumConverter = new StringEnumConverter(); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + Enum e = (Enum)value; + + string enumName = e.ToString("G"); + + if (char.IsNumber(enumName[0]) || + enumName[0] == '-') + { + // enum value has no name so write number + writer.WriteValue(value); + } + else + { + string finalName = SnakeCasePropertyResolver.ToSnakeCase(enumName); + + _stringEnumConverter.WriteJson(writer, finalName, serializer); + } + } + + class OneStringJsonReader: JsonReader + { + public OneStringJsonReader(string value) + { + Value = value; + } + + public override bool Read() + { + throw new NotImplementedException("This method should not be called."); + } + + public override JsonToken TokenType { get; } = JsonToken.String; + + public override object Value { get; } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + string enumText = reader.Value.ToString(); + + enumText = enumText.Replace("-", ""); + + reader = new OneStringJsonReader(enumText); + } + + return _stringEnumConverter.ReadJson(reader, objectType, existingValue, serializer); + } + + public override bool CanConvert(Type objectType) + { + return _stringEnumConverter.CanConvert(objectType); + } + } +} diff --git a/YandexDisk.Client/Http/Serialization/SnakeCasePropertyNamesContractResolver.cs b/YandexDisk.Client/Http/Serialization/SnakeCasePropertyNamesContractResolver.cs new file mode 100644 index 00000000..bb3282c5 --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/SnakeCasePropertyNamesContractResolver.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json.Serialization; + +namespace YandexDisk.Client.Http.Serialization +{ + internal class SnakeCasePropertyNamesContractResolver : DefaultContractResolver + { + protected override string ResolvePropertyName(string propertyName) + { + return SnakeCasePropertyResolver.ToSnakeCase(propertyName); + } + } +} diff --git a/YandexDisk.Client/Http/Serialization/ValueSerializer.cs b/YandexDisk.Client/Http/Serialization/ValueSerializer.cs new file mode 100644 index 00000000..348ff29c --- /dev/null +++ b/YandexDisk.Client/Http/Serialization/ValueSerializer.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; + +namespace YandexDisk.Client.Http.Serialization +{ + internal class ValueSerializer : IObjectSerializer + { + public string Serialize(object obj, Type type) + { + if (type.IsEnum) + { + return SerializeEnum(obj, type); + } + if (type == typeof(bool)) + { + return obj.ToString().ToLower(); + } + return Convert.ToString(obj, CultureInfo.InvariantCulture); + } + + public string SerializeEnum(object obj, Type type) + { + string enumValue = Enum.GetName(type, obj); + + return SnakeCasePropertyResolver.ToSnakeCase(enumValue); + } + } +} diff --git a/YandexDisk.Client/IDiskApi.cs b/YandexDisk.Client/IDiskApi.cs new file mode 100644 index 00000000..ee4c18ae --- /dev/null +++ b/YandexDisk.Client/IDiskApi.cs @@ -0,0 +1,31 @@ +using System; +using JetBrains.Annotations; +using YandexDisk.Client.Clients; + +namespace YandexDisk.Client +{ + /// + /// Definition of all methods od Yandex Disk API + /// + [PublicAPI] + public interface IDiskApi : IDisposable + { + /// + /// Uploading and downloading file operation + /// + [PublicAPI, NotNull] + IFilesClient Files { get; } + + /// + /// Getting files and folders metadata + /// + [PublicAPI, NotNull] + IMetaInfoClient MetaInfo { get; } + + /// + /// Manipulating with existing files and folders + /// + [PublicAPI, NotNull] + ICommandsClient Commands { get; } + } +} diff --git a/YandexDisk.Client/Protocol/CopyFileRequest.cs b/YandexDisk.Client/Protocol/CopyFileRequest.cs new file mode 100644 index 00000000..f97c283a --- /dev/null +++ b/YandexDisk.Client/Protocol/CopyFileRequest.cs @@ -0,0 +1,29 @@ +using JetBrains.Annotations; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Request of coping file on Disk + /// + [PublicAPI] + public class CopyFileRequest + { + /// + /// Путь к копируемому ресурсу. Например, %2Ffoo%2Fphoto.png. + /// + public string From { get; set; } + + /// + /// Путь к создаваемой копии ресурса. Например, %2Fbar%2Fphoto.png. + /// + public string Path { get; set; } + + /// + /// Признак перезаписи. Учитывается, если ресурс копируется в папку, в которой уже есть ресурс с таким именем. + /// Допустимые значения: + /// true — удалять файлы с совпадающими именами и записывать копируемые файлы. + /// false — используется по умолчанию. Указывает не перезаписывать файлы и отменить копирование. + /// + public bool Overwrite { get; set; } + } +} diff --git a/YandexDisk.Client/Protocol/DeleteFileRequest.cs b/YandexDisk.Client/Protocol/DeleteFileRequest.cs new file mode 100644 index 00000000..4dd0363b --- /dev/null +++ b/YandexDisk.Client/Protocol/DeleteFileRequest.cs @@ -0,0 +1,23 @@ +using JetBrains.Annotations; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Request of deleting file on Disk + /// + [PublicAPI] + public class DeleteFileRequest + { + /// + /// Путь к новому положению ресурса. Например, %2Fbar%2Fphoto.png. + /// + public string Path { get; set; } + + /// + /// Признак безвозвратного удаления. Поддерживаемые значения: + /// false — удаляемый файл или папка перемещаются в Корзину(используется по умолчанию). + /// true — файл или папка удаляются без помещения в Корзину. + /// + public bool Permanently { get; set; } + } +} diff --git a/YandexDisk.Client/Protocol/Disk.cs b/YandexDisk.Client/Protocol/Disk.cs new file mode 100644 index 00000000..8e24adce --- /dev/null +++ b/YandexDisk.Client/Protocol/Disk.cs @@ -0,0 +1,45 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Данные о свободном и занятом пространстве на Диск + /// + public class Disk : ProtocolObjectResponse + { + /// + /// Объем файлов, находящихся в Корзине, в байтах. + /// + public long TrashSize { get; set; } + + /// + /// Общий объем Диска, доступный пользователю, в байтах. + /// + public long TotalSpace { get; set; } + + /// + /// Объем файлов, уже хранящихся на Диске, в байтах. + /// + public long UsedSpace { get; set; } + + /// + /// Абсолютные адреса системных папок Диска. Имена папок зависят от языка интерфейса пользователя в момент создания персонального Диска. + /// Например, для англоязычного пользователя создается папка Downloads, для русскоязычного — Загрузки и т. д. + /// + public SystemFolders SystemFolders { get; set; } + } + + /// + /// Системные папки Диска + /// + public class SystemFolders + { + /// + /// папка для файлов приложений + /// + public string Applications { get; set; } + + /// + /// папка для файлов, загруженных из интернета(не с устройства пользователя). + /// + public string Downloads { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/ErrorDescription.cs b/YandexDisk.Client/Protocol/ErrorDescription.cs new file mode 100644 index 00000000..9ee220e6 --- /dev/null +++ b/YandexDisk.Client/Protocol/ErrorDescription.cs @@ -0,0 +1,18 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Дополнительное описание ошибки + /// + public class ErrorDescription : ProtocolObjectResponse + { + /// + /// Подробное описание ошибки в помощь разработчику. + /// + public string Description { get; set; } + + /// + /// Идентификатор ошибки для программной обработки. + /// + public string Error { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/FilesResourceList.cs b/YandexDisk.Client/Protocol/FilesResourceList.cs new file mode 100644 index 00000000..bf902bdc --- /dev/null +++ b/YandexDisk.Client/Protocol/FilesResourceList.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Плоский список всех файлов на Диске в алфавитном порядке. + /// + public class FilesResourceList : ProtocolObjectResponse + { + /// + /// Массив ресурсов (Resource), содержащихся в папке. + /// + public List Items { get; set; } + + /// + /// Максимальное количество элементов в массиве items, заданное в запросе. + /// + public int Limit { get; set; } + + /// + /// Смещение начала списка от первого ресурса в папке. + /// + public int Offset { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/FilesResourceRequest.cs b/YandexDisk.Client/Protocol/FilesResourceRequest.cs new file mode 100644 index 00000000..d5958cf1 --- /dev/null +++ b/YandexDisk.Client/Protocol/FilesResourceRequest.cs @@ -0,0 +1,26 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Запрос метаинформации + /// + public class FilesResourceRequest + { + /// + /// Тип файлов, которые нужно включить в список. Диск определяет тип каждого файла при загрузке. + /// + /// https://tech.yandex.ru/disk/api/reference/all-files-docpage/ + public MediaType[] MediaType { get; set; } + + /// + /// Количество ресурсов, вложенных в папку, описание которых следует вернуть в ответе (например, для постраничного вывода). + /// Значение по умолчанию — 20. + /// + public int? Limit { get; set; } + + /// + /// Количество вложенных ресурсов с начала списка, которые следует опустить в ответе (например, для постраничного вывода). + /// Допустим, папка /foo содержит три файла.Если запросить метаинформацию о папке с параметром offset= 1 и сортировкой по умолчанию, API Диска вернет только описания второго и третьего файла. + /// + public int? Offset { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/LastUploadedResourceList.cs b/YandexDisk.Client/Protocol/LastUploadedResourceList.cs new file mode 100644 index 00000000..ef04081d --- /dev/null +++ b/YandexDisk.Client/Protocol/LastUploadedResourceList.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Список последних добавленных на Диск файлов, отсортированных по дате загрузки (от поздних к ранним). + /// + public class LastUploadedResourceList : ProtocolObjectResponse + { + /// + /// Массив ресурсов (Resource), содержащихся в папке. + /// + public List Items { get; set; } + + /// + /// Максимальное количество элементов в массиве items, заданное в запросе. + /// + public int Limit { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/LastUploadedResourceRequest.cs b/YandexDisk.Client/Protocol/LastUploadedResourceRequest.cs new file mode 100644 index 00000000..d6af6724 --- /dev/null +++ b/YandexDisk.Client/Protocol/LastUploadedResourceRequest.cs @@ -0,0 +1,20 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Запрос метаинформации + /// + public class LastUploadedResourceRequest + { + /// + /// Тип файлов, которые нужно включить в список. Диск определяет тип каждого файла при загрузке. + /// + /// https://tech.yandex.ru/disk/api/reference/all-files-docpage/ + public MediaType[] MediaType { get; set; } + + /// + /// Количество ресурсов, вложенных в папку, описание которых следует вернуть в ответе (например, для постраничного вывода). + /// Значение по умолчанию — 20. + /// + public int? Limit { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/Link.cs b/YandexDisk.Client/Protocol/Link.cs new file mode 100644 index 00000000..2cf1f733 --- /dev/null +++ b/YandexDisk.Client/Protocol/Link.cs @@ -0,0 +1,25 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Объект содержит URL для запроса метаданных ресурса. + /// + public class Link: ProtocolObjectResponse + { + /// + /// URL. Может быть шаблонизирован, см. ключ templated. + /// + public string Href { get; set; } + + /// + /// HTTP-метод для запроса URL из ключа href. + /// + public string Method { get; set; } + + /// + /// Признак URL, который был шаблонизирован согласно RFC 6570. Возможные значения: + /// «true» — URL шаблонизирован: прежде чем отправлять запрос на этот адрес, следует указать нужные значения параметров вместо значений в фигурных скобках. + /// «false» — URL может быть запрошен без изменений. + /// + public bool Templated { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/MediaType.cs b/YandexDisk.Client/Protocol/MediaType.cs new file mode 100644 index 00000000..50141fce --- /dev/null +++ b/YandexDisk.Client/Protocol/MediaType.cs @@ -0,0 +1,103 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Возможные типы медиаданных + /// + public enum MediaType + { + /// + /// аудио-файлы. + /// + Audio, + + /// + /// файлы резервных и временных копий. + /// + Backup, + + /// + /// электронные книги. + /// + Book, + + /// + /// сжатые и архивированные файлы. + /// + Compressed, + + /// + /// файлы с базами данных. + /// + Data, + + /// + /// файлы с кодом (C++, Java, XML и т. п.), а также служебные файлы IDE. + /// + Development, + + /// + /// образы носителей информации в различных форматах и сопутствующие файлы (например, CUE). + /// + Diskimage, + + /// + /// документы офисных форматов (Word, OpenOffice и т. п.). + /// + Document, + + /// + /// зашифрованные файлы. + /// + Encoded, + + /// + /// исполняемые файлы. + /// + Executable, + + /// + /// файлы с флэш-видео или анимацией. + /// + Flash, + + /// + /// файлы шрифтов. + /// + Font, + + /// + /// изображения. + /// + Image, + + /// + /// файлы настроек для различных программ. + /// + Settings, + + /// + /// файлы офисных таблиц (Numbers, Lotus). + /// + Spreadsheet, + + /// + /// текстовые файлы. + /// + Text, + + /// + /// неизвестный тип. + /// + Unknown, + + /// + /// видео-файлы. + /// + Video, + + /// + /// различные файлы, используемые браузерами и сайтами (CSS, сертификаты, файлы закладок). + /// + Web + } +} diff --git a/YandexDisk.Client/Protocol/MoveFileRequest.cs b/YandexDisk.Client/Protocol/MoveFileRequest.cs new file mode 100644 index 00000000..a90a7659 --- /dev/null +++ b/YandexDisk.Client/Protocol/MoveFileRequest.cs @@ -0,0 +1,29 @@ +using JetBrains.Annotations; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Request of moving file on Disk + /// + [PublicAPI] + public class MoveFileRequest + { + /// + /// Путь к перемещаемому ресурсу. Например, %2Ffoo%2Fphoto.png. + /// + public string From { get; set; } + + /// + /// Путь к новому положению ресурса. Например, %2Fbar%2Fphoto.png. + /// + public string Path { get; set; } + + /// + /// Признак перезаписи. Учитывается, если ресурс копируется в папку, в которой уже есть ресурс с таким именем. + /// Допустимые значения: + /// true — удалять файлы с совпадающими именами и записывать копируемые файлы. + /// false — используется по умолчанию. Указывает не перезаписывать файлы и отменить копирование. + /// + public bool Overwrite { get; set; } + } +} diff --git a/YandexDisk.Client/Protocol/Operation.cs b/YandexDisk.Client/Protocol/Operation.cs new file mode 100644 index 00000000..c26882c6 --- /dev/null +++ b/YandexDisk.Client/Protocol/Operation.cs @@ -0,0 +1,35 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Статус операции. Операции запускаются, когда вы копируете, перемещаете или удаляете непустые папки. + /// URL для запроса статуса возвращается в ответ на такие запросы. + /// + public class Operation : ProtocolObjectResponse + { + /// + /// Статус операции + /// + public OperationStatus Status { get; set; } + } + + /// + /// Возможные статусы опреаций + /// + public enum OperationStatus + { + /// + /// Операция успешно завершена. + /// + Success, + + /// + /// Операцию совершить не удалось, попробуйте повторить изначальный запрос копирования, перемещения или удаления. + /// + Failure, + + /// + /// Операция начата, но еще не завершена. + /// + InProgress, + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/ProtocolObjectResponse.cs b/YandexDisk.Client/Protocol/ProtocolObjectResponse.cs new file mode 100644 index 00000000..c0588b62 --- /dev/null +++ b/YandexDisk.Client/Protocol/ProtocolObjectResponse.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Base class of protocol object + /// + public class ProtocolObjectResponse + { + /// + /// Http status code of response from Yandex Disk API + /// + public HttpStatusCode HttpStatusCode { get; set; } + } +} diff --git a/YandexDisk.Client/Protocol/PublicResourcesList.cs b/YandexDisk.Client/Protocol/PublicResourcesList.cs new file mode 100644 index 00000000..efaa683e --- /dev/null +++ b/YandexDisk.Client/Protocol/PublicResourcesList.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Список опубликованных файлов на Диске. + /// + public class PublicResourcesList : ProtocolObjectResponse + { + /// + /// Массив ресурсов (Resource), содержащихся в папке. + /// + public List Items { get; set; } + + /// + /// Тип ресурса: + /// + public ResourceType Type { get; set; } + + /// + /// Максимальное количество элементов в массиве items, заданное в запросе. + /// + public int Limit { get; set; } + + /// + /// Смещение начала списка от первого ресурса в папке. + /// + public int Offset { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/Resource.cs b/YandexDisk.Client/Protocol/Resource.cs new file mode 100644 index 00000000..d4017b7a --- /dev/null +++ b/YandexDisk.Client/Protocol/Resource.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Описание ресурса, мета-информация о файле или папке. Включается в ответ на запрос метаинформации. + /// + public class Resource : ProtocolObjectResponse + { + /// + /// Ключ опубликованного ресурса. + /// Включается в ответ только если указанный файл или папка опубликован. + /// + public string PublicKey { get; set; } + + /// + /// Имя ресурса. + /// + public string Name { get; set; } + + /// + /// Ссылка на опубликованный ресурс. + /// Включается в ответ только если указанный файл или папка опубликован. + /// + public string PublicUrl { get; set; } + + /// + /// Путь к ресурсу до перемещения в Корзину. + /// Включается в ответ только для запроса метаинформации о ресурсе в Корзине. + /// + public string OriginPath { get; set; } + + /// + /// Полный путь к ресурсу на Диске. + /// В метаинформации опубликованной папки пути указываются относительно самой папки.Для опубликованных файлов значение ключа всегда «/». + /// Для ресурса, находящегося в Корзине, к атрибуту может быть добавлен уникальный идентификатор(например, trash:/foo_1408546879). С помощью этого идентификатора ресурс можно отличить от других удаленных ресурсов с тем же именем. + /// + public string Path { get; set; } + + /// + /// Ссылка на уменьшенное изображение из файла (превью). Включается в ответ только для файлов поддерживаемых графических форматов. + /// + /// Запросить превью можно только с OAuth-токеном пользователя, имеющего доступ к самому файлу. + public string Preview { get; set; } + + /// + /// MD5-хэш файла. + /// + public string Md5 { get; set; } + + /// + /// Тип ресурса: + /// + public ResourceType Type { get; set; } + + /// + /// MIME-тип файла. + /// + public string MimeType { get; set; } + + /// + /// Размер файла. + /// + public long Size { get; set; } + + /// + /// Дата и время создания ресурса, в формате ISO 8601. + /// + public DateTime Created { get; set; } + + /// + /// Дата и время изменения ресурса, в формате ISO 8601. + /// + public DateTime Modified { get; set; } + + /// + /// Объект со всеми атрибутами, заданными с помощью запроса Добавление метаинформации для ресурса. + /// Содержит только ключи вида имя:значение (объекты или массивы содержать не может). + /// + public Dictionary CustomProperties { get; set; } + + /// + /// Ресурсы, непосредственно содержащиеся в папке (содержит объект ResourceList). + /// Включается в ответ только при запросе метаинформации о папке. + /// + [JsonProperty("_embedded")] + public ResourceList Embedded { get; set; } + } + + /// + /// Тип ресурсов на Диске + /// + public enum ResourceType: byte + { + /// + /// Папка + /// + Dir, + + /// + /// Файл + /// + File + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/ResourceList.cs b/YandexDisk.Client/Protocol/ResourceList.cs new file mode 100644 index 00000000..4d60ded7 --- /dev/null +++ b/YandexDisk.Client/Protocol/ResourceList.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Список ресурсов, содержащихся в папке. Содержит объекты Resource и свойства списка. + /// + public class ResourceList : ProtocolObjectResponse + { + /// + /// Поле, по которому отсортирован список. + /// + public string Sort { get; set; } + + /// + /// Ключ опубликованной папки, в которой содержатся ресурсы из данного списка. + /// Включается только в ответ на запрос метаинформации о публичной папке. + /// + public string PublicKey { get; set; } + + /// + /// Массив ресурсов (Resource), содержащихся в папке. + /// + public List Items { get; set; } + + /// + /// Путь к папке, чье содержимое описывается в данном объекте ResourceList. + /// Для публичной папки значение атрибута всегда равно «/». + /// + public string Path { get; set; } + + /// + /// Максимальное количество элементов в массиве items, заданное в запросе. + /// + public int Limit { get; set; } + + /// + /// Смещение начала списка от первого ресурса в папке. + /// + public int Offset { get; set; } + + /// + /// Общее количество ресурсов в папке. + /// + public int Total { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/ResourceRequest.cs b/YandexDisk.Client/Protocol/ResourceRequest.cs new file mode 100644 index 00000000..86c304af --- /dev/null +++ b/YandexDisk.Client/Protocol/ResourceRequest.cs @@ -0,0 +1,37 @@ +namespace YandexDisk.Client.Protocol +{ + /// + /// Запрос метаинформации + /// + public class ResourceRequest + { + /// + /// Атрибут, по которому сортируется список ресурсов, вложенных в папку. В качестве значения можно указывать имена следующих ключей объекта Resource: + /// name(имя ресурса); + /// path(путь к ресурсу на Диске); + /// created(дата создания ресурса); + /// modified(дата изменения ресурса); + /// size(размер файла). + /// Для сортировки в обратном порядке добавьте дефис к значению параметра, например: sort=-name + /// + public string Sort { get; set; } + + /// + /// Путь к нужному ресурсу относительно корневого каталога Диска. Путь к ресурсу в Корзине следует указывать относительно корневого каталога Корзины. + /// Путь в значении параметра следует кодировать в URL-формате. + /// + public string Path { get; set; } + + /// + /// Количество ресурсов, вложенных в папку, описание которых следует вернуть в ответе (например, для постраничного вывода). + /// Значение по умолчанию — 20. + /// + public int? Limit { get; set; } + + /// + /// Количество вложенных ресурсов с начала списка, которые следует опустить в ответе (например, для постраничного вывода). + /// Допустим, папка /foo содержит три файла.Если запросить метаинформацию о папке с параметром offset= 1 и сортировкой по умолчанию, API Диска вернет только описания второго и третьего файла. + /// + public int? Offset { get; set; } + } +} \ No newline at end of file diff --git a/YandexDisk.Client/Protocol/RestoreFromTrashRequest.cs b/YandexDisk.Client/Protocol/RestoreFromTrashRequest.cs new file mode 100644 index 00000000..10798761 --- /dev/null +++ b/YandexDisk.Client/Protocol/RestoreFromTrashRequest.cs @@ -0,0 +1,29 @@ +using JetBrains.Annotations; + +namespace YandexDisk.Client.Protocol +{ + /// + /// Request of coping file on Disk + /// + [PublicAPI] + public class RestoreFromTrashRequest + { + /// + /// Путь к создаваемой копии ресурса. Например, %2Fbar%2Fphoto.png. + /// + public string Path { get; set; } + + /// + /// Новое имя восстанавливаемого ресурса. Например, selfie.png. + /// + public string Name { get; set; } + + /// + /// Признак перезаписи. Учитывается, если ресурс восстанавливается в папку, в которой уже есть ресурс с таким именем. + /// Допустимые значения: + /// true — удалять файлы с совпадающими именами и записывать восстанавливаемые файлы. + /// false — используется по умолчанию.Указывает не перезаписывать файлы и отменить восстановление. + /// + public bool Overwrite { get; set; } + } +} diff --git a/YandexDisk.Client/README.md b/YandexDisk.Client/README.md new file mode 100644 index 00000000..c8444424 --- /dev/null +++ b/YandexDisk.Client/README.md @@ -0,0 +1,3 @@ +# YandexDisk.Client - Updated Fork + +This is an updated fork of the [original YandexDisk.Client](https://github.com/raidenyn/yandexdisk.client/) project, which is no longer maintained. The purpose of this fork is to update dependencies and ensure compatibility with modern .NET. \ No newline at end of file diff --git a/YandexDisk.Client/YandexApiException.cs b/YandexDisk.Client/YandexApiException.cs new file mode 100644 index 00000000..0d2593b7 --- /dev/null +++ b/YandexDisk.Client/YandexApiException.cs @@ -0,0 +1,62 @@ +using System; +using System.Net; +using JetBrains.Annotations; +using YandexDisk.Client.Protocol; + +namespace YandexDisk.Client +{ + /// + /// Exception provide any exceptions risen on yandex server + /// + [PublicAPI] + [Serializable] + public class YandexApiException : Exception + { + /// + /// Http status code from Yandex server + /// + [PublicAPI] + public HttpStatusCode StatusCode { get; } + + /// + /// Reason phrase from Yandex server + /// + [PublicAPI, CanBeNull] + public string ReasonPhrase { get; } + + /// + /// Error description from Yandex + /// + [PublicAPI, CanBeNull] + public ErrorDescription Error { get; } + + internal YandexApiException(HttpStatusCode statusCode, string reasonPhrase, ErrorDescription error) + : base(error?.Description ?? reasonPhrase) + { + Error = error; + StatusCode = statusCode; + ReasonPhrase = reasonPhrase; + } + + /// + /// Format current exception to string + /// + /// + public override string ToString() + { + return String.Format("StatusCode: {0}, {1}. {2}" + Environment.NewLine + "{3}", StatusCode, ReasonPhrase, Message, StackTrace); + } + } + + /// + /// Exception provide authorization errors on yandex server + /// + [PublicAPI] + [Serializable] + public class NotAuthorizedException : YandexApiException + { + internal NotAuthorizedException(string reasonPhrase, ErrorDescription error) + : base(HttpStatusCode.Unauthorized, reasonPhrase, error) + { } + } +} diff --git a/YandexDisk.Client/YandexDisk.Client.csproj b/YandexDisk.Client/YandexDisk.Client.csproj new file mode 100644 index 00000000..a8576974 --- /dev/null +++ b/YandexDisk.Client/YandexDisk.Client.csproj @@ -0,0 +1,12 @@ + + + + + + all + + + + + +