using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Globalization;
using System.Security.Authentication;
using System.Net;
using FluentFTP.Proxy;
#if !CORE
using System.Web;
#endif

#if (CORE || NETFX)
using System.Threading;
#endif
#if ASYNC
using System.Threading.Tasks;
#endif

namespace FluentFTP {

	/// <summary>
	/// FTP Control Connection. Speaks the FTP protocol with the server and
	/// provides facilities for performing 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 <see cref="System.Diagnostics.TraceListener"/>s for capturing
	/// the conversation between FluentFTP and the server.
	/// </summary>
	/// <example>The following example illustrates how to assist in debugging
	/// FluentFTP by getting a transaction log from the server.
	/// <code source="..\Examples\Debug.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates adding a custom file
	/// listing parser in the event that you encounter a list format
	/// not already supported.
	/// <code source="..\Examples\CustomParser.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to validate
	/// a SSL certificate when using SSL/TLS.
	/// <code source="..\Examples\ValidateCertificate.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to download a file.
	/// <code source="..\Examples\OpenRead.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to download a file
	/// using a URI object.
	/// <code source="..\Examples\OpenReadURI.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to upload a file.
	/// <code source="..\Examples\OpenWrite.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to upload a file
	/// using a URI object.
	/// <code source="..\Examples\OpenWriteURI.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to append to a file.
	/// <code source="..\Examples\OpenAppend.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to append to a file
	/// using a URI object.
	/// <code source="..\Examples\OpenAppendURI.cs" lang="cs" />
	/// </example>
	/// <example>The following example demonstrates how to get a file
	/// listing from the server.
	/// <code source="..\Examples\GetListing.cs" lang="cs" />
	/// </example>
	public partial class FtpClient : IFtpClient, IDisposable {

		#region Delete File

