GetFile.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /*
  2. ** 2015 October 7
  3. **
  4. ** The author disclaims copyright to this source code. In place of
  5. ** a legal notice, here is a blessing:
  6. **
  7. ** May you do good and not evil.
  8. ** May you find forgiveness for yourself and forgive others.
  9. ** May you share freely, never taking more than you give.
  10. **
  11. *************************************************************************
  12. ** This file contains C# code to download a single file based on a URI.
  13. */
  14. using System;
  15. using System.ComponentModel;
  16. using System.Diagnostics;
  17. using System.IO;
  18. using System.Net;
  19. using System.Reflection;
  20. using System.Runtime.InteropServices;
  21. using System.Threading;
  22. ///////////////////////////////////////////////////////////////////////////////
  23. #region Assembly Metadata
  24. [assembly: AssemblyTitle("GetFile Tool")]
  25. [assembly: AssemblyDescription("Download a single file based on a URI.")]
  26. [assembly: AssemblyCompany("SQLite Development Team")]
  27. [assembly: AssemblyProduct("SQLite")]
  28. [assembly: AssemblyCopyright("Public Domain")]
  29. [assembly: ComVisible(false)]
  30. [assembly: Guid("5c4b3728-1693-4a33-a218-8e6973ca15a6")]
  31. [assembly: AssemblyVersion("1.0.*")]
  32. #if DEBUG
  33. [assembly: AssemblyConfiguration("Debug")]
  34. #else
  35. [assembly: AssemblyConfiguration("Release")]
  36. #endif
  37. #endregion
  38. ///////////////////////////////////////////////////////////////////////////////
  39. namespace GetFile
  40. {
  41. /// <summary>
  42. /// This enumeration is used to represent all the possible exit codes from
  43. /// this tool.
  44. /// </summary>
  45. internal enum ExitCode
  46. {
  47. /// <summary>
  48. /// The file download was a success.
  49. /// </summary>
  50. Success = 0,
  51. /// <summary>
  52. /// The command line arguments are missing (i.e. null). Generally,
  53. /// this should not happen.
  54. /// </summary>
  55. MissingArgs = 1,
  56. /// <summary>
  57. /// The wrong number of command line arguments was supplied.
  58. /// </summary>
  59. WrongNumArgs = 2,
  60. /// <summary>
  61. /// The URI specified on the command line could not be parsed as a
  62. /// supported absolute URI.
  63. /// </summary>
  64. BadUri = 3,
  65. /// <summary>
  66. /// The file name portion of the URI specified on the command line
  67. /// could not be extracted from it.
  68. /// </summary>
  69. BadFileName = 4,
  70. /// <summary>
  71. /// The temporary directory is either invalid (i.e. null) or does not
  72. /// represent an available directory.
  73. /// </summary>
  74. BadTempPath = 5,
  75. /// <summary>
  76. /// An exception was caught in <see cref="Main" />. Generally, this
  77. /// should not happen.
  78. /// </summary>
  79. Exception = 6,
  80. /// <summary>
  81. /// The file download was canceled. This tool does not make use of
  82. /// the <see cref="WebClient.CancelAsync" /> method; therefore, this
  83. /// should not happen.
  84. /// </summary>
  85. DownloadCanceled = 7,
  86. /// <summary>
  87. /// The file download encountered an error. Further information about
  88. /// this error should be displayed on the console.
  89. /// </summary>
  90. DownloadError = 8
  91. }
  92. ///////////////////////////////////////////////////////////////////////////
  93. internal static class Program
  94. {
  95. #region Private Data
  96. /// <summary>
  97. /// This is used to synchronize multithreaded access to the
  98. /// <see cref="previousPercent" /> and <see cref="exitCode"/>
  99. /// fields.
  100. /// </summary>
  101. private static readonly object syncRoot = new object();
  102. ///////////////////////////////////////////////////////////////////////
  103. /// <summary>
  104. /// This event will be signed when the file download has completed,
  105. /// even if the file download itself was canceled or unsuccessful.
  106. /// </summary>
  107. private static EventWaitHandle doneEvent;
  108. ///////////////////////////////////////////////////////////////////////
  109. /// <summary>
  110. /// The previous file download completion percentage seen by the
  111. /// <see cref="DownloadProgressChanged" /> event handler. This value
  112. /// is never decreased, nor is it ever reset to zero.
  113. /// </summary>
  114. private static int previousPercent = 0;
  115. ///////////////////////////////////////////////////////////////////////
  116. /// <summary>
  117. /// This will be the exit code returned by this tool after the file
  118. /// download completes, successfully or otherwise. This value is only
  119. /// changed by the <see cref="DownloadFileCompleted" /> event handler.
  120. /// </summary>
  121. private static ExitCode exitCode = ExitCode.Success;
  122. #endregion
  123. ///////////////////////////////////////////////////////////////////////
  124. #region Private Support Methods
  125. /// <summary>
  126. /// This method displays an error message to the console and/or
  127. /// displays the command line usage information for this tool.
  128. /// </summary>
  129. /// <param name="message">
  130. /// The error message to display, if any.
  131. /// </param>
  132. /// <param name="usage">
  133. /// Non-zero to display the command line usage information.
  134. /// </param>
  135. private static void Error(
  136. string message,
  137. bool usage
  138. )
  139. {
  140. if (message != null)
  141. Console.WriteLine(message);
  142. string fileName = Path.GetFileName(
  143. Process.GetCurrentProcess().MainModule.FileName);
  144. Console.WriteLine(String.Format(
  145. "usage: {0} <uri> [fileName]", fileName));
  146. }
  147. ///////////////////////////////////////////////////////////////////////
  148. /// <summary>
  149. /// This method attempts to determine the file name portion of the
  150. /// specified URI.
  151. /// </summary>
  152. /// <param name="uri">
  153. /// The URI to process.
  154. /// </param>
  155. /// <returns>
  156. /// The file name portion of the specified URI -OR- null if it cannot
  157. /// be determined.
  158. /// </returns>
  159. private static string GetFileName(
  160. Uri uri
  161. )
  162. {
  163. if (uri == null)
  164. return null;
  165. string pathAndQuery = uri.PathAndQuery;
  166. if (String.IsNullOrEmpty(pathAndQuery))
  167. return null;
  168. int index = pathAndQuery.LastIndexOf('/');
  169. if ((index < 0) || (index == pathAndQuery.Length))
  170. return null;
  171. return pathAndQuery.Substring(index + 1);
  172. }
  173. #endregion
  174. ///////////////////////////////////////////////////////////////////////
  175. #region Private Event Handlers
  176. /// <summary>
  177. /// This method is an event handler that is called when the file
  178. /// download completion percentage changes. It will display progress
  179. /// on the console. Special care is taken to make sure that progress
  180. /// events are not displayed out-of-order, even if duplicate and/or
  181. /// out-of-order events are received.
  182. /// </summary>
  183. /// <param name="sender">
  184. /// The source of the event.
  185. /// </param>
  186. /// <param name="e">
  187. /// Information for the event being processed.
  188. /// </param>
  189. private static void DownloadProgressChanged(
  190. object sender,
  191. DownloadProgressChangedEventArgs e
  192. )
  193. {
  194. if (e != null)
  195. {
  196. int percent = e.ProgressPercentage;
  197. lock (syncRoot)
  198. {
  199. if (percent > previousPercent)
  200. {
  201. Console.Write('.');
  202. if ((percent % 10) == 0)
  203. Console.Write(" {0}% ", percent);
  204. previousPercent = percent;
  205. }
  206. }
  207. }
  208. }
  209. ///////////////////////////////////////////////////////////////////////
  210. /// <summary>
  211. /// This method is an event handler that is called when the file
  212. /// download has completed, successfully or otherwise. It will
  213. /// display the overall result of the file download on the console,
  214. /// including any <see cref="Exception" /> information, if applicable.
  215. /// The <see cref="exitCode" /> field is changed by this method to
  216. /// indicate the overall result of the file download and the event
  217. /// within the <see cref="doneEvent" /> field will be signaled.
  218. /// </summary>
  219. /// <param name="sender">
  220. /// The source of the event.
  221. /// </param>
  222. /// <param name="e">
  223. /// Information for the event being processed.
  224. /// </param>
  225. private static void DownloadFileCompleted(
  226. object sender,
  227. AsyncCompletedEventArgs e
  228. )
  229. {
  230. if (e != null)
  231. {
  232. lock (syncRoot)
  233. {
  234. if (previousPercent < 100)
  235. Console.Write(' ');
  236. }
  237. if (e.Cancelled)
  238. {
  239. Console.WriteLine("Canceled");
  240. lock (syncRoot)
  241. {
  242. exitCode = ExitCode.DownloadCanceled;
  243. }
  244. }
  245. else
  246. {
  247. Exception error = e.Error;
  248. if (error != null)
  249. {
  250. Console.WriteLine("Error: {0}", error);
  251. lock (syncRoot)
  252. {
  253. exitCode = ExitCode.DownloadError;
  254. }
  255. }
  256. else
  257. {
  258. Console.WriteLine("Done");
  259. }
  260. }
  261. }
  262. if (doneEvent != null)
  263. doneEvent.Set();
  264. }
  265. #endregion
  266. ///////////////////////////////////////////////////////////////////////
  267. #region Program Entry Point
  268. /// <summary>
  269. /// This is the entry-point for this tool. It handles processing the
  270. /// command line arguments, setting up the web client, downloading the
  271. /// file, and saving it to the file system.
  272. /// </summary>
  273. /// <param name="args">
  274. /// The command line arguments.
  275. /// </param>
  276. /// <returns>
  277. /// Zero upon success; non-zero on failure. This will be one of the
  278. /// values from the <see cref="ExitCode" /> enumeration.
  279. /// </returns>
  280. private static int Main(
  281. string[] args
  282. )
  283. {
  284. //
  285. // NOTE: Sanity check the command line arguments.
  286. //
  287. if (args == null)
  288. {
  289. Error(null, true);
  290. return (int)ExitCode.MissingArgs;
  291. }
  292. if ((args.Length < 1) || (args.Length > 2))
  293. {
  294. Error(null, true);
  295. return (int)ExitCode.WrongNumArgs;
  296. }
  297. //
  298. // NOTE: Attempt to convert the first (and only) command line
  299. // argument to an absolute URI.
  300. //
  301. Uri uri;
  302. if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri))
  303. {
  304. Error("Could not create absolute URI from argument.", false);
  305. return (int)ExitCode.BadUri;
  306. }
  307. //
  308. // NOTE: If a file name was specified on the command line, try to
  309. // use it (without its directory name); otherwise, fallback
  310. // to using the file name portion of the URI.
  311. //
  312. string fileName = (args.Length == 2) ?
  313. Path.GetFileName(args[1]) : null;
  314. if (String.IsNullOrEmpty(fileName))
  315. {
  316. //
  317. // NOTE: Attempt to extract the file name portion of the URI
  318. // we just created.
  319. //
  320. fileName = GetFileName(uri);
  321. if (fileName == null)
  322. {
  323. Error("Could not extract file name from URI.", false);
  324. return (int)ExitCode.BadFileName;
  325. }
  326. }
  327. //
  328. // NOTE: Grab the temporary path setup for this process. If it is
  329. // unavailable, we will not continue.
  330. //
  331. string directory = Path.GetTempPath();
  332. if (String.IsNullOrEmpty(directory) ||
  333. !Directory.Exists(directory))
  334. {
  335. Error("Temporary directory is invalid or unavailable.", false);
  336. return (int)ExitCode.BadTempPath;
  337. }
  338. try
  339. {
  340. //
  341. // HACK: For use of the TLS 1.2 security protocol because some
  342. // web servers fail without it. In order to support the
  343. // .NET Framework 2.0+ at compilation time, must use its
  344. // integer constant here.
  345. //
  346. ServicePointManager.SecurityProtocol =
  347. (SecurityProtocolType)0xC00;
  348. using (WebClient webClient = new WebClient())
  349. {
  350. //
  351. // NOTE: Create the event used to signal completion of the
  352. // file download.
  353. //
  354. doneEvent = new ManualResetEvent(false);
  355. //
  356. // NOTE: Hookup the event handlers we care about on the web
  357. // client. These are necessary because the file is
  358. // downloaded asynchronously.
  359. //
  360. webClient.DownloadProgressChanged +=
  361. new DownloadProgressChangedEventHandler(
  362. DownloadProgressChanged);
  363. webClient.DownloadFileCompleted +=
  364. new AsyncCompletedEventHandler(
  365. DownloadFileCompleted);
  366. //
  367. // NOTE: Build the fully qualified path and file name,
  368. // within the temporary directory, where the file to
  369. // be downloaded will be saved.
  370. //
  371. fileName = Path.Combine(directory, fileName);
  372. //
  373. // NOTE: If the file name already exists (in the temporary)
  374. // directory, delete it.
  375. //
  376. // TODO: Perhaps an error should be raised here instead?
  377. //
  378. if (File.Exists(fileName))
  379. File.Delete(fileName);
  380. //
  381. // NOTE: After kicking off the asynchronous file download
  382. // process, wait [forever] until the "done" event is
  383. // signaled.
  384. //
  385. Console.WriteLine(
  386. "Downloading \"{0}\" to \"{1}\"...", uri, fileName);
  387. webClient.DownloadFileAsync(uri, fileName);
  388. doneEvent.WaitOne();
  389. }
  390. lock (syncRoot)
  391. {
  392. return (int)exitCode;
  393. }
  394. }
  395. catch (Exception e)
  396. {
  397. //
  398. // NOTE: An exception was caught. Report it via the console
  399. // and return failure.
  400. //
  401. Error(e.ToString(), false);
  402. return (int)ExitCode.Exception;
  403. }
  404. }
  405. #endregion
  406. }
  407. }