🗑️ Remove old tus api for file upload
This commit is contained in:
@@ -53,7 +53,6 @@
|
|||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" />
|
||||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using DysonNetwork.Shared.Auth;
|
|||||||
using DysonNetwork.Shared.Http;
|
using DysonNetwork.Shared.Http;
|
||||||
using DysonNetwork.Shared.Registry;
|
using DysonNetwork.Shared.Registry;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using tusdotnet.Stores;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -42,8 +41,7 @@ using (var scope = app.Services.CreateScope())
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var tusDiskStore = app.Services.GetRequiredService<TusDiskStore>();
|
app.ConfigureAppMiddleware();
|
||||||
app.ConfigureAppMiddleware(tusDiskStore);
|
|
||||||
|
|
||||||
// Configure gRPC
|
// Configure gRPC
|
||||||
app.ConfigureGrpcServices();
|
app.ConfigureGrpcServices();
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
using DysonNetwork.Drive.Storage;
|
using DysonNetwork.Drive.Storage;
|
||||||
using tusdotnet;
|
|
||||||
using tusdotnet.Interfaces;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Drive.Startup;
|
namespace DysonNetwork.Drive.Startup;
|
||||||
|
|
||||||
public static class ApplicationBuilderExtensions
|
public static class ApplicationBuilderExtensions
|
||||||
{
|
{
|
||||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, ITusStore tusStore)
|
public static WebApplication ConfigureAppMiddleware(this WebApplication app)
|
||||||
{
|
{
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.MapTus("/api/tus", _ => Task.FromResult(TusService.BuildConfiguration(tusStore, app.Configuration)));
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,301 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using DysonNetwork.Drive.Billing;
|
|
||||||
using DysonNetwork.Shared.Auth;
|
|
||||||
using DysonNetwork.Shared.Proto;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NodaTime;
|
|
||||||
using tusdotnet.Interfaces;
|
|
||||||
using tusdotnet.Models;
|
|
||||||
using tusdotnet.Models.Configuration;
|
|
||||||
|
|
||||||
namespace DysonNetwork.Drive.Storage;
|
|
||||||
|
|
||||||
public abstract class TusService
|
|
||||||
{
|
|
||||||
public static DefaultTusConfiguration BuildConfiguration(
|
|
||||||
ITusStore store,
|
|
||||||
IConfiguration configuration
|
|
||||||
) => new()
|
|
||||||
{
|
|
||||||
Store = store,
|
|
||||||
Events = new Events
|
|
||||||
{
|
|
||||||
OnAuthorizeAsync = async eventContext =>
|
|
||||||
{
|
|
||||||
if (eventContext.Intent == IntentType.DeleteFile)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
"Deleting files from this endpoint was disabled, please refer to the Dyson Network File API."
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpContext = eventContext.HttpContext;
|
|
||||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.Unauthorized);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventContext.Intent != IntentType.CreateFile) return;
|
|
||||||
|
|
||||||
using var scope = httpContext.RequestServices.CreateScope();
|
|
||||||
|
|
||||||
if (!currentUser.IsSuperuser)
|
|
||||||
{
|
|
||||||
var pm = scope.ServiceProvider.GetRequiredService<PermissionService.PermissionServiceClient>();
|
|
||||||
var allowed = await pm.HasPermissionAsync(new HasPermissionRequest
|
|
||||||
{ Actor = $"user:{currentUser.Id}", Area = "global", Key = "files.create" });
|
|
||||||
if (!allowed.HasPermission)
|
|
||||||
eventContext.FailRequest(HttpStatusCode.Forbidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(filePool)) filePool = configuration["Storage:PreferredRemote"];
|
|
||||||
if (!Guid.TryParse(filePool, out _))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file pool id");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fs = scope.ServiceProvider.GetRequiredService<FileService>();
|
|
||||||
var pool = await fs.GetPoolAsync(Guid.Parse(filePool!));
|
|
||||||
if (pool is null)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Pool not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pool.PolicyConfig.RequirePrivilege > 0)
|
|
||||||
{
|
|
||||||
if (currentUser.PerkSubscription is null)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
$"You need to have join the Stellar Program to use this pool"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var privilege =
|
|
||||||
PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier);
|
|
||||||
if (privilege < pool.PolicyConfig.RequirePrivilege)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
$"You need Stellar Program tier {pool.PolicyConfig.RequirePrivilege} to use this pool, you are tier {privilege}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(bundleId) && !Guid.TryParse(bundleId, out _))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file bundle id");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OnFileCompleteAsync = async eventContext =>
|
|
||||||
{
|
|
||||||
using var scope = eventContext.HttpContext.RequestServices.CreateScope();
|
|
||||||
var services = scope.ServiceProvider;
|
|
||||||
|
|
||||||
var httpContext = eventContext.HttpContext;
|
|
||||||
if (httpContext.Items["CurrentUser"] is not Account user) return;
|
|
||||||
|
|
||||||
var file = await eventContext.GetFileAsync();
|
|
||||||
var metadata = await file.GetMetadataAsync(eventContext.CancellationToken);
|
|
||||||
var fileName = metadata.TryGetValue("filename", out var fn)
|
|
||||||
? fn.GetString(Encoding.UTF8)
|
|
||||||
: "uploaded_file";
|
|
||||||
var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null;
|
|
||||||
|
|
||||||
var filePath = Path.Combine(configuration.GetValue<string>("Tus:StorePath")!, file.Id);
|
|
||||||
|
|
||||||
var filePool = httpContext.Request.Headers["X-FilePool"].FirstOrDefault();
|
|
||||||
var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault();
|
|
||||||
var encryptPassword = httpContext.Request.Headers["X-FilePass"].FirstOrDefault();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(filePool))
|
|
||||||
filePool = configuration["Storage:PreferredRemote"];
|
|
||||||
|
|
||||||
Instant? expiredAt = null;
|
|
||||||
var expiredString = httpContext.Request.Headers["X-FileExpire"].FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(expiredString) && int.TryParse(expiredString, out var expired))
|
|
||||||
expiredAt = Instant.FromUnixTimeSeconds(expired);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileService = services.GetRequiredService<FileService>();
|
|
||||||
var info = await fileService.ProcessNewFileAsync(
|
|
||||||
user,
|
|
||||||
file.Id,
|
|
||||||
filePool!,
|
|
||||||
bundleId,
|
|
||||||
filePath,
|
|
||||||
fileName,
|
|
||||||
contentType,
|
|
||||||
encryptPassword,
|
|
||||||
expiredAt
|
|
||||||
);
|
|
||||||
|
|
||||||
using var finalScope = eventContext.HttpContext.RequestServices.CreateScope();
|
|
||||||
var jsonOptions = finalScope.ServiceProvider.GetRequiredService<IOptions<JsonOptions>>().Value
|
|
||||||
.JsonSerializerOptions;
|
|
||||||
var infoJson = JsonSerializer.Serialize(info, jsonOptions);
|
|
||||||
eventContext.HttpContext.Response.Headers.Append("X-FileInfo", infoJson);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var logger = services.GetRequiredService<ILogger<TusService>>();
|
|
||||||
eventContext.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
||||||
await eventContext.HttpContext.Response.WriteAsync(ex.Message);
|
|
||||||
logger.LogError(ex, "Error handling file upload...");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
OnBeforeCreateAsync = async eventContext =>
|
|
||||||
{
|
|
||||||
var httpContext = eventContext.HttpContext;
|
|
||||||
if (httpContext.Items["CurrentUser"] is not Account currentUser)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.Unauthorized);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
|
||||||
|
|
||||||
var poolId = eventContext.HttpContext.Request.Headers["X-FilePool"].FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(poolId)) poolId = configuration["Storage:PreferredRemote"];
|
|
||||||
if (!Guid.TryParse(poolId, out _))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file pool id");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundleId = eventContext.HttpContext.Request.Headers["X-FileBundle"].FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(bundleId) && !Guid.TryParse(bundleId, out _))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Invalid file bundle id");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = eventContext.Metadata;
|
|
||||||
var contentType = metadata.TryGetValue("content-type", out var ct) ? ct.GetString(Encoding.UTF8) : null;
|
|
||||||
|
|
||||||
var scope = eventContext.HttpContext.RequestServices.CreateScope();
|
|
||||||
|
|
||||||
var rejected = false;
|
|
||||||
|
|
||||||
var fs = scope.ServiceProvider.GetRequiredService<FileService>();
|
|
||||||
var pool = await fs.GetPoolAsync(Guid.Parse(poolId!));
|
|
||||||
if (pool is null)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(HttpStatusCode.BadRequest, "Pool not found");
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TusService>>();
|
|
||||||
|
|
||||||
// Do the policy check
|
|
||||||
var policy = pool!.PolicyConfig;
|
|
||||||
if (!rejected && !pool.PolicyConfig.AllowEncryption)
|
|
||||||
{
|
|
||||||
var encryptPassword = eventContext.HttpContext.Request.Headers["X-FilePass"].FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(encryptPassword))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
"File encryption is not allowed in this pool"
|
|
||||||
);
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rejected && policy.AcceptTypes is not null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(contentType))
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
"Content type is required by the pool's policy"
|
|
||||||
);
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var foundMatch = false;
|
|
||||||
foreach (var acceptType in policy.AcceptTypes)
|
|
||||||
{
|
|
||||||
if (acceptType.EndsWith("/*", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var type = acceptType[..^2];
|
|
||||||
if (!contentType.StartsWith($"{type}/", StringComparison.OrdinalIgnoreCase)) continue;
|
|
||||||
foundMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (acceptType.Equals(contentType, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
foundMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundMatch)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
$"Content type {contentType} is not allowed by the pool's policy"
|
|
||||||
);
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rejected && policy.MaxFileSize is not null)
|
|
||||||
{
|
|
||||||
if (eventContext.UploadLength > policy.MaxFileSize)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
$"File size {eventContext.UploadLength} is larger than the pool's maximum file size {policy.MaxFileSize}"
|
|
||||||
);
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rejected)
|
|
||||||
{
|
|
||||||
var quotaService = scope.ServiceProvider.GetRequiredService<QuotaService>();
|
|
||||||
var (ok, billableUnit, quota) = await quotaService.IsFileAcceptable(
|
|
||||||
accountId,
|
|
||||||
pool.BillingConfig.CostMultiplier ?? 1.0,
|
|
||||||
eventContext.UploadLength
|
|
||||||
);
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
eventContext.FailRequest(
|
|
||||||
HttpStatusCode.Forbidden,
|
|
||||||
$"File size {billableUnit} MiB is exceeded the user's quota {quota} MiB"
|
|
||||||
);
|
|
||||||
rejected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rejected)
|
|
||||||
logger.LogInformation("File rejected #{FileId}", eventContext.FileId);
|
|
||||||
},
|
|
||||||
OnCreateCompleteAsync = eventContext =>
|
|
||||||
{
|
|
||||||
var directUpload = eventContext.HttpContext.Request.Headers["X-DirectUpload"].FirstOrDefault();
|
|
||||||
if (!string.IsNullOrEmpty(directUpload)) return Task.CompletedTask;
|
|
||||||
|
|
||||||
var gatewayUrl = configuration["GatewayUrl"];
|
|
||||||
if (gatewayUrl is not null)
|
|
||||||
eventContext.SetUploadUrl(new Uri(gatewayUrl + "/drive/tus/" + eventContext.FileId));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user