xref: /sqlite-3.40.0/tool/GetFile.cs (revision fe293347)
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 
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;
23 
24 ///////////////////////////////////////////////////////////////////////////////
25 
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.*")]
35 
36 #if DEBUG
37 [assembly: AssemblyConfiguration("Debug")]
38 #else
39 [assembly: AssemblyConfiguration("Release")]
40 #endif
41 #endregion
42 
43 ///////////////////////////////////////////////////////////////////////////////
44 
45 namespace GetFile
46 {
47     /// <summary>
48     /// This enumeration is used to represent all the possible exit codes from
49     /// this tool.
50     /// </summary>
51     internal enum ExitCode
52     {
53         /// <summary>
54         /// The file download was a success.
55         /// </summary>
56         Success = 0,
57 
58         /// <summary>
59         /// The command line arguments are missing (i.e. null).  Generally,
60         /// this should not happen.
61         /// </summary>
62         MissingArgs = 1,
63 
64         /// <summary>
65         /// The wrong number of command line arguments was supplied.
66         /// </summary>
67         WrongNumArgs = 2,
68 
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,
74 
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,
80 
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,
86 
87         /// <summary>
88         /// An exception was caught in <see cref="Main" />.  Generally, this
89         /// should not happen.
90         /// </summary>
91         Exception = 6,
92 
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,
99 
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
105     }
106 
107     ///////////////////////////////////////////////////////////////////////////
108 
109     internal static class Program
110     {
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();
118 
119         ///////////////////////////////////////////////////////////////////////
120 
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;
126 
127         ///////////////////////////////////////////////////////////////////////
128 
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;
135 
136         ///////////////////////////////////////////////////////////////////////
137 
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
145 
146         ///////////////////////////////////////////////////////////////////////
147 
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>
Error( string message, bool usage )159         private static void Error(
160             string message,
161             bool usage
162             )
163         {
164             if (message != null)
165                 Console.WriteLine(message);
166 
167             string fileName = Path.GetFileName(
168                 Process.GetCurrentProcess().MainModule.FileName);
169 
170             Console.WriteLine(String.Format(
171                 "usage: {0} <uri> [fileName]", fileName));
172         }
173 
174         ///////////////////////////////////////////////////////////////////////
175 
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>
GetFileName( Uri uri )187         private static string GetFileName(
188             Uri uri
189             )
190         {
191             if (uri == null)
192                 return null;
193 
194             string pathAndQuery = uri.PathAndQuery;
195 
196             if (String.IsNullOrEmpty(pathAndQuery))
197                 return null;
198 
199             int index = pathAndQuery.LastIndexOf('/');
200 
201             if ((index < 0) || (index == pathAndQuery.Length))
202                 return null;
203 
204             return pathAndQuery.Substring(index + 1);
205         }
206         #endregion
207 
208         ///////////////////////////////////////////////////////////////////////
209 
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>
DownloadProgressChanged( object sender, DownloadProgressChangedEventArgs e )224         private static void DownloadProgressChanged(
225             object sender,
226             DownloadProgressChangedEventArgs e
227             )
228         {
229             if (e != null)
230             {
231                 int percent = e.ProgressPercentage;
232 
233                 lock (syncRoot)
234                 {
235                     if (percent > previousPercent)
236                     {
237                         Console.Write('.');
238 
239                         if ((percent % 10) == 0)
240                             Console.Write(" {0}% ", percent);
241 
242                         previousPercent = percent;
243                     }
244                 }
245             }
246         }
247 
248         ///////////////////////////////////////////////////////////////////////
249 
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>
DownloadFileCompleted( object sender, AsyncCompletedEventArgs e )265         private static void DownloadFileCompleted(
266             object sender,
267             AsyncCompletedEventArgs e
268             )
269         {
270             if (e != null)
271             {
272                 lock (syncRoot)
273                 {
274                     if (previousPercent < 100)
275                         Console.Write(' ');
276                 }
277 
278                 if (e.Cancelled)
279                 {
280                     Console.WriteLine("Canceled");
281 
282                     lock (syncRoot)
283                     {
284                         exitCode = ExitCode.DownloadCanceled;
285                     }
286                 }
287                 else
288                 {
289                     Exception error = e.Error;
290 
291                     if (error != null)
292                     {
293                         Console.WriteLine("Error: {0}", error);
294 
295                         lock (syncRoot)
296                         {
297                             exitCode = ExitCode.DownloadError;
298                         }
299                     }
300                     else
301                     {
302                         Console.WriteLine("Done");
303                     }
304                 }
305             }
306 
307             if (doneEvent != null)
308                 doneEvent.Set();
309         }
310         #endregion
311 
312         ///////////////////////////////////////////////////////////////////////
313 
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>
Main( string[] args )327         private static int Main(
328             string[] args
329             )
330         {
331             //
332             // NOTE: Sanity check the command line arguments.
333             //
334             if (args == null)
335             {
336                 Error(null, true);
337                 return (int)ExitCode.MissingArgs;
338             }
339 
340             if ((args.Length < 1) || (args.Length > 2))
341             {
342                 Error(null, true);
343                 return (int)ExitCode.WrongNumArgs;
344             }
345 
346             //
347             // NOTE: Attempt to convert the first (and only) command line
348             //       argument to an absolute URI.
349             //
350             Uri uri;
351 
352             if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri))
353             {
354                 Error("Could not create absolute URI from argument.", false);
355                 return (int)ExitCode.BadUri;
356             }
357 
358             //
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.
362             //
363             string fileName = (args.Length == 2) ?
364                 Path.GetFileName(args[1]) : null;
365 
366             if (String.IsNullOrEmpty(fileName))
367             {
368                 //
369                 // NOTE: Attempt to extract the file name portion of the URI
370                 //       we just created.
371                 //
372                 fileName = GetFileName(uri);
373 
374                 if (fileName == null)
375                 {
376                     Error("Could not extract file name from URI.", false);
377                     return (int)ExitCode.BadFileName;
378                 }
379             }
380 
381             //
382             // NOTE: Grab the temporary path setup for this process.  If it is
383             //       unavailable, we will not continue.
384             //
385             string directory = Path.GetTempPath();
386 
387             if (String.IsNullOrEmpty(directory) ||
388                 !Directory.Exists(directory))
389             {
390                 Error("Temporary directory is invalid or unavailable.", false);
391                 return (int)ExitCode.BadTempPath;
392             }
393 
394             try
395             {
396                 //
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.
401                 //
402                 ServicePointManager.SecurityProtocol =
403                     (SecurityProtocolType)0xC00;
404 
405                 using (WebClient webClient = new WebClient())
406                 {
407                     //
408                     // NOTE: Create the event used to signal completion of the
409                     //       file download.
410                     //
411                     doneEvent = new ManualResetEvent(false);
412 
413                     //
414                     // NOTE: Hookup the event handlers we care about on the web
415                     //       client.  These are necessary because the file is
416                     //       downloaded asynchronously.
417                     //
418                     webClient.DownloadProgressChanged +=
419                         new DownloadProgressChangedEventHandler(
420                             DownloadProgressChanged);
421 
422                     webClient.DownloadFileCompleted +=
423                         new AsyncCompletedEventHandler(
424                             DownloadFileCompleted);
425 
426                     //
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.
430                     //
431                     fileName = Path.Combine(directory, fileName);
432 
433                     //
434                     // NOTE: If the file name already exists (in the temporary)
435                     //       directory, delete it.
436                     //
437                     // TODO: Perhaps an error should be raised here instead?
438                     //
439                     if (File.Exists(fileName))
440                         File.Delete(fileName);
441 
442                     //
443                     // NOTE: After kicking off the asynchronous file download
444                     //       process, wait [forever] until the "done" event is
445                     //       signaled.
446                     //
447                     Console.WriteLine(
448                         "Downloading \"{0}\" to \"{1}\"...", uri, fileName);
449 
450                     webClient.DownloadFileAsync(uri, fileName);
451                     doneEvent.WaitOne();
452                 }
453 
454                 lock (syncRoot)
455                 {
456                     return (int)exitCode;
457                 }
458             }
459             catch (Exception e)
460             {
461                 //
462                 // NOTE: An exception was caught.  Report it via the console
463                 //       and return failure.
464                 //
465                 Error(e.ToString(), false);
466                 return (int)ExitCode.Exception;
467             }
468         }
469         #endregion
470     }
471 }
472