		/// <summary>
		/// Deletes a file on the server
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		/// <example><code source="..\Examples\DeleteFile.cs" lang="cs" /></example>
		public void DeleteFile(string path) {
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

#if !CORE14
			lock (m_lock) {
#endif
				FtpTrace.WriteFunc("DeleteFile", new object[] { path });

				if (!(reply = Execute("DELE " + path.GetFtpPath())).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate void AsyncDeleteFile(string path);

		/// <summary>
		/// Begins an asynchronous operation to delete the specified file on the server
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginDeleteFile.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginDeleteFile"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from BeginDeleteFile</param>
		/// <example><code source="..\Examples\BeginDeleteFile.cs" lang="cs" /></example>
		public void EndDeleteFile(IAsyncResult ar) {
			GetAsyncDelegate<AsyncDeleteFile>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Deletes a file from the server asynchronously
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		public async Task DeleteFileAsync(string path)
		{
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(DeleteFileAsync), new object[] { path });

			if (!(reply = await ExecuteAsync("DELE " + path.GetFtpPath())).Success)
				throw new FtpCommandException(reply);
		}
#endif

		#endregion

		#region Delete Directory

		/// <summary>
		/// Deletes the specified directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <example><code source="..\Examples\DeleteDirectory.cs" lang="cs" /></example>
		public void DeleteDirectory(string path) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("DeleteDirectory", new object[] { path });
			DeleteDirInternal(path, true, FtpListOption.ForceList | FtpListOption.Recursive);
		}

		/// <summary>
		/// Deletes the specified directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="options">Useful to delete hidden files or dot-files.</param>
		/// <example><code source="..\Examples\DeleteDirectory.cs" lang="cs" /></example>
		public void DeleteDirectory(string path, FtpListOption options) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("DeleteDirectory", new object[] { path, options });
			DeleteDirInternal(path, true, options);
		}

		/// <summary>
		/// Deletes the specified directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="deleteContents">If the directory is not empty, remove its contents</param>
		/// <param name="options">Useful to delete hidden files or dot-files.</param>
		/// <example><code source="..\Examples\DeleteDirectory.cs" lang="cs" /></example>
		private void DeleteDirInternal(string path, bool deleteContents, FtpListOption options) {
			FtpReply reply;
			string ftppath = path.GetFtpPath();


#if !CORE14
			lock (m_lock) {
#endif



				// DELETE CONTENTS OF THE DIRECTORY
				if (deleteContents) {

					// 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.
					bool recurse = !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 (recurse) {
						itemList = GetListing(path, options);
					} else {
						itemList = GetListing(path, options).OrderByDescending(x => x.FullName.Count(c => c.Equals('/'))).ThenBy(x => x.Type).ToArray();
					}

					// delete the item based on the type
					foreach (FtpListItem item in itemList) {
						switch (item.Type) {
							case FtpFileSystemObjectType.File:
								DeleteFile(item.FullName);
								break;
							case FtpFileSystemObjectType.Directory:
								DeleteDirInternal(item.FullName, recurse, options);
								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 " + ftppath)).Success) {
					throw new FtpCommandException(reply);
				}

#if !CORE14
			}
#endif
		}

		/// <summary>
		/// Checks whether <see cref="o:GetListing"/> will be called recursively or not.
		/// </summary>
		/// <param name="options"></param>
		/// <returns></returns>
		private bool WasGetListingRecursive(FtpListOption options) {

			// if recursive listings not supported by the server then obviously NO
			if (!RecursiveList) {
				return false;
			}

			// if machine listings and not force list then NO
			if (HasFeature(FtpCapability.MLSD) && (options & FtpListOption.ForceList) != FtpListOption.ForceList) {
				return false;
			}

			// if name listings then NO
			if ((options & FtpListOption.UseLS) == FtpListOption.UseLS || (options & FtpListOption.NameList) == FtpListOption.NameList) {
				return false;
			}

			// lastly if recursive is enabled then YES
			if ((options & FtpListOption.Recursive) == FtpListOption.Recursive) {
				return true;
			}

			// in all other cases NO
			return false;
		}

#if !CORE
		delegate void AsyncDeleteDirectory(string path, FtpListOption options);

		/// <summary>
		/// Begins an asynchronous operation to delete the specified directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginDeleteDirectory.cs" lang="cs" /></example>
		public IAsyncResult BeginDeleteDirectory(string path, AsyncCallback callback, object state) {

			return BeginDeleteDirectory(path, FtpListOption.ForceList | FtpListOption.Recursive, callback, state);
		}

		/// <summary>
		/// Begins an asynchronous operation to delete the specified directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="options">Useful to delete hidden files or dot-files.</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginDeleteDirectory.cs" lang="cs" /></example>
		public IAsyncResult BeginDeleteDirectory(string path, FtpListOption options, AsyncCallback callback, object state) {

			AsyncDeleteDirectory func;
			IAsyncResult ar;

			ar = (func = new AsyncDeleteDirectory(DeleteDirectory)).BeginInvoke(path, options, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="o:BeginDeleteDirectory"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from BeginDeleteDirectory</param>
		/// <example><code source="..\Examples\BeginDeleteDirectory.cs" lang="cs" /></example>
		public void EndDeleteDirectory(IAsyncResult ar) {
			GetAsyncDelegate<AsyncDeleteDirectory>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Asynchronously removes a directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		public Task DeleteDirectoryAsync(string path) {
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(DeleteDirectoryAsync), new object[] { path });
			return DeleteDirInternalAsync(path, true, FtpListOption.ForceList | FtpListOption.Recursive);
		}

		/// <summary>
		/// Asynchronously removes a directory and all its contents.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="options">Useful to delete hidden files or dot-files.</param>
		public Task DeleteDirectoryAsync(string path, FtpListOption options) {
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(DeleteDirectoryAsync), new object[] { path, options });
			return DeleteDirInternalAsync(path, true, options);
		}

		/// <summary>
		/// Asynchronously removes a directory. Used by <see cref="DeleteDirectoryAsync(string)"/> and
		/// <see cref="DeleteDirectoryAsync(string, FtpListOption)"/>.
		/// </summary>
		/// <param name="path">The full or relative path of the directory to delete</param>
		/// <param name="deleteContents">Delete the contents before deleting the folder</param>
		/// <param name="options">Useful to delete hidden files or dot-files.</param>
		/// <returns></returns>
		private async Task DeleteDirInternalAsync(string path, bool deleteContents, FtpListOption options)
		{
			FtpReply reply;
			string ftppath = path.GetFtpPath();

			// DELETE CONTENTS OF THE DIRECTORY
			if (deleteContents)
			{
				// 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.
				bool recurse = !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 (recurse)
				{
					itemList = await GetListingAsync(path, options);
				}
				else
				{
					itemList = (await GetListingAsync(path, options)).OrderByDescending(x => x.FullName.Count(c => c.Equals('/'))).ThenBy(x => x.Type).ToArray();
				}

				// delete the item based on the type
				foreach (FtpListItem item in itemList)
				{
					switch (item.Type)
					{
						case FtpFileSystemObjectType.File:
							await DeleteFileAsync(item.FullName);
							break;
						case FtpFileSystemObjectType.Directory:
							await DeleteDirInternalAsync(item.FullName, recurse, options);
							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 = await ExecuteAsync("RMD " + ftppath)).Success)
			{
				throw new FtpCommandException(reply);
			}
		}
#endif

		#endregion

		#region Directory Exists

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The path of the directory</param>
		/// <returns>True if it exists, false otherwise.</returns>
		/// <example><code source="..\Examples\DirectoryExists.cs" lang="cs" /></example>
		public bool DirectoryExists(string path) {
			string pwd;

			// dont verify args as blank/null path is OK
			//if (path.IsBlank())
			//	throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("DirectoryExists", new object[] { path });

			// quickly check if root path, then it always exists!
			string ftppath = path.GetFtpPath();
			if (ftppath == "." || ftppath == "./" || ftppath == "/") {
				return true;
			}

			// check if a folder exists by changing the working dir to it
#if !CORE14
			lock (m_lock) {
#endif
				pwd = GetWorkingDirectory();

				if (Execute("CWD " + ftppath).Success) {
					FtpReply reply = Execute("CWD " + pwd.GetFtpPath());

					if (!reply.Success)
						throw new FtpException("DirectoryExists(): Failed to restore the working directory.");

					return true;
				}
#if !CORE14
			}
#endif

			return false;
		}

#if !CORE
		delegate bool AsyncDirectoryExists(string path);

		/// <summary>
		/// Begins an asynchronous operation to test 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.
		/// </summary>
		/// <returns>IAsyncResult</returns>
		/// <param name='path'>The full or relative path of the directory to check for</param>
		/// <param name='callback'>Async callback</param>
		/// <param name='state'>State object</param>
		/// <example><code source="..\Examples\BeginDirectoryExists.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginDirectoryExists"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from BeginDirectoryExists</param>
		/// <returns>True if the directory exists. False otherwise.</returns>
		/// <example><code source="..\Examples\BeginDirectoryExists.cs" lang="cs" /></example>
		public bool EndDirectoryExists(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncDirectoryExists>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Tests if the specified directory exists on the server asynchronously. 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.
		/// </summary>
		/// <param name='path'>The full or relative path of the directory to check for</param>
		/// <returns>True if the directory exists. False otherwise.</returns>
		public async Task<bool> DirectoryExistsAsync(string path) {
            // TODO: Add cancellation support
            string pwd;

            // dont verify args as blank/null path is OK
            //if (path.IsBlank())
            //	throw new ArgumentException("Required parameter is null or blank.", "path");

            FtpTrace.WriteFunc(nameof(DirectoryExistsAsync), new object[] { path });

            // quickly check if root path, then it always exists!
            string ftppath = path.GetFtpPath();
            if (ftppath == "." || ftppath == "./" || ftppath == "/")
            {
                return true;
            }

            // check if a folder exists by changing the working dir to it
            pwd = await GetWorkingDirectoryAsync();

            if ((await ExecuteAsync("CWD " + ftppath)).Success)
            {
                FtpReply reply = await ExecuteAsync("CWD " + pwd.GetFtpPath());

                if (!reply.Success)
                    throw new FtpException("DirectoryExists(): Failed to restore the working directory.");

                return true;
            }

            return false;
        }
#endif

		#endregion

		#region File Exists

		/// <summary>
		/// Checks if a file exists on the server.
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		/// <returns>True if the file exists</returns>
		/// <example><code source="..\Examples\FileExists.cs" lang="cs" /></example>
		public bool FileExists(string path) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

#if !CORE14
			lock (m_lock) {
#endif

				FtpTrace.WriteFunc("FileExists", new object[] { path });

				// calc the absolute filepath
				path = GetAbsolutePath(path.GetFtpPath());

				// since FTP does not include a specific command to check if a file exists
				// here we check if file exists by attempting to get its filesize (SIZE)
				if (HasFeature(FtpCapability.SIZE)) {
					FtpReply reply = Execute("SIZE " + path);
					char ch = reply.Code[0];
					if (ch == '2') {
						return true;
					}
					if (ch == '5' && IsKnownError(reply.Message, fileNotFoundStrings)) {
						return false;
					}
				}

				// check if file exists by attempting to get its date modified (MDTM)
				if (HasFeature(FtpCapability.MDTM)) {
					FtpReply reply = Execute("MDTM " + path);
					char ch = reply.Code[0];
					if (ch == '2') {
						return true;
					}
					if (ch == '5' && IsKnownError(reply.Message, fileNotFoundStrings)) {
						return false;
					}
				}

				// check if file exists by getting a name listing (NLST)
				string[] fileList = GetNameListing(path.GetFtpDirectoryName());
				string pathName = path.GetFtpFileName();
				if (fileList.Contains(pathName)) {
					return true;
				}

				// check if file exists by attempting to download it (RETR)
				/*try {
					Stream stream = OpenRead(path);
					stream.Close();
					return true;
				} catch (FtpException ex) {
				}*/

				return false;
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate bool AsyncFileExists(string path);

		/// <summary>
		/// Begins an asynchronous operation to check if a file exists on the 
		/// server by taking a  file listing of the parent directory in the path
		/// and comparing the results the path supplied.
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginFileExists.cs" lang="cs" /></example>
		public IAsyncResult BeginFileExists(string path, AsyncCallback callback, object state) {
			AsyncFileExists func;

			IAsyncResult ar = (func = new AsyncFileExists(FileExists)).BeginInvoke(path, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="o:BeginFileExists"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="o:BeginFileExists"/></param>
		/// <returns>True if the file exists, false otherwise</returns>
		/// <example><code source="..\Examples\BeginFileExists.cs" lang="cs" /></example>
		public bool EndFileExists(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncFileExists>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Checks if a file exists on the server asynchronously.
		/// </summary>
		/// <param name="path">The full or relative path to the file</param>
		/// <returns>True if the file exists, false otherwise</returns>
		public async Task<bool> FileExistsAsync(string path)
		{

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(FileExistsAsync), new object[] { path });

			// calc the absolute filepath
			path = await GetAbsolutePathAsync(path.GetFtpPath());

			// since FTP does not include a specific command to check if a file exists
			// here we check if file exists by attempting to get its filesize (SIZE)
			if (HasFeature(FtpCapability.SIZE))
			{
				FtpReply reply = await ExecuteAsync("SIZE " + path);
				char ch = reply.Code[0];
				if (ch == '2')
				{
					return true;
				}
				if (ch == '5' && IsKnownError(reply.Message, fileNotFoundStrings))
				{
					return false;
				}
			}

			// check if file exists by attempting to get its date modified (MDTM)
			if (HasFeature(FtpCapability.MDTM))
			{
				FtpReply reply = await ExecuteAsync("MDTM " + path);
				char ch = reply.Code[0];
				if (ch == '2')
				{
					return true;
				}
				if (ch == '5' && IsKnownError(reply.Message, fileNotFoundStrings))
				{
					return false;
				}
			}

			// check if file exists by getting a name listing (NLST)
			string[] fileList = await GetNameListingAsync(path.GetFtpDirectoryName());
			string pathName = path.GetFtpFileName();
			if (fileList.Contains(pathName))
			{
				return true;
			}

			// check if file exists by attempting to download it (RETR)
			/*try {
				Stream stream = OpenRead(path);
				stream.Close();
				return true;
			} catch (FtpException ex) {
			}*/

			return false;
		}
#endif

		#endregion

		#region Create Directory

		/// <summary>
		/// Creates a directory on the server. If the preceding
		/// directories do not exist, then they are created.
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		/// <example><code source="..\Examples\CreateDirectory.cs" lang="cs" /></example>
		public void CreateDirectory(string path) {
			CreateDirectory(path, true);
		}

		/// <summary>
		/// Creates a directory on the server
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		/// <param name="force">Try to force all non-existent pieces of the path to be created</param>
		/// <example><code source="..\Examples\CreateDirectory.cs" lang="cs" /></example>
		public void CreateDirectory(string path, bool force) {

			// dont verify args as blank/null path is OK
			//if (path.IsBlank())
			//	throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("CreateDirectory", new object[] { path, force });

			FtpReply reply;
			string ftppath = path.GetFtpPath();

			if (ftppath == "." || ftppath == "./" || ftppath == "/")
				return;

#if !CORE14
			lock (m_lock) {
#endif
				path = path.GetFtpPath().TrimEnd('/');

				if (force && !DirectoryExists(path.GetFtpDirectoryName())) {
					FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Create non-existent parent directory: " + path.GetFtpDirectoryName());
					CreateDirectory(path.GetFtpDirectoryName(), true);
				} else if (DirectoryExists(path))
					return;

				FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "CreateDirectory " + ftppath);

				if (!(reply = Execute("MKD " + ftppath)).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate void AsyncCreateDirectory(string path, bool force);

		/// <summary>
		/// Begins an asynchronous operation to create a remote directory. If the preceding
		/// directories do not exist, then they are created.
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginCreateDirectory.cs" lang="cs" /></example>
		public IAsyncResult BeginCreateDirectory(string path, AsyncCallback callback, object state) {

			return BeginCreateDirectory(path, true, callback, state);
		}

		/// <summary>
		/// Begins an asynchronous operation to create a remote directory
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		/// <param name="force">Try to create the whole path if the preceding directories do not exist</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginCreateDirectory.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="o:BeginCreateDirectory"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="o:BeginCreateDirectory"/></param>
		/// <example><code source="..\Examples\BeginCreateDirectory.cs" lang="cs" /></example>
		public void EndCreateDirectory(IAsyncResult ar) {
			GetAsyncDelegate<AsyncCreateDirectory>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Creates a remote directory asynchronously
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		/// <param name="force">Try to create the whole path if the preceding directories do not exist</param>
		public async Task CreateDirectoryAsync(string path, bool force)
		{
			// dont verify args as blank/null path is OK
			//if (path.IsBlank())
			//	throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(CreateDirectoryAsync), new object[] { path, force });

			FtpReply reply;
			string ftppath = path.GetFtpPath();

			if (ftppath == "." || ftppath == "./" || ftppath == "/")
				return;

			path = path.GetFtpPath().TrimEnd('/');

			if (force && !await DirectoryExistsAsync(path.GetFtpDirectoryName()))
			{
				FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Create non-existent parent directory: " + path.GetFtpDirectoryName());
				await CreateDirectoryAsync(path.GetFtpDirectoryName(), true);
			}
			else if (await DirectoryExistsAsync(path))
				return;

			FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "CreateDirectory " + ftppath);

			if (!(reply = await ExecuteAsync("MKD " + ftppath)).Success)
				throw new FtpCommandException(reply);
		}

		/// <summary>
		/// Creates a remote directory asynchronously. If the preceding
		/// directories do not exist, then they are created.
		/// </summary>
		/// <param name="path">The full or relative path to the new remote directory</param>
		public Task CreateDirectoryAsync(string path) {
			return CreateDirectoryAsync(path, true);
		}
#endif

		#endregion

		#region Rename File/Directory

		/// <summary>
		/// Renames an object on the remote file system.
		/// Low level method that should NOT be used in most cases. Prefer MoveFile() and MoveDirectory().
		/// Throws exceptions if the file does not exist, or if the destination file already exists.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <example><code source="..\Examples\Rename.cs" lang="cs" /></example>
		public void Rename(string path, string dest) {
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

#if !CORE14
			lock (m_lock) {
#endif
				FtpTrace.WriteFunc("Rename", new object[] { path, dest });

				// calc the absolute filepaths
				path = GetAbsolutePath(path.GetFtpPath());
				dest = GetAbsolutePath(dest.GetFtpPath());

				if (!(reply = Execute("RNFR " + path)).Success)
					throw new FtpCommandException(reply);

				if (!(reply = Execute("RNTO " + dest)).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate void AsyncRename(string path, string dest);

		/// <summary>
		/// Begins an asynchronous operation to rename an object on the remote file system.
		/// Low level method that should NOT be used in most cases. Prefer MoveFile() and MoveDirectory().
		/// Throws exceptions if the file does not exist, or if the destination file already exists.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginRename.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginRename"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginRename"/></param>
		/// <example><code source="..\Examples\BeginRename.cs" lang="cs" /></example>
		public void EndRename(IAsyncResult ar) {
			GetAsyncDelegate<AsyncRename>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Renames an object on the remote file system asynchronously.
		/// Low level method that should NOT be used in most cases. Prefer MoveFile() and MoveDirectory().
		/// Throws exceptions if the file does not exist, or if the destination file already exists.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		public async Task RenameAsync(string path, string dest)
		{
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

			FtpTrace.WriteFunc(nameof(RenameAsync), new object[] { path, dest });

			// calc the absolute filepaths
			path = await GetAbsolutePathAsync(path.GetFtpPath());
			dest = await GetAbsolutePathAsync(dest.GetFtpPath());

			if (!(reply = await ExecuteAsync("RNFR " + path)).Success)
				throw new FtpCommandException(reply);

			if (!(reply = await ExecuteAsync("RNTO " + dest)).Success)
				throw new FtpCommandException(reply);
		}
#endif

		#endregion

		#region Move File

		/// <summary>
		/// Moves a file on the remote file system from one directory to another.
		/// Always checks if the source file exists. Checks if the dest file exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest file exists? And if it does should we overwrite/skip the operation?</param>
		/// <returns>Whether the file was moved</returns>
		public bool MoveFile(string path, string dest, FtpExists existsMode = FtpExists.Overwrite) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

			FtpTrace.WriteFunc("MoveFile", new object[] { path, dest, existsMode });

			if (FileExists(path)) {

				// check if dest file exists and act accordingly
				if (existsMode != FtpExists.NoCheck) {
					bool destExists = FileExists(dest);
					if (destExists) {
						switch (existsMode) {
							case FtpExists.Overwrite:
								DeleteFile(dest);
								break;
							case FtpExists.Skip:
								return false;
						}
					}
				}

				// move the file
				Rename(path, dest);

				return true;
			}
			return false;
		}

#if !CORE
		delegate bool AsyncMoveFile(string path, string dest, FtpExists existsMode);

		/// <summary>
		/// Begins an asynchronous operation to move a file on the remote file system, from one directory to another.
		/// Always checks if the source file exists. Checks if the dest file exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest file exists? And if it does should we overwrite/skip the operation?</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		public IAsyncResult BeginMoveFile(string path, string dest, FtpExists existsMode, AsyncCallback callback, object state) {
			AsyncMoveFile func;
			IAsyncResult ar;

			ar = (func = new AsyncMoveFile(MoveFile)).BeginInvoke(path, dest, existsMode, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginMoveFile"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginMoveFile"/></param>
		public void EndMoveFile(IAsyncResult ar) {
			GetAsyncDelegate<AsyncMoveFile>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC

		/// <summary>
		/// Moves a file asynchronously on the remote file system from one directory to another.
		/// Always checks if the source file exists. Checks if the dest file exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest file exists? And if it does should we overwrite/skip the operation?</param>
		/// <returns>Whether the file was moved</returns>
		public async Task<bool> MoveFileAsync(string path, string dest, FtpExists existsMode = FtpExists.Overwrite) {
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

			FtpTrace.WriteFunc(nameof(MoveFileAsync), new object[] { path, dest, existsMode });

			if (await FileExistsAsync(path))
			{

				// check if dest file exists and act accordingly
				if (existsMode != FtpExists.NoCheck)
				{
					bool destExists = await FileExistsAsync(dest);
					if (destExists)
					{
						switch (existsMode)
						{
							case FtpExists.Overwrite:
								await DeleteFileAsync(dest);
								break;
							case FtpExists.Skip:
								return false;
						}
					}
				}

				// move the file
				await RenameAsync(path, dest);

				return true;
			}
			return false;
		}
#endif

		#endregion

		#region Move Directory

		/// <summary>
		/// Moves a directory on the remote file system from one directory to another.
		/// Always checks if the source directory exists. Checks if the dest directory exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest directory exists? And if it does should we overwrite/skip the operation?</param>
		/// <returns>Whether the directory was moved</returns>
		public bool MoveDirectory(string path, string dest, FtpExists existsMode = FtpExists.Overwrite) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

			FtpTrace.WriteFunc("MoveDirectory", new object[] { path, dest, existsMode });

			if (DirectoryExists(path)) {

				// check if dest directory exists and act accordingly
				if (existsMode != FtpExists.NoCheck) {
					bool destExists = DirectoryExists(dest);
					if (destExists) {
						switch (existsMode) {
							case FtpExists.Overwrite:
								DeleteDirectory(dest);
								break;
							case FtpExists.Skip:
								return false;
						}
					}
				}

				// move the directory
				Rename(path, dest);

				return true;
			}
			return false;
		}

#if !CORE
		delegate bool AsyncMoveDirectory(string path, string dest, FtpExists existsMode);

		/// <summary>
		/// Begins an asynchronous operation to move a directory on the remote file system, from one directory to another.
		/// Always checks if the source directory exists. Checks if the dest directory exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest directory exists? And if it does should we overwrite/skip the operation?</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		public IAsyncResult BeginMoveDirectory(string path, string dest, FtpExists existsMode, AsyncCallback callback, object state) {
			AsyncMoveDirectory func;
			IAsyncResult ar;

			ar = (func = new AsyncMoveDirectory(MoveDirectory)).BeginInvoke(path, dest, existsMode, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginMoveDirectory"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginMoveDirectory"/></param>
		public void EndMoveDirectory(IAsyncResult ar) {
			GetAsyncDelegate<AsyncMoveDirectory>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Moves a directory asynchronously on the remote file system from one directory to another.
		/// Always checks if the source directory exists. Checks if the dest directory exists based on the `existsMode` parameter.
		/// Only throws exceptions for critical errors.
		/// </summary>
		/// <param name="path">The full or relative path to the object</param>
		/// <param name="dest">The new full or relative path including the new name of the object</param>
		/// <param name="existsMode">Should we check if the dest directory exists? And if it does should we overwrite/skip the operation?</param>
		/// <returns>Whether the directory was moved</returns>
		public async Task<bool> MoveDirectoryAsync(string path, string dest, FtpExists existsMode = FtpExists.Overwrite) {
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (dest.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "dest");

			FtpTrace.WriteFunc(nameof(MoveDirectoryAsync), new object[] { path, dest, existsMode });

			if (await DirectoryExistsAsync(path))
			{

				// check if dest directory exists and act accordingly
				if (existsMode != FtpExists.NoCheck)
				{
					bool destExists = await DirectoryExistsAsync(dest);
					if (destExists)
					{
						switch (existsMode)
						{
							case FtpExists.Overwrite:
								await DeleteDirectoryAsync(dest);
								break;
							case FtpExists.Skip:
								return false;
						}
					}
				}

				// move the directory
				await RenameAsync(path, dest);

				return true;
			}
			return false;

		}
#endif

		#endregion

		#region File Permissions / Chmod

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="permissions">The permissions in CHMOD format</param>
		public void SetFilePermissions(string path, int permissions) {
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

#if !CORE14
			lock (m_lock) {
#endif
				FtpTrace.WriteFunc("SetFilePermissions", new object[] { path, permissions });

				if (!(reply = Execute("SITE CHMOD " + permissions.ToString() + " " + path.GetFtpPath())).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="permissions">The permissions in CHMOD format</param>
		public async Task SetFilePermissionsAsync(string path, int permissions)
		{
			FtpReply reply;

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(SetFilePermissionsAsync), new object[] { path, permissions });

			if (!(reply = await ExecuteAsync("SITE CHMOD " + permissions.ToString() + " " + path.GetFtpPath())).Success)
				throw new FtpCommandException(reply);
		}
#endif

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="permissions">The permissions in CHMOD format</param>
		public void Chmod(string path, int permissions) {
			SetFilePermissions(path, permissions);
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="permissions">The permissions in CHMOD format</param>
		public Task ChmodAsync(string path, int permissions)
		{
			return SetFilePermissionsAsync(path, permissions);
		}
#endif

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="owner">The owner permissions</param>
		/// <param name="group">The group permissions</param>
		/// <param name="other">The other permissions</param>
		public void SetFilePermissions(string path, FtpPermission owner, FtpPermission group, FtpPermission other) {
			SetFilePermissions(path, CalcChmod(owner, group, other));
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="owner">The owner permissions</param>
		/// <param name="group">The group permissions</param>
		/// <param name="other">The other permissions</param>
		public Task SetFilePermissionsAsync(string path, FtpPermission owner, FtpPermission group, FtpPermission other)
		{
			return SetFilePermissionsAsync(path, CalcChmod(owner, group, other));
		}
#endif

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="owner">The owner permissions</param>
		/// <param name="group">The group permissions</param>
		/// <param name="other">The other permissions</param>
		public void Chmod(string path, FtpPermission owner, FtpPermission group, FtpPermission other) {
			SetFilePermissions(path, owner, group, other);
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		/// <param name="owner">The owner permissions</param>
		/// <param name="group">The group permissions</param>
		/// <param name="other">The other permissions</param>
		public Task ChmodAsync(string path, FtpPermission owner, FtpPermission group, FtpPermission other)
		{
			return SetFilePermissionsAsync(path, owner, group, other);
		}
#endif

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		public FtpListItem GetFilePermissions(string path) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("GetFilePermissions", new object[] { path });

			string fullPath = path.GetFtpPath();
			foreach (FtpListItem i in GetListing(path)) {
				if (i.FullName == fullPath) {
					return i;
				}
			}
			return null;
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		public async Task<FtpListItem> GetFilePermissionsAsync(string path)
		{
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc(nameof(GetFilePermissionsAsync), new object[] { path });

			string fullPath = path.GetFtpPath();
			foreach (FtpListItem i in await GetListingAsync(path))
			{
				if (i.FullName == fullPath)
				{
					return i;
				}
			}
			return null;
		}
#endif

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		public int GetChmod(string path) {
			FtpListItem item = GetFilePermissions(path);
			return item != null ? item.Chmod : 0;
		}

#if ASYNC
		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="path">The full or relative path to the item</param>
		public async Task<int> GetChmodAsync(string path)
		{
			FtpListItem item = await GetFilePermissionsAsync(path);
			return item != null ? item.Chmod : 0;
		}
#endif

		#endregion

		#region Dereference Link

		/// <summary>
		/// Recursively dereferences a symbolic link. See the
		/// MaximumDereferenceCount property for controlling
		/// how deep this method will recurse before giving up.
		/// </summary>
		/// <param name="item">The symbolic link</param>
		/// <returns>FtpListItem, null if the link can't be dereferenced</returns>
		/// <example><code source="..\Examples\DereferenceLink.cs" lang="cs" /></example>
		public FtpListItem DereferenceLink(FtpListItem item) {
			return DereferenceLink(item, MaximumDereferenceCount);
		}

		/// <summary>
		/// Recursively dereferences a symbolic link
		/// </summary>
		/// <param name="item">The symbolic link</param>
		/// <param name="recMax">The maximum depth of recursion that can be performed before giving up.</param>
		/// <returns>FtpListItem, null if the link can't be dereferenced</returns>
		/// <example><code source="..\Examples\DereferenceLink.cs" lang="cs" /></example>
		public FtpListItem DereferenceLink(FtpListItem item, int recMax) {

			FtpTrace.WriteFunc("DereferenceLink", new object[] { item.FullName, recMax });

			int count = 0;
			return DereferenceLink(item, recMax, ref count);
		}

		/// <summary>
		/// Derefence a FtpListItem object
		/// </summary>
		/// <param name="item">The item to derefence</param>
		/// <param name="recMax">Maximum recursive calls</param>
		/// <param name="count">Counter</param>
		/// <returns>FtpListItem, null if the link can't be dereferenced</returns>
		/// <example><code source="..\Examples\DereferenceLink.cs" lang="cs" /></example>
		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;
		}

#if !CORE
		delegate FtpListItem AsyncDereferenceLink(FtpListItem item, int recMax);

		/// <summary>
		/// Begins an asynchronous operation to dereference a <see cref="FtpListItem"/> object
		/// </summary>
		/// <param name="item">The item to dereference</param>
		/// <param name="recMax">Maximum recursive calls</param>
		/// <param name="callback">AsyncCallback</param>
		/// <param name="state">State Object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginDereferenceLink.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Begins an asynchronous operation to dereference a <see cref="FtpListItem"/> object. See the
		/// <see cref="MaximumDereferenceCount"/> property for controlling
		/// how deep this method will recurse before giving up.
		/// </summary>
		/// <param name="item">The item to dereference</param>
		/// <param name="callback">AsyncCallback</param>
		/// <param name="state">State Object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginDereferenceLink.cs" lang="cs" /></example>
		public IAsyncResult BeginDereferenceLink(FtpListItem item, AsyncCallback callback, object state) {
			return BeginDereferenceLink(item, MaximumDereferenceCount, callback, state);
		}

		/// <summary>
		/// Ends a call to <see cref="o:BeginDereferenceLink"/>
		/// </summary>
		/// <param name="ar">IAsyncResult</param>
		/// <returns>A <see cref="FtpListItem"/>, or null if the link can't be dereferenced</returns>
		/// <example><code source="..\Examples\BeginDereferenceLink.cs" lang="cs" /></example>
		public FtpListItem EndDereferenceLink(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncDereferenceLink>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
        /// <summary>
        /// Derefence a FtpListItem object
        /// </summary>
        /// <param name="item">The item to derefence</param>
        /// <param name="recMax">Maximum recursive calls</param>
        /// <param name="count">Counter</param>
        /// <returns>FtpListItem, null if the link can't be dereferenced</returns>
        async Task<FtpListItem> DereferenceLinkAsync(FtpListItem item, int recMax, IntRef 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.Value == recMax)
                            return null;

                        return await DereferenceLinkAsync(obj, recMax, 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;
        }

        /// <summary>
        /// Dereference a <see cref="FtpListItem"/> object asynchronously
        /// </summary>
        /// <param name="item">The item to dereference</param>
        /// <param name="recMax">Maximum recursive calls</param>
        /// <returns>FtpListItem, null if the link can't be dereferenced</returns>
        public Task<FtpListItem> DereferenceLinkAsync(FtpListItem item, int recMax) {
            //TODO:  Add cancellation support
            FtpTrace.WriteFunc(nameof(DereferenceLinkAsync), new object[] { item.FullName, recMax });

            IntRef count = new IntRef { Value = 0 };
            return DereferenceLinkAsync(item, recMax, count);
		}

		/// <summary>
		/// Dereference a <see cref="FtpListItem"/> object asynchronously
		/// </summary>
		/// <param name="item">The item to dereference</param>
		/// <returns>FtpListItem, null if the link can't be dereferenced</returns>
		public Task<FtpListItem> DereferenceLinkAsync(FtpListItem item) {
            //TODO:  Add cancellation support
            return DereferenceLinkAsync(item, MaximumDereferenceCount);
        }
#endif

		#endregion

		#region Set Working Dir

		/// <summary>
		/// Sets the work directory on the server
		/// </summary>
		/// <param name="path">The path of the directory to change to</param>
		/// <example><code source="..\Examples\SetWorkingDirectory.cs" lang="cs" /></example>
		public void SetWorkingDirectory(string path) {

			FtpTrace.WriteFunc("SetWorkingDirectory", new object[] { path });

			FtpReply reply;
			string ftppath = path.GetFtpPath();

			if (ftppath == "." || ftppath == "./")
				return;

#if !CORE14
			lock (m_lock) {
#endif
				if (!(reply = Execute("CWD " + ftppath)).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate void AsyncSetWorkingDirectory(string path);

		/// <summary>
		/// Begins an asynchronous operation to set the working directory on the server
		/// </summary>
		/// <param name="path">The directory to change to</param>
		/// <param name="callback">Async Callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginSetWorkingDirectory.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginSetWorkingDirectory"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginSetWorkingDirectory"/></param>
		/// <example><code source="..\Examples\BeginSetWorkingDirectory.cs" lang="cs" /></example>
		public void EndSetWorkingDirectory(IAsyncResult ar) {
			GetAsyncDelegate<AsyncSetWorkingDirectory>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Sets the working directory on the server asynchronously
		/// </summary>
		/// <param name="path">The directory to change to</param>
		public async Task SetWorkingDirectoryAsync(string path)
		{
			//TODO:  Add cancellation support

			FtpTrace.WriteFunc(nameof(SetWorkingDirectoryAsync), new object[] { path });

			FtpReply reply;
			string ftppath = path.GetFtpPath();

			if (ftppath == "." || ftppath == "./")
				return;

			if (!(reply = await ExecuteAsync("CWD " + ftppath)).Success)
				throw new FtpCommandException(reply);
		}
#endif
		#endregion

		#region Get Working Dir

		/// <summary>
		/// Gets the current working directory
		/// </summary>
		/// <returns>The current working directory, ./ if the response couldn't be parsed.</returns>
		/// <example><code source="..\Examples\GetWorkingDirectory.cs" lang="cs" /></example>
		public string GetWorkingDirectory() {

			FtpTrace.WriteFunc("GetWorkingDirectory");

			FtpReply reply;
			Match m;

#if !CORE14
			lock (m_lock) {
#endif
				if (!(reply = Execute("PWD")).Success)
					throw new FtpCommandException(reply);
#if !CORE14
			}
#endif

			if ((m = Regex.Match(reply.Message, "\"(?<pwd>.*)\"")).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 = (?<pwd>.*)")).Success) {
				return m.Groups["pwd"].Value;
			}

			FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Failed to parse working directory from: " + reply.Message);

			return "./";
		}

#if !CORE
		delegate string AsyncGetWorkingDirectory();

		/// <summary>
		/// Begins an asynchronous operation to get the working directory
		/// </summary>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginGetWorkingDirectory.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginGetWorkingDirectory"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginGetWorkingDirectory"/></param>
		/// <returns>The current working directory</returns>
		/// <example><code source="..\Examples\BeginGetWorkingDirectory.cs" lang="cs" /></example>
		public string EndGetWorkingDirectory(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncGetWorkingDirectory>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Gets the current working directory asynchronously
		/// </summary>
		/// <returns>The current working directory, ./ if the response couldn't be parsed.</returns>
		public async Task<string> GetWorkingDirectoryAsync() {
            //TODO:  Add cancellation support
            FtpTrace.WriteFunc(nameof(GetWorkingDirectoryAsync));

            FtpReply reply;
            Match m;

            if (!(reply = await ExecuteAsync("PWD")).Success)
                throw new FtpCommandException(reply);

            if ((m = Regex.Match(reply.Message, "\"(?<pwd>.*)\"")).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 = (?<pwd>.*)")).Success)
            {
                return m.Groups["pwd"].Value;
            }

            FtpTrace.WriteStatus(FtpTraceLevel.Warn, "Failed to parse working directory from: " + reply.Message);

            return "./";
        }
#endif
		#endregion

		#region Get File Size

		/// <summary>
		/// Gets the size of a remote file
		/// </summary>
		/// <param name="path">The full or relative path of the file</param>
		/// <returns>-1 if the command fails, otherwise the file size</returns>
		/// <example><code source="..\Examples\GetFileSize.cs" lang="cs" /></example>
		public virtual long GetFileSize(string path) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("GetFileSize", new object[] { path });

			FtpReply reply;
			long length = 0;

#if !CORE14
			lock (m_lock) {
#endif
				// Switch to binary mode since some servers don't support SIZE command for ASCII files.
				// 
				// NOTE: We do this inside the lock so we're guaranteed to switch it back to the original
				// type in a thread-safe manner
				var savedDataType = CurrentDataType;
				if (savedDataType != FtpDataType.Binary) {
					this.SetDataTypeInternal(FtpDataType.Binary);
				}

				if (!(reply = Execute("SIZE " + path.GetFtpPath())).Success)
					length = -1;
				else if (!long.TryParse(reply.Message, out length))
					length = -1;

				if (savedDataType != FtpDataType.Binary)
					this.SetDataTypeInternal(savedDataType);
#if !CORE14
			}
#endif


			return length;
		}

#if !CORE
		delegate long AsyncGetFileSize(string path);

		/// <summary>
		/// Begins an asynchronous operation to retrieve the size of a remote file
		/// </summary>
		/// <param name="path">The full or relative path of the file</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginGetFileSize.cs" lang="cs" /></example>
		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;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginGetFileSize"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginGetFileSize"/></param>
		/// <returns>The size of the file, -1 if there was a problem.</returns>
		/// <example><code source="..\Examples\BeginGetFileSize.cs" lang="cs" /></example>
		public long EndGetFileSize(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncGetFileSize>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Retrieve the size of a remote file asynchronously
		/// </summary>
		/// <param name="path">The full or relative path of the file</param>
		/// <returns>The size of the file, -1 if there was a problem.</returns>
		public async Task<long> GetFileSizeAsync(string path) {
            //TODO:  Add cancellation support

            // verify args
            if (path.IsBlank())
                throw new ArgumentException("Required parameter is null or blank.", "path");

            FtpTrace.WriteFunc(nameof(GetFileSizeAsync), new object[] { path });

            FtpReply reply;
            long length = 0;

            // Switch to binary mode since some servers don't support SIZE command for ASCII files.
            // 
            var savedDataType = CurrentDataType;
            if (savedDataType != FtpDataType.Binary)
            {
                await this.SetDataTypeAsync(FtpDataType.Binary);
            }

            if (!(reply = await ExecuteAsync("SIZE " + path.GetFtpPath())).Success)
                length = -1;
            else if (!long.TryParse(reply.Message, out length))
                length = -1;

            if (savedDataType != FtpDataType.Binary)
                await this.SetDataTypeAsync(savedDataType);

            return length;
        }
#endif
		#endregion

		#region Get Modified Time

		/// <summary>
		/// Gets the modified time of a remote file
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="type">Return the date in local timezone or UTC?  Use FtpDate.Original to disable timezone conversion.</param>
		/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
		/// <example><code source="..\Examples\GetModifiedTime.cs" lang="cs" /></example>
		public virtual DateTime GetModifiedTime(string path, FtpDate type = FtpDate.Original) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");

			FtpTrace.WriteFunc("GetModifiedTime", new object[] { path, type });

			DateTime date = DateTime.MinValue;
			FtpReply reply;

#if !CORE14
			lock (m_lock) {
#endif
				// get modified date of a file
				if ((reply = Execute("MDTM " + path.GetFtpPath())).Success) {
					date = reply.Message.GetFtpDate(DateTimeStyles.AssumeUniversal);

					// convert server timezone to UTC, based on the TimeOffset property
					if (type != FtpDate.Original && m_listParser.hasTimeOffset) {
						date = (date - m_listParser.timeOffset);
					}

					// convert to local time if wanted
#if !CORE
					if (type == FtpDate.Local) {
						date = TimeZone.CurrentTimeZone.ToLocalTime(date);
					}
#endif
				}
#if !CORE14
			}
#endif

			return date;
		}

#if !CORE
		delegate DateTime AsyncGetModifiedTime(string path, FtpDate type);

		/// <summary>
		/// Begins an asynchronous operation to get the modified time of a remote file
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="type">Return the date in local timezone or UTC?  Use FtpDate.Original to disable timezone conversion.</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		/// <example><code source="..\Examples\BeginGetModifiedTime.cs" lang="cs" /></example>
		public IAsyncResult BeginGetModifiedTime(string path, FtpDate type, AsyncCallback callback, object state) {
			IAsyncResult ar;
			AsyncGetModifiedTime func;

			ar = (func = new AsyncGetModifiedTime(GetModifiedTime)).BeginInvoke(path, type, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginGetModifiedTime"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginGetModifiedTime"/></param>
		/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
		/// <example><code source="..\Examples\BeginGetModifiedTime.cs" lang="cs" /></example>
		public DateTime EndGetModifiedTime(IAsyncResult ar) {
			return GetAsyncDelegate<AsyncGetModifiedTime>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Gets the modified time of a remote file asynchronously
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="type">Return the date in local timezone or UTC?  Use FtpDate.Original to disable timezone conversion.</param>
		/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
		public async Task<DateTime> GetModifiedTimeAsync(string path, FtpDate type = FtpDate.Original) {
            //TODO:  Add cancellation support
            // verify args
            if (path.IsBlank())
                throw new ArgumentException("Required parameter is null or blank.", "path");

            FtpTrace.WriteFunc(nameof(GetModifiedTimeAsync), new object[] { path, type });

            DateTime date = DateTime.MinValue;
            FtpReply reply;

            // get modified date of a file
            if ((reply = await ExecuteAsync("MDTM " + path.GetFtpPath())).Success)
            {
                date = reply.Message.GetFtpDate(DateTimeStyles.AssumeUniversal);

                // convert server timezone to UTC, based on the TimeOffset property
                if (type != FtpDate.Original && m_listParser.hasTimeOffset)
                {
                    date = (date - m_listParser.timeOffset);
                }

                // convert to local time if wanted
#if !CORE
				if (type == FtpDate.Local) {
					date = TimeZone.CurrentTimeZone.ToLocalTime(date);
				}
#endif
            }

            return date;
        }
#endif

		#endregion

		#region Set Modified Time

		/// <summary>
		/// Changes the modified time of a remote file
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="date">The new modified date/time value</param>
		/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
		public virtual void SetModifiedTime(string path, DateTime date, FtpDate type = FtpDate.Original) {

			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (date == null)
				throw new ArgumentException("Required parameter is null or blank.", "date");

			FtpTrace.WriteFunc("SetModifiedTime", new object[] { path, date, type });

			FtpReply reply;

#if !CORE14
			lock (m_lock) {
#endif

				// convert local to UTC if wanted
#if !CORE
				if (type == FtpDate.Local) {
					date = TimeZone.CurrentTimeZone.ToUniversalTime(date);
				}
#endif

				// convert UTC to server timezone, based on the TimeOffset property
				if (type != FtpDate.Original && m_listParser.hasTimeOffset) {
					date = (date + m_listParser.timeOffset);
				}

				// set modified date of a file
				string timeStr = date.ToString("yyyyMMddHHmmss");
				if ((reply = Execute("MFMT " + timeStr + " " + path.GetFtpPath())).Success) {

				}
#if !CORE14
			}
#endif
		}

#if !CORE
		delegate void AsyncSetModifiedTime(string path, DateTime date, FtpDate type);

		/// <summary>
		/// Begins an asynchronous operation to get the modified time of a remote file
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="date">The new modified date/time value</param>
		/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
		/// <param name="callback">Async callback</param>
		/// <param name="state">State object</param>
		/// <returns>IAsyncResult</returns>
		public IAsyncResult BeginSetModifiedTime(string path, DateTime date, FtpDate type, AsyncCallback callback, object state) {
			IAsyncResult ar;
			AsyncSetModifiedTime func;

			ar = (func = new AsyncSetModifiedTime(SetModifiedTime)).BeginInvoke(path, date, type, callback, state);
			lock (m_asyncmethods) {
				m_asyncmethods.Add(ar, func);
			}

			return ar;
		}

		/// <summary>
		/// Ends a call to <see cref="BeginSetModifiedTime"/>
		/// </summary>
		/// <param name="ar">IAsyncResult returned from <see cref="BeginSetModifiedTime"/></param>
		/// <returns>The modified time, or <see cref="DateTime.MinValue"/> if there was a problem</returns>
		public void EndSetModifiedTime(IAsyncResult ar) {
			GetAsyncDelegate<AsyncSetModifiedTime>(ar).EndInvoke(ar);
		}

#endif
#if ASYNC
		/// <summary>
		/// Gets the modified time of a remote file asynchronously
		/// </summary>
		/// <param name="path">The full path to the file</param>
		/// <param name="date">The new modified date/time value</param>
		/// <param name="type">Is the date provided in local timezone or UTC? Use FtpDate.Original to disable timezone conversion.</param>
		public async Task SetModifiedTimeAsync(string path, DateTime date, FtpDate type = FtpDate.Original)
		{
			//TODO:  Add cancellation support
			// verify args
			if (path.IsBlank())
				throw new ArgumentException("Required parameter is null or blank.", "path");
			if (date == null)
				throw new ArgumentException("Required parameter is null or blank.", "date");

			FtpTrace.WriteFunc(nameof(SetModifiedTimeAsync), new object[] { path, date, type });

			FtpReply reply;

			// convert local to UTC if wanted
#if !CORE
			if (type == FtpDate.Local) {
				date = TimeZone.CurrentTimeZone.ToUniversalTime(date);
			}
#endif

			// convert UTC to server timezone, based on the TimeOffset property
			if (type != FtpDate.Original && m_listParser.hasTimeOffset)
			{
				date = (date + m_listParser.timeOffset);
			}

			// set modified date of a file
			string timeStr = date.ToString("yyyyMMddHHmmss");
			if ((reply = await ExecuteAsync("MFMT " + timeStr + " " + path.GetFtpPath())).Success)
			{

			}
		}
#endif

		#endregion

	}
}