✨ Fast upload API
This commit is contained in:
@@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Google.Protobuf;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
@@ -13,7 +12,7 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
|
||||
{
|
||||
/// The id generated by TuS, basically just UUID remove the dash lines
|
||||
[MaxLength(32)]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty);
|
||||
|
||||
[MaxLength(1024)] public string Name { get; set; } = string.Empty;
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
@@ -58,6 +57,10 @@ public class CloudFile : ModelBase, ICloudFile, IIdentifiedResource
|
||||
[MaxLength(4096)]
|
||||
public string? StorageUrl { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? FastUploadLink { get; set; }
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
public CloudFileReferenceObject ToReferenceObject()
|
||||
@@ -138,4 +141,4 @@ public class CloudFileReference : ModelBase
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -258,4 +258,62 @@ public class FileController(
|
||||
var count = await fs.DeleteAllRecycledFilesAsync();
|
||||
return Ok(new { Count = count });
|
||||
}
|
||||
|
||||
public class CreateFastFileRequest
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public long Size { get; set; }
|
||||
public string Hash { get; set; } = null!;
|
||||
public string? MimeType { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public Dictionary<string, object?>? UserMeta { get; set; }
|
||||
public Dictionary<string, object?>? FileMeta { get; set; }
|
||||
public List<ContentSensitiveMark>? SensitiveMarks { get; set; }
|
||||
public Guid PoolId { get; set; }
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("fast")]
|
||||
[RequiredPermission("global", "files.create")]
|
||||
public async Task<ActionResult<CloudFile>> CreateFastFile([FromBody] CreateFastFileRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var pool = await db.Pools.FirstOrDefaultAsync(p => p.Id == request.PoolId);
|
||||
if (pool is null) return BadRequest();
|
||||
if (!currentUser.IsSuperuser && pool.AccountId != accountId)
|
||||
return StatusCode(403, "You don't have permission to create files in this pool.");
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var file = new CloudFile
|
||||
{
|
||||
Name = request.Name,
|
||||
Size = request.Size,
|
||||
Hash = request.Hash,
|
||||
MimeType = request.MimeType,
|
||||
Description = request.Description,
|
||||
AccountId = accountId,
|
||||
UserMeta = request.UserMeta,
|
||||
FileMeta = request.FileMeta,
|
||||
SensitiveMarks = request.SensitiveMarks,
|
||||
PoolId = request.PoolId
|
||||
};
|
||||
db.Files.Add(file);
|
||||
await db.SaveChangesAsync();
|
||||
await fs._PurgeCacheAsync(file.Id);
|
||||
await transaction.CommitAsync();
|
||||
|
||||
file.FastUploadLink = await fs.CreateFastUploadLinkAsync(file);
|
||||
|
||||
return file;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
@@ -821,6 +821,27 @@ public class FileService(
|
||||
await db.SaveChangesAsync();
|
||||
return count;
|
||||
}
|
||||
|
||||
public async Task<string> CreateFastUploadLinkAsync(CloudFile file)
|
||||
{
|
||||
if (file.PoolId is null) throw new InvalidOperationException("Pool ID is null");
|
||||
|
||||
var dest = await GetRemoteStorageConfig(file.PoolId.Value);
|
||||
if (dest is null) throw new InvalidOperationException($"No remote storage configured for pool {file.PoolId}");
|
||||
var client = CreateMinioClient(dest);
|
||||
if (client is null)
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to configure client for remote destination '{file.PoolId}'"
|
||||
);
|
||||
|
||||
var url = await client.PresignedPutObjectAsync(
|
||||
new PresignedPutObjectArgs()
|
||||
.WithBucket(dest.Bucket)
|
||||
.WithObject(file.Id)
|
||||
.WithExpiry(60 * 60 * 24)
|
||||
);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -105,6 +105,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APathTransformExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf3f51607a3e4e76b5b91640cd7409195c430_003Fd9_003Faff65774_003FPathTransformExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APath_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb6f0571a6bc744b0b551fd4578292582e54c00_003Fd3_003F7b05b2bd_003FPath_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedGetObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F0df26a9d89e29319e9efcaea0a8489db9e97bc1aedcca3f7e360cc50f8f4ea_003FPresignedGetObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APresignedPutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9202b7fc1762440f9948968ca0e7397b62200_003Fc1_003F4de0298e_003FPresignedPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F556d6adabb16af913b52d8825c320d848cd36ae3d8818e8d12db3a23680db73_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APutObjectArgs_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F6efe388c7585d5dd5587416a55298550b030c2a107edf45f988791297c3ffa_003FPutObjectArgs_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AQueryable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F42d8f09d6a294d00a6f49efc989927492fe00_003F4e_003F26d1ee34_003FQueryable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
|
Reference in New Issue
Block a user