180372ae2Smistachkin /* 280372ae2Smistachkin ** 2015 October 7 380372ae2Smistachkin ** 480372ae2Smistachkin ** The author disclaims copyright to this source code. In place of 580372ae2Smistachkin ** a legal notice, here is a blessing: 680372ae2Smistachkin ** 780372ae2Smistachkin ** May you do good and not evil. 880372ae2Smistachkin ** May you find forgiveness for yourself and forgive others. 980372ae2Smistachkin ** May you share freely, never taking more than you give. 1080372ae2Smistachkin ** 1180372ae2Smistachkin ************************************************************************* 1280372ae2Smistachkin ** This file contains C# code to download a single file based on a URI. 1380372ae2Smistachkin */ 1480372ae2Smistachkin 1580372ae2Smistachkin using System; 1680372ae2Smistachkin using System.ComponentModel; 1780372ae2Smistachkin using System.Diagnostics; 1880372ae2Smistachkin using System.IO; 1980372ae2Smistachkin using System.Net; 2080372ae2Smistachkin using System.Reflection; 2180372ae2Smistachkin using System.Runtime.InteropServices; 2280372ae2Smistachkin using System.Threading; 2380372ae2Smistachkin 2480372ae2Smistachkin /////////////////////////////////////////////////////////////////////////////// 2580372ae2Smistachkin 2680372ae2Smistachkin #region Assembly Metadata 2780372ae2Smistachkin [assembly: AssemblyTitle("GetFile Tool")] 2880372ae2Smistachkin [assembly: AssemblyDescription("Download a single file based on a URI.")] 2980372ae2Smistachkin [assembly: AssemblyCompany("SQLite Development Team")] 3080372ae2Smistachkin [assembly: AssemblyProduct("SQLite")] 3180372ae2Smistachkin [assembly: AssemblyCopyright("Public Domain")] 3280372ae2Smistachkin [assembly: ComVisible(false)] 3380372ae2Smistachkin [assembly: Guid("5c4b3728-1693-4a33-a218-8e6973ca15a6")] 3480372ae2Smistachkin [assembly: AssemblyVersion("1.0.*")] 3580372ae2Smistachkin 3680372ae2Smistachkin #if DEBUG 3780372ae2Smistachkin [assembly: AssemblyConfiguration("Debug")] 3880372ae2Smistachkin #else 3980372ae2Smistachkin [assembly: AssemblyConfiguration("Release")] 4080372ae2Smistachkin #endif 4180372ae2Smistachkin #endregion 4280372ae2Smistachkin 4380372ae2Smistachkin /////////////////////////////////////////////////////////////////////////////// 4480372ae2Smistachkin 4580372ae2Smistachkin namespace GetFile 4680372ae2Smistachkin { 4780372ae2Smistachkin /// <summary> 4880372ae2Smistachkin /// This enumeration is used to represent all the possible exit codes from 4980372ae2Smistachkin /// this tool. 5080372ae2Smistachkin /// </summary> 5180372ae2Smistachkin internal enum ExitCode 5280372ae2Smistachkin { 5380372ae2Smistachkin /// <summary> 5480372ae2Smistachkin /// The file download was a success. 5580372ae2Smistachkin /// </summary> 5680372ae2Smistachkin Success = 0, 5780372ae2Smistachkin 5880372ae2Smistachkin /// <summary> 5980372ae2Smistachkin /// The command line arguments are missing (i.e. null). Generally, 6080372ae2Smistachkin /// this should not happen. 6180372ae2Smistachkin /// </summary> 6280372ae2Smistachkin MissingArgs = 1, 6380372ae2Smistachkin 6480372ae2Smistachkin /// <summary> 6580372ae2Smistachkin /// The wrong number of command line arguments was supplied. 6680372ae2Smistachkin /// </summary> 6780372ae2Smistachkin WrongNumArgs = 2, 6880372ae2Smistachkin 6980372ae2Smistachkin /// <summary> 7080372ae2Smistachkin /// The URI specified on the command line could not be parsed as a 7180372ae2Smistachkin /// supported absolute URI. 7280372ae2Smistachkin /// </summary> 7380372ae2Smistachkin BadUri = 3, 7480372ae2Smistachkin 7580372ae2Smistachkin /// <summary> 7680372ae2Smistachkin /// The file name portion of the URI specified on the command line 7780372ae2Smistachkin /// could not be extracted from it. 7880372ae2Smistachkin /// </summary> 7980372ae2Smistachkin BadFileName = 4, 8080372ae2Smistachkin 8180372ae2Smistachkin /// <summary> 8280372ae2Smistachkin /// The temporary directory is either invalid (i.e. null) or does not 8380372ae2Smistachkin /// represent an available directory. 8480372ae2Smistachkin /// </summary> 8580372ae2Smistachkin BadTempPath = 5, 8680372ae2Smistachkin 8780372ae2Smistachkin /// <summary> 8880372ae2Smistachkin /// An exception was caught in <see cref="Main" />. Generally, this 8980372ae2Smistachkin /// should not happen. 9080372ae2Smistachkin /// </summary> 9180372ae2Smistachkin Exception = 6, 9280372ae2Smistachkin 9380372ae2Smistachkin /// <summary> 9480372ae2Smistachkin /// The file download was canceled. This tool does not make use of 9580372ae2Smistachkin /// the <see cref="WebClient.CancelAsync" /> method; therefore, this 9680372ae2Smistachkin /// should not happen. 9780372ae2Smistachkin /// </summary> 9880372ae2Smistachkin DownloadCanceled = 7, 9980372ae2Smistachkin 10080372ae2Smistachkin /// <summary> 10180372ae2Smistachkin /// The file download encountered an error. Further information about 10280372ae2Smistachkin /// this error should be displayed on the console. 10380372ae2Smistachkin /// </summary> 10480372ae2Smistachkin DownloadError = 8 10580372ae2Smistachkin } 10680372ae2Smistachkin 10780372ae2Smistachkin /////////////////////////////////////////////////////////////////////////// 10880372ae2Smistachkin 10980372ae2Smistachkin internal static class Program 11080372ae2Smistachkin { 11180372ae2Smistachkin #region Private Data 11280372ae2Smistachkin /// <summary> 11380372ae2Smistachkin /// This is used to synchronize multithreaded access to the 11480372ae2Smistachkin /// <see cref="previousPercent" /> and <see cref="exitCode"/> 11580372ae2Smistachkin /// fields. 11680372ae2Smistachkin /// </summary> 11780372ae2Smistachkin private static readonly object syncRoot = new object(); 11880372ae2Smistachkin 11980372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 12080372ae2Smistachkin 12180372ae2Smistachkin /// <summary> 12280372ae2Smistachkin /// This event will be signed when the file download has completed, 12380372ae2Smistachkin /// even if the file download itself was canceled or unsuccessful. 12480372ae2Smistachkin /// </summary> 12580372ae2Smistachkin private static EventWaitHandle doneEvent; 12680372ae2Smistachkin 12780372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 12880372ae2Smistachkin 12980372ae2Smistachkin /// <summary> 13080372ae2Smistachkin /// The previous file download completion percentage seen by the 13180372ae2Smistachkin /// <see cref="DownloadProgressChanged" /> event handler. This value 13280372ae2Smistachkin /// is never decreased, nor is it ever reset to zero. 13380372ae2Smistachkin /// </summary> 13480372ae2Smistachkin private static int previousPercent = 0; 13580372ae2Smistachkin 13680372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 13780372ae2Smistachkin 13880372ae2Smistachkin /// <summary> 13980372ae2Smistachkin /// This will be the exit code returned by this tool after the file 14080372ae2Smistachkin /// download completes, successfully or otherwise. This value is only 14180372ae2Smistachkin /// changed by the <see cref="DownloadFileCompleted" /> event handler. 14280372ae2Smistachkin /// </summary> 14380372ae2Smistachkin private static ExitCode exitCode = ExitCode.Success; 14480372ae2Smistachkin #endregion 14580372ae2Smistachkin 14680372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 14780372ae2Smistachkin 14880372ae2Smistachkin #region Private Support Methods 14980372ae2Smistachkin /// <summary> 15080372ae2Smistachkin /// This method displays an error message to the console and/or 15180372ae2Smistachkin /// displays the command line usage information for this tool. 15280372ae2Smistachkin /// </summary> 15380372ae2Smistachkin /// <param name="message"> 15480372ae2Smistachkin /// The error message to display, if any. 15580372ae2Smistachkin /// </param> 15680372ae2Smistachkin /// <param name="usage"> 15780372ae2Smistachkin /// Non-zero to display the command line usage information. 15880372ae2Smistachkin /// </param> Error( string message, bool usage )15980372ae2Smistachkin private static void Error( 16080372ae2Smistachkin string message, 16180372ae2Smistachkin bool usage 16280372ae2Smistachkin ) 16380372ae2Smistachkin { 16480372ae2Smistachkin if (message != null) 16580372ae2Smistachkin Console.WriteLine(message); 16680372ae2Smistachkin 16780372ae2Smistachkin string fileName = Path.GetFileName( 16880372ae2Smistachkin Process.GetCurrentProcess().MainModule.FileName); 16980372ae2Smistachkin 170*fe293347Smistachkin Console.WriteLine(String.Format( 171*fe293347Smistachkin "usage: {0} <uri> [fileName]", fileName)); 17280372ae2Smistachkin } 17380372ae2Smistachkin 17480372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 17580372ae2Smistachkin 17680372ae2Smistachkin /// <summary> 17780372ae2Smistachkin /// This method attempts to determine the file name portion of the 17880372ae2Smistachkin /// specified URI. 17980372ae2Smistachkin /// </summary> 18080372ae2Smistachkin /// <param name="uri"> 18180372ae2Smistachkin /// The URI to process. 18280372ae2Smistachkin /// </param> 18380372ae2Smistachkin /// <returns> 18480372ae2Smistachkin /// The file name portion of the specified URI -OR- null if it cannot 18580372ae2Smistachkin /// be determined. 18680372ae2Smistachkin /// </returns> GetFileName( Uri uri )18780372ae2Smistachkin private static string GetFileName( 18880372ae2Smistachkin Uri uri 18980372ae2Smistachkin ) 19080372ae2Smistachkin { 19180372ae2Smistachkin if (uri == null) 19280372ae2Smistachkin return null; 19380372ae2Smistachkin 19480372ae2Smistachkin string pathAndQuery = uri.PathAndQuery; 19580372ae2Smistachkin 19680372ae2Smistachkin if (String.IsNullOrEmpty(pathAndQuery)) 19780372ae2Smistachkin return null; 19880372ae2Smistachkin 19980372ae2Smistachkin int index = pathAndQuery.LastIndexOf('/'); 20080372ae2Smistachkin 20180372ae2Smistachkin if ((index < 0) || (index == pathAndQuery.Length)) 20280372ae2Smistachkin return null; 20380372ae2Smistachkin 20480372ae2Smistachkin return pathAndQuery.Substring(index + 1); 20580372ae2Smistachkin } 20680372ae2Smistachkin #endregion 20780372ae2Smistachkin 20880372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 20980372ae2Smistachkin 21080372ae2Smistachkin #region Private Event Handlers 21180372ae2Smistachkin /// <summary> 21280372ae2Smistachkin /// This method is an event handler that is called when the file 21380372ae2Smistachkin /// download completion percentage changes. It will display progress 21480372ae2Smistachkin /// on the console. Special care is taken to make sure that progress 21580372ae2Smistachkin /// events are not displayed out-of-order, even if duplicate and/or 21680372ae2Smistachkin /// out-of-order events are received. 21780372ae2Smistachkin /// </summary> 21880372ae2Smistachkin /// <param name="sender"> 21980372ae2Smistachkin /// The source of the event. 22080372ae2Smistachkin /// </param> 22180372ae2Smistachkin /// <param name="e"> 22280372ae2Smistachkin /// Information for the event being processed. 22380372ae2Smistachkin /// </param> DownloadProgressChanged( object sender, DownloadProgressChangedEventArgs e )22480372ae2Smistachkin private static void DownloadProgressChanged( 22580372ae2Smistachkin object sender, 22680372ae2Smistachkin DownloadProgressChangedEventArgs e 22780372ae2Smistachkin ) 22880372ae2Smistachkin { 22980372ae2Smistachkin if (e != null) 23080372ae2Smistachkin { 23180372ae2Smistachkin int percent = e.ProgressPercentage; 23280372ae2Smistachkin 23380372ae2Smistachkin lock (syncRoot) 23480372ae2Smistachkin { 23580372ae2Smistachkin if (percent > previousPercent) 23680372ae2Smistachkin { 23780372ae2Smistachkin Console.Write('.'); 23880372ae2Smistachkin 23980372ae2Smistachkin if ((percent % 10) == 0) 24080372ae2Smistachkin Console.Write(" {0}% ", percent); 24180372ae2Smistachkin 24280372ae2Smistachkin previousPercent = percent; 24380372ae2Smistachkin } 24480372ae2Smistachkin } 24580372ae2Smistachkin } 24680372ae2Smistachkin } 24780372ae2Smistachkin 24880372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 24980372ae2Smistachkin 25080372ae2Smistachkin /// <summary> 25180372ae2Smistachkin /// This method is an event handler that is called when the file 25280372ae2Smistachkin /// download has completed, successfully or otherwise. It will 25380372ae2Smistachkin /// display the overall result of the file download on the console, 25480372ae2Smistachkin /// including any <see cref="Exception" /> information, if applicable. 25580372ae2Smistachkin /// The <see cref="exitCode" /> field is changed by this method to 25680372ae2Smistachkin /// indicate the overall result of the file download and the event 25780372ae2Smistachkin /// within the <see cref="doneEvent" /> field will be signaled. 25880372ae2Smistachkin /// </summary> 25980372ae2Smistachkin /// <param name="sender"> 26080372ae2Smistachkin /// The source of the event. 26180372ae2Smistachkin /// </param> 26280372ae2Smistachkin /// <param name="e"> 26380372ae2Smistachkin /// Information for the event being processed. 26480372ae2Smistachkin /// </param> DownloadFileCompleted( object sender, AsyncCompletedEventArgs e )26580372ae2Smistachkin private static void DownloadFileCompleted( 26680372ae2Smistachkin object sender, 26780372ae2Smistachkin AsyncCompletedEventArgs e 26880372ae2Smistachkin ) 26980372ae2Smistachkin { 27080372ae2Smistachkin if (e != null) 27180372ae2Smistachkin { 27280372ae2Smistachkin lock (syncRoot) 27380372ae2Smistachkin { 27480372ae2Smistachkin if (previousPercent < 100) 27580372ae2Smistachkin Console.Write(' '); 27680372ae2Smistachkin } 27780372ae2Smistachkin 27880372ae2Smistachkin if (e.Cancelled) 27980372ae2Smistachkin { 28080372ae2Smistachkin Console.WriteLine("Canceled"); 28180372ae2Smistachkin 28280372ae2Smistachkin lock (syncRoot) 28380372ae2Smistachkin { 28480372ae2Smistachkin exitCode = ExitCode.DownloadCanceled; 28580372ae2Smistachkin } 28680372ae2Smistachkin } 28780372ae2Smistachkin else 28880372ae2Smistachkin { 28980372ae2Smistachkin Exception error = e.Error; 29080372ae2Smistachkin 29180372ae2Smistachkin if (error != null) 29280372ae2Smistachkin { 29380372ae2Smistachkin Console.WriteLine("Error: {0}", error); 29480372ae2Smistachkin 29580372ae2Smistachkin lock (syncRoot) 29680372ae2Smistachkin { 29780372ae2Smistachkin exitCode = ExitCode.DownloadError; 29880372ae2Smistachkin } 29980372ae2Smistachkin } 30080372ae2Smistachkin else 30180372ae2Smistachkin { 30280372ae2Smistachkin Console.WriteLine("Done"); 30380372ae2Smistachkin } 30480372ae2Smistachkin } 30580372ae2Smistachkin } 30680372ae2Smistachkin 30780372ae2Smistachkin if (doneEvent != null) 30880372ae2Smistachkin doneEvent.Set(); 30980372ae2Smistachkin } 31080372ae2Smistachkin #endregion 31180372ae2Smistachkin 31280372ae2Smistachkin /////////////////////////////////////////////////////////////////////// 31380372ae2Smistachkin 31480372ae2Smistachkin #region Program Entry Point 31580372ae2Smistachkin /// <summary> 31680372ae2Smistachkin /// This is the entry-point for this tool. It handles processing the 31780372ae2Smistachkin /// command line arguments, setting up the web client, downloading the 31880372ae2Smistachkin /// file, and saving it to the file system. 31980372ae2Smistachkin /// </summary> 32080372ae2Smistachkin /// <param name="args"> 32180372ae2Smistachkin /// The command line arguments. 32280372ae2Smistachkin /// </param> 32380372ae2Smistachkin /// <returns> 32480372ae2Smistachkin /// Zero upon success; non-zero on failure. This will be one of the 32580372ae2Smistachkin /// values from the <see cref="ExitCode" /> enumeration. 32680372ae2Smistachkin /// </returns> Main( string[] args )32780372ae2Smistachkin private static int Main( 32880372ae2Smistachkin string[] args 32980372ae2Smistachkin ) 33080372ae2Smistachkin { 33180372ae2Smistachkin // 33280372ae2Smistachkin // NOTE: Sanity check the command line arguments. 33380372ae2Smistachkin // 33480372ae2Smistachkin if (args == null) 33580372ae2Smistachkin { 33680372ae2Smistachkin Error(null, true); 33780372ae2Smistachkin return (int)ExitCode.MissingArgs; 33880372ae2Smistachkin } 33980372ae2Smistachkin 340*fe293347Smistachkin if ((args.Length < 1) || (args.Length > 2)) 34180372ae2Smistachkin { 34280372ae2Smistachkin Error(null, true); 34380372ae2Smistachkin return (int)ExitCode.WrongNumArgs; 34480372ae2Smistachkin } 34580372ae2Smistachkin 34680372ae2Smistachkin // 34780372ae2Smistachkin // NOTE: Attempt to convert the first (and only) command line 34880372ae2Smistachkin // argument to an absolute URI. 34980372ae2Smistachkin // 35080372ae2Smistachkin Uri uri; 35180372ae2Smistachkin 35280372ae2Smistachkin if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri)) 35380372ae2Smistachkin { 354cc730488Smistachkin Error("Could not create absolute URI from argument.", false); 35580372ae2Smistachkin return (int)ExitCode.BadUri; 35680372ae2Smistachkin } 35780372ae2Smistachkin 35880372ae2Smistachkin // 359*fe293347Smistachkin // NOTE: If a file name was specified on the command line, try to 360*fe293347Smistachkin // use it (without its directory name); otherwise, fallback 361*fe293347Smistachkin // to using the file name portion of the URI. 36280372ae2Smistachkin // 363*fe293347Smistachkin string fileName = (args.Length == 2) ? 364*fe293347Smistachkin Path.GetFileName(args[1]) : null; 365*fe293347Smistachkin 366*fe293347Smistachkin if (String.IsNullOrEmpty(fileName)) 367*fe293347Smistachkin { 368*fe293347Smistachkin // 369*fe293347Smistachkin // NOTE: Attempt to extract the file name portion of the URI 370*fe293347Smistachkin // we just created. 371*fe293347Smistachkin // 372*fe293347Smistachkin fileName = GetFileName(uri); 37380372ae2Smistachkin 37480372ae2Smistachkin if (fileName == null) 37580372ae2Smistachkin { 376*fe293347Smistachkin Error("Could not extract file name from URI.", false); 37780372ae2Smistachkin return (int)ExitCode.BadFileName; 37880372ae2Smistachkin } 379*fe293347Smistachkin } 38080372ae2Smistachkin 38180372ae2Smistachkin // 38280372ae2Smistachkin // NOTE: Grab the temporary path setup for this process. If it is 38380372ae2Smistachkin // unavailable, we will not continue. 38480372ae2Smistachkin // 38580372ae2Smistachkin string directory = Path.GetTempPath(); 38680372ae2Smistachkin 38780372ae2Smistachkin if (String.IsNullOrEmpty(directory) || 38880372ae2Smistachkin !Directory.Exists(directory)) 38980372ae2Smistachkin { 39080372ae2Smistachkin Error("Temporary directory is invalid or unavailable.", false); 39180372ae2Smistachkin return (int)ExitCode.BadTempPath; 39280372ae2Smistachkin } 39380372ae2Smistachkin 39480372ae2Smistachkin try 39580372ae2Smistachkin { 396*fe293347Smistachkin // 397*fe293347Smistachkin // HACK: For use of the TLS 1.2 security protocol because some 398*fe293347Smistachkin // web servers fail without it. In order to support the 399*fe293347Smistachkin // .NET Framework 2.0+ at compilation time, must use its 400*fe293347Smistachkin // integer constant here. 401*fe293347Smistachkin // 402*fe293347Smistachkin ServicePointManager.SecurityProtocol = 403*fe293347Smistachkin (SecurityProtocolType)0xC00; 404*fe293347Smistachkin 40580372ae2Smistachkin using (WebClient webClient = new WebClient()) 40680372ae2Smistachkin { 40780372ae2Smistachkin // 40880372ae2Smistachkin // NOTE: Create the event used to signal completion of the 40980372ae2Smistachkin // file download. 41080372ae2Smistachkin // 41180372ae2Smistachkin doneEvent = new ManualResetEvent(false); 41280372ae2Smistachkin 41380372ae2Smistachkin // 41480372ae2Smistachkin // NOTE: Hookup the event handlers we care about on the web 41580372ae2Smistachkin // client. These are necessary because the file is 41680372ae2Smistachkin // downloaded asynchronously. 41780372ae2Smistachkin // 41880372ae2Smistachkin webClient.DownloadProgressChanged += 41980372ae2Smistachkin new DownloadProgressChangedEventHandler( 42080372ae2Smistachkin DownloadProgressChanged); 42180372ae2Smistachkin 42280372ae2Smistachkin webClient.DownloadFileCompleted += 42380372ae2Smistachkin new AsyncCompletedEventHandler( 42480372ae2Smistachkin DownloadFileCompleted); 42580372ae2Smistachkin 42680372ae2Smistachkin // 42780372ae2Smistachkin // NOTE: Build the fully qualified path and file name, 42880372ae2Smistachkin // within the temporary directory, where the file to 42980372ae2Smistachkin // be downloaded will be saved. 43080372ae2Smistachkin // 43180372ae2Smistachkin fileName = Path.Combine(directory, fileName); 43280372ae2Smistachkin 43380372ae2Smistachkin // 43480372ae2Smistachkin // NOTE: If the file name already exists (in the temporary) 43580372ae2Smistachkin // directory, delete it. 43680372ae2Smistachkin // 43780372ae2Smistachkin // TODO: Perhaps an error should be raised here instead? 43880372ae2Smistachkin // 43980372ae2Smistachkin if (File.Exists(fileName)) 44080372ae2Smistachkin File.Delete(fileName); 44180372ae2Smistachkin 44280372ae2Smistachkin // 44380372ae2Smistachkin // NOTE: After kicking off the asynchronous file download 44480372ae2Smistachkin // process, wait [forever] until the "done" event is 44580372ae2Smistachkin // signaled. 44680372ae2Smistachkin // 44780372ae2Smistachkin Console.WriteLine( 44880372ae2Smistachkin "Downloading \"{0}\" to \"{1}\"...", uri, fileName); 44980372ae2Smistachkin 45080372ae2Smistachkin webClient.DownloadFileAsync(uri, fileName); 45180372ae2Smistachkin doneEvent.WaitOne(); 45280372ae2Smistachkin } 45380372ae2Smistachkin 45480372ae2Smistachkin lock (syncRoot) 45580372ae2Smistachkin { 45680372ae2Smistachkin return (int)exitCode; 45780372ae2Smistachkin } 45880372ae2Smistachkin } 45980372ae2Smistachkin catch (Exception e) 46080372ae2Smistachkin { 46180372ae2Smistachkin // 46280372ae2Smistachkin // NOTE: An exception was caught. Report it via the console 46380372ae2Smistachkin // and return failure. 46480372ae2Smistachkin // 46580372ae2Smistachkin Error(e.ToString(), false); 46680372ae2Smistachkin return (int)ExitCode.Exception; 46780372ae2Smistachkin } 46880372ae2Smistachkin } 46980372ae2Smistachkin #endregion 47080372ae2Smistachkin } 47180372ae2Smistachkin } 472