Compare commits
No commits in common. "4d9e5a932644f0454462792b8a5656da6394abd6" and "8a2a7d15e270da18eb74f72511a59d70bc724246" have entirely different histories.
4d9e5a9326
...
8a2a7d15e2
@ -3,7 +3,7 @@
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:ursa="https://irihi.tech/ursa/themes/semi"
|
||||
x:Class="Flawless.Client.App"
|
||||
RequestedThemeVariant="Light">
|
||||
RequestedThemeVariant="Default">
|
||||
|
||||
<Application.Styles>
|
||||
<semi:SemiTheme Locale="zh-cn"/>
|
||||
|
||||
@ -21,9 +21,8 @@ public static class PathUtility
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo,
|
||||
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
|
||||
|
||||
public static string ConvertBytesToBestDisplay(long bytes)
|
||||
public static string ConvertBytesToBestDisplay(ulong bytes)
|
||||
{
|
||||
if (bytes < 0) return string.Empty;
|
||||
string[] units = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
|
||||
int unitIndex = 0;
|
||||
double size = bytes;
|
||||
|
||||
@ -124,16 +124,14 @@ public class LocalFileTreeAccessor
|
||||
if (!WorkPath.IsPathValid(workPath) || IgnoredDirectories.Any(d => workPath.StartsWith(d)))
|
||||
continue;
|
||||
|
||||
var inf = new FileInfo(f);
|
||||
_currentFiles.Add(workPath, new WorkspaceFile(inf.LastWriteTimeUtc, workPath, inf.Length));
|
||||
var modifyTime = File.GetLastWriteTimeUtc(f);
|
||||
_currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
|
||||
}
|
||||
|
||||
LastScanTimeUtc = DateTime.UtcNow;
|
||||
|
||||
// Find those are changed
|
||||
var changes = _currentFiles.Values.Where(v =>
|
||||
_baseline.TryGetValue(v.WorkPath, out var fi) &&
|
||||
(fi.ModifyTime != v.ModifyTime || fi.Size != v.Size));
|
||||
var changes = _currentFiles.Values.Where(v => _baseline.TryGetValue(v.WorkPath, out var fi) && fi.ModifyTime != v.ModifyTime);
|
||||
var news = _currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
|
||||
var removed = _baseline.Values.Where(v => !_currentFiles.ContainsKey(v.WorkPath));
|
||||
|
||||
|
||||
@ -955,9 +955,6 @@ namespace Flawless.Client.Remote
|
||||
|
||||
[JsonPropertyName("workPath")]
|
||||
public string WorkPath { get; set; }
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -156,7 +156,7 @@ public class RepositoryFileTreeAccessor : IDisposable, IAsyncDisposable, IEnumer
|
||||
public IEnumerator<WorkspaceFile> GetEnumerator()
|
||||
{
|
||||
return _cached.Values
|
||||
.Select(cv => new WorkspaceFile(cv.FileInfo.ModifyTime, cv.FileInfo.Path, (long)cv.FileInfo.Size)).GetEnumerator();
|
||||
.Select(cv => new WorkspaceFile(cv.FileInfo.ModifyTime, cv.FileInfo.Path)).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
|
||||
@ -1177,7 +1177,7 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return new(
|
||||
rsp.ManifestId,
|
||||
rsp.Depot.Select(x => new DepotLabel(x.Id, x.Length)).ToArray(),
|
||||
rsp.FilePaths.Select(x => new WorkspaceFile(x.ModifyTime.UtcDateTime, x.WorkPath, x.Size)).ToArray());
|
||||
rsp.FilePaths.Select(x => new WorkspaceFile(x.ModifyTime.UtcDateTime, x.WorkPath)).ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -1205,12 +1205,11 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<CommitManifest?> CommitWorkspaceAsync
|
||||
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message, bool forceBaseline = false)
|
||||
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
|
||||
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message)
|
||||
{
|
||||
// Check if current version is the latest
|
||||
var api = Api.C;
|
||||
if (!await UpdateCommitsHistoryFromServerAsync(repo)) return null;
|
||||
var requireUpdate = await IsPointedToCommitNotPeekResultFromServerAsync(repo);
|
||||
if (!requireUpdate.HasValue) return null;
|
||||
|
||||
@ -1222,13 +1221,11 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
|
||||
|
||||
var localDb = GetRepositoryLocalDatabase(repo);
|
||||
var (manifestList, miniManifestList) = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
|
||||
var snapshot = manifestList.Select(l => $"{l.ModifyTime.ToBinary()}${l.WorkPath}");
|
||||
|
||||
Guid commitId;
|
||||
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Renew for once.
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
@ -1236,64 +1233,33 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decide create new depot, create full depot or create addon depot.
|
||||
var hasAddOrModify = localDb.LocalAccessor.Changes.Any(x =>
|
||||
x.Value.Type == ChangeInfoType.Add || x.Value.Type == ChangeInfoType.Modify);
|
||||
var newDepot = forceBaseline || hasAddOrModify || localDb.RepoAccessor == null;
|
||||
// Generate depot
|
||||
var tempDepotPath = await CreateDepotIntoTempFileAsync(repo, manifestList);
|
||||
if (tempDepotPath == null) return null;
|
||||
|
||||
if (newDepot)
|
||||
// Upload and create commit
|
||||
await using var str = File.OpenRead(tempDepotPath);
|
||||
var snapshot = manifestList.Select(l => $"{l.ModifyTime.ToBinary()}${l.WorkPath}");
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
var changesSize = localDb.LocalAccessor.Changes.Values.Sum(x => (long) x.File.Size);
|
||||
var baselineSize = localDb.RepoAccessor?.Sum(x => (long) x.Size) ?? 0;
|
||||
var changesRatio = changesSize / (double) baselineSize;
|
||||
var isBaseline = forceBaseline || localDb.CurrentCommit == null || changesRatio > 0.8f;
|
||||
var depotFiles = isBaseline ? manifestList : miniManifestList;
|
||||
|
||||
// Generate depot
|
||||
var tempDepotPath = await CreateDepotIntoTempFileAsync(repo, depotFiles);
|
||||
if (tempDepotPath == null) return null;
|
||||
Console.WriteLine($"Create new {(isBaseline ? "baseline" : "overlay")} depot at \"{tempDepotPath}\"");
|
||||
|
||||
// Upload and create commit
|
||||
await using var str = File.OpenRead(tempDepotPath);
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
}
|
||||
|
||||
var requireDepot = isBaseline ? [] : new[] {repo.Commits.First().DepotId.ToString()};
|
||||
var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name,
|
||||
new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, requireDepot, "");
|
||||
|
||||
commitId = rsp.CommitId;
|
||||
|
||||
// Move depot file to destination
|
||||
var depotsPath = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
|
||||
var finalPath = Path.Combine(depotsPath, rsp.MainDepotId.ToString());
|
||||
Directory.CreateDirectory(depotsPath);
|
||||
File.Move(tempDepotPath, finalPath, true);
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Upload and create commit
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
}
|
||||
|
||||
var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name, null!,
|
||||
message, snapshot, [], repo.Commits.First().DepotId.ToString());
|
||||
|
||||
commitId = rsp.CommitId;
|
||||
}
|
||||
var rsp = await api.Gateway.CreateCommit(repo.OwnerName, repo.Name,
|
||||
new StreamPart(str, Path.GetFileName(tempDepotPath)), message, snapshot, [], string.Empty);
|
||||
|
||||
// Move depot file to destination
|
||||
var depotsPath = PathUtility.GetWorkspaceDepotCachePath(Api.Current.Username.Value!, repo.OwnerName, repo.Name);
|
||||
var finalPath = Path.Combine(depotsPath, rsp.MainDepotId.ToString());
|
||||
Directory.CreateDirectory(depotsPath);
|
||||
File.Move(tempDepotPath, finalPath, true);
|
||||
|
||||
// Fetch mapped manifest
|
||||
var manifest = await DownloadManifestFromServerAsync(repo, commitId);
|
||||
var manifest = await DownloadManifestFromServerAsync(repo, rsp.CommitId);
|
||||
if (manifest == null) return null;
|
||||
|
||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
|
||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, rsp.CommitId);
|
||||
if (accessor == null) return null; //todo this is a really fatal issue...
|
||||
if (localDb.RepoAccessor != null)
|
||||
{
|
||||
@ -1310,11 +1276,10 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
|
||||
// Point to latest state.
|
||||
localDb.RepoAccessor = accessor;
|
||||
localDb.CurrentCommit = commitId;
|
||||
localDb.CurrentCommit = rsp.CommitId;
|
||||
localDb.LocalAccessor.SetBaseline(accessor);
|
||||
SaveRepositoryLocalDatabaseChanges(repo);
|
||||
|
||||
Console.WriteLine($"Successful create commit as {commitId}");
|
||||
return manifest;
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -1325,12 +1290,11 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
}
|
||||
|
||||
private (List<WorkspaceFile>, List<WorkspaceFile>) CreateCommitManifestByCurrentBaselineAndChanges
|
||||
public List<WorkspaceFile> CreateCommitManifestByCurrentBaselineAndChanges
|
||||
(LocalFileTreeAccessor accessor, IEnumerable<ChangeInfo> changes, bool hard = false)
|
||||
{
|
||||
// Create a new depot file manifest.
|
||||
var files = accessor.BaselineFiles.Values.ToList();
|
||||
var overlayFsStatus = new List<WorkspaceFile>();
|
||||
foreach (var c in changes)
|
||||
{
|
||||
switch (c.Type)
|
||||
@ -1355,7 +1319,6 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
|
||||
files.Add(c.File);
|
||||
overlayFsStatus.Add(c.File);
|
||||
break;
|
||||
}
|
||||
case ChangeInfoType.Remove:
|
||||
@ -1386,13 +1349,12 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
|
||||
files[idx] = c.File;
|
||||
overlayFsStatus.Add(c.File);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (files, overlayFsStatus);
|
||||
return files;
|
||||
}
|
||||
|
||||
public async ValueTask<string?> CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable<WorkspaceFile> depotFiles)
|
||||
|
||||
@ -32,8 +32,6 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
[Reactive] private DateTime? _modifiedTime;
|
||||
|
||||
[Reactive] private LocalChangesNode? _parent;
|
||||
|
||||
[Reactive] private long _size;
|
||||
|
||||
public bool Included
|
||||
{
|
||||
@ -61,8 +59,7 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
Type = "Folder",
|
||||
FullPath = folderPath,
|
||||
Contents = new(),
|
||||
Parent = parent,
|
||||
Size = -1
|
||||
Parent = parent
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,8 +70,7 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
Type = file.Type.ToString(),
|
||||
FullPath = file.File.WorkPath,
|
||||
ModifiedTime = file.File.ModifyTime,
|
||||
Parent = parent,
|
||||
Size = (long) file.File.Size
|
||||
Parent = parent
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -183,7 +179,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
|
||||
new TextColumn<LocalChangesNode, string>(
|
||||
"Size",
|
||||
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
|
||||
n => PathUtility.ConvertBytesToBestDisplay(GetFileSize(n))),
|
||||
|
||||
new TextColumn<LocalChangesNode, DateTime?>(
|
||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
||||
@ -225,9 +221,9 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
"File Type",
|
||||
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
|
||||
|
||||
new TextColumn<LocalChangesNode, string>(
|
||||
new TextColumn<LocalChangesNode, ulong>(
|
||||
"Size",
|
||||
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
|
||||
n => 0),
|
||||
|
||||
new TextColumn<LocalChangesNode, DateTime?>(
|
||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
||||
@ -236,6 +232,13 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
|
||||
_ = StartupTasksAsync();
|
||||
}
|
||||
|
||||
private ulong GetFileSize(LocalChangesNode node)
|
||||
{
|
||||
if (!File.Exists(node.FullPath)) return 0;
|
||||
var path = Path.Combine(_wsRoot, node.FullPath);
|
||||
return (ulong)new FileInfo(path).Length;
|
||||
}
|
||||
|
||||
|
||||
private async Task StartupTasksAsync()
|
||||
@ -549,7 +552,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
}
|
||||
|
||||
using var l = UIHelper.MakeLoading("Committing changes...");
|
||||
var manifest = await RepositoryService.C.CommitWorkspaceAsync(Repository, changes, LocalDatabase.CommitMessage!);
|
||||
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
|
||||
if (manifest == null) return;
|
||||
|
||||
LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
|
||||
|
||||
@ -76,14 +76,6 @@ public partial class SettingViewModel : RoutableViewModelBase
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UIHelper.NotifyError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadClientSettings()
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
MinWidth="400"
|
||||
x:Class="Flawless.Client.Views.ModalBox.IssueDetailEditView">
|
||||
|
||||
<u:Form HorizontalAlignment="Stretch">
|
||||
<u:Form>
|
||||
<u:FormItem Label="Title" IsRequired="True">
|
||||
<TextBox Text="{Binding Title}"/>
|
||||
</u:FormItem>
|
||||
|
||||
@ -3,18 +3,11 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Flawless.Client.ViewModels"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
x:DataType="vm:RepositoryViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoFileTreePageView">
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" Margin="0, 6"
|
||||
HorizontalAlignment="Stretch" IsVisible="{Binding !IsDeveloperRole}">
|
||||
<u:IconButton
|
||||
Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
|
||||
Command="{Binding PullLatestRepositoryCommand}"/>
|
||||
</StackPanel>
|
||||
<TreeDataGrid Grid.Row="1" Source="{Binding FileTree}">
|
||||
<Grid ColumnDefinitions="2*, *">
|
||||
<TreeDataGrid Grid.Column="0" Grid.ColumnSpan="2" Source="{Binding FileTree}">
|
||||
</TreeDataGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@ -9,11 +9,15 @@
|
||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoIssuePageView">
|
||||
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="6">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0 0 0 12">
|
||||
<u:IconButton Icon="{StaticResource SemiIconRefresh}" Content="Refresh"
|
||||
Command="{Binding RefreshRepositoryIssuesAsyncCommand}"/>
|
||||
<u:IconButton Icon="{StaticResource SemiIconPlus}" Content="Create"
|
||||
Command="{Binding CreateIssueCommand}"/>
|
||||
<!-- <ComboBox ItemsSource="{Binding IssueFilters}" SelectedIndex="0" -->
|
||||
<!-- Width="120" PlaceholderText="筛选状态"/> -->
|
||||
<!-- <ComboBox ItemsSource="{Binding IssueTags}" SelectedIndex="0" -->
|
||||
<!-- Width="160" PlaceholderText="筛选标签"/> -->
|
||||
</StackPanel>
|
||||
|
||||
<ListBox ItemsSource="{Binding Repository.Issues}" SelectionMode="Single">
|
||||
|
||||
@ -21,15 +21,15 @@
|
||||
<TreeDataGrid Grid.Row="1" Grid.Column="0" Source="{Binding LocalChange}" CanUserSortColumns="True"/>
|
||||
<Border Grid.Row="1" Grid.Column="2" Classes="Shadow" Theme="{StaticResource CardBorder}">
|
||||
<StackPanel Spacing="8">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Stretch">
|
||||
<u:ElasticWrapPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
|
||||
<u:IconButton
|
||||
Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
|
||||
Command="{Binding PullLatestRepositoryCommand}"/>
|
||||
<u:IconButton
|
||||
Icon="{StaticResource SemiIconBackward}" Content="Revert" HorizontalAlignment="Stretch"
|
||||
Icon="{StaticResource SemiIconBackward}" Content="Revert Select" HorizontalAlignment="Stretch"
|
||||
Command="{Binding RevertSelectedChangesCommand}"/>
|
||||
</StackPanel>
|
||||
<u:Divider/>
|
||||
</u:ElasticWrapPanel>
|
||||
<u:Divider></u:Divider>
|
||||
<TextBox Text="{Binding LocalDatabase.CommitMessage}"
|
||||
Classes="TextArea" MaxHeight="300" Watermark="Description of this commit"/>
|
||||
<u:IconButton Content="Commit" Icon="{StaticResource SemiIconUpload}" HorizontalAlignment="Stretch"
|
||||
|
||||
@ -17,15 +17,15 @@
|
||||
<Label FontWeight="400" FontSize="28" Content="{Binding Repository.StandaloneName}"/>
|
||||
</StackPanel>
|
||||
<TabControl TabStripPlacement="Top" Margin="0 20 0 0">
|
||||
<!-- <TabItem> -->
|
||||
<!-- <TabItem.Header> -->
|
||||
<!-- <StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"> -->
|
||||
<!-- <PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconBox}"/> -->
|
||||
<!-- <Label Content="Dashboard"/> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </TabItem.Header> -->
|
||||
<!-- <page:RepoDashboardPageView/> -->
|
||||
<!-- </TabItem> -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<PathIcon MaxHeight="14" MaxWidth="14" Data="{StaticResource SemiIconBox}"/>
|
||||
<Label Content="Dashboard"/>
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<page:RepoDashboardPageView/>
|
||||
</TabItem>
|
||||
<TabItem IsVisible="{Binding IsDeveloperRole}">
|
||||
<TabItem.Header>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
|
||||
@ -42,16 +42,16 @@
|
||||
<u:FormItem Label="Default Storage Location">
|
||||
<u:PathPicker/>
|
||||
</u:FormItem>
|
||||
<!-- <u:FormGroup Header="Workspace Refresh"> -->
|
||||
<!-- <StackPanel Spacing="4"> -->
|
||||
<!-- <CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace" -->
|
||||
<!-- IsChecked="{Binding SettingModel.RefreshWorkspaceOnOpen}"/> -->
|
||||
<!-- <CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed" -->
|
||||
<!-- IsChecked="{Binding SettingModel.RefreshWorkspaceOnFilesystemChanges}"/> -->
|
||||
<!-- </StackPanel> -->
|
||||
<!-- </u:FormGroup> -->
|
||||
<u:FormGroup Header="Workspace Refresh">
|
||||
<StackPanel Spacing="4">
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Open Workspace"
|
||||
IsChecked="{Binding SettingModel.RefreshWorkspaceOnOpen}"/>
|
||||
<CheckBox Theme="{StaticResource CardCheckBox}" Content="Filesystem Changed"
|
||||
IsChecked="{Binding SettingModel.RefreshWorkspaceOnFilesystemChanges}"/>
|
||||
</StackPanel>
|
||||
</u:FormGroup>
|
||||
<u:FormGroup Header="Extern Tools">
|
||||
<!-- <u:PathPicker u:FormItem.Label="Open Folder"/> -->
|
||||
<u:PathPicker u:FormItem.Label="Open Folder"/>
|
||||
<u:PathPicker u:FormItem.Label="Diff Tool"/>
|
||||
</u:FormGroup>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
@ -84,11 +84,11 @@
|
||||
<Grid RowDefinitions="Auto, *" Margin="10">
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Content="Refresh"
|
||||
<Button Content="Refresh Users"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CreateUserCommand}"/>
|
||||
<Button Content="Add"
|
||||
<Button Content="Add Users"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CreateUserCommand}"/>
|
||||
|
||||
@ -6,14 +6,11 @@ public struct WorkspaceFile : IEquatable<WorkspaceFile>
|
||||
public DateTime ModifyTime { get; set; }
|
||||
|
||||
public string WorkPath { get; set; }
|
||||
|
||||
public ulong Size { get; set; }
|
||||
|
||||
public WorkspaceFile(DateTime modifyTime, string workPath, long size)
|
||||
public WorkspaceFile(DateTime modifyTime, string workPath)
|
||||
{
|
||||
ModifyTime = modifyTime;
|
||||
WorkPath = workPath;
|
||||
Size = (ulong) size;
|
||||
}
|
||||
|
||||
public bool Equals(WorkspaceFile other)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Flawless.Communication.Request;
|
||||
using System.Net;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
@ -9,26 +10,15 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Authorize, Route("api/admin")]
|
||||
[ApiController, Authorize(Roles = "admin"), Route("api/admin")]
|
||||
public class AdminController(
|
||||
UserManager<AppUser> userManager,
|
||||
AccessControlService accessControlService,
|
||||
AppDbContext dbContext) : ControllerBase
|
||||
{
|
||||
|
||||
private async ValueTask<ActionResult?> TestIfValid()
|
||||
{
|
||||
var user = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
if (user.Admin == false) return BadRequest(new FailedResponse("Only admin can do this!"));
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost("superuser/{username}")]
|
||||
public async Task<IActionResult> SetSuperuserAsync(string username, bool toSuper)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
var optUser = (await userManager.GetUserAsync(HttpContext.User))!;
|
||||
|
||||
@ -44,31 +34,15 @@ public class AdminController(
|
||||
[HttpGet("superuser/{username}")]
|
||||
public async Task<ActionResult<bool>> GetSuperuserAsync(string username)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
|
||||
return user.Admin;
|
||||
}
|
||||
|
||||
[HttpGet("user/list")]
|
||||
public async Task<ActionResult<List<AppUser>>> GetUsersAsync()
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var users = await userManager.Users.ToListAsync();
|
||||
return users;
|
||||
}
|
||||
|
||||
[HttpPost("user/delete/{username}")]
|
||||
public async Task<IActionResult> DeleteUserAsync(string username)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
@ -93,9 +67,6 @@ public class AdminController(
|
||||
[HttpPost("user/disable/{username}")]
|
||||
public async Task<IActionResult> DisableUserAsync(string username)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var user = await userManager.FindByNameAsync(username);
|
||||
|
||||
if (user == null) return BadRequest(new FailedResponse("User does not exist!"));
|
||||
@ -108,9 +79,6 @@ public class AdminController(
|
||||
[HttpPost("user/reset_password")]
|
||||
public async Task<IActionResult> ResetPasswordAsync(ResetPasswordRequest r)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
if (r.Identity == null) return BadRequest(new FailedResponse("Identity (User Id) is not set!"));
|
||||
var user = await userManager.FindByIdAsync(r.Identity);
|
||||
|
||||
@ -125,9 +93,6 @@ public class AdminController(
|
||||
[HttpPost("access_control/ip_whitelist")]
|
||||
public async Task<IActionResult> SetIpWhitelistAsync([FromBody] string[] ips)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
await accessControlService.UpdatePolicyAsync(IpPolicyType.Whitelist, ips);
|
||||
return Ok();
|
||||
}
|
||||
@ -135,18 +100,12 @@ public class AdminController(
|
||||
[HttpGet("access_control/ip_whitelist")]
|
||||
public async Task<ActionResult<IEnumerable<string>>> GetIpWhitelistAsync()
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
return Ok(await accessControlService.GetIpListAsync(IpPolicyType.Whitelist));
|
||||
}
|
||||
|
||||
[HttpPost("access_control/ip_blacklist")]
|
||||
public async Task<IActionResult> SetIpBlacklistAsync([FromBody] string[] ips)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
await accessControlService.UpdatePolicyAsync(IpPolicyType.Blacklist, ips);
|
||||
return Ok();
|
||||
}
|
||||
@ -154,9 +113,6 @@ public class AdminController(
|
||||
[HttpGet("access_control/ip_blacklist")]
|
||||
public async Task<ActionResult<IEnumerable<string>>> GetIpBlacklistAsync()
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
return Ok(await accessControlService.GetIpListAsync(IpPolicyType.Blacklist));
|
||||
}
|
||||
|
||||
@ -168,9 +124,6 @@ public class AdminController(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50)
|
||||
{
|
||||
var t = await TestIfValid();
|
||||
if (t != null) return t;
|
||||
|
||||
var query = dbContext.SystemLogs.AsQueryable();
|
||||
|
||||
// 时间过滤
|
||||
|
||||
@ -410,11 +410,11 @@ public class RepositoryInnieController(
|
||||
var dateTimeStr = s.Substring(0, idx);
|
||||
var pathStr = s.Substring(idx + 1);
|
||||
|
||||
return new WorkspaceFile(DateTime.FromBinary(long.Parse(dateTimeStr)), pathStr, 0);
|
||||
return new WorkspaceFile(DateTime.FromBinary(long.Parse(dateTimeStr)), pathStr);
|
||||
}).ToArray();
|
||||
|
||||
var sizeMap = new Dictionary<WorkspaceFile, long>();
|
||||
var test = new HashSet<WorkspaceFile>(actualFs);
|
||||
var createNewDepot = false;
|
||||
RepositoryDepot mainDepot;
|
||||
List<DepotLabel> depotLabels = new();
|
||||
|
||||
@ -423,7 +423,7 @@ public class RepositoryInnieController(
|
||||
{
|
||||
// Use existed depots
|
||||
var mainDepotId = Guid.Parse(req.MainDepotId!);
|
||||
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test, sizeMap, null);
|
||||
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test);
|
||||
|
||||
if (preDepot == null)
|
||||
return BadRequest(new FailedResponse("Not a valid main depot!", "NoMainDepot"));
|
||||
@ -441,14 +441,14 @@ public class RepositoryInnieController(
|
||||
{
|
||||
// Get a new depot from request - this will flat to direct depot reference to avoid deep search that
|
||||
// raise memory issue.
|
||||
var directDependDepots = new HashSet<Guid>();
|
||||
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, sizeMap);
|
||||
await StencilWorkspaceSnapshotAsync(cacheStream, test);
|
||||
|
||||
// Oh no, will we need to load their parents?
|
||||
var unresolvedCount = test.Count;
|
||||
@ -457,13 +457,21 @@ public class RepositoryInnieController(
|
||||
// Yes
|
||||
foreach (var subDepot in req.RequiredDepots?.Select(Guid.Parse) ?? [])
|
||||
{
|
||||
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test, sizeMap, directDependDepots);
|
||||
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test);
|
||||
|
||||
var rest = test.Count;
|
||||
if (rest == 0)
|
||||
{
|
||||
actualRequiredDepots.Add(subDepot);
|
||||
break;
|
||||
}
|
||||
|
||||
// Quit if nothing to do.
|
||||
unresolvedCount = rest;
|
||||
if (rest == 0) break;
|
||||
// If test is changed?
|
||||
if (unresolvedCount != rest)
|
||||
{
|
||||
actualRequiredDepots.Add(subDepot);
|
||||
unresolvedCount = rest;
|
||||
}
|
||||
}
|
||||
|
||||
// If still not able to resolve
|
||||
@ -481,7 +489,7 @@ public class RepositoryInnieController(
|
||||
await dbContext.Repositories
|
||||
.Where(r => r.Id == rp.Id)
|
||||
.SelectMany(r => r.Depots)
|
||||
.Where(cm => directDependDepots.Contains(cm.DepotId))
|
||||
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
||||
.ToArrayAsync());
|
||||
|
||||
// Create commit and let it alloc a main key
|
||||
@ -502,14 +510,6 @@ public class RepositoryInnieController(
|
||||
depotLabels.AddRange(mainDepot.Dependencies.Select(d => new DepotLabel(d.DepotId, d.Length)));
|
||||
|
||||
}
|
||||
|
||||
// Update size info of FilePaths
|
||||
for (var i = 0; i < actualFs.Length; i++)
|
||||
{
|
||||
var ws = actualFs[i];
|
||||
ws.Size = (ulong)sizeMap.GetValueOrDefault(ws, 0);
|
||||
actualFs[i] = ws;
|
||||
}
|
||||
|
||||
// Create commit info
|
||||
var commit = new RepositoryCommit
|
||||
@ -549,8 +549,7 @@ public class RepositoryInnieController(
|
||||
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id, mainDepot.DepotId));
|
||||
}
|
||||
|
||||
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream,
|
||||
HashSet<WorkspaceFile> unresolved, Dictionary<WorkspaceFile, long> sizeMap)
|
||||
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
|
||||
{
|
||||
// Get version
|
||||
depotStream.Seek(0, SeekOrigin.Begin); // Fix: Get an invalid version code due to offset is bad.
|
||||
@ -569,21 +568,14 @@ public class RepositoryInnieController(
|
||||
{
|
||||
ModifyTime = inf.ModifyTime,
|
||||
WorkPath = inf.Path,
|
||||
Size = 0
|
||||
};
|
||||
|
||||
if (!sizeMap.TryAdd(test, (long)inf.Size)) sizeMap[test] = (long)inf.Size;
|
||||
|
||||
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,
|
||||
Dictionary<WorkspaceFile, long> sizeMap,
|
||||
HashSet<Guid>? directDependDepots)
|
||||
private async Task<RepositoryDepot?> StencilWorkspaceSnapshotViaDatabaseAsync(Guid rpId, Guid depotId, HashSet<WorkspaceFile> unresolved)
|
||||
{
|
||||
var depot = await dbContext.Repositories
|
||||
.SelectMany(r => r.Depots)
|
||||
@ -591,28 +583,21 @@ public class RepositoryInnieController(
|
||||
.ThenInclude(repositoryDepot => repositoryDepot.Dependencies)
|
||||
.FirstOrDefaultAsync(r => r.DepotId == depotId);
|
||||
|
||||
await RecurringAsync(transformer, rpId, depot, unresolved, sizeMap, directDependDepots);
|
||||
await RecurringAsync(transformer, rpId, depot, unresolved);
|
||||
return depot;
|
||||
|
||||
static async ValueTask RecurringAsync(
|
||||
PathTransformer transformer, Guid rpId,
|
||||
RepositoryDepot? depot, HashSet<WorkspaceFile> unresolved,
|
||||
Dictionary<WorkspaceFile, long> sizeMap,
|
||||
HashSet<Guid>? directDependDepots)
|
||||
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)))
|
||||
{
|
||||
var before = unresolved.Count;
|
||||
await StencilWorkspaceSnapshotAsync(fs, unresolved, sizeMap);
|
||||
if (before != unresolved.Count && directDependDepots != null) directDependDepots.Add(depot.DepotId);
|
||||
}
|
||||
await StencilWorkspaceSnapshotAsync(fs, unresolved);
|
||||
|
||||
for (var i = 0; i < depot.Dependencies.Count && unresolved.Count > 0; i++)
|
||||
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved, sizeMap, directDependDepots);
|
||||
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ public class RepositoryOutieController(AppDbContext dbContext, UserManager<AppUs
|
||||
}
|
||||
|
||||
[HttpPost("repo_create")]
|
||||
public async Task<ActionResult<RepositoryInfoResponse>> CreateRepositoryAsync([FromQuery] string repositoryName, [FromQuery] string? description)
|
||||
public async Task<ActionResult<RepositoryInfoResponse>> CreateRepositoryAsync([FromQuery] string repositoryName, [FromQuery] string description)
|
||||
{
|
||||
repositoryName = repositoryName.Trim().Replace(' ', '_');
|
||||
if (repositoryName.Length <= 3)
|
||||
|
||||
@ -189,6 +189,7 @@ public static class Program
|
||||
|
||||
if (auth?.Any() ?? false)
|
||||
{
|
||||
var adminOnly = auth.Any(a => a.Policy?.ToLower() == "admin");
|
||||
var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
|
||||
|
||||
@ -200,6 +201,8 @@ public static class Program
|
||||
|
||||
var u = await db.FindByIdAsync(id!);
|
||||
if (u == null) throw new SecurityTokenExpiredException("User is not existed.");
|
||||
if (adminOnly && u.Admin == false)
|
||||
throw new SecurityException("This api is Admin called only!");
|
||||
|
||||
if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
|
||||
// if (u.LockoutEnabled) throw new SecurityTokenExpiredException("User has been locked."); //todo Fix lockout prob
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"ConnectionStrings": {
|
||||
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Database=flawless"
|
||||
},
|
||||
"LocalStoragePath": "/Users/cardidi/flawless-data",
|
||||
"LocalStoragePath": "./data/development",
|
||||
"User": {
|
||||
"PublicRegister": true
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"ConnectionStrings": {
|
||||
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Database=flawless"
|
||||
},
|
||||
"LocalStoragePath": "/Users/cardidi/flawless-data",
|
||||
"LocalStoragePath": "./data/development",
|
||||
"User": {
|
||||
"PublicRegister": true
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user