start testing cloud service implementation
This commit is contained in:
parent
3c08d93bc5
commit
2c3f19d7ce
5 changed files with 328 additions and 0 deletions
8
MetaforceInstaller.Cloud/ICloudService.cs
Normal file
8
MetaforceInstaller.Cloud/ICloudService.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using MetaforceInstaller.Cloud.Models;
|
||||||
|
|
||||||
|
namespace MetaforceInstaller.Cloud;
|
||||||
|
|
||||||
|
public interface ICloudService
|
||||||
|
{
|
||||||
|
Task<List<CloudObject>> GetObjects(string webLink);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace MetaforceInstaller.Cloud.Implementations.MailRu.DTO;
|
||||||
|
|
||||||
|
public sealed class CloudObjectsResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("count")]
|
||||||
|
public CountInfo? Count { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("weblink")]
|
||||||
|
public string? WebLink { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("size")]
|
||||||
|
public long? Size { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("rev")]
|
||||||
|
public long? Rev { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("kind")]
|
||||||
|
public string? Kind { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Type { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("public")]
|
||||||
|
public PublicInfo? Public { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("list")]
|
||||||
|
public List<CloudObjectBase>? List { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("owner")]
|
||||||
|
public OwnerInfo? Owner { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CountInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("folders")]
|
||||||
|
public int? Folders { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("files")]
|
||||||
|
public int? Files { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PublicInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Type { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string? Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ctime")]
|
||||||
|
public long? CTime { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("views")]
|
||||||
|
public long? Views { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("downloads")]
|
||||||
|
public long? Downloads { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OwnerInfo
|
||||||
|
{
|
||||||
|
[JsonPropertyName("email")]
|
||||||
|
public string? Email { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("user_flags")]
|
||||||
|
public UserFlags? UserFlags { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserFlags
|
||||||
|
{
|
||||||
|
[JsonPropertyName("PAID_ACCOUNT")]
|
||||||
|
public bool? PaidAccount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Базовый тип элемента из "list". Конкретный тип выбирается по полю "type".
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(CloudObjectJsonConverter))]
|
||||||
|
public abstract class CloudObjectBase
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("weblink")]
|
||||||
|
public string? WebLink { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("size")]
|
||||||
|
public long? Size { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("kind")]
|
||||||
|
public string? Kind { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string? Type { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CloudFolderObject : CloudObjectBase
|
||||||
|
{
|
||||||
|
[JsonPropertyName("count")]
|
||||||
|
public CountInfo? Count { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("rev")]
|
||||||
|
public long? Rev { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CloudFileObject : CloudObjectBase
|
||||||
|
{
|
||||||
|
[JsonPropertyName("mtime")]
|
||||||
|
public long? MTime { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("hash")]
|
||||||
|
public string? Hash { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ZipWebLinkRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("x-email")]
|
||||||
|
public string XEmail { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("weblink_list")]
|
||||||
|
public IReadOnlyList<string> WeblinkList { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ZipWebLinkResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("key")]
|
||||||
|
public string Key { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Конвертер, который смотрит на поле "type" и десериализует в CloudFolderObject или CloudFileObject.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CloudObjectJsonConverter : JsonConverter<CloudObjectBase>
|
||||||
|
{
|
||||||
|
public override CloudObjectBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.ParseValue(ref reader);
|
||||||
|
|
||||||
|
var root = doc.RootElement;
|
||||||
|
if (!root.TryGetProperty("type", out var typeProp))
|
||||||
|
throw new JsonException("Missing required property 'type' for cloud object.");
|
||||||
|
|
||||||
|
var type = typeProp.GetString();
|
||||||
|
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
"folder" => root.Deserialize<CloudFolderObject>(options)
|
||||||
|
?? throw new JsonException("Failed to deserialize folder object."),
|
||||||
|
"file" => root.Deserialize<CloudFileObject>(options)
|
||||||
|
?? throw new JsonException("Failed to deserialize file object."),
|
||||||
|
_ => throw new JsonException($"Unknown cloud object type '{type}'.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, CloudObjectBase value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case CloudFolderObject folder:
|
||||||
|
JsonSerializer.Serialize(writer, folder, options);
|
||||||
|
break;
|
||||||
|
case CloudFileObject file:
|
||||||
|
JsonSerializer.Serialize(writer, file, options);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new JsonException($"Unknown runtime type '{value.GetType().Name}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Web;
|
||||||
|
using MetaforceInstaller.Cloud.Implementations.MailRu.DTO;
|
||||||
|
using MetaforceInstaller.Cloud.Models;
|
||||||
|
|
||||||
|
namespace MetaforceInstaller.Cloud.Implementations.MailRu;
|
||||||
|
|
||||||
|
public class MailRuCloudService : ICloudService
|
||||||
|
{
|
||||||
|
private const string BaseUrl = "https://cloud.mail.ru";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public MailRuCloudService(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CloudObject>> GetObjects(string webLink)
|
||||||
|
{
|
||||||
|
var dto = await GetFromPublicApi<CloudObjectsResponse>(
|
||||||
|
path: "/api/v4/public/list",
|
||||||
|
query: new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["weblink"] = webLink,
|
||||||
|
["sort"] = "name",
|
||||||
|
["order"] = "asc",
|
||||||
|
["offset"] = "0",
|
||||||
|
["limit"] = "500",
|
||||||
|
["version"] = "4"
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (dto?.List is null || dto.List.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var result = new List<CloudObject>(dto.List.Count);
|
||||||
|
foreach (var entry in dto.List)
|
||||||
|
result.Add(Map(entry));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetDownloadLink(IEnumerable<string> webLinks, string outputFileName = "archive")
|
||||||
|
{
|
||||||
|
var result = await PostToPublicApi<ZipWebLinkResponse>(
|
||||||
|
path: "/api/v3/zip/weblink",
|
||||||
|
query: new Dictionary<string, string?>(),
|
||||||
|
new ZipWebLinkRequest
|
||||||
|
{
|
||||||
|
XEmail = "anonym",
|
||||||
|
WeblinkList = webLinks.ToList(),
|
||||||
|
Name = outputFileName
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result?.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TResponse?> GetFromPublicApi<TResponse>(string path, IReadOnlyDictionary<string, string?> query)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
|
||||||
|
using var response = await _httpClient.GetAsync(uri).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
return JsonSerializer.Deserialize<TResponse>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TResponse?> PostToPublicApi<TResponse>(string path, IReadOnlyDictionary<string, string?> query,
|
||||||
|
object body)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
|
||||||
|
var jsonBody = JsonSerializer.Serialize(body, JsonOptions);
|
||||||
|
using var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await _httpClient.PostAsync(uri, content).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
return JsonSerializer.Deserialize<TResponse>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri BuildUri(string path, IReadOnlyDictionary<string, string?> query)
|
||||||
|
{
|
||||||
|
var builder = new UriBuilder(BaseUrl)
|
||||||
|
{
|
||||||
|
Port = -1,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
|
|
||||||
|
var qs = HttpUtility.ParseQueryString(builder.Query);
|
||||||
|
foreach (var (key, value) in query)
|
||||||
|
qs[key] = value;
|
||||||
|
|
||||||
|
builder.Query = qs.ToString();
|
||||||
|
return builder.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CloudObject Map(CloudObjectBase entry)
|
||||||
|
{
|
||||||
|
return new CloudObject
|
||||||
|
{
|
||||||
|
Name = entry.Name,
|
||||||
|
WebLink = entry.WebLink,
|
||||||
|
Type = entry.Type == "folder" ? CloudObjectType.Folder : CloudObjectType.File
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
MetaforceInstaller.Cloud/MetaforceInstaller.Cloud.csproj
Normal file
9
MetaforceInstaller.Cloud/MetaforceInstaller.Cloud.csproj
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
14
MetaforceInstaller.Cloud/Models/CloudObject.cs
Normal file
14
MetaforceInstaller.Cloud/Models/CloudObject.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace MetaforceInstaller.Cloud.Models;
|
||||||
|
|
||||||
|
public class CloudObject
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string WebLink { get; set; }
|
||||||
|
public CloudObjectType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CloudObjectType
|
||||||
|
{
|
||||||
|
Folder,
|
||||||
|
File
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue