@processlink/node-red-contrib-processlink 1.1.0 → 1.2.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.
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(git add:*)",
|
|
5
|
+
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd folder/area support to upload API\n\n- Add areaId and folderId params to /api/upload endpoint\n- Add API key auth to /api/sites/[siteId]/folders endpoint\n- Add new /api/sites/[siteId]/areas endpoint with API key auth\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
6
|
+
"Bash(git commit -m \"$\\(cat <<''EOF''\nAdd location selector for file uploads \\(v1.2.0\\)\n\n- Add Location dropdown showing Area > Folder hierarchy\n- Fetch areas and folders from Files API with API key auth\n- Send areaId and folderId in upload requests\n- Update README documentation\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
7
|
+
"Bash(npm publish:*)"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
}
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Connect your Node-RED flows to the [Process Link](https://processlink.com.au) pl
|
|
|
28
28
|
| Node | Description |
|
|
29
29
|
|------|-------------|
|
|
30
30
|
| **files upload** | Upload files to Process Link Files API |
|
|
31
|
+
| **system info** | Output system diagnostics (hostname, memory, disk, uptime, etc.) |
|
|
31
32
|
|
|
32
33
|
*More nodes coming soon: mail, downtime logging, notes*
|
|
33
34
|
|
|
@@ -70,49 +71,136 @@ Then restart Node-RED.
|
|
|
70
71
|
### 3. Connect Your Flow
|
|
71
72
|
|
|
72
73
|
```
|
|
73
|
-
[File In] → [files upload] → [Debug]
|
|
74
|
+
[Inject] → [File In] → [files upload] ─┬─ Output 1 (success) → [Debug]
|
|
75
|
+
└─ Output 2 (error) → [Debug]
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
---
|
|
79
|
+
|
|
76
80
|
## Node Reference
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Files Upload
|
|
79
85
|
|
|
80
86
|
Uploads files to the Process Link Files API.
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
### Configuration
|
|
83
89
|
|
|
84
90
|
| Property | Description |
|
|
85
91
|
|----------|-------------|
|
|
86
92
|
| Config | Your Process Link credentials (Site ID + API Key) |
|
|
87
|
-
| Filename |
|
|
93
|
+
| Filename | Filename for uploaded file (takes priority over `msg.filename`) |
|
|
94
|
+
| Location | Destination area/folder in the Files app (default: site root) |
|
|
95
|
+
| Prefix with timestamp | Adds `YYYY-MM-DD_HH-mm-ss_` prefix to filename (ISO 8601 format) |
|
|
88
96
|
| Timeout | Request timeout in milliseconds (default: 30000) |
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
**Filename priority:** Config filename → `msg.filename` → `file.bin`
|
|
99
|
+
|
|
100
|
+
**Location:** The dropdown shows your site's folder structure organized by area. Select where uploaded files should be stored. Areas and folders are fetched from the Files API when you open the node configuration.
|
|
101
|
+
|
|
102
|
+
**Timestamp prefix:** When enabled, always prepends the current date/time to the filename, regardless of whether it came from config or `msg.filename`.
|
|
103
|
+
|
|
104
|
+
### Inputs
|
|
91
105
|
|
|
92
106
|
| Property | Type | Description |
|
|
93
107
|
|----------|------|-------------|
|
|
94
108
|
| `msg.payload` | Buffer \| string | The file content to upload |
|
|
95
|
-
| `msg.filename` | string | *(Optional)*
|
|
109
|
+
| `msg.filename` | string | *(Optional)* Fallback filename if not set in config |
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
### Outputs
|
|
98
112
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
|
102
|
-
|
|
103
|
-
|
|
|
113
|
+
This node has **two outputs**:
|
|
114
|
+
|
|
115
|
+
| Output | When | Properties |
|
|
116
|
+
|--------|------|------------|
|
|
117
|
+
| **1 - Success** | HTTP 201 | `msg.payload.ok`, `msg.payload.file_id`, `msg.file_id`, `msg.statusCode` |
|
|
118
|
+
| **2 - Error** | API error, network error, timeout | `msg.payload.error`, `msg.statusCode` |
|
|
104
119
|
|
|
105
|
-
|
|
120
|
+
### Status Indicators
|
|
106
121
|
|
|
107
122
|
| Color | Meaning |
|
|
108
123
|
|-------|---------|
|
|
124
|
+
| 🔴 Red | Error occurred |
|
|
109
125
|
| 🟡 Yellow | Uploading in progress |
|
|
110
126
|
| 🟢 Green | Upload successful |
|
|
111
|
-
| 🔴 Red | Error occurred |
|
|
112
127
|
|
|
113
|
-
|
|
128
|
+
### Status Codes
|
|
129
|
+
|
|
130
|
+
| Code | Meaning |
|
|
131
|
+
|------|---------|
|
|
132
|
+
| 201 | Success |
|
|
133
|
+
| 400 | Bad request |
|
|
134
|
+
| 401 | Invalid API key |
|
|
135
|
+
| 403 | API access not enabled |
|
|
136
|
+
| 404 | Site not found |
|
|
137
|
+
| 429 | Rate limit exceeded (max 30/min) |
|
|
138
|
+
| 507 | Storage limit exceeded |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## System Info
|
|
114
143
|
|
|
115
|
-
|
|
144
|
+
Outputs system information for diagnostics and monitoring.
|
|
145
|
+
|
|
146
|
+
### Configuration
|
|
147
|
+
|
|
148
|
+
| Property | Description |
|
|
149
|
+
|----------|-------------|
|
|
150
|
+
| Send on deploy | When checked (default), outputs system info when the flow is deployed |
|
|
151
|
+
|
|
152
|
+
### Triggers
|
|
153
|
+
|
|
154
|
+
- **On deploy** (if enabled) - Automatically sends when flow starts
|
|
155
|
+
- **On input** - Any incoming message triggers a fresh reading
|
|
156
|
+
|
|
157
|
+
### Output
|
|
158
|
+
|
|
159
|
+
`msg.payload` contains:
|
|
160
|
+
|
|
161
|
+
| Property | Description |
|
|
162
|
+
|----------|-------------|
|
|
163
|
+
| `timestamp` | ISO 8601 UTC timestamp |
|
|
164
|
+
| `localTime` | Device local time string |
|
|
165
|
+
| `timezone` | Timezone name (e.g., "Australia/Sydney") |
|
|
166
|
+
| `hostname` | Device hostname |
|
|
167
|
+
| `platform` | "win32", "linux", or "darwin" |
|
|
168
|
+
| `os` | OS name and version |
|
|
169
|
+
| `arch` | CPU architecture |
|
|
170
|
+
| `user` | User running Node-RED |
|
|
171
|
+
| `workingDirectory` | Node-RED working directory |
|
|
172
|
+
| `uptime` | System uptime (`raw`, `breakdown`, `formatted`) |
|
|
173
|
+
| `cpu` | Model, cores, architecture |
|
|
174
|
+
| `memory` | Total, free, used (bytes + formatted), usedPercent |
|
|
175
|
+
| `disk` | Total, free, used (bytes + formatted), usedPercent |
|
|
176
|
+
| `network` | primaryIP, mac, interfaces |
|
|
177
|
+
| `nodeRed` | version, uptime |
|
|
178
|
+
| `nodejs` | version |
|
|
179
|
+
| `processMemory` | rss, heapTotal, heapUsed |
|
|
180
|
+
|
|
181
|
+
### Uptime/Memory Structure
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"uptime": {
|
|
186
|
+
"raw": 432000,
|
|
187
|
+
"breakdown": { "days": 5, "hours": 0, "minutes": 0, "seconds": 0 },
|
|
188
|
+
"formatted": "5d 0h 0m 0s"
|
|
189
|
+
},
|
|
190
|
+
"memory": {
|
|
191
|
+
"total": { "bytes": 17179869184, "formatted": "16.00 GB" },
|
|
192
|
+
"free": { "bytes": 8589934592, "formatted": "8.00 GB" },
|
|
193
|
+
"used": { "bytes": 8589934592, "formatted": "8.00 GB" },
|
|
194
|
+
"usedPercent": 50
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Example Flow
|
|
202
|
+
|
|
203
|
+
Copy the JSON below and import into Node-RED: **Menu → Import → Clipboard**
|
|
116
204
|
|
|
117
205
|
```json
|
|
118
206
|
[
|
|
@@ -158,21 +246,36 @@ Copy the JSON below and import it into Node-RED: **Menu → Import → Clipboard
|
|
|
158
246
|
"apiUrl": "https://files.processlink.com.au/api/upload",
|
|
159
247
|
"x": 470,
|
|
160
248
|
"y": 100,
|
|
161
|
-
"wires": [["pl-debug"]]
|
|
249
|
+
"wires": [["pl-debug-success"], ["pl-debug-error"]]
|
|
162
250
|
},
|
|
163
251
|
{
|
|
164
|
-
"id": "pl-debug",
|
|
252
|
+
"id": "pl-debug-success",
|
|
165
253
|
"type": "debug",
|
|
166
254
|
"z": "",
|
|
167
|
-
"name": "
|
|
255
|
+
"name": "Success",
|
|
168
256
|
"active": true,
|
|
169
257
|
"tosidebar": true,
|
|
170
258
|
"console": false,
|
|
171
259
|
"tostatus": false,
|
|
172
260
|
"complete": "true",
|
|
173
261
|
"targetType": "full",
|
|
174
|
-
"x":
|
|
175
|
-
"y":
|
|
262
|
+
"x": 680,
|
|
263
|
+
"y": 80,
|
|
264
|
+
"wires": []
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"id": "pl-debug-error",
|
|
268
|
+
"type": "debug",
|
|
269
|
+
"z": "",
|
|
270
|
+
"name": "Error",
|
|
271
|
+
"active": true,
|
|
272
|
+
"tosidebar": true,
|
|
273
|
+
"console": false,
|
|
274
|
+
"tostatus": false,
|
|
275
|
+
"complete": "true",
|
|
276
|
+
"targetType": "full",
|
|
277
|
+
"x": 670,
|
|
278
|
+
"y": 120,
|
|
176
279
|
"wires": []
|
|
177
280
|
}
|
|
178
281
|
]
|
|
@@ -184,43 +287,7 @@ Copy the JSON below and import it into Node-RED: **Menu → Import → Clipboard
|
|
|
184
287
|
3. Click **Deploy**
|
|
185
288
|
4. Click the inject button to upload
|
|
186
289
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
Use a Function node to set the filename dynamically:
|
|
190
|
-
|
|
191
|
-
```javascript
|
|
192
|
-
msg.filename = "report-" + new Date().toISOString().split('T')[0] + ".csv";
|
|
193
|
-
return msg;
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Upload with Error Handling
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
[File In] → [files upload] → [Switch] → [Debug (success)]
|
|
200
|
-
↘ [Debug (error)]
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Use a Switch node to route based on `msg.statusCode`:
|
|
204
|
-
- Route 1: `msg.statusCode == 201` (success)
|
|
205
|
-
- Route 2: Otherwise (error)
|
|
206
|
-
|
|
207
|
-
## Error Handling
|
|
208
|
-
|
|
209
|
-
| Status Code | Meaning | Solution |
|
|
210
|
-
|-------------|---------|----------|
|
|
211
|
-
| 201 | Success | File uploaded successfully |
|
|
212
|
-
| 400 | Bad Request | Check that payload is a valid file buffer |
|
|
213
|
-
| 401 | Unauthorized | Verify your API key is correct |
|
|
214
|
-
| 403 | Forbidden | Enable API access in site settings |
|
|
215
|
-
| 404 | Not Found | Verify your Site ID is correct |
|
|
216
|
-
| 429 | Rate Limited | Max 30 uploads/minute per site |
|
|
217
|
-
| 507 | Storage Full | Contact administrator to increase storage |
|
|
218
|
-
|
|
219
|
-
## Rate Limits
|
|
220
|
-
|
|
221
|
-
- **30 uploads per minute** per site
|
|
222
|
-
- Exceeding the limit returns a 429 status code
|
|
223
|
-
- Implement retry logic in your flow for high-volume uploads
|
|
290
|
+
---
|
|
224
291
|
|
|
225
292
|
## Security
|
|
226
293
|
|
|
@@ -235,15 +302,10 @@ Use a Switch node to route based on `msg.statusCode`:
|
|
|
235
302
|
|
|
236
303
|
## Support
|
|
237
304
|
|
|
238
|
-
- 📖 **Documentation**: [GitHub Wiki](https://github.com/process-link/node-red-contrib-processlink/wiki)
|
|
239
305
|
- 🐛 **Issues**: [GitHub Issues](https://github.com/process-link/node-red-contrib-processlink/issues)
|
|
240
306
|
- 📧 **Email**: support@processlink.com.au
|
|
241
307
|
- 🌐 **Website**: [processlink.com.au](https://processlink.com.au)
|
|
242
308
|
|
|
243
|
-
## Contributing
|
|
244
|
-
|
|
245
|
-
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) before submitting a pull request.
|
|
246
|
-
|
|
247
309
|
## License
|
|
248
310
|
|
|
249
311
|
[MIT](LICENSE)
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
server: { value: "", type: "processlink-config", required: true },
|
|
8
8
|
filename: { value: "" },
|
|
9
|
+
areaId: { value: "" },
|
|
10
|
+
folderId: { value: "" },
|
|
9
11
|
timestampPrefix: { value: false },
|
|
10
12
|
timeout: { value: "30000" },
|
|
11
13
|
apiUrl: { value: "https://files.processlink.com.au/api/upload" },
|
|
@@ -22,6 +24,131 @@
|
|
|
22
24
|
},
|
|
23
25
|
inputLabels: "file buffer",
|
|
24
26
|
outputLabels: ["success", "error"],
|
|
27
|
+
oneditprepare: function () {
|
|
28
|
+
var node = this;
|
|
29
|
+
var $location = $("#node-input-location");
|
|
30
|
+
var $areaId = $("#node-input-areaId");
|
|
31
|
+
var $folderId = $("#node-input-folderId");
|
|
32
|
+
var $server = $("#node-input-server");
|
|
33
|
+
|
|
34
|
+
function loadLocations() {
|
|
35
|
+
var configId = $server.val();
|
|
36
|
+
if (!configId) {
|
|
37
|
+
$location.html('<option value="">-- Select config first --</option>');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var configNode = RED.nodes.node(configId);
|
|
42
|
+
if (!configNode || !configNode.siteId) {
|
|
43
|
+
$location.html('<option value="">-- Config not ready --</option>');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$location.html('<option value="">Loading...</option>');
|
|
48
|
+
|
|
49
|
+
var baseUrl = "https://files.processlink.com.au/api/sites/" + configNode.siteId;
|
|
50
|
+
var headers = { "Authorization": "Bearer " + configNode.credentials.apiKey };
|
|
51
|
+
|
|
52
|
+
// Fetch both areas and folders in parallel
|
|
53
|
+
$.when(
|
|
54
|
+
$.ajax({ url: baseUrl + "/areas", method: "GET", headers: headers }),
|
|
55
|
+
$.ajax({ url: baseUrl + "/folders", method: "GET", headers: headers })
|
|
56
|
+
).done(function (areasResult, foldersResult) {
|
|
57
|
+
var areas = areasResult[0] || [];
|
|
58
|
+
var folders = foldersResult[0] || [];
|
|
59
|
+
|
|
60
|
+
// Build current selection key for matching
|
|
61
|
+
var currentAreaId = node.areaId || "";
|
|
62
|
+
var currentFolderId = node.folderId || "";
|
|
63
|
+
var currentKey = currentAreaId + "|" + currentFolderId;
|
|
64
|
+
|
|
65
|
+
var options = '<option value="|" data-area="" data-folder="">Site root (default)</option>';
|
|
66
|
+
|
|
67
|
+
// Group folders by area
|
|
68
|
+
var foldersByArea = {};
|
|
69
|
+
folders.forEach(function (f) {
|
|
70
|
+
var areaKey = f.area_id || "";
|
|
71
|
+
if (!foldersByArea[areaKey]) foldersByArea[areaKey] = [];
|
|
72
|
+
foldersByArea[areaKey].push(f);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Add areas with their folders
|
|
76
|
+
areas.forEach(function (area) {
|
|
77
|
+
var areaFolders = foldersByArea[area.id] || [];
|
|
78
|
+
options += '<optgroup label="' + area.area_name + '">';
|
|
79
|
+
|
|
80
|
+
// Area root option
|
|
81
|
+
var areaRootKey = area.id + "|";
|
|
82
|
+
var areaRootSelected = areaRootKey === currentKey ? ' selected' : '';
|
|
83
|
+
options += '<option value="' + areaRootKey + '" data-area="' + area.id + '" data-folder=""' + areaRootSelected + '>' + area.area_name + ' (root)</option>';
|
|
84
|
+
|
|
85
|
+
// Root folders in this area
|
|
86
|
+
var rootFolders = areaFolders.filter(function (f) { return !f.parent_id; });
|
|
87
|
+
rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
88
|
+
|
|
89
|
+
rootFolders.forEach(function (folder) {
|
|
90
|
+
var folderKey = area.id + "|" + folder.id;
|
|
91
|
+
var selected = folderKey === currentKey ? ' selected' : '';
|
|
92
|
+
options += '<option value="' + folderKey + '" data-area="' + area.id + '" data-folder="' + folder.id + '"' + selected + '> ' + folder.name + '</option>';
|
|
93
|
+
|
|
94
|
+
// Child folders
|
|
95
|
+
var children = areaFolders.filter(function (c) { return c.parent_id === folder.id; });
|
|
96
|
+
children.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
97
|
+
children.forEach(function (child) {
|
|
98
|
+
var childKey = area.id + "|" + child.id;
|
|
99
|
+
var childSelected = childKey === currentKey ? ' selected' : '';
|
|
100
|
+
options += '<option value="' + childKey + '" data-area="' + area.id + '" data-folder="' + child.id + '"' + childSelected + '> ' + child.name + '</option>';
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
options += '</optgroup>';
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Add folders without an area
|
|
108
|
+
var noAreaFolders = foldersByArea[""] || [];
|
|
109
|
+
if (noAreaFolders.length > 0) {
|
|
110
|
+
options += '<optgroup label="No Area">';
|
|
111
|
+
var rootFolders = noAreaFolders.filter(function (f) { return !f.parent_id; });
|
|
112
|
+
rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
113
|
+
|
|
114
|
+
rootFolders.forEach(function (folder) {
|
|
115
|
+
var folderKey = "|" + folder.id;
|
|
116
|
+
var selected = folderKey === currentKey ? ' selected' : '';
|
|
117
|
+
options += '<option value="' + folderKey + '" data-area="" data-folder="' + folder.id + '"' + selected + '>' + folder.name + '</option>';
|
|
118
|
+
|
|
119
|
+
var children = noAreaFolders.filter(function (c) { return c.parent_id === folder.id; });
|
|
120
|
+
children.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
121
|
+
children.forEach(function (child) {
|
|
122
|
+
var childKey = "|" + child.id;
|
|
123
|
+
var childSelected = childKey === currentKey ? ' selected' : '';
|
|
124
|
+
options += '<option value="' + childKey + '" data-area="" data-folder="' + child.id + '"' + childSelected + '> ' + child.name + '</option>';
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
options += '</optgroup>';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$location.html(options);
|
|
131
|
+
}).fail(function (xhr) {
|
|
132
|
+
console.error("Failed to load locations:", xhr.responseText);
|
|
133
|
+
$location.html('<option value="|">Site root (default)</option><option disabled>-- Failed to load --</option>');
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Update hidden inputs when selection changes
|
|
138
|
+
$location.on("change", function () {
|
|
139
|
+
var $selected = $location.find(":selected");
|
|
140
|
+
$areaId.val($selected.data("area") || "");
|
|
141
|
+
$folderId.val($selected.data("folder") || "");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Load locations when config changes
|
|
145
|
+
$server.on("change", function () {
|
|
146
|
+
loadLocations();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Initial load
|
|
150
|
+
loadLocations();
|
|
151
|
+
},
|
|
25
152
|
});
|
|
26
153
|
</script>
|
|
27
154
|
|
|
@@ -38,10 +165,18 @@
|
|
|
38
165
|
<label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
|
|
39
166
|
<input type="text" id="node-input-filename" placeholder="msg.filename (or specify default)" />
|
|
40
167
|
</div>
|
|
168
|
+
<div class="form-row">
|
|
169
|
+
<label for="node-input-location"><i class="fa fa-folder"></i> Location</label>
|
|
170
|
+
<select id="node-input-location" style="width: 70%">
|
|
171
|
+
<option value="|">Site root (default)</option>
|
|
172
|
+
</select>
|
|
173
|
+
<input type="hidden" id="node-input-areaId" />
|
|
174
|
+
<input type="hidden" id="node-input-folderId" />
|
|
175
|
+
</div>
|
|
41
176
|
<div class="form-row">
|
|
42
177
|
<label for="node-input-timestampPrefix"> </label>
|
|
43
178
|
<input type="checkbox" id="node-input-timestampPrefix" style="width: auto; margin-right: 10px;">
|
|
44
|
-
<span>Prefix filename with timestamp (YYYY-MM-DD_HH-
|
|
179
|
+
<span>Prefix filename with timestamp (YYYY-MM-DD_HH-mm-ss_)</span>
|
|
45
180
|
</div>
|
|
46
181
|
<div class="form-row">
|
|
47
182
|
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
@@ -62,7 +197,7 @@
|
|
|
62
197
|
<dt>payload <span class="property-type">buffer | string</span></dt>
|
|
63
198
|
<dd>The file content to upload.</dd>
|
|
64
199
|
<dt class="optional">filename <span class="property-type">string</span></dt>
|
|
65
|
-
<dd>
|
|
200
|
+
<dd>Fallback filename if not set in config. Priority: config → <code>msg.filename</code> → "file.bin".</dd>
|
|
66
201
|
</dl>
|
|
67
202
|
|
|
68
203
|
<h3>Outputs</h3>
|
|
@@ -95,9 +230,11 @@
|
|
|
95
230
|
<h3>Configuration</h3>
|
|
96
231
|
<dl class="message-properties">
|
|
97
232
|
<dt>Filename <span class="property-type">string</span></dt>
|
|
98
|
-
<dd>
|
|
233
|
+
<dd>Filename for uploaded file. Takes priority over <code>msg.filename</code>.</dd>
|
|
234
|
+
<dt>Location <span class="property-type">select</span></dt>
|
|
235
|
+
<dd>Destination area/folder in the Files app. Default: site root.</dd>
|
|
99
236
|
<dt>Prefix with timestamp <span class="property-type">boolean</span></dt>
|
|
100
|
-
<dd>Adds <code>YYYY-MM-DD_HH-
|
|
237
|
+
<dd>Adds <code>YYYY-MM-DD_HH-mm-ss_</code> prefix (ISO 8601). Applied regardless of filename source.</dd>
|
|
101
238
|
<dt>Timeout <span class="property-type">number</span></dt>
|
|
102
239
|
<dd>Request timeout in ms. Default: 30000.</dd>
|
|
103
240
|
</dl>
|
|
@@ -53,8 +53,8 @@ module.exports = function (RED) {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Get filename
|
|
57
|
-
const filename =
|
|
56
|
+
// Get filename (config takes priority over msg.filename)
|
|
57
|
+
const filename = config.filename || msg.filename || "file.bin";
|
|
58
58
|
let basename = filename.split(/[\\/]/).pop();
|
|
59
59
|
|
|
60
60
|
// Add timestamp prefix if enabled
|
|
@@ -71,14 +71,38 @@ module.exports = function (RED) {
|
|
|
71
71
|
|
|
72
72
|
// Build multipart form data
|
|
73
73
|
const boundary = "----NodeREDProcessLink" + Date.now() + Math.random().toString(36).substring(2);
|
|
74
|
+
const parts = [];
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
// Add file part
|
|
77
|
+
parts.push(Buffer.from(
|
|
76
78
|
`--${boundary}\r\n` +
|
|
77
79
|
`Content-Disposition: form-data; name="file"; filename="${basename}"\r\n` +
|
|
78
80
|
`Content-Type: application/octet-stream\r\n\r\n`
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
));
|
|
82
|
+
parts.push(fileBuffer);
|
|
83
|
+
|
|
84
|
+
// Add areaId if specified
|
|
85
|
+
if (config.areaId) {
|
|
86
|
+
parts.push(Buffer.from(
|
|
87
|
+
`\r\n--${boundary}\r\n` +
|
|
88
|
+
`Content-Disposition: form-data; name="areaId"\r\n\r\n` +
|
|
89
|
+
config.areaId
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add folderId if specified
|
|
94
|
+
if (config.folderId) {
|
|
95
|
+
parts.push(Buffer.from(
|
|
96
|
+
`\r\n--${boundary}\r\n` +
|
|
97
|
+
`Content-Disposition: form-data; name="folderId"\r\n\r\n` +
|
|
98
|
+
config.folderId
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add closing boundary
|
|
103
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
104
|
+
|
|
105
|
+
const body = Buffer.concat(parts);
|
|
82
106
|
|
|
83
107
|
// Parse URL
|
|
84
108
|
const apiUrl = config.apiUrl || "https://files.processlink.com.au/api/upload";
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.2.0",
|
|
7
7
|
"description": "Node-RED nodes for Process Link platform integration - upload files, send notifications, and connect to industrial automation systems",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"node-red",
|