Files
NewsCrawler/FluentFTP/Client/FtpClient_HighLevel.cs
2018-01-02 01:11:53 +09:00

2162 lines
96 KiB
C#

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;
using System.Threading;
#if !CORE
using System.Web;
#endif
#if (CORE || NETFX)
using System.Threading;
#endif
#if (CORE || NET45)
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 : IDisposable {
#region Properties
private int m_transferChunkSize = 65536;
/// <summary>
/// Gets or sets the number of bytes transferred in a single chunk (a single FTP command).
/// Used by <see cref="o:UploadFile"/>/<see cref="o:UploadFileAsync"/> and <see cref="o:DownloadFile"/>/<see cref="o:DownloadFileAsync"/>
/// to transfer large files in multiple chunks.
/// </summary>
public int TransferChunkSize {
get {
return m_transferChunkSize;
}
set {
m_transferChunkSize = value;
}
}
private FtpDataType CurrentDataType;
private int m_retryAttempts = 3;
/// <summary>
/// Gets or sets the retry attempts allowed when a verification failure occurs during download or upload.
/// This value must be set to 1 or more.
/// </summary>
public int RetryAttempts {
get { return m_retryAttempts; }
set { m_retryAttempts = value > 0 ? value : 1; }
}
uint m_uploadRateLimit = 0;
/// <summary>
/// Rate limit for uploads in kbyte/s. Set this to 0 for unlimited speed.
/// Honored by high-level API such as Upload(), Download(), UploadFile(), DownloadFile()..
/// </summary>
public uint UploadRateLimit {
get { return m_uploadRateLimit; }
set { m_uploadRateLimit = value; }
}
uint m_downloadRateLimit = 0;
/// <summary>
/// Rate limit for downloads in kbytes/s. Set this to 0 for unlimited speed.
/// Honored by high-level API such as Upload(), Download(), UploadFile(), DownloadFile()..
/// </summary>
public uint DownloadRateLimit {
get { return m_downloadRateLimit; }
set { m_downloadRateLimit = value; }
}
public FtpDataType m_UploadDataType = FtpDataType.Binary;
/// <summary>
/// Controls if the high-level API uploads files in Binary or ASCII mode.
/// </summary>
public FtpDataType UploadDataType {
get { return m_UploadDataType; }
set { m_UploadDataType = value; }
}
public FtpDataType m_DownloadDataType = FtpDataType.Binary;
/// <summary>
/// Controls if the high-level API downloads files in Binary or ASCII mode.
/// </summary>
public FtpDataType DownloadDataType {
get { return m_DownloadDataType; }
set { m_DownloadDataType = value; }
}
// ADD PROPERTIES THAT NEED TO BE CLONED INTO
// FtpClient.CloneConnection()
#endregion
#region Upload Multiple Files
/// <summary>
/// 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 <see cref="o:UploadFile"/> since it performs a single "file exists" check rather than one check per file.
/// </summary>
/// <param name="localPaths">The full or relative paths to the files on the local file system. Files can be from multiple folders.</param>
/// <param name="remoteDir">The full or relative path to the directory that files will be uploaded on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance,
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were uploaded successfully. Affected when files are skipped when they already exist.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public int UploadFiles(IEnumerable<string> localPaths, string remoteDir, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = true,
FtpVerify verifyOptions = FtpVerify.None, FtpError errorHandling = FtpError.None) {
// verify args
if (!errorHandling.IsValidCombination())
throw new ArgumentException("Invalid combination of FtpError flags. Throw & Stop cannot be combined");
if (remoteDir.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remoteDir");
FtpTrace.WriteFunc("UploadFiles", new object[] { localPaths, remoteDir, existsMode, createRemoteDir, verifyOptions, errorHandling });
//int count = 0;
bool errorEncountered = false;
List<string> successfulUploads = new List<string>();
// ensure ends with slash
remoteDir = !remoteDir.EndsWith("/") ? remoteDir + "/" : remoteDir;
//flag to determine if existence checks are required
bool checkFileExistence = true;
// create remote dir if wanted
if (createRemoteDir) {
if (!DirectoryExists(remoteDir)) {
CreateDirectory(remoteDir);
checkFileExistence = false;
}
}
// get all the already existing files
string[] existingFiles = checkFileExistence ? GetNameListing(remoteDir) : new string[0];
// per local file
foreach (string localPath in localPaths) {
// calc remote path
string fileName = Path.GetFileName(localPath);
string remotePath = remoteDir + fileName;
// try to upload it
try {
bool ok = UploadFileFromFile(localPath, remotePath, false, existsMode, existingFiles.Contains(fileName), true, verifyOptions, null);
if (ok) {
successfulUploads.Add(remotePath);
//count++;
} else if ((int)errorHandling > 1) {
errorEncountered = true;
break;
}
} catch (Exception ex) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Upload Failure for " + localPath + ": " + ex);
if (errorHandling.HasFlag(FtpError.Stop)) {
errorEncountered = true;
break;
}
if (errorHandling.HasFlag(FtpError.Throw)) {
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulUploads(successfulUploads);
}
throw new FtpException("An error occurred uploading file(s). See inner exception for more info.", ex);
}
}
}
if (errorEncountered) {
//Delete any successful uploads if needed
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulUploads(successfulUploads);
successfulUploads.Clear(); //forces return of 0
}
//Throw generic error because requested
if (errorHandling.HasFlag(FtpError.Throw)) {
throw new FtpException("An error occurred uploading one or more files. Refer to trace output if available.");
}
}
return successfulUploads.Count;
}
private void PurgeSuccessfulUploads(IEnumerable<string> remotePaths) {
foreach (string remotePath in remotePaths) {
this.DeleteFile(remotePath);
}
}
/// <summary>
/// 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 <see cref="o:UploadFile"/> since it performs a single "file exists" check rather than one check per file.
/// </summary>
/// <param name="localFiles">Files to be uploaded</param>
/// <param name="remoteDir">The full or relative path to the directory that files will be uploaded on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to FtpExists.None for fastest performance but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were downloaded successfully. When existing files are skipped, they are not counted.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public int UploadFiles(IEnumerable<FileInfo> localFiles, string remoteDir, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = true,
FtpVerify verifyOptions = FtpVerify.None, FtpError errorHandling = FtpError.None) {
return UploadFiles(localFiles.Select(f => f.FullName), remoteDir, existsMode, createRemoteDir, verifyOptions, errorHandling);
}
#if ASYNC
/// <summary>
/// Uploads the given file paths to a single folder on the server asynchronously.
/// 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 <see cref="o:UploadFile"/> since it performs a single "file exists" check rather than one check per file.
/// </summary>
/// <param name="localPaths">The full or relative paths to the files on the local file system. Files can be from multiple folders.</param>
/// <param name="remoteDir">The full or relative path to the directory that files will be uploaded on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to FtpExists.None for fastest performance but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful upload and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <param name="token">The token to monitor for cancellation requests</param>
/// <returns>The count of how many files were uploaded successfully. Affected when files are skipped when they already exist.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public async Task<int> UploadFilesAsync(IEnumerable<string> localPaths, string remoteDir, FtpExists existsMode, bool createRemoteDir, FtpVerify verifyOptions, FtpError errorHandling, CancellationToken token) {
// verify args
if (!errorHandling.IsValidCombination())
throw new ArgumentException("Invalid combination of FtpError flags. Throw & Stop cannot be combined");
if (remoteDir.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remoteDir");
FtpTrace.WriteFunc("UploadFilesAsync", new object[] { localPaths, remoteDir, existsMode, createRemoteDir, verifyOptions, errorHandling });
//check if cancellation was requested and throw to set TaskStatus state to Canceled
token.ThrowIfCancellationRequested();
//int count = 0;
bool errorEncountered = false;
List<string> successfulUploads = new List<string>();
// ensure ends with slash
remoteDir = !remoteDir.EndsWith("/") ? remoteDir + "/" : remoteDir;
//flag to determine if existence checks are required
bool checkFileExistence = true;
// create remote dir if wanted
if (createRemoteDir) {
if (!await DirectoryExistsAsync(remoteDir)) {
await CreateDirectoryAsync(remoteDir);
checkFileExistence = false;
}
}
// get all the already existing files (if directory was created just create an empty array)
string[] existingFiles = checkFileExistence ? await GetNameListingAsync(remoteDir) : new string[0];
// per local file
foreach (string localPath in localPaths) {
// check if cancellation was requested and throw to set TaskStatus state to Canceled
token.ThrowIfCancellationRequested();
// calc remote path
string fileName = Path.GetFileName(localPath);
string remotePath = remoteDir + fileName;
// try to upload it
try {
bool ok = await UploadFileFromFileAsync(localPath, remotePath, false, existsMode, existingFiles.Contains(fileName), true, verifyOptions, token, null);
if (ok) {
successfulUploads.Add(remotePath);
} else if ((int)errorHandling > 1) {
errorEncountered = true;
break;
}
} catch (Exception ex) {
if (ex is OperationCanceledException) {
//DO NOT SUPPRESS CANCELLATION REQUESTS -- BUBBLE UP!
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Upload cancellation requested");
throw;
}
//suppress all other upload exceptions (errors are still written to FtpTrace)
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Upload Failure for " + localPath + ": " + ex);
if (errorHandling.HasFlag(FtpError.Stop)) {
errorEncountered = true;
break;
}
if (errorHandling.HasFlag(FtpError.Throw)) {
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulUploads(successfulUploads);
}
throw new FtpException("An error occurred uploading file(s). See inner exception for more info.", ex);
}
}
}
if (errorEncountered) {
//Delete any successful uploads if needed
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
await PurgeSuccessfulUploadsAsync(successfulUploads);
successfulUploads.Clear(); //forces return of 0
}
//Throw generic error because requested
if (errorHandling.HasFlag(FtpError.Throw)) {
throw new FtpException("An error occurred uploading one or more files. Refer to trace output if available.");
}
}
return successfulUploads.Count;
}
private async Task PurgeSuccessfulUploadsAsync(IEnumerable<string> remotePaths) {
foreach (string remotePath in remotePaths) {
await this.DeleteDirectoryAsync(remotePath);
}
}
/// <summary>
/// Uploads the given file paths to a single folder on the server asynchronously.
/// 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 <see cref="o:UploadFile"/> since it performs a single "file exists" check rather than one check per file.
/// </summary>
/// <param name="localPaths">The full or relative paths to the files on the local file system. Files can be from multiple folders.</param>
/// <param name="remoteDir">The full or relative path to the directory that files will be uploaded on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to FtpExists.None for fastest performance but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful upload and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were uploaded successfully. Affected when files are skipped when they already exist.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public async Task<int> UploadFilesAsync(IEnumerable<string> localPaths, string remoteDir, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = true, FtpVerify verifyOptions = FtpVerify.None, FtpError errorHandling = FtpError.None) {
return await UploadFilesAsync(localPaths, remoteDir, existsMode, createRemoteDir, verifyOptions, errorHandling, CancellationToken.None);
}
#endif
#endregion
#region Download Multiple Files
/// <summary>
/// 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 <see cref="o:DownloadFile"/>.
/// </summary>
/// <param name="localDir">The full or relative path to the directory that files will be downloaded into.</param>
/// <param name="remotePaths">The full or relative paths to the files on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were downloaded successfully. When existing files are skipped, they are not counted.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically switch to true for subsequent attempts.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public int DownloadFiles(string localDir, IEnumerable<string> remotePaths, bool overwrite = true, FtpVerify verifyOptions = FtpVerify.None,
FtpError errorHandling = FtpError.None) {
// verify args
if (!errorHandling.IsValidCombination())
throw new ArgumentException("Invalid combination of FtpError flags. Throw & Stop cannot be combined");
if (localDir.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localDir");
FtpTrace.WriteFunc("DownloadFiles", new object[] { localDir, remotePaths, overwrite, verifyOptions });
bool errorEncountered = false;
List<string> successfulDownloads = new List<string>();
// 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, verifyOptions, null);
if (ok) {
successfulDownloads.Add(localPath);
} else if ((int)errorHandling > 1) {
errorEncountered = true;
break;
}
} catch (Exception ex) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Failed to download " + remotePath + ". Error: " + ex);
if (errorHandling.HasFlag(FtpError.Stop)) {
errorEncountered = true;
break;
}
if (errorHandling.HasFlag(FtpError.Throw)) {
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulDownloads(successfulDownloads);
}
throw new FtpException("An error occurred downloading file(s). See inner exception for more info.", ex);
}
}
}
if (errorEncountered) {
//Delete any successful uploads if needed
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulDownloads(successfulDownloads);
successfulDownloads.Clear(); //forces return of 0
}
//Throw generic error because requested
if (errorHandling.HasFlag(FtpError.Throw)) {
throw new FtpException("An error occurred downloading one or more files. Refer to trace output if available.");
}
}
return successfulDownloads.Count;
}
/*
/// <summary>
/// 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 <see cref="o:DownloadFile"/>.
/// </summary>
/// <param name="localDir">The full or relative path to the directory that files will be downloaded into.</param>
/// <param name="remotePaths">The full or relative paths to the files on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were downloaded successfully. When existing files are skipped, they are not counted.</returns>
public int DownloadFiles(string localDir, List<string> remotePaths, bool overwrite = true, FtpError errorHandling = FtpError.None) {
return DownloadFiles(localDir, remotePaths.ToArray(), overwrite);
}*/
private void PurgeSuccessfulDownloads(IEnumerable<string> localFiles) {
foreach (string localFile in localFiles) {
// absorb any errors because we don't want this to throw more errors!
try {
File.Delete(localFile);
} catch (Exception ex) {
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "FtpClient : Exception caught and discarded while attempting to delete file '" + localFile + "' : " + ex.ToString());
}
}
}
#if ASYNC
/// <summary>
/// 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 <see cref="o:DownloadFile"/>.
/// </summary>
/// <param name="localDir">The full or relative path to the directory that files will be downloaded.</param>
/// <param name="remotePaths">The full or relative paths to the files on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <param name="token">The token to monitor for cancellation requests</param>
/// <returns>The count of how many files were downloaded successfully. When existing files are skipped, they are not counted.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically be set to true for subsequent attempts.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public async Task<int> DownloadFilesAsync(string localDir, IEnumerable<string> remotePaths, bool overwrite, FtpVerify verifyOptions,
FtpError errorHandling, CancellationToken token) {
// verify args
if (!errorHandling.IsValidCombination())
throw new ArgumentException("Invalid combination of FtpError flags. Throw & Stop cannot be combined");
if (localDir.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localDir");
FtpTrace.WriteFunc("DownloadFilesAsync", new object[] { localDir, remotePaths, overwrite, verifyOptions });
//check if cancellation was requested and throw to set TaskStatus state to Canceled
token.ThrowIfCancellationRequested();
bool errorEncountered = false;
List<string> successfulDownloads = new List<string>();
// ensure ends with slash
localDir = !localDir.EndsWith(Path.DirectorySeparatorChar.ToString()) ? localDir + Path.DirectorySeparatorChar.ToString() : localDir;
foreach (string remotePath in remotePaths) {
//check if cancellation was requested and throw to set TaskStatus state to Canceled
token.ThrowIfCancellationRequested();
// calc local path
string localPath = localDir + remotePath.GetFtpFileName();
// try to download it
try {
bool ok = await DownloadFileToFileAsync(localPath, remotePath, overwrite, verifyOptions, token, null);
if (ok) {
successfulDownloads.Add(localPath);
} else if ((int)errorHandling > 1) {
errorEncountered = true;
break;
}
} catch (Exception ex) {
if (ex is OperationCanceledException) {
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Download cancellation requested");
//DO NOT SUPPRESS CANCELLATION REQUESTS -- BUBBLE UP!
throw;
}
if (errorHandling.HasFlag(FtpError.Stop)) {
errorEncountered = true;
break;
}
if (errorHandling.HasFlag(FtpError.Throw)) {
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulDownloads(successfulDownloads);
}
throw new FtpException("An error occurred downloading file(s). See inner exception for more info.", ex);
}
}
}
if (errorEncountered) {
//Delete any successful uploads if needed
if (errorHandling.HasFlag(FtpError.DeleteProcessed)) {
PurgeSuccessfulDownloads(successfulDownloads);
successfulDownloads.Clear(); //forces return of 0
}
//Throw generic error because requested
if (errorHandling.HasFlag(FtpError.Throw)) {
throw new FtpException("An error occurred downloading one or more files. Refer to trace output if available.");
}
}
return successfulDownloads.Count;
}
/// <summary>
/// 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 <see cref="o:DownloadFile"/>.
/// </summary>
/// <param name="localDir">The full or relative path to the directory that files will be downloaded into.</param>
/// <param name="remotePaths">The full or relative paths to the files on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="errorHandling">Used to determine how errors are handled</param>
/// <returns>The count of how many files were downloaded successfully. When existing files are skipped, they are not counted.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically be set to true for subsequent attempts.
/// If <see cref="FtpVerify.Throw"/> is set and <see cref="FtpError.Throw"/> is <i>not set</i>, then individual verification errors will not cause an exception
/// to propagate from this method.
/// </remarks>
public async Task<int> DownloadFilesAsync(string localDir, IEnumerable<string> remotePaths, bool overwrite = true,
FtpVerify verifyOptions = FtpVerify.None, FtpError errorHandling = FtpError.None) {
return await DownloadFilesAsync(localDir, remotePaths, overwrite, verifyOptions, errorHandling, CancellationToken.None);
}
#endif
#endregion
#region Upload File
/// <summary>
/// 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.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful upload and what to do if it fails verification (See Remarks)</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// </remarks>
public bool UploadFile(string localPath, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false,
FtpVerify verifyOptions = FtpVerify.None, IProgress<double> progress = null) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("UploadFile", new object[] { localPath, remotePath, existsMode, createRemoteDir, verifyOptions });
// skip uploading if the local file does not exist
if (!File.Exists(localPath)) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "File does not exist.");
return false;
}
return UploadFileFromFile(localPath, remotePath, createRemoteDir, existsMode, false, false, verifyOptions, progress);
}
#if ASYNC
/// <summary>
/// Uploads the specified file directly onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful upload and what to do if it fails verification (See Remarks)</param>
/// <param name="token">The token to monitor for cancellation requests.</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// </remarks>
public async Task<bool> UploadFileAsync(string localPath, string remotePath, FtpExists existsMode, bool createRemoteDir,
FtpVerify verifyOptions, CancellationToken token, IProgress<double> progress) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
// skip uploading if the local file does not exist
#if CORE
if (!await Task.Run(()=>File.Exists(localPath))) {
#else
if (!File.Exists(localPath)) {
#endif
FtpTrace.WriteStatus(FtpTraceLevel.Error, "File does not exist.");
return false;
}
FtpTrace.WriteFunc("UploadFileAsync", new object[] { localPath, remotePath, existsMode, createRemoteDir, verifyOptions });
return await UploadFileFromFileAsync(localPath, remotePath, createRemoteDir, existsMode, false, false, verifyOptions, token, progress);
}
/// <summary>
/// Uploads the specified file directly onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful upload and what to do if it fails verification (See Remarks)</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted the existsMode will automatically be set to <see cref="FtpExists.Overwrite"/>.
/// </remarks>
public async Task<bool> UploadFileAsync(string localPath, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false, FtpVerify verifyOptions = FtpVerify.None) {
return await UploadFileAsync(localPath, remotePath, existsMode, createRemoteDir, verifyOptions, CancellationToken.None, null);
}
#endif
private bool UploadFileFromFile(string localPath, string remotePath, bool createRemoteDir, FtpExists existsMode, bool fileExists, bool fileExistsKnown, FtpVerify verifyOptions, IProgress<double> progress) {
// If retries are allowed set the retry counter to the allowed count
int attemptsLeft = verifyOptions.HasFlag(FtpVerify.Retry) ? m_retryAttempts : 1;
// Default validation to true (if verification isn't needed it'll allow a pass-through)
bool verified = true;
bool uploadSuccess;
do {
// write the file onto the server
using (var fileStream = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
// Upload file
uploadSuccess = UploadFileInternal(fileStream, remotePath, createRemoteDir, existsMode, fileExists, fileExistsKnown, progress);
attemptsLeft--;
// If verification is needed, update the validated flag
if (uploadSuccess && verifyOptions != FtpVerify.None) {
verified = VerifyTransfer(localPath, remotePath);
FtpTrace.WriteStatus(FtpTraceLevel.Info, "File Verification: " + (verified ? "PASS" : "FAIL"));
if (!verified && attemptsLeft > 0) {
// Force overwrite if a retry is required
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Retrying due to failed verification." + (existsMode != FtpExists.Overwrite ? " Switching to FtpExists.Overwrite mode. " : " ") + attemptsLeft + " attempts remaining");
existsMode = FtpExists.Overwrite;
}
}
}
} while (!verified && attemptsLeft > 0);//Loop if attempts are available and validation failed
if (uploadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Delete)) {
this.DeleteFile(remotePath);
}
if (uploadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Throw)) {
throw new FtpException("Uploaded file checksum value does not match local file");
}
return uploadSuccess && verified;
}
#if ASYNC
private async Task<bool> UploadFileFromFileAsync(string localPath, string remotePath, bool createRemoteDir, FtpExists existsMode,
bool fileExists, bool fileExistsKnown, FtpVerify verifyOptions, CancellationToken token, IProgress<double> progress) {
// If retries are allowed set the retry counter to the allowed count
int attemptsLeft = verifyOptions.HasFlag(FtpVerify.Retry) ? m_retryAttempts : 1;
// Default validation to true (if verification isn't needed it'll allow a pass-through)
bool verified = true;
bool uploadSuccess;
do {
// write the file onto the server
using (var fileStream = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
uploadSuccess = await UploadFileInternalAsync(fileStream, remotePath, createRemoteDir, existsMode, fileExists, fileExistsKnown, token, progress);
attemptsLeft--;
// If verification is needed, update the validated flag
if (verifyOptions != FtpVerify.None) {
verified = await VerifyTransferAsync(localPath, remotePath);
FtpTrace.WriteStatus(FtpTraceLevel.Info, "File Verification: " + (verified ? "PASS" : "FAIL"));
if (!verified && attemptsLeft > 0) {
// Force overwrite if a retry is required
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Retrying due to failed verification." + (existsMode != FtpExists.Overwrite ? " Switching to FtpExists.Overwrite mode. " : " ") + attemptsLeft + " attempts remaining");
existsMode = FtpExists.Overwrite;
}
}
}
} while (!verified && attemptsLeft > 0);
if (uploadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Delete)) {
await this.DeleteFileAsync(remotePath);
}
if (uploadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Throw)) {
throw new FtpException("Uploaded file checksum value does not match local file");
}
return uploadSuccess && verified;
}
#endif
#endregion
#region Upload Bytes/Stream
/// <summary>
/// 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.
/// </summary>
/// <param name="fileStream">The full data of the file, as a stream</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
public bool Upload(Stream fileStream, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false, IProgress<double> progress = null) {
// verify args
if (fileStream == null)
throw new ArgumentException("Required parameter is null or blank.", "fileStream");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("Upload", new object[] { remotePath, existsMode, createRemoteDir });
// write the file onto the server
return UploadFileInternal(fileStream, remotePath, createRemoteDir, existsMode, false, false, progress);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="fileData">The full data of the file, as a byte array</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
public bool Upload(byte[] fileData, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false, IProgress<double> progress = null) {
// verify args
if (fileData == null)
throw new ArgumentException("Required parameter is null or blank.", "fileData");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("Upload", new object[] { remotePath, existsMode, createRemoteDir });
// write the file onto the server
using (MemoryStream ms = new MemoryStream(fileData)) {
ms.Position = 0;
return UploadFileInternal(ms, remotePath, createRemoteDir, existsMode, false, false, progress);
}
}
#if ASYNC
/// <summary>
/// Uploads the specified stream as a file onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="fileStream">The full data of the file, as a stream</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance,
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="token">The token to monitor for cancellation requests.</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
public async Task<bool> UploadAsync(Stream fileStream, string remotePath, FtpExists existsMode, bool createRemoteDir, CancellationToken token, IProgress<double> progress) {
// verify args
if (fileStream == null)
throw new ArgumentException("Required parameter is null or blank.", "fileStream");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("UploadAsync", new object[] { remotePath, existsMode, createRemoteDir });
// write the file onto the server
return await UploadFileInternalAsync(fileStream, remotePath, createRemoteDir, existsMode, false, false, token, progress);
}
/// <summary>
/// Uploads the specified byte array as a file onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="fileData">The full data of the file, as a byte array</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance,
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <param name="token">The token to monitor for cancellation requests.</param>
/// <param name="progress">Provide an implementation of IProgress to track upload progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
public async Task<bool> UploadAsync(byte[] fileData, string remotePath, FtpExists existsMode, bool createRemoteDir, CancellationToken token, IProgress<double> progress) {
// verify args
if (fileData == null)
throw new ArgumentException("Required parameter is null or blank.", "fileData");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("UploadAsync", new object[] { remotePath, existsMode, createRemoteDir });
// write the file onto the server
using (MemoryStream ms = new MemoryStream(fileData)) {
ms.Position = 0;
return await UploadFileInternalAsync(ms, remotePath, createRemoteDir, existsMode, false, false, token, progress);
}
}
/// <summary>
/// Uploads the specified stream as a file onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="fileStream">The full data of the file, as a stream</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance,
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
public async Task<bool> UploadAsync(Stream fileStream, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false) {
return await UploadAsync(fileStream, remotePath, existsMode, createRemoteDir, CancellationToken.None, null);
}
/// <summary>
/// Uploads the specified byte array as a file onto the server asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it uploads data in chunks.
/// </summary>
/// <param name="fileData">The full data of the file, as a byte array</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="existsMode">What to do if the file already exists? Skip, overwrite or append? Set this to <see cref="FtpExists.NoCheck"/> for fastest performance,
/// but only if you are SURE that the files do not exist on the server.</param>
/// <param name="createRemoteDir">Create the remote directory if it does not exist. Slows down upload due to additional checks required.</param>
/// <returns>If true then the file was uploaded, false otherwise.</returns>
public async Task<bool> UploadAsync(byte[] fileData, string remotePath, FtpExists existsMode = FtpExists.Overwrite, bool createRemoteDir = false) {
return await UploadAsync(fileData, remotePath, existsMode, createRemoteDir, CancellationToken.None, null);
}
#endif
#endregion
#region Upload File Internal
/// <summary>
/// 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.
/// </summary>
private bool UploadFileInternal(Stream fileData, string remotePath, bool createRemoteDir, FtpExists existsMode, bool fileExists, bool fileExistsKnown, IProgress<double> progress) {
Stream upStream = null;
try {
long offset = 0;
bool checkFileExistsAgain = false;
// check if the file exists, and skip, overwrite or append
if (existsMode == FtpExists.NoCheck) {
checkFileExistsAgain = true;
} else {
if (!fileExistsKnown) {
fileExists = FileExists(remotePath);
}
switch (existsMode) {
case FtpExists.Skip:
if (fileExists) {
FtpTrace.WriteStatus(FtpTraceLevel.Warn, "File " + remotePath + " exists on server & existsMode is set to FileExists.Skip");
return false;
}
break;
case FtpExists.Overwrite:
if (fileExists) {
DeleteFile(remotePath);
}
break;
case FtpExists.Append:
if (fileExists) {
offset = GetFileSize(remotePath);
if (offset == -1) {
offset = 0; // start from the beginning
}
}
break;
}
}
// ensure the remote dir exists .. only if the file does not already exist!
if (createRemoteDir && !fileExists) {
string dirname = remotePath.GetFtpDirectoryName();
if (!DirectoryExists(dirname)) {
CreateDirectory(dirname);
}
}
// FIX #213 : Do not change Stream.Position if not supported
if (fileData.CanSeek) {
try {
// seek to required offset
fileData.Position = offset;
} catch (Exception ex2) {
}
}
// open a file connection
if (offset == 0) {
upStream = OpenWrite(remotePath, UploadDataType, checkFileExistsAgain);
} else {
upStream = OpenAppend(remotePath, UploadDataType, checkFileExistsAgain);
}
// loop till entire file uploaded
long len = fileData.Length;
byte[] buffer = new byte[TransferChunkSize];
if (UploadRateLimit == 0) {
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;
// send progress reports
if (progress != null) {
ReportProgress(progress, len, offset);
}
}
// zero return value (with no Exception) indicates EOS; so we should terminate the outer loop here
break;
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeUpload(remotePath, ref upStream, offset, ex)) {
throw;
}
}
}
} else {
Stopwatch sw = new Stopwatch();
double rateLimitBytes = UploadRateLimit * 1024;
while (offset < len) {
try {
// read a chunk of bytes from the file
int readBytes;
double limitCheckBytes = 0;
sw.Start();
while ((readBytes = fileData.Read(buffer, 0, buffer.Length)) > 0) {
// write chunk to the FTP stream
upStream.Write(buffer, 0, readBytes);
upStream.Flush();
offset += readBytes;
limitCheckBytes += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, len, offset);
}
// honor the speed limit
int swTime = (int)sw.ElapsedMilliseconds;
if (swTime >= 1000) {
double timeShouldTake = limitCheckBytes / rateLimitBytes * 1000;
if (timeShouldTake > swTime) {
#if CORE14
Task.Delay((int)(timeShouldTake - swTime)).Wait();
#else
Thread.Sleep((int)(timeShouldTake - swTime));
#endif
}
limitCheckBytes = 0;
sw.Restart();
}
}
// zero return value (with no Exception) indicates EOS; so we should terminate the outer loop here
break;
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeUpload(remotePath, ref upStream, offset, ex)) {
sw.Stop();
throw;
}
}
}
sw.Stop();
}
// wait for transfer to get over
while (upStream.Position < upStream.Length) {
}
// send progress reports
if (progress != null) {
progress.Report(100.0);
}
// 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 (!EnableThreadSafeDataConnections) {
FtpReply status = GetReply();
}
return true;
} catch (Exception ex1) {
// close stream before throwing error
try {
if (upStream != null)
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);
}
}
#if ASYNC
/// <summary>
/// Upload the given stream to the server as a new file asynchronously. Overwrites the file if it exists.
/// Writes data in chunks. Retries if server disconnects midway.
/// </summary>
private async Task<bool> UploadFileInternalAsync(Stream fileData, string remotePath, bool createRemoteDir, FtpExists existsMode, bool fileExists, bool fileExistsKnown, CancellationToken token, IProgress<double> progress) {
Stream upStream = null;
try {
long offset = 0;
bool checkFileExistsAgain = false;
// check if the file exists, and skip, overwrite or append
if (existsMode == FtpExists.NoCheck) {
checkFileExistsAgain = true;
} else {
if (!fileExistsKnown) {
fileExists = await FileExistsAsync(remotePath);
}
switch (existsMode) {
case FtpExists.Skip:
if (fileExists) {
return false;
}
break;
case FtpExists.Overwrite:
if (fileExists) {
await DeleteFileAsync(remotePath);
}
break;
case FtpExists.Append:
if (fileExists) {
offset = await GetFileSizeAsync(remotePath);
if (offset == -1) {
offset = 0; // start from the beginning
}
}
break;
}
}
// ensure the remote dir exists .. only if the file does not already exist!
if (createRemoteDir && !fileExists) {
string dirname = remotePath.GetFtpDirectoryName();
if (!await DirectoryExistsAsync(dirname)) {
await CreateDirectoryAsync(dirname);
}
}
// FIX #213 : Do not change Stream.Position if not supported
if (fileData.CanSeek) {
try {
// seek to required offset
fileData.Position = offset;
} catch (Exception ex2) {
}
}
// open a file connection
// TODO: Here is differrent from synchronous version. Needs confirm
if (offset == 0) {
upStream = await OpenWriteAsync(remotePath, UploadDataType);
} else {
upStream = await OpenAppendAsync(remotePath, UploadDataType);
}
// loop till entire file uploaded
long len = fileData.Length;
byte[] buffer = new byte[TransferChunkSize];
if (UploadRateLimit == 0) {
while (offset < len) {
try {
// read a chunk of bytes from the file
int readBytes;
while ((readBytes = await fileData.ReadAsync(buffer, 0, buffer.Length, token)) > 0) {
// write chunk to the FTP stream
await upStream.WriteAsync(buffer, 0, readBytes, token);
await upStream.FlushAsync(token);
offset += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, len, offset);
}
}
// zero return value (with no Exception) indicates EOS; so we should terminate the outer loop here
break;
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeUpload(remotePath, ref upStream, offset, ex)) {
throw;
}
}
}
} else {
Stopwatch sw = new Stopwatch();
double rateLimitBytes = UploadRateLimit * 1024;
while (offset < len) {
try {
// read a chunk of bytes from the file
int readBytes;
double limitCheckBytes = 0;
sw.Start();
while ((readBytes = await fileData.ReadAsync(buffer, 0, buffer.Length, token)) > 0) {
// write chunk to the FTP stream
await upStream.WriteAsync(buffer, 0, readBytes, token);
await upStream.FlushAsync(token);
offset += readBytes;
limitCheckBytes += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, len, offset);
}
// honor the rate limit
int swTime = (int)sw.ElapsedMilliseconds;
if (swTime >= 1000) {
double timeShouldTake = limitCheckBytes / rateLimitBytes * 1000;
if (timeShouldTake > swTime) {
await Task.Delay((int)(timeShouldTake - swTime));
}
limitCheckBytes = 0;
sw.Restart();
}
}
// zero return value (with no Exception) indicates EOS; so we should terminate the outer loop here
break;
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeUpload(remotePath, ref upStream, offset, ex)) {
sw.Stop();
throw;
}
}
}
sw.Stop();
}
// 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();
}
return true;
} catch (Exception ex1) {
// close stream before throwing error
try {
if (upStream != null)
upStream.Dispose();
} catch (Exception) { }
if(ex1 is OperationCanceledException)
{
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Upload cancellation requested");
throw;
}
// catch errors during upload
throw new FtpException("Error while uploading the file to the server. See InnerException for more info.", ex1);
}
}
#endif
private bool ResumeUpload(string remotePath, ref Stream upStream, long offset, IOException ex) {
// resume if server disconnects midway (fixes #39)
if (ex.InnerException != null) {
var iex = ex.InnerException as System.Net.Sockets.SocketException;
#if CORE
if (iex != null && (int)iex.SocketErrorCode == 10054) {
#else
if (iex != null && iex.ErrorCode == 10054) {
#endif
upStream.Dispose();
upStream = OpenAppend(remotePath, UploadDataType, true);
upStream.Position = offset;
return true;
}
}
return false;
}
#endregion
#region Download File
/// <summary>
/// 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.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically be set to true for subsequent attempts.
/// </remarks>
public bool DownloadFile(string localPath, string remotePath, bool overwrite = true, FtpVerify verifyOptions = FtpVerify.None, IProgress<double> progress = null) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadFile", new object[] { localPath, remotePath, overwrite, verifyOptions });
return DownloadFileToFile(localPath, remotePath, overwrite, verifyOptions, progress);
}
private bool DownloadFileToFile(string localPath, string remotePath, bool overwrite, FtpVerify verifyOptions, IProgress<double> progress) {
// skip downloading if the local file exists
if (!overwrite && File.Exists(localPath)) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Overwrite is false and local file already exists.");
return false;
}
try {
// create the folders
string dirPath = Path.GetDirectoryName(localPath);
if (!FtpExtensions.IsNullOrWhiteSpace(dirPath) && !Directory.Exists(dirPath)) {
Directory.CreateDirectory(dirPath);
}
} catch (Exception ex1) {
// catch errors creating directory
throw new FtpException("Error while creating directories. See InnerException for more info.", ex1);
}
bool downloadSuccess;
bool verified = true;
int attemptsLeft = verifyOptions.HasFlag(FtpVerify.Retry) ? m_retryAttempts : 1;
do {
// download the file from server
using (var outStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None)) {
// download the file straight to a file stream
downloadSuccess = DownloadFileInternal(remotePath, outStream, progress);
attemptsLeft--;
}
// if verification is needed
if (downloadSuccess && verifyOptions != FtpVerify.None) {
verified = VerifyTransfer(localPath, remotePath);
FtpTrace.WriteLine(FtpTraceLevel.Info, "File Verification: " + (verified ? "PASS" : "FAIL"));
#if DEBUG
if (!verified && attemptsLeft > 0) {
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Retrying due to failed verification." + (overwrite ? " Overwrite will occur." : "") + " " + attemptsLeft + " attempts remaining");
}
#endif
}
} while (!verified && attemptsLeft > 0);
if (downloadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Delete)) {
File.Delete(localPath);
}
if (downloadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Throw)) {
throw new FtpException("Downloaded file checksum value does not match remote file");
}
return downloadSuccess && verified;
}
#if ASYNC
/// <summary>
/// Downloads the specified file onto the local file system asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="token">The token to monitor for cancellation requests</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically be set to true for subsequent attempts.
/// </remarks>
public async Task<bool> DownloadFileAsync(string localPath, string remotePath, bool overwrite, FtpVerify verifyOptions, CancellationToken token, IProgress<double> progress) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadFileAsync", new object[] { localPath, remotePath, overwrite, verifyOptions });
return await DownloadFileToFileAsync(localPath, remotePath, overwrite, verifyOptions, token, progress);
}
/// <summary>
/// Downloads the specified file onto the local file system asynchronously.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="localPath">The full or relative path to the file on the local file system</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="overwrite">True if you want the local file to be overwritten if it already exists. (Default value is true)</param>
/// <param name="verifyOptions">Sets if checksum verification is required for a successful download and what to do if it fails verification (See Remarks)</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
/// <remarks>
/// If verification is enabled (All options other than <see cref="FtpVerify.None"/>) the hash will be checked against the server. If the server does not support
/// any hash algorithm, then verification is ignored. If only <see cref="FtpVerify.OnlyChecksum"/> is set then the return of this method depends on both a successful
/// upload &amp; verification. Additionally, if any verify option is set and a retry is attempted then overwrite will automatically be set to true for subsequent attempts.
/// </remarks>
public async Task<bool> DownloadFileAsync(string localPath, string remotePath, bool overwrite = true, FtpVerify verifyOptions = FtpVerify.None, IProgress<double> progress = null) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadFileAsync", new object[] { localPath, remotePath, overwrite, verifyOptions });
return await DownloadFileToFileAsync(localPath, remotePath, overwrite, verifyOptions, CancellationToken.None, progress);
}
private async Task<bool> DownloadFileToFileAsync(string localPath, string remotePath, bool overwrite, FtpVerify verifyOptions, CancellationToken token, IProgress<double> progress) {
if (string.IsNullOrWhiteSpace(localPath))
throw new ArgumentNullException("localPath");
// skip downloading if the local file exists
#if CORE
if (!overwrite && await Task.Run(() => File.Exists(localPath))) {
#else
if (!overwrite && File.Exists(localPath)) {
#endif
FtpTrace.WriteStatus(FtpTraceLevel.Error, "Overwrite is false and local file already exists");
return false;
}
try {
// create the folders
string dirPath = Path.GetDirectoryName(localPath);
#if CORE
if (!String.IsNullOrWhiteSpace(dirPath) && !await Task.Run(() => Directory.Exists(dirPath))) {
#else
if (!String.IsNullOrWhiteSpace(dirPath) && !Directory.Exists(dirPath)) {
#endif
Directory.CreateDirectory(dirPath);
}
} catch (Exception ex1) {
// catch errors creating directory
throw new FtpException("Error while crated directories. See InnerException for more info.", ex1);
}
bool downloadSuccess;
bool verified = true;
int attemptsLeft = verifyOptions.HasFlag(FtpVerify.Retry) ? m_retryAttempts : 1;
do {
// download the file from server
using (var outStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None)) {
// download the file straight to a file stream
downloadSuccess = await DownloadFileInternalAsync(remotePath, outStream, token, progress);
attemptsLeft--;
}
// if verification is needed
if (downloadSuccess && verifyOptions != FtpVerify.None) {
verified = await VerifyTransferAsync(localPath, remotePath);
FtpTrace.WriteStatus(FtpTraceLevel.Info, "File Verification: " + (verified ? "PASS" : "FAIL"));
#if DEBUG
if (!verified && attemptsLeft > 0) {
FtpTrace.WriteStatus(FtpTraceLevel.Verbose, "Retrying due to failed verification." + (overwrite ? " Overwrite will occur." : "") + " " + attemptsLeft + " attempts remaining");
}
#endif
}
} while (!verified && attemptsLeft > 0);
if (downloadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Delete)) {
File.Delete(localPath);
}
if (downloadSuccess && !verified && verifyOptions.HasFlag(FtpVerify.Throw)) {
throw new FtpException("Downloaded file checksum value does not match remote file");
}
return downloadSuccess && verified;
}
#endif
#endregion
#region Download Bytes/Stream
/// <summary>
/// Downloads the specified file into the specified stream.
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="outStream">The stream that the file will be written to. Provide a new MemoryStream if you only want to read the file into memory.</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
public bool Download(Stream outStream, string remotePath, IProgress<double> progress = null) {
// verify args
if (outStream == null)
throw new ArgumentException("Required parameter is null or blank.", "outStream");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("Download", new object[] { remotePath });
// download the file from the server
return DownloadFileInternal(remotePath, outStream, progress);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="outBytes">The variable that will receive the bytes.</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
public bool Download(out byte[] outBytes, string remotePath, IProgress<double> progress = null) {
// verify args
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("Download", new object[] { remotePath });
outBytes = null;
// download the file from the server
bool ok;
using (MemoryStream outStream = new MemoryStream()) {
ok = DownloadFileInternal(remotePath, outStream, progress);
if (ok) {
outBytes = outStream.ToArray();
}
}
return ok;
}
#if ASYNC
/// <summary>
/// Downloads the specified file into the specified stream asynchronously .
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="outStream">The stream that the file will be written to. Provide a new MemoryStream if you only want to read the file into memory.</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="token">The token to monitor cancellation requests</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
public async Task<bool> DownloadAsync(Stream outStream, string remotePath, CancellationToken token, IProgress<double> progress = null) {
// verify args
if (outStream == null)
throw new ArgumentException("Required parameter is null or blank.", "outStream");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadAsync", new object[] { remotePath });
// download the file from the server
return await DownloadFileInternalAsync(remotePath, outStream, token, progress);
}
/// <summary>
/// Downloads the specified file into the specified stream asynchronously .
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="outStream">The stream that the file will be written to. Provide a new MemoryStream if you only want to read the file into memory.</param>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <returns>If true then the file was downloaded, false otherwise.</returns>
public async Task<bool> DownloadAsync(Stream outStream, string remotePath) {
// verify args
if (outStream == null)
throw new ArgumentException("Required parameter is null or blank.", "outStream");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadAsync", new object[] { remotePath });
// download the file from the server
return await DownloadFileInternalAsync(remotePath, outStream, CancellationToken.None, null);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <param name="token">The token to monitor cancellation requests</param>
/// <param name="progress">Provide an implementation of IProgress to track download progress. The value provided is in the range 0 to 100, indicating the percentage of the file transferred. If the progress is indeterminate, -1 is sent.</param>
/// <returns>A byte array containing the contents of the downloaded file if successful, otherwise null.</returns>
public async Task<byte[]> DownloadAsync(string remotePath, CancellationToken token, IProgress<double> progress = null) {
// verify args
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
FtpTrace.WriteFunc("DownloadAsync", new object[] { remotePath });
// download the file from the server
using (MemoryStream outStream = new MemoryStream()) {
bool ok = await DownloadFileInternalAsync(remotePath, outStream, token, progress);
return ok ? outStream.ToArray() : null;
}
}
/// <summary>
/// Downloads the specified file into the specified stream asynchronously .
/// High-level API that takes care of various edge cases internally.
/// Supports very large files since it downloads data in chunks.
/// </summary>
/// <param name="remotePath">The full or relative path to the file on the server</param>
/// <returns>A byte array containing the contents of the downloaded file if successful, otherwise null.</returns>
public async Task<byte[]> DownloadAsync(string remotePath) {
// download the file from the server
return await DownloadAsync(remotePath, CancellationToken.None, null);
}
#endif
#endregion
#region Download File Internal
/// <summary>
/// Download a file from the server and write the data into the given stream.
/// Reads data in chunks. Retries if server disconnects midway.
/// </summary>
private bool DownloadFileInternal(string remotePath, Stream outStream, IProgress<double> progress) {
Stream downStream = null;
try {
// get file size if downloading in binary mode (in ASCII mode we read until EOF)
long fileLen = 0;
if (DownloadDataType == FtpDataType.Binary) {
fileLen = GetFileSize(remotePath);
}
// open the file for reading
downStream = OpenRead(remotePath, DownloadDataType, 0, fileLen > 0);
// if the server has not provided a length for this file
// we read until EOF instead of reading a specific number of bytes
bool readToEnd = (fileLen <= 0);
// loop till entire file downloaded
byte[] buffer = new byte[TransferChunkSize];
long offset = 0;
if (DownloadRateLimit == 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;
// send progress reports
if (progress != null) {
ReportProgress(progress, fileLen, offset);
}
}
// if we reach here means EOF encountered
// stop if we are in "read until EOF" mode
if (readToEnd || offset == fileLen) {
break;
}
// zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume
throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]");
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeDownload(remotePath, ref downStream, offset, ex)) {
throw;
}
}
}
} else {
Stopwatch sw = new Stopwatch();
double rateLimitBytes = DownloadRateLimit * 1024;
while (offset < fileLen || readToEnd) {
try {
// read a chunk of bytes from the FTP stream
int readBytes = 1;
double limitCheckBytes = 0;
sw.Start();
while ((readBytes = downStream.Read(buffer, 0, buffer.Length)) > 0) {
// write chunk to output stream
outStream.Write(buffer, 0, readBytes);
offset += readBytes;
limitCheckBytes += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, fileLen, offset);
}
// honor the rate limit
int swTime = (int)sw.ElapsedMilliseconds;
if (swTime >= 1000) {
double timeShouldTake = limitCheckBytes / rateLimitBytes * 1000;
if (timeShouldTake > swTime) {
#if CORE14
Task.Delay((int)(timeShouldTake - swTime)).Wait();
#else
Thread.Sleep((int)(timeShouldTake - swTime));
#endif
}
limitCheckBytes = 0;
sw.Restart();
}
}
// if we reach here means EOF encountered
// stop if we are in "read until EOF" mode
if (readToEnd || offset == fileLen) {
break;
}
// zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume
throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]");
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeDownload(remotePath, ref downStream, offset, ex)) {
sw.Stop();
throw;
}
}
}
sw.Stop();
}
// 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")) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "File does not exist: " + ex1);
return false;
}
// catch errors during upload
throw new FtpException("Error while downloading the file from the server. See InnerException for more info.", ex1);
}
}
#if ASYNC
/// <summary>
/// Download a file from the server and write the data into the given stream asynchronously.
/// Reads data in chunks. Retries if server disconnects midway.
/// </summary>
private async Task<bool> DownloadFileInternalAsync(string remotePath, Stream outStream, CancellationToken token, IProgress<double> progress) {
Stream downStream = null;
try {
// get file size if downloading in binary mode (in ASCII mode we read until EOF)
long fileLen = 0;
if (DownloadDataType == FtpDataType.Binary){
fileLen = await GetFileSizeAsync(remotePath);
}
// open the file for reading
downStream = await OpenReadAsync(remotePath, DownloadDataType, 0, fileLen > 0);
// if the server has not provided a length for this file
// we read until EOF instead of reading a specific number of bytes
bool readToEnd = (fileLen <= 0);
// loop till entire file downloaded
byte[] buffer = new byte[TransferChunkSize];
long offset = 0;
if (DownloadRateLimit == 0) {
while (offset < fileLen || readToEnd) {
try {
// read a chunk of bytes from the FTP stream
int readBytes = 1;
while ((readBytes = await downStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) {
// write chunk to output stream
await outStream.WriteAsync(buffer, 0, readBytes, token);
offset += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, fileLen, offset);
}
}
// if we reach here means EOF encountered
// stop if we are in "read until EOF" mode
if (readToEnd || offset == fileLen) {
break;
}
// zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume
throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]");
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeDownload(remotePath, ref downStream, offset, ex)) {
throw;
}
}
}
} else {
Stopwatch sw = new Stopwatch();
double rateLimitBytes = DownloadRateLimit * 1024;
while (offset < fileLen || readToEnd) {
try {
// read a chunk of bytes from the FTP stream
int readBytes = 1;
double limitCheckBytes = 0;
sw.Start();
while ((readBytes = await downStream.ReadAsync(buffer, 0, buffer.Length, token)) > 0) {
// write chunk to output stream
await outStream.WriteAsync(buffer, 0, readBytes, token);
offset += readBytes;
limitCheckBytes += readBytes;
// send progress reports
if (progress != null) {
ReportProgress(progress, fileLen, offset);
}
// honor the rate limit
int swTime = (int)sw.ElapsedMilliseconds;
if (swTime >= 1000) {
double timeShouldTake = limitCheckBytes / rateLimitBytes * 1000;
if (timeShouldTake > swTime) {
await Task.Delay((int)(timeShouldTake - swTime));
}
limitCheckBytes = 0;
sw.Restart();
}
}
// if we reach here means EOF encountered
// stop if we are in "read until EOF" mode
if (readToEnd || offset == fileLen) {
break;
}
// zero return value (with no Exception) indicates EOS; so we should fail here and attempt to resume
throw new IOException($"Unexpected EOF for remote file {remotePath} [{offset}/{fileLen} bytes read]");
} catch (IOException ex) {
// resume if server disconnected midway, or throw if there is an exception doing that as well
if (!ResumeDownload(remotePath, ref downStream, offset, ex)) {
sw.Stop();
throw;
}
}
}
sw.Stop();
}
// disconnect FTP stream before exiting
await outStream.FlushAsync(token);
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) { }
if (ex1 is OperationCanceledException)
{
FtpTrace.WriteStatus(FtpTraceLevel.Info, "Upload cancellation requested");
throw;
}
// 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")) {
FtpTrace.WriteStatus(FtpTraceLevel.Error, "File does not exist: " + ex1);
return false;
}
// catch errors during upload
throw new FtpException("Error while downloading the file from the server. See InnerException for more info.", ex1);
}
}
#endif
private bool ResumeDownload(string remotePath, ref Stream downStream, long offset, IOException ex) {
// resume if server disconnects midway (fixes #39)
if (ex.InnerException != null) {
var ie = ex.InnerException as System.Net.Sockets.SocketException;
#if CORE
if (ie != null && (int)ie.SocketErrorCode == 10054) {
#else
if (ie != null && ie.ErrorCode == 10054) {
#endif
downStream.Dispose();
downStream = OpenRead(remotePath, DownloadDataType, restart: offset);
return true;
}
}
return false;
}
#endregion
#region Verification
private bool VerifyTransfer(string localPath, string remotePath) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
if (this.HasFeature(FtpCapability.HASH) || this.HasFeature(FtpCapability.MD5) ||
this.HasFeature(FtpCapability.XMD5) || this.HasFeature(FtpCapability.XCRC) ||
this.HasFeature(FtpCapability.XSHA1) || this.HasFeature(FtpCapability.XSHA256) ||
this.HasFeature(FtpCapability.XSHA512)) {
FtpHash hash = this.GetChecksum(remotePath);
if (!hash.IsValid)
return false;
return hash.Verify(localPath);
}
//Not supported return true to ignore validation
return true;
}
#if ASYNC
private async Task<bool> VerifyTransferAsync(string localPath, string remotePath) {
// verify args
if (localPath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "localPath");
if (remotePath.IsBlank())
throw new ArgumentException("Required parameter is null or blank.", "remotePath");
if (this.HasFeature(FtpCapability.HASH) || this.HasFeature(FtpCapability.MD5) ||
this.HasFeature(FtpCapability.XMD5) || this.HasFeature(FtpCapability.XCRC) ||
this.HasFeature(FtpCapability.XSHA1) || this.HasFeature(FtpCapability.XSHA256) ||
this.HasFeature(FtpCapability.XSHA512)) {
FtpHash hash = await this.GetChecksumAsync(remotePath);
if (!hash.IsValid)
return false;
return hash.Verify(localPath);
}
//Not supported return true to ignore validation
return true;
}
#endif
#endregion
#region Utilities
/// <summary>
/// Sends progress to the user, either a value between 0-100 indicating percentage complete, or -1 for indeterminate.
/// </summary>
private void ReportProgress(IProgress<double> progress, long fileSize, long position) {
// calculate % based on file len vs file offset
double value = ((double)position / (double)fileSize) * 100;
// suppress invalid values and send -1 instead
if (double.IsNaN(value) || double.IsInfinity(value)) {
progress.Report(-1);
} else {
// send a value between 0-100 indicating percentage complete
progress.Report(value);
}
}
#endregion
}
}