FtpsClient.cs

using System;
using System.IO;
using System.Management.Automation;
using System.Net;
using FluentFTP;

namespace PureStorage.AzureNative.Tools
{
    // Public static helper class designed to be called from PowerShell
    public static class FtpsClient
    {
        // Verifies FTPS connectivity by establishing a TLS-secured FTP session and issuing a NOOP.
        public static void AssertFtpsServerConnectible(string ftpsServer, int ftpsPort, PSCredential ftpsCredential)
        {
            if (string.IsNullOrWhiteSpace(ftpsServer)) throw new ArgumentException("FTPS server is required", nameof(ftpsServer));
            if (ftpsCredential == null) throw new ArgumentNullException(nameof(ftpsCredential));

            using (var client = CreateClient(ftpsServer, ftpsPort, ftpsCredential))
            {
                client.Connect();
                // NOOP is a lightweight connectivity check
                client.Noop();
            }
        }

        // Uploads a single local file to the FTPS server under /<remoteDir>/<filename>
        public static void SendFileToFtps(string ftpsServer, int ftpsPort, string localFilePath, PSCredential ftpsCredential, string remoteDir)
        {
            if (string.IsNullOrWhiteSpace(localFilePath)) throw new ArgumentException("Local file path is required", nameof(localFilePath));
            if (!File.Exists(localFilePath)) throw new FileNotFoundException("Local file not found", localFilePath);

            using (var client = CreateClient(ftpsServer, ftpsPort, ftpsCredential))
            {
                client.Connect();

                if (!client.DirectoryExists(remoteDir))
                {
                    client.CreateDirectory(remoteDir, true);
                }

                var fileName = Path.GetFileName(localFilePath);
                var remotePath = $"{remoteDir}/{fileName}";

                var status = client.UploadFile(localFilePath, remotePath,
                                               FtpRemoteExists.Overwrite,
                                               true,
                                               FtpVerify.None,
                                               p =>
                                               {
                                                   if (p.Progress >= 0)
                                                   {
                                                       Console.Write($"\rUploading: {p.Progress:0.0}%");
                                                       if (p.Progress >= 100.0)
                                                       {
                                                           Console.WriteLine();
                                                       }
                                                   }
                                               });
                if (status != FtpStatus.Success)
                {
                    throw new InvalidOperationException($"FTPS upload failed with status: {status}");
                }
            }
        }

        private static FtpClient CreateClient(string server, int port, PSCredential credential)
        {
            var netCred = credential.GetNetworkCredential();

            var client = new FtpClient(server, netCred.UserName, netCred.Password);

            // Use FTPS (TLS). Default to Implicit on 990, else Explicit.
            client.Config.EncryptionMode = port == 990 ? FtpEncryptionMode.Implicit : FtpEncryptionMode.Explicit;
            client.Config.DataConnectionType = FtpDataConnectionType.AutoPassive;
            client.Config.ValidateAnyCertificate = false; // set to true if you need to skip certificate validation

            // Control connection timeouts
            client.Config.ConnectTimeout = 1000 * 30; // 30 seconds
            client.Config.ReadTimeout = 1000 * 60 * 5; // 5 minutes

            // Data connection timeouts tuned for large file transfers (multi‑GB)
            client.Config.DataConnectionConnectTimeout = 1000 * 60; // 60 seconds
            client.Config.DataConnectionReadTimeout = 1000 * 60 * 30; // 30 minutes

            // Socket / keep‑alive behavior
            client.Config.SocketKeepAlive = true;


            // File transfer behavior
            client.Config.TransferChunkSize = 256 * 1024; // 256 KB chunks
            client.Config.UploadDataType = FtpDataType.Binary; // ensure binary uploads
            client.Config.DownloadDataType = FtpDataType.Binary; // ensure binary downloads

            // Verification / retries used with FtpVerify.Retry
            client.Config.RetryAttempts = 3;

            return client;
        }
    }
}