1
0

feat: Add basic file transmission via HTTP

This commit is contained in:
Ca2didi 2025-03-24 20:39:53 +08:00
parent 111eca1b3c
commit db0c95b3e8
11 changed files with 118 additions and 122 deletions

View File

@ -22,13 +22,13 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb3abda585dc54c3f81f64fdda8fc5ba72b708_003F2f_003F1d20f21a_003FUserManager_00601_002Ecs_002Fz_003A2_002D1/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fd56cb0a089b14dab96ad3ee133819f966d938_003F9c_003F183f8355_003FUserManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValueTuple_00602_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fa7_003F76eb4679_003FValueTuple_00602_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit&lt;/TestId&gt;
&lt;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD;
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest&lt;/TestId&gt;
&lt;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@ -1,15 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Identity.Core">
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\9.0.2\Microsoft.Extensions.Identity.Core.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
namespace Flawless.Communication.Request;
public class CommitRequest
{
public required string Message { get; set; }
public required string[] WorkspaceSnapshot { get; set; }
public string[]? RequiredDepots { get; set; }
public string? MainDepotId { get; set; } // If commit is not modify files, but changes workspace files (Delete) We will not require a main depot id.
}

View File

@ -1,7 +0,0 @@
using Microsoft.AspNetCore.Identity;
namespace Flawless.Communication.Response;
public class UserLoginHistoryResponse
{
}

View File

@ -1,10 +1,102 @@
using Microsoft.AspNetCore.Authorization;
using Flawless.Communication.Request;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
using Flawless.Server.Models;
using Flawless.Server.Services;
using Flawless.Server.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Flawless.Server.Controllers;
[ApiController, Authorize, Route("api/repo/{userName}/{repositoryName}")]
public class RepositoryControl : ControllerBase
public class RepositoryControl(
UserManager<AppUser> userManager,
AppDbContext dbContext,
PathTransformer transformer) : ControllerBase
{
public class FormCommitRequest : CommitRequest
{
public IFormFile Depot { get; set; }
}
private bool UserNotGranted(out IActionResult? rsp, AppUser user, Repository repo, RepositoryRole minRole)
{
if (repo.Owner == user || repo.Members.Any(m => m.User == user && m.Role >= minRole))
{
rsp = null;
return false;
}
rsp = Unauthorized(new FailedResponse("You are not allowed to do this operation"));
return true;
}
[HttpGet("fetch/manifest/{commitId}")]
public async Task<IActionResult> DownloadManifestAsync(string userName, string repositoryName, string commitId)
{
if (!Guid.TryParse(commitId, out var commitGuid)) return BadRequest(new FailedResponse("Invalid commit id"));
var rp = await dbContext.Repositories
.Include(r => r.Owner)
.Include(r => r.Members)
.FirstOrDefaultAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
if (rp == null) return NotFound(new FailedResponse($"Could not find repository {repositoryName}"));
var u = (await userManager.FindByNameAsync(userName))!;
if (UserNotGranted(out var rsp, u, rp, RepositoryRole.Guest)) return rsp!;
// Start checkout file.
var target = transformer.GetCommitManifest(rp.Id, commitGuid);
if (System.IO.File.Exists(target)) return File(System.IO.File.OpenRead(target), "application/octet-stream");
return NotFound(new FailedResponse($"Could not find commit manifest {target}"));
}
[HttpGet("fetch/depot/{depotId}")]
public async Task<IActionResult> DownloadDepotAsync(string userName, string repositoryName, string depotId)
{
if (!Guid.TryParse(depotId, out var depotGuid)) return BadRequest(new FailedResponse("Invalid depot id"));
var rp = await dbContext.Repositories
.Include(r => r.Owner)
.Include(r => r.Members)
.FirstOrDefaultAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
if (rp == null) return NotFound(new FailedResponse($"Could not find repository {repositoryName}"));
var u = (await userManager.FindByNameAsync(userName))!;
if (UserNotGranted(out var rsp, u, rp, RepositoryRole.Guest)) return rsp!;
// Start checkout file.
var target = transformer.GetDepotManifest(rp.Id, depotGuid);
if (System.IO.File.Exists(target)) return File(System.IO.File.OpenRead(target), "application/octet-stream");
return NotFound(new FailedResponse($"Could not find depot {target}"));
}
// [HttpPost("commit")]
// public async Task<IActionResult> CommitAsync([FromForm] FormCommitRequest request)
// {
// }
//
// [HttpGet("list/commit")]
// public async Task<IActionResult> ListCommitsAsync(string userName, string repositoryName)
// {
//
// }
//
// [HttpGet("query/commit/{keyword}")]
// public async Task<IActionResult> QueryCommitsAsync(string userName, string repositoryName, string keyword)
// {
//
// }
//
// [HttpGet("get/commit/{commitId}")]
// public async Task<IActionResult> GetCommitAsync(string userName, string repositoryName, string commitId)
// {
//
// }
}

