using System; using System.IO; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Reflection; using System.Threading; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Globalization; using FluentFTP.Extensions; using System.Security.Authentication; using System.Net; using FluentFTP.Proxy; #if !CORE using System.Web; #endif namespace FluentFTP { /// /// Event is fired when a ssl certificate needs to be validated /// /// The contol connection that triggered the event /// Event args public delegate void FtpSslValidation(FtpClient control, FtpSslValidationEventArgs e); /// /// FTP Control Connection. Speaks the FTP protocol with the server and /// provides facilities for performing basic transactions. /// /// Debugging problems with FTP transactions is much easier to do when /// you can see exactly what is sent to the server and the reply /// FluentFTP gets in return. Please review the Debug example /// below for information on how to add TraceListeners for capturing /// the convorsation between FluentFTP and the server. /// /// The following example illustrates how to assist in debugging /// FluentFTP by getting a transaction log from the server. /// /// /// The following example demonstrates adding a custom file /// listing parser in the event that you encounter a list format /// not already supported. /// /// /// The following example demonstrates how to validate /// a SSL certificate when using SSL/TLS. /// /// /// The following example demonsrates how to download a file. /// /// /// The following example demonstrates how to download a file /// using a URI object. /// /// /// The following example demonstrates how to upload a file. /// /// /// The following example demonstrates how to upload a file /// using a URI object. /// /// /// The following example demonstrates how to append to a file. /// /// /// The following example demonstrates how to append to a file /// using a URI object. /// /// /// The following example demonstrates how to get a file /// listing from the server. /// /// public class FtpClient : IDisposable { #region Properties /// /// FTP µ¨¸®°ÔÀÌÆ® /// /// /// /// public delegate void DelegateFTP(string sFileName, long nDownSize, long nTotalSize); /// /// µ¨¸®°ÔÀÌÆ® ¼±¾ð º¯¼ö /// public static DelegateFTP m_delegateFTP = null; #region µ¨¸®°ÔÀÌÆ® À̺¥Æ® ÇÚµé ¼³Á¤ /// /// µ¨¸®°ÔÀÌÆ® À̺¥Æ® ÇÚµé ¼³Á¤ /// /// public static void SetEventHandle(DelegateFTP delegateFTP) { try { m_delegateFTP = delegateFTP; } catch (Exception ex) { FtpTrace.WriteLine("FtpClient.SetEventHandle(): Exception : {0}", ex.Message); } } /// /// µ¨¸®°ÔÀÌÆ® À̺¥Æ® ÇÚµé ÇØÁ¦ /// public static void ReleaseEventHandle() { try { m_delegateFTP = null; } catch (Exception ex) { FtpTrace.WriteLine("FtpClient.ReleaseEventHandle(): Exception : {0}", ex.Message); } } #endregion /// /// Used for internally syncrhonizing access to this /// object from multiple threads /// readonly Object m_lock = new Object(); /// /// For usage by FTP proxies only /// protected Object Lock { get { return m_lock; } } /// /// A list of asynchronoous methods that are in progress /// readonly Dictionary m_asyncmethods = new Dictionary(); /// /// Control connection socket stream /// FtpSocketStream m_stream = null; bool m_isDisposed = false; /// /// Gets a value indicating if this object has already been disposed. /// public bool IsDisposed { get { return m_isDisposed; } private set { m_isDisposed = value; } } /// /// Gets the base stream for talking to the server via /// the control connection. /// protected Stream BaseStream { get { return m_stream; } } FtpIpVersion m_ipVersions = FtpIpVersion.ANY; /// /// Flags specifying which versions of the internet protocol to /// support when making a connection. All addresses returned during /// name resolution are tried until a successful connection is made. /// You can fine tune which versions of the internet protocol to use /// by adding or removing flags here. I.e., setting this property /// to FtpIpVersion.IPv4 will cause the connection process to /// ignore IPv6 addresses. The default value is ANY version. /// public FtpIpVersion InternetProtocolVersions { get { return m_ipVersions; } set { m_ipVersions = value; } } int m_socketPollInterval = 15000; /// /// Gets or sets the length of time in miliseconds /// that must pass since the last socket activity /// before calling Poll() on the socket to test for /// connectivity. Setting this interval too low will /// have a negative impact on perfomance. Setting this /// interval to 0 disables Poll()'ing all together. /// The default value is 15 seconds. /// public int SocketPollInterval { get { return m_socketPollInterval; } set { m_socketPollInterval = value; if (m_stream != null) m_stream.SocketPollInterval = value; } } bool m_staleDataTest = true; /// /// Gets or sets a value indicating whether a test should be performed to /// see if there is stale (unrequested data) sitting on the socket. In some /// cases the control connection may time out but before the server closes /// the connection it might send a 4xx response that was unexpected and /// can cause synchronization errors with transactions. To avoid this /// problem the Execute() method checks to see if there is any data /// available on the socket before executing a command. On Azure hosting /// platforms this check can cause an exception to be thrown. In order /// to work around the exception you can set this property to false /// which will skip the test entirely however doing so eliminates the /// best effort attempt of detecting such scenarios. See this thread /// for more details about the Azure problem: /// https://netftp.codeplex.com/discussions/535879 /// public bool StaleDataCheck { get { return m_staleDataTest; } set { m_staleDataTest = value; } } /// /// Gets a value indicating if the connection is alive /// public bool IsConnected { get { if (m_stream != null) return m_stream.IsConnected; return false; } } bool m_threadSafeDataChannels = false; /// /// When this value is set to true (default) the control connection /// is cloned and a new connection the server is established for the /// data channel operation. This is a thread safe approach to make /// asynchronous operations on a single control connection transparent /// to the developer. /// public bool EnableThreadSafeDataConnections { get { return m_threadSafeDataChannels; } set { m_threadSafeDataChannels = value; } } bool m_isClone = false; /// /// Gets a value indicating if this control connection is a clone. This property /// is used with data streams to determine if the connection should be closed /// when the stream is closed. Servers typically only allow 1 data connection /// per control connection. If you try to open multiple data connections this /// object will be cloned for 2 or more resulting in N new connections to the /// server. /// internal bool IsClone { get { return m_isClone; } private set { m_isClone = value; } } Encoding m_textEncoding = Encoding.ASCII; bool m_textEncodingAutoUTF = true; /// /// Gets or sets the text encoding being used when talking with the server. The default /// value is Encoding.ASCII however upon connection, the client checks /// for UTF8 support and if it's there this property is switched over to /// Encoding.UTF8. Manually setting this value overrides automatic detection /// based on the FEAT list; if you change this value it's always used /// regardless of what the server advertises, if anything. /// public Encoding Encoding { get { return m_textEncoding; } set { lock (m_lock) { m_textEncoding = value; m_textEncodingAutoUTF = false; } } } string m_host = null; /// /// The server to connect to /// public string Host { get { return m_host; } set { m_host = value; } } int m_port = 0; /// /// The port to connect to. If this value is set to 0 (Default) the port used /// will be determined by the type of SSL used or if no SSL is to be used it /// will automatically connect to port 21. /// public int Port { get { // automatically determine port // when m_port is 0. if (m_port == 0) { switch (EncryptionMode) { case FtpEncryptionMode.None: case FtpEncryptionMode.Explicit: return 21; case FtpEncryptionMode.Implicit: return 990; } } return m_port; } set { m_port = value; } } NetworkCredential m_credentials = null; /// /// Credentials used for authentication /// public NetworkCredential Credentials { get { return m_credentials; } set { m_credentials = value; } } int m_maxDerefCount = 20; /// /// Gets or sets a value that controls the maximum depth /// of recursion that DereferenceLink() will follow symbolic /// links before giving up. You can also specify the value /// to be used as one of the overloaded parameters to the /// DereferenceLink() method. The default value is 20. Specifying /// -1 here means inifinitly try to resolve a link. This is /// not recommended for obvious reasons (stack overflow). /// public int MaximumDereferenceCount { get { return m_maxDerefCount; } set { m_maxDerefCount = value; } } X509CertificateCollection m_clientCerts = new X509CertificateCollection(); /// /// Client certificates to be used in SSL authentication process /// public X509CertificateCollection ClientCertificates { get { return m_clientCerts; } protected set { m_clientCerts = value; } } // Holds the cached resolved address string m_Address; Func m_AddressResolver; /// /// Delegate used for resolving local address, used for active data connections /// This can be used in case you're behind a router, but port forwarding is configured to forward the /// ports from your router to your internal IP. In that case, we need to send the router's ip instead of our internal IP. /// See example: FtpClient.GetPublicIP -> This uses Ipify api to find external IP /// public Func AddressResolver { get { return m_AddressResolver; } set { m_AddressResolver = value; } } IEnumerable m_ActivePorts; /// /// Ports used for Active Data Connection /// public IEnumerable ActivePorts { get { return m_ActivePorts; } set { m_ActivePorts = value; } } FtpDataConnectionType m_dataConnectionType = FtpDataConnectionType.AutoPassive; /// /// Data connection type, default is AutoPassive which tries /// a connection with EPSV first and if it fails then tries /// PASV before giving up. If you know exactly which kind of /// connection you need you can slightly increase performance /// by defining a speicific type of passive or active data /// connection here. /// public FtpDataConnectionType DataConnectionType { get { return m_dataConnectionType; } set { m_dataConnectionType = value; } } bool m_ungracefullDisconnect = false; /// /// Disconnect from the server without sending QUIT. This helps /// work around IOExceptions caused by buggy connection resets /// when closing the control connection. /// public bool UngracefullDisconnection { get { return m_ungracefullDisconnect; } set { m_ungracefullDisconnect = value; } } int m_connectTimeout = 15000; /// /// Gets or sets the length of time in miliseconds to wait for a connection /// attempt to succeed before giving up. Default is 15000 (15 seconds). /// public int ConnectTimeout { get { return m_connectTimeout; } set { m_connectTimeout = value; } } int m_readTimeout = 15000; /// /// Gets or sets the length of time wait in miliseconds for data to be /// read from the underlying stream. The default value is 15000 (15 seconds). /// public int ReadTimeout { get { return m_readTimeout; } set { m_readTimeout = value; } } int m_dataConnectionConnectTimeout = 15000; /// /// Gets or sets the length of time in miliseconds for a data connection /// to be established before giving up. Default is 15000 (15 seconds). /// public int DataConnectionConnectTimeout { get { return m_dataConnectionConnectTimeout; } set { m_dataConnectionConnectTimeout = value; } } int m_dataConnectionReadTimeout = 15000; /// /// Gets or sets the length of time in miliseconds the data channel /// should wait for the server to send data. Default value is /// 15000 (15 seconds). /// public int DataConnectionReadTimeout { get { return m_dataConnectionReadTimeout; } set { m_dataConnectionReadTimeout = value; } } bool m_keepAlive = false; /// /// Gets or sets a value indicating if SocketOption.KeepAlive should be set on /// the underlying stream's socket. If the connection is alive, the option is /// adjusted in real-time. The value is stored and the KeepAlive option is set /// accordingly upon any new connections. The value set here is also applied to /// all future data streams. It has no affect on cloned control connections or /// data connections already in progress. The default value is false. /// public bool SocketKeepAlive { get { return m_keepAlive; } set { m_keepAlive = value; if (m_stream != null) m_stream.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, value); } } FtpCapability m_caps = FtpCapability.NONE; /// /// Gets the server capabilties represented by flags /// public FtpCapability Capabilities { get { if (m_stream == null || !m_stream.IsConnected) { Connect(); } return m_caps; } protected set { m_caps = value; } } FtpHashAlgorithm m_hashAlgorithms = FtpHashAlgorithm.NONE; /// /// Get the hash types supported by the server, if any. This /// is a recent extension to the protocol that is not fully /// standardized and is not guarateed to work. See here for /// more details: /// http://tools.ietf.org/html/draft-bryan-ftpext-hash-02 /// public FtpHashAlgorithm HashAlgorithms { get { if (m_stream == null || !m_stream.IsConnected) { Connect(); } return m_hashAlgorithms; } private set { m_hashAlgorithms = value; } } FtpEncryptionMode m_encryptionmode = FtpEncryptionMode.None; /// /// Type of SSL to use, or none. Default is none. Explicit is TLS, Implicit is SSL. /// public FtpEncryptionMode EncryptionMode { get { return m_encryptionmode; } set { m_encryptionmode = value; } } bool m_dataConnectionEncryption = true; /// /// Indicates if data channel transfers should be encrypted. Only valid if EncryptionMode /// property is not equal to FtpSslMode.None. /// public bool DataConnectionEncryption { get { return m_dataConnectionEncryption; } set { m_dataConnectionEncryption = value; } } #if CORE private SslProtocols m_SslProtocols = SslProtocols.Tls11 | SslProtocols.Ssl3; #else private SslProtocols m_SslProtocols = SslProtocols.Default; #endif /// /// Encryption protocols to use. Only valid if EncryptionMode property is not equal to FtpSslMode.None. /// Default value is .NET Framework defaults from SslStream class. /// public SslProtocols SslProtocols { get { return m_SslProtocols; } set { m_SslProtocols = value; } } FtpSslValidation m_sslvalidate = null; /// /// Event is fired to validate SSL certificates. If this event is /// not handled and there are errors validating the certificate /// the connection will be aborted. /// /// public event FtpSslValidation ValidateCertificate { add { m_sslvalidate += value; } remove { m_sslvalidate -= value; } } /// /// Gets the type of system/server that we're /// connected to. /// public string SystemType { get { FtpReply reply = Execute("SYST"); if (reply.Success) return reply.Message; return null; } } private string m_connectionType = "Default"; /// Gets the connection type public string ConnectionType { get { return m_connectionType; } protected set { m_connectionType = value; } } int m_transferChunkSize = 65536; /// /// Gets or sets the number of bytes transfered in a single chunk (a single FTP command). /// Used by UploadFile() and DownloadFile() to transfer large files in multiple chunks. /// public int TransferChunkSize { get { return m_transferChunkSize; } set { m_transferChunkSize = value; } } private FtpDataType CurrentDataType; // ADD PROPERTIES THAT NEED TO BE CLONED INTO // FtpClient.CloneConnection() #endregion #region Core /// /// Performs a bitwise and to check if the specified /// flag is set on the Capabilities enum property. /// /// The capability to check for /// True if the feature was found public bool HasFeature(FtpCapability cap) { return ((this.Capabilities & cap) == cap); } /// /// Fires the SSL validation event /// /// Event Args void OnValidateCertficate(FtpSslValidationEventArgs e) { FtpSslValidation evt; evt = m_sslvalidate; if (evt != null) evt(this, e); } /// /// Retretieves the delegate for the specified IAsyncResult and removes /// it from the m_asyncmethods collection if the operation is successfull /// /// Type of delegate to retrieve /// The IAsyncResult to retrieve the delegate for /// The delegate that generated the specified IAsyncResult protected T GetAsyncDelegate(IAsyncResult ar) { T func; lock (m_asyncmethods) { if (m_isDisposed) { throw new ObjectDisposedException("This connection object has already been disposed."); } if (!m_asyncmethods.ContainsKey(ar)) throw new InvalidOperationException("The specified IAsyncResult could not be located."); if (!(m_asyncmethods[ar] is T)) { #if CORE throw new InvalidCastException("The AsyncResult cannot be matched to the specified delegate. "); #else StackTrace st = new StackTrace(1); throw new InvalidCastException("The AsyncResult cannot be matched to the specified delegate. " + string.Format("Are you sure you meant to call {0} and not another method?", st.GetFrame(0).GetMethod().Name) ); #endif } func = (T)m_asyncmethods[ar]; m_asyncmethods.Remove(ar); } return func; } /// /// Clones the control connection for opening multiple data streams /// /// A new control connection with the same property settings as this one /// protected FtpClient CloneConnection() { FtpClient conn = Create(); conn.m_isClone = true; // configure new connection as clone of self conn.InternetProtocolVersions = InternetProtocolVersions; conn.SocketPollInterval = SocketPollInterval; conn.StaleDataCheck = StaleDataCheck; conn.EnableThreadSafeDataConnections = EnableThreadSafeDataConnections; conn.Encoding = Encoding; conn.Host = Host; conn.Port = Port; conn.Credentials = Credentials; conn.MaximumDereferenceCount = MaximumDereferenceCount; conn.ClientCertificates = ClientCertificates; conn.DataConnectionType = DataConnectionType; conn.UngracefullDisconnection = UngracefullDisconnection; conn.ConnectTimeout = ConnectTimeout; conn.ReadTimeout = ReadTimeout; conn.DataConnectionConnectTimeout = DataConnectionConnectTimeout; conn.DataConnectionReadTimeout = DataConnectionReadTimeout; conn.SocketKeepAlive = SocketKeepAlive; conn.Capabilities = Capabilities; conn.EncryptionMode = EncryptionMode; conn.DataConnectionEncryption = DataConnectionEncryption; conn.SslProtocols = SslProtocols; conn.TransferChunkSize = TransferChunkSize; // copy props using attributes (slower, not .NET core compatible) /*foreach (PropertyInfo prop in GetType().GetProperties()) { object[] attributes = prop.GetCustomAttributes(typeof(FtpControlConnectionClone), true); if (attributes.Length > 0) { prop.SetValue(conn, prop.GetValue(this, null), null); } }*/ // always accept certificate no matter what because if code execution ever // gets here it means the certificate on the control connection object being // cloned was already accepted. conn.ValidateCertificate += new FtpSslValidation( delegate(FtpClient obj, FtpSslValidationEventArgs e) { e.Accept = true; }); return conn; } /// /// Creates a new instance of this class. Useful in FTP proxy classes. /// /// protected virtual FtpClient Create() { return new FtpClient(); } /// /// Retrieves a reply from the server. Do not execute this method /// unless you are sure that a reply has been sent, i.e., you /// executed a command. Doing so will cause the code to hang /// indefinitely waiting for a server reply that is never coming. /// /// FtpReply representing the response from the server /// protected FtpReply GetReply() { FtpReply reply = new FtpReply(); string buf; lock (m_lock) { if (!IsConnected) throw new InvalidOperationException("No connection to the server has been established."); m_stream.ReadTimeout = m_readTimeout; while ((buf = m_stream.ReadLine(Encoding)) != null) { Match m; FtpTrace.WriteLine(buf); if ((m = Regex.Match(buf, "^(?[0-9]{3}) (?.*)$")).Success) { reply.Code = m.Groups["code"].Value; reply.Message = m.Groups["message"].Value; break; } reply.InfoMessages += string.Format("{0}\n", buf); } } return reply; } /// /// Executes a command /// /// The command to execute with optional format place holders /// Format parameters to the command /// The servers reply to the command /// public FtpReply Execute(string command, params object[] args) { return Execute(string.Format(command, args)); } /// /// Executes a command /// /// The command to execute /// The servers reply to the command /// public FtpReply Execute(string command) { FtpReply reply; lock (m_lock) { if (StaleDataCheck) { if (m_stream != null && m_stream.SocketDataAvailable > 0) { // Data shouldn't be on the socket, if it is it probably // means we've been disconnected. Read and discard // whatever is there and close the connection. FtpTrace.WriteLine("There is stale data on the socket, maybe our connection timed out. Re-connecting."); if (m_stream.IsConnected && !m_stream.IsEncrypted) { byte[] buf = new byte[m_stream.SocketDataAvailable]; m_stream.RawSocketRead(buf); FtpTrace.Write("The data was: "); FtpTrace.WriteLine(Encoding.GetString(buf).TrimEnd('\r', '\n')); } m_stream.Close(); } } if (!IsConnected) { if (command == "QUIT") { FtpTrace.WriteLine("Not sending QUIT because the connection has already been closed."); return new FtpReply() { Code = "200", Message = "Connection already closed." }; } Connect(); } FtpTrace.WriteLine(command.StartsWith("PASS") ? "PASS " : command); m_stream.WriteLine(m_textEncoding, command); reply = GetReply(); } return reply; } delegate FtpReply AsyncExecute(string command); /// /// Performs an asynchronouse execution of the specified command /// /// The command to execute /// The AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginExecute(string command, AsyncCallback callback, object state) { AsyncExecute func; IAsyncResult ar; ar = (func = new AsyncExecute(Execute)).BeginInvoke(command, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous command /// /// IAsyncResult returned from BeginExecute /// FtpReply object (never null). /// public FtpReply EndExecute(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region Connection /// /// Connect to the server. Throws ObjectDisposedException if this object has been disposed. /// /// public virtual void Connect() { FtpReply reply; lock (m_lock) { if (IsDisposed) throw new ObjectDisposedException("This FtpClient object has been disposed. It is no longer accessible."); if (m_stream == null) { m_stream = new FtpSocketStream(); m_stream.ValidateCertificate += new FtpSocketStreamSslValidation(FireValidateCertficate); } else if (IsConnected) Disconnect(); if (Host == null) throw new FtpException("No host has been specified"); if (!IsClone) m_caps = FtpCapability.NONE; m_hashAlgorithms = FtpHashAlgorithm.NONE; m_stream.ConnectTimeout = m_connectTimeout; m_stream.SocketPollInterval = m_socketPollInterval; Connect(m_stream); m_stream.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, m_keepAlive); #if !NO_SSL if (EncryptionMode == FtpEncryptionMode.Implicit) m_stream.ActivateEncryption(Host, m_clientCerts.Count > 0 ? m_clientCerts : null, m_SslProtocols); #endif Handshake(); #if !NO_SSL if (EncryptionMode == FtpEncryptionMode.Explicit) { if (!(reply = Execute("AUTH TLS")).Success) throw new FtpSecurityNotAvailableException("AUTH TLS command failed."); m_stream.ActivateEncryption(Host, m_clientCerts.Count > 0 ? m_clientCerts : null, m_SslProtocols); } #endif if (m_credentials != null) { Authenticate(); } if (m_stream.IsEncrypted && DataConnectionEncryption) { if (!(reply = Execute("PBSZ 0")).Success) throw new FtpCommandException(reply); if (!(reply = Execute("PROT P")).Success) throw new FtpCommandException(reply); } // if this is a clone these values // should have already been loaded // so save some bandwidth and CPU // time and skip executing this again. if (!IsClone) { if ((reply = Execute("FEAT")).Success && reply.InfoMessages != null) { GetFeatures(reply); } } // Enable UTF8 if the encoding is ASCII and UTF8 is supported if (m_textEncodingAutoUTF && m_textEncoding == Encoding.ASCII && HasFeature(FtpCapability.UTF8)) { m_textEncoding = Encoding.UTF8; } FtpTrace.WriteLine("Text encoding: " + m_textEncoding.ToString()); if (m_textEncoding == Encoding.UTF8) { // If the server supports UTF8 it should already be enabled and this // command should not matter however there are conflicting drafts // about this so we'll just execute it to be safe. Execute("OPTS UTF8 ON"); } } } /// /// Connect to the FTP server. Overwritten in proxy classes. /// /// protected virtual void Connect(FtpSocketStream stream) { stream.Connect(Host, Port, InternetProtocolVersions); } /// /// Connect to the FTP server. Overwritten in proxy classes. /// protected virtual void Connect(FtpSocketStream stream, string host, int port, FtpIpVersion ipVersions) { stream.Connect(host, port, ipVersions); } /// FTP Handshake. protected virtual void Handshake() { FtpReply reply; if (!(reply = GetReply()).Success) { if (reply.Code == null) { throw new IOException("The connection was terminated before a greeting could be read."); } else { throw new FtpCommandException(reply); } } } /// /// Performs a login on the server. This method is overridable so /// that the login procedure can be changed to support, for example, /// a FTP proxy. /// protected virtual void Authenticate() { Authenticate(Credentials.UserName, Credentials.Password); } /// /// Performs a login on the server. This method is overridable so /// that the login procedure can be changed to support, for example, /// a FTP proxy. /// protected virtual void Authenticate(string userName, string password) { FtpReply reply; if (!(reply = Execute("USER {0}", userName)).Success) throw new FtpCommandException(reply); if (reply.Type == FtpResponseType.PositiveIntermediate && !(reply = Execute("PASS {0}", password)).Success) throw new FtpCommandException(reply); } /// /// Populates the capabilities flags based on capabilities /// supported by this server. This method is overridable /// so that new features can be supported /// /// The reply object from the FEAT command. The InfoMessages property will /// contain a list of the features the server supported delimited by a new line '\n' character. protected virtual void GetFeatures(FtpReply reply) { foreach (string feat in reply.InfoMessages.Split('\n')) { if (feat.ToUpper().Trim().StartsWith("MLST") || feat.ToUpper().Trim().StartsWith("MLSD")) m_caps |= FtpCapability.MLSD; else if (feat.ToUpper().Trim().StartsWith("MDTM")) m_caps |= FtpCapability.MDTM; else if (feat.ToUpper().Trim().StartsWith("REST STREAM")) m_caps |= FtpCapability.REST; else if (feat.ToUpper().Trim().StartsWith("SIZE")) m_caps |= FtpCapability.SIZE; else if (feat.ToUpper().Trim().StartsWith("UTF8")) m_caps |= FtpCapability.UTF8; else if (feat.ToUpper().Trim().StartsWith("PRET")) m_caps |= FtpCapability.PRET; else if (feat.ToUpper().Trim().StartsWith("MFMT")) m_caps |= FtpCapability.MFMT; else if (feat.ToUpper().Trim().StartsWith("MFCT")) m_caps |= FtpCapability.MFCT; else if (feat.ToUpper().Trim().StartsWith("MFF")) m_caps |= FtpCapability.MFF; else if (feat.ToUpper().Trim().StartsWith("MD5")) m_caps |= FtpCapability.MD5; else if (feat.ToUpper().Trim().StartsWith("XMD5")) m_caps |= FtpCapability.XMD5; else if (feat.ToUpper().Trim().StartsWith("XCRC")) m_caps |= FtpCapability.XCRC; else if (feat.ToUpper().Trim().StartsWith("XSHA1")) m_caps |= FtpCapability.XSHA1; else if (feat.ToUpper().Trim().StartsWith("XSHA256")) m_caps |= FtpCapability.XSHA256; else if (feat.ToUpper().Trim().StartsWith("XSHA512")) m_caps |= FtpCapability.XSHA512; else if (feat.ToUpper().Trim().StartsWith("HASH")) { Match m; m_caps |= FtpCapability.HASH; if ((m = Regex.Match(feat.ToUpper().Trim(), @"^HASH\s+(?.*)$")).Success) { foreach (string type in m.Groups["types"].Value.Split(';')) { switch (type.ToUpper().Trim()) { case "SHA-1": case "SHA-1*": m_hashAlgorithms |= FtpHashAlgorithm.SHA1; break; case "SHA-256": case "SHA-256*": m_hashAlgorithms |= FtpHashAlgorithm.SHA256; break; case "SHA-512": case "SHA-512*": m_hashAlgorithms |= FtpHashAlgorithm.SHA512; break; case "MD5": case "MD5*": m_hashAlgorithms |= FtpHashAlgorithm.MD5; break; case "CRC": case "CRC*": m_hashAlgorithms |= FtpHashAlgorithm.CRC; break; } } } } } } delegate void AsyncConnect(); /// /// Initiates a connection to the server /// /// AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginConnect(AsyncCallback callback, object state) { AsyncConnect func; IAsyncResult ar; ar = (func = new AsyncConnect(Connect)).BeginInvoke(callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous connection attempt to the server /// /// IAsyncResult returned from BeginConnect() /// public void EndConnect(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Catches the socket stream ssl validation event and fires the event handlers /// attached to this object for validating SSL certificates /// /// The stream that fired the event /// The event args used to validate the certficate void FireValidateCertficate(FtpSocketStream stream, FtpSslValidationEventArgs e) { OnValidateCertficate(e); } /// /// Disconnect from the server /// public virtual void Disconnect() { lock (m_lock) { if (m_stream != null && m_stream.IsConnected) { try { if (!UngracefullDisconnection) { Execute("QUIT"); } } catch (SocketException sockex) { FtpTrace.WriteLine("FtpClient.Disconnect(): SocketException caught and discarded while closing control connection: {0}", sockex.ToString()); } catch (IOException ioex) { FtpTrace.WriteLine("FtpClient.Disconnect(): IOException caught and discarded while closing control connection: {0}", ioex.ToString()); } catch (FtpCommandException cmdex) { FtpTrace.WriteLine("FtpClient.Disconnect(): FtpCommandException caught and discarded while closing control connection: {0}", cmdex.ToString()); } catch (FtpException ftpex) { FtpTrace.WriteLine("FtpClient.Disconnect(): FtpException caught and discarded while closing control connection: {0}", ftpex.ToString()); } finally { m_stream.Close(); } } } } delegate void AsyncDisconnect(); /// /// Initiates a disconnection on the server /// /// AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginDisconnect(AsyncCallback callback, object state) { IAsyncResult ar; AsyncDisconnect func; ar = (func = new AsyncDisconnect(Disconnect)).BeginInvoke(callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginDisconnect /// /// IAsyncResult returned from BeginDisconnect /// public void EndDisconnect(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region File I/O /// /// Opens the specified type of passive data stream /// /// Type of passive data stream to open /// The command to execute that requires a data stream /// Restart location in bytes for file transfer /// A data stream ready to be used FtpDataStream OpenPassiveDataStream(FtpDataConnectionType type, string command, long restart) { FtpDataStream stream = null; FtpReply reply; Match m; string host = null; int port = 0; if (m_stream == null) throw new InvalidOperationException("The control connection stream is null! Generally this means there is no connection to the server. Cannot open a passive data stream."); if (type == FtpDataConnectionType.EPSV || type == FtpDataConnectionType.AutoPassive) { if (!(reply = Execute("EPSV")).Success) { // if we're connected with IPv4 and data channel type is AutoPassive then fallback to IPv4 if (reply.Type == FtpResponseType.PermanentNegativeCompletion && type == FtpDataConnectionType.AutoPassive && m_stream != null && m_stream.LocalEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) return OpenPassiveDataStream(FtpDataConnectionType.PASV, command, restart); throw new FtpCommandException(reply); } m = Regex.Match(reply.Message, @"\(\|\|\|(?\d+)\|\)"); if (!m.Success) { throw new FtpException("Failed to get the EPSV port from: " + reply.Message); } host = m_host; port = int.Parse(m.Groups["port"].Value); } else { if (m_stream.LocalEndPoint.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) throw new FtpException("Only IPv4 is supported by the PASV command. Use EPSV instead."); if (!(reply = Execute("PASV")).Success) throw new FtpCommandException(reply); m = Regex.Match(reply.Message, @"(?\d+)," + @"(?\d+)," + @"(?\d+)," + @"(?\d+)," + @"(?\d+)," + @"(?\d+)"); if (!m.Success || m.Groups.Count != 7) throw new FtpException(string.Format("Malformed PASV response: {0}", reply.Message)); // PASVEX mode ignores the host supplied in the PASV response if (type == FtpDataConnectionType.PASVEX) host = m_host; else host = string.Format("{0}.{1}.{2}.{3}", m.Groups["quad1"].Value, m.Groups["quad2"].Value, m.Groups["quad3"].Value, m.Groups["quad4"].Value); port = (int.Parse(m.Groups["port1"].Value) << 8) + int.Parse(m.Groups["port2"].Value); } stream = new FtpDataStream(this); stream.ConnectTimeout = DataConnectionConnectTimeout; stream.ReadTimeout = DataConnectionReadTimeout; Connect(stream, host, port, InternetProtocolVersions); stream.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, m_keepAlive); if (restart > 0) { if (!(reply = Execute("REST {0}", restart)).Success) throw new FtpCommandException(reply); } if (!(reply = Execute(command)).Success) { stream.Close(); throw new FtpCommandException(reply); } // the command status is used to determine // if a reply needs to be read from the server // when the stream is closed so always set it // otherwise things can get out of sync. stream.CommandStatus = reply; #if !NO_SSL // this needs to take place after the command is executed if (m_dataConnectionEncryption && m_encryptionmode != FtpEncryptionMode.None) stream.ActivateEncryption(m_host, this.ClientCertificates.Count > 0 ? this.ClientCertificates : null, m_SslProtocols); #endif return stream; } /// /// Returns the ip address to be sent to the server for the active connection /// /// /// string GetLocalAddress(IPAddress ip) { // Use resolver if (m_AddressResolver != null) { return m_Address ?? (m_Address = m_AddressResolver()); } // Use supplied ip return ip.ToString(); } /// /// Opens the specified type of active data stream /// /// Type of passive data stream to open /// The command to execute that requires a data stream /// Restart location in bytes for file transfer /// A data stream ready to be used FtpDataStream OpenActiveDataStream(FtpDataConnectionType type, string command, long restart) { FtpDataStream stream = new FtpDataStream(this); FtpReply reply; #if !CORE IAsyncResult ar; #endif if (m_stream == null) throw new InvalidOperationException("The control connection stream is null! Generally this means there is no connection to the server. Cannot open an active data stream."); if (m_ActivePorts == null || !m_ActivePorts.Any()) { // Use random port stream.Listen(m_stream.LocalEndPoint.Address, 0); } else { var success = false; // Use one of the specified ports foreach (var port in m_ActivePorts) { try { stream.Listen(m_stream.LocalEndPoint.Address, port); success = true; } catch (SocketException se) { #if NETFX // Already in use if (se.ErrorCode != 10048) throw; #else throw; #endif } } // No usable port found if (!success) throw new Exception("No valid active data port available!"); } #if !CORE ar = stream.BeginAccept(null, null); #endif if (type == FtpDataConnectionType.EPRT || type == FtpDataConnectionType.AutoActive) { int ipver = 0; switch (stream.LocalEndPoint.AddressFamily) { case System.Net.Sockets.AddressFamily.InterNetwork: ipver = 1; // IPv4 break; case System.Net.Sockets.AddressFamily.InterNetworkV6: ipver = 2; // IPv6 break; default: throw new InvalidOperationException("The IP protocol being used is not supported."); } if (!(reply = Execute("EPRT |{0}|{1}|{2}|", ipver, GetLocalAddress(stream.LocalEndPoint.Address), stream.LocalEndPoint.Port)).Success) { // if we're connected with IPv4 and the data channel type is AutoActive then try to fall back to the PORT command if (reply.Type == FtpResponseType.PermanentNegativeCompletion && type == FtpDataConnectionType.AutoActive && m_stream != null && m_stream.LocalEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) { stream.ControlConnection = null; // we don't want this failed EPRT attempt to close our control connection when the stream is closed so clear out the reference. stream.Close(); return OpenActiveDataStream(FtpDataConnectionType.PORT, command, restart); } else { stream.Close(); throw new FtpCommandException(reply); } } } else { if (m_stream.LocalEndPoint.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) throw new FtpException("Only IPv4 is supported by the PORT command. Use EPRT instead."); if (!(reply = Execute("PORT {0},{1},{2}", GetLocalAddress(stream.LocalEndPoint.Address).Replace('.', ','), stream.LocalEndPoint.Port / 256, stream.LocalEndPoint.Port % 256)).Success) { stream.Close(); throw new FtpCommandException(reply); } } if (restart > 0) { if (!(reply = Execute("REST {0}", restart)).Success) throw new FtpCommandException(reply); } if (!(reply = Execute(command)).Success) { stream.Close(); throw new FtpCommandException(reply); } // the command status is used to determine // if a reply needs to be read from the server // when the stream is closed so always set it // otherwise things can get out of sync. stream.CommandStatus = reply; #if CORE stream.AcceptAsync().Wait(); #else ar.AsyncWaitHandle.WaitOne(m_dataConnectionConnectTimeout); if (!ar.IsCompleted) { stream.Close(); throw new TimeoutException("Timed out waiting for the server to connect to the active data socket."); } stream.EndAccept(ar); #endif #if !NO_SSL if (m_dataConnectionEncryption && m_encryptionmode != FtpEncryptionMode.None) stream.ActivateEncryption(m_host, this.ClientCertificates.Count > 0 ? this.ClientCertificates : null, m_SslProtocols); #endif stream.SetSocketOption(System.Net.Sockets.SocketOptionLevel.Socket, System.Net.Sockets.SocketOptionName.KeepAlive, m_keepAlive); stream.ReadTimeout = m_dataConnectionReadTimeout; return stream; } /// /// Opens a data stream. /// /// The command to execute that requires a data stream /// Restart location in bytes for file transfer /// The data stream. FtpDataStream OpenDataStream(string command, long restart) { FtpDataConnectionType type = m_dataConnectionType; FtpDataStream stream = null; lock (m_lock) { if (!IsConnected) Connect(); // The PORT and PASV commands do not work with IPv6 so // if either one of those types are set change them // to EPSV or EPRT appropriately. if (m_stream.LocalEndPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) { switch (type) { case FtpDataConnectionType.PORT: type = FtpDataConnectionType.EPRT; FtpTrace.WriteLine("Changed data connection type to EPRT because we are connected with IPv6."); break; case FtpDataConnectionType.PASV: case FtpDataConnectionType.PASVEX: type = FtpDataConnectionType.EPSV; FtpTrace.WriteLine("Changed data connection type to EPSV because we are connected with IPv6."); break; } } switch (type) { case FtpDataConnectionType.AutoPassive: case FtpDataConnectionType.EPSV: case FtpDataConnectionType.PASV: case FtpDataConnectionType.PASVEX: stream = OpenPassiveDataStream(type, command, restart); break; case FtpDataConnectionType.AutoActive: case FtpDataConnectionType.EPRT: case FtpDataConnectionType.PORT: stream = OpenActiveDataStream(type, command, restart); break; } if (stream == null) throw new InvalidOperationException("The specified data channel type is not implemented."); } return stream; } /// /// Disconnects a data stream /// /// The data stream to close internal FtpReply CloseDataStream(FtpDataStream stream) { FtpReply reply = new FtpReply(); if (stream == null) throw new ArgumentException("The data stream parameter was null"); lock (m_lock) { try { if (IsConnected) { // if the command that required the data connection was // not successful then there will be no reply from // the server, however if the command was successful // the server will send a reply when the data connection // is closed. if (stream.CommandStatus.Type == FtpResponseType.PositivePreliminary) { if (!(reply = GetReply()).Success) { throw new FtpCommandException(reply); } } } } finally { // if this is a clone of the original control // connection we should Dispose() if (IsClone) { Disconnect(); Dispose(); } } } return reply; } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// A stream for reading the file on the server /// public Stream OpenRead(string path) { return OpenRead(path, FtpDataType.Binary, 0); } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// ASCII/Binary /// A stream for reading the file on the server /// public Stream OpenRead(string path, FtpDataType type) { return OpenRead(path, type, 0); } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// Resume location /// A stream for reading the file on the server /// public Stream OpenRead(string path, long restart) { return OpenRead(path, FtpDataType.Binary, restart); } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// ASCII/Binary /// Resume location /// A stream for reading the file on the server /// public virtual Stream OpenRead(string path, FtpDataType type, long restart) { FtpClient client = null; FtpDataStream stream = null; long length = 0; lock (m_lock) { if (m_threadSafeDataChannels) { client = CloneConnection(); client.Connect(); client.SetWorkingDirectory(GetWorkingDirectory()); } else { client = this; } client.SetDataType(type); length = client.GetFileSize(path); stream = client.OpenDataStream(string.Format("RETR {0}", path.GetFtpPath()), restart); } if (stream != null) { if (length > 0) stream.SetLength(length); if (restart > 0) stream.SetPosition(restart); } return stream; } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenRead(string path, AsyncCallback callback, object state) { return BeginOpenRead(path, FtpDataType.Binary, 0, callback, state); } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// ASCII/Binary /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenRead(string path, FtpDataType type, AsyncCallback callback, object state) { return BeginOpenRead(path, type, 0, callback, state); } /// /// Opens the specified file for reading /// /// The full or relative path of the file /// Resume location /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenRead(string path, long restart, AsyncCallback callback, object state) { return BeginOpenRead(path, FtpDataType.Binary, restart, callback, state); } delegate Stream AsyncOpenRead(string path, FtpDataType type, long restart); /// /// Opens the specified file for reading /// /// The full or relative path of the file /// ASCII/Binary /// Resume location /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenRead(string path, FtpDataType type, long restart, AsyncCallback callback, object state) { AsyncOpenRead func; IAsyncResult ar; ar = (func = new AsyncOpenRead(OpenRead)).BeginInvoke(path, type, restart, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginOpenRead() /// /// IAsyncResult returned from BeginOpenRead() /// A readable stream /// public Stream EndOpenRead(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Opens the specified file for writing /// /// Full or relative path of the file /// A stream for writing to the file on the server /// public Stream OpenWrite(string path) { return OpenWrite(path, FtpDataType.Binary); } /// /// Opens the specified file for writing /// /// Full or relative path of the file /// ASCII/Binary /// A stream for writing to the file on the server /// public virtual Stream OpenWrite(string path, FtpDataType type) { FtpClient client = null; FtpDataStream stream = null; long length = 0; lock (m_lock) { if (m_threadSafeDataChannels) { client = CloneConnection(); client.Connect(); client.SetWorkingDirectory(GetWorkingDirectory()); } else { client = this; } client.SetDataType(type); length = client.GetFileSize(path); stream = client.OpenDataStream(string.Format("STOR {0}", path.GetFtpPath()), 0); if (length > 0 && stream != null) stream.SetLength(length); } return stream; } /// /// Opens the specified file for writing /// /// Full or relative path of the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenWrite(string path, AsyncCallback callback, object state) { return BeginOpenWrite(path, FtpDataType.Binary, callback, state); } delegate Stream AsyncOpenWrite(string path, FtpDataType type); /// /// Opens the specified file for writing /// /// Full or relative path of the file /// ASCII/Binary /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenWrite(string path, FtpDataType type, AsyncCallback callback, object state) { AsyncOpenWrite func; IAsyncResult ar; ar = (func = new AsyncOpenWrite(OpenWrite)).BeginInvoke(path, type, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginOpenWrite() /// /// IAsyncResult returned from BeginOpenWrite() /// A writable stream /// public Stream EndOpenWrite(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Opens the specified file to be appended to /// /// The full or relative path to the file to be opened /// A stream for writing to the file on the server /// public Stream OpenAppend(string path) { return OpenAppend(path, FtpDataType.Binary); } /// /// Opens the specified file to be appended to /// /// The full or relative path to the file to be opened /// ASCII/Binary /// A stream for writing to the file on the server /// public virtual Stream OpenAppend(string path, FtpDataType type) { FtpClient client = null; FtpDataStream stream = null; long length = 0; lock (m_lock) { if (m_threadSafeDataChannels) { client = CloneConnection(); client.Connect(); client.SetWorkingDirectory(GetWorkingDirectory()); } else { client = this; } client.SetDataType(type); length = client.GetFileSize(path); stream = client.OpenDataStream(string.Format("APPE {0}", path.GetFtpPath()), 0); if (length > 0 && stream != null) { stream.SetLength(length); stream.SetPosition(length); } } return stream; } /// /// Opens the specified file for writing /// /// Full or relative path of the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenAppend(string path, AsyncCallback callback, object state) { return BeginOpenAppend(path, FtpDataType.Binary, callback, state); } delegate Stream AsyncOpenAppend(string path, FtpDataType type); /// /// Opens the specified file for writing /// /// Full or relative path of the file /// ASCII/Binary /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginOpenAppend(string path, FtpDataType type, AsyncCallback callback, object state) { IAsyncResult ar; AsyncOpenAppend func; ar = (func = new AsyncOpenAppend(OpenAppend)).BeginInvoke(path, type, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginOpenAppend() /// /// IAsyncResult returned from BeginOpenWrite() /// A writable stream /// public Stream EndOpenAppend(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region Multi File Upload/Download /// /// Uploads the given file paths to a single folder on the server. /// All files are placed directly into the given folder regardless of their path on the local filesystem. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it uploads data in chunks. /// Faster than uploading single files with UploadFile() since it performs a single "file exists" check rather than one check per file. /// /// The full or relative paths to the files on the local file system. Files can be from multiple folders. /// The full or relative path to the directory that files will be uploaded on the server /// Overwrite the file if it already exists? /// Create the remote directory if it does not exist. /// The count of how many files were uploaded successfully. Affected when files are skipped when they already exist. public int UploadFiles(string[] localPaths, string remoteDir, bool overwrite = true, bool createRemoteDir = true) { int count = 0; // ensure ends with slash remoteDir = !remoteDir.EndsWith("/") ? remoteDir + "/" : remoteDir; // create remote dir if wanted if (createRemoteDir) { if (!DirectoryExists(remoteDir)) { CreateDirectory(remoteDir); } } // get all the already existing files string[] existingFiles = GetNameListing(remoteDir); // per local file foreach (string localPath in localPaths) { // calc remote path string fileExt = Path.GetFileName(localPath); string remotePath = remoteDir + fileExt; // check if the remote file exists (always) if (existingFiles.Contains(fileExt)) { // skip uploading if the remote file exists if (!overwrite) { continue; } // delete file before uploading (also fixes #46) DeleteFile(remotePath); } // try to upload it try { bool ok = UploadFileFromFile(localPath, remotePath, false); if (ok) { count++; } } catch (Exception ex) { } } return count; } /// /// Uploads the given file paths to a single folder on the server. /// All files are placed directly into the given folder regardless of their path on the local filesystem. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it uploads data in chunks. /// Faster than uploading single files with UploadFile() since it performs a single "file exists" check rather than one check per file. /// /// The full or relative paths to the files on the local file system. Files can be from multiple folders. /// The full or relative path to the directory that files will be uploaded on the server /// Overwrite the file if it already exists? /// Create the remote directory if it does not exist. /// The count of how many files were downloaded successfully. When existing files are skipped, they are not counted. public int UploadFiles(List localPaths, string remoteDir, bool overwrite = true, bool createRemoteDir = true) { return UploadFiles(localPaths.ToArray(), remoteDir, overwrite, createRemoteDir); } /// /// Downloads the specified files into a local single directory. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it downloads data in chunks. /// Same speed as DownloadFile(). /// /// The full or relative path to the directory that files will be downloaded into. /// The full or relative paths to the files on the server /// Overwrite the file if it already exists? /// The count of how many files were downloaded successfully. When existing files are skipped, they are not counted. public int DownloadFiles(string localDir, string[] remotePaths, bool overwrite = true) { int count = 0; // ensure ends with slash localDir = !localDir.EndsWith(Path.DirectorySeparatorChar.ToString()) ? localDir + Path.DirectorySeparatorChar.ToString() : localDir; foreach (string remotePath in remotePaths) { // calc local path string localPath = localDir + remotePath.GetFtpFileName(); // try to download it try { bool ok = DownloadFileToFile(localPath, remotePath, overwrite); if (ok) { count++; } } catch (Exception ex) { } } return count; } /// /// Downloads the specified files into a local single directory. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it downloads data in chunks. /// Same speed as DownloadFile(). /// /// The full or relative path to the directory that files will be downloaded into. /// The full or relative paths to the files on the server /// Overwrite the file if it already exists? /// The count of how many files were downloaded successfully. When existing files are skipped, they are not counted. public int DownloadFiles(string localDir, List remotePaths, bool overwrite = true) { return DownloadFiles(localDir, remotePaths.ToArray(), overwrite); } #endregion #region File Upload/Download /// /// Uploads the specified file directly onto the server. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it uploads data in chunks. /// /// The full or relative path to the file on the local file system /// The full or relative path to the file on the server /// Overwrite the file if it already exists? /// Create the remote directory if it does not exist. Slows down upload due to additional checks required. /// Check if the file exists before uploaded. Keep it on if you might have partially uploaded files on the server. Turn this off to increase performance. /// If true then the file was uploaded, false otherwise. public bool UploadFile(string localPath, string remotePath, bool overwrite = true, bool createRemoteDir = false, bool checkFileExistance = true) { // skip uploading if the local file does not exist if (!File.Exists(localPath)) { return false; } // check if the remote file exists (always) if (checkFileExistance && FileExists(remotePath)) { // skip uploading if the remote file exists if (!overwrite) { return false; } // delete file before uploading (also fixes #46) DeleteFile(remotePath); } return UploadFileFromFile(localPath, remotePath, createRemoteDir); } private bool UploadFileFromFile(string localPath, string remotePath, bool createRemoteDir) { FileStream fileStream; try { // connect to the file fileStream = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read); } catch (Exception ex1) { // catch errors during upload throw new FtpException("Error while reading the file from the disk. See InnerException for more info.", ex1); } // write the file onto the server bool ok = UploadFileInternal(fileStream, remotePath, createRemoteDir); // close the file stream try { fileStream.Dispose(); } catch (Exception) { } return ok; } /// /// Uploads the specified byte array as a file onto the server. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it uploads data in chunks. /// /// The full data of the file, as a bytearray /// The full or relative path to the file on the server /// Overwrite the file if it already exists? /// Create the remote directory if it does not exist. Slows down upload due to additional checks required. /// Check if the file exists before uploaded. Keep it on if you might have partially uploaded files on the server. Turn this off to increase performance. /// If true then the file was uploaded, false otherwise. public bool UploadFile(byte[] fileData, string remotePath, bool overwrite = true, bool createRemoteDir = false, bool checkFileExistance = true) { // check if the remote file exists (always) if (checkFileExistance && FileExists(remotePath)) { // skip uploading if the remote file exists if (!overwrite) { return false; } // delete file before uploading (also fixes #46) DeleteFile(remotePath); } // write the file onto the server MemoryStream ms = new MemoryStream(fileData); ms.Position = 0; bool ok = UploadFileInternal(ms, remotePath, createRemoteDir); ms.Dispose(); return ok; } /// /// Uploads the specified stream as a file onto the server. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it uploads data in chunks. /// /// The full data of the file, as a stream /// The full or relative path to the file on the server /// Overwrite the file if it already exists? /// Create the remote directory if it does not exist. Slows down upload due to additional checks required. /// Check if the file exists before uploaded. Keep it on if you might have partially uploaded files on the server. Turn this off to increase performance. /// If true then the file was uploaded, false otherwise. public bool UploadFile(Stream fileStream, string remotePath, bool overwrite = true, bool createRemoteDir = false, bool checkFileExistance = true) { // check if the remote file exists (always) if (checkFileExistance && FileExists(remotePath)) { // skip uploading if the remote file exists if (!overwrite) { return false; } // delete file before uploading (also fixes #46) DeleteFile(remotePath); } // write the file onto the server return UploadFileInternal(fileStream, remotePath, createRemoteDir); } /// /// Downloads the specified file onto the local file system. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it downloads data in chunks. /// /// The full or relative path to the file on the local file system /// The full or relative path to the file on the server /// Overwrite the file if it already exists? /// If true then the file was downloaded, false otherwise. public bool DownloadFile(string localPath, string remotePath, bool overwrite = true) { return DownloadFileToFile(localPath, remotePath, overwrite); } private bool DownloadFileToFile(string localPath, string remotePath, bool overwrite) { // skip downloading if the local file exists if (!overwrite && File.Exists(localPath)) { return false; } FileStream outStream; try { // create the folders string dirPath = Path.GetDirectoryName(localPath); if (!Directory.Exists(dirPath)) { Directory.CreateDirectory(dirPath); } // connect to the file outStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None); } catch (Exception ex1) { // catch errors during upload throw new FtpException("Error while saving the file to disk. See InnerException for more info.", ex1); } // download the file straight to a file stream bool ok = DownloadFileInternal(remotePath, outStream); // close the file stream try { outStream.Dispose(); } catch (Exception) { } return ok; } /// /// Downloads the specified file into a new MemoryStream. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it downloads data in chunks. /// /// The stream that the file will be written to. Provide a new MemoryStream if you only want to read the file into memory. /// The full or relative path to the file on the server /// If true then the file was downloaded, false otherwise. public bool DownloadFile(Stream outStream, string remotePath) { // download the file from the server return DownloadFileInternal(remotePath, outStream); } /// /// Downloads the specified file and return the raw byte array. /// High-level API that takes care of various edge cases internally. /// Supports very large files since it downloads data in chunks. /// /// The variable that will recieve the bytes. /// The full or relative path to the file on the server /// If true then the file was downloaded, false otherwise. public bool DownloadFile(out byte[] outBytes, string remotePath) { outBytes = null; // download the file from the server MemoryStream outStream = new MemoryStream(); bool ok = DownloadFileInternal(remotePath, outStream); if (ok) { outBytes = outStream.ToArray(); outStream.Dispose(); } return ok; } /// /// Upload the given stream to the server as a new file. Overwrites the file if it exists. /// Writes data in chunks. Retries if server disconnects midway. /// private bool UploadFileInternal(Stream fileData, string remotePath, bool createRemoteDir) { Stream upStream = null; try { // ensure the remote dir exists if (createRemoteDir) { string dirname = remotePath.GetFtpDirectoryName(); if (!DirectoryExists(dirname)) { CreateDirectory(dirname); } } // open a file write connection upStream = OpenWrite(remotePath); // loop till entire file uploaded long offset = 0; long len = fileData.Length; byte[] buffer = new byte[TransferChunkSize]; while (offset < len) { try { // read a chunk of bytes from the file int readBytes; while ((readBytes = fileData.Read(buffer, 0, buffer.Length)) > 0) { // write chunk to the FTP stream upStream.Write(buffer, 0, readBytes); upStream.Flush(); offset += readBytes; } } catch (IOException ex) { // resume if server disconnects midway (fixes #39) if (ex.InnerException != null) { var iex = ex.InnerException as System.Net.Sockets.SocketException; #if CORE int code = (int)iex.SocketErrorCode; #else int code = iex.ErrorCode; #endif if (iex != null && code == 10054) { upStream.Dispose(); upStream = OpenAppend(remotePath); upStream.Position = offset; } else throw; } else throw; } } // wait for while transfer to get over while (upStream.Position < upStream.Length) { } // disconnect FTP stream before exiting upStream.Dispose(); // FIX : if this is not added, there appears to be "stale data" on the socket // listen for a success/failure reply if (!m_threadSafeDataChannels) { FtpReply status = GetReply(); } // fixes #30. the file can have some bytes at the end missing // on proxy servers, so we write those missing bytes now. if (IsProxy()) { // get the current length of the remote file // and check if any bytes are missing long curLen = GetFileSize(remotePath); if (curLen > 0 && curLen < len && fileData.CanSeek) { // seek to the point of the local file offset = curLen; fileData.Position = offset; // open the remote file for appending upStream = OpenAppend(remotePath, FtpDataType.Binary); upStream.Position = offset; // read a chunk of bytes from the file int readBytes; while ((readBytes = fileData.Read(buffer, 0, buffer.Length)) > 0) { // write chunk to the FTP stream upStream.Write(buffer, 0, readBytes); upStream.Flush(); offset += readBytes; } // disconnect FTP stream before exiting upStream.Dispose(); // FIX : if this is not added, there appears to be "stale data" on the socket // listen for a success/failure reply if (!m_threadSafeDataChannels) { FtpReply status = GetReply(); } } } return true; } catch (Exception ex1) { // close stream before throwing error try { upStream.Dispose(); } catch (Exception) { } // catch errors during upload throw new FtpException("Error while uploading the file to the server. See InnerException for more info.", ex1); } } /// /// Download a file from the server and write the data into the given stream. /// Reads data in chunks. Retries if server disconnects midway. /// private bool DownloadFileInternal(string remotePath, Stream outStream) { Stream downStream = null; try { // exit if file length not available downStream = OpenRead(remotePath); long fileLen = downStream.Length; if (fileLen == 0 && CurrentDataType == FtpDataType.ASCII) { // close stream before throwing error try { downStream.Dispose(); } catch (Exception) { } throw new FtpException("Cannot download file since file has length of 0. Use the FtpDataType.Binary data type and try again."); } // if the server has not reported a length for this file // we use an alternate method to download it - read until EOF bool readToEnd = (fileLen == 0); if (m_delegateFTP != null) m_delegateFTP(remotePath, 0, 0); // loop till entire file downloaded byte[] buffer = new byte[TransferChunkSize]; long offset = 0; while (offset < fileLen || readToEnd) { try { // read a chunk of bytes from the FTP stream int readBytes = 1; while ((readBytes = downStream.Read(buffer, 0, buffer.Length)) > 0) { // write chunk to output stream outStream.Write(buffer, 0, readBytes); offset += readBytes; if (m_delegateFTP != null) m_delegateFTP(remotePath, outStream.Length, fileLen); } // if we reach here means EOF encountered // stop if we are in "read until EOF" mode if (readToEnd) { break; } } catch (IOException ex) { // resume if server disconnects midway (fixes #39) if (ex.InnerException != null) { var ie = ex.InnerException as System.Net.Sockets.SocketException; #if CORE int code = (int)ie.SocketErrorCode; #else int code = ie.ErrorCode; #endif if (ie != null && code == 10054) { downStream.Dispose(); downStream = OpenRead(remotePath, restart: offset); } else throw; } else throw; } } // disconnect FTP stream before exiting outStream.Flush(); downStream.Dispose(); // FIX : if this is not added, there appears to be "stale data" on the socket // listen for a success/failure reply if (!m_threadSafeDataChannels) { FtpReply status = GetReply(); } return true; } catch (Exception ex1) { // close stream before throwing error try { downStream.Dispose(); } catch (Exception) { } // absorb "file does not exist" exceptions and simply return false if (ex1.Message.Contains("No such file") || ex1.Message.Contains("not exist") || ex1.Message.Contains("missing file") || ex1.Message.Contains("unknown file")) { return false; } // catch errors during upload throw new FtpException("Error while downloading the file from the server. See InnerException for more info.", ex1); } } #endregion #region File Management /// /// Deletes a file on the server /// /// The full or relative path to the file /// public void DeleteFile(string path) { FtpReply reply; lock (m_lock) { if (!(reply = Execute("DELE {0}", path.GetFtpPath())).Success) throw new FtpCommandException(reply); } } delegate void AsyncDeleteFile(string path); /// /// Asynchronously deletes a file from the server /// /// The full or relative path to the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginDeleteFile(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncDeleteFile func; ar = (func = new AsyncDeleteFile(DeleteFile)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginDeleteFile /// /// IAsyncResult returned from BeginDeleteFile /// public void EndDeleteFile(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Deletes the specified directory on the server. /// /// The full or relative path of the directory to delete /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// public void DeleteDirectory(string path, bool fastMode = false) { DeleteDirectory(path, false, 0, fastMode); } /// /// Delets the specified directory on the server /// /// The full or relative path of the directory to delete /// If the directory is not empty, remove its contents /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// public void DeleteDirectory(string path, bool force, bool fastMode = false) { DeleteDirectory(path, force, 0, fastMode); } /// /// Deletes the specified directory on the server /// /// The full or relative path of the directory to delete /// If the directory is not empty, remove its contents /// FtpListOptions for controlling how the directory /// contents are retrieved with the force option is true. If you experience problems /// the file listing can be fine tuned through this parameter. /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// public void DeleteDirectory(string path, bool force, FtpListOption options, bool fastMode = false) { FtpReply reply; string ftppath = path.GetFtpPath(); lock (m_lock) { // DELETE CONTENTS OF THE DIRECTORY if (force) { // experimental fast mode if (fastMode) { // when GetListing is called with recursive option, then it does not // make any sense to call another DeleteDirectory with force flag set. // however this requires always delete files first. var forceAgain = !WasGetListingRecursive(options); // items, that are deeper in directory tree, are listed first, // then files will be listed before directories. This matters // only if GetListing was called with recursive option. FtpListItem[] itemList; if (forceAgain) itemList = GetListing(path, options); else itemList = GetListing(path, options).OrderByDescending(x => x.FullName.Count(c => c.Equals('/'))).ThenBy(x => x.Type).ToArray(); foreach (FtpListItem item in itemList) { switch (item.Type) { case FtpFileSystemObjectType.File: DeleteFile(item.FullName); break; case FtpFileSystemObjectType.Directory: DeleteDirectory(item.FullName, forceAgain, options, fastMode); break; default: throw new FtpException("Don't know how to delete object type: " + item.Type); } } } else { // standard mode foreach (FtpListItem item in GetListing(path, options)) { // This check prevents infinity recursion, // when FtpListItem is actual parent or current directory. // This could happen only when MLSD command is used for GetListing method. if (!item.FullName.ToLower().Contains(path.ToLower()) || string.Equals(item.FullName.ToLower(), path.ToLower())) continue; switch (item.Type) { case FtpFileSystemObjectType.File: DeleteFile(item.FullName); break; case FtpFileSystemObjectType.Directory: DeleteDirectory(item.FullName, true, options, fastMode); break; default: throw new FtpException("Don't know how to delete object type: " + item.Type); } } } } // SKIP DELETING ROOT DIRS // can't delete the working directory and // can't delete the server root. if (ftppath == "." || ftppath == "./" || ftppath == "/") { return; } // DELETE ACTUAL DIRECTORY if (!(reply = Execute("RMD {0}", ftppath)).Success) { throw new FtpCommandException(reply); } } } /// /// Checks whether GetListing will be called recursively or not. /// /// /// private bool WasGetListingRecursive(FtpListOption options) { if (HasFeature(FtpCapability.MLSD) && (options & FtpListOption.ForceList) != FtpListOption.ForceList) return false; if ((options & FtpListOption.UseLS) == FtpListOption.UseLS || (options & FtpListOption.NameList) == FtpListOption.NameList) return false; if ((options & FtpListOption.Recursive) == FtpListOption.Recursive) return true; return false; } delegate void AsyncDeleteDirectory(string path, bool force, FtpListOption options, bool fastMode = false); /// /// Asynchronously removes a directory from the server /// /// The full or relative path of the directory to delete /// Async callback /// State object /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// IAsyncResult /// public IAsyncResult BeginDeleteDirectory(string path, AsyncCallback callback, object state, bool fastMode = false) { return BeginDeleteDirectory(path, true, 0, fastMode, callback, state); } /// /// Asynchronously removes a directory from the server /// /// The full or relative path of the directory to delete /// If the directory is not empty, remove its contents /// Async callback /// State object /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// IAsyncResult /// public IAsyncResult BeginDeleteDirectory(string path, bool force, AsyncCallback callback, object state, bool fastMode = false) { return BeginDeleteDirectory(path, force, 0, fastMode, callback, state); } /// /// Asynchronously removes a directory from the server /// /// The full or relative path of the directory to delete /// If the directory is not empty, remove its contents /// FtpListOptions for controlling how the directory /// contents are retrieved with the force option is true. If you experience problems /// the file listing can be fine tuned through this parameter. /// Async callback /// State object /// An experimental fast mode that file listing is only requested for once. This improves bandwidth usage and response time. /// IAsyncResult /// public IAsyncResult BeginDeleteDirectory(string path, bool force, FtpListOption options, bool fastMode, AsyncCallback callback, object state) { AsyncDeleteDirectory func; IAsyncResult ar; ar = (func = new AsyncDeleteDirectory(DeleteDirectory)).BeginInvoke(path, force, options, fastMode, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginDeleteDirectory() /// /// IAsyncResult returned from BeginDeleteDirectory /// public void EndDeleteDirectory(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Tests if the specified directory exists on the server. This /// method works by trying to change the working directory to /// the path specified. If it succeeds, the directory is changed /// back to the old working directory and true is returned. False /// is returned otherwise and since the CWD failed it is assumed /// the working directory is still the same. /// /// The path of the directory /// True if it exists, false otherwise. /// public bool DirectoryExists(string path) { string pwd; string ftppath = path.GetFtpPath(); if (ftppath == "." || ftppath == "./" || ftppath == "/") return true; lock (m_lock) { pwd = GetWorkingDirectory(); if (Execute("CWD {0}", ftppath).Success) { FtpReply reply = Execute("CWD {0}", pwd.GetFtpPath()); if (!reply.Success) throw new FtpException("DirectoryExists(): Failed to restore the working directory."); return true; } } return false; } delegate bool AsyncDirectoryExists(string path); /// /// Checks if a directory exists on the server asynchronously. /// /// IAsyncResult /// The full or relative path of the directory to check for /// Async callback /// State object /// public IAsyncResult BeginDirectoryExists(string path, AsyncCallback callback, object state) { AsyncDirectoryExists func; IAsyncResult ar; ar = (func = new AsyncDirectoryExists(DirectoryExists)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginDirectoryExists /// /// IAsyncResult returned from BeginDirectoryExists /// True if the directory exists. False otherwise. /// public bool EndDirectoryExists(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Checks if a file exsts on the server by taking a /// file listing of the parent directory in the path /// and comparing the results the path supplied. /// /// The full or relative path to the file /// True if the file exists /// public bool FileExists(string path) { return FileExists(path, 0); } /// /// Checks if a file exsts on the server by taking a /// file listing of the parent directory in the path /// and comparing the results the path supplied. /// /// The full or relative path to the file /// Options for controling the file listing used to /// determine if the file exists. /// True if the file exists /// public bool FileExists(string path, FtpListOption options) { string dirname = path.GetFtpDirectoryName(); lock (m_lock) { if (!DirectoryExists(dirname)) return false; foreach (FtpListItem item in GetListing(dirname, options)) if (item.Type == FtpFileSystemObjectType.File && item.Name == path.GetFtpFileName()) return true; } return false; } delegate bool AsyncFileExists(string path, FtpListOption options); /// /// Checks if a file exsts on the server by taking a /// file listing of the parent directory in the path /// and comparing the results the path supplied. /// /// The full or relative path to the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginFileExists(string path, AsyncCallback callback, object state) { return BeginFileExists(path, 0, callback, state); } /// /// Checks if a file exsts on the server by taking a /// file listing of the parent directory in the path /// and comparing the results the path supplied. /// /// The full or relative path to the file /// Options for controling the file listing used to /// determine if the file exists. /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginFileExists(string path, FtpListOption options, AsyncCallback callback, object state) { AsyncFileExists func; IAsyncResult ar; ar = (func = new AsyncFileExists(FileExists)).BeginInvoke(path, options, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginFileExists /// /// IAsyncResult returned from BeginFileExists /// True if the file exists /// public bool EndFileExists(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Creates a directory on the server. If the preceding /// directories do not exist they are created. /// /// The full or relative path to the new directory /// public void CreateDirectory(string path) { CreateDirectory(path, true); } /// /// Creates a directory on the server /// /// The full or relative path to the directory to create /// Try to force all non-existant pieces of the path to be created /// public void CreateDirectory(string path, bool force) { FtpReply reply; string ftppath = path.GetFtpPath(); if (ftppath == "." || ftppath == "./" || ftppath == "/") return; lock (m_lock) { path = path.GetFtpPath().TrimEnd('/'); if (force && !DirectoryExists(path.GetFtpDirectoryName())) { FtpTrace.WriteLine(string.Format( "CreateDirectory(\"{0}\", {1}): Create non-existent parent: {2}", path, force, path.GetFtpDirectoryName())); CreateDirectory(path.GetFtpDirectoryName(), true); } else if (DirectoryExists(path)) return; FtpTrace.WriteLine(string.Format("CreateDirectory(\"{0}\", {1})", ftppath, force)); if (!(reply = Execute("MKD {0}", ftppath)).Success) throw new FtpCommandException(reply); } } delegate void AsyncCreateDirectory(string path, bool force); /// /// Creates a directory asynchronously /// /// The full or relative path to the directory to create /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginCreateDirectory(string path, AsyncCallback callback, object state) { return BeginCreateDirectory(path, true, callback, state); } /// /// Creates a directory asynchronously /// /// The full or relative path to the directory to create /// Try to create the whole path if the preceding directories do not exist /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginCreateDirectory(string path, bool force, AsyncCallback callback, object state) { AsyncCreateDirectory func; IAsyncResult ar; ar = (func = new AsyncCreateDirectory(CreateDirectory)).BeginInvoke(path, force, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginCreateDirectory /// /// IAsyncResult returned from BeginCreateDirectory /// public void EndCreateDirectory(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Renames an object on the remote file system. /// /// The full or relative path to the object /// The old or new full or relative path including the new name of the object /// public void Rename(string path, string dest) { FtpReply reply; lock (m_lock) { if (!(reply = Execute("RNFR {0}", path.GetFtpPath())).Success) throw new FtpCommandException(reply); if (!(reply = Execute("RNTO {0}", dest.GetFtpPath())).Success) throw new FtpCommandException(reply); } } delegate void AsyncRename(string path, string dest); /// /// Asynchronously renames an object on the server /// /// The full or relative path to the object /// The old or new full or relative path including the new name of the object /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginRename(string path, string dest, AsyncCallback callback, object state) { AsyncRename func; IAsyncResult ar; ar = (func = new AsyncRename(Rename)).BeginInvoke(path, dest, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginRename /// /// IAsyncResult returned from BeginRename /// public void EndRename(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region File Permissions /// /// Modify the permissions of the given file/folder. /// Only works on *NIX systems, and not on Windows/IIS servers. /// Only works if the FTP server supports the SITE CHMOD command /// (requires the CHMOD extension to be installed and enabled). /// Throws FtpCommandException if there is an issue. /// /// The full or relative path to the item /// The permissions in CHMOD format public void SetFilePermissions(string path, int permissions) { FtpReply reply; lock (m_lock) { if (!(reply = Execute("SITE CHMOD {0} {1}", permissions.ToString(), path.GetFtpPath())).Success) throw new FtpCommandException(reply); } } /// /// Modify the permissions of the given file/folder. /// Only works on *NIX systems, and not on Windows/IIS servers. /// Only works if the FTP server supports the SITE CHMOD command /// (requires the CHMOD extension to be installed and enabled). /// Throws FtpCommandException if there is an issue. /// /// The full or relative path to the item /// The permissions in CHMOD format public void Chmod(string path, int permissions) { SetFilePermissions(path, permissions); } /// /// Modify the permissions of the given file/folder. /// Only works on *NIX systems, and not on Windows/IIS servers. /// Only works if the FTP server supports the SITE CHMOD command /// (requires the CHMOD extension to be installed and enabled). /// Throws FtpCommandException if there is an issue. /// /// The full or relative path to the item /// The owner permissions /// The group permissions /// The other permissions public void SetFilePermissions(string path, FtpPermission owner, FtpPermission group, FtpPermission other) { SetFilePermissions(path, CalcChmod(owner, group, other)); } /// /// Modify the permissions of the given file/folder. /// Only works on *NIX systems, and not on Windows/IIS servers. /// Only works if the FTP server supports the SITE CHMOD command /// (requires the CHMOD extension to be installed and enabled). /// Throws FtpCommandException if there is an issue. /// /// The full or relative path to the item /// The owner permissions /// The group permissions /// The other permissions public void Chmod(string path, FtpPermission owner, FtpPermission group, FtpPermission other) { SetFilePermissions(path, owner, group, other); } /// /// Retrieve the permissions of the given file/folder as an FtpListItem object with all "Permission" properties set. /// Throws FtpCommandException if there is an issue. /// Returns null if the server did not specify a permission value. /// Use `GetChmod` if you required the integer value instead. /// /// The full or relative path to the item public FtpListItem GetFilePermissions(string path) { string fullPath = path.GetFtpPath(); foreach (FtpListItem i in GetListing(path)) { if (i.FullName == fullPath) { return i; } } return null; } /// /// Retrieve the permissions of the given file/folder as an integer in the CHMOD format. /// Throws FtpCommandException if there is an issue. /// Returns 0 if the server did not specify a permission value. /// Use `GetFilePermissions` if you required the permissions in the FtpPermission format. /// /// The full or relative path to the item public int GetChmod(string path) { FtpListItem item = GetFilePermissions(path); return item != null ? item.Chmod : 0; } #endregion #region Link Dereferencing /// /// Recursively dereferences a symbolic link. See the /// MaximumDereferenceCount property for controlling /// how deep this method will recurse before giving up. /// /// The symbolic link /// FtpListItem, null if the link can't be dereferenced /// public FtpListItem DereferenceLink(FtpListItem item) { return DereferenceLink(item, MaximumDereferenceCount); } /// /// Recursively dereferences a symbolic link /// /// The symbolic link /// The maximum depth of recursion that can be performed before giving up. /// FtpListItem, null if the link can't be dereferenced /// public FtpListItem DereferenceLink(FtpListItem item, int recMax) { int count = 0; return DereferenceLink(item, recMax, ref count); } /// /// Derefence a FtpListItem object /// /// The item to derefence /// Maximum recursive calls /// Counter /// FtpListItem, null if the link can't be dereferenced /// FtpListItem DereferenceLink(FtpListItem item, int recMax, ref int count) { if (item.Type != FtpFileSystemObjectType.Link) throw new FtpException("You can only derefernce a symbolic link. Please verify the item type is Link."); if (item.LinkTarget == null) throw new FtpException("The link target was null. Please check this before trying to dereference the link."); foreach (FtpListItem obj in GetListing(item.LinkTarget.GetFtpDirectoryName(), FtpListOption.ForceList)) { if (item.LinkTarget == obj.FullName) { if (obj.Type == FtpFileSystemObjectType.Link) { if (++count == recMax) return null; return DereferenceLink(obj, recMax, ref count); } if (HasFeature(FtpCapability.MDTM)) { DateTime modify = GetModifiedTime(obj.FullName); if (modify != DateTime.MinValue) obj.Modified = modify; } if (obj.Type == FtpFileSystemObjectType.File && obj.Size < 0 && HasFeature(FtpCapability.SIZE)) obj.Size = GetFileSize(obj.FullName); return obj; } } return null; } delegate FtpListItem AsyncDereferenceLink(FtpListItem item, int recMax); /// /// Derefence a FtpListItem object asynchronously /// /// The item to derefence /// Maximum recursive calls /// AsyncCallback /// State Object /// IAsyncResult /// public IAsyncResult BeginDereferenceLink(FtpListItem item, int recMax, AsyncCallback callback, object state) { IAsyncResult ar; AsyncDereferenceLink func; ar = (func = new AsyncDereferenceLink(DereferenceLink)).BeginInvoke(item, recMax, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Derefence a FtpListItem object asynchronously. See the /// MaximumDereferenceCount property for controlling /// how deep this method will recurse before giving up. /// /// The item to derefence /// AsyncCallback /// State Object /// IAsyncResult /// public IAsyncResult BeginDereferenceLink(FtpListItem item, AsyncCallback callback, object state) { return BeginDereferenceLink(item, MaximumDereferenceCount, callback, state); } /// /// Ends a call to BeginDereferenceLink /// /// IAsyncResult /// FtpListItem, null if the link can't be dereferenced /// public FtpListItem EndDereferenceLink(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region File Listing /// /// Returns information about a file system object. You should check the Capabilities /// flags for the FtpCapability.MLSD flag before calling this method. Failing to do /// so will result in an InvalidOperationException being thrown when the server /// does not support machine listings. Returns null if the server response can't /// be parsed or the server returns a failure completion code. The error for a failure /// is logged with FtpTrace. No exception is thrown on error because that would negate /// the usefullness of this method for checking for the existence of an object. /// /// The path of the object to retrieve information about /// A FtpListItem object public FtpListItem GetObjectInfo(string path) { FtpReply reply; string[] res; if ((Capabilities & FtpCapability.MLSD) != FtpCapability.MLSD) { throw new InvalidOperationException("The GetObjectInfo method only works on servers that support machine listings. " + "Please check the Capabilities flags for FtpCapability.MLSD before calling this method."); } if ((reply = Execute("MLST {0}", path)).Success) { res = reply.InfoMessages.Split('\n'); if (res.Length > 1) { string info = ""; for (int i = 1; i < res.Length; i++) { info += res[i]; } return FtpListItem.Parse(null, info, m_caps); } } else { FtpTrace.WriteLine("Failed to get object info for path {0} with error {1}", path, reply.ErrorMessage); } return null; } delegate FtpListItem AsyncGetObjectInfo(string path); /// /// Returns information about a file system object. You should check the Capabilities /// flags for the FtpCapability.MLSD flag before calling this method. Failing to do /// so will result in an InvalidOperationException being thrown when the server /// does not support machine listings. Returns null if the server response can't /// be parsed or the server returns a failure completion code. The error for a failure /// is logged with FtpTrace. No exception is thrown on error because that would negate /// the usefullness of this method for checking for the existence of an object. /// /// Path of the item to retrieve information about /// Async Callback /// State object /// IAsyncResult public IAsyncResult BeginGetObjectInfo(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncGetObjectInfo func; ar = (func = new AsyncGetObjectInfo(GetObjectInfo)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginGetObjectInfo /// /// IAsyncResult returned from BeginGetObjectInfo /// FtpListItem if the command succeeded, null if there was a problem. public FtpListItem EndGetObjectInfo(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets a file listing from the server. Each FtpListItem object returned /// contains information about the file that was able to be retrieved. If /// a DateTime property is equal to DateTime.MinValue then it means the /// date in question was not able to be retrieved. If the Size property /// is equal to 0 then it means the size of the object could also not /// be retrieved. /// /// An array of FtpListItem objects /// public FtpListItem[] GetListing() { return GetListing(null); } /// /// Gets a file listing from the server. Each FtpListItem object returned /// contains information about the file that was able to be retrieved. If /// a DateTime property is equal to DateTime.MinValue then it means the /// date in question was not able to be retrieved. If the Size property /// is equal to 0 then it means the size of the object could also not /// be retrieved. /// /// The path of the directory to list /// An array of FtpListItem objects /// public FtpListItem[] GetListing(string path) { return GetListing(path, 0); } /// /// Gets a file listing from the server. Each FtpListItem object returned /// contains information about the file that was able to be retrieved. If /// a DateTime property is equal to DateTime.MinValue then it means the /// date in question was not able to be retrieved. If the Size property /// is equal to 0 then it means the size of the object could also not /// be retrieved. /// /// The path of the directory to list /// Options that dictacte how a list is performed and what information is gathered. /// An array of FtpListItem objects /// public FtpListItem[] GetListing(string path, FtpListOption options) { FtpListItem item = null; List lst = new List(); List rawlisting = new List(); string listcmd = null; string buf = null; bool includeSelf = (options & FtpListOption.IncludeSelfAndParent) == FtpListOption.IncludeSelfAndParent; if (path == null || path.Trim().Length == 0) { string pwd = GetWorkingDirectory(); if (pwd != null && pwd.Trim().Length > 0) path = pwd; else path = "./"; } else if (!path.StartsWith("/")) { string pwd = GetWorkingDirectory(); if (pwd != null && pwd.Trim().Length > 0) { if (path.StartsWith("./")) path = path.Remove(0, 2); path = string.Format("{0}/{1}", pwd, path).GetFtpPath(); } } // MLSD provides a machine parsable format with more // accurate information than most of the UNIX long list // formats which translates to more effcient file listings // so always prefer MLSD over LIST unless the caller of this // method overrides it with the ForceList option if ((options & FtpListOption.ForceList) != FtpListOption.ForceList && HasFeature(FtpCapability.MLSD)) { listcmd = "MLSD"; } else { if ((options & FtpListOption.UseLS) == FtpListOption.UseLS) { listcmd = "LS"; } else if ((options & FtpListOption.NameList) == FtpListOption.NameList) { listcmd = "NLST"; } else { string listopts = ""; listcmd = "LIST"; if ((options & FtpListOption.AllFiles) == FtpListOption.AllFiles) listopts += "a"; if ((options & FtpListOption.Recursive) == FtpListOption.Recursive) listopts += "R"; if (listopts.Length > 0) listcmd += " -" + listopts; } } if ((options & FtpListOption.NoPath) != FtpListOption.NoPath) { listcmd = string.Format("{0} {1}", listcmd, path.GetFtpPath()); } lock (m_lock) { Execute("TYPE I"); // read in raw file listing using (FtpDataStream stream = OpenDataStream(listcmd, 0)) { try { while ((buf = stream.ReadLine(Encoding)) != null) { if (buf.Length > 0) { rawlisting.Add(buf); FtpTrace.WriteLine(buf); } } } finally { stream.Close(); } } } for (int i = 0; i < rawlisting.Count; i++) { buf = rawlisting[i]; if ((options & FtpListOption.NameList) == FtpListOption.NameList) { // if NLST was used we only have a file name so // there is nothing to parse. item = new FtpListItem() { FullName = buf }; if (DirectoryExists(item.FullName)) item.Type = FtpFileSystemObjectType.Directory; else item.Type = FtpFileSystemObjectType.File; lst.Add(item); } else { // if this is a result of LIST -R then the path will be spit out // before each block of objects if (listcmd.StartsWith("LIST") && (options & FtpListOption.Recursive) == FtpListOption.Recursive) { if (buf.StartsWith("/") && buf.EndsWith(":")) { path = buf.TrimEnd(':'); continue; } } // if the next line in the listing starts with spaces // it is assumed to be a continuation of the current line if (i + 1 < rawlisting.Count && (rawlisting[i + 1].StartsWith("\t") || rawlisting[i + 1].StartsWith(" "))) buf += rawlisting[++i]; item = FtpListItem.Parse(path, buf, m_caps); // FtpListItem.Parse() returns null if the line // could not be parsed if (item != null) { if (includeSelf || !(item.Name == "." || item.Name == "..")) { lst.Add(item); } else { FtpTrace.WriteLine("Skipped self or parent item: " + item.Name); } } else { FtpTrace.WriteLine("Failed to parse file listing: " + buf); } } // load extended information that wasn't available if the list options flags say to do so. if (item != null) { // try to dereference symbolic links if the appropriate list // option was passed if (item.Type == FtpFileSystemObjectType.Link && (options & FtpListOption.DerefLinks) == FtpListOption.DerefLinks) { item.LinkObject = DereferenceLink(item); } if ((options & FtpListOption.Modify) == FtpListOption.Modify && HasFeature(FtpCapability.MDTM)) { // if the modified date was not loaded or the modified date is more than a day in the future // and the server supports the MDTM command, load the modified date. // most servers do not support retrieving the modified date // of a directory but we try any way. if (item.Modified == DateTime.MinValue || listcmd.StartsWith("LIST")) { DateTime modify; if (item.Type == FtpFileSystemObjectType.Directory) FtpTrace.WriteLine("Trying to retrieve modification time of a directory, some servers don't like this..."); if ((modify = GetModifiedTime(item.FullName)) != DateTime.MinValue) item.Modified = modify; } } if ((options & FtpListOption.Size) == FtpListOption.Size && HasFeature(FtpCapability.SIZE)) { // if no size was parsed, the object is a file and the server // supports the SIZE command, then load the file size if (item.Size == -1) { if (item.Type != FtpFileSystemObjectType.Directory) { item.Size = GetFileSize(item.FullName); } else { item.Size = 0; } } } } } return lst.ToArray(); } /// /// Gets a file listing from the server asynchronously /// /// AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginGetListing(AsyncCallback callback, Object state) { return BeginGetListing(null, callback, state); } /// /// Gets a file listing from the server asynchronously /// /// The path to list /// AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginGetListing(string path, AsyncCallback callback, Object state) { return BeginGetListing(path, FtpListOption.Modify | FtpListOption.Size, callback, state); } delegate FtpListItem[] AsyncGetListing(string path, FtpListOption options); /// /// Gets a file listing from the server asynchronously /// /// The path to list /// Options that dictate how the list operation is performed /// AsyncCallback method /// State object /// IAsyncResult /// public IAsyncResult BeginGetListing(string path, FtpListOption options, AsyncCallback callback, Object state) { IAsyncResult ar; AsyncGetListing func; ar = (func = new AsyncGetListing(GetListing)).BeginInvoke(path, options, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous file listing /// /// IAsyncResult return from BeginGetListing() /// An array of items retrieved in the listing /// public FtpListItem[] EndGetListing(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region Name Listing /// /// Returns a file/directory listing using the NLST command. /// /// A string array of file and directory names if any were returned. public string[] GetNameListing() { return GetNameListing(null); } /// /// Returns a file/directory listing using the NLST command. /// /// The path of the directory to list /// A string array of file and directory names if any were returned. /// public string[] GetNameListing(string path) { List lst = new List(); string pwd = GetWorkingDirectory(); /*if (path == null || path.GetFtpPath().Trim().Length == 0 || path.StartsWith(".")) { if (pwd == null || pwd.Length == 0) // couldn't get the working directory path = "./"; else if (path.StartsWith("./")) path = string.Format("{0}/{1}", pwd, path.Remove(0, 2)); else path = pwd; }*/ path = path.GetFtpPath(); if (path == null || path.Trim().Length == 0) { if (pwd != null && pwd.Trim().Length > 0) path = pwd; else path = "./"; } else if (!path.StartsWith("/") && pwd != null && pwd.Trim().Length > 0) { if (path.StartsWith("./")) path = path.Remove(0, 2); path = string.Format("{0}/{1}", pwd, path).GetFtpPath(); } lock (m_lock) { // always get the file listing in binary // to avoid any potential character translation // problems that would happen if in ASCII. Execute("TYPE I"); using (FtpDataStream stream = OpenDataStream(string.Format("NLST {0}", path.GetFtpPath()), 0)) { string buf; try { while ((buf = stream.ReadLine(Encoding)) != null) lst.Add(buf); } finally { stream.Close(); } } } return lst.ToArray(); } delegate string[] AsyncGetNameListing(string path); /// /// Asynchronously gets a list of file and directory names for the specified path. /// /// The path of the directory to list /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginGetNameListing(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncGetNameListing func; ar = (func = new AsyncGetNameListing(GetNameListing)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Asynchronously gets a list of file and directory names for the specified path. /// /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginGetNameListing(AsyncCallback callback, object state) { return BeginGetNameListing(null, callback, state); } /// /// Ends a call to BeginGetNameListing() /// /// IAsyncResult object returned from BeginGetNameListing /// An array of file and directory names if any were returned. /// public string[] EndGetNameListing(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } #endregion #region Misc Methods private static string DecodeUrl(string url) { #if CORE return WebUtility.UrlDecode(url); #else return HttpUtility.UrlDecode(url); #endif } private static byte[] ReadToEnd(Stream stream, long maxLength, int chunkLen) { int read = 1; byte[] buffer = new byte[chunkLen]; using (var mem = new MemoryStream()) { do { long length = maxLength == 0 ? buffer.Length : Math.Min(maxLength - (int)mem.Length, buffer.Length); read = stream.Read(buffer, 0, (int)length); mem.Write(buffer, 0, read); if (maxLength > 0 && mem.Length == maxLength) break; } while (read > 0); return mem.ToArray(); } } /// /// Sets the data type of information sent over the data stream /// /// ASCII/Binary protected void SetDataType(FtpDataType type) { FtpReply reply; lock (m_lock) { switch (type) { case FtpDataType.ASCII: if (!(reply = Execute("TYPE A")).Success) throw new FtpCommandException(reply); /*if (!(reply = Execute("STRU R")).Success) FtpTrace.WriteLine(reply.Message);*/ break; case FtpDataType.Binary: if (!(reply = Execute("TYPE I")).Success) throw new FtpCommandException(reply); /*if (!(reply = Execute("STRU F")).Success) FtpTrace.WriteLine(reply.Message);*/ break; default: throw new FtpException("Unsupported data type: " + type.ToString()); } } CurrentDataType = type; } delegate void AsyncSetDataType(FtpDataType type); /// /// Asynchronously sets the data type on the server /// /// ASCII/Binary /// Async callback /// State object /// IAsyncResult protected IAsyncResult BeginSetDataType(FtpDataType type, AsyncCallback callback, object state) { IAsyncResult ar; AsyncSetDataType func; ar = (func = new AsyncSetDataType(SetDataType)).BeginInvoke(type, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginSetDataType() /// /// IAsyncResult returned from BeginSetDataType() protected void EndSetDataType(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Sets the work directory on the server /// /// The path of the directory to change to /// public void SetWorkingDirectory(string path) { FtpReply reply; string ftppath = path.GetFtpPath(); if (ftppath == "." || ftppath == "./") return; lock (m_lock) { if (!(reply = Execute("CWD {0}", ftppath)).Success) throw new FtpCommandException(reply); } } delegate void AsyncSetWorkingDirectory(string path); /// /// Asynchronously changes the working directory on the server /// /// The directory to change to /// Async Callback /// State object /// IAsyncResult /// public IAsyncResult BeginSetWorkingDirectory(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncSetWorkingDirectory func; ar = (func = new AsyncSetWorkingDirectory(SetWorkingDirectory)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends asynchronous directory change /// /// IAsyncResult returned from BeginSetWorkingDirectory /// public void EndSetWorkingDirectory(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets the current working directory /// /// The current working directory, ./ if the response couldn't be parsed. /// public string GetWorkingDirectory() { FtpReply reply; Match m; lock (m_lock) { if (!(reply = Execute("PWD")).Success) throw new FtpCommandException(reply); } if ((m = Regex.Match(reply.Message, "\"(?.*)\"")).Success) { return m.Groups["pwd"].Value; } // check for MODCOMP ftp path mentioned in forums: https://netftp.codeplex.com/discussions/444461 if ((m = Regex.Match(reply.Message, "PWD = (?.*)")).Success) { return m.Groups["pwd"].Value; } FtpTrace.WriteLine("Failed to parse working directory from: " + reply.Message); return "./"; } delegate string AsyncGetWorkingDirectory(); /// /// Asynchronously retrieves the working directory /// /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginGetWorkingDirectory(AsyncCallback callback, object state) { IAsyncResult ar; AsyncGetWorkingDirectory func; ar = (func = new AsyncGetWorkingDirectory(GetWorkingDirectory)).BeginInvoke(callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous call to retrieve the working directory /// /// IAsyncResult returned from BeginGetWorkingDirectory /// The current working directory /// public string EndGetWorkingDirectory(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets the size of the file /// /// The full or relative path of the file /// -1 if the command fails, otherwise the file size /// public virtual long GetFileSize(string path) { FtpReply reply; long length = 0; lock (m_lock) { if (!(reply = Execute("SIZE {0}", path.GetFtpPath())).Success) return -1; if (!long.TryParse(reply.Message, out length)) return -1; } return length; } delegate long AsyncGetFileSize(string path); /// /// Asynchronously retrieve the size of the specified file /// /// The full or relative path of the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginGetFileSize(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncGetFileSize func; ar = (func = new AsyncGetFileSize(GetFileSize)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginGetFileSize() /// /// IAsyncResult returned from BeginGetFileSize /// The size of the file, -1 if there was a problem. /// public long EndGetFileSize(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets the modified time of the file /// /// The full path to the file /// The modified time, DateTime.MinValue if there was a problem /// public virtual DateTime GetModifiedTime(string path) { DateTime modify = DateTime.MinValue; FtpReply reply; lock (m_lock) { if ((reply = Execute("MDTM {0}", path.GetFtpPath())).Success) modify = reply.Message.GetFtpDate(DateTimeStyles.AssumeUniversal); } return modify; } delegate DateTime AsyncGetModifiedTime(string path); /// /// Gets the modified time of the file /// /// The full path to the file /// Async callback /// State object /// IAsyncResult /// public IAsyncResult BeginGetModifiedTime(string path, AsyncCallback callback, object state) { IAsyncResult ar; AsyncGetModifiedTime func; ar = (func = new AsyncGetModifiedTime(GetModifiedTime)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginGetModifiedTime() /// /// IAsyncResult returned from BeginGetModifiedTime() /// The modified time, DateTime.MinValue if there was a problem /// public DateTime EndGetModifiedTime(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets the currently selected hash algorith for the HASH /// command. This feature is experimental. See this link /// for details: /// http://tools.ietf.org/html/draft-bryan-ftpext-hash-02 /// /// The FtpHashType flag or FtpHashType.NONE if there was a problem. /// public FtpHashAlgorithm GetHashAlgorithm() { FtpReply reply; FtpHashAlgorithm type = FtpHashAlgorithm.NONE; lock (m_lock) { if ((reply = Execute("OPTS HASH")).Success) { switch (reply.Message) { case "SHA-1": type = FtpHashAlgorithm.SHA1; break; case "SHA-256": type = FtpHashAlgorithm.SHA256; break; case "SHA-512": type = FtpHashAlgorithm.SHA512; break; case "MD5": type = FtpHashAlgorithm.MD5; break; } } } return type; } delegate FtpHashAlgorithm AsyncGetHashAlgorithm(); /// /// Asynchronously get the hash algorithm being used by the HASH command. /// /// Async callback /// State object /// IAsyncResult public IAsyncResult BeginGetHashAlgorithm(AsyncCallback callback, object state) { AsyncGetHashAlgorithm func; IAsyncResult ar; ar = (func = new AsyncGetHashAlgorithm(GetHashAlgorithm)).BeginInvoke(callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends a call to BeginGetHashAlgorithm /// /// IAsyncResult returned from BeginGetHashAlgorithm public FtpHashAlgorithm EndGetHashAlgorithm(IAsyncResult ar) { return GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Tells the server which hash algorith to use /// for the HASH command. If you specifiy an /// algorithm not listed in FtpClient.HashTypes /// a NotImplemented() exectpion will be thrown /// so be sure to query that list of Flags before /// selecting a hash algorithm. Support for the /// HASH command is experimental. Please see /// the following link for more details: /// http://tools.ietf.org/html/draft-bryan-ftpext-hash-02 /// /// Hash Algorithm /// public void SetHashAlgorithm(FtpHashAlgorithm type) { FtpReply reply; string algorithm; lock (m_lock) { if ((HashAlgorithms & type) != type) throw new NotImplementedException(string.Format("The hash algorithm {0} was not advertised by the server.", type.ToString())); switch (type) { case FtpHashAlgorithm.SHA1: algorithm = "SHA-1"; break; case FtpHashAlgorithm.SHA256: algorithm = "SHA-256"; break; case FtpHashAlgorithm.SHA512: algorithm = "SHA-512"; break; case FtpHashAlgorithm.MD5: algorithm = "MD5"; break; default: algorithm = type.ToString(); break; } if (!(reply = Execute("OPTS HASH {0}", algorithm)).Success) throw new FtpCommandException(reply); } } delegate void AsyncSetHashAlgorithm(FtpHashAlgorithm type); /// /// Asynchronously sets the hash algorithm type to be used with the HASH command. /// /// Hash algorithm to use /// Async Callback /// State object /// IAsyncResult public IAsyncResult BeginSetHashAlgorithm(FtpHashAlgorithm type, AsyncCallback callback, object state) { AsyncSetHashAlgorithm func; IAsyncResult ar; ar = (func = new AsyncSetHashAlgorithm(SetHashAlgorithm)).BeginInvoke(type, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous call to BeginSetHashAlgorithm /// /// IAsyncResult returned from BeginSetHashAlgorithm public void EndSetHashAlgorithm(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Gets the hash of an object on the server using the /// currently selected hash algorithm. Supported /// algorithms, if any, are available in the HashAlgorithms /// property. You should confirm that it's not equal /// to FtpHashAlgorithm.NONE before calling this method /// otherwise the server trigger a FtpCommandException() /// due to a lack of support for the HASH command. You can /// set the algorithm using the SetHashAlgorithm() method and /// you can query the server for the current hash algorithm /// using the GetHashAlgorithm() method. /// /// This feature is experimental and based on the following draft: /// http://tools.ietf.org/html/draft-bryan-ftpext-hash-02 /// /// Full or relative path of the object to compute the hash for. /// The hash of the file. /// public FtpHash GetHash(string path) { FtpReply reply; FtpHash hash = new FtpHash(); Match m; if (path == null) throw new ArgumentException("GetHash(path) argument can't be null"); lock (m_lock) { if (!(reply = Execute("HASH {0}", path.GetFtpPath())).Success) throw new FtpCommandException(reply); } // Current draft says the server should return this: // SHA-256 0-49 169cd22282da7f147cb491e559e9dd filename.ext if (!(m = Regex.Match(reply.Message, @"(?.+)\s" + @"(?\d+)-(?\d+)\s" + @"(?.+)\s" + @"(?.+)")).Success) { // Current version of FileZilla returns this: // SHA-1 21c2ca15cf570582949eb59fb78038b9c27ffcaf m = Regex.Match(reply.Message, @"(?.+)\s(?.+)\s"); } if (m != null && m.Success) { switch (m.Groups["algorithm"].Value) { case "SHA-1": hash.Algorithm = FtpHashAlgorithm.SHA1; break; case "SHA-256": hash.Algorithm = FtpHashAlgorithm.SHA256; break; case "SHA-512": hash.Algorithm = FtpHashAlgorithm.SHA512; break; case "MD5": hash.Algorithm = FtpHashAlgorithm.MD5; break; default: throw new NotImplementedException("Unknown hash algorithm: " + m.Groups["algorithm"].Value); } hash.Value = m.Groups["hash"].Value; } else { FtpTrace.WriteLine("Failed to parse hash from: {0}", reply.Message); } return hash; } delegate FtpHash AsyncGetHash(string path); /// /// Asynchronously retrieves the hash for the specified file /// /// The file you want the server to compute the hash for /// AsyncCallback /// State object /// IAsyncResult public IAsyncResult BeginGetHash(string path, AsyncCallback callback, object state) { AsyncGetHash func; IAsyncResult ar; ar = (func = new AsyncGetHash(GetHash)).BeginInvoke(path, callback, state); lock (m_asyncmethods) { m_asyncmethods.Add(ar, func); } return ar; } /// /// Ends an asynchronous call to BeginGetHash /// /// IAsyncResult returned from BeginGetHash public void EndGetHash(IAsyncResult ar) { GetAsyncDelegate(ar).EndInvoke(ar); } /// /// Disables UTF8 support and changes the Encoding property /// back to ASCII. If the server returns an error when trying /// to turn UTF8 off a FtpCommandException will be thrown. /// public void DisableUTF8() { FtpReply reply; lock (m_lock) { if (!(reply = Execute("OPTS UTF8 OFF")).Success) throw new FtpCommandException(reply); m_textEncoding = Encoding.ASCII; m_textEncodingAutoUTF = false; } } /// /// Disconnects from the server, releases resources held by this /// object. /// public void Dispose() { lock (m_lock) { if (IsDisposed) return; FtpTrace.WriteLine("Disposing FtpClient object..."); try { if (IsConnected) { Disconnect(); } } catch (Exception ex) { FtpTrace.WriteLine("FtpClient.Dispose(): Caught and discarded an exception while disconnecting from host: {0}", ex.ToString()); } if (m_stream != null) { try { m_stream.Dispose(); } catch (Exception ex) { FtpTrace.WriteLine("FtpClient.Dispose(): Caught and discarded an exception while disposing FtpStream object: {0}", ex.ToString()); } finally { m_stream = null; } } m_credentials = null; m_textEncoding = null; m_host = null; m_asyncmethods.Clear(); IsDisposed = true; GC.SuppressFinalize(this); } } /// /// Finalizer /// ~FtpClient() { Dispose(); } /// /// Creates a new isntance of FtpClient /// public FtpClient() { } private void ReadStaleData() { if (m_stream != null && m_stream.SocketDataAvailable > 0) { if (m_stream.IsConnected && !m_stream.IsEncrypted) { byte[] buf = new byte[m_stream.SocketDataAvailable]; m_stream.RawSocketRead(buf); FtpTrace.Write("The data was: "); FtpTrace.WriteLine(Encoding.GetString(buf).TrimEnd('\r', '\n')); } } } private bool IsProxy() { return (this is FtpClientProxy); } #endregion #region Static API /// /// Calculate the CHMOD integer value given a set of permissions. /// public static int CalcChmod(FtpPermission owner, FtpPermission group, FtpPermission other) { int chmod = 0; if (HasPermission(owner, FtpPermission.Read)) { chmod += 400; } if (HasPermission(owner, FtpPermission.Write)) { chmod += 200; } if (HasPermission(owner, FtpPermission.Execute)) { chmod += 100; } if (HasPermission(group, FtpPermission.Read)) { chmod += 40; } if (HasPermission(group, FtpPermission.Write)) { chmod += 20; } if (HasPermission(group, FtpPermission.Execute)) { chmod += 10; } if (HasPermission(other, FtpPermission.Read)) { chmod += 4; } if (HasPermission(other, FtpPermission.Write)) { chmod += 2; } if (HasPermission(other, FtpPermission.Execute)) { chmod += 1; } return chmod; } private static bool HasPermission(FtpPermission owner, FtpPermission flag) { return (owner & flag) == flag; } /// /// Connects to the specified URI. If the path specified by the URI ends with a /// / then the working directory is changed to the path specified. /// /// The URI to parse /// Indicates if a ssl certificate should be validated when using FTPS schemes /// FtpClient object public static FtpClient Connect(Uri uri, bool checkcertificate) { FtpClient cl = new FtpClient(); if (uri == null) throw new ArgumentException("Invalid URI object"); switch (uri.Scheme.ToLower()) { case "ftp": case "ftps": break; default: throw new UriFormatException("The specified URI scheme is not supported. Please use ftp:// or ftps://"); } cl.Host = uri.Host; cl.Port = uri.Port; if (uri.UserInfo != null && uri.UserInfo.Length > 0) { if (uri.UserInfo.Contains(":")) { string[] parts = uri.UserInfo.Split(':'); if (parts.Length != 2) throw new UriFormatException("The user info portion of the URI contains more than 1 colon. The username and password portion of the URI should be URL encoded."); cl.Credentials = new NetworkCredential(DecodeUrl(parts[0]), DecodeUrl(parts[1])); } else cl.Credentials = new NetworkCredential(DecodeUrl(uri.UserInfo), ""); } else { // if no credentials were supplied just make up // some for anonymous authentication. cl.Credentials = new NetworkCredential("ftp", "ftp"); } cl.ValidateCertificate += new FtpSslValidation(delegate(FtpClient control, FtpSslValidationEventArgs e) { if (e.PolicyErrors != System.Net.Security.SslPolicyErrors.None && checkcertificate) e.Accept = false; else e.Accept = true; }); cl.Connect(); if (uri.PathAndQuery != null && uri.PathAndQuery.EndsWith("/")) cl.SetWorkingDirectory(uri.PathAndQuery); return cl; } /// /// Connects to the specified URI. If the path specified by the URI ends with a /// / then the working directory is changed to the path specified. /// /// The URI to parse /// FtpClient object public static FtpClient Connect(Uri uri) { return Connect(uri, true); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// ASCII/Binary mode /// Restart location /// Stream object /// public static Stream OpenRead(Uri uri, bool checkcertificate, FtpDataType datatype, long restart) { FtpClient cl = null; CheckURI(uri); cl = Connect(uri, checkcertificate); cl.EnableThreadSafeDataConnections = false; return cl.OpenRead(uri.PathAndQuery, datatype, restart); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// ASCII/Binary mode /// Stream object /// public static Stream OpenRead(Uri uri, bool checkcertificate, FtpDataType datatype) { return OpenRead(uri, checkcertificate, datatype, 0); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// Stream object /// public static Stream OpenRead(Uri uri, bool checkcertificate) { return OpenRead(uri, checkcertificate, FtpDataType.Binary, 0); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Stream object /// public static Stream OpenRead(Uri uri) { return OpenRead(uri, true, FtpDataType.Binary, 0); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// ASCII/Binary mode /// Stream object /// public static Stream OpenWrite(Uri uri, bool checkcertificate, FtpDataType datatype) { FtpClient cl = null; CheckURI(uri); cl = Connect(uri, checkcertificate); cl.EnableThreadSafeDataConnections = false; return cl.OpenWrite(uri.PathAndQuery, datatype); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// Stream object /// public static Stream OpenWrite(Uri uri, bool checkcertificate) { return OpenWrite(uri, checkcertificate, FtpDataType.Binary); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Stream object /// public static Stream OpenWrite(Uri uri) { return OpenWrite(uri, true, FtpDataType.Binary); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// ASCII/Binary mode /// Stream object /// public static Stream OpenAppend(Uri uri, bool checkcertificate, FtpDataType datatype) { FtpClient cl = null; CheckURI(uri); cl = Connect(uri, checkcertificate); cl.EnableThreadSafeDataConnections = false; return cl.OpenAppend(uri.PathAndQuery, datatype); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Indicates if a ssl certificate should be validated when using FTPS schemes /// Stream object /// public static Stream OpenAppend(Uri uri, bool checkcertificate) { return OpenAppend(uri, checkcertificate, FtpDataType.Binary); } /// /// Opens a stream to the file specified by the URI /// /// FTP/FTPS URI pointing at a file /// Stream object /// public static Stream OpenAppend(Uri uri) { return OpenAppend(uri, true, FtpDataType.Binary); } private static void CheckURI(Uri uri) { if (uri.PathAndQuery == null || uri.PathAndQuery.Length == 0) { throw new UriFormatException("The supplied URI does not contain a valid path."); } if (uri.PathAndQuery.EndsWith("/")) { throw new UriFormatException("The supplied URI points at a directory."); } } /// /// Static method used to resolve internet IP /// /// ip public static string GetPublicIP() { #if NETFX var request = WebRequest.Create("https://api.ipify.org/"); request.Method = "GET"; using (var response = request.GetResponse()) { using (var stream = new StreamReader(response.GetResponseStream())) { return stream.ReadToEnd(); } } #endif return null; } #endregion } }