1
0

426 lines
17 KiB
C#

using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using Flawless.Communication.Request;
using Flawless.Communication.Response;
using Flawless.Communication.Shared;
using Flawless.Core.BinaryDataFormat;
using Flawless.Core.Modal;
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(
UserManager<AppUser> userManager,
AppDbContext dbContext,
PathTransformer transformer,
JsonSerializerOptions serializerOpt) : 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;
}
private async ValueTask<object> ValidateRepositoryAsync(
string userName, string repositoryName, AppUser user, RepositoryRole role)
{
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 {userName}:{repositoryName}"));
if (UserNotGranted(out var rsp, user, rp, role)) return rsp!;
return rp;
}
[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 user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
// Start checkout file.
var target = transformer.GetCommitManifestPath(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 user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
// Start checkout file.
var target = transformer.GetDepotPath(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}"));
}
[HttpGet("list/commit")]
public async Task<IActionResult> ListCommitsAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
var commit = await dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
.SelectMany(r => r.Commits)
.Include(r => r.Author)
.OrderByDescending(r => r.CommittedOn)
.ToArrayAsync();
return Ok(new
{
Commits = commit
});
}
[HttpGet("list/locked")]
public async Task<IActionResult> ListLocksAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
var lockers = await dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
.SelectMany(r => r.Locked)
.Select(l => new { Path = l.Path, Owner = l.LockDownUser.UserName})
.ToArrayAsync();
return Ok(new
{
Lockers = lockers
});
}
[HttpGet("peek/commit")]
public async Task<IActionResult> PeekCommitAsync(string userName, string repositoryName)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Guest);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
var commit = await dbContext.Repositories
.Where(r => r.Owner.UserName == userName && r.Name == repositoryName)
.SelectMany(r => r.Commits)
.Include(r => r.Author)
.OrderByDescending(r => r.CommittedOn)
.FirstOrDefaultAsync();
return Ok(new
{
Commit = commit?.Id.ToString() ?? string.Empty
});
}
[HttpGet("lock")]
public async Task<IActionResult> LockAsync(string userName, string repositoryName, string path)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
var lockers = await dbContext.Repositories
.Include(repository => repository.Locked)
.FirstAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
try
{
switch (RepoLocktableUtility.AddFileLockOwner(lockers.Locked, path, user))
{
case RepoLocktableOptResult.Expand:
case RepoLocktableOptResult.IsChild:
return Ok();
case RepoLocktableOptResult.Success:
return Created();
case RepoLocktableOptResult.Rejected:
return Conflict("It was locked by others");
case RepoLocktableOptResult.Already:
return Accepted("Already been locked by you");
}
}
finally
{
await dbContext.SaveChangesAsync();
}
return BadRequest("Unknown error");
}
[HttpGet("unlock")]
public async Task<IActionResult> UnockAsync(string userName, string repositoryName, string path)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
if (grantIssue is not Repository) return (IActionResult) grantIssue;
var lockers = await dbContext.Repositories
.Include(repository => repository.Locked)
.FirstAsync(r => r.Owner.UserName == userName && r.Name == repositoryName);
try
{
switch (RepoLocktableUtility.RemoveFileLockOwner(lockers.Locked, path, user))
{
case RepoLocktableOptResult.Expand:
case RepoLocktableOptResult.IsChild:
return Ok();
case RepoLocktableOptResult.Success:
return Created();
case RepoLocktableOptResult.Rejected:
return Conflict("It was locked by others");
case RepoLocktableOptResult.Already:
return Accepted("Already been unlocked");
}
}
finally
{
await dbContext.SaveChangesAsync();
}
return BadRequest("Unknown error");
}
[HttpPost("commit")]
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
{
var user = (await userManager.GetUserAsync(HttpContext.User))!;
var grantIssue = await ValidateRepositoryAsync(userName, repositoryName, user, RepositoryRole.Developer);
if (grantIssue is not Repository rp) return (IActionResult) grantIssue;
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
// Not valid response
if (req.Depot != null) return BadRequest(new FailedResponse("You can not choose a existed depot while upload a new depot!"));
return BadRequest(new FailedResponse("You must choose a existed depot upload a new depot as baseline!"));
}
private async Task<IActionResult> CommitInternalAsync(Repository rp, AppUser user, FormCommitRequest req)
{
var test = new HashSet<WorkspaceFile>(req.WorkspaceSnapshot);
var createNewDepot = false;
RepositoryDepot mainDepot;
DepotLabel depotLabel;
// Judge if receive new depot or use old one...
if (req.Depot == null)
{
var mainDepotId = Guid.Parse(req.MainDepotId!);
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test);
if (preDepot == null)
return BadRequest(new FailedResponse("Not a valid main depot!", "NoMainDepot"));
// If still not able to resolve
if (test.Count > 0)
return BadRequest(new FailedResponse(test, "DependencyUnsolved"));
// Set this depot label...
mainDepot = preDepot;
var deps = mainDepot.Dependencies.Select(d => d.DepotId).ToArray();
depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, deps);
}
else
{
var actualRequiredDepots = new HashSet<Guid>();
// Read and create a new depot
using var cacheStream = new MemoryStream(new byte[req.Depot.Length], true);
await req.Depot.CopyToAsync(cacheStream, HttpContext.RequestAborted);
// Look if main depot can do this...
await StencilWorkspaceSnapshotAsync(cacheStream, test);
// Oh no, will we need to load their parents?
var unresolvedCount = test.Count;
if (unresolvedCount != 0)
{
// Yes
foreach (var subDepot in req.RequiredDepots?.Select(Guid.Parse) ?? [])
{
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test);
var rest = test.Count;
if (rest == 0)
{
actualRequiredDepots.Add(subDepot);
break;
}
// If test is changed?
if (unresolvedCount != rest)
{
actualRequiredDepots.Add(subDepot);
unresolvedCount = rest;
}
}
// If still not able to resolve
if (unresolvedCount > 0)
return BadRequest(new
{
Reason = "Unable to resolve workspace snapshot! May lost some essential dependency.",
Unresolved = test
});
}
// Great, a valid depot, so create it in db.
mainDepot = new RepositoryDepot { Length = req.Depot.Length };
mainDepot.Dependencies.AddRange(
await dbContext.Repositories
.Where(r => r.Id == rp.Id)
.SelectMany(r => r.Depots)
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
.ToArrayAsync());
depotLabel = new DepotLabel(mainDepot.DepotId, mainDepot.Length, actualRequiredDepots.ToArray());
rp.Depots.Add(mainDepot);
// Then write depot into disk
var depotPath = transformer.GetDepotPath(rp.Id, mainDepot.DepotId);
await using (var depotStream = System.IO.File.Create(depotPath))
{
cacheStream.Seek(0, SeekOrigin.Begin);
await cacheStream.CopyToAsync(depotStream, HttpContext.RequestAborted);
}
createNewDepot = true;
}
// Create manifest file and write to disk
var commitId = Guid.NewGuid();
var manifestPath = transformer.GetCommitManifestPath(rp.Id, commitId);
var manifest = new CommitManifest
{
ManifestId = commitId,
Depot = depotLabel,
FilePaths = req.WorkspaceSnapshot
};
await using (var manifestStream = System.IO.File.Create(manifestPath))
await JsonSerializer.SerializeAsync(manifestStream, manifest, serializerOpt);
// Create commit info
var commit = new RepositoryCommit
{
Id = commitId,
Author = user,
CommittedOn = DateTime.UtcNow,
MainDepot = mainDepot,
Message = manifestPath
};
rp.Commits.Add(commit);
try
{
// Write changes into db.
await dbContext.SaveChangesAsync();
}
catch (Exception)
{
// Revert manifest create operation
if (createNewDepot) System.IO.File.Delete(transformer.GetDepotPath(rp.Id, mainDepot.DepotId));
System.IO.File.Delete(manifestPath);
throw;
}
return Ok(new
{
CreatedAt = commit.CommittedOn,
CommitId = commit.Id
});
}
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
{
// Get version
var version = DataTransformer.GuessStandardDepotHeaderVersion(depotStream);
if (version != 1) throw new InvalidDataException($"Unable to get depot header version, feedback is {version}.");
// Get header
depotStream.Seek(0, SeekOrigin.Begin);
var header = DataTransformer.ExtractStandardDepotHeaderV1(depotStream);
// Start validation
depotStream.Seek(0, SeekOrigin.Begin);
await foreach (var inf in DataTransformer.ExtractDepotFileInfoMapAsync(depotStream, header.FileMapSize))
{
var test = new WorkspaceFile
{
ModifyTime = inf.ModifyTime,
WorkPath = inf.Path,
};
unresolved.Remove(test);
if (unresolved.Count == 0) break; // Early quit to avoid extra performance loss.
}
}
private async Task<RepositoryDepot?> StencilWorkspaceSnapshotViaDatabaseAsync(Guid rpId, Guid depotId, HashSet<WorkspaceFile> unresolved)
{
var depot = await dbContext.Repositories
.SelectMany(r => r.Depots)
.Include(r => r.Dependencies)
.ThenInclude(repositoryDepot => repositoryDepot.Dependencies)
.FirstOrDefaultAsync(r => r.DepotId == depotId);
await RecurringAsync(transformer, rpId, depot, unresolved);
return depot;
static async ValueTask RecurringAsync(PathTransformer transformer, Guid rpId, RepositoryDepot? depot, HashSet<WorkspaceFile> unresolved)
{
if (depot == null) return;
var path = transformer.GetDepotPath(rpId, depot.DepotId);
// require do not extend lifetime scope.
await using (var fs = new BufferedStream(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)))
await StencilWorkspaceSnapshotAsync(fs, unresolved);
for (var i = 0; i < depot.Dependencies.Count && unresolved.Count > 0; i++)
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved);
}
}
}