mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 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 |
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 170 | Console.WriteLine(String.Format(
|
| 171 | "usage: {0} <uri> [fileName]", fileName));
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 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>
|
| 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>
|
| 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>
|
| 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>
|
| 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 |
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 340 | if ((args.Length < 1) || (args.Length > 2))
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 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 | {
|
mistachkin | cc73048 | 2015-10-10 00:53:28 +0000 | [diff] [blame] | 354 | Error("Could not create absolute URI from argument.", false);
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 355 | return (int)ExitCode.BadUri;
|
| 356 | }
|
| 357 |
|
| 358 | //
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 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.
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 362 | //
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 363 | string fileName = (args.Length == 2) ?
|
| 364 | Path.GetFileName(args[1]) : null;
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 365 |
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 366 | if (String.IsNullOrEmpty(fileName))
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 367 | {
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 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 | }
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 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 | {
|
mistachkin | fe29334 | 2019-11-25 00:07:03 +0000 | [diff] [blame] | 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 |
|
mistachkin | 80372ae | 2015-10-09 17:36:06 +0000 | [diff] [blame] | 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 | }
|