using System.Net; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace mws.backend.dotnet.infrastructure.CloudFile; public interface ICloudFileHttpClient { Task UploadAsync(string bucket, string key, string contentType, Stream body, CancellationToken ct); Task DownloadAsync(string bucket, string key, CancellationToken ct); Task ExistsAsync(string bucket, string key, CancellationToken ct); Task DeleteAsync(string bucket, string key, CancellationToken ct); Task EnsureBucketAsync(string bucket, CancellationToken ct); } public class CloudFileHttpClient( HttpClient http, IOptions opt, ILogger log) : ICloudFileHttpClient { private static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private HttpRequestMessage Req(HttpMethod method, string url) { var request = new HttpRequestMessage(method, url); request.Headers.Add("X-API-Key", opt.Value.ApiKey); return request; } public async Task UploadAsync(string bucket, string key, string contentType, Stream body, CancellationToken ct) { using var form = new MultipartFormDataContent { { new StreamContent(body), "file", key }, { new StringContent(bucket), "bucket" }, { new StringContent(key), "key" } }; var request = Req(HttpMethod.Post, "/api/files/upload"); request.Content = form; var resp = await http.SendAsync(request, ct); var responseText = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode) { log.LogWarning("CloudFile upload failed {Status}: {Body}", resp.StatusCode, responseText); resp.EnsureSuccessStatusCode(); } var dto = JsonSerializer.Deserialize(responseText, Json); return dto?.Metadata; } public async Task DownloadAsync(string bucket, string key, CancellationToken ct) { var request = Req(HttpMethod.Get, $"/api/files/download/{bucket}/{Uri.EscapeDataString(key)}"); var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); if (resp.StatusCode == HttpStatusCode.NotFound) { return Stream.Null; } resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStreamAsync(ct); } public async Task ExistsAsync(string bucket, string key, CancellationToken ct) { var request = Req(HttpMethod.Get, $"/api/files/{bucket}/metadata/{Uri.EscapeDataString(key)}"); var resp = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); return resp.IsSuccessStatusCode; } public async Task DeleteAsync(string bucket, string key, CancellationToken ct) { var request = Req(HttpMethod.Delete, $"/api/files/{bucket}/{Uri.EscapeDataString(key)}"); var resp = await http.SendAsync(request, ct); if (resp.StatusCode != HttpStatusCode.NotFound) { resp.EnsureSuccessStatusCode(); } } public async Task EnsureBucketAsync(string bucket, CancellationToken ct) { var request = Req(HttpMethod.Post, "/api/buckets"); var json = JsonSerializer.Serialize(new { name = bucket }, Json); request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); var resp = await http.SendAsync(request, ct); var responseText = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode && resp.StatusCode != HttpStatusCode.BadRequest) { log.LogWarning("CloudFile ensureBucket failed {Status}: {Body}", resp.StatusCode, responseText); resp.EnsureSuccessStatusCode(); } } } public class CloudFileUploadResponse { public bool Success { get; set; } public CloudFileMetadata? Metadata { get; set; } } public class CloudFileMetadata { [JsonPropertyName("fileId")] public string FileId { get; set; } = string.Empty; [JsonPropertyName("fileName")] public string FileName { get; set; } = string.Empty; [JsonPropertyName("extension")] public string Extension { get; set; } = string.Empty; [JsonPropertyName("contentType")] public string ContentType { get; set; } = string.Empty; [JsonPropertyName("size")] public long Size { get; set; } [JsonPropertyName("bucket")] public string Bucket { get; set; } = string.Empty; [JsonPropertyName("key")] public string Key { get; set; } = string.Empty; [JsonPropertyName("contentHash")] public string ContentHash { get; set; } = string.Empty; [JsonPropertyName("createdAt")] public DateTime CreatedAt { get; set; } [JsonPropertyName("modifiedAt")] public DateTime ModifiedAt { get; set; } }