OLD | NEW |
(Empty) | |
| 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 } |
OLD | NEW |