204 lines
9.2 KiB
C#
204 lines
9.2 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using Renci.SshNet;
|
|
using Spectre.Console;
|
|
|
|
namespace aeqw89.tools.Publish;
|
|
|
|
sealed record DestinationContext(
|
|
ProgressTask Task,
|
|
ProgressContext ProgressContext,
|
|
MemoryStream Reader,
|
|
FileInfo PackageFile,
|
|
string Name,
|
|
long BufferSize,
|
|
long PackageSize
|
|
);
|
|
|
|
interface IDestination {
|
|
Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default);
|
|
}
|
|
|
|
sealed record LocalDestination(DestinationContext Context) : IDestination {
|
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
|
// Path = "$USER/local-%ctx.name/%pkg.filename"
|
|
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Context.Name, Context.PackageFile.Name);
|
|
try {
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!); // create the directory
|
|
await using var writer = File.OpenWrite(path);
|
|
var buffer = new byte[Context.BufferSize];
|
|
int read;
|
|
do {
|
|
read = await Context.Reader.ReadAsync(buffer, ct);
|
|
writer.Write(buffer, 0, read);
|
|
Context.Task.Increment(read);
|
|
} while (read > 0);
|
|
return Success.AsResult();
|
|
} catch (Exception e) {
|
|
return new ReadableError(string.Format(Exceptions.generic_error, e), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
sealed record CloudDestiantion(DestinationContext Context) : IDestination {
|
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
|
var connectionTask = Context.ProgressContext.AddTaskBefore($"Preparing cloud-{Context.Name}",
|
|
new ProgressTaskSettings() {
|
|
MaxValue = 100
|
|
}, Context.Task);
|
|
|
|
if (!SshHosts.TryGetHost(Context.Name, out var host)) {
|
|
return new ReadableError(string.Format(Exceptions.cloud_host_not_found.EscapeMarkup(), Context.Name), false);
|
|
}
|
|
|
|
var connectionInfo = SshHosts.GetConnection(Context.Name);
|
|
using var sshClient = new SshClient(connectionInfo);
|
|
|
|
if (!sshClient.IsConnected)
|
|
await sshClient.ConnectAsync(ct);
|
|
|
|
connectionTask.Increment(33); // ? One-Third of the way done (connection success).
|
|
|
|
var winC = sshClient.RunCommand("cmd /c ver");
|
|
var othC = sshClient.RunCommand("uname -s");
|
|
|
|
var os = (winC.ExitStatus, othC.ExitStatus) switch {
|
|
(0, _) => "windows",
|
|
(_, 0) => "linux",
|
|
_ => "unknown"
|
|
};
|
|
|
|
string remoteDirectory;
|
|
string packageFileDirectory;
|
|
|
|
if (os == "windows") {
|
|
var userDirC = sshClient.RunCommand("cmd /c echo %USERPROFILE%");
|
|
if (userDirC.ExitStatus != 0) { // * missing env variable case (could not work out where to place packages in remote).
|
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
|
userDirC.Result), false);
|
|
}
|
|
|
|
var userDir = userDirC.Result.Trim();
|
|
remoteDirectory = RemotePath.Combine(RemoteOs.Windows, userDir, "dotnet-packages");
|
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Windows, remoteDirectory, Context.PackageFile.Name);
|
|
|
|
var mkdirC = sshClient.RunCommand($"cmd /c if not exist \"{remoteDirectory}\" mkdir \"{remoteDirectory}\"");
|
|
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name, os,
|
|
mkdirC.Result), false);
|
|
}
|
|
}
|
|
else if (os == "linux") {
|
|
var homeDirC = sshClient.RunCommand("printf %s \"$HOME\"");
|
|
if (homeDirC.ExitStatus != 0) { // * missing env variable case 2 (could not work out where to place packages in remote).
|
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
|
homeDirC.Result), false);
|
|
}
|
|
|
|
var homeDir = homeDirC.Result.Trim(); // no CRLF on unix, but Trim() is safest
|
|
remoteDirectory = RemotePath.Combine(RemoteOs.Unix, homeDir, ".dotnet-packages");
|
|
packageFileDirectory = RemotePath.Combine(RemoteOs.Unix, remoteDirectory,
|
|
Context.PackageFile.Name);
|
|
|
|
// Use -p and single quotes to handle spaces safely
|
|
var mkdirC = sshClient.RunCommand($"mkdir -p '{remoteDirectory}'");
|
|
if (mkdirC.ExitStatus != 0) { // * MKDir error case (make directory command failed on remote).
|
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, remoteDirectory, Context.PackageFile.Name,
|
|
os, mkdirC.Result), false);
|
|
}
|
|
}
|
|
else {
|
|
return new ReadableError(string.Format(Exceptions.failed_to_prepare_server_directory, "n/a", Context.PackageFile.Name, os,
|
|
"Unsupported OS"), false); // * MacOS failure path (or other unrecognizable oses).
|
|
}
|
|
|
|
connectionTask.Increment(33); // ? Two-Thirds of the way done (directory located and ready).
|
|
|
|
sshClient.Disconnect();
|
|
|
|
using var client = new SftpClient(connectionInfo);
|
|
if (!client.IsConnected)
|
|
await client.ConnectAsync(ct);
|
|
connectionTask.Increment(33); // ? Connection task complete (now proceeding to upload task).
|
|
connectionTask.StopTask();
|
|
|
|
await using var writer = client.OpenWrite(packageFileDirectory);
|
|
byte[] buffer = new byte[Context.BufferSize];
|
|
int read;
|
|
do {
|
|
read = await Context.Reader.ReadAsync(buffer, ct);
|
|
writer.Write(buffer, 0, read);
|
|
Context.Task.Increment(read);
|
|
} while (read > 0);
|
|
|
|
return Success.AsResult();
|
|
}
|
|
}
|
|
|
|
|
|
sealed record GitDestination(DestinationContext Context, string Source, string ApiKey, bool Verbose = false) : IDestination {
|
|
private static Result<string, ReadableError> GetApiKey(string host) {
|
|
var key = Environment.GetEnvironmentVariable($"git-{host}-packages-key");
|
|
if (key is null) return new ReadableError(string.Format("No key stored in EnvironmentVariables with name {0}", $"git-{host}-packages-key"), false);
|
|
return key.Ok();
|
|
}
|
|
public static Result<GitDestination, ReadableError> CreateForGitea(DestinationContext context, string repoOwner, bool verbose = false) {
|
|
// gitea source structure = https://gitea.example.com/api/packages/{owner}/nuget/index.json
|
|
var keyResult = GetApiKey("gitea");
|
|
if (keyResult is ReadableError error) return error;
|
|
return new GitDestination(context, $"https://git.main.qwsdcvghyu.com/api/packages/{repoOwner}/nuget/index.json", keyResult.Unwrap(), verbose).Ok();
|
|
}
|
|
|
|
public static Result<GitDestination, ReadableError> CreateForGithub(DestinationContext context, string repoOwner, bool verbose = false) {
|
|
// github source structure = https://nuget.pkg.github.com/NAMESPACE/index.json
|
|
// namespace usually = repoOwner
|
|
var keyResult = GetApiKey("github");
|
|
if (keyResult is ReadableError error) return error;
|
|
return new GitDestination(context, $"https://nuget.pkg.github.com/{repoOwner}/index.json", keyResult.Unwrap(), verbose).Ok();
|
|
}
|
|
|
|
public async Task<Result<Success, ReadableError>> WaitForCompletion(CancellationToken ct = default) {
|
|
using var p = Process.Start(new ProcessStartInfo() {
|
|
FileName = "dotnet",
|
|
Arguments = $"nuget push \"{Context.PackageFile.FullName}\" -s {Source} -k {ApiKey}",
|
|
WorkingDirectory = Environment.CurrentDirectory,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
});
|
|
|
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
StringBuilder errorLines = new();
|
|
p?.ErrorDataReceived += (sender, eventArgs) => {
|
|
if (Verbose && eventArgs.Data != null)
|
|
AnsiConsole.WriteLine(eventArgs.Data);
|
|
errorLines.Append(eventArgs.Data);
|
|
};
|
|
p?.OutputDataReceived += (sender, eventArgs) => {
|
|
if (eventArgs.Data?.ToLower().Contains("press any key") == true)
|
|
cts.Cancel();
|
|
if (Verbose && eventArgs.Data != null)
|
|
AnsiConsole.WriteLine(eventArgs.Data);
|
|
};
|
|
|
|
p?.BeginOutputReadLine();
|
|
p?.BeginErrorReadLine();
|
|
|
|
Context.Task.Increment(Context.PackageSize / 2);
|
|
if (p != null)
|
|
try {
|
|
await (p?.WaitForExitAsync(cts.Token) ?? Task.CompletedTask);
|
|
}
|
|
catch (OperationCanceledException) {
|
|
p?.Kill();
|
|
await (p?.WaitForExitAsync(ct));
|
|
}
|
|
if (p?.ExitCode != 0) {
|
|
Context.Task.StopTask();
|
|
return new ReadableError(errorLines.ToString().EscapeMarkup() + "\n" + string.Format(Exceptions.dotnet_nuget_push_failure, p?.ExitCode ?? -1), false);
|
|
}
|
|
|
|
Context.Task.Increment(Context.PackageSize / 2);
|
|
return Success.AsResult();
|
|
}
|
|
} |