1
0

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;
}
}