feat: integration cloudfile
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using mws.backend.dotnet.application.Common;
|
||||
using mws.backend.dotnet.application.Documents;
|
||||
|
||||
namespace mws.backend.dotnet.infrastructure.CloudFile;
|
||||
|
||||
public class CloudFileDocumentContentStore(
|
||||
ICloudFileHttpClient cf,
|
||||
ILogger<CloudFileDocumentContentStore> log) : IDocumentContentStore
|
||||
{
|
||||
private static string Bucket(Guid projectId) => $"mws-p-{projectId:N}";
|
||||
private static string Key(Guid documentId) => $"{documentId:N}.html";
|
||||
|
||||
public Task EnsureBucketAsync(Guid projectId, CancellationToken ct) =>
|
||||
cf.EnsureBucketAsync(Bucket(projectId), ct);
|
||||
|
||||
public async Task UploadAsync(Guid projectId, Guid documentId, string contentType, Stream body, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var meta = await cf.UploadAsync(Bucket(projectId), Key(documentId), contentType, body, ct);
|
||||
log.LogDebug("Uploaded document {DocumentId} to cloudfile: {Hash}", documentId, meta?.ContentHash);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogError(ex, "CloudFile upload failed for document {DocumentId}", documentId);
|
||||
throw new StorageUnavailableException("Cloud file upload failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream> DownloadAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await cf.DownloadAsync(Bucket(projectId), Key(documentId), ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogError(ex, "CloudFile download failed for document {DocumentId}", documentId);
|
||||
throw new StorageUnavailableException("Cloud file download failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await cf.ExistsAsync(Bucket(projectId), Key(documentId), ct);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid projectId, Guid documentId, CancellationToken ct)
|
||||
{
|
||||
var bucket = Bucket(projectId);
|
||||
var key = Key(documentId);
|
||||
log.LogInformation("CloudFile delete: bucket={Bucket}, key={Key}", bucket, key);
|
||||
try
|
||||
{
|
||||
await cf.DeleteAsync(bucket, key, ct);
|
||||
log.LogInformation("CloudFile delete OK for document {DocumentId}", documentId);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
log.LogWarning(ex, "CloudFile delete failed for document {DocumentId} (bucket={Bucket}, key={Key})", documentId, bucket, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mws.backend.dotnet.infrastructure.CloudFile;
|
||||
|
||||
public class CloudFileOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string BucketPrefix { get; set; } = "mws-p-";
|
||||
public int HttpTimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
Reference in New Issue
Block a user