2162 lines
96 KiB
C#
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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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
|
|
|
|
}
|
|
} |