123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- /*
- ** 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
- }
- }
|