1
0

feat: Add locktable for lock\unlock files.

This commit is contained in:
Ca2didi 2025-03-26 00:28:03 +08:00
parent 4a7c92a979
commit 437b5c6ab8
7 changed files with 208 additions and 11 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;
&lt;TestAncestor&gt;
&lt;TestId&gt;MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit&lt;/TestId&gt;
&lt;/TestAncestor&gt;
<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;
&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;
&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;
<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;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View File

@ -402,6 +402,16 @@ public static class WorkPath
return null;
}
/// <summary>
/// Get a relative work path from another work path in case first one is a sub path of second one.
/// <param name="relatedTo">The parent one.</param>
/// <param name="workPath">The child one.</param>
/// <returns>Same or is child of relatedTo will return true.</returns>
public static bool IsPathRelated(string relatedTo, string workPath)
{
return workPath.StartsWith(EndsInDirectorySeparator(relatedTo) ? relatedTo[..^1] : relatedTo);
}
private static void CombineInternal(StringBuilder sb, string addon)
{

View File

@ -106,8 +106,26 @@ public class RepositoryControl(
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)
{
@ -128,11 +146,86 @@ public class RepositoryControl(
});
}
[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.Guest);
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);

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Flawless.Server.Controllers;
namespace Flawless.Server.Models;
@ -24,4 +25,6 @@ public class Repository
public List<RepositoryCommit> Commits { get; set; } = new();
public List<RepositoryDepot> Depots { get; set; } = new();
public List<RepositoryLockedFile> Locked { get; set; } = new();
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Flawless.Server.Models;
public class RepositoryLockedFile
{
[Key]
public required string Path { get; set; }
public required AppUser LockDownUser { get; set; }
}

View File

@ -1,4 +1,5 @@
using Flawless.Server.Models;
using Flawless.Communication.Shared;
using Flawless.Server.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
@ -11,4 +12,17 @@ public class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
public DbSet<Repository> Repositories { get; set; }
public async ValueTask<(bool existed, bool authorized)> CheckRepositoryExistedAuthorizedAsync(
AppUser owner, string name, AppUser user, RepositoryRole role)
{
var r = await Repositories
.FirstOrDefaultAsync(r => r.Name == name && r.Owner == owner);
var existed = r != null;
var authorized = existed && (r!.Owner == owner || r.Members.Any(m => m.User == owner && m.Role >= role));
return (existed, authorized);
}
}

View File

@ -0,0 +1,66 @@
using Flawless.Abstraction;
using Flawless.Server.Models;
namespace Flawless.Server.Utility;
public enum RepoLocktableOptResult
{
Expand,
Success,
Already,
IsChild,
Rejected,
}
public static class RepoLocktableUtility
{
public static RepositoryLockedFile? GetFileLockByPath(List<RepositoryLockedFile> table, string testPath)
{
return table.FirstOrDefault(f => WorkPath.IsPathRelated(f.Path, testPath));
}
public static RepoLocktableOptResult AddFileLockOwner(List<RepositoryLockedFile> table, string testPath, AppUser owner)
{
var expanded = false;
foreach (var entry in table)
{
if (!WorkPath.IsPathRelated(entry.Path, testPath)) continue;
// Check is locked by others
if (entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected;
return testPath == entry.Path ? RepoLocktableOptResult.Already : RepoLocktableOptResult.IsChild;
}
// Check if includes can be locked
for (var i = 0; i < table.Count; i++)
{
var entry = table[i];
if (!WorkPath.IsPathRelated(testPath, entry.Path)) continue;
if (entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected;
table.RemoveAt(i);
i -= 1;
expanded = true;
}
table.Add(new RepositoryLockedFile { Path = testPath, LockDownUser = owner });
return expanded ? RepoLocktableOptResult.Expand : RepoLocktableOptResult.Success;
}
public static RepoLocktableOptResult RemoveFileLockOwner(List<RepositoryLockedFile> table, string testPath, AppUser? owner)
{
for (var i = 0; i < table.Count; i++)
{
var entry = table[i];
if (!WorkPath.IsPathRelated(entry.Path, testPath)) continue;
if (owner != null && entry.LockDownUser != owner) return RepoLocktableOptResult.Rejected;
if (testPath != entry.Path) return RepoLocktableOptResult.IsChild;
table.Remove(entry);
return RepoLocktableOptResult.Success;
}
return RepoLocktableOptResult.Rejected;
}
}