diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 6a1ac48..09c5595 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -16,7 +16,7 @@ on:
env:
APP_NAME: 'OneCloud.S3.API'
- DOTNET_VERSION: '7.0.x'
+ DOTNET_VERSION: '8.0.x'
jobs:
build-and-publish:
@@ -52,7 +52,7 @@ jobs:
- name: Build Container Image
working-directory: ./${{env.APP_NAME}}/
- run: dotnet publish -p:PublishProfile=DefaultContainer -p:ContainerImageName=ghcr.io/${{ github.repository }} -c Release
+ run: dotnet publish --os linux --arch x64 --self-contained true -c Release -p:PublishProfile=DefaultContainer -p:PublishSingleFile=true -p:ContainerImageName=ghcr.io/${{ github.repository }}
- name: Push Image to Container Registry
run: docker push ghcr.io/${{ github.repository }} --all-tags
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 81a6b8a..45b7957 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,11 +1,11 @@
-
- true
-
-
-
-
-
-
-
-
+
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OneCloud.S3.API/Controllers/BucketsController.cs b/OneCloud.S3.API/Controllers/BucketsController.cs
deleted file mode 100644
index 7fdfbf2..0000000
--- a/OneCloud.S3.API/Controllers/BucketsController.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using OneCloud.S3.API.Infrastructure.Interfaces;
-using OneCloud.S3.API.Models.Dto;
-using System.Net.Mime;
-
-namespace OneCloud.S3.API.Controllers;
-
-///
-/// Buckets controller
-///
-[ApiController]
-[ApiConventionType(typeof(DefaultApiConventions))]
-[Route("api/storage/buckets")]
-[Produces(MediaTypeNames.Application.Json)]
-public class BucketsController(IStorageBucketRepository storageRepository) : ControllerBase
-{
- private readonly IStorageBucketRepository _storageRepository = storageRepository;
-
- ///
- /// Get list of buckets
- ///
- ///
- ///
- [HttpGet(Name = "GetBuckets")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
- public async Task GetBuckets(CancellationToken cancellationToken)
- {
- var buckets = await _storageRepository.ListBucketsAsync(cancellationToken);
- if(!buckets.Any())
- return NotFound();
- var result = buckets.Select(s => new BucketDto
- {
- BucketName = s.BucketName,
- CreationDate = s.CreationDate,
- });
- return Ok(result);
- }
-
- ///
- /// List of bucket objects
- ///
- /// Bucket name
- ///
- ///
- [HttpGet("content/{bucket}", Name = "GetBucketContent")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
- public async Task GetBucketContent(string bucket, CancellationToken cancellationToken)
- {
- var result = await _storageRepository.ListBucketContentAsync(bucket, cancellationToken);
- return Ok(result.Select(s => new ObjectDto
- {
- BucketName = s.BucketName,
- Key = s.Key,
- ETag = s.ETag,
- Size = s.Size,
- LastModified = s.LastModified,
- }));
- }
-
- ///
- /// Create bucket
- ///
- /// Bucket name
- ///
- ///
- [HttpPost("{bucket}", Name = "CreateBucket")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
- public async Task PostBucket(string bucket, CancellationToken cancellationToken)
- {
- await _storageRepository.PutBucketAsync(bucket, cancellationToken);
- return CreatedAtAction("GetBucketContent", new { bucket });
- }
-
- ///
- /// Delete bucket
- ///
- /// Bucket name
- ///
- ///
- [HttpDelete("{bucket}", Name = "DeleteBucket")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))]
- public async Task DeleteBucket(string bucket, CancellationToken cancellationToken)
- {
- await _storageRepository.DeleteBucketAsync(bucket, cancellationToken);
- return Ok();
- }
-}
diff --git a/OneCloud.S3.API/Controllers/ObjectsController.cs b/OneCloud.S3.API/Controllers/ObjectsController.cs
deleted file mode 100644
index b7fd48f..0000000
--- a/OneCloud.S3.API/Controllers/ObjectsController.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using OneCloud.S3.API.Infrastructure.Interfaces;
-using OneCloud.S3.API.Models.Dto;
-using System.ComponentModel.DataAnnotations;
-using System.Net.Mime;
-
-namespace OneCloud.S3.API.Controllers;
-
-///
-/// Object controller
-///
-[ApiController]
-[Route("api/storage/objects")]
-[Produces(MediaTypeNames.Application.Json)]
-public class ObjectsController(IStorageObjectRepository storageRepository) : ControllerBase
-{
- private readonly IStorageObjectRepository _storageRepository = storageRepository;
-
- ///
- /// Get object
- ///
- /// Bucket name
- /// Object key
- /// Object MIME-type
- ///
- ///
- [HttpGet("{bucket}/{filePath}", Name = "GetObject")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
- public async Task GetObject(string bucket, string filePath, [Required] string contentType, CancellationToken cancellationToken)
- {
- var result = await _storageRepository.GetObjectAsync(bucket, filePath, cancellationToken);
- if(result.Length == 0)
- return NotFound();
- return File(result, contentType, Path.GetFileName(filePath));
- }
-
- ///
- /// Get temporary public link to object
- ///
- /// Bucket name
- /// Object key
- /// Date link expires
- ///
- [HttpGet("url/{bucket}/{filePath}", Name = "GetObjectUrl")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Get))]
- public IActionResult GetObjectUrl(string bucket, string filePath, [Required] DateTime expires)
- {
- var result = _storageRepository.GetPreSignedUrl(bucket, filePath, expires);
- return Ok(new ObjectUrlDto
- {
- Url = result,
- Expires = expires,
- });
- }
-
- ///
- /// Upload object
- ///
- /// Target bucket name
- /// Target object key
- ///
- ///
- ///
- [HttpPost("{bucket}/{filePath}", Name = "CreateObject")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
- public async Task PostObject(string bucket, string filePath, [Required] IFormFile file, CancellationToken cancellationToken)
- {
- await _storageRepository.PutObjectAsync(bucket, filePath, file, cancellationToken);
- return CreatedAtAction("GetObject", new { bucket, filePath, file.ContentType });
- }
-
- ///
- /// Change object permissions
- ///
- /// Bucket name
- /// Object key
- /// Is object public?
- ///
- ///
- [HttpPut("permission/{bucket}/{filePath}", Name = "ChangeObjectPermissions")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Put))]
- public async Task PutObjectPermission(string bucket, string filePath, bool isPublicRead, CancellationToken cancellationToken)
- {
- await _storageRepository.PutAclAsync(bucket, filePath, isPublicRead, cancellationToken);
- return Ok();
- }
-
- ///
- /// Copy object
- ///
- /// Source bucket name
- /// Source object key
- /// Target bucket name
- /// Target object key
- ///
- ///
- [HttpPut("copy/{srcBucket}/{srcFilePath}", Name = "CopyObject")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Put))]
- public async Task PutObjectCopy(string srcBucket, string srcFilePath, [Required] string destBucket, [Required] string destFilePath, CancellationToken cancellationToken)
- {
- await _storageRepository.CopyObjectAsync(srcBucket, srcFilePath, destBucket, destFilePath, cancellationToken);
- return Ok();
- }
-
- ///
- /// Delete object
- ///
- /// Bucket name
- /// Object key
- ///
- ///
- [HttpDelete("{bucket}/{filePath}", Name = "DeleteObject")]
- [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Delete))]
- public async Task DeleteObject(string bucket, string filePath, CancellationToken cancellationToken)
- {
- await _storageRepository.DeleteObjectAsync(bucket, filePath, cancellationToken);
- return Ok();
- }
-}
diff --git a/OneCloud.S3.API/EndPoints/BucketsEndPoints.cs b/OneCloud.S3.API/EndPoints/BucketsEndPoints.cs
new file mode 100644
index 0000000..a0eed6f
--- /dev/null
+++ b/OneCloud.S3.API/EndPoints/BucketsEndPoints.cs
@@ -0,0 +1,59 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+using Microsoft.AspNetCore.Http.HttpResults;
+using OneCloud.S3.API.Models;
+using System.Net;
+
+namespace OneCloud.S3.API.EndPoints;
+
+public static class BucketsEndPoints
+{
+ public static IEndpointRouteBuilder UseBucketsEndpoints(this IEndpointRouteBuilder builder)
+ {
+ var buckets = builder
+ .MapGroup("storage/buckets")
+ .WithTags("Buckets");
+
+ buckets.MapGet("", async (AmazonS3Client client, CancellationToken cancellationToken) =>
+ await client.ListBucketsAsync(cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK } response
+ ? TypedResults.Ok(response.Buckets.Select(s => new BucketDto(s.BucketName, s.CreationDate)))
+ : Results.NotFound())
+ .Produces>()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("GetBuckets")
+ .WithSummary("Buckets list");
+
+ buckets.MapGet("{bucketName}", async (AmazonS3Client client, string bucketName, CancellationToken cancellationToken) =>
+ await client.ListObjectsAsync(new ListObjectsRequest { BucketName = bucketName }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK } response
+ ? TypedResults.Ok(response.S3Objects.Select(s => new ObjectDto(s.BucketName, s.Key, s.ETag, s.LastModified, s.Size)))
+ : Results.NotFound())
+ .Produces>()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("GetBucketContent")
+ .WithSummary("Bucket content");
+
+ buckets.MapPost("{bucketName}", async (AmazonS3Client client, string bucketName, CancellationToken cancellationToken) =>
+ await client.PutBucketAsync(bucketName, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.CreatedAtRoute("GetBucketContent", new { bucketName })
+ : Results.BadRequest())
+ .Produces(StatusCodes.Status201Created)
+ .Produces(StatusCodes.Status403Forbidden)
+ .WithName("CreateBucket")
+ .WithSummary("Create bucket");
+
+ buckets.MapDelete("{bucketName}", async (AmazonS3Client client, string bucketName, CancellationToken cancellationToken) =>
+ await client.DeleteBucketAsync(bucketName, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.NoContent()
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("DeleteBucket")
+ .WithSummary("Delete bucket");
+
+ return builder;
+ }
+}
diff --git a/OneCloud.S3.API/EndPoints/ObjectsEndPoints.cs b/OneCloud.S3.API/EndPoints/ObjectsEndPoints.cs
new file mode 100644
index 0000000..af6da3b
--- /dev/null
+++ b/OneCloud.S3.API/EndPoints/ObjectsEndPoints.cs
@@ -0,0 +1,83 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.Net.Http.Headers;
+using OneCloud.S3.API.Models;
+using System.Net;
+
+namespace OneCloud.S3.API.EndPoints;
+
+public static class ObjectsEndPoints
+{
+ public static IEndpointRouteBuilder UseObjectsEndpoints(this IEndpointRouteBuilder builder)
+ {
+ var objects = builder
+ .MapGroup("storage/objects")
+ .WithTags("Objects");
+
+ objects.MapGet("{bucketName}", async (AmazonS3Client client, string bucketName, string filePath, CancellationToken cancellationToken) =>
+ await client.GetObjectAsync(new GetObjectRequest { BucketName = bucketName, Key = filePath }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK } response
+ ? TypedResults.Stream(response.ResponseStream, response.Headers.ContentType, response.Key, response.LastModified, EntityTagHeaderValue.Parse(response.ETag), true)
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("GetObject")
+ .WithSummary("Get object");
+
+ objects.MapGet("{bucketName}/url", (AmazonS3Client client, string bucketName, string filePath, DateTime expires) =>
+ client.GetPreSignedURL(new GetPreSignedUrlRequest { BucketName = bucketName, Key = filePath, Expires = expires, Protocol = Protocol.HTTPS })
+ is { } url
+ ? TypedResults.Created(url, new ObjectUrlDto(url, expires))
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("GetObjectUrl")
+ .WithSummary("Temporary public URL to object");
+
+ objects.MapPost("{bucketName}", async (AmazonS3Client client, string bucketName, string filePath, IFormFile file, CancellationToken cancellationToken) =>
+ {
+ await using var stream = file.OpenReadStream();
+ return await client.PutObjectAsync(new PutObjectRequest { BucketName = bucketName, Key = filePath, ContentType = file.ContentType, InputStream = stream, AutoCloseStream = true, UseChunkEncoding = false }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.CreatedAtRoute("GetObject", new { bucketName, filePath, file.ContentType })
+ : Results.BadRequest();
+ })
+ .Produces(StatusCodes.Status201Created)
+ .Produces(StatusCodes.Status403Forbidden)
+ .WithName("CreateObject")
+ .WithSummary("Create object");
+
+ objects.MapPut("{bucketName}/permission", async (AmazonS3Client client, string bucketName, string filePath, bool isPublicRead, CancellationToken cancellationToken) =>
+ await client.PutACLAsync(new PutACLRequest { BucketName = bucketName, Key = filePath, CannedACL = isPublicRead ? S3CannedACL.PublicRead : S3CannedACL.Private }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.NoContent()
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("ChangeObjectPermissions")
+ .WithSummary("Change object permissions");
+
+ objects.MapPut("{bucketName}/copy", async (AmazonS3Client client, string bucketName, string filePath, string destBucket, string destFilePath, CancellationToken cancellationToken) =>
+ await client.CopyObjectAsync(new CopyObjectRequest { SourceBucket = bucketName, SourceKey = filePath, DestinationBucket = destBucket, DestinationKey = destFilePath }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.NoContent()
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("CopyObject")
+ .WithSummary("Copy object");
+
+ objects.MapDelete("{bucketName}", async (AmazonS3Client client, string bucketName, string filePath, CancellationToken cancellationToken) =>
+ await client.DeleteObjectAsync(new DeleteObjectRequest { BucketName = bucketName, Key = filePath }, cancellationToken)
+ is { HttpStatusCode: HttpStatusCode.OK }
+ ? TypedResults.NoContent()
+ : Results.NotFound())
+ .Produces()
+ .Produces(StatusCodes.Status404NotFound)
+ .WithName("DeleteObject")
+ .WithSummary("Delete object");
+
+ return builder;
+ }
+}
diff --git a/OneCloud.S3.API/EndPoints/StorageEndPoints.cs b/OneCloud.S3.API/EndPoints/StorageEndPoints.cs
new file mode 100644
index 0000000..44cf48b
--- /dev/null
+++ b/OneCloud.S3.API/EndPoints/StorageEndPoints.cs
@@ -0,0 +1,40 @@
+using OneCloud.S3.API.Models;
+
+namespace OneCloud.S3.API.EndPoints;
+
+public static class StorageEndPoints
+{
+ public static IEndpointRouteBuilder UseStorageEndPoints(this RouteGroupBuilder builder)
+ {
+ var storage = builder.MapGroup("storage").WithTags("Storage");
+
+ storage.MapPost("", async (IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
+ {
+ using var client = httpClientFactory.CreateClient("api");
+ using var request = await client.PostAsync("storage", null, cancellationToken);
+ return await request.Content.ReadFromJsonAsync(cancellationToken: cancellationToken)
+ is { } response
+ ? TypedResults.Ok(response)
+ : Results.BadRequest();
+ })
+ .Produces()
+ .Produces(StatusCodes.Status403Forbidden)
+ .WithName("StorageActivate")
+ .WithSummary("Activate storage");
+
+ storage.MapDelete("", async (IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
+ {
+ using var client = httpClientFactory.CreateClient("api");
+ using var request = await client.DeleteAsync("storage", cancellationToken);
+ return await request.Content.ReadFromJsonAsync