diff --git a/repo-backup-util.sln b/git-anchor.sln similarity index 86% rename from repo-backup-util.sln rename to git-anchor.sln index d5d8ff0..0edb8a0 100644 --- a/repo-backup-util.sln +++ b/git-anchor.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.8.34309.116 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "repo-backup-util", "repo-backup-util\repo-backup-util.csproj", "{352652F3-958B-416B-B5F5-CDC36B0AA0C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "git-anchor", "repo-backup-util\git-anchor.csproj", "{352652F3-958B-416B-B5F5-CDC36B0AA0C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/repo-backup-util/Actions/Base.cs b/repo-backup-util/Actions/Base.cs new file mode 100644 index 0000000..483c929 --- /dev/null +++ b/repo-backup-util/Actions/Base.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace GitAnchor.Actions; + +public abstract class BaseAction { + public virtual async Task Run() + { + throw new NotImplementedException(); + } +} diff --git a/repo-backup-util/Actions/Create.cs b/repo-backup-util/Actions/Create.cs new file mode 100644 index 0000000..812ea1e --- /dev/null +++ b/repo-backup-util/Actions/Create.cs @@ -0,0 +1,107 @@ +using GitAnchor.Lib; +using Octokit; +using System.Diagnostics; + +namespace GitAnchor.Actions; +public class Create : BaseAction +{ + public new static async Task Run() + { + string[] args = Environment.GetCommandLineArgs(); + + string? backupName = args.FirstOrDefault(arg => arg.StartsWith("--name")); + string? token = args.FirstOrDefault(arg => arg.StartsWith("--token")); + bool verbose = args.Any(arg => arg.StartsWith("--verbose") + || arg.StartsWith("-v")); + + // find or configure the default backup directory + DirectoryInfo backupDir = FileSystemTools.SetUpMainBackupFile() ?? throw new Exception("Encountered a problem creating main backup directory"); + + // create backup file + DirectoryInfo? anchorDir = SetUpAnchor(backupDir, backupName); + + if (anchorDir == null) + { + Console.WriteLine("Could not create anchor directory."); + return ActivityStatusCode.Error; + } + + // authenticate with github + GitHubClient github = GitHub.CreateClient(); + token = GitHub.Authenticate(github, token); + + // load accessible repositories + IEnumerable repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found"); + + // organize and ensure all repositories are unique + HashSet uniqueRepos = GitHub.GetUniqueRepos(repos); + + if (verbose) + { + // 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."); + } + + // set up, update directories for hosting projects + HashSet newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, anchorDir); + + // identify all work to be done and prepare as tasks + List taskPool = []; + + foreach (Repository newProject in newProjects) + { + GitHub.CloneProjectOptions options = new() + { + BackupDir = anchorDir, + Repo = newProject, + Token = token, + User = github.User.Current().Result, + }; + + taskPool.Add(new Task(() => GitHub.CloneProject(options))); + } + + Console.WriteLine($"Preparing to clone {taskPool.Count} repositories. Starting..."); + + // start a timer to track progress + Stopwatch stopwatch = new(); + stopwatch.Start(); + + // execute all tasks + Parallel.ForEach(taskPool, task => task.Start()); + Task.WaitAll([.. taskPool]); + + stopwatch.Stop(); + long 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..."); + + return ActivityStatusCode.Ok; + } + + private static DirectoryInfo? SetUpAnchor(DirectoryInfo backupDir, string? initialInput) + { + string defaultName = FileSystemTools.DefaultAnchorPath; + string? backupName = initialInput; + + if (backupName == null) + { + Console.WriteLine($"Enter a name for this backup: (default {defaultName})"); + backupName = Console.ReadLine(); + if (backupName == "") backupName = defaultName; + } + + if (backupName == null || backupName == "") throw new Exception("No backup name provided"); + + // create backup file + return Volumes.CreateAnchor(backupDir, backupName); + } +} diff --git a/repo-backup-util/Actions/Help.cs b/repo-backup-util/Actions/Help.cs new file mode 100644 index 0000000..70c68cd --- /dev/null +++ b/repo-backup-util/Actions/Help.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace GitAnchor.Actions; + +public class Help : BaseAction +{ + public static async new Task Run() + { + Console.WriteLine("\nGitAnchor - a tool for performing scoped backups of GitHub repositories based on credentials and configuration."); + Console.WriteLine("Usage:\n"); + + Console.WriteLine("git-anchor create [--verbose, -v] [--name ] [--token ]"); + Console.WriteLine(" Creates a new backup directory and clones all repositories accessible by the provided token."); + Console.WriteLine(" --name The name of the backup directory to create. Defaults to the current date."); + Console.WriteLine(" --token The GitHub token to use for authentication."); + Console.WriteLine(" --verbose Prints verbose output.\n"); + Console.WriteLine("Example: git-anchor create -v --name=cool-new-backup --token=(your token)"); + + Console.WriteLine("git-anchor pull [--name ] [--verbose, -v]"); + Console.WriteLine(" Pulls all repositories in an existing backup directory."); + Console.WriteLine(" --name The name of the backup directory to pull from. Defaults to the current date."); + Console.WriteLine(" --verbose Prints verbose output.\n"); + Console.WriteLine("Example: git-anchor pull -v --name=cool-new-backup"); + + Console.WriteLine("git-anchor find [--regex, -e] [--name, -n ]"); + Console.WriteLine(" Finds an existing backup matching the provided pattern."); + Console.WriteLine(" --regex, -e Find your backup directory based on a Regex search."); + Console.WriteLine(" --name, -n The name of the backup directory to pull from. Defaults to the current date.\n\n"); + + return ActivityStatusCode.Ok; + } + +} diff --git a/repo-backup-util/Actions/Pull.cs b/repo-backup-util/Actions/Pull.cs new file mode 100644 index 0000000..f3f3ec5 --- /dev/null +++ b/repo-backup-util/Actions/Pull.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; + +namespace GitAnchor.Actions +{ + internal class Pull : BaseAction + { + public static async new Task Run() + { + throw new NotImplementedException(); + } + } +} diff --git a/repo-backup-util/Program.cs b/repo-backup-util/Program.cs index 8266b76..b19733e 100644 --- a/repo-backup-util/Program.cs +++ b/repo-backup-util/Program.cs @@ -1,82 +1 @@ -// See https://aka.ms/new-console-template for more information -using Octokit; -using RepoBackupUtil.Lib; -using System.Diagnostics; - -static async Task Main() -{ - // initialize our github client - ProductHeaderValue productHeaderValue = new("repo-backup-util"); - GitHubClient github = new(productHeaderValue); - - // check for user with received input - string token = GitHub.GetCredentialsToken(github); - - string username = github.User.Current().Result.Login; - User user = await GitHub.GetUser(username, github); - - // get all repos for user - IEnumerable repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found"); - - // organize and ensure all repositories are unique - HashSet uniqueRepos = GitHub.GetUniqueRepos(repos); - - // 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."); - - // create backup directory - DirectoryInfo backupDir = Volumes.SetUpBackupFile() ?? throw new Exception("Error setting up backup directory"); - - // 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) - { - taskPool.Add(new Task(() => GitHub.UpdateExistingRepo(project, backupDir))); - } - - 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..."); -} - -await Main(); \ No newline at end of file +await GitAnchor.Lib.EntryPoint.Run(); diff --git a/repo-backup-util/repo-backup-util.csproj b/repo-backup-util/git-anchor.csproj similarity index 78% rename from repo-backup-util/repo-backup-util.csproj rename to repo-backup-util/git-anchor.csproj index 14c587d..a1e8af9 100644 --- a/repo-backup-util/repo-backup-util.csproj +++ b/repo-backup-util/git-anchor.csproj @@ -3,9 +3,9 @@ Exe net8.0 - RepoBackupUtil + GitAnchor enable - enable + enable diff --git a/repo-backup-util/lib/CommandLine.cs b/repo-backup-util/lib/CommandLine.cs new file mode 100644 index 0000000..7d4a150 --- /dev/null +++ b/repo-backup-util/lib/CommandLine.cs @@ -0,0 +1,39 @@ +using Octokit; + +namespace GitAnchor.Lib; + +public static class CommandLineTools { + public static int ReadAsInt() + { + string? input = Console.ReadLine(); + + while (input == null) + { + input = Console.ReadLine(); + } + + try + { + return int.Parse(input); + } + catch + { + Console.WriteLine("Invaid input. Please enter a number: "); + return ReadAsInt(); + } + } + + public static string ReadAccessToken(GitHubClient github) + { + Console.WriteLine("Please enter your Github Personal Access Token: "); + string? token = Console.ReadLine(); + + if (token == null) + { + Console.WriteLine("Received invalid input. Try again:"); + return ReadAccessToken(github); + } + + return token; + } +} diff --git a/repo-backup-util/lib/Entrypoint.cs b/repo-backup-util/lib/Entrypoint.cs new file mode 100644 index 0000000..48bb2d5 --- /dev/null +++ b/repo-backup-util/lib/Entrypoint.cs @@ -0,0 +1,38 @@ +using System.Collections; +using System.Diagnostics; +using GitAnchor.Actions; + +namespace GitAnchor.Lib; + +public class EntryPoint +{ + public static async Task Run() + { + string[] args = Environment.GetCommandLineArgs(); + + SortedDictionary> options = new() + { + { "create", Create.Run() }, + { "pull", Pull.Run() }, + { "help", Help.Run() } + }; + + try + { + foreach (string arg in args) + { + var result = options.TryGetValue(arg, out Task? action); + + if (!result) continue; + if (action != null) return await action; + } + + return await Help.Run(); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return ActivityStatusCode.Error; + } + } +} diff --git a/repo-backup-util/lib/FileSystem.cs b/repo-backup-util/lib/FileSystem.cs new file mode 100644 index 0000000..0c0de5c --- /dev/null +++ b/repo-backup-util/lib/FileSystem.cs @@ -0,0 +1,91 @@ +namespace GitAnchor.Lib; + +public static class FileSystemTools { + public static string DefaultAnchorPath + { + get => DateTime.Now.ToString("Mddy"); + } + + public static bool MainBackupExists + { + get => MainBackupFile != null; + } + + public static bool AnchorExists(string anchorName) + { + return MainBackupExists && Directory.Exists($"{MainBackupFile}/anchorName"); + } + + public static string? MainBackupFile + { + get => Volumes.FindMainBackupFile(); + } + + public static DirectoryInfo? SetUpMainBackupFile() + { + IEnumerable volumes = Volumes.GetVolumes(); + DriveInfo? selection = Volumes.SelectFromList(volumes); + + if (selection == null) + { + Console.WriteLine("No selection found"); + return null; + } + + if (MainBackupFile != null) return new DirectoryInfo(MainBackupFile); + + bool isWindows = OperatingSystem.IsWindows(); + bool isMacOS = OperatingSystem.IsMacOS(); + bool isLinux = OperatingSystem.IsLinux(); + + if (isWindows) + return CreateMainWindowsDirectory(selection); + else if (isLinux || isMacOS) + return CreateMainUnixDirectory(selection); + else + throw new Exception("Unsupported operating system"); + } + + private static DirectoryInfo? CreateMainWindowsDirectory(DriveInfo volume, string? location = ".git-anchor") + { + bool isSystemDrive = volume.Name == "C:\\"; + + try + { + if (isSystemDrive) + { + Console.WriteLine("Using system drive. Directing you to user folder..."); + + string? userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + DirectoryInfo userDir = new(userPath); + + Console.WriteLine($"Checking for folder `.ghbackups` at `{userDir.FullName}`..."); + DirectoryInfo? backupDir = userDir.CreateSubdirectory(".ghbackups"); + + return backupDir; + } + else + { + Console.WriteLine($"Using {volume.Name}. Directing you to root folder..."); + + string rootPath = volume.RootDirectory.FullName; + + DirectoryInfo rootDir = new(rootPath); + Console.WriteLine($"Checking for folder `.ghbackups` at `{rootDir.FullName}`..."); + + DirectoryInfo backupDir = Directory.CreateDirectory($"{rootDir.FullName}.ghbackups"); + return backupDir; + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + return null; + } + } + + private static DirectoryInfo? CreateMainUnixDirectory(DriveInfo volume) + { + throw new NotImplementedException(); + } +} diff --git a/repo-backup-util/lib/GitHub.cs b/repo-backup-util/lib/GitHub.cs index 5ddc60e..37cfd97 100644 --- a/repo-backup-util/lib/GitHub.cs +++ b/repo-backup-util/lib/GitHub.cs @@ -1,30 +1,27 @@ using Octokit; using System.Diagnostics; -namespace RepoBackupUtil.Lib +namespace GitAnchor.Lib { public class GitHub { - public static string GetCredentialsToken(GitHubClient github) + public static GitHubClient CreateClient() { - Console.WriteLine("Please enter your Github Personal Access Token: "); - string? token = Console.ReadLine(); - - if (token == null) - { - Console.WriteLine("Received invalid input. Try again:"); - return GetCredentialsToken(github); - } - else - { - AuthenticationType authType = AuthenticationType.Bearer; - Credentials auth = new(token, authType); - github.Credentials = auth; - Console.WriteLine("Successfully authenticated with GitHub."); - return token; - } + ProductHeaderValue productHeaderValue = new("git-anchor"); + return new(productHeaderValue); } + public static string Authenticate(GitHubClient github, string? initialToken = null) + { + string token = initialToken ?? CommandLineTools.ReadAccessToken(github); + AuthenticationType authType = AuthenticationType.Bearer; + Credentials auth = new(token, authType); + github.Credentials = auth; + Console.WriteLine("Successfully authenticated with GitHub."); + return token; + } + + /** @deprecated */ public static string SetOAuthCredentials(GitHubClient github) { Console.WriteLine("Provide OAuth Client Secret: "); @@ -83,15 +80,17 @@ namespace RepoBackupUtil.Lib return projectsToUpdate; } - public record CloneProjectOptions : ICloneProject + public record CloneProjectOptions { 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 string? AnchorPath { get; init; } + public bool Verbose { get; init; } = false; } - public static void CloneProject(ICloneProject options) + public static void CloneProject(CloneProjectOptions options) { string cloneString = ""; cloneString += $"clone https://{options.Token}@github.com/"; @@ -111,14 +110,15 @@ namespace RepoBackupUtil.Lib }; process.Start(); - Console.WriteLine(process.StandardOutput.ReadToEnd()); + if (options.Verbose) Console.WriteLine(process.StandardOutput.ReadToEnd()); process.WaitForExit(); + Console.WriteLine($"Clone complete for {options.Repo.Name}"); }; } - public static void UpdateExistingRepo(Repository repo, DirectoryInfo backupDir) + public static void PullExistingRepo(Repository repo, DirectoryInfo backupDir, bool verbose = false) { - Console.WriteLine($"Preparing update task for {repo.Name}..."); + Console.WriteLine($"Pulling {repo.Name}..."); using (Process process = new()) { @@ -132,8 +132,9 @@ namespace RepoBackupUtil.Lib }; process.Start(); - Console.WriteLine(process.StandardOutput.ReadToEnd()); + if (verbose) Console.WriteLine(process.StandardOutput.ReadToEnd()); process.WaitForExit(); + Console.WriteLine($"Pull complete for {repo.Name}"); }; } } diff --git a/repo-backup-util/lib/ICloneProject.cs b/repo-backup-util/lib/ICloneProject.cs deleted file mode 100644 index 5d1cbf1..0000000 --- a/repo-backup-util/lib/ICloneProject.cs +++ /dev/null @@ -1,10 +0,0 @@ -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 ce3d528..fc46a3c 100644 --- a/repo-backup-util/lib/Volumes.cs +++ b/repo-backup-util/lib/Volumes.cs @@ -1,163 +1,116 @@ using Octokit; using System.Collections; -namespace RepoBackupUtil.Lib +namespace GitAnchor.Lib; +public static class Volumes { - public static class Volumes + public static DriveInfo[] GetVolumes() { - public static DirectoryInfo? SetUpBackupFile() + try { - IEnumerable volumes = GetVolumes(); - DriveInfo? selection = SelectFromList(volumes); - if (selection == null) - { - Console.WriteLine("No selection found"); - return null; - } - - var backupDir = CreateBackupDirectory(selection); - return backupDir; + return DriveInfo.GetDrives(); } - - public static IEnumerable GetVolumes() + catch (UnauthorizedAccessException e) { - try - { - return DriveInfo.GetDrives(); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - return []; - } + Console.WriteLine("Unauthorized access to volumes. Please run as administrator."); + Console.WriteLine(e.Message); + return []; } - - private static int ReadInput() + catch (Exception e) { - string? input = Console.ReadLine(); - - while (input == null) - { - Console.WriteLine("Please enter a number: "); - input = Console.ReadLine(); - } - - try - { - return int.Parse(input); - } - catch - { - Console.WriteLine("Invaid input. Please enter a number: "); - return ReadInput(); - } - } - - private static DriveInfo? GetSelection(Hashtable options) - { - int inputAsInt = ReadInput(); - - if (!options.ContainsKey(inputAsInt)) - { - return null; - } - - foreach (DictionaryEntry entry in options) - { - if (entry.Key.Equals(inputAsInt)) - { - Console.WriteLine($"value: {entry.Value}"); - Console.WriteLine($"type: {entry.Value?.GetType()}"); - return entry.Value != null ? (DriveInfo)entry.Value : null; - } - } - - return null; - } - - public static DriveInfo? SelectFromList(IEnumerable volumes) - { - int i = 0; - Hashtable options = []; - - // prepare table and present options to user - Console.WriteLine("Select the drive you want to backup to (enter a number): \n"); - foreach (DriveInfo volume in volumes) - { - ++i; - string option = $"{i}: {volume.Name} ({GetSizeInGB(volume)}GB available)"; - Console.WriteLine(option); - options.Add(i, volume); - } - - try - { - // parse user input and return appropiate drive info - return GetSelection(options); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - return null; - } - } - - public static double GetSizeInGB(DriveInfo volume) => volume.TotalSize / 1024 / 1024 / 1024; - - public static DirectoryInfo? CreateBackupDirectory(DriveInfo volume) - { - bool isSystemDrive = volume.DriveType == DriveType.Fixed; - - try - { - if (isSystemDrive) - { - Console.WriteLine("Using system drive. Directing you to user folder..."); - - string? userPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - DirectoryInfo userDir = new(userPath); - - Console.WriteLine($"Checking for folder `.ghbackups` at `{userDir.FullName}`..."); - DirectoryInfo? backupDir = userDir.CreateSubdirectory(".ghbackups"); - - return backupDir; - } - else - { - Console.WriteLine($"Using {volume.Name}. Directing you to root folder..."); - - DirectoryInfo? rootDir = volume.RootDirectory; - Console.WriteLine($"Checking for folder `.ghbackups` at `{rootDir.FullName}`..."); - DirectoryInfo? backupDir = rootDir.CreateSubdirectory(".ghbackups"); - - return backupDir; - } - } - catch (Exception e) - { - Console.WriteLine(e.Message); - 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; + Console.WriteLine(e.Message); + return []; } } + + private static DriveInfo? GetSelectedDrive(Hashtable options) + { + int inputAsInt = CommandLineTools.ReadAsInt(); + + if (!options.ContainsKey(inputAsInt)) return null; + + foreach (DictionaryEntry entry in options) + { + if (entry.Key.Equals(inputAsInt)) + { + return entry.Value != null ? (DriveInfo)entry.Value : null; + } + } + + return null; + } + + public static DriveInfo? SelectFromList(IEnumerable volumes) + { + int i = 0; + Hashtable options = []; + + // prepare table and present options to user + Console.WriteLine("Select the drive you want to backup to (enter a number): \n"); + foreach (DriveInfo volume in volumes) + { + ++i; + string option = $"{i}: {volume.Name} ({GetSizeInGB(volume)}GB available)"; + Console.WriteLine(option); + options.Add(i, volume); + } + + try + { + // parse user input and return appropiate drive info + return GetSelectedDrive(options); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + return null; + } + } + + public static DirectoryInfo? CreateAnchor(DirectoryInfo backupDir, string anchorName) + { + return backupDir.CreateSubdirectory(anchorName); + } + + public static string? FindMainBackupFile() + { + // find a folder entitled `.ghbackups` + // check the system drive first, then scan for additional drives and check the root of each + + var userFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + bool backupExistsInUserFolder = Directory.Exists($"{userFolder}/.ghbackups"); + + if (backupExistsInUserFolder) return $"{userFolder}/.ghbackups"; + + DriveInfo[] volumes = GetVolumes(); + foreach (DriveInfo volume in volumes) + { + bool backupExistsInRoot = Directory.Exists($"{volume.RootDirectory.FullName}/.ghbackups"); + if (backupExistsInRoot) return $"{volume.RootDirectory.FullName}/.ghbackups"; + } + + return null; + } + + public static double GetSizeInGB(DriveInfo volume) => volume.TotalSize / 1024 / 1024 / 1024; + + 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; + } }