/* ** 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 { /// /// This enumeration is used to represent all the possible exit codes from /// this tool. /// internal enum ExitCode { /// /// The file download was a success. /// Success = 0, /// /// The command line arguments are missing (i.e. null). Generally, /// this should not happen. /// MissingArgs = 1, /// /// The wrong number of command line arguments was supplied. /// WrongNumArgs = 2, /// /// The URI specified on the command line could not be parsed as a /// supported absolute URI. /// BadUri = 3, /// /// The file name portion of the URI specified on the command line /// could not be extracted from it. /// BadFileName = 4, /// /// The temporary directory is either invalid (i.e. null) or does not /// represent an available directory. /// BadTempPath = 5, /// /// An exception was caught in . Generally, this /// should not happen. /// Exception = 6, /// /// The file download was canceled. This tool does not make use of /// the method; therefore, this /// should not happen. /// DownloadCanceled = 7, /// /// The file download encountered an error. Further information about /// this error should be displayed on the console. /// DownloadError = 8 } /////////////////////////////////////////////////////////////////////////// internal static class Program { #region Private Data /// /// This is used to synchronize multithreaded access to the /// and /// fields. /// private static readonly object syncRoot = new object(); /////////////////////////////////////////////////////////////////////// /// /// This event will be signed when the file download has completed, /// even if the file download itself was canceled or unsuccessful. /// private static EventWaitHandle doneEvent; /////////////////////////////////////////////////////////////////////// /// /// The previous file download completion percentage seen by the /// event handler. This value /// is never decreased, nor is it ever reset to zero. /// private static int previousPercent = 0; /////////////////////////////////////////////////////////////////////// /// /// 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 event handler. /// private static ExitCode exitCode = ExitCode.Success; #endregion /////////////////////////////////////////////////////////////////////// #region Private Support Methods /// /// This method displays an error message to the console and/or /// displays the command line usage information for this tool. /// /// /// The error message to display, if any. /// /// /// Non-zero to display the command line usage information. /// 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} ", fileName)); } /////////////////////////////////////////////////////////////////////// /// /// This method attempts to determine the file name portion of the /// specified URI. /// /// /// The URI to process. /// /// /// The file name portion of the specified URI -OR- null if it cannot /// be determined. /// 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 /// /// 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. /// /// /// The source of the event. /// /// /// Information for the event being processed. /// 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; } } } } /////////////////////////////////////////////////////////////////////// /// /// 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 information, if applicable. /// The field is changed by this method to /// indicate the overall result of the file download and the event /// within the field will be signaled. /// /// /// The source of the event. /// /// /// Information for the event being processed. /// 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 /// /// 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. /// /// /// The command line arguments. /// /// /// Zero upon success; non-zero on failure. This will be one of the /// values from the enumeration. /// 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) { 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: Attempt to extract the file name portion of the URI we // just created. // string fileName = GetFileName(uri); if (fileName == null) { Error("Could not extract the file name from the 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 { 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 } }