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> 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("usage: {0} <uri>", fileName)); 171 } 172 173 /////////////////////////////////////////////////////////////////////// 174 175 /// <summary> 176 /// This method attempts to determine the file name portion of the 177 /// specified URI. 178 /// </summary> 179 /// <param name="uri"> 180 /// The URI to process. 181 /// </param> 182 /// <returns> 183 /// The file name portion of the specified URI -OR- null if it cannot 184 /// be determined. 185 /// </returns> 186 private static string GetFileName( 187 Uri uri 188 ) 189 { 190 if (uri == null) 191 return null; 192 193 string pathAndQuery = uri.PathAndQuery; 194 195 if (String.IsNullOrEmpty(pathAndQuery)) 196 return null; 197 198 int index = pathAndQuery.LastIndexOf('/'); 199 200 if ((index < 0) || (index == pathAndQuery.Length)) 201 return null; 202 203 return pathAndQuery.Substring(index + 1); 204 } 205 #endregion 206 207 /////////////////////////////////////////////////////////////////////// 208 209 #region Private Event Handlers 210 /// <summary> 211 /// This method is an event handler that is called when the file 212 /// download completion percentage changes. It will display progress 213 /// on the console. Special care is taken to make sure that progress 214 /// events are not displayed out-of-order, even if duplicate and/or 215 /// out-of-order events are received. 216 /// </summary> 217 /// <param name="sender"> 218 /// The source of the event. 219 /// </param> 220 /// <param name="e"> 221 /// Information for the event being processed. 222 /// </param> 223 private static void DownloadProgressChanged( 224 object sender, 225 DownloadProgressChangedEventArgs e 226 ) 227 { 228 if (e != null) 229 { 230 int percent = e.ProgressPercentage; 231 232 lock (syncRoot) 233 { 234 if (percent > previousPercent) 235 { 236 Console.Write('.'); 237 238 if ((percent % 10) == 0) 239 Console.Write(" {0}% ", percent); 240 241 previousPercent = percent; 242 } 243 } 244 } 245 } 246 247 /////////////////////////////////////////////////////////////////////// 248 249 /// <summary> 250 /// This method is an event handler that is called when the file 251 /// download has completed, successfully or otherwise. It will 252 /// display the overall result of the file download on the console, 253 /// including any <see cref="Exception" /> information, if applicable. 254 /// The <see cref="exitCode" /> field is changed by this method to 255 /// indicate the overall result of the file download and the event 256 /// within the <see cref="doneEvent" /> field will be signaled. 257 /// </summary> 258 /// <param name="sender"> 259 /// The source of the event. 260 /// </param> 261 /// <param name="e"> 262 /// Information for the event being processed. 263 /// </param> 264 private static void DownloadFileCompleted( 265 object sender, 266 AsyncCompletedEventArgs e 267 ) 268 { 269 if (e != null) 270 { 271 lock (syncRoot) 272 { 273 if (previousPercent < 100) 274 Console.Write(' '); 275 } 276 277 if (e.Cancelled) 278 { 279 Console.WriteLine("Canceled"); 280 281 lock (syncRoot) 282 { 283 exitCode = ExitCode.DownloadCanceled; 284 } 285 } 286 else 287 { 288 Exception error = e.Error; 289 290 if (error != null) 291 { 292 Console.WriteLine("Error: {0}", error); 293 294 lock (syncRoot) 295 { 296 exitCode = ExitCode.DownloadError; 297 } 298 } 299 else 300 { 301 Console.WriteLine("Done"); 302 } 303 } 304 } 305 306 if (doneEvent != null) 307 doneEvent.Set(); 308 } 309 #endregion 310 311 /////////////////////////////////////////////////////////////////////// 312 313 #region Program Entry Point 314 /// <summary> 315 /// This is the entry-point for this tool. It handles processing the 316 /// command line arguments, setting up the web client, downloading the 317 /// file, and saving it to the file system. 318 /// </summary> 319 /// <param name="args"> 320 /// The command line arguments. 321 /// </param> 322 /// <returns> 323 /// Zero upon success; non-zero on failure. This will be one of the 324 /// values from the <see cref="ExitCode" /> enumeration. 325 /// </returns> 326 private static int Main( 327 string[] args 328 ) 329 { 330 // 331 // NOTE: Sanity check the command line arguments. 332 // 333 if (args == null) 334 { 335 Error(null, true); 336 return (int)ExitCode.MissingArgs; 337 } 338 339 if (args.Length != 1) 340 { 341 Error(null, true); 342 return (int)ExitCode.WrongNumArgs; 343 } 344 345 // 346 // NOTE: Attempt to convert the first (and only) command line 347 // argument to an absolute URI. 348 // 349 Uri uri; 350 351 if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri)) 352 { 353 Error("Could not create absolute URI from argument.", false); 354 return (int)ExitCode.BadUri; 355 } 356 357 // 358 // NOTE: Attempt to extract the file name portion of the URI we 359 // just created. 360 // 361 string fileName = GetFileName(uri); 362 363 if (fileName == null) 364 { 365 Error("Could not extract the file name from the URI.", false); 366 return (int)ExitCode.BadFileName; 367 } 368 369 // 370 // NOTE: Grab the temporary path setup for this process. If it is 371 // unavailable, we will not continue. 372 // 373 string directory = Path.GetTempPath(); 374 375 if (String.IsNullOrEmpty(directory) || 376 !Directory.Exists(directory)) 377 { 378 Error("Temporary directory is invalid or unavailable.", false); 379 return (int)ExitCode.BadTempPath; 380 } 381 382 try 383 { 384 using (WebClient webClient = new WebClient()) 385 { 386 // 387 // NOTE: Create the event used to signal completion of the 388 // file download. 389 // 390 doneEvent = new ManualResetEvent(false); 391 392 // 393 // NOTE: Hookup the event handlers we care about on the web 394 // client. These are necessary because the file is 395 // downloaded asynchronously. 396 // 397 webClient.DownloadProgressChanged += 398 new DownloadProgressChangedEventHandler( 399 DownloadProgressChanged); 400 401 webClient.DownloadFileCompleted += 402 new AsyncCompletedEventHandler( 403 DownloadFileCompleted); 404 405 // 406 // NOTE: Build the fully qualified path and file name, 407 // within the temporary directory, where the file to 408 // be downloaded will be saved. 409 // 410 fileName = Path.Combine(directory, fileName); 411 412 // 413 // NOTE: If the file name already exists (in the temporary) 414 // directory, delete it. 415 // 416 // TODO: Perhaps an error should be raised here instead? 417 // 418 if (File.Exists(fileName)) 419 File.Delete(fileName); 420 421 // 422 // NOTE: After kicking off the asynchronous file download 423 // process, wait [forever] until the "done" event is 424 // signaled. 425 // 426 Console.WriteLine( 427 "Downloading \"{0}\" to \"{1}\"...", uri, fileName); 428 429 webClient.DownloadFileAsync(uri, fileName); 430 doneEvent.WaitOne(); 431 } 432 433 lock (syncRoot) 434 { 435 return (int)exitCode; 436 } 437 } 438 catch (Exception e) 439 { 440 // 441 // NOTE: An exception was caught. Report it via the console 442 // and return failure. 443 // 444 Error(e.ToString(), false); 445 return (int)ExitCode.Exception; 446 } 447 } 448 #endregion 449 } 450 } 451