edge-install/Program.cs
Robert Morrison 60e06b2144
Another bad commit
Lots of changes here...

- Removed code using RecursivExtractor due to bad usage of /tmp
  Note: the code that used RecursiveExtractor may not have been in the
  previous commit
- Created _functioning_ implementation of DebUnpacker
- Restructured Project layout.

TODO:
- Possibly rewrite how the DebUnpacker works to have the file contents
  part of an entry.
- Further restructuring and refactoring to make the code a little
  neater.
- Add code for the final unpack steps Un-XZ -> untar -> write to disk
- Add code to install
- Add code for modified post-install pre-remove etc..
  This is kinda needed to ensure proper system integration of new
  packages.
2023-06-08 04:36:31 +01:00

299 lines
11 KiB
C#

//using System.CommandLine;
using Serilog;
using Spectre.Console;
using System.Text.Json;
using System.Text;
namespace EdgeInstaller;
public static partial class EdgeInstall
{
// NOTE: tweaking these values can be used to make downloads look more "fancy"
// A.K.A make downloads take longer so the progress bar appears for a little longer
private static readonly int downloadDelayMS = 0;
private static readonly int downloadBufferBYTES = 10240;
// The default architecture to get
// likely will go unused until proper cli interface is added
private static readonly string _DefaultArch = "amd64";
private static HttpClient _client = new HttpClient();
private static readonly string DataDir =
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ClosedLess",
"EdgeInstall-Jankmode");
public static async Task<int> Main()
{
_client.BaseAddress = new Uri("https://packages.microsoft.com/repos/edge/");
// Ensure DataDir exists.
// NOTE: Directory.CreateDirectory will ALWAYS create the entire path
Directory.CreateDirectory(DataDir);
BUGFIX_ansiDetection();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(Path.Combine(DataDir, ".EdgeInstaller.Log")) // Only ever log to the logfile
.CreateLogger();
var Release = AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Getting Latest Release data",
_ => GetRelease()
);
string ReleaseString = await Release;
Log.Debug("{rs}", ReleaseString);
AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]");
ReleaseData data = new(ReleaseString);
RenderReleaseTable(data);
// only select arch if needed
string TargetArch = data.Architectures.Count > 1 ? AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose Architecture")
.PageSize(5)
.MoreChoicesText("[grey]Scroll for more...[/]")
.AddChoices(data.Architectures)
) : data.Architectures.First();
// create a directory for the chosen architecture
// NOTE: we also grab the path here so we can use it later.
var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, TargetArch)).FullName;
// Check if the Release data we just grabbed is newer than the one that exists.
// NOTE: we only do this if our chosen architecture hasn't been downloaded
bool NeedDownload = true;
Log.Information("Reached Package download Stage");
if (File.Exists(Path.Combine(Archdir, "Packages.gz")))
{
Log.Information("Checking for release.json");
// we need to check the release since we have an existing package.gz
string JSONstring = File.ReadAllText(Path.Combine(DataDir, "release.json"));
ReleaseData? oldData = JsonSerializer.Deserialize<ReleaseData>(JSONstring);
if (oldData is not null)
{
// if equal then our data is the newest available
if (DateTime.Compare(oldData.Date, data.Date) == 0)
{
Log.Information("Our packages.gz is up to date");
NeedDownload = false;
}
}
}
if (NeedDownload)
{
PackageFile gzFile =
data.PackageFiles
.Where(
file =>
file.filename.Contains(".gz") &&
file.filename.Contains(TargetArch)) // ensure we only fetch for the chosen architecture
.First();
if (gzFile is null)
{
throw new NullReferenceException("OOPS gzfile null");
}
string URL = "dists/stable/" + gzFile.filename;
string filename = await AnsiConsole.Progress()
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn(),
})
.StartAsync<string>(async ctx =>
{
var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false });
return await downloadFile(URL, Archdir, task);
});
string JSONdata = JsonSerializer.Serialize<ReleaseData>(data);
File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata);
if (!ValidateChecksum(new FileInfo(filename), gzFile.Checksum))
{
File.Delete(filename);
Environment.Exit(1);
}
}
string PackagesFile = Path.Combine(Archdir, "Packages.gz");
string PackagesString = GetPackagesString(PackagesFile);
AnsiConsole.MarkupLineInterpolated($"Read {PackagesFile} see log for contents");
Log.Debug("{packagesString}", PackagesString);
List<Package> packages = new();
StringBuilder sb = new();
foreach (var line in PackagesString.Split('\n'))
{
if (line == "")
{
string PackageString = sb.ToString();
if (PackageString == "")
{
continue; // The last "package" will be empty
}
sb.Clear(); // ensure we clear the StringBuilder when we are done with it.
try
{ // Just in case we somehow get a broken Package
Package p = new Package(PackageString);
packages.Add(p);
}
catch (Exception e)
{
AnsiConsole.MarkupLine("[red]Error:[/] Could not create package from string");
AnsiConsole.MarkupLine("[yellow i]see log for more details[/]");
Log.Error("Could not create Package from passed string:\n{string}", PackageString);
Log.Error(e, "Exception thrown");
}
}
sb.AppendLine(line);
}
/* INFO: This LINQ query allows sorting.
* Pay special attention to the usage of the version comparer
*/
packages = packages
.OrderBy(package => package.PackageName)
.ThenByDescending(package => package.Version).ToList();
// Print any packages we have
var Variant = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose variant")
.PageSize(4)
.AddChoices(new[] {
"stable",
"beta",
"dev",
"all"
})
);
List<Package> VariantPackages = new();
if (Variant == "" || Variant == "all")
{
VariantPackages = packages;
}
else
{
VariantPackages = packages
.Where(package =>
package.PackageName.Contains(Variant))
.ToList();
}
RenderPackagesTable(VariantPackages);
var Package = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose Package to Install")
.PageSize(5)
.AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct())
);
var ChosenPackages = packages
.Where(package =>
package.PackageName == Package)
.ToList();
Package LatestVersion = ChosenPackages.OrderByDescending(pack => pack.Version).First();
RenderPackageAsTable(LatestVersion);
var versionPath = LatestVersion.Filename
.Replace("pool/main/m/", "")
.Replace(".deb", "");
var versionsDir = Path.Join(Archdir, "Versions");
// Let's get and unpack the latest version
if (true)//!Directory.Exists(Path.Join(versionsDir, versionPath)))
{
using (MemoryStream debStream =
await AnsiConsole.Progress()
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn(),
})
.StartAsync<MemoryStream>(async ctx =>
{
var task = ctx.AddTask(LatestVersion.Filename, new ProgressTaskSettings { AutoStart = false });
return await downloadStream(LatestVersion.Filename, task);
}))
{
if (!ValidateChecksum(debStream, LatestVersion.SHA256))
{
AnsiConsole.MarkupLine("[red b]ERROR:[/] checksum did not validate");
Environment.Exit(1);
}
using (DebFile debfile = new DebFile(debStream))
{
foreach (var fileEntry in debfile.fileEntries)
{
AnsiConsole.MarkupLineInterpolated($"FileName: {fileEntry.name}");
AnsiConsole.MarkupLineInterpolated($"FilePos: {fileEntry.offset}");
AnsiConsole.MarkupLineInterpolated($"FileSize: {fileEntry.sizeBytes}");
// NOTE: This is dumb test code
// to verify the files are actually being "extracted" properly
if (!debfile.getFile(fileEntry, out Stream contents))
{
return 1;
}
// Now we write this to tmp.
// WARN: Without deleting these you will fill up your ram
var temp = Path.GetTempFileName();
using (var tmpfile = File.Open(temp, FileMode.Open))
{
contents.CopyTo(tmpfile);
contents.Dispose();
}
}
}
}
return 0;
}
}
/*TODO:
- Check version
- Offer update
- Make SENSIBLE backup
- Do install
= if install fails revert backup
*/
}