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.
299 lines
11 KiB
C#
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
|
|
*/
|
|
}
|