582 lines
19 KiB
C#
582 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Flawless.Abstraction;
|
|
using Flawless.Client.Models;
|
|
using Flawless.Core.Modal;
|
|
using Newtonsoft.Json;
|
|
using ValueTaskSupplement;
|
|
|
|
namespace Flawless.Client.Service;
|
|
|
|
public class RepositoryService : BaseService<RepositoryService>
|
|
{
|
|
public ObservableCollection<RepositoryModel> Repositories => _repositories;
|
|
|
|
private readonly ObservableCollection<RepositoryModel> _repositories = new();
|
|
|
|
private readonly Dictionary<RepositoryModel, RepositoryLocalDatabaseModel> _localRepoDbModel = new();
|
|
|
|
private readonly HashSet<RepositoryModel> _openedRepos = new();
|
|
|
|
private bool TryCreateRepositoryBaseStorageStructure(RepositoryModel repo)
|
|
{
|
|
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
|
if (File.Exists(dbPath)) return false;
|
|
|
|
// Get directories
|
|
var localRepoDb = GetRepositoryLocalDatabase(repo);
|
|
var folderPath = PathUtility.GetWorkspaceManagerPath(repo.OwnerName, repo.Name);
|
|
|
|
// Create initial data.
|
|
Directory.CreateDirectory(folderPath);
|
|
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Create));
|
|
JsonSerializer.CreateDefault().Serialize(writeFs, localRepoDb);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
public bool SaveRepositoryLocalDatabaseChanges(RepositoryModel repo)
|
|
{
|
|
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
|
if (File.Exists(dbPath)) return false;
|
|
|
|
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
|
|
{
|
|
using var writeFs = new StreamWriter(new FileStream(dbPath, FileMode.Truncate));
|
|
JsonSerializer.CreateDefault().Serialize(writeFs, localRepo);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public RepositoryLocalDatabaseModel GetRepositoryLocalDatabase(RepositoryModel repo)
|
|
{
|
|
if (!_localRepoDbModel.TryGetValue(repo, out var localRepo))
|
|
{
|
|
var dbPath = PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name);
|
|
if (File.Exists(dbPath))
|
|
{
|
|
// Use existed target
|
|
using var readFs = new StreamReader(new FileStream(dbPath, FileMode.Open));
|
|
localRepo = (JsonSerializer.CreateDefault().Deserialize(readFs, typeof(RepositoryLocalDatabaseModel))
|
|
as RepositoryLocalDatabaseModel)!; // todo add broken test.
|
|
|
|
localRepo.RootModal = repo;
|
|
localRepo.LocalAccessor = new LocalFileTreeAccessor(repo, []);
|
|
}
|
|
else
|
|
{
|
|
// Create new one.
|
|
localRepo = new RepositoryLocalDatabaseModel
|
|
{
|
|
RootModal = repo,
|
|
LocalAccessor = new LocalFileTreeAccessor(repo, [])
|
|
};
|
|
}
|
|
|
|
_localRepoDbModel.Add(repo, localRepo);
|
|
}
|
|
|
|
return localRepo;
|
|
}
|
|
|
|
public async ValueTask<RepositoryModel?> CreateRepositoryOnServerAsync(string repositoryName, string description)
|
|
{
|
|
RepositoryModel repo;
|
|
var api = Api.C;
|
|
try
|
|
{
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return null;
|
|
}
|
|
|
|
var r = await api.Gateway.RepoCreate(repositoryName, description);
|
|
|
|
repo = new RepositoryModel
|
|
{
|
|
Name = r.RepositoryName,
|
|
OwnerName = r.OwnerUsername,
|
|
StandaloneName = RepositoryModel.GetStandaloneName(r.RepositoryName, r.OwnerUsername),
|
|
Description = r.Description,
|
|
Archived = r.IsArchived,
|
|
OwnByCurrentUser = (int) r.Role == (int) RepositoryModel.RepositoryRole.Owner
|
|
};
|
|
|
|
Repositories.Insert(0, repo);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return null;
|
|
}
|
|
|
|
return repo;
|
|
}
|
|
|
|
public async ValueTask<bool> UpdateRepositoriesFromServerAsync()
|
|
{
|
|
var api = Api.C;
|
|
try
|
|
{
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return false;
|
|
}
|
|
|
|
var result = (await api.Gateway.RepoList()).Result;
|
|
var dict = result.ToDictionary(rp => RepositoryModel.GetStandaloneName(rp.RepositoryName, rp.OwnerUsername));
|
|
for (var i = 0; i < Repositories.Count; i++)
|
|
{
|
|
var ele = Repositories[i];
|
|
if (!dict.Remove(ele.StandaloneName, out var role))
|
|
{
|
|
Repositories.RemoveAt(i);
|
|
i -= 1;
|
|
continue;
|
|
}
|
|
|
|
ele.Archived = ele.Archived;
|
|
ele.Description = ele.Description;
|
|
}
|
|
|
|
foreach (var (repoStandaloneName, rsp) in dict)
|
|
{
|
|
var repo = new RepositoryModel();
|
|
repo.Name = rsp.RepositoryName;
|
|
repo.OwnerName = rsp.OwnerUsername;
|
|
repo.StandaloneName = repoStandaloneName;
|
|
repo.Description = rsp.Description;
|
|
repo.Archived = rsp.IsArchived;
|
|
repo.OwnByCurrentUser = (int) rsp.Role == (int) RepositoryModel.RepositoryRole.Owner;
|
|
|
|
Repositories.Add(repo);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> UpdateRepositoriesDownloadedStatusFromDiskAsync()
|
|
{
|
|
foreach (var repo in _repositories)
|
|
{
|
|
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
|
|
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
|
|
repo.IsDownloaded = isFolderExists && isDbFileExists;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> UpdateDownloadedStatusFromDiskAsync(RepositoryModel repo)
|
|
{
|
|
var isFolderExists = Directory.Exists(PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name));
|
|
var isDbFileExists = File.Exists(PathUtility.GetWorkspaceDbPath(repo.OwnerName, repo.Name));
|
|
|
|
repo.IsDownloaded = isFolderExists && isDbFileExists;
|
|
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> UpdateMembersFromServerAsync(RepositoryModel repo)
|
|
{
|
|
var api = Api.C;
|
|
try
|
|
{
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return false;
|
|
}
|
|
|
|
var members = await api.Gateway.GetUsers(repo.Name, repo.OwnerName);
|
|
|
|
// Update existed
|
|
var dict = members.Result.ToDictionary(m => m.Username);
|
|
for (var i = 0; i < repo.Members.Count; i++)
|
|
{
|
|
var ele = repo.Members[i];
|
|
if (!dict.Remove(ele.Username, out var role))
|
|
{
|
|
repo.Members.RemoveAt(i);
|
|
i -= 1;
|
|
continue;
|
|
}
|
|
|
|
ele.Username = role.Username;
|
|
ele.Role = (RepositoryModel.RepositoryRole) role.Role;
|
|
}
|
|
|
|
// Add missing
|
|
foreach (var role in dict.Values)
|
|
{
|
|
var r = new RepositoryModel.Member
|
|
{
|
|
Username = role.Username,
|
|
Role = (RepositoryModel.RepositoryRole) role.Role
|
|
};
|
|
|
|
repo.Members.Add(r);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool IsRepositoryOpened(RepositoryModel repo)
|
|
{
|
|
return _openedRepos.Any(r => r == repo);
|
|
}
|
|
|
|
public async ValueTask<bool> CloseRepositoryAsync(RepositoryModel repo)
|
|
{
|
|
if (_openedRepos.Contains(repo))
|
|
{
|
|
var ls = GetRepositoryLocalDatabase(repo);
|
|
if (ls.RepoAccessor != null) await ls.RepoAccessor!.DisposeAsync();
|
|
ls.RepoAccessor = null;
|
|
|
|
if (!SaveRepositoryLocalDatabaseChanges(repo)) return false;
|
|
|
|
_openedRepos.Remove(repo);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async ValueTask<bool> OpenRepositoryOnStorageAsync(RepositoryModel repo)
|
|
{
|
|
if (!await UpdateDownloadedStatusFromDiskAsync(repo) || repo.IsDownloaded == false) return false;
|
|
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
|
var ls = GetRepositoryLocalDatabase(repo);
|
|
|
|
if (ls.CurrentCommit != null)
|
|
{
|
|
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, ls.CurrentCommit.Value);
|
|
if (accessor == null) return false;
|
|
ls.RepoAccessor = accessor;
|
|
}
|
|
|
|
_openedRepos.Add(repo);
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> CreateNewRepositoryOnStorageAsync(RepositoryModel repo)
|
|
{
|
|
// Create basic structures.
|
|
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
|
|
|
repo.IsDownloaded = true;
|
|
_openedRepos.Add(repo);
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> CloneRepositoryFromRemoteAsync(RepositoryModel repo)
|
|
{
|
|
// Create basic structures.
|
|
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
|
|
|
if (!await UpdateCommitsFromServerAsync(repo)) return false;
|
|
var peekCommit = repo.Commits.MaxBy(sl => sl.CommittedOn);
|
|
if (peekCommit == null) return false; // Should not use this function!
|
|
|
|
// Create basic structures.
|
|
if (!TryCreateRepositoryBaseStorageStructure(repo)) return false;
|
|
|
|
// Download base repo info
|
|
var accessor = await DownloadDepotsToGetCommitFileTreeFromServerAsync(repo, peekCommit.CommitId);
|
|
if (accessor == null)
|
|
{
|
|
await DeleteFromDiskAsync(repo);
|
|
return false;
|
|
};
|
|
|
|
var ls = GetRepositoryLocalDatabase(repo);
|
|
ls.CurrentCommit = peekCommit.CommitId;
|
|
ls.RepoAccessor = accessor;
|
|
|
|
try
|
|
{
|
|
foreach (var f in accessor.Manifest.FilePaths)
|
|
{
|
|
var pfs = WorkPath.ToPlatformPath(f.WorkPath, ls.LocalAccessor.WorkingDirectory);
|
|
var directory = Path.GetDirectoryName(pfs);
|
|
|
|
// Write into fs
|
|
if (directory != null) Directory.CreateDirectory(directory);
|
|
await using var write = File.Create(pfs);
|
|
accessor.TryWriteDataIntoStream(f.WorkPath, write);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
await DeleteFromDiskAsync(repo);
|
|
return false;
|
|
}
|
|
|
|
repo.IsDownloaded = true;
|
|
_openedRepos.Add(repo);
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<bool> ShouldUpdateLocalCommitsCacheFromServerAsync(RepositoryModel repo)
|
|
{
|
|
var api = Api.C;
|
|
try
|
|
{
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return false;
|
|
}
|
|
|
|
var rsp = await api.Gateway.PeekCommit(repo.OwnerName, repo.Name);
|
|
var emptyRepo = repo.Commits.Count == 0;
|
|
|
|
// If they both empty
|
|
if ((rsp.Result == Guid.Empty) == emptyRepo) return false;
|
|
|
|
if (emptyRepo) return true;
|
|
return rsp.Result == repo.Commits.MaxBy(cm => cm.CommittedOn)!.CommitId;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async ValueTask<bool> UpdateCommitsFromServerAsync(RepositoryModel repo)
|
|
{
|
|
var api = Api.C;
|
|
try
|
|
{
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return false;
|
|
}
|
|
|
|
var rsp = await api.Gateway.ListCommit(repo.OwnerName, repo.Name);
|
|
|
|
// Update existed
|
|
var dict = rsp.Result.ToDictionary(m => m.Id);
|
|
for (var i = 0; i < repo.Commits.Count; i++)
|
|
{
|
|
var ele = repo.Commits[i];
|
|
if (!dict.Remove(ele.CommitId, out var cm))
|
|
{
|
|
repo.Members.RemoveAt(i);
|
|
i -= 1;
|
|
continue;
|
|
}
|
|
|
|
ele.Message = cm.Message;
|
|
ele.DepotId = cm.MainDepotId;
|
|
ele.CommittedOn = cm.CommitedOn.UtcDateTime;
|
|
ele.Author = cm.Author;
|
|
}
|
|
|
|
// Add missing
|
|
foreach (var cm in dict.Values)
|
|
{
|
|
var r = new RepositoryModel.Commit
|
|
{
|
|
CommitId = cm.Id,
|
|
Message = cm.Message,
|
|
DepotId = cm.MainDepotId,
|
|
CommittedOn = cm.CommitedOn.UtcDateTime,
|
|
Author = cm.Author,
|
|
};
|
|
|
|
repo.Commits.Add(r);
|
|
}
|
|
|
|
// Resort them again
|
|
repo.Commits.Sort((l, r) => r.CommittedOn.CompareTo(l.CommittedOn));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async ValueTask<RepositoryFileTreeAccessor?> DownloadDepotsToGetCommitFileTreeFromServerAsync
|
|
(RepositoryModel repo, Guid commit, bool storeDownloadedDepots = true)
|
|
{
|
|
if (commit == Guid.Empty) return null;
|
|
|
|
// Get depots list and manifest
|
|
var manifest = await DownloadManifestFromServerAsync(repo, commit);
|
|
if (manifest == null) return null;
|
|
|
|
// Prepare folders
|
|
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
|
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
|
Directory.CreateDirectory(path);
|
|
|
|
// Generate download depots list
|
|
var mainDepotLabel = manifest.Value.Depot;
|
|
var willDownload = mainDepotLabel.Where(label =>
|
|
{
|
|
var dpPath = Path.Combine(depotsRoot, label.Id.ToString());
|
|
if (!File.Exists(dpPath)) return true;
|
|
// todo Needs a way to check if that valid.
|
|
return false;
|
|
});
|
|
|
|
// Download them
|
|
var downloadedDepots = await DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync(repo, willDownload);
|
|
|
|
try
|
|
{
|
|
// Check if anyone download failed
|
|
if (downloadedDepots == null || downloadedDepots.Any(dl => dl.Item2 == null))
|
|
throw new Exception("Some depots are not able to be downloaded.");
|
|
|
|
if (storeDownloadedDepots)
|
|
{
|
|
// Write new downloaded depots into disk
|
|
var transform = downloadedDepots.Select(dl => (dl.Item1, dl.Item2!));
|
|
await WriteDownloadedDepotsFromServerToStorageAsync(repo, transform);
|
|
}
|
|
|
|
// Create mapping dictionary
|
|
var mappingDict = downloadedDepots.ToDictionary(i => i.Item1, i => i.Item2!);
|
|
foreach (var dl in mainDepotLabel)
|
|
{
|
|
if (mappingDict.ContainsKey(dl.Id)) continue;
|
|
var dst = Path.Combine(depotsRoot, dl.Id.ToString());
|
|
mappingDict.Add(dl.Id, new FileStream(dst, FileMode.Create));
|
|
}
|
|
|
|
return new RepositoryFileTreeAccessor(mappingDict, manifest.Value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (downloadedDepots != null)
|
|
foreach (var t in downloadedDepots)
|
|
{
|
|
if (t.Item2 == null) continue;
|
|
try { await t.Item2.DisposeAsync(); }
|
|
catch (Exception ex) { Console.WriteLine(ex); }
|
|
}
|
|
|
|
Console.WriteLine(e);
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
public async ValueTask WriteDownloadedDepotsFromServerToStorageAsync
|
|
(RepositoryModel repo, IEnumerable<(Guid, Stream)> depots)
|
|
{
|
|
var depotsRoot = PathUtility.GetWorkspaceDepotCachePath(repo.OwnerName, repo.Name);
|
|
var tasks = depots.Select(async d =>
|
|
{
|
|
var dst = Path.Combine(depotsRoot, d.Item1.ToString());
|
|
await using var ws = new FileStream(dst, FileMode.Create);
|
|
await d.Item2.CopyToAsync(ws);
|
|
d.Item2.Seek(0, SeekOrigin.Begin);
|
|
});
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
|
|
public async ValueTask<(Guid, Stream?)[]?> DownloadDepotsAndCopyNetworkStreamIntoNewMemoryStreamFromServerAsync
|
|
(RepositoryModel repo, IEnumerable<DepotLabel> depotsId)
|
|
{
|
|
try
|
|
{
|
|
var api = Api.C;
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return null;
|
|
}
|
|
|
|
return await ValueTaskEx.WhenAll(depotsId.Select(p => DownloadDepotInternalAsync(p.Id, p.Length)));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return null;
|
|
}
|
|
|
|
async ValueTask<(Guid, Stream?)> DownloadDepotInternalAsync(Guid depotId, long length)
|
|
{
|
|
using var rsp = await Api.C.Gateway.FetchDepot(repo.OwnerName, repo.Name, depotId.ToString());
|
|
if (rsp.StatusCode != 200) return (depotId, null);
|
|
if (rsp.Stream.Length != length) return (depotId, null);
|
|
|
|
var memoryStream = new MemoryStream(new byte[rsp.Stream.Length]);
|
|
await rsp.Stream.CopyToAsync(memoryStream);
|
|
return (depotId, memoryStream);
|
|
}
|
|
}
|
|
|
|
public async ValueTask<CommitManifest?> DownloadManifestFromServerAsync(RepositoryModel repo, Guid manifestId)
|
|
{
|
|
try
|
|
{
|
|
var api = Api.C;
|
|
if (api.RequireRefreshToken() && !(await api.TryRefreshTokenAsync()))
|
|
{
|
|
api.ClearGateway();
|
|
return null;
|
|
}
|
|
|
|
using var manifestResponse = await api.Gateway.FetchManifest(repo.OwnerName, repo.Name, manifestId.ToString());
|
|
if (manifestResponse.StatusCode != 200) return null;
|
|
|
|
return await System.Text.Json.JsonSerializer.DeserializeAsync<CommitManifest>(manifestResponse.Stream);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async ValueTask<bool> DeleteFromDiskAsync(RepositoryModel repo)
|
|
{
|
|
try
|
|
{
|
|
var path = PathUtility.GetWorkspacePath(repo.OwnerName, repo.Name);
|
|
if (Directory.Exists(path)) Directory.Delete(path, true);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
return false;
|
|
}
|
|
|
|
repo.IsDownloaded = false;
|
|
return true;
|
|
}
|
|
} |