restructure for ease of use from command line
This commit is contained in:
@@ -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
|
||||
10
repo-backup-util/Actions/Base.cs
Normal file
10
repo-backup-util/Actions/Base.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GitAnchor.Actions;
|
||||
|
||||
public abstract class BaseAction {
|
||||
public virtual async Task<ActivityStatusCode> Run()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
107
repo-backup-util/Actions/Create.cs
Normal file
107
repo-backup-util/Actions/Create.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using GitAnchor.Lib;
|
||||
using Octokit;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GitAnchor.Actions;
|
||||
public class Create : BaseAction
|
||||
{
|
||||
public new static async Task<ActivityStatusCode> 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<Repository> repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found");
|
||||
|
||||
// organize and ensure all repositories are unique
|
||||
HashSet<Repository> 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<Repository> newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, anchorDir);
|
||||
|
||||
// identify all work to be done and prepare as tasks
|
||||
List<Task> 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);
|
||||
}
|
||||
}
|
||||
33
repo-backup-util/Actions/Help.cs
Normal file
33
repo-backup-util/Actions/Help.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GitAnchor.Actions;
|
||||
|
||||
public class Help : BaseAction
|
||||
{
|
||||
public static async new Task<ActivityStatusCode> 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 <name>] [--token <token>]");
|
||||
Console.WriteLine(" Creates a new backup directory and clones all repositories accessible by the provided token.");
|
||||
Console.WriteLine(" --name <name> The name of the backup directory to create. Defaults to the current date.");
|
||||
Console.WriteLine(" --token <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 <name>] [--verbose, -v]");
|
||||
Console.WriteLine(" Pulls all repositories in an existing backup directory.");
|
||||
Console.WriteLine(" --name <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 <name>]");
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
12
repo-backup-util/Actions/Pull.cs
Normal file
12
repo-backup-util/Actions/Pull.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace GitAnchor.Actions
|
||||
{
|
||||
internal class Pull : BaseAction
|
||||
{
|
||||
public static async new Task<ActivityStatusCode> Run()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Repository> repos = await GitHub.GetRepos(github) ?? throw new Exception("No repos found");
|
||||
|
||||
// organize and ensure all repositories are unique
|
||||
HashSet<Repository> 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<Repository> newProjects = Volumes.PopulateBackupDirectories(uniqueRepos, backupDir);
|
||||
IEnumerable<Repository> 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<Task> 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();
|
||||
await GitAnchor.Lib.EntryPoint.Run();
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>RepoBackupUtil</RootNamespace>
|
||||
<RootNamespace>GitAnchor</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
39
repo-backup-util/lib/CommandLine.cs
Normal file
39
repo-backup-util/lib/CommandLine.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
38
repo-backup-util/lib/Entrypoint.cs
Normal file
38
repo-backup-util/lib/Entrypoint.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using GitAnchor.Actions;
|
||||
|
||||
namespace GitAnchor.Lib;
|
||||
|
||||
public class EntryPoint
|
||||
{
|
||||
public static async Task<ActivityStatusCode> Run()
|
||||
{
|
||||
string[] args = Environment.GetCommandLineArgs();
|
||||
|
||||
SortedDictionary<string, Task<ActivityStatusCode>> options = new()
|
||||
{
|
||||
{ "create", Create.Run() },
|
||||
{ "pull", Pull.Run() },
|
||||
{ "help", Help.Run() }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string arg in args)
|
||||
{
|
||||
var result = options.TryGetValue(arg, out Task<ActivityStatusCode>? action);
|
||||
|
||||
if (!result) continue;
|
||||
if (action != null) return await action;
|
||||
}
|
||||
|
||||
return await Help.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
return ActivityStatusCode.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
91
repo-backup-util/lib/FileSystem.cs
Normal file
91
repo-backup-util/lib/FileSystem.cs
Normal file
@@ -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<DriveInfo> 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();
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using Octokit;
|
||||
|
||||
namespace RepoBackupUtil.Lib;
|
||||
public interface ICloneProject
|
||||
{
|
||||
Repository Repo { get; }
|
||||
DirectoryInfo BackupDir { get; }
|
||||
User User { get; }
|
||||
string Token { get; }
|
||||
}
|
||||
@@ -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<DriveInfo> 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<DriveInfo> 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<DriveInfo> 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<Repository> PopulateBackupDirectories(HashSet<Repository> repos, DirectoryInfo backupDir)
|
||||
{
|
||||
HashSet<Repository> 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<DriveInfo> 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<Repository> PopulateBackupDirectories(HashSet<Repository> repos, DirectoryInfo backupDir)
|
||||
{
|
||||
HashSet<Repository> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user