// NAnt - A .NET build tool // Copyright (C) 2001-2002 Gerry Shaw // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Jay Turpin (jayturpin@hotmail.com) // Gerry Shaw (gerry_shaw@yahoo.com) using System; using System.Globalization; using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; using NAnt.Core.Attributes; using NAnt.Core.Types; using NAnt.Core.Util; namespace NAnt.Core.Tasks { /// /// Gets a particular file from a URL source. /// /// /// /// Options include verbose reporting and timestamp based fetches. /// /// /// Currently, only HTTP and UNC protocols are supported. FTP support may /// be added when more pluggable protocols are added to the System.Net /// assembly. /// /// /// The option enables you to control downloads /// so that the remote file is only fetched if newer than the local copy. /// If there is no local copy, the download always takes place. When a file /// is downloaded, the timestamp of the downloaded file is set to the remote /// timestamp. /// /// /// This timestamp facility only works on downloads using the HTTP protocol. /// /// /// /// /// Gets the index page of the NAnt home page, and stores it in the file /// help/index.html relative to the project base directory. /// /// /// /// ]]> /// /// /// /// /// Gets the index page of a secured web site using the given credentials, /// while connecting using the specified password-protected proxy server. /// /// /// /// /// /// /// /// /// ]]> /// /// [TaskName("get")] public class GetTask : Task { #region Private Instance Fields private string _src; private FileInfo _destFile; private string _httpProxy; private Proxy _proxy; private int _timeout = 100000; private bool _useTimeStamp; private Credential _credentials; private FileSet _certificates = new FileSet(); #endregion Private Instance Fields #region Public Instance Properties /// /// The URL from which to retrieve a file. /// [TaskAttribute("src", Required=true)] [StringValidator(AllowEmpty=false)] public string Source { get { return _src; } set { _src = StringUtils.ConvertEmptyToNull(value); } } /// /// The file where to store the retrieved file. /// [TaskAttribute("dest", Required=true)] public FileInfo DestinationFile { get { return _destFile; } set { _destFile = value; } } /// /// If inside a firewall, proxy server/port information /// Format: {proxy server name}:{port number} /// Example: proxy.mycompany.com:8080 /// [TaskAttribute("httpproxy")] [Obsolete("Use the child element instead.", false)] public string HttpProxy { get { return _httpProxy; } set { _httpProxy = value; } } /// /// The network proxy to use to access the Internet resource. /// [BuildElement("proxy")] public Proxy Proxy { get { return _proxy; } set { _proxy = value; } } /// /// The network credentials used for authenticating the request with /// the Internet resource. /// [BuildElement("credentials")] public Credential Credentials { get { return _credentials; } set { _credentials = value; } } /// /// Log errors but don't treat as fatal. The default is . /// [TaskAttribute("ignoreerrors")] [Obsolete("Use the 'failonerror' attribute instead.")] [BooleanValidator()] public bool IgnoreErrors { get { return FailOnError; } set { FailOnError = value; } } /// /// Conditionally download a file based on the timestamp of the local /// copy. HTTP only. The default is . /// [TaskAttribute("usetimestamp")] [BooleanValidator()] public bool UseTimeStamp { get { return _useTimeStamp; } set { _useTimeStamp = value; } } /// /// The length of time, in milliseconds, until the request times out. /// The default is 100000 milliseconds. /// [TaskAttribute("timeout")] [Int32Validator()] public int Timeout { get { return _timeout; } set { _timeout = value; } } /// /// The security certificates to associate with the request. /// [BuildElement("certificates")] public FileSet Certificates { get { return _certificates; } set { _certificates = value; } } #endregion Public Instance Properties #region Override implementation of Task /// /// Initializes task and ensures the supplied attributes are valid. /// /// Xml node used to define this task instance. protected override void InitializeTask(System.Xml.XmlNode taskNode) { if (DestinationFile.Exists && (FileAttributes.ReadOnly == (File.GetAttributes(DestinationFile.FullName) & FileAttributes.ReadOnly))) { throw new BuildException(string.Format("Destination file '{0}' is read-only.", DestinationFile.FullName), Location); } if (Proxy != null && HttpProxy != null) { throw new BuildException("The child element and the 'httpproxy' attribute are mutually exclusive.", Location); } } /// /// This is where the work is done /// protected override void ExecuteTask() { try { //set the timestamp to the file date. DateTime fileTimeStamp = new DateTime(); if (UseTimeStamp && DestinationFile.Exists) { fileTimeStamp = DestinationFile.LastWriteTime; Log(Level.Verbose, "Local file time stamp is {0}.", fileTimeStamp.ToString(CultureInfo.InvariantCulture)); } //set up the URL connection WebRequest webRequest = GetWebRequest(Source, fileTimeStamp); WebResponse webResponse = webRequest.GetResponse(); Stream responseStream = null; // Get stream // try three times, then error out int tryCount = 1; while (true) { try { responseStream = webResponse.GetResponseStream(); break; } catch (IOException ex) { if (tryCount > 3) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1125"), Source, DestinationFile.FullName), Location); } else { Log(Level.Warning, "Unable to open connection to '{0}' (try {1} of 3): " + ex.Message, Source, tryCount); } } // increment try count tryCount++; } // open file for writing BinaryWriter destWriter = new BinaryWriter(new FileStream( DestinationFile.FullName, FileMode.Create)); Log(Level.Info, "Retrieving '{0}' to '{1}'.", Source, DestinationFile.FullName); // Read in stream from URL and write data in chunks // to the dest file. int bufferSize = 100 * 1024; byte[] buffer = new byte[bufferSize]; int totalReadCount = 0; int totalBytesReadFromStream = 0; int totalBytesReadSinceLastDot = 0; do { totalReadCount = responseStream.Read(buffer, 0, bufferSize); if (totalReadCount != 0) { // zero means EOF // write buffer into file destWriter.Write(buffer, 0, totalReadCount); // increment byte counters totalBytesReadFromStream += totalReadCount; totalBytesReadSinceLastDot += totalReadCount; // display progress if (Verbose && totalBytesReadSinceLastDot > bufferSize) { if (totalBytesReadSinceLastDot == totalBytesReadFromStream) { // TO-DO !!!! //Log.Write(LogPrefix); } // TO-DO !!! //Log.Write("."); totalBytesReadSinceLastDot = 0; } } } while (totalReadCount != 0); if (totalBytesReadFromStream > bufferSize) { Log(Level.Verbose, ""); } Log(Level.Verbose, "Number of bytes read: {0}.", totalBytesReadFromStream.ToString(CultureInfo.InvariantCulture)); // clean up response streams destWriter.Close(); responseStream.Close(); // refresh file info DestinationFile.Refresh(); // check to see if we actually have a file... if(!DestinationFile.Exists) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1125"), Source, DestinationFile.FullName), Location); } // if (and only if) the use file time option is set, then the // saved file now has its timestamp set to that of the downloaded file if (UseTimeStamp) { // HTTP only if (webRequest is HttpWebRequest) { HttpWebResponse httpResponse = (HttpWebResponse) webResponse; // get timestamp of remote file DateTime remoteTimestamp = httpResponse.LastModified; Log(Level.Verbose, "'{0}' last modified on {1}.", Source, remoteTimestamp.ToString(CultureInfo.InvariantCulture)); // update timestamp of local file to match that of the // remote file TouchFile(DestinationFile, remoteTimestamp); } } } catch (BuildException) { // re-throw the exception throw; } catch (WebException ex) { // If status is WebExceptionStatus.ProtocolError, // there has been a protocol error and a WebResponse // should exist. Display the protocol error. if (ex.Status == WebExceptionStatus.ProtocolError) { // test for a 304 result (HTTP only) // Get HttpWebResponse so we can check the HTTP status code HttpWebResponse httpResponse = (HttpWebResponse) ex.Response; if (httpResponse.StatusCode == HttpStatusCode.NotModified) { //not modified so no file download. just return instead //and trace out something so the user doesn't think that the //download happened when it didn't Log(Level.Verbose, "'{0}' not downloaded. Not modified since {1}.", Source, httpResponse.LastModified.ToString(CultureInfo.InvariantCulture)); return; } else { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1125"), Source, DestinationFile.FullName), Location, ex); } } else { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1125"), Source, DestinationFile.FullName), Location, ex); } } catch (Exception ex) { throw new BuildException(string.Format(CultureInfo.InvariantCulture, ResourceUtils.GetString("NA1125"), Source, DestinationFile.FullName), Location, ex); } } #endregion Override implementation of Task #region Protected Instance Methods /// /// Sets the timestamp of a given file to a specified time. /// protected void TouchFile(FileInfo file, DateTime touchDateTime) { try { if (file.Exists) { Log(Level.Verbose, "Touching file {0} with {1}.", file.FullName, touchDateTime.ToString(CultureInfo.InvariantCulture)); file.LastWriteTime = touchDateTime; } else { throw new FileNotFoundException(); } } catch (Exception e) { // swallow any errors and move on Log(Level.Verbose, "Error: {0}.", e.ToString()); } } #endregion Protected Instance Methods #region Private Instance Methods private WebRequest GetWebRequest(string url, DateTime fileLastModified) { WebRequest webRequest = null; Uri uri = new Uri(url); // conditionally determine type of connection // if HTTP, cast to an HttpWebRequest so that IfModifiedSince can be set if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps) { HttpWebRequest httpRequest = (HttpWebRequest) WebRequest.Create(uri); //modify the headers if (!fileLastModified.Equals(new DateTime())) { // When IfModifiedSince is set, it internally converts the local time // to UTC (or, for us old farts, GMT). For all locations behind UTC // (US and Canada), this causes the IfModifiedSince time to always be // set to a time earlier than the file timestamp and force the file // to be fetched, even if it hasn't changed. The UtcOffset is used to // counter this behavior and a second is added for good measure. TimeSpan timeSpan = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now); DateTime gmtTime = fileLastModified.AddSeconds(1).Subtract(timeSpan); httpRequest.IfModifiedSince = gmtTime; //REVISIT: at this point even non HTTP connections may support the if-modified-since //behaviour -we just check the date of the content and skip the write if it is not //newer. Some protocols (FTP) dont include dates, of course. } // associate security certificates foreach (string certificate in Certificates.FileNames) { httpRequest.ClientCertificates.Add( X509Certificate.CreateFromCertFile(certificate)); } webRequest = httpRequest; } else { webRequest = WebRequest.Create(uri); } // set the number of milliseconds that the request will wait // for a response webRequest.Timeout = Timeout; // configure proxy settings if (Proxy != null) { webRequest.Proxy = Proxy.GetWebProxy(); } else if (HttpProxy != null) { webRequest.Proxy = new WebProxy(HttpProxy); } // set authentication information if (Credentials != null) { webRequest.Credentials = Credentials.GetCredential(); } return webRequest; } #endregion Private Instance Methods } }