Introduction

You'd like to add, fetch, and possibly delete files on a locally available or remote server, and you're looking for the best way to do it.

Well, SFTP is the solution you're looking for since it is, secure, offers authentication, easy to use, needs a single connection, offers resumable transfers, and is very well documented.

Creating an SFTP server

Once can create an SFTP server by following the instructions for their operating system. For instance, here is how to do it on Ubuntu/Debian. Also, one can use a readily available sftp server such as FileZilla's.

Methods of usage

Similar to hosting a server, one can either use an SFTP client or write a script to automate a process using SFTP. This article illustrates how to write an SFTP client using modern .NET.

The Client

The client is self explanatory and pretty easy to use by another service. One can also turn it into a library. Without further ado, let us start creating it.

Dependency - SSH.NET

SSH.NET is the dependency we would use in order to communicate with the SFTP server.

Code

The code works as follows:

  • Connect to the SFTP server when the application starts.
  • Be ready to do any SFTP-related operation using that connection.
  • Close the connection upon program termination
  • The service supports dependency injection out of the box

The appsettings.json configuration file:


// Configuration before...

"Sftp": {
    "IsConnected": false,
    "Host": "192.168.XXX.YYY",
    "Username": "sftpUserHere",
    "Password": "sftpPasswordHere",
    "BasePath": "/Server/MyApp/Files"
  },

// Configuration after...

Note that IsConnected is needed in cases where one needs to use dependency injection for their service/API while the sftp server is unavailable at the moment.

The ISftpUtility interface

public interface ISftpUtillity
{
    Task<(byte[] Content, string ContentType)> DownloadFileBytesAsync(string remotePath);
    Task<(string Base64Content, string ContentType)> DownloadFileBase64Async(string remotePath);
    void DisposeConnection();
}

And below is the SftpUtility

using System;
using Microsoft.Extensions.Configuration;
using Renci.SshNet;

public class SftpUtility : ISftpUtillity
{
    private readonly SftpClient _sftpClient;
    public SftpUtility(IConfiguration configuration)
    {
        var isSftp   = configuration.GetSection("Sftp:IsConnected").Get<bool>();
        if(isSftp)
        {
            var host     = configuration.GetSection("Sftp:Host").Get<string>();
            var username = configuration.GetSection("Sftp:Username").Get<string>();
            var password = configuration.GetSection("Sftp:Password").Get<string>();
            _sftpClient  = new SftpClient(host ?? "", username ?? "", password ?? "");
            _sftpClient.Connect();
        }
    }

    public async Task<(byte[] Content, string ContentType)> DownloadFileBytesAsync(string remotePath)
    {
        try
        {
            using (var memoryStream = new MemoryStream())
            {
                await Task.Run(() => _sftpClient.DownloadFile(remotePath, memoryStream));
                return (memoryStream.ToArray(), GetContentTypeFromPath(remotePath));
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error downloading file: {ex.Message}");
            return (null, null);
        }
    }

    public async Task<(string Base64Content, string ContentType)> DownloadFileBase64Async(string remotePath)
    {
        var (content, contentType) = await DownloadFileBytesAsync(remotePath);
        if (content != null)
            return (Convert.ToBase64String(content), contentType);
        return (null, null);
    }

    public void DisposeConnection()
    {
        _sftpClient?.Disconnect();
        _sftpClient?.Dispose();
    }

    private string GetContentTypeFromPath(string path)
    {
        string extension = Path.GetExtension(path).ToLowerInvariant();
        switch (extension)
        {
            case ".pdf": return "application/pdf";
            case ".jpg":
            case ".jpeg": return "image/jpeg";
            case ".png": return "image/png";
            case ".txt": return "text/plain";
            case ".xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            // Add more content types as needed
            default: return "application/octet-stream"; // Default for unknown types
        }
    }
}

Disposing the connection

In ASP.NET, the connection can be disposed by tapping in the ApplicationStopping lifetime method, like so

Program.cs

app.Lifetime.ApplicationStopping.Register(() =>
{
    Console.WriteLine("Application is stopping...");
    using (var scope = app.Services.CreateScope())
    {
        var serviceProvider = scope.ServiceProvider;
        var sftpUtility = serviceProvider.GetRequiredService<ISftpUtillity>();
        sftpUtility.DisposeConnection();
    }
});

Room for improvement

One can easily use the same library to upload or delete files on the remote server.

Conclusion

This was a short tutorial illustrating the usage and benefits of using SFTP in the case of writing an API or a service when file management is needed.

Photo Credit.

Author Of article : Amjad Abujamous Read full article