Merge pull request #497 from Cyberboss/TheRepoExceptDoneRight

Repository component
This commit is contained in:
Jordan Brown
2018-04-19 11:54:47 -04:00
committed by GitHub
11 changed files with 787 additions and 0 deletions
@@ -41,6 +41,12 @@ namespace Tgstation.Server.Api.Models.Internal
[Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
public bool PushTestMergeCommits { get; set; }
/// <summary>
/// If test merge commits are signed with the username of the person who merged it. Note this only affects future commits
/// </summary>
[Permissions(WriteRight = RepositoryRights.ChangeTestMergeCommits)]
public bool ShowTestMergeCommitters { get; set; }
/// <summary>
/// How often the <see cref="Repository"/> automatically updates in minutes
/// </summary>
@@ -26,6 +26,12 @@ namespace Tgstation.Server.Api.Models
[Permissions(DenyWrite = true)]
public RevisionInformation RevisionInformation { get; set; }
/// <summary>
/// If the repository was cloned from GitHub.com. If <see langword="true"/> this enables test merge functionality
/// </summary>
[Permissions(DenyWrite = true)]
public bool IsGitHub { get; set; }
/// <summary>
/// The branch or tag HEAD points to
/// </summary>
@@ -17,5 +17,10 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Required]
public string PullRequestRevision { get; set; }
/// <summary>
/// Optional comment about the test
/// </summary>
public string Comment { get; set; }
}
}
@@ -0,0 +1,84 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Represents an on-disk git repository
/// </summary>
interface IRepository : IDisposable
{
/// <summary>
/// If the <see cref="IRepository"/> was cloned from GitHub.com
/// </summary>
bool IsGitHubRepository { get; }
/// <summary>
/// The SHA of the <see cref="IRepository"/> HEAD
/// </summary>
string Head { get; }
/// <summary>
/// The current reference the <see cref="IRepository"/> HEAD is using. This can be a branch or tag
/// </summary>
string Reference { get; }
/// <summary>
/// The current origin remote the <see cref="IRepository"/> is using
/// </summary>
string Origin { get; }
/// <summary>
/// Checks out a given <paramref name="committish"/>
/// </summary>
/// <param name="committish">The sha or reference to checkout</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CheckoutObject(string committish, CancellationToken cancellationToken);
/// <summary>
/// Attempt to merge a GitHub pull request into HEAD
/// </summary>
/// <param name="pullRequestNumber">The pull request number on the remote repository</param>
/// <param name="targetCommit">The commit in the pull request to merge</param>
/// <param name="committerName">The name of the merge committer</param>
/// <param name="committerEmail">The e-mail of the merge committer</param>
/// <param name="commitBody">The body of the commit message</param>
/// <param name="accessString">The access string to fetch from the origin repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the SHA of the new HEAD on success, <see langword="null"/> on merge conflict</returns>
Task<string> AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken);
/// <summary>
/// Fetch commits from the origin repository
/// </summary>
/// <param name="accessString">The access string to fetch from the origin repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task FetchOrigin(string accessString, CancellationToken cancellationToken);
/// <summary>
/// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the SHA of the new HEAD</returns>
Task<string> ResetToOrigin(CancellationToken cancellationToken);
/// <summary>
/// Force push the current repository HEAD to <see cref="Repository.RemoteTemporaryBranchName"/>;
/// </summary>
/// <param name="accessString">The access string to fetch from the origin repository</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task PushHeadToTemporaryBranch(string accessString, CancellationToken cancellationToken);
/// <summary>
/// Copies the current working directory to a given <paramref name="path"/>
/// </summary>
/// <param name="path">The path to copy repository contents to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CopyTo(string path, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Factory for creating and loading <see cref="IRepository"/>s
/// </summary>
interface IRepositoryManager
{
/// <summary>
/// Attempt to load the <see cref="IRepository"/> from the default location
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The loaded <see cref="IRepository"/></returns>
Task<IRepository> LoadRepository(CancellationToken cancellationToken);
/// <summary>
/// Delete the current <see cref="IRepository"/> and replaces it with a clone of the repository at <paramref name="url"/>
/// </summary>
/// <param name="url">The location of the remote repository to clone</param>
/// <param name="accessString">The access string to clone from <paramref name="url"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>The newly cloned <see cref="IRepository"/></returns>
Task<IRepository> CloneRepository(string url, string accessString, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,223 @@
using LibGit2Sharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class Repository : IRepository
{
/// <summary>
/// The branch name used for publishing testmerge commits
/// </summary>
public const string RemoteTemporaryBranchName = "___TGSTempBranch";
/// <inheritdoc />
public bool IsGitHubRepository { get; }
/// <inheritdoc />
public string Head => repository.Head.Tip.Sha;
/// <inheritdoc />
public string Reference => repository.Head.FriendlyName;
/// <inheritdoc />
public string Origin => repository.Network.Remotes.First().Url;
/// <summary>
/// The <see cref="LibGit2Sharp.IRepository"/> for the <see cref="Repository"/>
/// </summary>
readonly LibGit2Sharp.IRepository repository;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="Repository"/>
/// </summary>
readonly IIOManager ioMananger;
/// <summary>
/// Construct a <see cref="Repository"/>
/// </summary>
/// <param name="repository">The value of <see cref="repository"/></param>
/// <param name="ioMananger">The value of <see cref="ioMananger"/></param>
public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
IsGitHubRepository = Origin.ToUpperInvariant().Contains("GITHUB.COM");
}
/// <inheritdoc />
public void Dispose() => repository.Dispose();
/// <summary>
/// Convert <paramref name="url"/> to an "https://<paramref name="accessString"/>@{url} equivalent
/// </summary>
/// <param name="url">The URL to convert</param>
/// <param name="accessString">The <see cref="string"/> containing authentication info for the remote repository</param>
/// <returns>An authenticated URL for accessing the remote repository</returns>
public static string GenerateAuthUrl(string url, string accessString)
{
if (url == null)
throw new ArgumentNullException(nameof(url));
if (String.IsNullOrWhiteSpace(accessString))
return url;
const string HttProtocolSecure = "HTTPS://";
if (!url.ToUpperInvariant().StartsWith(HttProtocolSecure, StringComparison.InvariantCulture))
throw new InvalidOperationException("Cannot use access string without HTTPS remote!");
//ONLY support https urls
return url.ToUpperInvariant().Replace(HttProtocolSecure, String.Concat(HttProtocolSecure, accessString, '@'));
}
/// <summary>
/// Runs a blocking force checkout to <paramref name="committish"/>
/// </summary>
/// <param name="committish">The committish to checkout</param>
void RawCheckout(string committish)
{
Commands.Checkout(repository, committish, new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
repository.RemoveUntrackedFiles();
}
/// <inheritdoc />
public Task<string> AddTestMerge(int pullRequestNumber, string targetCommit, string committerName, string committerEmail, string commitBody, string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var Refspec = new List<string>();
var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", pullRequestNumber);
var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", pullRequestNumber, prBranchName);
Refspec.Add(String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", pullRequestNumber, prBranchName));
var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", pullRequestNumber);
var remote = repository.Network.Remotes.Add("temp_pr_fetch", GenerateAuthUrl(Origin, accessString));
try
{
cancellationToken.ThrowIfCancellationRequested();
Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, Refspec, new FetchOptions
{
Prune = true,
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested
}, logMessage);
}
catch (UserCancelledException) { }
finally
{
repository.Network.Remotes.Remove(remote.Name);
//commit is there and we never gc so
repository.Branches.Remove(localBranchName);
repository.Branches.Remove(prBranchName);
}
cancellationToken.ThrowIfCancellationRequested();
var originalCommit = repository.Head;
var result = repository.Merge(targetCommit, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions
{
CommitOnSuccess = true,
FailOnConflict = true,
FastForwardStrategy = FastForwardStrategy.NoFastForward,
SkipReuc = true,
});
if (result.Status != MergeStatus.NonFastForward)
{
RawCheckout(originalCommit.FriendlyName ?? originalCommit.Tip.Sha);
return null;
}
return result.Commit.Sha;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task CheckoutObject(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(() => RawCheckout(committish), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task FetchOrigin(string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var remote = repository.Network.Remotes.First();
try
{
Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, remote.FetchRefSpecs.Select(x => x.Specification), new FetchOptions
{
Prune = true,
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested
}, "Fetch origin commits");
}
catch (UserCancelledException)
{
cancellationToken.ThrowIfCancellationRequested();
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task PushHeadToTemporaryBranch(string accessString, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var branch = repository.CreateBranch(RemoteTemporaryBranchName);
try
{
cancellationToken.ThrowIfCancellationRequested();
var remote = repository.Network.Remotes.Add("temp_push", GenerateAuthUrl(Origin, accessString));
try
{
cancellationToken.ThrowIfCancellationRequested();
try
{
repository.Network.Push(remote, String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName), new PushOptions
{
OnPackBuilderProgress = (a, b, c) => !cancellationToken.IsCancellationRequested,
OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
OnPushTransferProgress = (a, b, c) => !cancellationToken.IsCancellationRequested
});
}
catch (UserCancelledException)
{
cancellationToken.ThrowIfCancellationRequested();
}
}
finally
{
repository.Network.Remotes.Remove(remote.Name);
}
}
finally
{
repository.Branches.Remove(branch);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task<string> ResetToOrigin(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (!repository.Head.IsTracking)
throw new InvalidOperationException("Cannot reset to origin while not on a tracked reference!");
var trackedBranch = repository.Head.TrackedBranch;
Commands.Checkout((LibGit2Sharp.Repository)repository, repository.Head.TrackedBranch, new CheckoutOptions
{
CheckoutModifiers = CheckoutModifiers.Force
});
repository.RemoveUntrackedFiles();
return trackedBranch.Tip.Sha;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public async Task CopyTo(string path, CancellationToken cancellationToken)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
await ioMananger.CopyDirectory(".", path, new List<string> { ".git" }, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,56 @@
using LibGit2Sharp;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class RepositoryManager : IRepositoryManager
{
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="RepositoryManager"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// Construct a <see cref="RepositoryManager"/>
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
public RepositoryManager(IIOManager ioManager) => this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
/// <inheritdoc />
public async Task<IRepository> CloneRepository(string url, string accessString, CancellationToken cancellationToken)
{
await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
await Task.Factory.StartNew(() =>
{
string path = null;
try
{
path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url, accessString), ioManager.ResolvePath("."), new CloneOptions
{
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
RecurseSubmodules = true,
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested
});
}
catch (UserCancelledException) { }
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
return await LoadRepository(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task<IRepository> LoadRepository(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
var repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
return (IRepository)new Repository(repo, ioManager);
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// <see cref="IIOManager"/> that resolves paths to <see cref="Environment.CurrentDirectory"/>
/// </summary>
class DefaultIOManager : IIOManager
{
/// <summary>
/// Default <see cref="FileStream"/> buffer size used by .NET
/// </summary>
public const int DefaultBufferSize = 4096;
/// <summary>
/// Recursively empty a directory
/// </summary>
/// <param name="dir"><see cref="DirectoryInfo"/> of the directory to empty</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
static async Task NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
{
var tasks = new List<Task>();
foreach (var subDir in dir.EnumerateDirectories())
{
cancellationToken.ThrowIfCancellationRequested();
tasks.Add(NormalizeAndDelete(subDir, cancellationToken));
}
foreach (var file in dir.EnumerateFiles())
{
cancellationToken.ThrowIfCancellationRequested();
file.Attributes = FileAttributes.Normal;
file.Delete();
}
await Task.WhenAll(tasks).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
dir.Delete(true);
}
/// <summary>
/// Copies a directory from <paramref name="src"/> to <paramref name="dest"/>
/// </summary>
/// <param name="src">The source directory path</param>
/// <param name="dest">The destination directory path</param>
/// <param name="ignore">Files and folders to ignore at the root level</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task CopyDirectoryImpl(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
{
await CreateDirectory(dest, cancellationToken).ConfigureAwait(false);
var dir = new DirectoryInfo(src);
cancellationToken.ThrowIfCancellationRequested();
var dirs = dir.EnumerateDirectories();
var files = dir.EnumerateFiles();
var fileCopyTasks = files.Select(x =>
{
cancellationToken.ThrowIfCancellationRequested();
if (ignore != null && ignore.Contains(x.Name))
return Task.CompletedTask;
return CopyFile(x.FullName, Path.Combine(dest, x.Name), cancellationToken);
});
var directoryCopyTasks = dirs.Select(x =>
{
cancellationToken.ThrowIfCancellationRequested();
if (ignore != null && ignore.Contains(x.Name))
return Task.CompletedTask;
return CopyDirectoryImpl(x.FullName, Path.Combine(dest, x.Name), null, cancellationToken);
});
await Task.WhenAll(fileCopyTasks.Concat(directoryCopyTasks)).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task CopyDirectory(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
{
if (dest == null)
throw new ArgumentNullException(nameof(src));
if (dest == null)
throw new ArgumentNullException(nameof(src));
src = ResolvePath(src);
dest = ResolvePath(dest);
await CopyDirectoryImpl(src, dest, ignore, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task AppendAllText(string path, string additional_contents, CancellationToken cancellationToken)
{
if (additional_contents == null)
throw new ArgumentNullException(nameof(additional_contents));
using (var destStream = new FileStream(ResolvePath(path), FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true))
{
var buf = Encoding.UTF8.GetBytes(additional_contents);
await destStream.WriteAsync(buf, 0, buf.Length, cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public string ConcatPath(params string[] paths)
{
if (paths == null)
throw new ArgumentNullException(nameof(paths));
return Path.Combine(paths);
}
/// <inheritdoc />
public async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
{
if (src == null)
throw new ArgumentNullException(nameof(src));
if (dest == null)
throw new ArgumentNullException(nameof(dest));
using (var srcStream = new FileStream(ResolvePath(src), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
using (var destStream = new FileStream(ResolvePath(dest), FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true))
await srcStream.CopyToAsync(destStream, DefaultBufferSize, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public async Task DeleteDirectory(string path, CancellationToken cancellationToken)
{
path = ResolvePath(path);
var di = new DirectoryInfo(path);
if (!di.Exists)
return;
await NormalizeAndDelete(di, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)));
/// <inheritdoc />
public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
/// <inheritdoc />
public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
/// <inheritdoc />
public Task<List<string>> GetFilesWithExtension(string path, string extension, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
path = ResolvePath(path);
if (extension == null)
throw new ArgumentNullException(extension);
var results = new List<string>();
foreach (var I in Directory.EnumerateFiles(path, String.Format(CultureInfo.InvariantCulture, "*.{0}", extension), SearchOption.TopDirectoryOnly))
{
cancellationToken.ThrowIfCancellationRequested();
results.Add(I);
}
return results;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (destination == null)
throw new ArgumentNullException(nameof(destination));
source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
destination = ResolvePath(destination);
File.Move(source, destination);
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
{
path = ResolvePath(path);
using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
{
byte[] buf;
buf = new byte[file.Length];
await file.ReadAsync(buf, 0, (int)file.Length, cancellationToken).ConfigureAwait(false);
return buf;
}
}
/// <inheritdoc />
public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
/// <inheritdoc />
public async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
{
path = ResolvePath(path);
using (var file = File.Open(path, FileMode.Create, FileAccess.Write))
await file.WriteAsync(contents, 0, contents.Length, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Interface for using filesystems
/// </summary>
interface IIOManager
{
/// <summary>
/// Retrieve the full path of some <paramref name="path"/> given a relative path. Must be used before passing relative paths to other APIs. All other operations in this <see langword="interface"/> call this internally on given paths
/// </summary>
/// <param name="path">Some path to retrieve the full path of</param>
/// <returns><paramref name="path"/> as a full canonical path</returns>
string ResolvePath(string path);
/// <summary>
/// Gets the file name portion of a <paramref name="path"/>
/// </summary>
/// <param name="path">The path to get the file name of</param>
/// <returns>The file name portion of <paramref name="path"/></returns>
string GetFileName(string path);
/// <summary>
/// Gets the file name portion of a <paramref name="path"/> with
/// </summary>
/// <param name="path">The path to get the file name of</param>
/// <returns>The file name portion of <paramref name="path"/></returns>
string GetFileNameWithoutExtension(string path);
/// <summary>
/// Copies a directory from <paramref name="src"/> to <paramref name="dest"/>
/// </summary>
/// <param name="src">The source directory path</param>
/// <param name="dest">The destination directory path</param>
/// <param name="ignore">Files and folders to ignore at the root level</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CopyDirectory(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken);
/// <summary>
/// Check that the file at <paramref name="path"/> exists
/// </summary>
/// <param name="path">The file to check for existence</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> resulting in <see langword="true"/> if the file at <paramref name="path"/> exists, <see langword="false"/> otherwise</returns>
Task<bool> FileExists(string path, CancellationToken cancellationToken);
/// <summary>
/// Returns all the contents of a file at <paramref name="path"/> as a <see cref="byte"/> array
/// </summary>
/// <param name="path">The path of the file to read</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> that results in the contents of a file at <paramref name="path"/></returns>
Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken);
/// <summary>
/// Writes some <paramref name="contents"/> to a file at <paramref name="path"/> overwriting previous content
/// </summary>
/// <param name="path">The path of the file to write</param>
/// <param name="contents">The contents of the file</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken);
/// <summary>
/// Copy a file from <paramref name="src"/> to <paramref name="dest"/>
/// </summary>
/// <param name="src">The source file to copy</param>
/// <param name="dest">The destination path</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CopyFile(string src, string dest, CancellationToken cancellationToken);
/// <summary>
/// Gets the directory portion of a given <paramref name="path"/>
/// </summary>
/// <param name="path">A path to check</param>
/// <returns>The directory portion of the given <paramref name="path"/></returns>
string GetDirectoryName(string path);
/// <summary>
/// Gets a list of files in <paramref name="path"/> with the given <paramref name="extension"/>
/// </summary>
/// <param name="path">The directory which contains the files</param>
/// <param name="extension">The extension to look for without the preceeding "."</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> resulting in a list of paths to files in <paramref name="path"/> with the given <paramref name="extension"/></returns>
Task<List<string>> GetFilesWithExtension(string path, string extension, CancellationToken cancellationToken);
/// <summary>
/// Deletes a file at <paramref name="path"/>
/// </summary>
/// <param name="path">The path of the file to delete</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task DeleteFile(string path, CancellationToken cancellationToken);
/// <summary>
/// Create a directory at <paramref name="path"/>
/// </summary>
/// <param name="path">The path of the directory to create</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CreateDirectory(string path, CancellationToken cancellationToken);
/// <summary>
/// Recursively delete a directory
/// </summary>
/// <param name="path">The path to the directory to delete</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task DeleteDirectory(string path, CancellationToken cancellationToken);
/// <summary>
/// Combines an array of strings into a path
/// </summary>
/// <param name="paths">The paths to combine</param>
/// <returns>The combined path</returns>
string ConcatPath(params string[] paths);
/// <summary>
/// Moves a file at <paramref name="source"/> to <paramref name="destination"/>
/// </summary>
/// <param name="source">The source file path</param>
/// <param name="destination">The destination path</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task MoveFile(string source, string destination, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,39 @@
using System;
using System.IO;
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// An <see cref="IIOManager"/> that resolve relative paths from another <see cref="IIOManager"/> to a subdirectory of that
/// </summary>
sealed class ResolvingIOManager : DefaultIOManager
{
/// <summary>
/// Path to the subdirectory attached to path resolutions
/// </summary>
readonly string subdirectory;
/// <summary>
/// Construct a <see cref="ResolvingIOManager"/>
/// </summary>
/// <param name="parent">The <see cref="IIOManager"/> that resolves to the directory to work out of</param>
/// <param name="_subdirectory">The value of <see cref="subdirectory"/></param>
public ResolvingIOManager(IIOManager parent, string _subdirectory)
{
if(parent == null)
throw new ArgumentNullException(nameof(parent));
if (_subdirectory == null)
throw new ArgumentNullException(nameof(_subdirectory));
subdirectory = ConcatPath(parent.ResolvePath("."), _subdirectory);
}
/// <inheritdoc />
public override string ResolvePath(string path)
{
if (!Path.IsPathRooted(path))
return base.ResolvePath(ConcatPath(subdirectory, path));
return path;
}
}
}
@@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="Byond.TopicSender" Version="1.1.0.1" />
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.1.0" />
<PackageReference Include="LibGit2Sharp" Version="0.26.0-preview-0017" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.0-preview2-final" />