eCourt-Doc-Edit/backend/WordVersioning.Api/Services/ObjectStorageService.cs
Suphonchai Phoonsawat b976cf3124 implement Phase 1-2
2026-08-19 10:54:55 +07:00

97 lines
4.5 KiB
C#

using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Options;
namespace WordVersioning.Api.Services;
public interface IObjectStorageService
{
/// <summary>อัปโหลด stream ไป bucket (content ต้อง seek ได้ — จะได้กำหนด ContentLength และ retry ได้)</summary>
Task PutAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default);
/// <summary>ดึง object กลับมาเป็น stream (proxy ต่อให้ client) — stream นี้ dispose แล้วจะปิด S3 response ให้ด้วย</summary>
Task<Stream> GetStreamAsync(string bucket, string key, CancellationToken ct = default);
}
/// <summary>Wrapper เรียก MinIO ผ่าน AWSSDK.S3 (S3-compatible API)</summary>
public class ObjectStorageService : IObjectStorageService
{
private readonly IAmazonS3 _s3;
private readonly ILogger<ObjectStorageService> _logger;
public ObjectStorageService(IOptions<MinioOptions> options, ILogger<ObjectStorageService> logger)
{
var opt = options.Value;
var config = new AmazonS3Config
{
ServiceURL = (opt.UseSSL ? "https://" : "http://") + opt.Endpoint,
ForcePathStyle = true, // จำเป็นสำหรับ MinIO (path-style bucket)
};
_s3 = new AmazonS3Client(new BasicAWSCredentials(opt.AccessKey, opt.SecretKey), config);
_logger = logger;
}
public async Task PutAsync(string bucket, string key, Stream content, string contentType, CancellationToken ct = default)
{
var request = new PutObjectRequest
{
BucketName = bucket,
Key = key,
InputStream = content,
ContentType = contentType,
};
if (content.CanSeek)
{
// ระบุความยาวชัดๆ ไม่ต้องให้ SDK ไป buffer ทั้ง stream
request.Headers.ContentLength = content.Length;
}
await _s3.PutObjectAsync(request, ct);
_logger.LogInformation("Uploaded object {Bucket}/{Key} ({Length} bytes)", bucket, key, content.Length);
}
public async Task<Stream> GetStreamAsync(string bucket, string key, CancellationToken ct = default)
{
GetObjectResponse response;
try
{
response = await _s3.GetObjectAsync(new GetObjectRequest { BucketName = bucket, Key = key }, ct);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
throw new KeyNotFoundException($"Object {bucket}/{key} not found");
}
_logger.LogInformation("Fetching object {Bucket}/{Key} ({Length} bytes)", bucket, key, response.ContentLength);
return new S3ResponseStream(response);
}
/// <summary>Stream ที่ครอบ GetObjectResponse — dispose stream = dispose response ทั้งอัน
/// (ไม่งั้น controller ต้องรับ response มาด้วยแล้วจัดการ lifetime เอง ยุ่ง)</summary>
private sealed class S3ResponseStream(GetObjectResponse response) : Stream
{
private readonly Stream _inner = response.ResponseStream;
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
public override long Length => _inner.Length;
public override long Position { get => _inner.Position; set => _inner.Position = value; }
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) =>
_inner.ReadAsync(buffer, offset, count, ct);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct = default) => _inner.ReadAsync(buffer, ct);
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void Flush() => _inner.Flush();
public override void SetLength(long value) => _inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
protected override void Dispose(bool disposing)
{
if (disposing) response.Dispose();
base.Dispose(disposing);
}
}
}