xref: /sqlite-3.40.0/tool/GetFile.cs (revision cc730488)
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>
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("usage: {0} <uri>", fileName));
171         }
172 
173         ///////////////////////////////////////////////////////////////////////
174 
175         /// <summary>
176         /// This method attempts to determine the file name portion of the
177         /// specified URI.
178         /// </summary>
179         /// <param name="uri">
180         /// The URI to process.
181         /// </param>
182         /// <returns>
183         /// The file name portion of the specified URI -OR- null if it cannot
184         /// be determined.
185         /// </returns>
186         private static string GetFileName(
187             Uri uri
188             )
189         {
190             if (uri == null)
191                 return null;
192 
193             string pathAndQuery = uri.PathAndQuery;
194 
195             if (String.IsNullOrEmpty(pathAndQuery))
196                 return null;
197 
198             int index = pathAndQuery.LastIndexOf('/');
199 
200             if ((index < 0) || (index == pathAndQuery.Length))
201                 return null;
202 
203             return pathAndQuery.Substring(index + 1);
204         }
205         #endregion
206 
207         ///////////////////////////////////////////////////////////////////////
208 
209         #region Private Event Handlers
210         /// <summary>
211         /// This method is an event handler that is called when the file
212         /// download completion percentage changes.  It will display progress
213         /// on the console.  Special care is taken to make sure that progress
214         /// events are not displayed out-of-order, even if duplicate and/or
215         /// out-of-order events are received.
216         /// </summary>
217         /// <param name="sender">
218         /// The source of the event.
219         /// </param>
220         /// <param name="e">
221         /// Information for the event being processed.
222         /// </param>
223         private static void DownloadProgressChanged(
224             object sender,
225             DownloadProgressChangedEventArgs e
226             )
227         {
228             if (e != null)
229             {
230                 int percent = e.ProgressPercentage;
231 
232                 lock (syncRoot)
233                 {
234                     if (percent > previousPercent)
235                     {
236                         Console.Write('.');
237 
238                         if ((percent % 10) == 0)
239                             Console.Write(" {0}% ", percent);
240 
241                         previousPercent = percent;
242                     }
243                 }
244             }
245         }
246 
247         ///////////////////////////////////////////////////////////////////////
248 
249         /// <summary>
250         /// This method is an event handler that is called when the file
251         /// download has completed, successfully or otherwise.  It will
252         /// display the overall result of the file download on the console,
253         /// including any <see cref="Exception" /> information, if applicable.
254         /// The <see cref="exitCode" /> field is changed by this method to
255         /// indicate the overall result of the file download and the event
256         /// within the <see cref="doneEvent" /> field will be signaled.
257         /// </summary>
258         /// <param name="sender">
259         /// The source of the event.
260         /// </param>
261         /// <param name="e">
262         /// Information for the event being processed.
263         /// </param>
264         private static void DownloadFileCompleted(
265             object sender,
266             AsyncCompletedEventArgs e
267             )
268         {
269             if (e != null)
270             {
271                 lock (syncRoot)
272                 {
273                     if (previousPercent < 100)
274                         Console.Write(' ');
275                 }
276 
277                 if (e.Cancelled)
278                 {
279                     Console.WriteLine("Canceled");
280 
281                     lock (syncRoot)
282                     {
283                         exitCode = ExitCode.DownloadCanceled;
284                     }
285                 }
286                 else
287                 {
288                     Exception error = e.Error;
289 
290                     if (error != null)
291                     {
292                         Console.WriteLine("Error: {0}", error);
293 
294                         lock (syncRoot)
295                         {
296                             exitCode = ExitCode.DownloadError;
297                         }
298                     }
299                     else
300                     {
301                         Console.WriteLine("Done");
302                     }
303                 }
304             }
305 
306             if (doneEvent != null)
307                 doneEvent.Set();
308         }
309         #endregion
310 
311         ///////////////////////////////////////////////////////////////////////
312 
313         #region Program Entry Point
314         /// <summary>
315         /// This is the entry-point for this tool.  It handles processing the
316         /// command line arguments, setting up the web client, downloading the
317         /// file, and saving it to the file system.
318         /// </summary>
319         /// <param name="args">
320         /// The command line arguments.
321         /// </param>
322         /// <returns>
323         /// Zero upon success; non-zero on failure.  This will be one of the
324         /// values from the <see cref="ExitCode" /> enumeration.
325         /// </returns>
326         private static int Main(
327             string[] args
328             )
329         {
330             //
331             // NOTE: Sanity check the command line arguments.
332             //
333             if (args == null)
334             {
335                 Error(null, true);
336                 return (int)ExitCode.MissingArgs;
337             }
338 
339             if (args.Length != 1)
340             {
341                 Error(null, true);
342                 return (int)ExitCode.WrongNumArgs;
343             }
344 
345             //
346             // NOTE: Attempt to convert the first (and only) command line
347             //       argument to an absolute URI.
348             //
349             Uri uri;
350 
351             if (!Uri.TryCreate(args[0], UriKind.Absolute, out uri))
352             {
353                 Error("Could not create absolute URI from argument.", false);
354                 return (int)ExitCode.BadUri;
355             }
356 
357             //
358             // NOTE: Attempt to extract the file name portion of the URI we
359             //       just created.
360             //
361             string fileName = GetFileName(uri);
362 
363             if (fileName == null)
364             {
365                 Error("Could not extract the file name from the URI.", false);
366                 return (int)ExitCode.BadFileName;
367             }
368 
369             //
370             // NOTE: Grab the temporary path setup for this process.  If it is
371             //       unavailable, we will not continue.
372             //
373             string directory = Path.GetTempPath();
374 
375             if (String.IsNullOrEmpty(directory) ||
376                 !Directory.Exists(directory))
377             {
378                 Error("Temporary directory is invalid or unavailable.", false);
379                 return (int)ExitCode.BadTempPath;
380             }
381 
382             try
383             {
384                 using (WebClient webClient = new WebClient())
385                 {
386                     //
387                     // NOTE: Create the event used to signal completion of the
388                     //       file download.
389                     //
390                     doneEvent = new ManualResetEvent(false);
391 
392                     //
393                     // NOTE: Hookup the event handlers we care about on the web
394                     //       client.  These are necessary because the file is
395                     //       downloaded asynchronously.
396                     //
397                     webClient.DownloadProgressChanged +=
398                         new DownloadProgressChangedEventHandler(
399                             DownloadProgressChanged);
400 
401                     webClient.DownloadFileCompleted +=
402                         new AsyncCompletedEventHandler(
403                             DownloadFileCompleted);
404 
405                     //
406                     // NOTE: Build the fully qualified path and file name,
407                     //       within the temporary directory, where the file to
408                     //       be downloaded will be saved.
409                     //
410                     fileName = Path.Combine(directory, fileName);
411 
412                     //
413                     // NOTE: If the file name already exists (in the temporary)
414                     //       directory, delete it.
415                     //
416                     // TODO: Perhaps an error should be raised here instead?
417                     //
418                     if (File.Exists(fileName))
419                         File.Delete(fileName);
420 
421                     //
422                     // NOTE: After kicking off the asynchronous file download
423                     //       process, wait [forever] until the "done" event is
424                     //       signaled.
425                     //
426                     Console.WriteLine(
427                         "Downloading \"{0}\" to \"{1}\"...", uri, fileName);
428 
429                     webClient.DownloadFileAsync(uri, fileName);
430                     doneEvent.WaitOne();
431                 }
432 
433                 lock (syncRoot)
434                 {
435                     return (int)exitCode;
436                 }
437             }
438             catch (Exception e)
439             {
440                 //
441                 // NOTE: An exception was caught.  Report it via the console
442                 //       and return failure.
443                 //
444                 Error(e.ToString(), false);
445                 return (int)ExitCode.Exception;
446             }
447         }
448         #endregion
449     }
450 }
451