Skip to content

Commit

Permalink
Adding update command (#418)
Browse files Browse the repository at this point in the history
Need a command to update bmx with a new version if one is available
  • Loading branch information
gord5500 authored Dec 21, 2023
1 parent cc7b135 commit 38aed87
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/D2L.Bmx/BmxPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ internal static class BmxPaths {
public static readonly string SESSIONS_FILE_LEGACY_NAME = Path.Join( BMX_DIR, "sessions" );
public static readonly string UPDATE_CHECK_FILE_NAME = Path.Join( CACHE_DIR, "updateCheck" );
public static readonly string AWS_CREDS_CACHE_FILE_NAME = Path.Join( CACHE_DIR, "awsCreds" );
public static readonly string OLD_BMX_VERSIONS_PATH = Path.Join( BMX_DIR, "temp" );
}
37 changes: 37 additions & 0 deletions src/D2L.Bmx/GithubRelease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;

namespace D2L.Bmx;
internal record GithubRelease {
[JsonPropertyName( "tag_name" )]
public string? TagName { get; init; }

[JsonPropertyName( "assets" )]
public List<GithubAsset>? Assets { get; init; }

public Version? GetReleaseVersion() {
string? version = TagName?.TrimStart( 'v' );
if( version is null ) {
return null;
}
return new Version( version );
}

public static async Task<GithubRelease?> GetLatestReleaseDataAsync() {
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri( "https://api.github.com" );
httpClient.Timeout = TimeSpan.FromSeconds( 2 );
httpClient.DefaultRequestHeaders.Add( "User-Agent", "BMX" );
return await httpClient.GetFromJsonAsync(
"repos/Brightspace/bmx/releases/latest",
SourceGenerationContext.Default.GithubRelease );
}
}

internal record GithubAsset {
[JsonPropertyName( "name" )]
public string? Name { get; init; }

[JsonPropertyName( "browser_download_url" )]
public string? BrowserDownloadUrl { get; init; }
}
8 changes: 8 additions & 0 deletions src/D2L.Bmx/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,20 @@
);
} );

var updateCommand = new Command( "update", "Updates BMX to the latest version" );
updateCommand.SetHandler( ( InvocationContext context ) => {
var handler = new UpdateHandler();
return handler.HandleAsync();
} );

// root command
var rootCommand = new RootCommand( "BMX grants you API access to your AWS accounts!" ) {
// put more frequently used commands first, as the order here affects help text
printCommand,
writeCommand,
loginCommand,
configureCommand,
updateCommand,
};

// start bmx
Expand All @@ -226,6 +233,7 @@
}
}

UpdateHandler.Cleanup();
await UpdateChecker.CheckForUpdatesAsync( configProvider.GetConfiguration() );

await next( context );
Expand Down
37 changes: 8 additions & 29 deletions src/D2L.Bmx/UpdateChecker.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace D2L.Bmx;

