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_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_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/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">
|
<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>
|
<TestAncestor>
|
||||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit</TestId>
|
||||||
</TestAncestor>
|
</TestAncestor>
|
||||||
</SessionState></s:String>
|
</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">
|
<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>
|
<TestAncestor>
|
||||||
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
<TestId>MSTest::5B1CB26D-99F5-491A-B368-7E3552FE67E9::net9.0::Flawless.Abstract.Test.WorkPathTestUnit.PathValidationTest</TestId>
|
||||||
</TestAncestor>
|
</TestAncestor>
|
||||||
</SessionState></s:String></wpf:ResourceDictionary>
|
</SessionState></s:String></wpf:ResourceDictionary>
|
||||||
@ -402,6 +402,16 @@ public static class WorkPath
|
|||||||
|
|
||||||
return null;
|
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)
|
private static void CombineInternal(StringBuilder sb, string addon)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -106,8 +106,26 @@ public class RepositoryControl(
|
|||||||
Commits = commit
|
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")]
|
[HttpGet("peek/commit")]
|
||||||
public async Task<IActionResult> PeekCommitAsync(string userName, string repositoryName)
|
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")]
|
[HttpPost("commit")]
|
||||||
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
|
public async Task<IActionResult> CommitAsync(string userName, string repositoryName, [FromForm] FormCommitRequest req)
|
||||||
{
|
{
|
||||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
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 (grantIssue is not Repository rp) return (IActionResult) grantIssue;
|
||||||
|
|
||||||
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
|
if (req.Depot != null ^ req.MainDepotId != null) return await CommitInternalAsync(rp, user, req);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Flawless.Server.Controllers;
|
||||||
|
|
||||||
namespace Flawless.Server.Models;
|
namespace Flawless.Server.Models;
|
||||||
|
|
||||||
@ -24,4 +25,6 @@ public class Repository
|
|||||||
public List<RepositoryCommit> Commits { get; set; } = new();
|
public List<RepositoryCommit> Commits { get; set; } = new();
|
||||||
|
|
||||||
public List<RepositoryDepot> Depots { 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;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -11,4 +12,17 @@ public class AppDbContext(DbContextOptions<AppDbContext> options)
|
|||||||
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
|
public DbSet<AppUserRefreshKey> RefreshTokens { get; set; }
|
||||||
|
|
||||||
public DbSet<Repository> Repositories { 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