472 lines
16 KiB
C#
472 lines
16 KiB
C#
/*
|
|
** 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
|
|
}
|
|
}
|