mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Merge pull request #497 from Cyberboss/TheRepoExceptDoneRight
Repository component
This commit is contained in:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user