@processlink/node-red-contrib-processlink 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/LICENSE +21 -0
- package/README.md +126 -0
- package/examples/files-upload.json +140 -0
- package/icons/processlink.svg +10 -0
- package/nodes/config/processlink-config.html +65 -0
- package/nodes/config/processlink-config.js +19 -0
- package/nodes/files/processlink-files-upload.html +113 -0
- package/nodes/files/processlink-files-upload.js +172 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Process Link
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @processlink/node-red-contrib-processlink
|
|
2
|
+
|
|
3
|
+
Node-RED nodes for Process Link platform integration.
|
|
4
|
+
|
|
5
|
+
## Available Nodes
|
|
6
|
+
|
|
7
|
+
| Node | Category | Description |
|
|
8
|
+
|------|----------|-------------|
|
|
9
|
+
| **files upload** | Process Link | Upload files to Process Link Files API |
|
|
10
|
+
|
|
11
|
+
*More nodes coming soon: mail, downtime, notes*
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Via Node-RED Palette Manager (Recommended)
|
|
16
|
+
|
|
17
|
+
1. Open Node-RED
|
|
18
|
+
2. Go to **Menu → Manage palette → Install**
|
|
19
|
+
3. Search for `@processlink/node-red-contrib-processlink`
|
|
20
|
+
4. Click **Install**
|
|
21
|
+
|
|
22
|
+
### Via npm
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd ~/.node-red
|
|
26
|
+
npm install @processlink/node-red-contrib-processlink
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then restart Node-RED.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
1. **Get your credentials** from your Process Link app:
|
|
34
|
+
- Go to **Settings → API Keys**
|
|
35
|
+
- Click **Generate API Key**
|
|
36
|
+
- Copy the **Site ID** and **API Key**
|
|
37
|
+
|
|
38
|
+
2. **Add a node** to your flow:
|
|
39
|
+
- Find nodes in the palette under "Process Link"
|
|
40
|
+
- Drag one into your flow
|
|
41
|
+
|
|
42
|
+
3. **Configure credentials**:
|
|
43
|
+
- Double-click the node
|
|
44
|
+
- Click the pencil icon next to "Config"
|
|
45
|
+
- Enter your **Site ID** and **API Key**
|
|
46
|
+
- Click **Add**, then **Done**
|
|
47
|
+
|
|
48
|
+
## Node Reference
|
|
49
|
+
|
|
50
|
+
### Files Upload
|
|
51
|
+
|
|
52
|
+
Uploads files to the Process Link Files API.
|
|
53
|
+
|
|
54
|
+
#### Inputs
|
|
55
|
+
|
|
56
|
+
| Property | Type | Description |
|
|
57
|
+
|----------|------|-------------|
|
|
58
|
+
| `msg.payload` | Buffer \| string | The file content to upload |
|
|
59
|
+
| `msg.filename` | string | (Optional) Filename to use |
|
|
60
|
+
|
|
61
|
+
#### Outputs
|
|
62
|
+
|
|
63
|
+
| Property | Type | Description |
|
|
64
|
+
|----------|------|-------------|
|
|
65
|
+
| `msg.payload` | object | API response with `ok`, `file_id`, `created_at` |
|
|
66
|
+
| `msg.file_id` | string | The UUID of the uploaded file |
|
|
67
|
+
| `msg.statusCode` | number | HTTP status code (201 on success) |
|
|
68
|
+
|
|
69
|
+
#### Status Indicators
|
|
70
|
+
|
|
71
|
+
- Yellow: Uploading in progress
|
|
72
|
+
- Green: Upload successful
|
|
73
|
+
- Red: Error occurred
|
|
74
|
+
|
|
75
|
+
## Example Flow
|
|
76
|
+
|
|
77
|
+
Upload a file from disk:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
[File In] → [files upload] → [Debug]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
1. Configure a **File In** node to read your file
|
|
84
|
+
2. Connect it to the **files upload** node
|
|
85
|
+
3. Add a **Debug** node to see the result
|
|
86
|
+
4. Configure the upload node with your Site ID and API Key
|
|
87
|
+
|
|
88
|
+
### Dynamic Filename
|
|
89
|
+
|
|
90
|
+
Set the filename dynamically in a Function node:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
msg.filename = "report-" + new Date().toISOString().split('T')[0] + ".csv";
|
|
94
|
+
return msg;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Error Handling
|
|
98
|
+
|
|
99
|
+
| Status Code | Meaning | Solution |
|
|
100
|
+
|-------------|---------|----------|
|
|
101
|
+
| 201 | Success | File uploaded successfully |
|
|
102
|
+
| 400 | Bad Request | Check that payload is a valid file buffer |
|
|
103
|
+
| 401 | Unauthorized | Check your API key is correct |
|
|
104
|
+
| 403 | Forbidden | Enable API access in site settings |
|
|
105
|
+
| 404 | Not Found | Check your Site ID is correct |
|
|
106
|
+
| 429 | Rate Limited | Slow down - max 30 uploads/minute per site |
|
|
107
|
+
| 507 | Storage Full | Contact your administrator to increase storage |
|
|
108
|
+
|
|
109
|
+
## Rate Limits
|
|
110
|
+
|
|
111
|
+
The API allows **30 uploads per minute** per site. If you exceed this limit, you'll receive a 429 status code. The node will still output the message so you can implement retry logic in your flow.
|
|
112
|
+
|
|
113
|
+
## Security
|
|
114
|
+
|
|
115
|
+
- API keys are stored encrypted by Node-RED
|
|
116
|
+
- All communication uses HTTPS
|
|
117
|
+
- Keys are never logged or exposed in flow exports
|
|
118
|
+
|
|
119
|
+
## Support
|
|
120
|
+
|
|
121
|
+
- **Issues**: [GitHub Issues](https://github.com/process-link/node-red-contrib-processlink/issues)
|
|
122
|
+
- **Email**: support@processlink.com.au
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "pl-example-flow",
|
|
4
|
+
"type": "tab",
|
|
5
|
+
"label": "Process Link File Upload",
|
|
6
|
+
"disabled": false,
|
|
7
|
+
"info": "Example flow demonstrating how to upload files to Process Link Files API"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "pl-inject",
|
|
11
|
+
"type": "inject",
|
|
12
|
+
"z": "pl-example-flow",
|
|
13
|
+
"name": "Trigger Upload",
|
|
14
|
+
"props": [],
|
|
15
|
+
"repeat": "",
|
|
16
|
+
"crontab": "",
|
|
17
|
+
"once": false,
|
|
18
|
+
"onceDelay": 0.1,
|
|
19
|
+
"topic": "",
|
|
20
|
+
"x": 130,
|
|
21
|
+
"y": 100,
|
|
22
|
+
"wires": [
|
|
23
|
+
[
|
|
24
|
+
"pl-file-in"
|
|
25
|
+
]
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "pl-file-in",
|
|
30
|
+
"type": "file in",
|
|
31
|
+
"z": "pl-example-flow",
|
|
32
|
+
"name": "Read File",
|
|
33
|
+
"filename": "/path/to/your/file.pdf",
|
|
34
|
+
"filenameType": "str",
|
|
35
|
+
"format": "",
|
|
36
|
+
"chunk": false,
|
|
37
|
+
"sendError": false,
|
|
38
|
+
"encoding": "none",
|
|
39
|
+
"allProps": true,
|
|
40
|
+
"x": 310,
|
|
41
|
+
"y": 100,
|
|
42
|
+
"wires": [
|
|
43
|
+
[
|
|
44
|
+
"pl-upload"
|
|
45
|
+
]
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "pl-upload",
|
|
50
|
+
"type": "processlink-files-upload",
|
|
51
|
+
"z": "pl-example-flow",
|
|
52
|
+
"name": "Upload to Process Link",
|
|
53
|
+
"server": "",
|
|
54
|
+
"filename": "",
|
|
55
|
+
"timeout": "30000",
|
|
56
|
+
"apiUrl": "https://files.processlink.com.au/api/upload",
|
|
57
|
+
"x": 530,
|
|
58
|
+
"y": 100,
|
|
59
|
+
"wires": [
|
|
60
|
+
[
|
|
61
|
+
"pl-switch"
|
|
62
|
+
]
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "pl-switch",
|
|
67
|
+
"type": "switch",
|
|
68
|
+
"z": "pl-example-flow",
|
|
69
|
+
"name": "Check Status",
|
|
70
|
+
"property": "statusCode",
|
|
71
|
+
"propertyType": "msg",
|
|
72
|
+
"rules": [
|
|
73
|
+
{
|
|
74
|
+
"t": "eq",
|
|
75
|
+
"v": "201",
|
|
76
|
+
"vt": "num"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"t": "else"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"checkall": "true",
|
|
83
|
+
"repair": false,
|
|
84
|
+
"outputs": 2,
|
|
85
|
+
"x": 730,
|
|
86
|
+
"y": 100,
|
|
87
|
+
"wires": [
|
|
88
|
+
[
|
|
89
|
+
"pl-debug-success"
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"pl-debug-error"
|
|
93
|
+
]
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "pl-debug-success",
|
|
98
|
+
"type": "debug",
|
|
99
|
+
"z": "pl-example-flow",
|
|
100
|
+
"name": "Upload Success",
|
|
101
|
+
"active": true,
|
|
102
|
+
"tosidebar": true,
|
|
103
|
+
"console": false,
|
|
104
|
+
"tostatus": true,
|
|
105
|
+
"complete": "file_id",
|
|
106
|
+
"targetType": "msg",
|
|
107
|
+
"statusVal": "file_id",
|
|
108
|
+
"statusType": "msg",
|
|
109
|
+
"x": 940,
|
|
110
|
+
"y": 80,
|
|
111
|
+
"wires": []
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "pl-debug-error",
|
|
115
|
+
"type": "debug",
|
|
116
|
+
"z": "pl-example-flow",
|
|
117
|
+
"name": "Upload Error",
|
|
118
|
+
"active": true,
|
|
119
|
+
"tosidebar": true,
|
|
120
|
+
"console": false,
|
|
121
|
+
"tostatus": true,
|
|
122
|
+
"complete": "payload",
|
|
123
|
+
"targetType": "msg",
|
|
124
|
+
"statusVal": "payload.error",
|
|
125
|
+
"statusType": "msg",
|
|
126
|
+
"x": 930,
|
|
127
|
+
"y": 120,
|
|
128
|
+
"wires": []
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "pl-comment",
|
|
132
|
+
"type": "comment",
|
|
133
|
+
"z": "pl-example-flow",
|
|
134
|
+
"name": "Instructions: Configure the 'Upload to Process Link' node with your Site ID and API Key",
|
|
135
|
+
"info": "1. Double-click the orange 'Upload to Process Link' node\n2. Click the pencil icon next to 'Config'\n3. Enter your Site ID and API Key from the Process Link Files app\n4. Update the 'Read File' node with the path to your file\n5. Click the inject button to upload",
|
|
136
|
+
"x": 330,
|
|
137
|
+
"y": 40,
|
|
138
|
+
"wires": []
|
|
139
|
+
}
|
|
140
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
2
|
+
<g fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<polyline points="8,14 14,6 20,14"/>
|
|
4
|
+
<polyline points="20,14 26,6 32,14"/>
|
|
5
|
+
<polyline points="32,17 38,23 32,29"/>
|
|
6
|
+
<polyline points="32,26 26,34 20,26"/>
|
|
7
|
+
<polyline points="20,26 14,34 8,26"/>
|
|
8
|
+
<polyline points="8,29 2,23 8,17"/>
|
|
9
|
+
</g>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("processlink-config", {
|
|
3
|
+
category: "config",
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: "" },
|
|
6
|
+
siteId: { value: "", required: true },
|
|
7
|
+
},
|
|
8
|
+
credentials: {
|
|
9
|
+
apiKey: { type: "password" },
|
|
10
|
+
},
|
|
11
|
+
label: function () {
|
|
12
|
+
return this.name || "Process Link Config";
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script type="text/html" data-template-name="processlink-config">
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
20
|
+
<input type="text" id="node-config-input-name" placeholder="My Site Config" />
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-config-input-siteId"><i class="fa fa-building"></i> Site ID</label>
|
|
24
|
+
<input
|
|
25
|
+
type="text"
|
|
26
|
+
id="node-config-input-siteId"
|
|
27
|
+
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label for="node-config-input-apiKey"><i class="fa fa-key"></i> API Key</label>
|
|
32
|
+
<input type="password" id="node-config-input-apiKey" placeholder="Your API key" />
|
|
33
|
+
</div>
|
|
34
|
+
<div class="form-tips">
|
|
35
|
+
<p>
|
|
36
|
+
Get your Site ID and API Key from
|
|
37
|
+
<a href="https://processlink.com.au" target="_blank">Process Link</a>.
|
|
38
|
+
</p>
|
|
39
|
+
<p>
|
|
40
|
+
Go to <strong>Settings → API Keys</strong> in your app to generate a new key.
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<script type="text/html" data-help-name="processlink-config">
|
|
46
|
+
<p>Configuration node for Process Link API authentication.</p>
|
|
47
|
+
|
|
48
|
+
<h3>Properties</h3>
|
|
49
|
+
<dl class="message-properties">
|
|
50
|
+
<dt>Name <span class="property-type">string</span></dt>
|
|
51
|
+
<dd>Optional friendly name for this configuration.</dd>
|
|
52
|
+
<dt>Site ID <span class="property-type">string</span></dt>
|
|
53
|
+
<dd>The UUID of your site from Process Link. Found in your site settings.</dd>
|
|
54
|
+
<dt>API Key <span class="property-type">string</span></dt>
|
|
55
|
+
<dd>
|
|
56
|
+
The API key for authentication. Generate one from Settings → API Keys in your Process Link app.
|
|
57
|
+
</dd>
|
|
58
|
+
</dl>
|
|
59
|
+
|
|
60
|
+
<h3>Details</h3>
|
|
61
|
+
<p>
|
|
62
|
+
This configuration is shared across all Process Link nodes in your flow. The API key is stored
|
|
63
|
+
encrypted by Node-RED.
|
|
64
|
+
</p>
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Link Configuration Node
|
|
3
|
+
* Shared credentials for all Process Link nodes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = function (RED) {
|
|
7
|
+
function ProcessLinkConfigNode(config) {
|
|
8
|
+
RED.nodes.createNode(this, config);
|
|
9
|
+
this.name = config.name;
|
|
10
|
+
this.siteId = config.siteId;
|
|
11
|
+
// API key is stored in this.credentials.apiKey (encrypted by Node-RED)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
RED.nodes.registerType("processlink-config", ProcessLinkConfigNode, {
|
|
15
|
+
credentials: {
|
|
16
|
+
apiKey: { type: "password" },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("processlink-files-upload", {
|
|
3
|
+
category: "Process Link",
|
|
4
|
+
color: "#f97316",
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
server: { value: "", type: "processlink-config", required: true },
|
|
8
|
+
filename: { value: "" },
|
|
9
|
+
timeout: { value: "30000" },
|
|
10
|
+
apiUrl: { value: "https://files.processlink.com.au/api/upload" },
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 1,
|
|
14
|
+
icon: "processlink.svg",
|
|
15
|
+
paletteLabel: "files upload",
|
|
16
|
+
label: function () {
|
|
17
|
+
return this.name || "files upload";
|
|
18
|
+
},
|
|
19
|
+
labelStyle: function () {
|
|
20
|
+
return this.name ? "node_label_italic" : "";
|
|
21
|
+
},
|
|
22
|
+
inputLabels: "file buffer",
|
|
23
|
+
outputLabels: "response",
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script type="text/html" data-template-name="processlink-files-upload">
|
|
28
|
+
<div class="form-row">
|
|
29
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
30
|
+
<input type="text" id="node-input-name" placeholder="Name" />
|
|
31
|
+
</div>
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-server"><i class="fa fa-server"></i> Config</label>
|
|
34
|
+
<input type="text" id="node-input-server" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
|
|
38
|
+
<input type="text" id="node-input-filename" placeholder="msg.filename (or specify default)" />
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
|
|
42
|
+
<input type="text" id="node-input-timeout" placeholder="30000" />
|
|
43
|
+
<span style="margin-left: 5px; color: #888">ms</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row" style="display: none">
|
|
46
|
+
<label for="node-input-apiUrl"><i class="fa fa-link"></i> API URL</label>
|
|
47
|
+
<input type="text" id="node-input-apiUrl" />
|
|
48
|
+
</div>
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<script type="text/html" data-help-name="processlink-files-upload">
|
|
52
|
+
<p>Uploads files to the Process Link Files API.</p>
|
|
53
|
+
|
|
54
|
+
<h3>Inputs</h3>
|
|
55
|
+
<dl class="message-properties">
|
|
56
|
+
<dt>payload <span class="property-type">buffer | string</span></dt>
|
|
57
|
+
<dd>The file content to upload.</dd>
|
|
58
|
+
<dt class="optional">filename <span class="property-type">string</span></dt>
|
|
59
|
+
<dd>The filename to use. Defaults to the configured filename or "file.bin".</dd>
|
|
60
|
+
</dl>
|
|
61
|
+
|
|
62
|
+
<h3>Outputs</h3>
|
|
63
|
+
<dl class="message-properties">
|
|
64
|
+
<dt>payload <span class="property-type">object</span></dt>
|
|
65
|
+
<dd>The API response containing <code>ok</code>, <code>file_id</code>, and <code>created_at</code>.</dd>
|
|
66
|
+
<dt>file_id <span class="property-type">string</span></dt>
|
|
67
|
+
<dd>The UUID of the uploaded file (convenience property).</dd>
|
|
68
|
+
<dt>statusCode <span class="property-type">number</span></dt>
|
|
69
|
+
<dd>The HTTP status code (201 on success).</dd>
|
|
70
|
+
</dl>
|
|
71
|
+
|
|
72
|
+
<h3>Details</h3>
|
|
73
|
+
<p>
|
|
74
|
+
This node uploads files to your Process Link site. The file content should be passed in
|
|
75
|
+
<code>msg.payload</code> as a Buffer (from a file-in node) or as a string.
|
|
76
|
+
</p>
|
|
77
|
+
<p>
|
|
78
|
+
The filename can be set in <code>msg.filename</code> or configured in the node properties. If
|
|
79
|
+
the filename contains a path, only the basename will be used.
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<h3>Status Codes</h3>
|
|
83
|
+
<ul>
|
|
84
|
+
<li><strong>201</strong> - Upload successful</li>
|
|
85
|
+
<li><strong>400</strong> - Bad request (missing file, invalid site ID)</li>
|
|
86
|
+
<li><strong>401</strong> - Invalid API key</li>
|
|
87
|
+
<li><strong>403</strong> - API access not enabled for this site</li>
|
|
88
|
+
<li><strong>404</strong> - Site not found</li>
|
|
89
|
+
<li><strong>429</strong> - Rate limit exceeded (max 30 uploads/minute)</li>
|
|
90
|
+
<li><strong>507</strong> - Storage limit exceeded</li>
|
|
91
|
+
</ul>
|
|
92
|
+
|
|
93
|
+
<h3>Example Flow</h3>
|
|
94
|
+
<pre>
|
|
95
|
+
[File In] → [Process Link Upload] → [Debug]
|
|
96
|
+
|
|
97
|
+
The File In node reads a file and outputs msg.payload (Buffer)
|
|
98
|
+
and msg.filename. Connect directly to this node to upload.</pre>
|
|
99
|
+
|
|
100
|
+
<h3>References</h3>
|
|
101
|
+
<ul>
|
|
102
|
+
<li>
|
|
103
|
+
<a href="https://files.processlink.com.au" target="_blank">Process Link Files</a> - Manage
|
|
104
|
+
your files and API keys
|
|
105
|
+
</li>
|
|
106
|
+
<li>
|
|
107
|
+
<a href="https://github.com/process-link/node-red-contrib-processlink" target="_blank"
|
|
108
|
+
>GitHub</a
|
|
109
|
+
>
|
|
110
|
+
- Source code and issues
|
|
111
|
+
</li>
|
|
112
|
+
</ul>
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Link Files Upload Node
|
|
3
|
+
* Uploads files to Process Link Files API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
module.exports = function (RED) {
|
|
7
|
+
const https = require("https");
|
|
8
|
+
const http = require("http");
|
|
9
|
+
|
|
10
|
+
function ProcessLinkFilesUploadNode(config) {
|
|
11
|
+
RED.nodes.createNode(this, config);
|
|
12
|
+
const node = this;
|
|
13
|
+
|
|
14
|
+
// Get the config node
|
|
15
|
+
this.server = RED.nodes.getNode(config.server);
|
|
16
|
+
|
|
17
|
+
node.on("input", function (msg, send, done) {
|
|
18
|
+
// For Node-RED 0.x compatibility
|
|
19
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
20
|
+
done = done || function (err) { if (err) node.error(err, msg); };
|
|
21
|
+
|
|
22
|
+
// Validate config
|
|
23
|
+
if (!node.server) {
|
|
24
|
+
node.status({ fill: "red", shape: "ring", text: "no config" });
|
|
25
|
+
done(new Error("No Process Link configuration selected"));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const siteId = node.server.siteId;
|
|
30
|
+
const apiKey = node.server.credentials?.apiKey;
|
|
31
|
+
|
|
32
|
+
if (!siteId) {
|
|
33
|
+
node.status({ fill: "red", shape: "ring", text: "no site ID" });
|
|
34
|
+
done(new Error("Site ID not configured"));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
node.status({ fill: "red", shape: "ring", text: "no API key" });
|
|
40
|
+
done(new Error("API Key not configured"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate payload
|
|
45
|
+
let fileBuffer;
|
|
46
|
+
if (Buffer.isBuffer(msg.payload)) {
|
|
47
|
+
fileBuffer = msg.payload;
|
|
48
|
+
} else if (typeof msg.payload === "string") {
|
|
49
|
+
fileBuffer = Buffer.from(msg.payload);
|
|
50
|
+
} else {
|
|
51
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
52
|
+
done(new Error("msg.payload must be a Buffer or string"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get filename
|
|
57
|
+
const filename = msg.filename || config.filename || "file.bin";
|
|
58
|
+
const basename = filename.split(/[\\/]/).pop();
|
|
59
|
+
|
|
60
|
+
// Build multipart form data
|
|
61
|
+
const boundary = "----NodeREDProcessLink" + Date.now() + Math.random().toString(36).substring(2);
|
|
62
|
+
|
|
63
|
+
const header = Buffer.from(
|
|
64
|
+
`--${boundary}\r\n` +
|
|
65
|
+
`Content-Disposition: form-data; name="file"; filename="${basename}"\r\n` +
|
|
66
|
+
`Content-Type: application/octet-stream\r\n\r\n`
|
|
67
|
+
);
|
|
68
|
+
const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
69
|
+
const body = Buffer.concat([header, fileBuffer, footer]);
|
|
70
|
+
|
|
71
|
+
// Parse URL
|
|
72
|
+
const apiUrl = config.apiUrl || "https://files.processlink.com.au/api/upload";
|
|
73
|
+
const url = new URL(apiUrl);
|
|
74
|
+
const isHttps = url.protocol === "https:";
|
|
75
|
+
|
|
76
|
+
const options = {
|
|
77
|
+
hostname: url.hostname,
|
|
78
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
79
|
+
path: url.pathname,
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
83
|
+
"Content-Length": body.length,
|
|
84
|
+
"x-site-id": siteId,
|
|
85
|
+
"x-api-key": apiKey,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
node.status({ fill: "yellow", shape: "dot", text: "uploading..." });
|
|
90
|
+
|
|
91
|
+
const transport = isHttps ? https : http;
|
|
92
|
+
const req = transport.request(options, (res) => {
|
|
93
|
+
let responseData = "";
|
|
94
|
+
|
|
95
|
+
res.on("data", (chunk) => {
|
|
96
|
+
responseData += chunk;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
res.on("end", () => {
|
|
100
|
+
let parsedResponse;
|
|
101
|
+
try {
|
|
102
|
+
parsedResponse = JSON.parse(responseData);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
parsedResponse = { raw: responseData };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
msg.payload = parsedResponse;
|
|
108
|
+
msg.statusCode = res.statusCode;
|
|
109
|
+
msg.headers = res.headers;
|
|
110
|
+
|
|
111
|
+
if (res.statusCode === 201 && parsedResponse.ok) {
|
|
112
|
+
// Success
|
|
113
|
+
msg.file_id = parsedResponse.file_id;
|
|
114
|
+
node.status({
|
|
115
|
+
fill: "green",
|
|
116
|
+
shape: "dot",
|
|
117
|
+
text: `uploaded: ${parsedResponse.file_id?.substring(0, 8)}...`,
|
|
118
|
+
});
|
|
119
|
+
send(msg);
|
|
120
|
+
done();
|
|
121
|
+
|
|
122
|
+
// Clear status after 5 seconds
|
|
123
|
+
setTimeout(() => node.status({}), 5000);
|
|
124
|
+
} else {
|
|
125
|
+
// API error
|
|
126
|
+
const errorMsg = parsedResponse.error || parsedResponse.message || `HTTP ${res.statusCode}`;
|
|
127
|
+
node.status({ fill: "red", shape: "dot", text: errorMsg });
|
|
128
|
+
|
|
129
|
+
// Still send the message so users can handle errors in their flow
|
|
130
|
+
send(msg);
|
|
131
|
+
done();
|
|
132
|
+
|
|
133
|
+
// Clear status after 10 seconds
|
|
134
|
+
setTimeout(() => node.status({}), 10000);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
req.on("error", (err) => {
|
|
140
|
+
node.status({ fill: "red", shape: "ring", text: "request failed" });
|
|
141
|
+
msg.payload = { error: err.message };
|
|
142
|
+
msg.statusCode = 0;
|
|
143
|
+
send(msg);
|
|
144
|
+
done(err);
|
|
145
|
+
|
|
146
|
+
setTimeout(() => node.status({}), 10000);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Set timeout
|
|
150
|
+
const timeout = parseInt(config.timeout) || 30000;
|
|
151
|
+
req.setTimeout(timeout, () => {
|
|
152
|
+
req.destroy();
|
|
153
|
+
node.status({ fill: "red", shape: "ring", text: "timeout" });
|
|
154
|
+
msg.payload = { error: "Request timed out" };
|
|
155
|
+
msg.statusCode = 0;
|
|
156
|
+
send(msg);
|
|
157
|
+
done(new Error("Request timed out"));
|
|
158
|
+
|
|
159
|
+
setTimeout(() => node.status({}), 10000);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
req.write(body);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
node.on("close", function () {
|
|
167
|
+
node.status({});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
RED.nodes.registerType("processlink-files-upload", ProcessLinkFilesUploadNode);
|
|
172
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@processlink/node-red-contrib-processlink",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "Node-RED nodes for Process Link platform integration",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"node-red",
|
|
10
|
+
"processlink",
|
|
11
|
+
"process-link",
|
|
12
|
+
"files",
|
|
13
|
+
"upload",
|
|
14
|
+
"api",
|
|
15
|
+
"industrial",
|
|
16
|
+
"iot",
|
|
17
|
+
"automation"
|
|
18
|
+
],
|
|
19
|
+
"node-red": {
|
|
20
|
+
"version": ">=2.0.0",
|
|
21
|
+
"nodes": {
|
|
22
|
+
"processlink-config": "nodes/config/processlink-config.js",
|
|
23
|
+
"processlink-files-upload": "nodes/files/processlink-files-upload.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
28
|
+
},
|
|
29
|
+
"author": "Process Link <support@processlink.com.au>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/process-link/node-red-contrib-processlink.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/process-link/node-red-contrib-processlink/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/process-link/node-red-contrib-processlink#readme",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|