node-red-contrib-best-sftp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -0
- package/nodes/sftp-config.html +95 -0
- package/nodes/sftp-config.js +29 -0
- package/nodes/sftp.html +209 -0
- package/nodes/sftp.js +196 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# node-red-contrib-best-sftp
|
|
2
|
+
|
|
3
|
+
A reliable SFTP client node for Node-RED with keyboard-interactive authentication support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multiple authentication methods**: Password, keyboard-interactive, and SSH private key
|
|
8
|
+
- **9 SFTP operations**: list, get, put, delete, mkdir, rmdir, rename, exists, stat
|
|
9
|
+
- **Keyboard-interactive auth**: Works with servers that require interactive authentication
|
|
10
|
+
- **Dynamic configuration**: Override settings via message properties
|
|
11
|
+
- **Status indicators**: Visual feedback for connection and operation states
|
|
12
|
+
- **Secure credential storage**: Passwords and keys encrypted by Node-RED
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### Via Node-RED Palette Manager
|
|
17
|
+
|
|
18
|
+
1. Open Node-RED
|
|
19
|
+
2. Go to Menu → Manage Palette → Install
|
|
20
|
+
3. Search for `node-red-contrib-best-sftp`
|
|
21
|
+
4. Click Install
|
|
22
|
+
|
|
23
|
+
### Via npm
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/.node-red
|
|
27
|
+
npm install node-red-contrib-best-sftp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then restart Node-RED.
|
|
31
|
+
|
|
32
|
+
## Nodes
|
|
33
|
+
|
|
34
|
+
### sftp-config
|
|
35
|
+
|
|
36
|
+
Configuration node that stores SFTP server connection settings.
|
|
37
|
+
|
|
38
|
+
| Property | Type | Description |
|
|
39
|
+
|----------|------|-------------|
|
|
40
|
+
| Host | string | SFTP server hostname or IP |
|
|
41
|
+
| Port | number | SFTP port (default: 22) |
|
|
42
|
+
| Username | string | Authentication username |
|
|
43
|
+
| Password | string | Authentication password |
|
|
44
|
+
| Keyboard Auth | boolean | Enable keyboard-interactive authentication |
|
|
45
|
+
| Private Key | string | SSH private key (optional) |
|
|
46
|
+
| Passphrase | string | Private key passphrase (optional) |
|
|
47
|
+
|
|
48
|
+
### sftp-best
|
|
49
|
+
|
|
50
|
+
Main SFTP operations node.
|
|
51
|
+
|
|
52
|
+
| Property | Type | Description |
|
|
53
|
+
|----------|------|-------------|
|
|
54
|
+
| Server | sftp-config | Reference to server configuration |
|
|
55
|
+
| Operation | string | SFTP operation to perform |
|
|
56
|
+
| Remote Path | string | Path on the remote server |
|
|
57
|
+
| Local Path | string | Local file path (for get/put operations) |
|
|
58
|
+
| Recursive | boolean | Recursive mode for mkdir/rmdir |
|
|
59
|
+
|
|
60
|
+
## Operations
|
|
61
|
+
|
|
62
|
+
### list
|
|
63
|
+
Lists contents of a remote directory.
|
|
64
|
+
|
|
65
|
+
**Input:** `msg.remotePath` or `msg.payload` (path string)
|
|
66
|
+
**Output:** `msg.payload` - Array of file objects with `name`, `type`, `size`, `modifyTime`
|
|
67
|
+
|
|
68
|
+
### get
|
|
69
|
+
Downloads a file from the remote server.
|
|
70
|
+
|
|
71
|
+
**Input:** `msg.remotePath` - Remote file path
|
|
72
|
+
**Output:** `msg.payload` - File contents as Buffer, or `{success, localPath}` if local path specified
|
|
73
|
+
|
|
74
|
+
### put
|
|
75
|
+
Uploads a file to the remote server.
|
|
76
|
+
|
|
77
|
+
**Input:** `msg.payload` - Buffer with file contents, or use `msg.localPath` for local file
|
|
78
|
+
**Output:** `msg.payload` - `{success: true, remotePath}`
|
|
79
|
+
|
|
80
|
+
### delete
|
|
81
|
+
Deletes a file on the remote server.
|
|
82
|
+
|
|
83
|
+
**Input:** `msg.remotePath` - File to delete
|
|
84
|
+
**Output:** `msg.payload` - `{success: true, deleted}`
|
|
85
|
+
|
|
86
|
+
### mkdir
|
|
87
|
+
Creates a directory on the remote server.
|
|
88
|
+
|
|
89
|
+
**Input:** `msg.remotePath` - Directory path, `msg.recursive` - Create parent directories
|
|
90
|
+
**Output:** `msg.payload` - `{success: true, created}`
|
|
91
|
+
|
|
92
|
+
### rmdir
|
|
93
|
+
Removes a directory on the remote server.
|
|
94
|
+
|
|
95
|
+
**Input:** `msg.remotePath` - Directory path, `msg.recursive` - Remove contents
|
|
96
|
+
**Output:** `msg.payload` - `{success: true, removed}`
|
|
97
|
+
|
|
98
|
+
### rename
|
|
99
|
+
Renames or moves a file/directory.
|
|
100
|
+
|
|
101
|
+
**Input:** `msg.remotePath` - Source path, `msg.newPath` - Destination path
|
|
102
|
+
**Output:** `msg.payload` - `{success: true, from, to}`
|
|
103
|
+
|
|
104
|
+
### exists
|
|
105
|
+
Checks if a path exists on the remote server.
|
|
106
|
+
|
|
107
|
+
**Input:** `msg.remotePath` - Path to check
|
|
108
|
+
**Output:** `msg.payload` - `'d'` (directory), `'-'` (file), `'l'` (link), or `false`; `msg.exists` - boolean
|
|
109
|
+
|
|
110
|
+
### stat
|
|
111
|
+
Gets file/directory information.
|
|
112
|
+
|
|
113
|
+
**Input:** `msg.remotePath` - Path to stat
|
|
114
|
+
**Output:** `msg.payload` - Stats object with `size`, `mode`, `accessTime`, `modifyTime`
|
|
115
|
+
|
|
116
|
+
## Message Properties
|
|
117
|
+
|
|
118
|
+
All operations add metadata to the output message:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
msg.sftp = {
|
|
122
|
+
operation: 'list',
|
|
123
|
+
remotePath: '/home/user/files',
|
|
124
|
+
host: 'sftp.example.com',
|
|
125
|
+
port: 22
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Input Overrides
|
|
130
|
+
|
|
131
|
+
You can override node configuration via message properties:
|
|
132
|
+
|
|
133
|
+
| Property | Description |
|
|
134
|
+
|----------|-------------|
|
|
135
|
+
| `msg.operation` | Override the operation type |
|
|
136
|
+
| `msg.remotePath` | Override the remote path |
|
|
137
|
+
| `msg.localPath` | Override the local path |
|
|
138
|
+
| `msg.newPath` | Destination path for rename |
|
|
139
|
+
| `msg.recursive` | Override recursive setting |
|
|
140
|
+
| `msg.host` | Override server host |
|
|
141
|
+
| `msg.port` | Override server port |
|
|
142
|
+
| `msg.username` | Override username |
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
### List Directory
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
msg.remotePath = '/home/user/documents';
|
|
150
|
+
msg.operation = 'list';
|
|
151
|
+
return msg;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Download File to Buffer
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
msg.remotePath = '/home/user/data.csv';
|
|
158
|
+
msg.operation = 'get';
|
|
159
|
+
return msg;
|
|
160
|
+
// Output: msg.payload contains file buffer
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Upload from Buffer
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
msg.payload = Buffer.from('Hello World');
|
|
167
|
+
msg.remotePath = '/home/user/hello.txt';
|
|
168
|
+
msg.operation = 'put';
|
|
169
|
+
return msg;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Check if File Exists
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
msg.remotePath = '/home/user/config.json';
|
|
176
|
+
msg.operation = 'exists';
|
|
177
|
+
return msg;
|
|
178
|
+
// Output: msg.exists = true/false
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Status Indicators
|
|
182
|
+
|
|
183
|
+
| Status | Meaning |
|
|
184
|
+
|--------|---------|
|
|
185
|
+
| Yellow dot | Connecting to server |
|
|
186
|
+
| Green dot | Connected / Operation complete |
|
|
187
|
+
| Blue dot | Operation in progress |
|
|
188
|
+
| Red ring | Error occurred |
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
- Node.js >= 18.0.0
|
|
193
|
+
- Node-RED >= 2.0.0
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
198
|
+
|
|
199
|
+
## Contributing
|
|
200
|
+
|
|
201
|
+
Issues and pull requests welcome at [GitHub](https://github.com/digitalnodecom/node-red-contrib-best-sftp).
|
|
202
|
+
|
|
203
|
+
## Author
|
|
204
|
+
|
|
205
|
+
Trajche <trajche@kralev.eu>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('sftp-config', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
host: { value: '', required: true },
|
|
7
|
+
port: { value: 22, required: true, validate: RED.validators.number() },
|
|
8
|
+
tryKeyboard: { value: true }
|
|
9
|
+
},
|
|
10
|
+
credentials: {
|
|
11
|
+
username: { type: 'text' },
|
|
12
|
+
password: { type: 'password' },
|
|
13
|
+
privateKey: { type: 'password' },
|
|
14
|
+
passphrase: { type: 'password' }
|
|
15
|
+
},
|
|
16
|
+
label: function() {
|
|
17
|
+
if (this.name) {
|
|
18
|
+
return this.name;
|
|
19
|
+
}
|
|
20
|
+
var user = this.credentials && this.credentials.username ? this.credentials.username : 'user';
|
|
21
|
+
return user + '@' + (this.host || 'sftp-server') + ':' + (this.port || 22);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script type="text/html" data-template-name="sftp-config">
|
|
27
|
+
<div class="form-row">
|
|
28
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
29
|
+
<input type="text" id="node-config-input-name" placeholder="Optional display name">
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-config-input-host"><i class="fa fa-server"></i> Host</label>
|
|
33
|
+
<input type="text" id="node-config-input-host" placeholder="sftp.example.com">
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-row">
|
|
36
|
+
<label for="node-config-input-port"><i class="fa fa-hashtag"></i> Port</label>
|
|
37
|
+
<input type="number" id="node-config-input-port" placeholder="22">
|
|
38
|
+
</div>
|
|
39
|
+
<hr>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
|
|
42
|
+
<input type="text" id="node-config-input-username" placeholder="username">
|
|
43
|
+
</div>
|
|
44
|
+
<div class="form-row">
|
|
45
|
+
<label for="node-config-input-password"><i class="fa fa-lock"></i> Password</label>
|
|
46
|
+
<input type="password" id="node-config-input-password" placeholder="password">
|
|
47
|
+
</div>
|
|
48
|
+
<div class="form-row">
|
|
49
|
+
<label for="node-config-input-tryKeyboard"><i class="fa fa-keyboard-o"></i> Keyboard Auth</label>
|
|
50
|
+
<input type="checkbox" id="node-config-input-tryKeyboard" style="width:auto; margin-left:0;">
|
|
51
|
+
<span style="margin-left:5px;">Use keyboard-interactive authentication</span>
|
|
52
|
+
</div>
|
|
53
|
+
<hr>
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<label for="node-config-input-privateKey"><i class="fa fa-key"></i> Private Key</label>
|
|
56
|
+
<textarea id="node-config-input-privateKey" rows="4" style="width:70%" placeholder="-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----"></textarea>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label for="node-config-input-passphrase"><i class="fa fa-shield"></i> Passphrase</label>
|
|
60
|
+
<input type="password" id="node-config-input-passphrase" placeholder="Key passphrase (if encrypted)">
|
|
61
|
+
</div>
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<script type="text/html" data-help-name="sftp-config">
|
|
65
|
+
<p>Configuration node for SFTP server connection settings.</p>
|
|
66
|
+
|
|
67
|
+
<h3>Settings</h3>
|
|
68
|
+
<dl class="message-properties">
|
|
69
|
+
<dt>Host <span class="property-type">string</span></dt>
|
|
70
|
+
<dd>The SFTP server hostname or IP address.</dd>
|
|
71
|
+
|
|
72
|
+
<dt>Port <span class="property-type">number</span></dt>
|
|
73
|
+
<dd>The SFTP server port (default: 22).</dd>
|
|
74
|
+
|
|
75
|
+
<dt>Username <span class="property-type">string</span></dt>
|
|
76
|
+
<dd>The username for authentication.</dd>
|
|
77
|
+
|
|
78
|
+
<dt>Password <span class="property-type">string</span></dt>
|
|
79
|
+
<dd>The password for authentication. Used for password and keyboard-interactive auth.</dd>
|
|
80
|
+
|
|
81
|
+
<dt>Private Key <span class="property-type">string</span></dt>
|
|
82
|
+
<dd>Optional SSH private key for key-based authentication.</dd>
|
|
83
|
+
|
|
84
|
+
<dt>Passphrase <span class="property-type">string</span></dt>
|
|
85
|
+
<dd>Passphrase for encrypted private keys.</dd>
|
|
86
|
+
</dl>
|
|
87
|
+
|
|
88
|
+
<h3>Authentication</h3>
|
|
89
|
+
<p>This node supports multiple authentication methods:</p>
|
|
90
|
+
<ul>
|
|
91
|
+
<li><b>Password</b> - Standard password authentication</li>
|
|
92
|
+
<li><b>Keyboard-interactive</b> - Interactive authentication (uses password)</li>
|
|
93
|
+
<li><b>Public key</b> - SSH key-based authentication</li>
|
|
94
|
+
</ul>
|
|
95
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function SFTPConfigNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
|
|
7
|
+
// Non-sensitive configuration
|
|
8
|
+
this.name = config.name;
|
|
9
|
+
this.host = config.host;
|
|
10
|
+
this.port = config.port || 22;
|
|
11
|
+
this.tryKeyboard = config.tryKeyboard;
|
|
12
|
+
|
|
13
|
+
// Credentials (stored encrypted)
|
|
14
|
+
const creds = this.credentials || {};
|
|
15
|
+
this.username = creds.username;
|
|
16
|
+
this.password = creds.password;
|
|
17
|
+
this.privateKey = creds.privateKey;
|
|
18
|
+
this.passphrase = creds.passphrase;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
RED.nodes.registerType('sftp-config', SFTPConfigNode, {
|
|
22
|
+
credentials: {
|
|
23
|
+
username: { type: 'text' },
|
|
24
|
+
password: { type: 'password' },
|
|
25
|
+
privateKey: { type: 'password' },
|
|
26
|
+
passphrase: { type: 'password' }
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
package/nodes/sftp.html
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('sftp-best', {
|
|
3
|
+
category: 'storage',
|
|
4
|
+
color: '#4B8BBE',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
server: { value: '', type: 'sftp-config', required: true },
|
|
8
|
+
operation: { value: 'list' },
|
|
9
|
+
remotePath: { value: '/' },
|
|
10
|
+
localPath: { value: '' },
|
|
11
|
+
recursive: { value: false }
|
|
12
|
+
},
|
|
13
|
+
inputs: 1,
|
|
14
|
+
outputs: 1,
|
|
15
|
+
icon: 'file.svg',
|
|
16
|
+
paletteLabel: 'SFTP',
|
|
17
|
+
label: function() {
|
|
18
|
+
if (this.name) {
|
|
19
|
+
return this.name;
|
|
20
|
+
}
|
|
21
|
+
var op = this.operation || 'list';
|
|
22
|
+
return 'sftp-best ' + op;
|
|
23
|
+
},
|
|
24
|
+
labelStyle: function() {
|
|
25
|
+
return this.name ? 'node_label_italic' : '';
|
|
26
|
+
},
|
|
27
|
+
oneditprepare: function() {
|
|
28
|
+
var node = this;
|
|
29
|
+
|
|
30
|
+
function updateVisibility() {
|
|
31
|
+
var op = $('#node-input-operation').val();
|
|
32
|
+
|
|
33
|
+
// Hide all optional rows first
|
|
34
|
+
$('.sftp-best-localpath-row').hide();
|
|
35
|
+
$('.sftp-best-recursive-row').hide();
|
|
36
|
+
|
|
37
|
+
// Show relevant fields based on operation
|
|
38
|
+
switch (op) {
|
|
39
|
+
case 'get':
|
|
40
|
+
$('.sftp-best-localpath-row').show();
|
|
41
|
+
$('#localpath-label').text('Local Path (optional)');
|
|
42
|
+
$('#localpath-help').text('Leave empty to return file as buffer');
|
|
43
|
+
break;
|
|
44
|
+
case 'put':
|
|
45
|
+
$('.sftp-best-localpath-row').show();
|
|
46
|
+
$('#localpath-label').text('Local Path');
|
|
47
|
+
$('#localpath-help').text('Source file path, or send buffer in msg.payload');
|
|
48
|
+
break;
|
|
49
|
+
case 'rename':
|
|
50
|
+
$('.sftp-best-localpath-row').show();
|
|
51
|
+
$('#localpath-label').text('New Path');
|
|
52
|
+
$('#localpath-help').text('New remote path/name');
|
|
53
|
+
break;
|
|
54
|
+
case 'mkdir':
|
|
55
|
+
case 'rmdir':
|
|
56
|
+
$('.sftp-best-recursive-row').show();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$('#node-input-operation').on('change', updateVisibility);
|
|
62
|
+
updateVisibility();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<script type="text/html" data-template-name="sftp-best">
|
|
68
|
+
<div class="form-row">
|
|
69
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
70
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
71
|
+
</div>
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<label for="node-input-server"><i class="fa fa-server"></i> Server</label>
|
|
74
|
+
<input type="text" id="node-input-server">
|
|
75
|
+
</div>
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-operation"><i class="fa fa-tasks"></i> Operation</label>
|
|
78
|
+
<select id="node-input-operation">
|
|
79
|
+
<option value="list">List Directory</option>
|
|
80
|
+
<option value="get">Download File</option>
|
|
81
|
+
<option value="put">Upload File</option>
|
|
82
|
+
<option value="delete">Delete File</option>
|
|
83
|
+
<option value="mkdir">Create Directory</option>
|
|
84
|
+
<option value="rmdir">Remove Directory</option>
|
|
85
|
+
<option value="rename">Rename/Move</option>
|
|
86
|
+
<option value="exists">Check Exists</option>
|
|
87
|
+
<option value="stat">Get File Info</option>
|
|
88
|
+
</select>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="form-row">
|
|
91
|
+
<label for="node-input-remotePath"><i class="fa fa-folder-open"></i> Remote Path</label>
|
|
92
|
+
<input type="text" id="node-input-remotePath" placeholder="/">
|
|
93
|
+
</div>
|
|
94
|
+
<div class="form-row sftp-best-localpath-row">
|
|
95
|
+
<label for="node-input-localPath"><i class="fa fa-file"></i> <span id="localpath-label">Local Path</span></label>
|
|
96
|
+
<input type="text" id="node-input-localPath" placeholder="">
|
|
97
|
+
<div class="form-tips" id="localpath-help"></div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="form-row sftp-best-recursive-row">
|
|
100
|
+
<label for="node-input-recursive"><i class="fa fa-sitemap"></i> Recursive</label>
|
|
101
|
+
<input type="checkbox" id="node-input-recursive" style="width:auto; margin-left:0;">
|
|
102
|
+
<span style="margin-left:5px;">Create/remove subdirectories</span>
|
|
103
|
+
</div>
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<script type="text/html" data-help-name="sftp-best">
|
|
107
|
+
<p>Performs SFTP operations on a remote server using ssh2-sftp-client.</p>
|
|
108
|
+
|
|
109
|
+
<h3>Operations</h3>
|
|
110
|
+
<dl class="message-properties">
|
|
111
|
+
<dt>list</dt>
|
|
112
|
+
<dd>Lists contents of a remote directory. Returns array of file objects.</dd>
|
|
113
|
+
|
|
114
|
+
<dt>get</dt>
|
|
115
|
+
<dd>Downloads a file. Returns file buffer or writes to local path.</dd>
|
|
116
|
+
|
|
117
|
+
<dt>put</dt>
|
|
118
|
+
<dd>Uploads a file from local path or msg.payload buffer.</dd>
|
|
119
|
+
|
|
120
|
+
<dt>delete</dt>
|
|
121
|
+
<dd>Deletes a remote file.</dd>
|
|
122
|
+
|
|
123
|
+
<dt>mkdir</dt>
|
|
124
|
+
<dd>Creates a remote directory. Use recursive for nested paths.</dd>
|
|
125
|
+
|
|
126
|
+
<dt>rmdir</dt>
|
|
127
|
+
<dd>Removes a remote directory. Use recursive to remove contents.</dd>
|
|
128
|
+
|
|
129
|
+
<dt>rename</dt>
|
|
130
|
+
<dd>Renames or moves a remote file/directory.</dd>
|
|
131
|
+
|
|
132
|
+
<dt>exists</dt>
|
|
133
|
+
<dd>Checks if path exists. Returns 'd' (dir), '-' (file), 'l' (link), or false.</dd>
|
|
134
|
+
|
|
135
|
+
<dt>stat</dt>
|
|
136
|
+
<dd>Gets file/directory statistics (size, permissions, times).</dd>
|
|
137
|
+
</dl>
|
|
138
|
+
|
|
139
|
+
<h3>Inputs</h3>
|
|
140
|
+
<dl class="message-properties">
|
|
141
|
+
<dt>payload <span class="property-type">string | buffer</span></dt>
|
|
142
|
+
<dd>For list/get/delete/exists/stat: remote path (overrides config).
|
|
143
|
+
For put: file content as buffer.</dd>
|
|
144
|
+
|
|
145
|
+
<dt class="optional">remotePath <span class="property-type">string</span></dt>
|
|
146
|
+
<dd>Override the remote path setting.</dd>
|
|
147
|
+
|
|
148
|
+
<dt class="optional">localPath <span class="property-type">string</span></dt>
|
|
149
|
+
<dd>Override the local path setting.</dd>
|
|
150
|
+
|
|
151
|
+
<dt class="optional">operation <span class="property-type">string</span></dt>
|
|
152
|
+
<dd>Override the operation type.</dd>
|
|
153
|
+
|
|
154
|
+
<dt class="optional">newPath <span class="property-type">string</span></dt>
|
|
155
|
+
<dd>For rename: the destination path.</dd>
|
|
156
|
+
|
|
157
|
+
<dt class="optional">recursive <span class="property-type">boolean</span></dt>
|
|
158
|
+
<dd>For mkdir/rmdir: create/remove subdirectories.</dd>
|
|
159
|
+
</dl>
|
|
160
|
+
|
|
161
|
+
<h3>Outputs</h3>
|
|
162
|
+
<dl class="message-properties">
|
|
163
|
+
<dt>payload <span class="property-type">varies</span></dt>
|
|
164
|
+
<dd>
|
|
165
|
+
<ul>
|
|
166
|
+
<li><b>list:</b> Array of file objects with name, type, size, modifyTime</li>
|
|
167
|
+
<li><b>get:</b> File buffer or {success, localPath}</li>
|
|
168
|
+
<li><b>put:</b> {success, remotePath}</li>
|
|
169
|
+
<li><b>delete:</b> {success, deleted}</li>
|
|
170
|
+
<li><b>mkdir:</b> {success, created}</li>
|
|
171
|
+
<li><b>rmdir:</b> {success, removed}</li>
|
|
172
|
+
<li><b>rename:</b> {success, from, to}</li>
|
|
173
|
+
<li><b>exists:</b> 'd', '-', 'l', or false</li>
|
|
174
|
+
<li><b>stat:</b> File statistics object</li>
|
|
175
|
+
</ul>
|
|
176
|
+
</dd>
|
|
177
|
+
|
|
178
|
+
<dt>sftp-best <span class="property-type">object</span></dt>
|
|
179
|
+
<dd>Metadata: operation, remotePath, host, port</dd>
|
|
180
|
+
|
|
181
|
+
<dt>exists <span class="property-type">boolean</span></dt>
|
|
182
|
+
<dd>For exists operation: true if path exists.</dd>
|
|
183
|
+
</dl>
|
|
184
|
+
|
|
185
|
+
<h3>Status</h3>
|
|
186
|
+
<p>The node shows connection and operation status:</p>
|
|
187
|
+
<ul>
|
|
188
|
+
<li><b>Yellow dot:</b> Connecting</li>
|
|
189
|
+
<li><b>Green dot:</b> Connected / Done</li>
|
|
190
|
+
<li><b>Blue dot:</b> Operation in progress</li>
|
|
191
|
+
<li><b>Red ring:</b> Error occurred</li>
|
|
192
|
+
</ul>
|
|
193
|
+
|
|
194
|
+
<h3>Example: List Directory</h3>
|
|
195
|
+
<pre>msg.payload = '/home/user/files';
|
|
196
|
+
// or
|
|
197
|
+
msg.remotePath = '/home/user/files';
|
|
198
|
+
msg.operation = 'list';</pre>
|
|
199
|
+
|
|
200
|
+
<h3>Example: Download File</h3>
|
|
201
|
+
<pre>msg.remotePath = '/home/user/file.csv';
|
|
202
|
+
msg.operation = 'get';
|
|
203
|
+
// Returns file buffer in msg.payload</pre>
|
|
204
|
+
|
|
205
|
+
<h3>Example: Upload Buffer</h3>
|
|
206
|
+
<pre>msg.payload = Buffer.from('file content');
|
|
207
|
+
msg.remotePath = '/home/user/newfile.txt';
|
|
208
|
+
msg.operation = 'put';</pre>
|
|
209
|
+
</script>
|
package/nodes/sftp.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const Client = require('ssh2-sftp-client');
|
|
5
|
+
|
|
6
|
+
function SFTPNode(config) {
|
|
7
|
+
RED.nodes.createNode(this, config);
|
|
8
|
+
const node = this;
|
|
9
|
+
|
|
10
|
+
// Get config node reference
|
|
11
|
+
this.server = RED.nodes.getNode(config.server);
|
|
12
|
+
this.operation = config.operation || 'list';
|
|
13
|
+
this.remotePath = config.remotePath || '/';
|
|
14
|
+
this.localPath = config.localPath || '';
|
|
15
|
+
this.recursive = config.recursive || false;
|
|
16
|
+
|
|
17
|
+
if (!this.server) {
|
|
18
|
+
node.error('No SFTP server configured');
|
|
19
|
+
node.status({ fill: 'red', shape: 'ring', text: 'no server' });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
node.on('input', async function(msg, send, done) {
|
|
24
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
25
|
+
|
|
26
|
+
const sftp = new Client();
|
|
27
|
+
const password = node.server.password;
|
|
28
|
+
|
|
29
|
+
// Build connection config
|
|
30
|
+
const tryKeyboard = node.server.tryKeyboard !== false;
|
|
31
|
+
const connectionConfig = {
|
|
32
|
+
host: msg.host || node.server.host,
|
|
33
|
+
port: msg.port || node.server.port || 22,
|
|
34
|
+
username: msg.username || node.server.username,
|
|
35
|
+
tryKeyboard: tryKeyboard,
|
|
36
|
+
readyTimeout: 30000
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Add password if provided
|
|
40
|
+
if (password) {
|
|
41
|
+
connectionConfig.password = password;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add private key if provided
|
|
45
|
+
if (node.server.privateKey) {
|
|
46
|
+
connectionConfig.privateKey = node.server.privateKey;
|
|
47
|
+
if (node.server.passphrase) {
|
|
48
|
+
connectionConfig.passphrase = node.server.passphrase;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle keyboard-interactive authentication
|
|
53
|
+
if (connectionConfig.tryKeyboard) {
|
|
54
|
+
sftp.client.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => {
|
|
55
|
+
finish(prompts.map(() => password));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get operation parameters from msg or config
|
|
60
|
+
const operation = msg.operation || node.operation;
|
|
61
|
+
// Only use msg.payload as path if it's a non-empty string
|
|
62
|
+
let remotePath = node.remotePath || '/';
|
|
63
|
+
if (msg.remotePath && typeof msg.remotePath === 'string') {
|
|
64
|
+
remotePath = msg.remotePath;
|
|
65
|
+
} else if (msg.payload && typeof msg.payload === 'string' && msg.payload.startsWith('/')) {
|
|
66
|
+
remotePath = msg.payload;
|
|
67
|
+
}
|
|
68
|
+
const localPath = msg.localPath || node.localPath;
|
|
69
|
+
const recursive = msg.recursive !== undefined ? msg.recursive : node.recursive;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
node.status({ fill: 'yellow', shape: 'dot', text: 'connecting...' });
|
|
73
|
+
await sftp.connect(connectionConfig);
|
|
74
|
+
node.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
75
|
+
|
|
76
|
+
let result;
|
|
77
|
+
|
|
78
|
+
switch (operation) {
|
|
79
|
+
case 'list':
|
|
80
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'listing...' });
|
|
81
|
+
result = await sftp.list(remotePath);
|
|
82
|
+
msg.payload = result;
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'get':
|
|
86
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'downloading...' });
|
|
87
|
+
if (localPath) {
|
|
88
|
+
await sftp.get(remotePath, localPath);
|
|
89
|
+
msg.payload = { success: true, localPath: localPath };
|
|
90
|
+
} else {
|
|
91
|
+
// Return as buffer
|
|
92
|
+
result = await sftp.get(remotePath);
|
|
93
|
+
msg.payload = result;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
|
|
97
|
+
case 'put':
|
|
98
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'uploading...' });
|
|
99
|
+
if (Buffer.isBuffer(msg.payload)) {
|
|
100
|
+
await sftp.put(msg.payload, remotePath);
|
|
101
|
+
} else if (localPath) {
|
|
102
|
+
await sftp.put(localPath, remotePath);
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error('put operation requires msg.payload as Buffer or localPath');
|
|
105
|
+
}
|
|
106
|
+
msg.payload = { success: true, remotePath: remotePath };
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'delete':
|
|
110
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'deleting...' });
|
|
111
|
+
await sftp.delete(remotePath);
|
|
112
|
+
msg.payload = { success: true, deleted: remotePath };
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'mkdir':
|
|
116
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'creating dir...' });
|
|
117
|
+
await sftp.mkdir(remotePath, recursive);
|
|
118
|
+
msg.payload = { success: true, created: remotePath };
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'rmdir':
|
|
122
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'removing dir...' });
|
|
123
|
+
await sftp.rmdir(remotePath, recursive);
|
|
124
|
+
msg.payload = { success: true, removed: remotePath };
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'rename':
|
|
128
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'renaming...' });
|
|
129
|
+
const newPath = msg.newPath || localPath;
|
|
130
|
+
if (!newPath) {
|
|
131
|
+
throw new Error('rename operation requires msg.newPath or localPath');
|
|
132
|
+
}
|
|
133
|
+
await sftp.rename(remotePath, newPath);
|
|
134
|
+
msg.payload = { success: true, from: remotePath, to: newPath };
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'exists':
|
|
138
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'checking...' });
|
|
139
|
+
result = await sftp.exists(remotePath);
|
|
140
|
+
msg.payload = result; // false, 'd', '-', or 'l'
|
|
141
|
+
msg.exists = result !== false;
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'stat':
|
|
145
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'getting stats...' });
|
|
146
|
+
result = await sftp.stat(remotePath);
|
|
147
|
+
msg.payload = result;
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
throw new Error('Unknown operation: ' + operation);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add metadata
|
|
155
|
+
msg.sftp = {
|
|
156
|
+
operation: operation,
|
|
157
|
+
remotePath: remotePath,
|
|
158
|
+
host: connectionConfig.host,
|
|
159
|
+
port: connectionConfig.port
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
node.status({ fill: 'green', shape: 'dot', text: 'done' });
|
|
163
|
+
send(msg);
|
|
164
|
+
|
|
165
|
+
// Clear status after 2 seconds
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
node.status({});
|
|
168
|
+
}, 2000);
|
|
169
|
+
|
|
170
|
+
if (done) {
|
|
171
|
+
done();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
} catch (err) {
|
|
175
|
+
node.status({ fill: 'red', shape: 'ring', text: err.message.substring(0, 20) });
|
|
176
|
+
if (done) {
|
|
177
|
+
done(err);
|
|
178
|
+
} else {
|
|
179
|
+
node.error(err.message, msg);
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
try {
|
|
183
|
+
await sftp.end();
|
|
184
|
+
} catch (e) {
|
|
185
|
+
// Ignore close errors
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
node.on('close', function() {
|
|
191
|
+
node.status({});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
RED.nodes.registerType('sftp-best', SFTPNode);
|
|
196
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-best-sftp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reliable SFTP client node for Node-RED with keyboard-interactive auth support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node-red",
|
|
7
|
+
"sftp",
|
|
8
|
+
"ssh",
|
|
9
|
+
"file-transfer",
|
|
10
|
+
"ftp"
|
|
11
|
+
],
|
|
12
|
+
"author": "Trajche <trajche@kralev.eu>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/digitalnodecom/node-red-contrib-best-sftp.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/digitalnodecom/node-red-contrib-best-sftp/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/digitalnodecom/node-red-contrib-best-sftp#readme",
|
|
22
|
+
"node-red": {
|
|
23
|
+
"version": ">=2.0.0",
|
|
24
|
+
"nodes": {
|
|
25
|
+
"sftp-config": "nodes/sftp-config.js",
|
|
26
|
+
"sftp-best": "nodes/sftp.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ssh2-sftp-client": "^12.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"jest": "^30.2.0",
|
|
34
|
+
"node-red": "^4.1.3",
|
|
35
|
+
"node-red-node-test-helper": "^0.3.6"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "jest"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"jest": {
|
|
44
|
+
"testEnvironment": "node",
|
|
45
|
+
"testMatch": [
|
|
46
|
+
"**/test/**/*_spec.js"
|
|
47
|
+
],
|
|
48
|
+
"testTimeout": 10000
|
|
49
|
+
}
|
|
50
|
+
}
|