feat: integration cloudfile

This commit is contained in:
2026-09-06 12:33:38 +07:00
parent c2088f42dc
commit 888dd41dcd
24 changed files with 1148 additions and 28 deletions
@@ -0,0 +1,146 @@
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<CloudFileMetadata?> UploadAsync(string bucket, string key, string contentType, Stream body, CancellationToken ct);
Task<Stream> DownloadAsync(string bucket, string key, CancellationToken ct);
Task<bool> 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<CloudFileOptions> opt,
ILogger<CloudFileHttpClient> 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<CloudFileMetadata?> 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<CloudFileUploadResponse>(responseText, Json);
return dto?.Metadata;
}
public async Task<Stream> 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<bool> 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; }
}