diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 642ce1a33..80f7cbee3 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -79,6 +79,7 @@ jobs:
{ name: "Testcontainers.Redis", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" },
+ { name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 444bdb1c3..4afa799e9 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -97,6 +97,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Redpanda", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus", "src\Testcontainers.ServiceBus\Testcontainers.ServiceBus.csproj", "{2E39E532-B81E-4B48-A004-FAE18EDF9E79}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver", "src\Testcontainers.WebDriver\Testcontainers.WebDriver.csproj", "{64A87DE5-29B0-4A54-9E74-560484D8C7C0}"
@@ -201,6 +203,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ResourceReap
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus.Tests", "tests\Testcontainers.ServiceBus.Tests\Testcontainers.ServiceBus.Tests.csproj", "{232DD918-46ED-4BA8-B383-1A9146D83064}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp.Tests", "tests\Testcontainers.Sftp.Tests\Testcontainers.Sftp.Tests.csproj", "{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate.Tests", "tests\Testcontainers.Weaviate.Tests\Testcontainers.Weaviate.Tests.csproj", "{DDB41BC8-5826-4D97-9C5F-001151E3FFD6}"
@@ -386,6 +390,10 @@ Global
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E39E532-B81E-4B48-A004-FAE18EDF9E79}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D5C6816-0DD2-4E13-A585-033B5D3C80D5}.Release|Any CPU.Build.0 = Release|Any CPU
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -594,6 +602,10 @@ Global
{232DD918-46ED-4BA8-B383-1A9146D83064}.Debug|Any CPU.Build.0 = Debug|Any CPU
{232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.ActiveCfg = Release|Any CPU
{232DD918-46ED-4BA8-B383-1A9146D83064}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B73C3CC0-9F16-4B34-92BE-6EC0853912C5}.Release|Any CPU.Build.0 = Release|Any CPU
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27CDB869-A150-4593-958F-6F26E5391E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27CDB869-A150-4593-958F-6F26E5391E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -654,6 +666,7 @@ Global
{BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{45D6F69C-4D87-4130-AA90-0DB2F7460DAE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{2E39E532-B81E-4B48-A004-FAE18EDF9E79} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {7D5C6816-0DD2-4E13-A585-033B5D3C80D5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -706,6 +719,7 @@ Global
{867BD04E-4670-4FBA-98D5-9F83220E6DFB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{9E8E6AA5-65D1-498F-BEAB-BA34723A0050} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{232DD918-46ED-4BA8-B383-1A9146D83064} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {B73C3CC0-9F16-4B34-92BE-6EC0853912C5} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
diff --git a/src/Testcontainers.Sftp/.editorconfig b/src/Testcontainers.Sftp/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.Sftp/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.Sftp/SftpBuilder.cs b/src/Testcontainers.Sftp/SftpBuilder.cs
new file mode 100644
index 000000000..f294c55a2
--- /dev/null
+++ b/src/Testcontainers.Sftp/SftpBuilder.cs
@@ -0,0 +1,132 @@
+namespace Testcontainers.Sftp;
+
+///
+[PublicAPI]
+public sealed class SftpBuilder : ContainerBuilder
+{
+ public const string SftpImage = "atmoz/sftp:alpine";
+
+ public const ushort SftpPort = 22;
+
+ public const string DefaultUsername = "sftp";
+
+ public const string DefaultPassword = "sftp";
+
+ public const string DefaultUploadDirectory = "upload";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SftpBuilder()
+ : this(new SftpConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private SftpBuilder(SftpConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override SftpConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Sets the Sftp username.
+ ///
+ /// The Sftp username.
+ /// A configured instance of .
+ public SftpBuilder WithUsername(string username)
+ {
+ return Merge(DockerResourceConfiguration, new SftpConfiguration(username: username));
+ }
+
+ ///
+ /// Sets the Sftp password.
+ ///
+ /// The Sftp password.
+ /// A configured instance of .
+ public SftpBuilder WithPassword(string password)
+ {
+ return Merge(DockerResourceConfiguration, new SftpConfiguration(password: password));
+ }
+
+ ///
+ /// Sets the directory to which files are uploaded.
+ ///
+ /// The upload directory.
+ /// A configured instance of .
+ public SftpBuilder WithUploadDirectory(string uploadDirectory)
+ {
+ return Merge(DockerResourceConfiguration, new SftpConfiguration(uploadDirectory: uploadDirectory));
+ }
+
+ ///
+ public override SftpContainer Build()
+ {
+ Validate();
+
+ var sftpContainer = WithCommand(string.Join(
+ ":",
+ DockerResourceConfiguration.Username,
+ DockerResourceConfiguration.Password,
+ string.Empty,
+ string.Empty,
+ DockerResourceConfiguration.UploadDirectory));
+
+ return new SftpContainer(sftpContainer.DockerResourceConfiguration);
+ }
+
+ ///
+ protected override SftpBuilder Init()
+ {
+ return base.Init()
+ .WithImage(SftpImage)
+ .WithPortBinding(SftpPort, true)
+ .WithUsername(DefaultUsername)
+ .WithPassword(DefaultPassword)
+ .WithUploadDirectory(DefaultUploadDirectory)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Server listening on .+"));
+ }
+
+ ///
+ protected override void Validate()
+ {
+ base.Validate();
+
+ _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username))
+ .NotNull()
+ .NotEmpty();
+
+ _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
+ .NotNull()
+ .NotEmpty();
+
+ _ = Guard.Argument(DockerResourceConfiguration.UploadDirectory, nameof(DockerResourceConfiguration.UploadDirectory))
+ .NotNull()
+ .NotEmpty();
+ }
+
+ ///
+ protected override SftpBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override SftpBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new SftpConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override SftpBuilder Merge(SftpConfiguration oldValue, SftpConfiguration newValue)
+ {
+ return new SftpBuilder(new SftpConfiguration(oldValue, newValue));
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Sftp/SftpConfiguration.cs b/src/Testcontainers.Sftp/SftpConfiguration.cs
new file mode 100644
index 000000000..9dfb00c44
--- /dev/null
+++ b/src/Testcontainers.Sftp/SftpConfiguration.cs
@@ -0,0 +1,80 @@
+namespace Testcontainers.Sftp;
+
+///
+[PublicAPI]
+public sealed class SftpConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Sftp username.
+ /// The Sftp password.
+ /// The directory to which files are uploaded.
+ public SftpConfiguration(
+ string username = null,
+ string password = null,
+ string uploadDirectory = null)
+ {
+ Username = username;
+ Password = password;
+ UploadDirectory = uploadDirectory;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public SftpConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public SftpConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public SftpConfiguration(SftpConfiguration resourceConfiguration)
+ : this(new SftpConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public SftpConfiguration(SftpConfiguration oldValue, SftpConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
+ Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
+ UploadDirectory = BuildConfiguration.Combine(oldValue.UploadDirectory, newValue.UploadDirectory);
+ }
+
+ ///
+ /// Gets the Sftp username.
+ ///
+ public string Username { get; }
+
+ ///
+ /// Gets the Sftp password.
+ ///
+ public string Password { get; }
+
+ ///
+ /// Gets the directory to which files are uploaded.
+ ///
+ public string UploadDirectory { get; }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Sftp/SftpContainer.cs b/src/Testcontainers.Sftp/SftpContainer.cs
new file mode 100644
index 000000000..d93956f73
--- /dev/null
+++ b/src/Testcontainers.Sftp/SftpContainer.cs
@@ -0,0 +1,15 @@
+namespace Testcontainers.Sftp;
+
+///
+[PublicAPI]
+public sealed class SftpContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public SftpContainer(SftpConfiguration configuration)
+ : base(configuration)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj b/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj
new file mode 100644
index 000000000..9a25b9c4d
--- /dev/null
+++ b/src/Testcontainers.Sftp/Testcontainers.Sftp.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0;net9.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.Sftp/Usings.cs b/src/Testcontainers.Sftp/Usings.cs
new file mode 100644
index 000000000..fa3a104a1
--- /dev/null
+++ b/src/Testcontainers.Sftp/Usings.cs
@@ -0,0 +1,6 @@
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.Sftp.Tests/.editorconfig b/tests/Testcontainers.Sftp.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.Sftp.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs b/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs
new file mode 100644
index 000000000..e064280f8
--- /dev/null
+++ b/tests/Testcontainers.Sftp.Tests/SftpContainerTest.cs
@@ -0,0 +1,35 @@
+namespace Testcontainers.Sftp;
+
+public sealed class SftpContainerTest : IAsyncLifetime
+{
+ private readonly SftpContainer _sftpContainer = new SftpBuilder().Build();
+
+ public Task InitializeAsync()
+ {
+ return _sftpContainer.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _sftpContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task IsConnectedReturnsTrue()
+ {
+ // Given
+ var host = _sftpContainer.Hostname;
+
+ var port = _sftpContainer.GetMappedPublicPort(SftpBuilder.SftpPort);
+
+ using var sftpClient = new SftpClient(host, port, SftpBuilder.DefaultUsername, SftpBuilder.DefaultPassword);
+
+ // When
+ await sftpClient.ConnectAsync(CancellationToken.None)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.True(sftpClient.IsConnected);
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj b/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj
new file mode 100644
index 000000000..f9cfbbac6
--- /dev/null
+++ b/tests/Testcontainers.Sftp.Tests/Testcontainers.Sftp.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+ net9.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.Sftp.Tests/Usings.cs b/tests/Testcontainers.Sftp.Tests/Usings.cs
new file mode 100644
index 000000000..9e93730ce
--- /dev/null
+++ b/tests/Testcontainers.Sftp.Tests/Usings.cs
@@ -0,0 +1,5 @@
+global using System.Threading;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Commons;
+global using Renci.SshNet;
+global using Xunit;
\ No newline at end of file