Compare commits
7 Commits
8a2a7d15e2
...
4d9e5a9326
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d9e5a9326 | |||
| 89ed49ab93 | |||
| c63a2506d2 | |||
| c0d22d3959 | |||
| 33d1a4bf49 | |||
| da95083ebb | |||
| d9fec8e595 |
@ -3,7 +3,7 @@
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:ursa="https://irihi.tech/ursa/themes/semi"
|
||||
x:Class="Flawless.Client.App"
|
||||
RequestedThemeVariant="Default">
|
||||
RequestedThemeVariant="Light">
|
||||
|
||||
<Application.Styles>
|
||||
<semi:SemiTheme Locale="zh-cn"/>
|
||||
|
||||
@ -21,8 +21,9 @@ public static class PathUtility
|
||||
=> Path.Combine(SettingService.C.AppSetting.RepositoryPath, login, owner, repo,
|
||||
AppDefaultValues.RepoLocalStorageManagerFolder, AppDefaultValues.RepoLocalStorageDepotFolder);
|
||||
|
||||
public static string ConvertBytesToBestDisplay(ulong bytes)
|
||||
public static string ConvertBytesToBestDisplay(long 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,14 +124,16 @@ public class LocalFileTreeAccessor
|
||||
if (!WorkPath.IsPathValid(workPath) || IgnoredDirectories.Any(d => workPath.StartsWith(d)))
|
||||
continue;
|
||||
|
||||
var modifyTime = File.GetLastWriteTimeUtc(f);
|
||||
_currentFiles.Add(workPath, new WorkspaceFile { WorkPath = workPath, ModifyTime = modifyTime });
|
||||
var inf = new FileInfo(f);
|
||||
_currentFiles.Add(workPath, new WorkspaceFile(inf.LastWriteTimeUtc, workPath, inf.Length));
|
||||
}
|
||||
|
||||
LastScanTimeUtc = DateTime.UtcNow;
|
||||
|
||||
// Find those are changed
|
||||
var changes = _currentFiles.Values.Where(v => _baseline.TryGetValue(v.WorkPath, out var fi) && fi.ModifyTime != v.ModifyTime);
|
||||
var changes = _currentFiles.Values.Where(v =>
|
||||
_baseline.TryGetValue(v.WorkPath, out var fi) &&
|
||||
(fi.ModifyTime != v.ModifyTime || fi.Size != v.Size));
|
||||
var news = _currentFiles.Values.Where(v => !_baseline.ContainsKey(v.WorkPath));
|
||||
var removed = _baseline.Values.Where(v => !_currentFiles.ContainsKey(v.WorkPath));
|
||||
|
||||
|
||||
@ -955,6 +955,9 @@ 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)).GetEnumerator();
|
||||
.Select(cv => new WorkspaceFile(cv.FileInfo.ModifyTime, cv.FileInfo.Path, (long)cv.FileInfo.Size)).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)).ToArray());
|
||||
rsp.FilePaths.Select(x => new WorkspaceFile(x.ModifyTime.UtcDateTime, x.WorkPath, x.Size)).ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -1205,11 +1205,12 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return true;
|
||||
}
|
||||
|
||||
public async ValueTask<CommitManifest?> CommitWorkspaceAsBaselineAsync
|
||||
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message)
|
||||
public async ValueTask<CommitManifest?> CommitWorkspaceAsync
|
||||
(RepositoryModel repo, IEnumerable<ChangeInfo> changes, string message, bool forceBaseline = false)
|
||||
{
|
||||
// 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;
|
||||
|
||||
@ -1221,11 +1222,13 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
|
||||
|
||||
var localDb = GetRepositoryLocalDatabase(repo);
|
||||
var manifestList = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
|
||||
var (manifestList, miniManifestList) = CreateCommitManifestByCurrentBaselineAndChanges(localDb.LocalAccessor, changes);
|
||||
var snapshot = manifestList.Select(l => $"{l.ModifyTime.ToBinary()}${l.WorkPath}");
|
||||
|
||||
Guid commitId;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Renew for once.
|
||||
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
||||
{
|
||||
@ -1233,33 +1236,64 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate depot
|
||||
var tempDepotPath = await CreateDepotIntoTempFileAsync(repo, manifestList);
|
||||
if (tempDepotPath == null) 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;
|
||||
|
||||
// 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()))
|
||||
if (newDepot)
|
||||
{
|
||||
api.ClearGateway();
|
||||
return null;
|
||||
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);
|
||||
}
|
||||
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, rsp.CommitId);
|
||||
var manifest = await DownloadManifestFromServerAsync(repo, commitId);
|
||||
if (manifest == null) return null;
|
||||
|
||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, rsp.CommitId);
|
||||
var accessor = await DownloadDepotsAndUseLocalCachesToGenerateRepositoryFileTreeAccessorFromServerAsync(repo, commitId);
|
||||
if (accessor == null) return null; //todo this is a really fatal issue...
|
||||
if (localDb.RepoAccessor != null)
|
||||
{
|
||||
@ -1276,10 +1310,11 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
|
||||
// Point to latest state.
|
||||
localDb.RepoAccessor = accessor;
|
||||
localDb.CurrentCommit = rsp.CommitId;
|
||||
localDb.CurrentCommit = commitId;
|
||||
localDb.LocalAccessor.SetBaseline(accessor);
|
||||
SaveRepositoryLocalDatabaseChanges(repo);
|
||||
|
||||
Console.WriteLine($"Successful create commit as {commitId}");
|
||||
return manifest;
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -1290,11 +1325,12 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
}
|
||||
|
||||
public List<WorkspaceFile> CreateCommitManifestByCurrentBaselineAndChanges
|
||||
private (List<WorkspaceFile>, 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)
|
||||
@ -1319,6 +1355,7 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
|
||||
files.Add(c.File);
|
||||
overlayFsStatus.Add(c.File);
|
||||
break;
|
||||
}
|
||||
case ChangeInfoType.Remove:
|
||||
@ -1349,12 +1386,13 @@ public class RepositoryService : BaseService<RepositoryService>
|
||||
}
|
||||
|
||||
files[idx] = c.File;
|
||||
overlayFsStatus.Add(c.File);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
return (files, overlayFsStatus);
|
||||
}
|
||||
|
||||
public async ValueTask<string?> CreateDepotIntoTempFileAsync(RepositoryModel repo, IEnumerable<WorkspaceFile> depotFiles)
|
||||
|
||||
@ -32,6 +32,8 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
[Reactive] private DateTime? _modifiedTime;
|
||||
|
||||
[Reactive] private LocalChangesNode? _parent;
|
||||
|
||||
[Reactive] private long _size;
|
||||
|
||||
public bool Included
|
||||
{
|
||||
@ -59,7 +61,8 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
Type = "Folder",
|
||||
FullPath = folderPath,
|
||||
Contents = new(),
|
||||
Parent = parent
|
||||
Parent = parent,
|
||||
Size = -1
|
||||
};
|
||||
}
|
||||
|
||||
@ -70,7 +73,8 @@ public partial class LocalChangesNode : ReactiveModel
|
||||
Type = file.Type.ToString(),
|
||||
FullPath = file.File.WorkPath,
|
||||
ModifiedTime = file.File.ModifyTime,
|
||||
Parent = parent
|
||||
Parent = parent,
|
||||
Size = (long) file.File.Size
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -179,7 +183,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
|
||||
new TextColumn<LocalChangesNode, string>(
|
||||
"Size",
|
||||
n => PathUtility.ConvertBytesToBestDisplay(GetFileSize(n))),
|
||||
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
|
||||
|
||||
new TextColumn<LocalChangesNode, DateTime?>(
|
||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
||||
@ -221,9 +225,9 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
"File Type",
|
||||
n => n.Contents != null ? "Folder" : Path.GetExtension(n.FullPath)),
|
||||
|
||||
new TextColumn<LocalChangesNode, ulong>(
|
||||
new TextColumn<LocalChangesNode, string>(
|
||||
"Size",
|
||||
n => 0),
|
||||
n => PathUtility.ConvertBytesToBestDisplay(n.Size)),
|
||||
|
||||
new TextColumn<LocalChangesNode, DateTime?>(
|
||||
"ModifiedTime", n => n.ModifiedTime.HasValue ? n.ModifiedTime.Value.ToLocalTime() : null),
|
||||
@ -232,13 +236,6 @@ 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()
|
||||
@ -552,7 +549,7 @@ public partial class RepositoryViewModel : RoutableViewModelBase
|
||||
}
|
||||
|
||||
using var l = UIHelper.MakeLoading("Committing changes...");
|
||||
var manifest = await RepositoryService.C.CommitWorkspaceAsBaselineAsync(Repository, changes, LocalDatabase.CommitMessage!);
|
||||
var manifest = await RepositoryService.C.CommitWorkspaceAsync(Repository, changes, LocalDatabase.CommitMessage!);
|
||||
if (manifest == null) return;
|
||||
|
||||
LocalDatabase.LocalAccessor.SetBaseline(manifest.Value.FilePaths);
|
||||
|
||||
@ -76,6 +76,14 @@ 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>
|
||||
<u:Form HorizontalAlignment="Stretch">
|
||||
<u:FormItem Label="Title" IsRequired="True">
|
||||
<TextBox Text="{Binding Title}"/>
|
||||
</u:FormItem>
|
||||
|
||||
@ -3,11 +3,18 @@
|
||||
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 ColumnDefinitions="2*, *">
|
||||
<TreeDataGrid Grid.Column="0" Grid.ColumnSpan="2" Source="{Binding FileTree}">
|
||||
<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}">
|
||||
</TreeDataGrid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@ -9,15 +9,11 @@
|
||||
x:Class="Flawless.Client.Views.RepositoryPage.RepoIssuePageView">
|
||||
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0 0 0 12">
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="6">
|
||||
<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">
|
||||
<u:ElasticWrapPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" HorizontalAlignment="Stretch">
|
||||
<u:IconButton
|
||||
Icon="{StaticResource SemiIconDownload}" Content="Pull" HorizontalAlignment="Stretch"
|
||||
Command="{Binding PullLatestRepositoryCommand}"/>
|
||||
<u:IconButton
|
||||
Icon="{StaticResource SemiIconBackward}" Content="Revert Select" HorizontalAlignment="Stretch"
|
||||
Icon="{StaticResource SemiIconBackward}" Content="Revert" HorizontalAlignment="Stretch"
|
||||
Command="{Binding RevertSelectedChangesCommand}"/>
|
||||
</u:ElasticWrapPanel>
|
||||
<u:Divider></u:Divider>
|
||||
</StackPanel>
|
||||
<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 Users"
|
||||
<Button Content="Refresh"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CreateUserCommand}"/>
|
||||
<Button Content="Add Users"
|
||||
<Button Content="Add"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding CreateUserCommand}"/>
|
||||
|
||||
@ -6,11 +6,14 @@ 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)
|
||||
public WorkspaceFile(DateTime modifyTime, string workPath, long size)
|
||||
{
|
||||
ModifyTime = modifyTime;
|
||||
WorkPath = workPath;
|
||||
Size = (ulong) size;
|
||||
}
|
||||
|
||||
public bool Equals(WorkspaceFile other)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Request;
|
||||
using Flawless.Communication.Response;
|
||||
using Flawless.Server.Models;
|
||||
using Flawless.Server.Services;
|
||||
@ -10,15 +9,26 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Flawless.Server.Controllers;
|
||||
|
||||
[ApiController, Authorize(Roles = "admin"), Route("api/admin")]
|
||||
[ApiController, Authorize, 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))!;
|
||||
|
||||
@ -34,15 +44,31 @@ 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!"));
|
||||
@ -67,6 +93,9 @@ 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!"));
|
||||
@ -79,6 +108,9 @@ 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);
|
||||
|
||||
@ -93,6 +125,9 @@ 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();
|
||||
}
|
||||
@ -100,12 +135,18 @@ 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();
|
||||
}
|
||||
@ -113,6 +154,9 @@ 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));
|
||||
}
|
||||
|
||||
@ -124,6 +168,9 @@ 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);
|
||||
return new WorkspaceFile(DateTime.FromBinary(long.Parse(dateTimeStr)), pathStr, 0);
|
||||
}).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);
|
||||
var preDepot = await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, mainDepotId, test, sizeMap, null);
|
||||
|
||||
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 actualRequiredDepots = new HashSet<Guid>();
|
||||
var directDependDepots = 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);
|
||||
await StencilWorkspaceSnapshotAsync(cacheStream, test, sizeMap);
|
||||
|
||||
// Oh no, will we need to load their parents?
|
||||
var unresolvedCount = test.Count;
|
||||
@ -457,21 +457,13 @@ public class RepositoryInnieController(
|
||||
// Yes
|
||||
foreach (var subDepot in req.RequiredDepots?.Select(Guid.Parse) ?? [])
|
||||
{
|
||||
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test);
|
||||
await StencilWorkspaceSnapshotViaDatabaseAsync(rp.Id, subDepot, test, sizeMap, directDependDepots);
|
||||
|
||||
var rest = test.Count;
|
||||
if (rest == 0)
|
||||
{
|
||||
actualRequiredDepots.Add(subDepot);
|
||||
break;
|
||||
}
|
||||
|
||||
// If test is changed?
|
||||
if (unresolvedCount != rest)
|
||||
{
|
||||
actualRequiredDepots.Add(subDepot);
|
||||
unresolvedCount = rest;
|
||||
}
|
||||
// Quit if nothing to do.
|
||||
unresolvedCount = rest;
|
||||
if (rest == 0) break;
|
||||
}
|
||||
|
||||
// If still not able to resolve
|
||||
@ -489,7 +481,7 @@ public class RepositoryInnieController(
|
||||
await dbContext.Repositories
|
||||
.Where(r => r.Id == rp.Id)
|
||||
.SelectMany(r => r.Depots)
|
||||
.Where(cm => actualRequiredDepots.Contains(cm.DepotId))
|
||||
.Where(cm => directDependDepots.Contains(cm.DepotId))
|
||||
.ToArrayAsync());
|
||||
|
||||
// Create commit and let it alloc a main key
|
||||
@ -510,6 +502,14 @@ 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,7 +549,8 @@ public class RepositoryInnieController(
|
||||
return Ok(new CommitSuccessResponse(commit.CommittedOn, commit.Id, mainDepot.DepotId));
|
||||
}
|
||||
|
||||
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream, HashSet<WorkspaceFile> unresolved)
|
||||
private static async ValueTask StencilWorkspaceSnapshotAsync(Stream depotStream,
|
||||
HashSet<WorkspaceFile> unresolved, Dictionary<WorkspaceFile, long> sizeMap)
|
||||
{
|
||||
// Get version
|
||||
depotStream.Seek(0, SeekOrigin.Begin); // Fix: Get an invalid version code due to offset is bad.
|
||||
@ -568,14 +569,21 @@ 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)
|
||||
private async Task<RepositoryDepot?> StencilWorkspaceSnapshotViaDatabaseAsync(
|
||||
Guid rpId, Guid depotId,
|
||||
HashSet<WorkspaceFile> unresolved,
|
||||
Dictionary<WorkspaceFile, long> sizeMap,
|
||||
HashSet<Guid>? directDependDepots)
|
||||
{
|
||||
var depot = await dbContext.Repositories
|
||||
.SelectMany(r => r.Depots)
|
||||
@ -583,21 +591,28 @@ public class RepositoryInnieController(
|
||||
.ThenInclude(repositoryDepot => repositoryDepot.Dependencies)
|
||||
.FirstOrDefaultAsync(r => r.DepotId == depotId);
|
||||
|
||||
await RecurringAsync(transformer, rpId, depot, unresolved);
|
||||
await RecurringAsync(transformer, rpId, depot, unresolved, sizeMap, directDependDepots);
|
||||
return depot;
|
||||
|
||||
static async ValueTask RecurringAsync(PathTransformer transformer, Guid rpId, RepositoryDepot? depot, HashSet<WorkspaceFile> unresolved)
|
||||
static async ValueTask RecurringAsync(
|
||||
PathTransformer transformer, Guid rpId,
|
||||
RepositoryDepot? depot, HashSet<WorkspaceFile> unresolved,
|
||||
Dictionary<WorkspaceFile, long> sizeMap,
|
||||
HashSet<Guid>? directDependDepots)
|
||||
{
|
||||
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);
|
||||
{
|
||||
var before = unresolved.Count;
|
||||
await StencilWorkspaceSnapshotAsync(fs, unresolved, sizeMap);
|
||||
if (before != unresolved.Count && directDependDepots != null) directDependDepots.Add(depot.DepotId);
|
||||
}
|
||||
|
||||
for (var i = 0; i < depot.Dependencies.Count && unresolved.Count > 0; i++)
|
||||
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved);
|
||||
|
||||
await RecurringAsync(transformer, rpId, depot.Dependencies[i], unresolved, sizeMap, directDependDepots);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,7 +189,6 @@ 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!");
|
||||
|
||||
@ -201,8 +200,6 @@ 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": "./data/development",
|
||||
"LocalStoragePath": "/Users/cardidi/flawless-data",
|
||||
"User": {
|
||||
"PublicRegister": true
|
||||
},
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"ConnectionStrings": {
|
||||
"CoreDb": "Server=localhost;Port=5432;User Id=postgres;Database=flawless"
|
||||
},
|
||||
"LocalStoragePath": "./data/development",
|
||||
"LocalStoragePath": "/Users/cardidi/flawless-data",
|
||||
"User": {
|
||||
"PublicRegister": true
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user