View File

@ -1,46 +0,0 @@
using Flawless.Server.Models;
using Flawless.Server.Services;
using Flawless.Server.Utility;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace Flawless.Server.Controllers;
[ApiController, Authorize, Route("ws/transfer")]
public class WebSocketTransferController(
PathTransformer transformer,
RepoTransmissionContext transmission,
UserManager<AppUser> userManager) : ControllerBase
{
[Route("download/{taskId}")]
public async Task DownloadDepotAsync(string taskId)
{
var (u, task) = await GetTaskAsync(transmission.DownloadTask, taskId);
}
[Route("upload/{taskId}")]
public async Task UploadDepotAsync(string taskId)
{
var (u, task) = await GetTaskAsync(transmission.DownloadTask, taskId);
}
private async ValueTask<(AppUser user, RepoDepotTransmissionTask task)> GetTaskAsync
(IDictionary<Guid, RepoDepotTransmissionTask> taskList, string taskId)
{
if (!Guid.TryParse(taskId, out var id))
throw new ArgumentException("Not a valid task id!", nameof(taskId));
if (!taskList.TryGetValue(id, out var task))
throw new ArgumentException("Task not found.", nameof(taskId));
var u = (await userManager.GetUserAsync(HttpContext.User))!;
if (task.UserId != u.Id) throw new ArgumentException("Not being authorized!", nameof(taskId));
if (DateTime.UtcNow > task.ExpiresOn)
throw new ArgumentException("Task is expired!", nameof(taskId));
return (u, task);
}
}

View File

@ -1,21 +0,0 @@
using Flawless.Communication.Response;
namespace Flawless.Server.Middlewares;
public class WebSocketHandoffMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context)
{
// Is a WebSocket call and no ws presents.
if (context.Request.Path.StartsWithSegments("/ws") && !context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(
new FailedResponse("Connection should be a WebSocket!", "NotWebSocketRequest"));
return;
}
await next(context);
}
}

View File

@ -1,12 +0,0 @@
namespace Flawless.Server.Models;
public class RepoDepotTransmissionTask
{
public required Guid UserId { get; set; }
public required Guid RepositoryId { get; set; }
public required Guid DepotId { get; set; }
public required DateTime ExpiresOn { get; set; }
}

View File

@ -35,7 +35,6 @@ public static class Program
{
// Api related
builder.Services.AddSingleton<PathTransformer>();
builder.Services.AddSingleton<RepoTransmissionContext>();
builder.Services.AddOpenApi();
builder.Services.AddControllers()
.AddJsonOptions(opt =>
@ -121,7 +120,6 @@ public static class Program
KeepAliveInterval = TimeSpan.FromSeconds(60),
KeepAliveTimeout = TimeSpan.FromSeconds(300),
});
app.UseMiddleware<WebSocketHandoffMiddleware>();
// Configure identity control
app.UseAuthentication();

View File

@ -1,15 +0,0 @@
using System.Collections.Concurrent;
using Flawless.Server.Models;
namespace Flawless.Server.Services;
public class RepoTransmissionContext
{
private readonly ConcurrentDictionary<Guid, RepoDepotTransmissionTask> _uploadTask = new();
private readonly ConcurrentDictionary<Guid, RepoDepotTransmissionTask> _downloadTask = new();
public IDictionary<Guid, RepoDepotTransmissionTask> UploadTask => _uploadTask;
public IDictionary<Guid, RepoDepotTransmissionTask> DownloadTask => _downloadTask;
}

View File

@ -21,7 +21,7 @@ public class PathTransformer(IConfiguration config)
if (repositoryId == Guid.Empty) throw new ArgumentException(nameof(repositoryId));
if (commitId == Guid.Empty) throw new ArgumentException(nameof(commitId));
return Path.Combine(_rootPath, repositoryId.ToString(), commitId.ToString());
return Path.Combine(_rootPath, repositoryId.ToString(), "Manifests", commitId.ToString());
}
public string GetDepotManifest(Guid repositoryId, Guid depotId)
@ -29,6 +29,6 @@ public class PathTransformer(IConfiguration config)
if (repositoryId == Guid.Empty) throw new ArgumentException(nameof(repositoryId));
if (depotId == Guid.Empty) throw new ArgumentException(nameof(depotId));
return Path.Combine(_rootPath, repositoryId.ToString(), depotId.ToString());
return Path.Combine(_rootPath, repositoryId.ToString(), "Depots", depotId.ToString());
}
}