✨ More file operations
🐛 Bug fixes on file uploading
This commit is contained in:
@ -7,7 +7,7 @@ namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
[ApiController]
|
||||
[Route("/accounts")]
|
||||
public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
public class AccountController(AppDatabase db, IHttpContextAccessor httpContext) : ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType<Account>(StatusCodes.Status200OK)]
|
||||
@ -37,7 +37,7 @@ public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
{
|
||||
var dupeNameCount = await db.Accounts.Where(a => a.Name == request.Name).CountAsync();
|
||||
if (dupeNameCount > 0)
|
||||
return new BadRequestObjectResult("The name is already taken.");
|
||||
return BadRequest("The name is already taken.");
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
@ -77,6 +77,6 @@ public class AccountController(AppDatabase db, IHttpContextAccessor httpContext)
|
||||
|
||||
var account = await db.Accounts.FindAsync(userId);
|
||||
|
||||
return new OkObjectResult(account);
|
||||
return Ok(account);
|
||||
}
|
||||
}
|
@ -10,7 +10,12 @@ namespace DysonNetwork.Sphere.Auth;
|
||||
|
||||
[ApiController]
|
||||
[Route("/auth")]
|
||||
public class AuthController(AppDatabase db, AccountService accounts, AuthService auth, IHttpContextAccessor httpContext)
|
||||
public class AuthController(
|
||||
AppDatabase db,
|
||||
AccountService accounts,
|
||||
AuthService auth,
|
||||
IHttpContextAccessor httpContext
|
||||
) : ControllerBase
|
||||
{
|
||||
public class ChallengeRequest
|
||||
{
|
||||
@ -24,7 +29,7 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
public async Task<ActionResult<Challenge>> StartChallenge([FromBody] ChallengeRequest request)
|
||||
{
|
||||
var account = await accounts.LookupAccount(request.Account);
|
||||
if (account is null) return new NotFoundObjectResult("Account was not found.");
|
||||
if (account is null) return NotFound("Account was not found.");
|
||||
|
||||
var ipAddress = httpContext.HttpContext?.Connection.RemoteIpAddress?.ToString();
|
||||
var userAgent = httpContext.HttpContext?.Request.Headers.UserAgent.ToString();
|
||||
@ -66,7 +71,7 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
.Include(e => e.Account.AuthFactors)
|
||||
.Where(e => e.Id == id).FirstOrDefaultAsync();
|
||||
return challenge is null
|
||||
? new NotFoundObjectResult("Auth challenge was not found.")
|
||||
? NotFound("Auth challenge was not found.")
|
||||
: challenge.Account.AuthFactors.ToList();
|
||||
}
|
||||
|
||||
@ -83,14 +88,14 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
)
|
||||
{
|
||||
var challenge = await db.AuthChallenges.FindAsync(id);
|
||||
if (challenge is null) return new NotFoundObjectResult("Auth challenge was not found.");
|
||||
if (challenge is null) return NotFound("Auth challenge was not found.");
|
||||
|
||||
var factor = await db.AccountAuthFactors.FindAsync(request.FactorId);
|
||||
if (factor is null) return new NotFoundObjectResult("Auth factor was not found.");
|
||||
if (factor is null) return NotFound("Auth factor was not found.");
|
||||
|
||||
if (challenge.StepRemain == 0) return challenge;
|
||||
if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow))
|
||||
return new BadRequestResult();
|
||||
return BadRequest();
|
||||
|
||||
try
|
||||
{
|
||||
@ -102,7 +107,7 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new BadRequestResult();
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
@ -125,21 +130,21 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
case "authorization_code":
|
||||
var code = Guid.TryParse(request.Code, out var codeId) ? codeId : Guid.Empty;
|
||||
if (code == Guid.Empty)
|
||||
return new BadRequestObjectResult("Invalid or missing authorization code.");
|
||||
return BadRequest("Invalid or missing authorization code.");
|
||||
var challenge = await db.AuthChallenges
|
||||
.Include(e => e.Account)
|
||||
.Where(e => e.Id == code)
|
||||
.FirstOrDefaultAsync();
|
||||
if (challenge is null)
|
||||
return new NotFoundObjectResult("Authorization code not found or expired.");
|
||||
return BadRequest("Authorization code not found or expired.");
|
||||
if (challenge.StepRemain != 0)
|
||||
return new BadRequestObjectResult("Challenge not yet completed.");
|
||||
return BadRequest("Challenge not yet completed.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Where(e => e.Challenge == challenge)
|
||||
.FirstOrDefaultAsync();
|
||||
if (session is not null)
|
||||
return new BadRequestObjectResult("Session already exists for this challenge.");
|
||||
return BadRequest("Session already exists for this challenge.");
|
||||
|
||||
session = new Session
|
||||
{
|
||||
@ -159,21 +164,21 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
var sessionIdClaim = token.Claims.FirstOrDefault(c => c.Type == "session_id")?.Value;
|
||||
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return new UnauthorizedObjectResult("Invalid or missing session_id claim in refresh token.");
|
||||
return Unauthorized("Invalid or missing session_id claim in refresh token.");
|
||||
|
||||
session = await db.AuthSessions
|
||||
.Include(e => e.Account)
|
||||
.Include(e => e.Challenge)
|
||||
.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null)
|
||||
return new NotFoundObjectResult("Session not found or expired.");
|
||||
return NotFound("Session not found or expired.");
|
||||
|
||||
session.LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return auth.CreateToken(session);
|
||||
default:
|
||||
return new BadRequestObjectResult("Unsupported grant type.");
|
||||
return BadRequest("Unsupported grant type.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,11 +188,11 @@ public class AuthController(AppDatabase db, AccountService accounts, AuthService
|
||||
{
|
||||
var sessionIdClaim = httpContext.HttpContext?.User.FindFirst("session_id")?.Value;
|
||||
if (!Guid.TryParse(sessionIdClaim, out var sessionId))
|
||||
return new UnauthorizedResult();
|
||||
return Unauthorized();
|
||||
|
||||
var session = await db.AuthSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
||||
if (session is null) return new NotFoundResult();
|
||||
if (session is null) return NotFound();
|
||||
|
||||
return new OkObjectResult(session);
|
||||
return Ok(session);
|
||||
}
|
||||
}
|
@ -10,7 +10,9 @@ using DysonNetwork.Sphere.Auth;
|
||||
using DysonNetwork.Sphere.Storage;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
@ -21,6 +23,8 @@ using File = System.IO.File;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddDbContext<AppDatabase>();
|
||||
@ -201,20 +205,28 @@ app.MapTus("/files/tus", (_) => Task.FromResult<DefaultTusConfiguration>(new()
|
||||
var fileService = eventContext.HttpContext.RequestServices.GetRequiredService<FileService>();
|
||||
|
||||
var info = await fileService.AnalyzeFileAsync(account, file.Id, fileStream, fileName, contentType);
|
||||
|
||||
var jsonOptions = httpContext.RequestServices.GetRequiredService<IOptions<JsonOptions>>().Value
|
||||
.JsonSerializerOptions;
|
||||
var infoJson = JsonSerializer.Serialize(info, jsonOptions);
|
||||
eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson);
|
||||
|
||||
#pragma warning disable CS4014
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await fileService.UploadFileToRemoteAsync(info, fileStream, null);
|
||||
await tusDiskStore.DeleteFileAsync(file.Id, eventContext.CancellationToken);
|
||||
using var scope = eventContext.HttpContext.RequestServices
|
||||
.GetRequiredService<IServiceScopeFactory>()
|
||||
.CreateScope();
|
||||
// Keep the service didn't be disposed
|
||||
var fs = scope.ServiceProvider.GetRequiredService<FileService>();
|
||||
// Keep the file stream opened
|
||||
var fileData = await tusDiskStore.GetFileAsync(file.Id, CancellationToken.None);
|
||||
var newStream = await fileData.GetContentAsync(CancellationToken.None);
|
||||
await fs.UploadFileToRemoteAsync(info, newStream, null);
|
||||
await tusDiskStore.DeleteFileAsync(file.Id, CancellationToken.None);
|
||||
});
|
||||
#pragma warning restore CS4014
|
||||
},
|
||||
OnCreateCompleteAsync = eventContext =>
|
||||
{
|
||||
// var baseUrl = builder.Configuration.GetValue<string>("Storage:BaseUrl")!;
|
||||
// eventContext.SetUploadUrl(new Uri($"{baseUrl}/files/{eventContext.FileId}"));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
103
DysonNetwork.Sphere/Storage/FileController.cs
Normal file
103
DysonNetwork.Sphere/Storage/FileController.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Minio.DataModel.Args;
|
||||
|
||||
namespace DysonNetwork.Sphere.Storage;
|
||||
|
||||
[ApiController]
|
||||
[Route("/files")]
|
||||
public class FileController(
|
||||
AppDatabase db,
|
||||
FileService fs,
|
||||
IConfiguration configuration,
|
||||
IWebHostEnvironment env
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpGet("{id}")]
|
||||
public async Task<ActionResult> OpenFile(string id)
|
||||
{
|
||||
var file = await db.Files.FindAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
|
||||
if (file.UploadedTo is null)
|
||||
{
|
||||
var tusStorePath = configuration.GetValue<string>("Tus:StorePath")!;
|
||||
var filePath = Path.Combine(env.ContentRootPath, tusStorePath, file.Id);
|
||||
if (!System.IO.File.Exists(filePath)) return new NotFoundResult();
|
||||
return PhysicalFile(filePath, file.MimeType ?? "application/octet-stream", file.Name);
|
||||
}
|
||||
|
||||
var dest = fs.GetRemoteStorageConfig(file.UploadedTo);
|
||||
|
||||
if (dest.ImageProxy is not null && (file.MimeType?.StartsWith("image/") ?? false))
|
||||
{
|
||||
var proxyUrl = dest.ImageProxy;
|
||||
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
||||
var fullUri = new Uri(baseUri, file.Id);
|
||||
return Redirect(fullUri.ToString());
|
||||
}
|
||||
|
||||
if (dest.AccessProxy is not null)
|
||||
{
|
||||
var proxyUrl = dest.AccessProxy;
|
||||
var baseUri = new Uri(proxyUrl.EndsWith('/') ? proxyUrl : $"{proxyUrl}/");
|
||||
var fullUri = new Uri(baseUri, file.Id);
|
||||
return Redirect(fullUri.ToString());
|
||||
}
|
||||
|
||||
if (dest.EnableSigned)
|
||||
{
|
||||
var client = fs.CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
return BadRequest(
|
||||
"Failed to configure client for remote destination, file got an invalid storage remote.");
|
||||
|
||||
var bucket = dest.Bucket;
|
||||
var openUrl = await client.PresignedGetObjectAsync(
|
||||
new PresignedGetObjectArgs()
|
||||
.WithBucket(bucket)
|
||||
.WithObject(file.Id)
|
||||
.WithExpiry(3600)
|
||||
);
|
||||
|
||||
return Redirect(openUrl);
|
||||
}
|
||||
|
||||
// Fallback redirect to the S3 endpoint (public read)
|
||||
var protocol = dest.EnableSsl ? "https" : "http";
|
||||
// Use the path bucket lookup mode
|
||||
return Redirect($"{protocol}://{dest.Endpoint}/{dest.Bucket}/{file.Id}");
|
||||
}
|
||||
|
||||
[HttpGet("{id}/info")]
|
||||
public async Task<ActionResult<CloudFile>> GetFileInfo(string id)
|
||||
{
|
||||
var file = await db.Files.FindAsync(id);
|
||||
if (file is null) return NotFound();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ActionResult> DeleteFile(string id)
|
||||
{
|
||||
var userIdClaim = User.FindFirst("user_id")?.Value;
|
||||
if (userIdClaim is null) return Unauthorized();
|
||||
var userId = long.Parse(userIdClaim);
|
||||
|
||||
var file = await db.Files
|
||||
.Where(e => e.Id == id)
|
||||
.Where(e => e.Account.Id == userId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (file is null) return NotFound();
|
||||
|
||||
await fs.DeleteFileDataAsync(file);
|
||||
|
||||
db.Files.Remove(file);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
@ -117,11 +117,30 @@ public class FileService(AppDatabase db, IConfiguration configuration)
|
||||
);
|
||||
|
||||
file.UploadedAt = Instant.FromDateTimeUtc(DateTime.UtcNow);
|
||||
db.Update(file);
|
||||
await db.SaveChangesAsync();
|
||||
return file;
|
||||
}
|
||||
|
||||
private RemoteStorageConfig GetRemoteStorageConfig(string destination)
|
||||
public async Task DeleteFileDataAsync(CloudFile file)
|
||||
{
|
||||
if (file.UploadedTo is null) return;
|
||||
var dest = GetRemoteStorageConfig(file.UploadedTo);
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to configure client for remote destination '{file.UploadedTo}'"
|
||||
);
|
||||
|
||||
var bucket = dest.Bucket;
|
||||
await client.RemoveObjectAsync(
|
||||
new RemoveObjectArgs().WithBucket(bucket).WithObject(file.Id)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public RemoteStorageConfig GetRemoteStorageConfig(string destination)
|
||||
{
|
||||
var destinations = configuration.GetSection("Storage:Remote").Get<List<RemoteStorageConfig>>()!;
|
||||
var dest = destinations.FirstOrDefault(d => d.Id == destination);
|
||||
@ -129,7 +148,7 @@ public class FileService(AppDatabase db, IConfiguration configuration)
|
||||
return dest;
|
||||
}
|
||||
|
||||
private IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
|
||||
public IMinioClient? CreateMinioClient(RemoteStorageConfig dest)
|
||||
{
|
||||
var client = new MinioClient()
|
||||
.WithEndpoint(dest.Endpoint)
|
||||
|
Reference in New Issue
Block a user