Skip to content

Commit 08d526e

Browse files
ivicacclaude
andcommitted
4446 FTP
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 258d920 commit 08d526e

16 files changed

Lines changed: 4360 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# FTP Component - SFTP Support Implementation
2+
3+
## Overview
4+
5+
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.
6+
7+
## Changes Made
8+
9+
### 1. Added sshj Dependency
10+
11+
**File:** `build.gradle.kts`
12+
13+
```kotlin
14+
dependencies {
15+
implementation("commons-net:commons-net:3.11.1")
16+
implementation("com.hierynomus:sshj:0.40.0") // NEW
17+
}
18+
```
19+
20+
### 2. Added SFTP Constant
21+
22+
**File:** `src/main/java/com/bytechef/component/ftp/constant/FtpConstants.java`
23+
24+
```java
25+
public static final String SFTP = "sftp";
26+
```
27+
28+
### 3. Added SFTP Connection Property
29+
30+
**File:** `src/main/java/com/bytechef/component/ftp/connection/FtpConnection.java`
31+
32+
Added new boolean property to connection definition:
33+
```java
34+
bool(SFTP)
35+
.label("Use SFTP")
36+
.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.")
37+
.defaultValue(false)
38+
```
39+
40+
### 4. Created RemoteFileClient Interface
41+
42+
**File:** `src/main/java/com/bytechef/component/ftp/util/RemoteFileClient.java`
43+
44+
Created an abstraction interface for remote file operations supporting both FTP and SFTP:
45+
46+
```java
47+
public interface RemoteFileClient extends Closeable {
48+
49+
int DEFAULT_FTP_PORT = 21;
50+
int DEFAULT_SFTP_PORT = 22;
51+
52+
// Factory method to create appropriate client
53+
static RemoteFileClient of(Parameters connectionParameters);
54+
55+
// File operations
56+
void storeFile(String remotePath, InputStream inputStream) throws IOException;
57+
void retrieveFile(String remotePath, OutputStream outputStream) throws IOException;
58+
List<RemoteFileInfo> listFiles(String path) throws IOException;
59+
void deleteFile(String path) throws IOException;
60+
void deleteDirectory(String path) throws IOException;
61+
void rename(String oldPath, String newPath) throws IOException;
62+
void createDirectoryTree(String path) throws IOException;
63+
boolean isDirectory(String path) throws IOException;
64+
65+
record RemoteFileInfo(String name, String path, boolean directory, long size, Instant modifiedAt) {}
66+
}
67+
```
68+
69+
The interface includes:
70+
- Static factory method `of()` that creates either FTP or SFTP client based on connection parameters
71+
- Private static methods `createFtpClient()` and `createSftpClient()` for client instantiation
72+
- Common file operation methods
73+
74+
### 5. Created FtpRemoteFileClient Implementation
75+
76+
**File:** `src/main/java/com/bytechef/component/ftp/util/FtpRemoteFileClient.java`
77+
78+
FTP implementation using Apache Commons Net `FTPClient`:
79+
- Wraps `FTPClient` from commons-net
80+
- Implements all `RemoteFileClient` methods
81+
- Handles FTP-specific operations like passive/active mode
82+
83+
### 6. Created SftpRemoteFileClient Implementation
84+
85+
**File:** `src/main/java/com/bytechef/component/ftp/util/SftpRemoteFileClient.java`
86+
87+
SFTP implementation using sshj library:
88+
- Uses `SSHClient` and `SFTPClient` from sshj
89+
- Implements all `RemoteFileClient` methods
90+
- Handles SSH connection and authentication
91+
- Includes custom `InMemorySourceFile` and `InMemoryDestFile` implementations for stream-based transfers
92+
93+
### 7. Deleted FtpUtils.java
94+
95+
The utility class was removed as its functionality was moved into `RemoteFileClient.of()`.
96+
97+
### 8. Updated All Action Classes
98+
99+
Updated all action classes to use the new `RemoteFileClient` abstraction:
100+
101+
**Files updated:**
102+
- `FtpUploadFileAction.java`
103+
- `FtpDownloadFileAction.java`
104+
- `FtpListAction.java`
105+
- `FtpDeleteAction.java`
106+
- `FtpRenameAction.java`
107+
108+
**Change pattern:**
109+
```java
110+
// Before
111+
FTPClient ftpClient = FtpUtils.getFtpClient(connectionParameters);
112+
try {
113+
// operations using ftpClient
114+
} finally {
115+
FtpUtils.closeFtpClient(ftpClient);
116+
}
117+
118+
// After
119+
try (RemoteFileClient remoteFileClient = RemoteFileClient.of(connectionParameters)) {
120+
// operations using remoteFileClient
121+
}
122+
```
123+
124+
Also updated action descriptions from "FTP server" to "FTP/SFTP server".
125+
126+
## Protocol Comparison
127+
128+
| Feature | FTP | SFTP |
129+
|---------|-----|------|
130+
| Library | Apache Commons Net | sshj |
131+
| Default Port | 21 | 22 |
132+
| Encryption | None (or FTPS for TLS) | SSH-based |
133+
| Passive Mode | Yes | N/A |
134+
135+
## Usage
136+
137+
### FTP Connection (default)
138+
```json
139+
{
140+
"host": "ftp.example.com",
141+
"port": 21,
142+
"username": "user",
143+
"password": "pass",
144+
"passiveMode": true,
145+
"sftp": false
146+
}
147+
```
148+
149+
### SFTP Connection
150+
```json
151+
{
152+
"host": "sftp.example.com",
153+
"port": 22,
154+
"username": "user",
155+
"password": "pass",
156+
"sftp": true
157+
}
158+
```
159+
160+
## Testing
161+
162+
- Component definition test regenerated with new SFTP property
163+
- All tests pass with `./gradlew :server:libs:modules:components:ftp:test`
164+
- Spotless formatting applied
165+
166+
## File Structure After Changes
167+
168+
```
169+
server/libs/modules/components/ftp/
170+
├── build.gradle.kts # Added sshj dependency
171+
└── src/
172+
├── main/java/com/bytechef/component/ftp/
173+
│ ├── FtpComponentHandler.java
174+
│ ├── action/
175+
│ │ ├── FtpDeleteAction.java # Updated
176+
│ │ ├── FtpDownloadFileAction.java # Updated
177+
│ │ ├── FtpListAction.java # Updated
178+
│ │ ├── FtpRenameAction.java # Updated
179+
│ │ └── FtpUploadFileAction.java # Updated
180+
│ ├── connection/
181+
│ │ └── FtpConnection.java # Added SFTP property
182+
│ ├── constant/
183+
│ │ └── FtpConstants.java # Added SFTP constant
184+
│ └── util/
185+
│ ├── FtpRemoteFileClient.java # NEW
186+
│ ├── RemoteFileClient.java # NEW
187+
│ └── SftpRemoteFileClient.java # NEW
188+
└── test/resources/definition/
189+
└── ftp_v1.json # Regenerated
190+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version="1.0"
2+
3+
dependencies {
4+
implementation("commons-net:commons-net:3.11.1")
5+
implementation("com.hierynomus:sshj:0.40.0")
6+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2025 ByteChef
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.bytechef.component.ftp;
18+
19+
import static com.bytechef.component.definition.ComponentDsl.component;
20+
import static com.bytechef.component.definition.ComponentDsl.tool;
21+
22+
import com.bytechef.component.ComponentHandler;
23+
import com.bytechef.component.definition.ComponentCategory;
24+
import com.bytechef.component.definition.ComponentDefinition;
25+
import com.bytechef.component.ftp.action.FtpDeleteAction;
26+
import com.bytechef.component.ftp.action.FtpDownloadFileAction;
27+
import com.bytechef.component.ftp.action.FtpListAction;
28+
import com.bytechef.component.ftp.action.FtpRenameAction;
29+
import com.bytechef.component.ftp.action.FtpUploadFileAction;
30+
import com.bytechef.component.ftp.connection.FtpConnection;
31+
import com.google.auto.service.AutoService;
32+
33+
/**
34+
* @author Ivica Cardic
35+
*/
36+
@AutoService(ComponentHandler.class)
37+
public class FtpComponentHandler implements ComponentHandler {
38+
39+
private static final ComponentDefinition COMPONENT_DEFINITION = component("ftp")
40+
.title("FTP")
41+
.description(
42+
"FTP (File Transfer Protocol) is a standard network protocol for transferring files between a client " +
43+
"and a server. It allows uploading, downloading, and managing files on remote servers.")
44+
.icon("path:assets/ftp.svg")
45+
.categories(ComponentCategory.FILE_STORAGE, ComponentCategory.HELPERS)
46+
.connection(FtpConnection.CONNECTION_DEFINITION)
47+
.actions(
48+
FtpUploadFileAction.ACTION_DEFINITION,
49+
FtpDownloadFileAction.ACTION_DEFINITION,
50+
FtpListAction.ACTION_DEFINITION,
51+
FtpDeleteAction.ACTION_DEFINITION,
52+
FtpRenameAction.ACTION_DEFINITION)
53+
.clusterElements(
54+
tool(FtpUploadFileAction.ACTION_DEFINITION),
55+
tool(FtpDownloadFileAction.ACTION_DEFINITION),
56+
tool(FtpListAction.ACTION_DEFINITION),
57+
tool(FtpDeleteAction.ACTION_DEFINITION),
58+
tool(FtpRenameAction.ACTION_DEFINITION));
59+
60+
@Override
61+
public ComponentDefinition getDefinition() {
62+
return COMPONENT_DEFINITION;
63+
}
64+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2025 ByteChef
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.bytechef.component.ftp.action;
18+
19+
import static com.bytechef.component.definition.ComponentDsl.action;
20+
import static com.bytechef.component.definition.ComponentDsl.bool;
21+
import static com.bytechef.component.definition.ComponentDsl.object;
22+
import static com.bytechef.component.definition.ComponentDsl.outputSchema;
23+
import static com.bytechef.component.definition.ComponentDsl.sampleOutput;
24+
import static com.bytechef.component.definition.ComponentDsl.string;
25+
import static com.bytechef.component.ftp.constant.FtpConstants.PATH;
26+
import static com.bytechef.component.ftp.constant.FtpConstants.RECURSIVE;
27+
28+
import com.bytechef.component.definition.ComponentDsl.ModifiableActionDefinition;
29+
import com.bytechef.component.definition.Context;
30+
import com.bytechef.component.definition.Parameters;
31+
import com.bytechef.component.exception.ProviderException;
32+
import com.bytechef.component.ftp.util.RemoteFileClient;
33+
import com.bytechef.component.ftp.util.RemoteFileClient.RemoteFileInfo;
34+
import java.io.IOException;
35+
import java.util.List;
36+
import java.util.Map;
37+
38+
/**
39+
* @author Ivica Cardic
40+
*/
41+
public class FtpDeleteAction {
42+
43+
public static final ModifiableActionDefinition ACTION_DEFINITION = action("delete")
44+
.title("Delete")
45+
.description("Deletes a file or directory from the FTP/SFTP server.")
46+
.properties(
47+
string(PATH)
48+
.label("Path")
49+
.description("The path of the file or directory to delete.")
50+
.placeholder("/uploads/old-file.pdf")
51+
.required(true),
52+
bool(RECURSIVE)
53+
.label("Recursive")
54+
.description("If the path is a directory, delete all contents recursively.")
55+
.defaultValue(false))
56+
.output(
57+
outputSchema(
58+
object()
59+
.properties(
60+
string("deletedPath").description("The path that was deleted."),
61+
bool("success").description("Whether the deletion was successful."))),
62+
sampleOutput(Map.of("deletedPath", "/uploads/old-file.pdf", "success", true)))
63+
.perform(FtpDeleteAction::perform);
64+
65+
private FtpDeleteAction() {
66+
}
67+
68+
protected static Map<String, Object> perform(
69+
Parameters inputParameters, Parameters connectionParameters, Context context) {
70+
71+
try (RemoteFileClient remoteFileClient = RemoteFileClient.of(connectionParameters)) {
72+
String path = inputParameters.getRequiredString(PATH);
73+
boolean recursive = inputParameters.getBoolean(RECURSIVE, false);
74+
75+
if (remoteFileClient.isDirectory(path)) {
76+
if (recursive) {
77+
deleteDirectoryRecursively(remoteFileClient, path);
78+
} else {
79+
remoteFileClient.deleteDirectory(path);
80+
}
81+
} else {
82+
remoteFileClient.deleteFile(path);
83+
}
84+
85+
return Map.of("deletedPath", path, "success", true);
86+
} catch (IOException ioException) {
87+
throw new ProviderException("Failed to delete: " + ioException.getMessage(), ioException);
88+
}
89+
}
90+
91+
private static void deleteDirectoryRecursively(RemoteFileClient remoteFileClient, String directoryPath)
92+
throws IOException {
93+
94+
List<RemoteFileInfo> files = remoteFileClient.listFiles(directoryPath);
95+
96+
for (RemoteFileInfo file : files) {
97+
if (file.directory()) {
98+
deleteDirectoryRecursively(remoteFileClient, file.path());
99+
} else {
100+
remoteFileClient.deleteFile(file.path());
101+
}
102+
}
103+
104+
remoteFileClient.deleteDirectory(directoryPath);
105+
}
106+
}

0 commit comments

Comments
 (0)