@processlink/node-red-contrib-processlink 1.1.1 → 1.2.1
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/CLAUDE.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CLAUDE.md - AI Assistant Guidelines
|
|
2
|
+
|
|
3
|
+
## Critical: This is a Published NPM Package
|
|
4
|
+
|
|
5
|
+
This package (`@processlink/node-red-contrib-processlink`) is **published to NPM** and used by real customers in production Node-RED environments. Changes here can break many people's workflows.
|
|
6
|
+
|
|
7
|
+
### Before Making Any Changes
|
|
8
|
+
|
|
9
|
+
1. **Understand the impact** - Changes affect all users who update the package
|
|
10
|
+
2. **Backwards compatibility** - Existing node configurations must continue to work
|
|
11
|
+
3. **Test thoroughly** - Test in a real Node-RED instance before publishing
|
|
12
|
+
4. **Version carefully** - Follow semver (patch for fixes, minor for features, major for breaking changes)
|
|
13
|
+
|
|
14
|
+
### Publishing Checklist
|
|
15
|
+
|
|
16
|
+
Before running `npm publish`:
|
|
17
|
+
- [ ] Test all nodes in Node-RED editor (deploy, configure, run)
|
|
18
|
+
- [ ] Verify existing flows still work after update
|
|
19
|
+
- [ ] Check browser console for JavaScript errors
|
|
20
|
+
- [ ] Test error handling paths
|
|
21
|
+
- [ ] Update version in package.json appropriately
|
|
22
|
+
- [ ] Document changes in commit message
|
|
23
|
+
|
|
24
|
+
## Architecture Notes
|
|
25
|
+
|
|
26
|
+
### Node-RED Security Model
|
|
27
|
+
|
|
28
|
+
**Important**: Credentials (API keys, passwords) are stored server-side only and are NEVER accessible from the browser/editor HTML code.
|
|
29
|
+
|
|
30
|
+
- `node.credentials.apiKey` - Only accessible in `.js` files (server-side)
|
|
31
|
+
- `configNode.credentials` - Always `undefined` in `.html` files (client-side)
|
|
32
|
+
|
|
33
|
+
To access credentials from the editor, create an admin HTTP endpoint:
|
|
34
|
+
```javascript
|
|
35
|
+
// In the .js file
|
|
36
|
+
RED.httpAdmin.get("/your-endpoint/:id", function(req, res) {
|
|
37
|
+
const node = RED.nodes.getNode(req.params.id);
|
|
38
|
+
const apiKey = node.credentials?.apiKey; // Access credentials here
|
|
39
|
+
// Make API calls server-side, return results to client
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### File Structure
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
nodes/
|
|
47
|
+
config/
|
|
48
|
+
processlink-config.js # Shared credentials config node
|
|
49
|
+
processlink-config.html
|
|
50
|
+
files/
|
|
51
|
+
processlink-files-upload.js # File upload node
|
|
52
|
+
processlink-files-upload.html
|
|
53
|
+
system/
|
|
54
|
+
processlink-system-info.js # System info node
|
|
55
|
+
processlink-system-info.html
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Node-RED Conventions
|
|
59
|
+
|
|
60
|
+
- Dual outputs: First output for success, second for errors
|
|
61
|
+
- Status indicators: green=success, yellow=processing, red=error
|
|
62
|
+
- Config nodes: Store shared credentials, referenced by other nodes
|
|
63
|
+
- Always use `send` and `done` callbacks for proper async handling
|
package/README.md
CHANGED
|
@@ -71,35 +71,44 @@ Then restart Node-RED.
|
|
|
71
71
|
### 3. Connect Your Flow
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
[File In] → [files upload] ─┬─ Output 1 (success) → [Debug]
|
|
75
|
-
|
|
74
|
+
[Inject] → [File In] → [files upload] ─┬─ Output 1 (success) → [Debug]
|
|
75
|
+
└─ Output 2 (error) → [Debug]
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
80
80
|
## Node Reference
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Files Upload
|
|
83
85
|
|
|
84
86
|
Uploads files to the Process Link Files API.
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
### Configuration
|
|
87
89
|
|
|
88
90
|
| Property | Description |
|
|
89
91
|
|----------|-------------|
|
|
90
92
|
| Config | Your Process Link credentials (Site ID + API Key) |
|
|
91
|
-
| Filename |
|
|
92
|
-
|
|
|
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) |
|
|
93
96
|
| Timeout | Request timeout in milliseconds (default: 30000) |
|
|
94
97
|
|
|
95
|
-
|
|
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
|
|
96
105
|
|
|
97
106
|
| Property | Type | Description |
|
|
98
107
|
|----------|------|-------------|
|
|
99
108
|
| `msg.payload` | Buffer \| string | The file content to upload |
|
|
100
|
-
| `msg.filename` | string | *(Optional)*
|
|
109
|
+
| `msg.filename` | string | *(Optional)* Fallback filename if not set in config |
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
### Outputs
|
|
103
112
|
|
|
104
113
|
This node has **two outputs**:
|
|
105
114
|
|
|
@@ -108,15 +117,15 @@ This node has **two outputs**:
|
|
|
108
117
|
| **1 - Success** | HTTP 201 | `msg.payload.ok`, `msg.payload.file_id`, `msg.file_id`, `msg.statusCode` |
|
|
109
118
|
| **2 - Error** | API error, network error, timeout | `msg.payload.error`, `msg.statusCode` |
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
### Status Indicators
|
|
112
121
|
|
|
113
122
|
| Color | Meaning |
|
|
114
123
|
|-------|---------|
|
|
124
|
+
| 🔴 Red | Error occurred |
|
|
115
125
|
| 🟡 Yellow | Uploading in progress |
|
|
116
126
|
| 🟢 Green | Upload successful |
|
|
117
|
-
| 🔴 Red | Error occurred |
|
|
118
127
|
|
|
119
|
-
|
|
128
|
+
### Status Codes
|
|
120
129
|
|
|
121
130
|
| Code | Meaning |
|
|
122
131
|
|------|---------|
|
|
@@ -130,22 +139,22 @@ This node has **two outputs**:
|
|
|
130
139
|
|
|
131
140
|
---
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
## System Info
|
|
134
143
|
|
|
135
144
|
Outputs system information for diagnostics and monitoring.
|
|
136
145
|
|
|
137
|
-
|
|
146
|
+
### Configuration
|
|
138
147
|
|
|
139
148
|
| Property | Description |
|
|
140
149
|
|----------|-------------|
|
|
141
150
|
| Send on deploy | When checked (default), outputs system info when the flow is deployed |
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
### Triggers
|
|
144
153
|
|
|
145
154
|
- **On deploy** (if enabled) - Automatically sends when flow starts
|
|
146
155
|
- **On input** - Any incoming message triggers a fresh reading
|
|
147
156
|
|
|
148
|
-
|
|
157
|
+
### Output
|
|
149
158
|
|
|
150
159
|
`msg.payload` contains:
|
|
151
160
|
|
|
@@ -169,7 +178,7 @@ Outputs system information for diagnostics and monitoring.
|
|
|
169
178
|
| `nodejs` | version |
|
|
170
179
|
| `processMemory` | rss, heapTotal, heapUsed |
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
### Uptime/Memory Structure
|
|
173
182
|
|
|
174
183
|
```json
|
|
175
184
|
{
|
|
@@ -293,7 +302,6 @@ Copy the JSON below and import into Node-RED: **Menu → Import → Clipboard**
|
|
|
293
302
|
|
|
294
303
|
## Support
|
|
295
304
|
|
|
296
|
-
- 📖 **Documentation**: [GitHub Wiki](https://github.com/process-link/node-red-contrib-processlink/wiki)
|
|
297
305
|
- 🐛 **Issues**: [GitHub Issues](https://github.com/process-link/node-red-contrib-processlink/issues)
|
|
298
306
|
- 📧 **Email**: support@processlink.com.au
|
|
299
307
|
- 🌐 **Website**: [processlink.com.au](https://processlink.com.au)
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
module.exports = function (RED) {
|
|
7
|
+
const https = require("https");
|
|
8
|
+
|
|
7
9
|
function ProcessLinkConfigNode(config) {
|
|
8
10
|
RED.nodes.createNode(this, config);
|
|
9
11
|
this.name = config.name;
|
|
@@ -16,4 +18,95 @@ module.exports = function (RED) {
|
|
|
16
18
|
apiKey: { type: "password" },
|
|
17
19
|
},
|
|
18
20
|
});
|
|
21
|
+
|
|
22
|
+
// Admin endpoint to fetch locations (areas and folders) for a config node
|
|
23
|
+
RED.httpAdmin.get("/processlink/locations/:id", function (req, res) {
|
|
24
|
+
const configNode = RED.nodes.getNode(req.params.id);
|
|
25
|
+
if (!configNode) {
|
|
26
|
+
return res.status(404).json({ error: "Config node not found" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const siteId = configNode.siteId;
|
|
30
|
+
const apiKey = configNode.credentials?.apiKey;
|
|
31
|
+
|
|
32
|
+
if (!siteId || !apiKey) {
|
|
33
|
+
return res.status(400).json({ error: "Config not ready" });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseUrl = `/api/sites/${siteId}`;
|
|
37
|
+
const headers = {
|
|
38
|
+
Authorization: `Bearer ${apiKey}`,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Fetch both areas and folders
|
|
42
|
+
let areasData = [];
|
|
43
|
+
let foldersData = [];
|
|
44
|
+
let completed = 0;
|
|
45
|
+
let hasError = false;
|
|
46
|
+
|
|
47
|
+
function checkComplete() {
|
|
48
|
+
completed++;
|
|
49
|
+
if (completed === 2 && !hasError) {
|
|
50
|
+
res.json({ areas: areasData, folders: foldersData });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fetch areas
|
|
55
|
+
const areasReq = https.request(
|
|
56
|
+
{
|
|
57
|
+
hostname: "files.processlink.com.au",
|
|
58
|
+
path: `${baseUrl}/areas`,
|
|
59
|
+
method: "GET",
|
|
60
|
+
headers: headers,
|
|
61
|
+
},
|
|
62
|
+
(areasRes) => {
|
|
63
|
+
let data = "";
|
|
64
|
+
areasRes.on("data", (chunk) => (data += chunk));
|
|
65
|
+
areasRes.on("end", () => {
|
|
66
|
+
try {
|
|
67
|
+
areasData = JSON.parse(data);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
areasData = [];
|
|
70
|
+
}
|
|
71
|
+
checkComplete();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
areasReq.on("error", () => {
|
|
76
|
+
if (!hasError) {
|
|
77
|
+
hasError = true;
|
|
78
|
+
res.status(500).json({ error: "Failed to fetch areas" });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
areasReq.end();
|
|
82
|
+
|
|
83
|
+
// Fetch folders
|
|
84
|
+
const foldersReq = https.request(
|
|
85
|
+
{
|
|
86
|
+
hostname: "files.processlink.com.au",
|
|
87
|
+
path: `${baseUrl}/folders`,
|
|
88
|
+
method: "GET",
|
|
89
|
+
headers: headers,
|
|
90
|
+
},
|
|
91
|
+
(foldersRes) => {
|
|
92
|
+
let data = "";
|
|
93
|
+
foldersRes.on("data", (chunk) => (data += chunk));
|
|
94
|
+
foldersRes.on("end", () => {
|
|
95
|
+
try {
|
|
96
|
+
foldersData = JSON.parse(data);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
foldersData = [];
|
|
99
|
+
}
|
|
100
|
+
checkComplete();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
foldersReq.on("error", () => {
|
|
105
|
+
if (!hasError) {
|
|
106
|
+
hasError = true;
|
|
107
|
+
res.status(500).json({ error: "Failed to fetch folders" });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
foldersReq.end();
|
|
111
|
+
});
|
|
19
112
|
};
|
|
@@ -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,129 @@
|
|
|
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
|
+
// Fetch locations via server-side endpoint (credentials are not accessible client-side)
|
|
50
|
+
$.ajax({
|
|
51
|
+
url: "processlink/locations/" + configId,
|
|
52
|
+
method: "GET",
|
|
53
|
+
}).done(function (result) {
|
|
54
|
+
var areas = result.areas || [];
|
|
55
|
+
var folders = result.folders || [];
|
|
56
|
+
|
|
57
|
+
// Build current selection key for matching
|
|
58
|
+
var currentAreaId = node.areaId || "";
|
|
59
|
+
var currentFolderId = node.folderId || "";
|
|
60
|
+
var currentKey = currentAreaId + "|" + currentFolderId;
|
|
61
|
+
|
|
62
|
+
var options = '<option value="|" data-area="" data-folder="">Site root (default)</option>';
|
|
63
|
+
|
|
64
|
+
// Group folders by area
|
|
65
|
+
var foldersByArea = {};
|
|
66
|
+
folders.forEach(function (f) {
|
|
67
|
+
var areaKey = f.area_id || "";
|
|
68
|
+
if (!foldersByArea[areaKey]) foldersByArea[areaKey] = [];
|
|
69
|
+
foldersByArea[areaKey].push(f);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Add areas with their folders
|
|
73
|
+
areas.forEach(function (area) {
|
|
74
|
+
var areaFolders = foldersByArea[area.id] || [];
|
|
75
|
+
options += '<optgroup label="' + area.area_name + '">';
|
|
76
|
+
|
|
77
|
+
// Area root option
|
|
78
|
+
var areaRootKey = area.id + "|";
|
|
79
|
+
var areaRootSelected = areaRootKey === currentKey ? ' selected' : '';
|
|
80
|
+
options += '<option value="' + areaRootKey + '" data-area="' + area.id + '" data-folder=""' + areaRootSelected + '>' + area.area_name + ' (root)</option>';
|
|
81
|
+
|
|
82
|
+
// Root folders in this area
|
|
83
|
+
var rootFolders = areaFolders.filter(function (f) { return !f.parent_id; });
|
|
84
|
+
rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
85
|
+
|
|
86
|
+
rootFolders.forEach(function (folder) {
|
|
87
|
+
var folderKey = area.id + "|" + folder.id;
|
|
88
|
+
var selected = folderKey === currentKey ? ' selected' : '';
|
|
89
|
+
options += '<option value="' + folderKey + '" data-area="' + area.id + '" data-folder="' + folder.id + '"' + selected + '> ' + folder.name + '</option>';
|
|
90
|
+
|
|
91
|
+
// Child folders
|
|
92
|
+
var children = areaFolders.filter(function (c) { return c.parent_id === folder.id; });
|
|
93
|
+
children.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
94
|
+
children.forEach(function (child) {
|
|
95
|
+
var childKey = area.id + "|" + child.id;
|
|
96
|
+
var childSelected = childKey === currentKey ? ' selected' : '';
|
|
97
|
+
options += '<option value="' + childKey + '" data-area="' + area.id + '" data-folder="' + child.id + '"' + childSelected + '> ' + child.name + '</option>';
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
options += '</optgroup>';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Add folders without an area
|
|
105
|
+
var noAreaFolders = foldersByArea[""] || [];
|
|
106
|
+
if (noAreaFolders.length > 0) {
|
|
107
|
+
options += '<optgroup label="No Area">';
|
|
108
|
+
var rootFolders = noAreaFolders.filter(function (f) { return !f.parent_id; });
|
|
109
|
+
rootFolders.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
110
|
+
|
|
111
|
+
rootFolders.forEach(function (folder) {
|
|
112
|
+
var folderKey = "|" + folder.id;
|
|
113
|
+
var selected = folderKey === currentKey ? ' selected' : '';
|
|
114
|
+
options += '<option value="' + folderKey + '" data-area="" data-folder="' + folder.id + '"' + selected + '>' + folder.name + '</option>';
|
|
115
|
+
|
|
116
|
+
var children = noAreaFolders.filter(function (c) { return c.parent_id === folder.id; });
|
|
117
|
+
children.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
118
|
+
children.forEach(function (child) {
|
|
119
|
+
var childKey = "|" + child.id;
|
|
120
|
+
var childSelected = childKey === currentKey ? ' selected' : '';
|
|
121
|
+
options += '<option value="' + childKey + '" data-area="" data-folder="' + child.id + '"' + childSelected + '> ' + child.name + '</option>';
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
options += '</optgroup>';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$location.html(options);
|
|
128
|
+
}).fail(function (xhr) {
|
|
129
|
+
console.error("Failed to load locations:", xhr.responseText);
|
|
130
|
+
// Still allow site root if API fails
|
|
131
|
+
$location.html('<option value="|">Site root (default)</option><option disabled>-- Failed to load locations --</option>');
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Update hidden inputs when selection changes
|
|
136
|
+
$location.on("change", function () {
|
|
137
|
+
var $selected = $location.find(":selected");
|
|
138
|
+
$areaId.val($selected.data("area") || "");
|
|
139
|
+
$folderId.val($selected.data("folder") || "");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Load locations when config changes
|
|
143
|
+
$server.on("change", function () {
|
|
144
|
+
loadLocations();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Initial load
|
|
148
|
+
loadLocations();
|
|
149
|
+
},
|
|
25
150
|
});
|
|
26
151
|
</script>
|
|
27
152
|
|
|
@@ -38,10 +163,18 @@
|
|
|
38
163
|
<label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
|
|
39
164
|
<input type="text" id="node-input-filename" placeholder="msg.filename (or specify default)" />
|
|
40
165
|
</div>
|
|
166
|
+
<div class="form-row">
|
|
167
|
+
<label for="node-input-location"><i class="fa fa-folder"></i> Location</label>
|
|
168
|
+
<select id="node-input-location" style="width: 70%">
|
|
169
|
+
<option value="|">Site root (default)</option>
|
|
170
|
+
</select>
|
|
171
|
+
<input type="hidden" id="node-input-areaId" />
|
|
172
|
+
<input type="hidden" id="node-input-folderId" />
|
|
173
|
+
</div>
|
|
41
174
|
<div class="form-row">
|
|
42
175
|
<label for="node-input-timestampPrefix"> </label>
|
|
43
176
|
<input type="checkbox" id="node-input-timestampPrefix" style="width: auto; margin-right: 10px;">
|
|
44
|
-
<span>Prefix filename with timestamp (YYYY-MM-DD_HH-
|
|
177
|
+
<span>Prefix filename with timestamp (YYYY-MM-DD_HH-mm-ss_)</span>
|
|
45
178
|
</div>
|
|
46
179
|
<div class="form-row">
|
|
47
180
|
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
@@ -62,7 +195,7 @@
|
|
|
62
195
|
<dt>payload <span class="property-type">buffer | string</span></dt>
|
|
63
196
|
<dd>The file content to upload.</dd>
|
|
64
197
|
<dt class="optional">filename <span class="property-type">string</span></dt>
|
|
65
|
-
<dd>
|
|
198
|
+
<dd>Fallback filename if not set in config. Priority: config → <code>msg.filename</code> → "file.bin".</dd>
|
|
66
199
|
</dl>
|
|
67
200
|
|
|
68
201
|
<h3>Outputs</h3>
|
|
@@ -95,9 +228,11 @@
|
|
|
95
228
|
<h3>Configuration</h3>
|
|
96
229
|
<dl class="message-properties">
|
|
97
230
|
<dt>Filename <span class="property-type">string</span></dt>
|
|
98
|
-
<dd>
|
|
231
|
+
<dd>Filename for uploaded file. Takes priority over <code>msg.filename</code>.</dd>
|
|
232
|
+
<dt>Location <span class="property-type">select</span></dt>
|
|
233
|
+
<dd>Destination area/folder in the Files app. Default: site root.</dd>
|
|
99
234
|
<dt>Prefix with timestamp <span class="property-type">boolean</span></dt>
|
|
100
|
-
<dd>Adds <code>YYYY-MM-DD_HH-
|
|
235
|
+
<dd>Adds <code>YYYY-MM-DD_HH-mm-ss_</code> prefix (ISO 8601). Applied regardless of filename source.</dd>
|
|
101
236
|
<dt>Timeout <span class="property-type">number</span></dt>
|
|
102
237
|
<dd>Request timeout in ms. Default: 30000.</dd>
|
|
103
238
|
</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.1",
|
|
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",
|