11using System ;
2- using System . CommandLine . Invocation ;
32using System . IO ;
43using System . IO . Compression ;
5- using System . Net ;
4+ using System . Net . Http ;
65using System . Threading . Tasks ;
76using System . Timers ;
7+ using DotMake . CommandLine ;
88using NodeSwap . Utils ;
99using ShellProgressBar ;
1010
1111namespace NodeSwap . Commands ;
1212
13+ [ CliCommand (
14+ Description = "Install a version of Node.js" ,
15+ Parent = typeof ( RootCommand )
16+ ) ]
1317public class InstallCommand ( GlobalContext globalContext , NodeJsWebApi nodeWeb , NodeJs nodeLocal )
14- : ICommandHandler
1518{
16- public async Task < int > InvokeAsync ( InvocationContext context )
19+ [ CliArgument ( Description = "`latest`, specific e.g. `22.6.0`, or fuzzy e.g. `22.6` or `22`." ) ]
20+ public string Version { get ; set ; }
21+
22+ [ CliOption ( Description = "Re-install if installed already" ) ]
23+ public bool Force { get ; set ; }
24+
25+ public async Task < int > RunAsync ( )
1726 {
18- var rawVersion = context . ParseResult . ValueForArgument ( " version" ) ;
19- if ( rawVersion == null )
27+ // Retrieve and validate version argument
28+ if ( string . IsNullOrEmpty ( Version ) )
2029 {
2130 await Console . Error . WriteLineAsync ( "Missing version argument" ) ;
2231 return 1 ;
2332 }
2433
25- Version version ;
26- if ( rawVersion . ToString ( ) ? . ToLower ( ) == "latest" )
27- {
28- try
29- {
30- version = nodeWeb . GetLatestNodeVersion ( ) ;
31- }
32- catch ( Exception )
33- {
34- await Console . Error . WriteLineAsync ( "Unable to determine latest Node.js version." ) ;
35- return 1 ;
36- }
37- }
38- else if ( rawVersion . ToString ( ) ? . Split ( "." ) . Length < 3 )
34+ // Determine the version to install
35+ var version = await GetVersion ( Version ) ;
36+ if ( version == null ) return 1 ;
37+
38+ // Check if the requested version is already installed
39+ if ( ! Force && IsVersionInstalled ( version ) )
3940 {
40- try
41- {
42- version = nodeWeb . GetLatestNodeVersion ( rawVersion . ToString ( ) ) ;
43- }
44- catch ( Exception )
45- {
46- await Console . Error . WriteLineAsync ( $ "Unable to get latest Node.js version " +
47- $ "with prefix { rawVersion } .") ;
48- return 1 ;
49- }
41+ await Console . Error . WriteLineAsync ( $ "{ version } already installed") ;
42+ return 1 ;
5043 }
51- else
44+
45+ // Download and install Node.js
46+ var downloadUrl = nodeWeb . GetDownloadUrl ( version ) ;
47+ var zipPath = Path . Join ( globalContext . StoragePath , Path . GetFileName ( downloadUrl ) ) ;
48+ var downloadResult = await DownloadNodeJs ( downloadUrl , zipPath ) ;
49+
50+ if ( ! downloadResult ) return 1 ;
51+
52+ // Extract the downloaded file
53+ ExtractNodeJs ( zipPath ) ;
54+
55+ // Completion message
56+ Console . WriteLine ( $ "Done. To use, run `nodeswap use { version } `") ;
57+ return 0 ;
58+ }
59+
60+ private async Task < Version > GetVersion ( string rawVersion )
61+ {
62+ try
5263 {
53- try
54- {
55- version = VersionParser . Parse ( rawVersion . ToString ( ) ) ;
56- }
57- catch ( ArgumentException )
58- {
59- await Console . Error . WriteLineAsync ( $ "Invalid version argument: { rawVersion } ") ;
60- return 1 ;
61- }
62- }
64+ if ( rawVersion . Equals ( "latest" , StringComparison . CurrentCultureIgnoreCase ) )
65+ return await nodeWeb . GetLatestNodeVersion ( ) ;
6366
64- //
65- // Is the requested version already installed?
66- //
67+ if ( rawVersion . Split ( "." ) . Length < 3 )
68+ return await nodeWeb . GetLatestNodeVersion ( rawVersion ) ;
6769
68- if ( nodeLocal . GetInstalledVersions ( ) . FindIndex ( v => v . Version . Equals ( version ) ) != - 1 )
70+ return VersionParser . Parse ( rawVersion ) ;
71+ }
72+ catch ( Exception ex )
6973 {
70- await Console . Error . WriteLineAsync ( $ "{ version } already installed ") ;
71- return 1 ;
74+ await Console . Error . WriteLineAsync ( $ "Error determining version: { ex . Message } ") ;
75+ return null ;
7276 }
77+ }
7378
74- //
75- // Download it
76- //
79+ private bool IsVersionInstalled ( Version version )
80+ {
81+ return nodeLocal . GetInstalledVersions ( ) . FindIndex ( v => v . Version . Equals ( version ) ) != - 1 ;
82+ }
7783
78- var downloadUrl = nodeWeb . GetDownloadUrl ( version ) ;
79- var zipPath = Path . Join ( globalContext . StoragePath , Path . GetFileName ( downloadUrl ) ) ;
84+ private async Task < bool > DownloadNodeJs ( string downloadUrl , string zipPath )
85+ {
8086 var progressBar = new ProgressBar ( 100 , "Download progress" , new ProgressBarOptions
8187 {
8288 ProgressCharacter = '\u2593 ' ,
8389 ForegroundColor = ConsoleColor . Yellow ,
8490 ForegroundColorDone = ConsoleColor . Green ,
8591 } ) ;
8692
87- var webClient = new WebClient ( ) ;
88- webClient . DownloadProgressChanged += ( s , e ) => { progressBar . Tick ( e . ProgressPercentage ) ; } ;
89- webClient . DownloadFileCompleted += ( s , e ) => { progressBar . Dispose ( ) ; } ;
90-
9193 try
9294 {
93- await webClient . DownloadFileTaskAsync ( downloadUrl , zipPath ) . ConfigureAwait ( false ) ;
95+ var httpClient = new HttpClient ( ) ;
96+ using var response = await httpClient . GetAsync ( downloadUrl , HttpCompletionOption . ResponseHeadersRead ) ;
97+ response . EnsureSuccessStatusCode ( ) ;
98+
99+ var totalBytes = response . Content . Headers . ContentLength ?? - 1L ;
100+ var canReportProgress = totalBytes != - 1 ;
101+
102+ await using var fileStream = new FileStream ( zipPath , FileMode . Create , FileAccess . Write , FileShare . None ) ;
103+ await using var contentStream = await response . Content . ReadAsStreamAsync ( ) ;
104+
105+ var buffer = new byte [ 8192 ] ;
106+ long totalRead = 0 ;
107+ int bytesRead ;
108+
109+ while ( ( bytesRead = await contentStream . ReadAsync ( buffer ) ) > 0 )
110+ {
111+ await fileStream . WriteAsync ( buffer . AsMemory ( 0 , bytesRead ) ) ;
112+ if ( ! canReportProgress ) continue ;
113+ totalRead += bytesRead ;
114+ var progressPercentage = ( int ) ( totalRead * 100 / totalBytes ) ;
115+ progressBar . Tick ( progressPercentage ) ;
116+ }
117+
118+ progressBar . Dispose ( ) ;
119+ return true ;
94120 }
95121 catch ( Exception e )
96122 {
97123 await Console . Error . WriteLineAsync ( "Unable to download the Node.js zip file." ) ;
98- if ( e . InnerException == null ) return 1 ;
124+ if ( e . InnerException == null ) return false ;
99125 await Console . Error . WriteLineAsync ( e . InnerException . Message ) ;
100- await Console . Error . WriteLineAsync ( "You may need to run this command from an " +
101- " elevated prompt. (Run as Administrator)") ;
102- return 1 ;
126+ await Console . Error . WriteLineAsync (
127+ "You may need to run this command from an elevated prompt. (Run as Administrator)") ;
128+ return false ;
103129 }
130+ }
104131
132+ private void ExtractNodeJs ( string zipPath )
133+ {
105134 Console . WriteLine ( "Extracting..." ) ;
106135 ConsoleSpinner . Instance . Update ( ) ;
136+
107137 var timer = new Timer ( 250 ) ;
108- timer . Elapsed += ( s , e ) => ConsoleSpinner . Instance . Update ( ) ;
109- timer . Enabled = true ;
110- ZipFile . ExtractToDirectory ( zipPath , globalContext . StoragePath ) ;
111- timer . Enabled = false ;
138+ timer . Elapsed += ( _ , _ ) => ConsoleSpinner . Instance . Update ( ) ;
139+ timer . Start ( ) ;
140+
141+ ZipFile . ExtractToDirectory ( zipPath , globalContext . StoragePath , overwriteFiles : true ) ;
142+
143+ timer . Stop ( ) ;
112144 ConsoleSpinner . Reset ( ) ;
113145 File . Delete ( zipPath ) ;
114-
115- Console . WriteLine ( $ "Done. To use, run `nodeswap use { version } `") ;
116- return 0 ;
117146 }
118147}
0 commit comments