feat: Add locktable for lock\unlock files.
This commit is contained in:
parent
4a7c92a979
commit
437b5c6ab8
@ -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"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b5573ef9_002Db554_002D4a56_002D82c4_002D2531c8feef65/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" IsLocked="True" Name="PathValidationTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=f3f8a684_002Dc08e_002D489f_002D949c_002D6c38a1ed63b0/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="PathValidationTest #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
11
Flawless.Server/Models/RepositoryLockedFile.cs
Normal file
11
Flawless.Server/Models/RepositoryLockedFile.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
66
Flawless.Server/Utility/RepoLocktableUtility.cs
Normal file
66
Flawless.Server/Utility/RepoLocktableUtility.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user