Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions server/libs/modules/components/ftp/PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# FTP Component - SFTP Support Implementation

## Overview

Added SFTP (SSH File Transfer Protocol) support to the FTP component as a boolean connection option. Users can now choose between FTP and SFTP protocols when configuring their connection.

## Changes Made

### 1. Added sshj Dependency

**File:** `build.gradle.kts`

```kotlin
dependencies {
implementation("commons-net:commons-net:3.11.1")
implementation("com.hierynomus:sshj:0.40.0") // NEW
}
```

### 2. Added SFTP Constant

**File:** `src/main/java/com/bytechef/component/ftp/constant/FtpConstants.java`

```java
public static final String SFTP = "sftp";
```

### 3. Added SFTP Connection Property

**File:** `src/main/java/com/bytechef/component/ftp/connection/FtpConnection.java`

Added new boolean property to connection definition:
```java
bool(SFTP)
.label("Use SFTP")
.description("Use SFTP (SSH File Transfer Protocol) instead of FTP. SFTP provides encrypted file transfer over SSH. When enabled, the port defaults to 22 instead of 21.")
.defaultValue(false)
```

### 4. Created RemoteFileClient Interface

**File:** `src/main/java/com/bytechef/component/ftp/util/RemoteFileClient.java`

Created an abstraction interface for remote file operations supporting both FTP and SFTP:

```java
public interface RemoteFileClient extends Closeable {

int DEFAULT_FTP_PORT = 21;
int DEFAULT_SFTP_PORT = 22;

// Factory method to create appropriate client
static RemoteFileClient of(Parameters connectionParameters);

// File operations
void storeFile(String remotePath, InputStream inputStream) throws IOException;
void retrieveFile(String remotePath, OutputStream outputStream) throws IOException;
List<RemoteFileInfo> listFiles(String path) throws IOException;
void deleteFile(String path) throws IOException;
void deleteDirectory(String path) throws IOException;
void rename(String oldPath, String newPath) throws IOException;
void createDirectoryTree(String path) throws IOException;
boolean isDirectory(String path) throws IOException;

record RemoteFileInfo(String name, String path, boolean directory, long size, Instant modifiedAt) {}
}
```

The interface includes:
- Static factory method `of()` that creates either FTP or SFTP client based on connection parameters
- Private static methods `createFtpClient()` and `createSftpClient()` for client instantiation
- Common file operation methods

### 5. Created FtpRemoteFileClient Implementation

**File:** `src/main/java/com/bytechef/component/ftp/util/FtpRemoteFileClient.java`

FTP implementation using Apache Commons Net `FTPClient`:
- Wraps `FTPClient` from commons-net
- Implements all `RemoteFileClient` methods
- Handles FTP-specific operations like passive/active mode

### 6. Created SftpRemoteFileClient Implementation

**File:** `src/main/java/com/bytechef/component/ftp/util/SftpRemoteFileClient.java`

SFTP implementation using sshj library:
- Uses `SSHClient` and `SFTPClient` from sshj
- Implements all `RemoteFileClient` methods
- Handles SSH connection and authentication
- Includes custom `InMemorySourceFile` and `InMemoryDestFile` implementations for stream-based transfers

### 7. Deleted FtpUtils.java

The utility class was removed as its functionality was moved into `RemoteFileClient.of()`.

### 8. Updated All Action Classes

Updated all action classes to use the new `RemoteFileClient` abstraction:

**Files updated:**
- `FtpUploadFileAction.java`
- `FtpDownloadFileAction.java`
- `FtpListAction.java`
- `FtpDeleteAction.java`
- `FtpRenameAction.java`

**Change pattern:**
```java
// Before
FTPClient ftpClient = FtpUtils.getFtpClient(connectionParameters);
try {
// operations using ftpClient
} finally {
FtpUtils.closeFtpClient(ftpClient);
}

// After
try (RemoteFileClient remoteFileClient = RemoteFileClient.of(connectionParameters)) {
// operations using remoteFileClient
}
```

Also updated action descriptions from "FTP server" to "FTP/SFTP server".

## Protocol Comparison

| Feature | FTP | SFTP |
|---------|-----|------|
| Library | Apache Commons Net | sshj |
| Default Port | 21 | 22 |
| Encryption | None (or FTPS for TLS) | SSH-based |
| Passive Mode | Yes | N/A |

## Usage

### FTP Connection (default)
```json
{
"host": "ftp.example.com",
"port": 21,
"username": "user",
"password": "pass",
"passiveMode": true,
"sftp": false
}
```

### SFTP Connection
```json
{
"host": "sftp.example.com",
"port": 22,
"username": "user",
"password": "pass",
"sftp": true
}
```

## Testing

- Component definition test regenerated with new SFTP property
- All tests pass with `./gradlew :server:libs:modules:components:ftp:test`
- Spotless formatting applied

## File Structure After Changes

```
server/libs/modules/components/ftp/
├── build.gradle.kts # Added sshj dependency
└── src/
├── main/java/com/bytechef/component/ftp/
│ ├── FtpComponentHandler.java
│ ├── action/
│ │ ├── FtpDeleteAction.java # Updated
│ │ ├── FtpDownloadFileAction.java # Updated
│ │ ├── FtpListAction.java # Updated
│ │ ├── FtpRenameAction.java # Updated
│ │ └── FtpUploadFileAction.java # Updated
│ ├── connection/
│ │ └── FtpConnection.java # Added SFTP property
│ ├── constant/
│ │ └── FtpConstants.java # Added SFTP constant
│ └── util/
│ ├── FtpRemoteFileClient.java # NEW
│ ├── RemoteFileClient.java # NEW
│ └── SftpRemoteFileClient.java # NEW
└── test/resources/definition/
└── ftp_v1.json # Regenerated
```
6 changes: 6 additions & 0 deletions server/libs/modules/components/ftp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version="1.0"

