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 { public ObservableCollection Repositories => _repositories; private readonly ObservableCollection _repositories = new(); private readonly Dictionary _localRepoDbModel = new(); private readonly HashSet _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. } else { // Create new one. localRepo = new RepositoryLocalDatabaseModel { RootModal = repo, LocalAccessor = new LocalFileTreeAccessor(repo, []) }; } _localRepoDbModel.Add(repo, localRepo); } return localRepo; } public async ValueTask 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 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 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 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 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 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 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 CreateNewRepositoryOnStorageAsync(RepositoryModel repo) { // Create basic structures. if (!TryCreateRepositoryBaseStorageStructure(repo)) return false; repo.IsDownloaded = true; _openedRepos.Add(repo); return true; } public async ValueTask 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 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 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 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 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 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(manifestResponse.Stream); } catch (Exception e) { Console.WriteLine(e); return null; } } public async ValueTask 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; } return true; } }