Add tests for the new code on this branch.
[sqlite.git] / tool / GetFile.cs
blob1784a7926386bc37b82c54cba4dfe2974ad40c33
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.
11 *************************************************************************
12 ** This file contains C# code to download a single file based on a URI.
15 using System;
16 using System.ComponentModel;
17 using System.Diagnostics;
18 using System.IO;
19 using System.Net;
20 using System.Reflection;
21 using System.Runtime.InteropServices;
22 using System.Threading;
24 ///////////////////////////////////////////////////////////////////////////////
26 #region Assembly Metadata
27 [assembly: AssemblyTitle("GetFile Tool")]
28 [assembly: AssemblyDescription("Download a single file based on a URI.")]
29 [assembly: AssemblyCompany("SQLite Development Team")]
30 [assembly: AssemblyProduct("SQLite")]
31 [assembly: AssemblyCopyright("Public Domain")]
32 [assembly: ComVisible(false)]
33 [assembly: Guid("5c4b3728-1693-4a33-a218-8e6973ca15a6")]
34 [assembly: AssemblyVersion("1.0.*")]
36 #if DEBUG
37 [assembly: AssemblyConfiguration("Debug")]
38 #else
39 [assembly: AssemblyConfiguration("Release")]
40 #endif
41 #endregion
43 ///////////////////////////////////////////////////////////////////////////////
45 namespace GetFile
47 /// <summary>
48 /// This enumeration is used to represent all the possible exit codes from
49 /// this tool.
50 /// </summary>
51 internal enum ExitCode
53 /// <summary>
54 /// The file download was a success.
55 /// </summary>
56 Success = 0,
58 /// <summary>
59 /// The command line arguments are missing (i.e. null). Generally,
60 /// this should not happen.
61 /// </summary>
62 MissingArgs = 1,
64 /// <summary>
65 /// The wrong number of command line arguments was supplied.
66 /// </summary>
67 WrongNumArgs = 2,
69 /// <summary>
70 /// The URI specified on the command line could not be parsed as a
71 /// supported absolute URI.
72 /// </summary>
73 BadUri = 3,
75 /// <summary>
76 /// The file name portion of the URI specified on the command line
77 /// could not be extracted from it.
78 /// </summary>
79 BadFileName = 4,
81 /// <summary>
82 /// The temporary directory is either invalid (i.e. null) or does not
83 /// represent an available directory.
84 /// </summary>
85 BadTempPath = 5,
87 /// <summary>
88 /// An exception was caught in <see cref="Main" />. Generally, this
89 /// should not happen.
90 /// </summary>
91 Exception = 6,
93 /// <summary>
94 /// The file download was canceled. This tool does not make use of
95 /// the <see cref="WebClient.CancelAsync" /> method; therefore, this
96 /// should not happen.
97 /// </summary>
98 DownloadCanceled = 7,
100 /// <summary>
101 /// The file download encountered an error. Further information about
102 /// this error should be displayed on the console.
103 /// </summary>
104 DownloadError = 8
107 ///////////////////////////////////////////////////////////////////////////
109 internal static class Program
111 #region Private Data
112 /// <summary>
113 /// This is used to synchronize multithreaded access to the
114 /// <see cref="previousPercent" /> and <see cref="exitCode"/>
115 /// fields.
116 /// </summary>
117 private static readonly object syncRoot = new object();
119 ///////////////////////////////////////////////////////////////////////
121 /// <summary>
122 /// This event will be signed when the file download has completed,
123 /// even if the file download itself was canceled or unsuccessful.
124 /// </summary>
125 private static EventWaitHandle doneEvent;
127 ///////////////////////////////////////////////////////////////////////
129 /// <summary>
130 /// The previous file download completion percentage seen by the
131 /// <see cref="DownloadProgressChanged" /> event handler. This value
132 /// is never decreased, nor is it ever reset to zero.
133 /// </summary>
134 private static int previousPercent = 0;
136 ///////////////////////////////////////////////////////////////////////
138 /// <summary>
139 /// This will be the exit code returned by this tool after the file
140 /// download completes, successfully or otherwise. This value is only
141 /// changed by the <see cref="DownloadFileCompleted" /> event handler.
142 /// </summary>
143 private static ExitCode exitCode = ExitCode.Success;
144 #endregion
146 ///////////////////////////////////////////////////////////////////////
148 #region Private Support Methods
149 /// <summary>
150 /// This method displays an error message to the console and/or
151 /// displays the command line usage information for this tool.
152 /// </summary>
153 /// <param name="message">
154 /// The error message to display, if any.
155 /// </param>
156 /// <param name="usage">
157 /// Non-zero to display the command line usage information.
158 /// </param>
159 private static void Error(
160 string message,
161 bool usage
164 if (message != null)
165 Console.WriteLine(message);
167 string fileName = Path.GetFileName(
168 Process.GetCurrentProcess().MainModule.FileName);
170 Console.WriteLine(String.Format(
171 "usage: {0} <uri> [fileName]", fileName));
174 ///////////////////////////////////////////////////////////////////////
176 /// <summary>
177 /// This method attempts to determine the file name portion of the
178 /// specified URI.
179 /// </summary>
180 /// <param name="uri">
181 /// The URI to process.
182 /// </param>
183 /// <returns>
184 /// The file name portion of the specified URI -OR- null if it cannot
185 /// be determined.
186 /// </returns>
187 private static string GetFileName(
188 Uri uri
191 if (uri == null)
192 return null;
194 string pathAndQuery = uri.PathAndQuery;
196 if (String.IsNullOrEmpty(pathAndQuery))
197 return null;
199 int index = pathAndQuery.LastIndexOf('/');
201 if ((index < 0) || (index == pathAndQuery.Length))
202 return null;
204 return pathAndQuery.Substring(index + 1);
206 #endregion
208 ///////////////////////////////////////////////////////////////////////
210 #region Private Event Handlers
211 /// <summary>
212 /// This method is an event handler that is called when the file
213 /// download completion percentage changes. It will display progress
214 /// on the console. Special care is taken to make sure that progress
215 /// events are not displayed out-of-order, even if duplicate and/or
216 /// out-of-order events are received.
217 /// </summary>
218 /// <param name="sender">
219 /// The source of the event.
220 /// </param>
221 /// <param name="e">
222 /// Information for the event being processed.
223 /// </param>
224 private static void DownloadProgressChanged(
225 object sender,
226 DownloadProgressChangedEventArgs e
229 if (e != null)
231 int percent = e.ProgressPercentage;
233 lock (syncRoot)
235 if (percent > previousPercent)
237 Console.Write('.');
239 if ((percent % 10) == 0)
240 Console.Write(" {0}% ", percent);
242 previousPercent = percent;
248 ///////////////////////////////////////////////////////////////////////
250 /// <summary>
251 /// This method is an event handler that is called when the file
252 /// download has completed, successfully or otherwise. It will
253 /// display the overall result of the file download on the console,
254 /// including any <see cref="Exception" /> information, if applicable.
255 /// The <see cref="exitCode" /> field is changed by this method to
256 /// indicate the overall result of the file download and the event
257 /// within the <see cref="doneEvent" /> field will be signaled.
258 /// </summary>
259 /// <param name="sender">
260 /// The source of the event.
261 /// </param>
262 /// <param name="e">
263 /// Information for the event being processed.
264 /// </param>
265 private static void DownloadFileCompleted(
266 object sender,
267 AsyncCompletedEventArgs e
270 if (e != null)
272 lock (syncRoot)
274 if (previousPercent < 100)
275 Console.Write(' ');
278 if (e.Cancelled)
280 Console.WriteLine("Canceled");
282 lock (syncRoot)
284 exitCode = ExitCode.DownloadCanceled;
287 else
289 Exception error = e.Error;
291 if (error != null)
293 Console.WriteLine("Error: {0}", error);
295 lock (syncRoot)
297 exitCode = ExitCode.DownloadError;
300 else
302 Console.WriteLine("Done");
307 if (doneEvent != null)
308 doneEvent.Set();
310 #endregion
312 ///////////////////////////////////////////////////////////////////////
314 #region Program Entry Point
315 /// <summary>
316 /// This is the entry-point for this tool. It handles processing the
317 /// command line arguments, setting up the web client, downloading the
318 /// file, and saving it to the file system.
319 /// </summary>
320 /// <param name="args">
321 /// The command line arguments.
322 /// </param>
323 /// <returns>
324 /// Zero upon success; non-zero on failure. This will be one of the
325 /// values from the <see cref="ExitCode" /> enumeration.
326 /// </returns>
327 private static int Main(
328 string[] args
332 // NOTE: Sanity check the command line arguments.
334 if (args == null)
336 Error(null, true);
337 return (int)ExitCode.MissingArgs;
340 if ((args.Length < 1) || (args.Length > 2))
342 Error(null, true);
343 return (int)ExitCode.WrongNumArgs;
347 // NOTE: Attempt to convert the first (and only) command line
348 // argument to an absolute URI.
350 Uri uri;
352 if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri))
354 Error("Could not create absolute URI from argument.", false);
355 return (int)ExitCode.BadUri;
359 // NOTE: If a file name was specified on the command line, try to
360 // use it (without its directory name); otherwise, fallback
361 // to using the file name portion of the URI.
363 string fileName = (args.Length == 2) ?
364 Path.GetFileName(args[1]) : null;
366 if (String.IsNullOrEmpty(fileName))
369 // NOTE: Attempt to extract the file name portion of the URI
370 // we just created.
372 fileName = GetFileName(uri);
374 if (fileName == null)
376 Error("Could not extract file name from URI.", false);
377 return (int)ExitCode.BadFileName;
382 // NOTE: Grab the temporary path setup for this process. If it is
383 // unavailable, we will not continue.
385 string directory = Path.GetTempPath();
387 if (String.IsNullOrEmpty(directory) ||
388 !Directory.Exists(directory))
390 Error("Temporary directory is invalid or unavailable.", false);
391 return (int)ExitCode.BadTempPath;
397 // HACK: For use of the TLS 1.2 security protocol because some
398 // web servers fail without it. In order to support the
399 // .NET Framework 2.0+ at compilation time, must use its
400 // integer constant here.
402 ServicePointManager.SecurityProtocol =
403 (SecurityProtocolType)0xC00;
405 using (WebClient webClient = new WebClient())
408 // NOTE: Create the event used to signal completion of the
409 // file download.
411 doneEvent = new ManualResetEvent(false);
414 // NOTE: Hookup the event handlers we care about on the web
415 // client. These are necessary because the file is
416 // downloaded asynchronously.
418 webClient.DownloadProgressChanged +=
419 new DownloadProgressChangedEventHandler(
420 DownloadProgressChanged);
422 webClient.DownloadFileCompleted +=
423 new AsyncCompletedEventHandler(
424 DownloadFileCompleted);
427 // NOTE: Build the fully qualified path and file name,
428 // within the temporary directory, where the file to
429 // be downloaded will be saved.
431 fileName = Path.Combine(directory, fileName);
434 // NOTE: If the file name already exists (in the temporary)
435 // directory, delete it.
437 // TODO: Perhaps an error should be raised here instead?
439 if (File.Exists(fileName))
440 File.Delete(fileName);
443 // NOTE: After kicking off the asynchronous file download
444 // process, wait [forever] until the "done" event is
445 // signaled.
447 Console.WriteLine(
448 "Downloading \"{0}\" to \"{1}\"...", uri, fileName);
450 webClient.DownloadFileAsync(uri, fileName);
451 doneEvent.WaitOne();
454 lock (syncRoot)
456 return (int)exitCode;
459 catch (Exception e)
462 // NOTE: An exception was caught. Report it via the console
463 // and return failure.
465 Error(e.ToString(), false);
466 return (int)ExitCode.Exception;
469 #endregion