diff --git a/repo-backup-util/Program.cs b/repo-backup-util/Program.cs index 39ae356..8266b76 100644 --- a/repo-backup-util/Program.cs +++ b/repo-backup-util/Program.cs @@ -1,6 +1,7 @@ // See https://aka.ms/new-console-template for more information using Octokit; using RepoBackupUtil.Lib; +using System.Diagnostics; static async Task Main() { @@ -9,7 +10,9 @@ static async Task Main() GitHubClient github = new(productHeaderValue); // check for user with received input - string username = GitHub.SetCredentialsToken(github); + string token = GitHub.GetCredentialsToken(github); + + string username = github.User.Current().Result.Login; User user = await GitHub.GetUser(username, github); // get all repos for user @@ -21,24 +24,59 @@ static async Task Main() // derive repository count and clone urls var privateRepos = uniqueRepos.Where(r => r.Private); var publicRepos = uniqueRepos.Where(r => !r.Private); + var cloneUrls = uniqueRepos.Select(r => r.CloneUrl); Console.WriteLine($"Found {privateRepos.Count()} private repos and {publicRepos.Count()} public repos."); - var cloneUrls = uniqueRepos.Select(r => r.CloneUrl); -} + // create backup directory + DirectoryInfo backupDir = Volumes.SetUpBackupFile() ?? throw new Exception("Error setting up backup directory"); -static void DriveStuff() -{ - IEnumerable volumes = Volumes.GetVolumes(); - DriveInfo? selection = Volumes.SelectFromList(volumes); - if (selection == null) + // set up, update directories for hosting projects + HashSet newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, backupDir); + IEnumerable projectsToUpdate = GitHub.GetProjectsToUpdate(uniqueRepos, backupDir); + + Console.WriteLine(projectsToUpdate.Count() + " projects to update."); + Console.WriteLine(newProjects.Count + " new projects to clone."); + + // identify all work to be done and prepare as tasks + List taskPool = []; + + foreach (Repository project in projectsToUpdate) { - Console.WriteLine("No selection found"); - return; + taskPool.Add(new Task(() => GitHub.UpdateExistingRepo(project, backupDir))); } - var backupDir = Volumes.CreateBackupDirectory(selection); + foreach (Repository newProject in newProjects) + { + ICloneProject options = new GitHub.CloneProjectOptions + { + BackupDir = backupDir, + Repo = newProject, + Token = token, + User = user + }; + + taskPool.Add(new Task(() => GitHub.CloneProject(options))); + } + + Console.WriteLine($"Prepared {taskPool.Count} tasks. Starting..."); + + // start a timer to track progress + Stopwatch stopwatch = new(); + stopwatch.Start(); + + // execute all tasks + Parallel.ForEach(taskPool, task => task.Start()); + Task.WaitAll(taskPool.ToArray()); + + stopwatch.Stop(); + double elapsed = stopwatch.ElapsedMilliseconds < 1000 + ? stopwatch.ElapsedMilliseconds + : stopwatch.ElapsedMilliseconds / 1000; + + string unit = stopwatch.ElapsedMilliseconds < 1000 ? "ms" : "s"; + + Console.WriteLine($"All tasks completed in {elapsed}{unit}. Exiting..."); } -DriveStuff(); -//await Main(); \ No newline at end of file +await Main(); \ No newline at end of file diff --git a/repo-backup-util/lib/GitHub.cs b/repo-backup-util/lib/GitHub.cs index f40c39d..5ddc60e 100644 --- a/repo-backup-util/lib/GitHub.cs +++ b/repo-backup-util/lib/GitHub.cs @@ -1,10 +1,11 @@ using Octokit; +using System.Diagnostics; namespace RepoBackupUtil.Lib { - public static class GitHub + public class GitHub { - public static string SetCredentialsToken(GitHubClient github) + public static string GetCredentialsToken(GitHubClient github) { Console.WriteLine("Please enter your Github Personal Access Token: "); string? token = Console.ReadLine(); @@ -12,7 +13,7 @@ namespace RepoBackupUtil.Lib if (token == null) { Console.WriteLine("Received invalid input. Try again:"); - return SetCredentialsToken(github); + return GetCredentialsToken(github); } else { @@ -20,7 +21,7 @@ namespace RepoBackupUtil.Lib Credentials auth = new(token, authType); github.Credentials = auth; Console.WriteLine("Successfully authenticated with GitHub."); - return github.User.Current().Result.Login; + return token; } } @@ -46,27 +47,13 @@ namespace RepoBackupUtil.Lib public static async Task GetUser(string username, GitHubClient github) { Console.WriteLine("Checking for user..."); - User? user = await github.User.Get(username); + User user = await github.User.Get(username) ?? throw new Exception("User does not exist"); - if (user == null) - { - Console.WriteLine("User does not exist"); - string newUsername = SetCredentialsToken(github); - return await GetUser(newUsername, github); - } - else - { - Console.WriteLine($"Found user {user.Login}. Loading repos..."); - return user; - } + Console.WriteLine($"Found user {user.Login}. Loading repos..."); + return user; } - public static async Task?> GetRepos(GitHubClient github) - { - Console.WriteLine("Loading repos..."); - var repos = await github.Repository.GetAllForCurrent(); - return repos; - } + public static async Task?> GetRepos(GitHubClient github) => await github.Repository.GetAllForCurrent(); public static HashSet GetUniqueRepos(IEnumerable repos) { @@ -81,5 +68,73 @@ namespace RepoBackupUtil.Lib return uniqueRepos; } + + public static IEnumerable GetProjectsToUpdate(HashSet allRepos, DirectoryInfo backupDir) + { + HashSet projectsToUpdate = []; + foreach (DirectoryInfo dir in backupDir.EnumerateDirectories()) + { + bool isRepo = dir.GetDirectories(".git").Any(); + if (!isRepo) continue; + Repository? repo = allRepos.FirstOrDefault(r => r.Name == dir.Name); + if (repo != null) projectsToUpdate.Add(repo); + } + + return projectsToUpdate; + } + + public record CloneProjectOptions : ICloneProject + { + public User User { get; init; } = default!; + public Repository Repo { get; init; } = default!; + public DirectoryInfo BackupDir { get; init; } = default!; + public string Token { get; init; } = default!; + } + + public static void CloneProject(ICloneProject options) + { + string cloneString = ""; + cloneString += $"clone https://{options.Token}@github.com/"; + cloneString += $"{options.User.Login}/{options.Repo.Name} ."; + + Console.WriteLine($"Cloning {options.Repo.Name}..."); + + using (Process process = new()) + { + process.StartInfo = new() + { + FileName = "git", + Arguments = cloneString, + WorkingDirectory = $"{options.BackupDir}/{options.Repo.Name}", + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + process.Start(); + Console.WriteLine(process.StandardOutput.ReadToEnd()); + process.WaitForExit(); + }; + } + + public static void UpdateExistingRepo(Repository repo, DirectoryInfo backupDir) + { + Console.WriteLine($"Preparing update task for {repo.Name}..."); + + using (Process process = new()) + { + process.StartInfo = new() + { + FileName = "git", + Arguments = "pull", + WorkingDirectory = $"{backupDir}/{repo.Name}", + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + process.Start(); + Console.WriteLine(process.StandardOutput.ReadToEnd()); + process.WaitForExit(); + }; + } } } diff --git a/repo-backup-util/lib/ICloneProject.cs b/repo-backup-util/lib/ICloneProject.cs new file mode 100644 index 0000000..5d1cbf1 --- /dev/null +++ b/repo-backup-util/lib/ICloneProject.cs @@ -0,0 +1,10 @@ +using Octokit; + +namespace RepoBackupUtil.Lib; +public interface ICloneProject +{ + Repository Repo { get; } + DirectoryInfo BackupDir { get; } + User User { get; } + string Token { get; } +} \ No newline at end of file diff --git a/repo-backup-util/lib/Volumes.cs b/repo-backup-util/lib/Volumes.cs index 5a015ab..ce3d528 100644 --- a/repo-backup-util/lib/Volumes.cs +++ b/repo-backup-util/lib/Volumes.cs @@ -1,9 +1,24 @@ -using System.Collections; +using Octokit; +using System.Collections; namespace RepoBackupUtil.Lib { public static class Volumes { + public static DirectoryInfo? SetUpBackupFile() + { + IEnumerable volumes = GetVolumes(); + DriveInfo? selection = SelectFromList(volumes); + if (selection == null) + { + Console.WriteLine("No selection found"); + return null; + } + + var backupDir = CreateBackupDirectory(selection); + return backupDir; + } + public static IEnumerable GetVolumes() { try @@ -95,7 +110,6 @@ namespace RepoBackupUtil.Lib try { - if (isSystemDrive) { Console.WriteLine("Using system drive. Directing you to user folder..."); @@ -125,5 +139,25 @@ namespace RepoBackupUtil.Lib return null; } } + + public static HashSet PopulateBackupDirectories(HashSet repos, DirectoryInfo backupDir) + { + HashSet newProjects = []; + + foreach (Repository repo in repos) + { + bool exists = Directory.Exists($"{backupDir.FullName}/{repo.Name}"); + bool hasContents = exists && Directory.EnumerateFileSystemEntries( + $"{backupDir.FullName}/{repo.Name}").Any(); + + if (exists && hasContents) continue; + + DirectoryInfo? repoDir = backupDir.CreateSubdirectory(repo.Name); + Console.WriteLine($"Created directory for project {repoDir.FullName}"); + newProjects.Add(repo); + } + + return newProjects; + } } }