| /* |
| ** 2015 October 7 |
| ** |
| ** The author disclaims copyright to this source code. In place of |
| ** a legal notice, here is a blessing: |
| ** |
| ** May you do good and not evil. |
| ** May you find forgiveness for yourself and forgive others. |
| ** May you share freely, never taking more than you give. |
| ** |
| ************************************************************************* |
| ** This file contains C# code to download a single file based on a URI. |
| */ |
|
|
| using System;
|
| using System.ComponentModel;
|
| using System.Diagnostics;
|
| using System.IO;
|
| using System.Net;
|
| using System.Reflection;
|
| using System.Runtime.InteropServices;
|
| using System.Threading;
|
|
|
| ///////////////////////////////////////////////////////////////////////////////
|
|
|
| #region Assembly Metadata
|
| [assembly: AssemblyTitle("GetFile Tool")]
|
| [assembly: AssemblyDescription("Download a single file based on a URI.")]
|
| [assembly: AssemblyCompany("SQLite Development Team")]
|
| [assembly: AssemblyProduct("SQLite")]
|
| [assembly: AssemblyCopyright("Public Domain")]
|
| [assembly: ComVisible(false)]
|
| [assembly: Guid("5c4b3728-1693-4a33-a218-8e6973ca15a6")]
|
| [assembly: AssemblyVersion("1.0.*")]
|
|
|
| #if DEBUG
|
| [assembly: AssemblyConfiguration("Debug")]
|
| #else
|
| [assembly: AssemblyConfiguration("Release")]
|
| #endif
|
| #endregion
|
|
|
| ///////////////////////////////////////////////////////////////////////////////
|
|
|
| namespace GetFile
|
| {
|
| /// <summary>
|
| /// This enumeration is used to represent all the possible exit codes from
|
| /// this tool.
|
| /// </summary>
|
| internal enum ExitCode
|
| {
|
| /// <summary>
|
| /// The file download was a success.
|
| /// </summary>
|
| Success = 0,
|
|
|
| /// <summary>
|
| /// The command line arguments are missing (i.e. null). Generally,
|
| /// this should not happen.
|
| /// </summary>
|
| MissingArgs = 1,
|
|
|
| /// <summary>
|
| /// The wrong number of command line arguments was supplied.
|
| /// </summary>
|
| WrongNumArgs = 2,
|
|
|
| /// <summary>
|
| /// The URI specified on the command line could not be parsed as a
|
| /// supported absolute URI.
|
| /// </summary>
|
| BadUri = 3,
|
|
|
| /// <summary>
|
| /// The file name portion of the URI specified on the command line
|
| /// could not be extracted from it.
|
| /// </summary>
|
| BadFileName = 4,
|
|
|
| /// <summary>
|
| /// The temporary directory is either invalid (i.e. null) or does not
|
| /// represent an available directory.
|
| /// </summary>
|
| BadTempPath = 5,
|
|
|
| /// <summary>
|
| /// An exception was caught in <see cref="Main" />. Generally, this
|
| /// should not happen.
|
| /// </summary>
|
| Exception = 6,
|
|
|
| /// <summary>
|
| /// The file download was canceled. This tool does not make use of
|
| /// the <see cref="WebClient.CancelAsync" /> method; therefore, this
|
| /// should not happen.
|
| /// </summary>
|
| DownloadCanceled = 7,
|
|
|
| /// <summary>
|
| /// The file download encountered an error. Further information about
|
| /// this error should be displayed on the console.
|
| /// </summary>
|
| DownloadError = 8
|
| }
|
|
|
| ///////////////////////////////////////////////////////////////////////////
|
|
|
| internal static class Program
|
| {
|
| #region Private Data
|
| /// <summary>
|
| /// This is used to synchronize multithreaded access to the
|
| /// <see cref="previousPercent" /> and <see cref="exitCode"/>
|
| /// fields.
|
| /// </summary>
|
| private static readonly object syncRoot = new object();
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| /// <summary>
|
| /// This event will be signed when the file download has completed,
|
| /// even if the file download itself was canceled or unsuccessful.
|
| /// </summary>
|
| private static EventWaitHandle doneEvent;
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| /// <summary>
|
| /// The previous file download completion percentage seen by the
|
| /// <see cref="DownloadProgressChanged" /> event handler. This value
|
| /// is never decreased, nor is it ever reset to zero.
|
| /// </summary>
|
| private static int previousPercent = 0;
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| /// <summary>
|
| /// This will be the exit code returned by this tool after the file
|
| /// download completes, successfully or otherwise. This value is only
|
| /// changed by the <see cref="DownloadFileCompleted" /> event handler.
|
| /// </summary>
|
| private static ExitCode exitCode = ExitCode.Success;
|
| #endregion
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| #region Private Support Methods
|
| /// <summary>
|
| /// This method displays an error message to the console and/or
|
| /// displays the command line usage information for this tool.
|
| /// </summary>
|
| /// <param name="message">
|
| /// The error message to display, if any.
|
| /// </param>
|
| /// <param name="usage">
|
| /// Non-zero to display the command line usage information.
|
| /// </param>
|
| private static void Error(
|
| string message,
|
| bool usage
|
| )
|
| {
|
| if (message != null)
|
| Console.WriteLine(message);
|
|
|
| string fileName = Path.GetFileName(
|
| Process.GetCurrentProcess().MainModule.FileName);
|
|
|
| Console.WriteLine(String.Format(
|
| "usage: {0} <uri> [fileName]", fileName));
|
| }
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| /// <summary>
|
| /// This method attempts to determine the file name portion of the
|
| /// specified URI.
|
| /// </summary>
|
| /// <param name="uri">
|
| /// The URI to process.
|
| /// </param>
|
| /// <returns>
|
| /// The file name portion of the specified URI -OR- null if it cannot
|
| /// be determined.
|
| /// </returns>
|
| private static string GetFileName(
|
| Uri uri
|
| )
|
| {
|
| if (uri == null)
|
| return null;
|
|
|
| string pathAndQuery = uri.PathAndQuery;
|
|
|
| if (String.IsNullOrEmpty(pathAndQuery))
|
| return null;
|
|
|
| int index = pathAndQuery.LastIndexOf('/');
|
|
|
| if ((index < 0) || (index == pathAndQuery.Length))
|
| return null;
|
|
|
| return pathAndQuery.Substring(index + 1);
|
| }
|
| #endregion
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| #region Private Event Handlers
|
| /// <summary>
|
| /// This method is an event handler that is called when the file
|
| /// download completion percentage changes. It will display progress
|
| /// on the console. Special care is taken to make sure that progress
|
| /// events are not displayed out-of-order, even if duplicate and/or
|
| /// out-of-order events are received.
|
| /// </summary>
|
| /// <param name="sender">
|
| /// The source of the event.
|
| /// </param>
|
| /// <param name="e">
|
| /// Information for the event being processed.
|
| /// </param>
|
| private static void DownloadProgressChanged(
|
| object sender,
|
| DownloadProgressChangedEventArgs e
|
| )
|
| {
|
| if (e != null)
|
| {
|
| int percent = e.ProgressPercentage;
|
|
|
| lock (syncRoot)
|
| {
|
| if (percent > previousPercent)
|
| {
|
| Console.Write('.');
|
|
|
| if ((percent % 10) == 0)
|
| Console.Write(" {0}% ", percent);
|
|
|
| previousPercent = percent;
|
| }
|
| }
|
| }
|
| }
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| /// <summary>
|
| /// This method is an event handler that is called when the file
|
| /// download has completed, successfully or otherwise. It will
|
| /// display the overall result of the file download on the console,
|
| /// including any <see cref="Exception" /> information, if applicable.
|
| /// The <see cref="exitCode" /> field is changed by this method to
|
| /// indicate the overall result of the file download and the event
|
| /// within the <see cref="doneEvent" /> field will be signaled.
|
| /// </summary>
|
| /// <param name="sender">
|
| /// The source of the event.
|
| /// </param>
|
| /// <param name="e">
|
| /// Information for the event being processed.
|
| /// </param>
|
| private static void DownloadFileCompleted(
|
| object sender,
|
| AsyncCompletedEventArgs e
|
| )
|
| {
|
| if (e != null)
|
| {
|
| lock (syncRoot)
|
| {
|
| if (previousPercent < 100)
|
| Console.Write(' ');
|
| }
|
|
|
| if (e.Cancelled)
|
| {
|
| Console.WriteLine("Canceled");
|
|
|
| lock (syncRoot)
|
| {
|
| exitCode = ExitCode.DownloadCanceled;
|
| }
|
| }
|
| else
|
| {
|
| Exception error = e.Error;
|
|
|
| if (error != null)
|
| {
|
| Console.WriteLine("Error: {0}", error);
|
|
|
| lock (syncRoot)
|
| {
|
| exitCode = ExitCode.DownloadError;
|
| }
|
| }
|
| else
|
| {
|
| Console.WriteLine("Done");
|
| }
|
| }
|
| }
|
|
|
| if (doneEvent != null)
|
| doneEvent.Set();
|
| }
|
| #endregion
|
|
|
| ///////////////////////////////////////////////////////////////////////
|
|
|
| #region Program Entry Point
|
| /// <summary>
|
| /// This is the entry-point for this tool. It handles processing the
|
| /// command line arguments, setting up the web client, downloading the
|
| /// file, and saving it to the file system.
|
| /// </summary>
|
| /// <param name="args">
|
| /// The command line arguments.
|
| /// </param>
|
| /// <returns>
|
| /// Zero upon success; non-zero on failure. This will be one of the
|
| /// values from the <see cref="ExitCode" /> enumeration.
|
| /// </returns>
|
| private static int Main(
|
| string[] args
|
| )
|
| {
|
| //
|
| // NOTE: Sanity check the command line arguments.
|
| //
|
| if (args == null)
|
| {
|
| Error(null, true);
|
| return (int)ExitCode.MissingArgs;
|
| }
|
|
|
| if ((args.Length < 1) || (args.Length > 2))
|
| {
|
| Error(null, true);
|
| return (int)ExitCode.WrongNumArgs;
|
| }
|
|
|
| //
|
| // NOTE: Attempt to convert the first (and only) command line
|
| // argument to an absolute URI.
|
| //
|
| Uri uri;
|
|
|
| if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri))
|
| {
|
| Error("Could not create absolute URI from argument.", false);
|
| return (int)ExitCode.BadUri;
|
| }
|
|
|
| //
|
| // NOTE: If a file name was specified on the command line, try to
|
| // use it (without its directory name); otherwise, fallback
|
| // to using the file name portion of the URI.
|
| //
|
| string fileName = (args.Length == 2) ?
|
| Path.GetFileName(args[1]) : null;
|
|
|
| if (String.IsNullOrEmpty(fileName))
|
| {
|
| //
|
| // NOTE: Attempt to extract the file name portion of the URI
|
| // we just created.
|
| //
|
| fileName = GetFileName(uri);
|
|
|
| if (fileName == null)
|
| {
|
| Error("Could not extract file name from URI.", false);
|
| return (int)ExitCode.BadFileName;
|
| }
|
| }
|
|
|
| //
|
| // NOTE: Grab the temporary path setup for this process. If it is
|
| // unavailable, we will not continue.
|
| //
|
| string directory = Path.GetTempPath();
|
|
|
| if (String.IsNullOrEmpty(directory) ||
|
| !Directory.Exists(directory))
|
| {
|
| Error("Temporary directory is invalid or unavailable.", false);
|
| return (int)ExitCode.BadTempPath;
|
| }
|
|
|
| try
|
| {
|
| //
|
| // HACK: For use of the TLS 1.2 security protocol because some
|
| // web servers fail without it. In order to support the
|
| // .NET Framework 2.0+ at compilation time, must use its
|
| // integer constant here.
|
| //
|
| ServicePointManager.SecurityProtocol =
|
| (SecurityProtocolType)0xC00;
|
|
|
| using (WebClient webClient = new WebClient())
|
| {
|
| //
|
| // NOTE: Create the event used to signal completion of the
|
| // file download.
|
| //
|
| doneEvent = new ManualResetEvent(false);
|
|
|
| //
|
| // NOTE: Hookup the event handlers we care about on the web
|
| // client. These are necessary because the file is
|
| // downloaded asynchronously.
|
| //
|
| webClient.DownloadProgressChanged +=
|
| new DownloadProgressChangedEventHandler(
|
| DownloadProgressChanged);
|
|
|
| webClient.DownloadFileCompleted +=
|
| new AsyncCompletedEventHandler(
|
| DownloadFileCompleted);
|
|
|
| //
|
| // NOTE: Build the fully qualified path and file name,
|
| // within the temporary directory, where the file to
|
| // be downloaded will be saved.
|
| //
|
| fileName = Path.Combine(directory, fileName);
|
|
|
| //
|
| // NOTE: If the file name already exists (in the temporary)
|
| // directory, delete it.
|
| //
|
| // TODO: Perhaps an error should be raised here instead?
|
| //
|
| if (File.Exists(fileName))
|
| File.Delete(fileName);
|
|
|
| //
|
| // NOTE: After kicking off the asynchronous file download
|
| // process, wait [forever] until the "done" event is
|
| // signaled.
|
| //
|
| Console.WriteLine(
|
| "Downloading \"{0}\" to \"{1}\"...", uri, fileName);
|
|
|
| webClient.DownloadFileAsync(uri, fileName);
|
| doneEvent.WaitOne();
|
| }
|
|
|
| lock (syncRoot)
|
| {
|
| return (int)exitCode;
|
| }
|
| }
|
| catch (Exception e)
|
| {
|
| //
|
| // NOTE: An exception was caught. Report it via the console
|
| // and return failure.
|
| //
|
| Error(e.ToString(), false);
|
| return (int)ExitCode.Exception;
|
| }
|
| }
|
| #endregion
|
| }
|
| }
|