Expand All @@ -12,7 +11,12 @@ public static async Task CheckForUpdatesAsync( BmxConfig config ) {
var localVersion = Assembly.GetExecutingAssembly().GetName().Version;
var latestVersion = new Version( cachedVersion?.VersionName ?? "0.0.0" );
if( ShouldFetchLatestVersion( cachedVersion ) ) {
latestVersion = new Version( await GetLatestReleaseVersionAsync() );
GithubRelease? releaseData = await GithubRelease.GetLatestReleaseDataAsync();
latestVersion = releaseData?.GetReleaseVersion();
if( latestVersion is null ) {
return;
}
SaveVersion( latestVersion );
}

string updateLocation = string.Equals( config.Org, "d2l", StringComparison.OrdinalIgnoreCase )
Expand Down Expand Up @@ -49,30 +53,9 @@ private static void DisplayUpdateMessage( string message ) {
Console.Error.WriteLine();
}

private static async Task<string> GetLatestReleaseVersionAsync() {
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri( "https://api.github.com" );
httpClient.Timeout = TimeSpan.FromSeconds( 2 );
httpClient.DefaultRequestHeaders.Add( "User-Agent", "BMX" );
var response = await httpClient.GetAsync( "repos/Brightspace/bmx/releases/latest" );
response.EnsureSuccessStatusCode();

await using var responseStream = await response.Content.ReadAsStreamAsync();
var releaseData = await JsonSerializer.DeserializeAsync(
responseStream,
SourceGenerationContext.Default.GithubRelease
);
string version = releaseData?.TagName?.TrimStart( 'v' ) ?? string.Empty;
SaveLatestVersion( version );
return version;
}

private static void SaveLatestVersion( string version ) {
if( string.IsNullOrWhiteSpace( version ) ) {
return;
}
private static void SaveVersion( Version version ) {
var cache = new UpdateCheckCache {
VersionName = version,
VersionName = version.ToString(),
TimeLastChecked = DateTimeOffset.UtcNow
};
string content = JsonSerializer.Serialize( cache, SourceGenerationContext.Default.UpdateCheckCache );
Expand Down Expand Up @@ -112,10 +95,6 @@ private static bool ShouldFetchLatestVersion( UpdateCheckCache? cache ) {
}
}

internal record GithubRelease {
[JsonPropertyName( "tag_name" )]
public string? TagName { get; set; }
}

internal record UpdateCheckCache {
public string? VersionName { get; set; }
Expand Down
156 changes: 156 additions & 0 deletions src/D2L.Bmx/UpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System.Formats.Tar;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.InteropServices;

namespace D2L.Bmx;

internal class UpdateHandler {

public async Task HandleAsync() {
if( !Directory.Exists( BmxPaths.OLD_BMX_VERSIONS_PATH ) ) {
try {
Directory.CreateDirectory( BmxPaths.OLD_BMX_VERSIONS_PATH );
} catch( Exception ex ) {
throw new BmxException( "Failed to initialize temporary BMX file directory (~/.bmx/temp)", ex );
}
}

using var httpClient = new HttpClient();
GithubRelease? releaseData = await GithubRelease.GetLatestReleaseDataAsync();
Version? latestVersion = releaseData?.GetReleaseVersion();
if( latestVersion is null ) {
throw new BmxException( "Failed to find the latest version of BMX." );
}

var localVersion = Assembly.GetExecutingAssembly().GetName().Version;
if( latestVersion <= localVersion ) {
Console.WriteLine( $"You already have the latest version {latestVersion}" );
return;
}

string archiveName = GetOSFileName();
string? downloadUrl = releaseData?.Assets?.FirstOrDefault( a => a.Name == archiveName )?.BrowserDownloadUrl;
if( string.IsNullOrWhiteSpace( downloadUrl ) ) {
throw new BmxException( "Failed to find the download URL of the latest BMX" );
}

string? currentFilePath = Environment.ProcessPath;
if( string.IsNullOrEmpty( currentFilePath ) ) {
throw new BmxException( "BMX could not update" );
}

string downloadPath = Path.GetTempFileName();
try {
var archiveRes = await httpClient.GetAsync( downloadUrl, HttpCompletionOption.ResponseHeadersRead );
using var fs = new FileStream( downloadPath, FileMode.Create, FileAccess.Write, FileShare.None );
await archiveRes.Content.CopyToAsync( fs );
await fs.FlushAsync();
} catch( Exception ex ) {
throw new BmxException( "Failed to download the update", ex );
}

string extractFolder = Path.Combine( Path.GetTempPath(), Path.GetRandomFileName() );
try {
Directory.CreateDirectory( extractFolder );
} catch( Exception ex ) {
File.Delete( downloadPath );
throw new BmxException( "Failed to initialize temporary folder for downloaded file", ex );
}

string currentDirectory = Path.GetDirectoryName( currentFilePath )!;
long backupPathTimeStamp = DateTime.Now.Millisecond;
string backupPath = Path.Join( BmxPaths.OLD_BMX_VERSIONS_PATH, $"bmx-v{localVersion}-{backupPathTimeStamp}-old.bak" );
try {
string extension = Path.GetExtension( downloadUrl );

if( extension.Equals( ".zip", StringComparison.OrdinalIgnoreCase ) ) {
ExtractZipFile( downloadPath, extractFolder );
} else if( extension.Equals( ".gz", StringComparison.OrdinalIgnoreCase ) ) {
ExtractTarGzipFile( downloadPath, extractFolder );
} else {
throw new Exception( "Unknown archive type" );
}
} catch( Exception ex ) {
Directory.Delete( extractFolder, recursive: true );
throw new BmxException( "Failed to update with new files", ex );
} finally {
File.Delete( downloadPath );
}

try {
File.Move( currentFilePath, backupPath );
} catch( IOException ex ) {
Directory.Delete( extractFolder, recursive: true );
throw new BmxException( "Could not remove the old version. Please try again with elevated permissions.", ex );
} catch {
Directory.Delete( extractFolder, recursive: true );
throw new BmxException( "BMX could not update" );
}

try {
foreach( string file in Directory.GetFiles( extractFolder ) ) {
string destinationFile = Path.Combine( currentDirectory, Path.GetFileName( file ) );
File.Move( file, destinationFile );
}
} catch( Exception ex ) {
File.Move( backupPath, currentFilePath );
throw new BmxException( "BMX could not update with the new version", ex );
} finally {
Directory.Delete( extractFolder, recursive: true );
}
}

private static string GetOSFileName() {

if( RuntimeInformation.IsOSPlatform( OSPlatform.Windows ) ) {
return "bmx-win-x64.zip";
} else if( RuntimeInformation.IsOSPlatform( OSPlatform.OSX ) ) {
return "bmx-osx-x64.tar.gz";
} else if( RuntimeInformation.IsOSPlatform( OSPlatform.Linux ) ) {
return "bmx-linux-x64.tar.gz";
} else {
throw new BmxException( "New version does not support you current OS" );
}
}

public static void Cleanup() {
if( Directory.Exists( BmxPaths.OLD_BMX_VERSIONS_PATH ) ) {
try {
Directory.Delete( BmxPaths.OLD_BMX_VERSIONS_PATH, recursive: true );
} catch( Exception ) {
Console.Error.WriteLine( "WARNING: Failed to delete old version files" );
}
}
}

private static void ExtractTarGzipFile( string compressedFilePath, string decompressedFilePath ) {
string tarPath = Path.Combine( decompressedFilePath, "bmx.tar" );
using( FileStream compressedFileStream = File.Open(
compressedFilePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read )
) {
using FileStream outputFileStream = File.Create( tarPath );
using var decompressor = new GZipStream( compressedFileStream, CompressionMode.Decompress );
decompressor.CopyTo( outputFileStream );
}

try {
TarFile.ExtractToDirectory( tarPath, decompressedFilePath, true );
} finally {
File.Delete( tarPath );
}
}

private static void ExtractZipFile( string compressedFilePath, string decompressedFilePath ) {
using ZipArchive archive = ZipFile.OpenRead( compressedFilePath );
foreach( ZipArchiveEntry entry in archive.Entries ) {
string destinationPath = Path.GetFullPath( Path.Combine( decompressedFilePath, entry.FullName ) );
if( destinationPath.StartsWith( decompressedFilePath, StringComparison.Ordinal ) ) {
entry.ExtractToFile( destinationPath, overwrite: true );
}
}
}
}

0 comments on commit 38aed87

Please sign in to comment.