1 /* 2 ** 2015 October 7 3 ** 4 ** The author disclaims copyright to this source code. In place of 5 ** a legal notice, here is a blessing: 6 ** 7 ** May you do good and not evil. 8 ** May you find forgiveness for yourself and forgive others. 9 ** May you share freely, never taking more than you give. 10 ** 11 ************************************************************************* 12 ** This file contains C# code to download a single file based on a URI. 13 */ 14 15 using System; 16 using System.ComponentModel; 17 using System.Diagnostics; 18 using System.IO; 19 using System.Net; 20 using System.Reflection; 21 using System.Runtime.InteropServices; 22 using System.Threading; 23 24 /////////////////////////////////////////////////////////////////////////////// 25 26 #region Assembly Metadata 27 [assembly: AssemblyTitle("GetFile Tool")] 28 [assembly: AssemblyDescription("Download a single file based on a URI.")] 29 [assembly: AssemblyCompany("SQLite Development Team")] 30 [assembly: AssemblyProduct("SQLite")] 31 [assembly: AssemblyCopyright("Public Domain")] 32 [assembly: ComVisible(false)] 33 [assembly: Guid("5c4b3728-1693-4a33-a218-8e6973ca15a6")] 34 [assembly: AssemblyVersion("1.0.*")] 35 36 #if DEBUG 37 [assembly: AssemblyConfiguration("Debug")] 38 #else 39 [assembly: AssemblyConfiguration("Release")] 40 #endif 41 #endregion 42 43 /////////////////////////////////////////////////////////////////////////////// 44 45 namespace GetFile 46 { 47 /// <summary> 48 /// This enumeration is used to represent all the possible exit codes from 49 /// this tool. 50 /// </summary> 51 internal enum ExitCode 52 { 53 /// <summary> 54 /// The file download was a success. 55 /// </summary> 56 Success = 0, 57 58 /// <summary> 59 /// The command line arguments are missing (i.e. null). Generally, 60 /// this should not happen. 61 /// </summary> 62 MissingArgs = 1, 63 64 /// <summary> 65 /// The wrong number of command line arguments was supplied. 66 /// </summary> 67 WrongNumArgs = 2, 68 69 /// <summary> 70 /// The URI specified on the command line could not be parsed as a 71 /// supported absolute URI. 72 /// </summary> 73 BadUri = 3, 74 75 /// <summary> 76 /// The file name portion of the URI specified on the command line 77 /// could not be extracted from it. 78 /// </summary> 79 BadFileName = 4, 80 81 /// <summary> 82 /// The temporary directory is either invalid (i.e. null) or does not 83 /// represent an available directory. 84 /// </summary> 85 BadTempPath = 5, 86 87 /// <summary> 88 /// An exception was caught in <see cref="Main" />. Generally, this 89 /// should not happen. 90 /// </summary> 91 Exception = 6, 92 93 /// <summary> 94 /// The file download was canceled. This tool does not make use of 95 /// the <see cref="WebClient.CancelAsync" /> method; therefore, this 96 /// should not happen. 97 /// </summary> 98 DownloadCanceled = 7, 99 100 /// <summary> 101 /// The file download encountered an error. Further information about 102 /// this error should be displayed on the console. 103 /// </summary> 104 DownloadError = 8 105 } 106 107 /////////////////////////////////////////////////////////////////////////// 108 109 internal static class Program 110 { 111 #region Private Data 112 /// <summary> 113 /// This is used to synchronize multithreaded access to the 114 /// <see cref="previousPercent" /> and <see cref="exitCode"/> 115 /// fields. 116 /// </summary> 117 private static readonly object syncRoot = new object(); 118 119 /////////////////////////////////////////////////////////////////////// 120 121 /// <summary> 122 /// This event will be signed when the file download has completed, 123 /// even if the file download itself was canceled or unsuccessful. 124 /// </summary> 125 private static EventWaitHandle doneEvent; 126 127 /////////////////////////////////////////////////////////////////////// 128 129 /// <summary> 130 /// The previous file download completion percentage seen by the 131 /// <see cref="DownloadProgressChanged" /> event handler. This value 132 /// is never decreased, nor is it ever reset to zero. 133 /// </summary> 134 private static int previousPercent = 0; 135 136 /////////////////////////////////////////////////////////////////////// 137 138 /// <summary> 139 /// This will be the exit code returned by this tool after the file 140 /// download completes, successfully or otherwise. This value is only 141 /// changed by the <see cref="DownloadFileCompleted" /> event handler. 142 /// </summary> 143 private static ExitCode exitCode = ExitCode.Success; 144 #endregion 145 146 /////////////////////////////////////////////////////////////////////// 147 148 #region Private Support Methods 149 /// <summary> 150 /// This method displays an error message to the console and/or 151 /// displays the command line usage information for this tool. 152 /// </summary> 153 /// <param name="message"> 154 /// The error message to display, if any. 155 /// </param> 156 /// <param name="usage"> 157 /// Non-zero to display the command line usage information. 158 /// </param> Error( string message, bool usage )159 private static void Error( 160 string message, 161 bool usage 162 ) 163 { 164 if (message != null) 165 Console.WriteLine(message); 166 167 string fileName = Path.GetFileName( 168 Process.GetCurrentProcess().MainModule.FileName); 169 170 Console.WriteLine(String.Format( 171 "usage: {0} <uri> [fileName]", fileName)); 172 } 173 174 /////////////////////////////////////////////////////////////////////// 175 176 /// <summary> 177 /// This method attempts to determine the file name portion of the 178 /// specified URI. 179 /// </summary> 180 /// <param name="uri"> 181 /// The URI to process. 182 /// </param> 183 /// <returns> 184 /// The file name portion of the specified URI -OR- null if it cannot 185 /// be determined. 186 /// </returns> GetFileName( Uri uri )187 private static string GetFileName( 188 Uri uri 189 ) 190 { 191 if (uri == null) 192 return null; 193 194 string pathAndQuery = uri.PathAndQuery; 195 196 if (String.IsNullOrEmpty(pathAndQuery)) 197 return null; 198 199 int index = pathAndQuery.LastIndexOf('/'); 200 201 if ((index < 0) || (index == pathAndQuery.Length)) 202 return null; 203 204 return pathAndQuery.Substring(index + 1); 205 } 206 #endregion 207 208 /////////////////////////////////////////////////////////////////////// 209 210 #region Private Event Handlers 211 /// <summary> 212 /// This method is an event handler that is called when the file 213 /// download completion percentage changes. It will display progress 214 /// on the console. Special care is taken to make sure that progress 215 /// events are not displayed out-of-order, even if duplicate and/or 216 /// out-of-order events are received. 217 /// </summary> 218 /// <param name="sender"> 219 /// The source of the event. 220 /// </param> 221 /// <param name="e"> 222 /// Information for the event being processed. 223 /// </param> DownloadProgressChanged( object sender, DownloadProgressChangedEventArgs e )224 private static void DownloadProgressChanged( 225 object sender, 226 DownloadProgressChangedEventArgs e 227 ) 228 { 229 if (e != null) 230 { 231 int percent = e.ProgressPercentage; 232 233 lock (syncRoot) 234 { 235 if (percent > previousPercent) 236 { 237 Console.Write('.'); 238 239 if ((percent % 10) == 0) 240 Console.Write(" {0}% ", percent); 241 242 previousPercent = percent; 243 } 244 } 245 } 246 } 247 248 /////////////////////////////////////////////////////////////////////// 249 250 /// <summary> 251 /// This method is an event handler that is called when the file 252 /// download has completed, successfully or otherwise. It will 253 /// display the overall result of the file download on the console, 254 /// including any <see cref="Exception" /> information, if applicable. 255 /// The <see cref="exitCode" /> field is changed by this method to 256 /// indicate the overall result of the file download and the event 257 /// within the <see cref="doneEvent" /> field will be signaled. 258 /// </summary> 259 /// <param name="sender"> 260 /// The source of the event. 261 /// </param> 262 /// <param name="e"> 263 /// Information for the event being processed. 264 /// </param> DownloadFileCompleted( object sender, AsyncCompletedEventArgs e )265 private static void DownloadFileCompleted( 266 object sender, 267 AsyncCompletedEventArgs e 268 ) 269 { 270 if (e != null) 271 { 272 lock (syncRoot) 273 { 274 if (previousPercent < 100) 275 Console.Write(' '); 276 } 277 278 if (e.Cancelled) 279 { 280 Console.WriteLine("Canceled"); 281 282 lock (syncRoot) 283 { 284 exitCode = ExitCode.DownloadCanceled; 285 } 286 } 287 else 288 { 289 Exception error = e.Error; 290 291 if (error != null) 292 { 293 Console.WriteLine("Error: {0}", error); 294 295 lock (syncRoot) 296 { 297 exitCode = ExitCode.DownloadError; 298 } 299 } 300 else 301 { 302 Console.WriteLine("Done"); 303 } 304 } 305 } 306 307 if (doneEvent != null) 308 doneEvent.Set(); 309 } 310 #endregion 311 312 /////////////////////////////////////////////////////////////////////// 313 314 #region Program Entry Point 315 /// <summary> 316 /// This is the entry-point for this tool. It handles processing the 317 /// command line arguments, setting up the web client, downloading the 318 /// file, and saving it to the file system. 319 /// </summary> 320 /// <param name="args"> 321 /// The command line arguments. 322 /// </param> 323 /// <returns> 324 /// Zero upon success; non-zero on failure. This will be one of the 325 /// values from the <see cref="ExitCode" /> enumeration. 326 /// </returns> Main( string[] args )327 private static int Main( 328 string[] args 329 ) 330 { 331 // 332 // NOTE: Sanity check the command line arguments. 333 // 334 if (args == null) 335 { 336 Error(null, true); 337 return (int)ExitCode.MissingArgs; 338 } 339 340 if ((args.Length < 1) || (args.Length > 2)) 341 { 342 Error(null, true); 343 return (int)ExitCode.WrongNumArgs; 344 } 345 346 // 347 // NOTE: Attempt to convert the first (and only) command line 348 // argument to an absolute URI. 349 // 350 Uri uri; 351 352 if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri)) 353 { 354 Error("Could not create absolute URI from argument.", false); 355 return (int)ExitCode.BadUri; 356 } 357 358 // 359 // NOTE: If a file name was specified on the command line, try to 360 // use it (without its directory name); otherwise, fallback 361 // to using the file name portion of the URI. 362 // 363 string fileName = (args.Length == 2) ? 364 Path.GetFileName(args[1]) : null; 365 366 if (String.IsNullOrEmpty(fileName)) 367 { 368 // 369 // NOTE: Attempt to extract the file name portion of the URI 370 // we just created. 371 // 372 fileName = GetFileName(uri); 373 374 if (fileName == null) 375 { 376 Error("Could not extract file name from URI.", false); 377 return (int)ExitCode.BadFileName; 378 } 379 } 380 381 // 382 // NOTE: Grab the temporary path setup for this process. If it is 383 // unavailable, we will not continue. 384 // 385 string directory = Path.GetTempPath(); 386 387 if (String.IsNullOrEmpty(directory) || 388 !Directory.Exists(directory)) 389 { 390 Error("Temporary directory is invalid or unavailable.", false); 391 return (int)ExitCode.BadTempPath; 392 } 393 394 try 395 { 396 // 397 // HACK: For use of the TLS 1.2 security protocol because some 398 // web servers fail without it. In order to support the 399 // .NET Framework 2.0+ at compilation time, must use its 400 // integer constant here. 401 // 402 ServicePointManager.SecurityProtocol = 403 (SecurityProtocolType)0xC00; 404 405 using (WebClient webClient = new WebClient()) 406 { 407 // 408 // NOTE: Create the event used to signal completion of the 409 // file download. 410 // 411 doneEvent = new ManualResetEvent(false); 412 413 // 414 // NOTE: Hookup the event handlers we care about on the web 415 // client. These are necessary because the file is 416 // downloaded asynchronously. 417 // 418 webClient.DownloadProgressChanged += 419 new DownloadProgressChangedEventHandler( 420 DownloadProgressChanged); 421 422 webClient.DownloadFileCompleted += 423 new AsyncCompletedEventHandler( 424 DownloadFileCompleted); 425 426 // 427 // NOTE: Build the fully qualified path and file name, 428 // within the temporary directory, where the file to 429 // be downloaded will be saved. 430 // 431 fileName = Path.Combine(directory, fileName); 432 433 // 434 // NOTE: If the file name already exists (in the temporary) 435 // directory, delete it. 436 // 437 // TODO: Perhaps an error should be raised here instead? 438 // 439 if (File.Exists(fileName)) 440 File.Delete(fileName); 441 442 // 443 // NOTE: After kicking off the asynchronous file download 444 // process, wait [forever] until the "done" event is 445 // signaled. 446 // 447 Console.WriteLine( 448 "Downloading \"{0}\" to \"{1}\"...", uri, fileName); 449 450 webClient.DownloadFileAsync(uri, fileName); 451 doneEvent.WaitOne(); 452 } 453 454 lock (syncRoot) 455 { 456 return (int)exitCode; 457 } 458 } 459 catch (Exception e) 460 { 461 // 462 // NOTE: An exception was caught. Report it via the console 463 // and return failure. 464 // 465 Error(e.ToString(), false); 466 return (int)ExitCode.Exception; 467 } 468 } 469 #endregion 470 } 471 } 472