dependencies {
implementation("commons-net:commons-net:3.11.1")
implementation("com.hierynomus:sshj:0.40.0")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2025 ByteChef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bytechef.component.ftp;

import static com.bytechef.component.definition.ComponentDsl.component;
import static com.bytechef.component.definition.ComponentDsl.tool;

import com.bytechef.component.ComponentHandler;
import com.bytechef.component.definition.ComponentCategory;
import com.bytechef.component.definition.ComponentDefinition;
import com.bytechef.component.ftp.action.FtpDeleteAction;
import com.bytechef.component.ftp.action.FtpDownloadFileAction;
import com.bytechef.component.ftp.action.FtpListAction;
import com.bytechef.component.ftp.action.FtpRenameAction;
import com.bytechef.component.ftp.action.FtpUploadFileAction;
import com.bytechef.component.ftp.connection.FtpConnection;
import com.google.auto.service.AutoService;

/**
* @author Ivica Cardic
*/
@AutoService(ComponentHandler.class)
public class FtpComponentHandler implements ComponentHandler {

private static final ComponentDefinition COMPONENT_DEFINITION = component("ftp")
.title("FTP")
.description(
"FTP (File Transfer Protocol) is a standard network protocol for transferring files between a client " +
"and a server. It allows uploading, downloading, and managing files on remote servers.")
.icon("path:assets/ftp.svg")
.categories(ComponentCategory.FILE_STORAGE, ComponentCategory.HELPERS)
.connection(FtpConnection.CONNECTION_DEFINITION)
.actions(
FtpUploadFileAction.ACTION_DEFINITION,
FtpDownloadFileAction.ACTION_DEFINITION,
FtpListAction.ACTION_DEFINITION,
FtpDeleteAction.ACTION_DEFINITION,
FtpRenameAction.ACTION_DEFINITION)
.clusterElements(
tool(FtpUploadFileAction.ACTION_DEFINITION),
tool(FtpDownloadFileAction.ACTION_DEFINITION),
tool(FtpListAction.ACTION_DEFINITION),
tool(FtpDeleteAction.ACTION_DEFINITION),
tool(FtpRenameAction.ACTION_DEFINITION));

@Override
public ComponentDefinition getDefinition() {
return COMPONENT_DEFINITION;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2025 ByteChef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bytechef.component.ftp.action;

import static com.bytechef.component.definition.ComponentDsl.action;
import static com.bytechef.component.definition.ComponentDsl.bool;
import static com.bytechef.component.definition.ComponentDsl.object;
import static com.bytechef.component.definition.ComponentDsl.outputSchema;
import static com.bytechef.component.definition.ComponentDsl.sampleOutput;
import static com.bytechef.component.definition.ComponentDsl.string;
import static com.bytechef.component.ftp.constant.FtpConstants.PATH;
import static com.bytechef.component.ftp.constant.FtpConstants.RECURSIVE;

import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition;
import com.bytechef.component.definition.Context;
import com.bytechef.component.definition.Parameters;
import com.bytechef.component.exception.ProviderException;
import com.bytechef.component.ftp.util.RemoteFileClient;
import com.bytechef.component.ftp.util.RemoteFileClient.RemoteFileInfo;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
* @author Ivica Cardic
*/
public class FtpDeleteAction {

public static final ModifiableActionDefinition ACTION_DEFINITION = action("delete")
.title("Delete")
.description("Deletes a file or directory from the FTP/SFTP server.")
.properties(
string(PATH)
.label("Path")
.description("The path of the file or directory to delete.")
.placeholder("/uploads/old-file.pdf")
.required(true),
bool(RECURSIVE)
.label("Recursive")
.description("If the path is a directory, delete all contents recursively.")
.defaultValue(false))
.output(
outputSchema(
object()
.properties(
string("deletedPath").description("The path that was deleted."),
bool("success").description("Whether the deletion was successful."))),
sampleOutput(Map.of("deletedPath", "/uploads/old-file.pdf", "success", true)))
.perform(FtpDeleteAction::perform);

private FtpDeleteAction() {
}

protected static Map<String, Object> perform(
Parameters inputParameters, Parameters connectionParameters, Context context) {

try (RemoteFileClient remoteFileClient = RemoteFileClient.of(connectionParameters)) {
String path = inputParameters.getRequiredString(PATH);
boolean recursive = inputParameters.getBoolean(RECURSIVE, false);

if (remoteFileClient.isDirectory(path)) {
if (recursive) {
deleteDirectoryRecursively(remoteFileClient, path);
} else {
remoteFileClient.deleteDirectory(path);
}
} else {
remoteFileClient.deleteFile(path);
}

return Map.of("deletedPath", path, "success", true);
} catch (IOException ioException) {
throw new ProviderException("Failed to delete: " + ioException.getMessage(), ioException);
}
}

private static void deleteDirectoryRecursively(RemoteFileClient remoteFileClient, String directoryPath)
throws IOException {

List<RemoteFileInfo> files = remoteFileClient.listFiles(directoryPath);

for (RemoteFileInfo file : files) {
if (file.directory()) {
deleteDirectoryRecursively(remoteFileClient, file.path());
} else {
remoteFileClient.deleteFile(file.path());
}
}

remoteFileClient.deleteDirectory(directoryPath);
}
}
Loading