@5minds/node-red-contrib-processcube-tools 1.2.0-feature-608421-mg9cjskq → 1.2.0-feature-6d921c-mgp4itmd
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/.env.template +1 -7
- package/.mocharc.json +5 -0
- package/dist/email-receiver/email-receiver.d.ts +3 -0
- package/{email-receiver → dist/email-receiver}/email-receiver.html +4 -82
- package/dist/email-receiver/email-receiver.js +342 -0
- package/dist/email-receiver/email-receiver.js.map +1 -0
- package/dist/email-sender/email-sender.d.ts +3 -0
- package/dist/email-sender/email-sender.js +183 -0
- package/dist/email-sender/email-sender.js.map +1 -0
- package/dist/html-to-text/html-to-text.d.ts +3 -0
- package/{processcube-html-to-text/processcube-html-to-text.html → dist/html-to-text/html-to-text.html} +3 -3
- package/dist/html-to-text/html-to-text.js +40 -0
- package/dist/html-to-text/html-to-text.js.map +1 -0
- package/dist/imap-config/imap-config.d.ts +3 -0
- package/dist/imap-config/imap-config.html +139 -0
- package/dist/imap-config/imap-config.js +22 -0
- package/dist/imap-config/imap-config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/EmailReceiverMessage.d.ts +20 -0
- package/dist/interfaces/EmailReceiverMessage.js +3 -0
- package/dist/interfaces/EmailReceiverMessage.js.map +1 -0
- package/dist/interfaces/EmailSenderNodeProperties.d.ts +33 -0
- package/dist/interfaces/EmailSenderNodeProperties.js +3 -0
- package/dist/interfaces/EmailSenderNodeProperties.js.map +1 -0
- package/dist/interfaces/FetchState.d.ts +11 -0
- package/dist/interfaces/FetchState.js +3 -0
- package/dist/interfaces/FetchState.js.map +1 -0
- package/dist/interfaces/ImapConnectionConfig.d.ts +16 -0
- package/dist/interfaces/ImapConnectionConfig.js +3 -0
- package/dist/interfaces/ImapConnectionConfig.js.map +1 -0
- package/package.json +27 -17
- package/tsconfig.json +23 -0
- package/email-receiver/email-receiver.js +0 -304
- package/email-sender/email-sender.js +0 -178
- package/examples/.gitkeep +0 -0
- package/file-storage/file-storage.html +0 -203
- package/file-storage/file-storage.js +0 -148
- package/processcube-html-to-text/processcube-html-to-text.js +0 -22
- package/storage/providers/fs.js +0 -117
- package/storage/providers/postgres.js +0 -160
- package/storage/storage-core.js +0 -77
- package/test/helpers/email-receiver.mocks.js +0 -447
- package/test/helpers/email-sender.mocks.js +0 -368
- package/test/integration/email-receiver.integration.test.js +0 -515
- package/test/integration/email-sender.integration.test.js +0 -239
- package/test/unit/email-receiver.unit.test.js +0 -304
- package/test/unit/email-sender.unit.test.js +0 -570
- /package/{email-sender → dist/email-sender}/email-sender.html +0 -0
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('file-storage', {
|
|
3
|
-
category: 'ProcessCube Tools',
|
|
4
|
-
color: '#02AFD6',
|
|
5
|
-
defaults: {
|
|
6
|
-
name: { value: '' },
|
|
7
|
-
provider: { value: 'fs' },
|
|
8
|
-
baseDir: { value: '' },
|
|
9
|
-
usernameType: { value: 'str' },
|
|
10
|
-
username: { value: '', required: true, validate: RED.validators.typedInput('usernameType') },
|
|
11
|
-
passwordType: { value: 'str' },
|
|
12
|
-
password: { value: '', required: true, validate: RED.validators.typedInput('passwordType') },
|
|
13
|
-
hostType: { value: 'str' },
|
|
14
|
-
host: { value: '', required: true, validate: RED.validators.typedInput('hostType') },
|
|
15
|
-
portType: { value: 'str' },
|
|
16
|
-
port: { value: '', required: true, validate: RED.validators.typedInput('portType') },
|
|
17
|
-
databaseType: { value: 'str' },
|
|
18
|
-
database: { value: '', required: true, validate: RED.validators.typedInput('databaseType') },
|
|
19
|
-
|
|
20
|
-
pgSchema: { value: 'public' },
|
|
21
|
-
pgTable: { value: 'files' },
|
|
22
|
-
outputAs: { value: 'stream' },
|
|
23
|
-
defaultAction: { value: 'store' },
|
|
24
|
-
},
|
|
25
|
-
inputs: 1,
|
|
26
|
-
outputs: 1,
|
|
27
|
-
icon: 'file.png',
|
|
28
|
-
label: function () {
|
|
29
|
-
return this.name || 'file-storage';
|
|
30
|
-
},
|
|
31
|
-
oneditprepare: function () {
|
|
32
|
-
// postgres fields user, password, host, port, database
|
|
33
|
-
$('#node-input-username').typedInput({
|
|
34
|
-
default: 'str',
|
|
35
|
-
types: ['str', 'env'],
|
|
36
|
-
typeField: '#node-input-usernameType',
|
|
37
|
-
});
|
|
38
|
-
$('#node-input-password').typedInput({
|
|
39
|
-
default: 'str',
|
|
40
|
-
types: ['str', 'env'],
|
|
41
|
-
typeField: '#node-input-passwordType',
|
|
42
|
-
});
|
|
43
|
-
$('#node-input-host').typedInput({
|
|
44
|
-
default: 'str',
|
|
45
|
-
types: ['str', 'env'],
|
|
46
|
-
typeField: '#node-input-hostType',
|
|
47
|
-
});
|
|
48
|
-
$('#node-input-port').typedInput({
|
|
49
|
-
default: 'str',
|
|
50
|
-
types: ['str', 'env'],
|
|
51
|
-
typeField: '#node-input-portType',
|
|
52
|
-
});
|
|
53
|
-
$('#node-input-database').typedInput({
|
|
54
|
-
default: 'str',
|
|
55
|
-
types: ['str', 'env'],
|
|
56
|
-
typeField: '#node-input-databaseType',
|
|
57
|
-
});
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
</script>
|
|
61
|
-
|
|
62
|
-
<script type="text/html" data-template-name="file-storage">
|
|
63
|
-
<div class="form-row">
|
|
64
|
-
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
65
|
-
<input type="text" id="node-input-name" placeholder="file-storage" />
|
|
66
|
-
</div>
|
|
67
|
-
<div class="form-row">
|
|
68
|
-
<label for="node-input-provider"><i class="fa fa-database"></i> Provider</label>
|
|
69
|
-
<select id="node-input-provider">
|
|
70
|
-
<option value="fs">Filesystem</option>
|
|
71
|
-
<option value="pg">PostgreSQL</option>
|
|
72
|
-
</select>
|
|
73
|
-
</div>
|
|
74
|
-
<div class="form-row">
|
|
75
|
-
<label for="node-input-outputAs"><i class="fa fa-share-square-o"></i> Output</label>
|
|
76
|
-
<select id="node-input-outputAs">
|
|
77
|
-
<option value="stream">Stream</option>
|
|
78
|
-
<option value="buffer">Buffer</option>
|
|
79
|
-
<option value="path">Path (nur FS)</option>
|
|
80
|
-
</select>
|
|
81
|
-
</div>
|
|
82
|
-
<hr />
|
|
83
|
-
<div class="form-tips">Filesystem</div>
|
|
84
|
-
<div class="form-row">
|
|
85
|
-
<label for="node-input-baseDir"><i class="fa fa-folder-open"></i> Base Dir</label>
|
|
86
|
-
<input type="text" id="node-input-baseDir" placeholder="/data/files" />
|
|
87
|
-
</div>
|
|
88
|
-
<hr />
|
|
89
|
-
<div class="form-tips">PostgreSQL</div>
|
|
90
|
-
<div class="form-row">
|
|
91
|
-
<label for="node-input-username"><i class="fa fa-user"></i> Username</label>
|
|
92
|
-
<input type="text" id="node-input-username" placeholder="postgres" />
|
|
93
|
-
<input type="hidden" id="node-input-usernameType" />
|
|
94
|
-
</div>
|
|
95
|
-
<div class="form-row">
|
|
96
|
-
<label for="node-input-password"><i class="fa fa-key"></i> Password</label>
|
|
97
|
-
<input type="text" id="node-input-password" placeholder="postgres" />
|
|
98
|
-
<input type="hidden" id="node-input-passwordType" />
|
|
99
|
-
</div>
|
|
100
|
-
<div class="form-row">
|
|
101
|
-
<label for="node-input-host"><i class="fa fa-server"></i> Host</label>
|
|
102
|
-
<input type="text" id="node-input-host" placeholder="localhost" />
|
|
103
|
-
<input type="hidden" id="node-input-hostType" />
|
|
104
|
-
</div>
|
|
105
|
-
<div class="form-row">
|
|
106
|
-
<label for="node-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
107
|
-
<input type="text" id="node-input-port" placeholder="5432" />
|
|
108
|
-
<input type="hidden" id="node-input-portType" />
|
|
109
|
-
</div>
|
|
110
|
-
<div class="form-row">
|
|
111
|
-
<label for="node-input-database"><i class="fa fa-database"></i> Database</label>
|
|
112
|
-
<input type="text" id="node-input-database" placeholder="postgres" />
|
|
113
|
-
<input type="hidden" id="node-input-databaseType" />
|
|
114
|
-
</div>
|
|
115
|
-
<div class="form-row">
|
|
116
|
-
<label for="node-input-pgSchema"><i class="fa fa-sitemap"></i> Schema</label>
|
|
117
|
-
<input type="text" id="node-input-pgSchema" placeholder="public" />
|
|
118
|
-
</div>
|
|
119
|
-
<div class="form-row">
|
|
120
|
-
<label for="node-input-pgTable"><i class="fa fa-table"></i> Table</label>
|
|
121
|
-
<input type="text" id="node-input-pgTable" placeholder="files" />
|
|
122
|
-
</div>
|
|
123
|
-
<hr />
|
|
124
|
-
<div class="form-row">
|
|
125
|
-
<label for="node-input-defaultAction"><i class="fa fa-cog"></i> Default Action</label>
|
|
126
|
-
<select id="node-input-defaultAction">
|
|
127
|
-
<option value="store">store</option>
|
|
128
|
-
<option value="get">get</option>
|
|
129
|
-
<option value="delete">delete</option>
|
|
130
|
-
</select>
|
|
131
|
-
</div>
|
|
132
|
-
</script>
|
|
133
|
-
|
|
134
|
-
<script type="text/html" data-help-name="file-storage">
|
|
135
|
-
<h2>File Storage Node</h2>
|
|
136
|
-
<p>
|
|
137
|
-
A Node-RED node for storing, retrieving, and deleting files (including metadata) using either the local filesystem or PostgreSQL (Large Objects + metadata table) as a backend.
|
|
138
|
-
</p>
|
|
139
|
-
<h3>Features</h3>
|
|
140
|
-
<ul>
|
|
141
|
-
<li><b>Providers:</b>
|
|
142
|
-
<ul>
|
|
143
|
-
<li>Filesystem (stores file and metadata as JSON)</li>
|
|
144
|
-
<li>PostgreSQL (stores file as Large Object and metadata in a table)</li>
|
|
145
|
-
</ul>
|
|
146
|
-
</li>
|
|
147
|
-
<li><b>Actions:</b> Store, Retrieve, Delete files</li>
|
|
148
|
-
<li><b>Flexible output:</b> Stream, Buffer, or Path (filesystem only)</li>
|
|
149
|
-
</ul>
|
|
150
|
-
<h3>Node Properties</h3>
|
|
151
|
-
<ul>
|
|
152
|
-
<li><b>Name:</b> Optional node label.</li>
|
|
153
|
-
<li><b>Provider:</b> Select between Filesystem and PostgreSQL.</li>
|
|
154
|
-
<li><b>Output:</b> Choose the output type for retrieval: Stream, Buffer, or Path (Path only for filesystem).</li>
|
|
155
|
-
<li><b>Base Dir:</b> (Filesystem) Directory where files are stored.</li>
|
|
156
|
-
<li><b>Schema:</b> (PostgreSQL) Database schema (default: public).</li>
|
|
157
|
-
<li><b>Table:</b> (PostgreSQL) Table for metadata (default: files).</li>
|
|
158
|
-
<li><b>Default Action:</b> Default action if not specified in the message (store, get, or delete).</li>
|
|
159
|
-
</ul>
|
|
160
|
-
<h3>Input</h3>
|
|
161
|
-
<pre>
|
|
162
|
-
msg.action = "store" | "get" | "delete" // Optional, overrides defaultAction
|
|
163
|
-
msg.payload = Buffer | ReadableStream | String // For "store"
|
|
164
|
-
msg.file = {
|
|
165
|
-
id?: string, // For "get" or "delete"
|
|
166
|
-
filename?: string, // For "store"
|
|
167
|
-
contentType?: string,// For "store"
|
|
168
|
-
metadata?: object // For "store"
|
|
169
|
-
}
|
|
170
|
-
</pre>
|
|
171
|
-
<h3>Output</h3>
|
|
172
|
-
<ul>
|
|
173
|
-
<li><b>store:</b> <code>msg.payload</code> contains metadata including the generated <code>id</code>. <code>msg.file</code> contains the merged file info and result.</li>
|
|
174
|
-
<li><b>get:</b> <code>msg.payload</code> contains the file as a Stream, Buffer, or Path (depending on output setting). <code>msg.file</code> contains the file metadata.</li>
|
|
175
|
-
<li><b>delete:</b> <code>msg.payload</code> contains the result of the delete operation.</li>
|
|
176
|
-
</ul>
|
|
177
|
-
<h3>Example Usage</h3>
|
|
178
|
-
<pre>
|
|
179
|
-
// Store a file:
|
|
180
|
-
msg.action = "store";
|
|
181
|
-
msg.payload = Buffer.from("Hello World");
|
|
182
|
-
msg.file = {
|
|
183
|
-
filename: "hello.txt",
|
|
184
|
-
contentType: "text/plain",
|
|
185
|
-
metadata: { author: "Alice" }
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Retrieve a file:
|
|
189
|
-
msg.action = "get";
|
|
190
|
-
msg.file = { id: "your-file-id" };
|
|
191
|
-
|
|
192
|
-
// Delete a file:
|
|
193
|
-
msg.action = "delete";
|
|
194
|
-
msg.file = { id: "your-file-id" };
|
|
195
|
-
</pre>
|
|
196
|
-
<h3>Notes</h3>
|
|
197
|
-
<ul>
|
|
198
|
-
<li>For PostgreSQL, ensure the connection string, schema, and table exist and the user has the necessary permissions.</li>
|
|
199
|
-
<li>For filesystem storage, ensure the base directory is writable by Node-RED.</li>
|
|
200
|
-
<li>The node is designed to handle large files efficiently using streams.</li>
|
|
201
|
-
</ul>
|
|
202
|
-
<p><b>Enjoy using the File Storage Node in your Node-RED flows!</b></p>
|
|
203
|
-
</script>
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* # File Storage Node
|
|
3
|
-
*
|
|
4
|
-
* A Node-RED node for storing, retrieving, and deleting files (including metadata) using either the local filesystem or PostgreSQL (Large Objects + metadata table) as a backend.
|
|
5
|
-
*
|
|
6
|
-
* ## Features
|
|
7
|
-
* - **Providers:**
|
|
8
|
-
* - Filesystem (stores file and metadata as JSON)
|
|
9
|
-
* - PostgreSQL (stores file as Large Object and metadata in a table)
|
|
10
|
-
* - **Actions:**
|
|
11
|
-
* - Store a file
|
|
12
|
-
* - Retrieve a file
|
|
13
|
-
* - Delete a file
|
|
14
|
-
* - **Flexible output:**
|
|
15
|
-
* - Stream, Buffer, or Path (filesystem only)
|
|
16
|
-
*
|
|
17
|
-
* ## Node Properties
|
|
18
|
-
* - **Name:** Optional node label.
|
|
19
|
-
* - **Provider:** Select between `Filesystem` and `PostgreSQL`.
|
|
20
|
-
* - **Output:** Choose the output type for retrieval: `Stream`, `Buffer`, or `Path` (Path only for filesystem).
|
|
21
|
-
* - **Base Dir:** (Filesystem) Directory where files are stored.
|
|
22
|
-
* - **Connection:** (PostgreSQL) Connection string for the database.
|
|
23
|
-
* - **Schema:** (PostgreSQL) Database schema (default: `public`).
|
|
24
|
-
* - **Table:** (PostgreSQL) Table for metadata (default: `files`).
|
|
25
|
-
* - **Default Action:** Default action if not specified in the message (`store`, `get`, or `delete`).
|
|
26
|
-
*
|
|
27
|
-
* ## Input
|
|
28
|
-
* The node expects the following properties in the incoming message:
|
|
29
|
-
* ```
|
|
30
|
-
* msg.action = "store" | "get" | "delete" // Optional, overrides defaultAction
|
|
31
|
-
* msg.payload = Buffer | ReadableStream | String // For "store"
|
|
32
|
-
* msg.file = {
|
|
33
|
-
* id?: string, // For "get" or "delete"
|
|
34
|
-
* filename?: string, // For "store"
|
|
35
|
-
* contentType?: string,// For "store"
|
|
36
|
-
* metadata?: object // For "store"
|
|
37
|
-
* }
|
|
38
|
-
* ```
|
|
39
|
-
*
|
|
40
|
-
* ## Output
|
|
41
|
-
* - For **store**:
|
|
42
|
-
* - `msg.payload` contains metadata including the generated `id`.
|
|
43
|
-
* - `msg.file` contains the merged file info and result.
|
|
44
|
-
* - For **get**:
|
|
45
|
-
* - `msg.payload` contains the file as a Stream, Buffer, or Path (depending on output setting).
|
|
46
|
-
* - `msg.file` contains the file metadata.
|
|
47
|
-
* - For **delete**:
|
|
48
|
-
* - `msg.payload` contains the result of the delete operation.
|
|
49
|
-
*
|
|
50
|
-
* ## Example Usage
|
|
51
|
-
* // Store a file:
|
|
52
|
-
* msg.action = "store";
|
|
53
|
-
* msg.payload = Buffer.from("Hello World");
|
|
54
|
-
* msg.file = {
|
|
55
|
-
* filename: "hello.txt",
|
|
56
|
-
* contentType: "text/plain",
|
|
57
|
-
* metadata: { author: "Alice" }
|
|
58
|
-
* };
|
|
59
|
-
*
|
|
60
|
-
* // Retrieve a file:
|
|
61
|
-
* msg.action = "get";
|
|
62
|
-
* msg.file = { id: "your-file-id" };
|
|
63
|
-
*
|
|
64
|
-
* // Delete a file:
|
|
65
|
-
* msg.action = "delete";
|
|
66
|
-
* msg.file = { id: "your-file-id" };
|
|
67
|
-
*
|
|
68
|
-
* ## Notes
|
|
69
|
-
* - For PostgreSQL, ensure the connection string, schema, and table exist and the user has the necessary permissions.
|
|
70
|
-
* - For filesystem storage, ensure the base directory is writable by Node-RED.
|
|
71
|
-
* - The node is designed to handle large files efficiently using streams.
|
|
72
|
-
*
|
|
73
|
-
* Enjoy using the File Storage Node in your Node-RED flows!
|
|
74
|
-
*/
|
|
75
|
-
module.exports = function (RED) {
|
|
76
|
-
const StorageCore = require('../storage/storage-core');
|
|
77
|
-
|
|
78
|
-
function FileStorageNode(config) {
|
|
79
|
-
RED.nodes.createNode(this, config);
|
|
80
|
-
const node = this;
|
|
81
|
-
|
|
82
|
-
// Node-Konfiguration
|
|
83
|
-
node.provider = config.provider || 'fs';
|
|
84
|
-
node.baseDir = config.baseDir;
|
|
85
|
-
node.pg = {
|
|
86
|
-
username: RED.util.evaluateNodeProperty(config.username, config.usernameType, node) || 'postgres',
|
|
87
|
-
password: RED.util.evaluateNodeProperty(config.password, config.passwordType, node) || 'postgres',
|
|
88
|
-
host: RED.util.evaluateNodeProperty(config.host, config.hostType, node) || 'localhost',
|
|
89
|
-
port: RED.util.evaluateNodeProperty(config.port, config.portType, node) || 5432,
|
|
90
|
-
database: RED.util.evaluateNodeProperty(config.database, config.databaseType, node) || 'postgres',
|
|
91
|
-
schema: config.pgSchema || 'public',
|
|
92
|
-
table: config.pgTable || 'files',
|
|
93
|
-
};
|
|
94
|
-
node.outputAs = config.outputAs || 'stream'; // 'stream' | 'buffer' | 'path' (path nur fs)
|
|
95
|
-
|
|
96
|
-
// Storage-Kern
|
|
97
|
-
const storage = new StorageCore({
|
|
98
|
-
provider: node.provider,
|
|
99
|
-
fs: { baseDir: node.baseDir },
|
|
100
|
-
pg: node.pg,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
storage.init().catch((err) => node.error(err));
|
|
104
|
-
|
|
105
|
-
node.on('input', async function (msg, send, done) {
|
|
106
|
-
try {
|
|
107
|
-
const action = msg.action || config.defaultAction || 'store';
|
|
108
|
-
if (action === 'store') {
|
|
109
|
-
const file = msg.file || {};
|
|
110
|
-
const result = await storage.store(msg.payload, file);
|
|
111
|
-
msg.payload = result;
|
|
112
|
-
msg.file = { ...file, ...result };
|
|
113
|
-
send(msg);
|
|
114
|
-
done();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (action === 'get') {
|
|
119
|
-
const id = msg.file && msg.file.id;
|
|
120
|
-
if (!id) throw new Error('file.id is required for get');
|
|
121
|
-
const { meta, payload } = await storage.get(id, { as: node.outputAs });
|
|
122
|
-
msg.file = { ...meta, id: meta.id };
|
|
123
|
-
msg.payload = payload;
|
|
124
|
-
send(msg);
|
|
125
|
-
done();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (action === 'delete') {
|
|
130
|
-
const id = msg.file && msg.file.id;
|
|
131
|
-
if (!id) throw new Error('file.id is required for delete');
|
|
132
|
-
const result = await storage.delete(id);
|
|
133
|
-
msg.payload = result;
|
|
134
|
-
send(msg);
|
|
135
|
-
done();
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
throw new Error(`Unknown action: ${action}`);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
node.error(err, msg);
|
|
142
|
-
if (done) done(err);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
RED.nodes.registerType('file-storage', FileStorageNode);
|
|
148
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
module.exports = function (RED) {
|
|
2
|
-
const { compile } = require('html-to-text');
|
|
3
|
-
|
|
4
|
-
function ProcesscubeHtmlToText(config) {
|
|
5
|
-
RED.nodes.createNode(this, config);
|
|
6
|
-
const node = this;
|
|
7
|
-
|
|
8
|
-
const options = {
|
|
9
|
-
wordwrap: 130,
|
|
10
|
-
// ...
|
|
11
|
-
};
|
|
12
|
-
const compiledConvert = compile(options); // options passed here
|
|
13
|
-
|
|
14
|
-
node.on('input', async function (msg) {
|
|
15
|
-
msg.payload = compiledConvert(msg.payload);
|
|
16
|
-
|
|
17
|
-
node.send(msg);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
RED.nodes.registerType('processcube-html-to-text', ProcesscubeHtmlToText);
|
|
22
|
-
};
|
package/storage/providers/fs.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const fsp = require('fs/promises');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { pipeline } = require('stream');
|
|
5
|
-
const { createHash } = require('crypto');
|
|
6
|
-
const { promisify } = require('util');
|
|
7
|
-
const pump = promisify(pipeline);
|
|
8
|
-
|
|
9
|
-
class FsProvider {
|
|
10
|
-
constructor(opts = {}) {
|
|
11
|
-
this.baseDir = opts.baseDir || path.resolve(process.cwd(), 'data');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async init() {
|
|
15
|
-
await fsp.mkdir(this.baseDir, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_buildPaths(id) {
|
|
19
|
-
const d = new Date();
|
|
20
|
-
const parts = [
|
|
21
|
-
String(d.getUTCFullYear()),
|
|
22
|
-
String(d.getUTCMonth() + 1).padStart(2, '0'),
|
|
23
|
-
String(d.getUTCDate()).padStart(2, '0'),
|
|
24
|
-
];
|
|
25
|
-
const dir = path.join(this.baseDir, ...parts);
|
|
26
|
-
const filePath = path.join(dir, id);
|
|
27
|
-
const metaPath = path.join(dir, `${id}.json`);
|
|
28
|
-
return { dir, filePath, metaPath };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async store(readable, info) {
|
|
32
|
-
const { id, filename, contentType, metadata, createdAt } = info;
|
|
33
|
-
const { dir, filePath, metaPath } = this._buildPaths(id);
|
|
34
|
-
await fsp.mkdir(dir, { recursive: true });
|
|
35
|
-
|
|
36
|
-
const hash = createHash('sha256');
|
|
37
|
-
let size = 0;
|
|
38
|
-
|
|
39
|
-
const out = fs.createWriteStream(filePath);
|
|
40
|
-
readable.on('data', (chunk) => {
|
|
41
|
-
hash.update(chunk);
|
|
42
|
-
size += chunk.length;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
await pump(readable, out);
|
|
46
|
-
|
|
47
|
-
const sha256 = hash.digest('hex');
|
|
48
|
-
const meta = { id, filename, contentType, size, sha256, metadata, createdAt };
|
|
49
|
-
await fsp.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
50
|
-
|
|
51
|
-
return { size, sha256, path: filePath };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async get(id, options = { as: 'stream' }) {
|
|
55
|
-
// Find meta file by searching dated folders
|
|
56
|
-
const meta = await this._findMeta(id);
|
|
57
|
-
if (!meta) throw new Error(`File not found: ${id}`);
|
|
58
|
-
const filePath = meta.__filePath;
|
|
59
|
-
|
|
60
|
-
if (options.as === 'path') {
|
|
61
|
-
return { meta, payload: filePath };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (options.as === 'buffer') {
|
|
65
|
-
const buf = await fsp.readFile(filePath);
|
|
66
|
-
return { meta, payload: buf };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// default: stream
|
|
70
|
-
const stream = fs.createReadStream(filePath);
|
|
71
|
-
return { meta, payload: stream };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async delete(id) {
|
|
75
|
-
const meta = await this._findMeta(id);
|
|
76
|
-
if (!meta) return; // idempotent
|
|
77
|
-
await fsp.unlink(meta.__filePath).catch(() => {});
|
|
78
|
-
await fsp.unlink(meta.__metaPath).catch(() => {});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async _findMeta(id) {
|
|
82
|
-
// Walk date folders (YYYY/MM/DD). For Performance: keep index/cache in prod.
|
|
83
|
-
const years = await this._ls(this.baseDir);
|
|
84
|
-
for (const y of years) {
|
|
85
|
-
const yearDir = path.join(this.baseDir, y);
|
|
86
|
-
const months = await this._ls(yearDir);
|
|
87
|
-
for (const m of months) {
|
|
88
|
-
const monthDir = path.join(yearDir, m);
|
|
89
|
-
const days = await this._ls(monthDir);
|
|
90
|
-
for (const d of days) {
|
|
91
|
-
const dir = path.join(monthDir, d);
|
|
92
|
-
const metaPath = path.join(dir, `${id}.json`);
|
|
93
|
-
try {
|
|
94
|
-
const raw = await fsp.readFile(metaPath, 'utf-8');
|
|
95
|
-
const meta = JSON.parse(raw);
|
|
96
|
-
meta.__metaPath = metaPath;
|
|
97
|
-
meta.__filePath = path.join(dir, id);
|
|
98
|
-
return meta;
|
|
99
|
-
} catch (_) {
|
|
100
|
-
/* continue */
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async _ls(dir) {
|
|
109
|
-
try {
|
|
110
|
-
return (await fsp.readdir(dir)).filter((n) => !n.startsWith('.'));
|
|
111
|
-
} catch {
|
|
112
|
-
return [];
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
module.exports = FsProvider;
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
const { Pool } = require('pg');
|
|
2
|
-
const { LargeObjectManager } = require('pg-large-object');
|
|
3
|
-
const { pipeline, PassThrough } = require('stream');
|
|
4
|
-
const { createHash } = require('crypto');
|
|
5
|
-
const { promisify } = require('util');
|
|
6
|
-
const pump = promisify(pipeline);
|
|
7
|
-
|
|
8
|
-
class PgProvider {
|
|
9
|
-
constructor(opts = {}) {
|
|
10
|
-
const conString = "postgres://" + opts.username + ":" + opts.password + "@" + opts.host + ":" + opts.port + "/" + opts.database;
|
|
11
|
-
this.connectionString = conString
|
|
12
|
-
this.schema = opts.schema || 'public';
|
|
13
|
-
this.table = opts.table || 'files';
|
|
14
|
-
this.pool = new Pool({ connectionString: this.connectionString });
|
|
15
|
-
this.pool = new Pool();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async init() {
|
|
19
|
-
const client = await this.pool.connect();
|
|
20
|
-
try {
|
|
21
|
-
await client.query(`CREATE TABLE IF NOT EXISTS ${this.schema}.${this.table} (
|
|
22
|
-
id UUID PRIMARY KEY,
|
|
23
|
-
loid OID NOT NULL,
|
|
24
|
-
filename TEXT,
|
|
25
|
-
content_type TEXT,
|
|
26
|
-
size BIGINT,
|
|
27
|
-
sha256 TEXT,
|
|
28
|
-
metadata JSONB,
|
|
29
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
30
|
-
)`);
|
|
31
|
-
await client.query(
|
|
32
|
-
`CREATE INDEX IF NOT EXISTS idx_${this.table}_created_at ON ${this.schema}.${this.table}(created_at)`,
|
|
33
|
-
);
|
|
34
|
-
} finally {
|
|
35
|
-
client.release();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async store(readable, info) {
|
|
40
|
-
const { id, filename, contentType, metadata, createdAt } = info;
|
|
41
|
-
const client = await this.pool.connect();
|
|
42
|
-
try {
|
|
43
|
-
const lom = new LargeObjectManager({pg: client});
|
|
44
|
-
await client.query('BEGIN');
|
|
45
|
-
const bufSize = 16384;
|
|
46
|
-
|
|
47
|
-
const result = await lom.createAndWritableStreamAsync(bufSize);
|
|
48
|
-
const oid =result[0];
|
|
49
|
-
const stream = result[1];
|
|
50
|
-
if (!oid || !stream) {
|
|
51
|
-
throw new Error('Failed to create large object');
|
|
52
|
-
}
|
|
53
|
-
const hash = createHash('sha256');
|
|
54
|
-
let size = 0;
|
|
55
|
-
|
|
56
|
-
// Wir berechnen Hash und Größe "on the fly", während wir in das Large Object schreiben
|
|
57
|
-
const hashAndSize = new PassThrough();
|
|
58
|
-
hashAndSize.on('data', (chunk) => {
|
|
59
|
-
hash.update(chunk);
|
|
60
|
-
size += chunk.length;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// readable -> hashAndSize -> stream
|
|
64
|
-
const pipelinePromise = pump(readable, hashAndSize, stream);
|
|
65
|
-
|
|
66
|
-
await pipelinePromise;
|
|
67
|
-
|
|
68
|
-
const sha256 = hash.digest('hex');
|
|
69
|
-
|
|
70
|
-
await client.query(
|
|
71
|
-
`INSERT INTO ${this.schema}.${this.table} (id, loid, filename, content_type, size, sha256, metadata, created_at)
|
|
72
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)`,
|
|
73
|
-
[id, oid, filename, contentType, size, sha256, JSON.stringify(metadata || {}), createdAt],
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await client.query('COMMIT');
|
|
77
|
-
return { size, sha256, oid };
|
|
78
|
-
} catch (err) {
|
|
79
|
-
await client.query('ROLLBACK').catch(() => {});
|
|
80
|
-
throw err;
|
|
81
|
-
} finally {
|
|
82
|
-
client.release();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async get(id, options = { as: 'stream' }) {
|
|
87
|
-
const client = await this.pool.connect();
|
|
88
|
-
try {
|
|
89
|
-
const { rows } = await client.query(`SELECT * FROM ${this.schema}.${this.table} WHERE id=$1`, [id]);
|
|
90
|
-
if (rows.length === 0) throw new Error(`File not found: ${id}`);
|
|
91
|
-
const meta = rows[0];
|
|
92
|
-
|
|
93
|
-
const bufSize = 16384;
|
|
94
|
-
if (options.as === 'buffer') {
|
|
95
|
-
// Stream LO into memory
|
|
96
|
-
await client.query('BEGIN');
|
|
97
|
-
const lom = new LargeObjectManager({ pg: client });
|
|
98
|
-
|
|
99
|
-
const ro = await lom.openAndReadableStreamAsync(meta.loid, bufSize);
|
|
100
|
-
const totalSize = ro[0];
|
|
101
|
-
const stream = ro[1];
|
|
102
|
-
if (!stream) {
|
|
103
|
-
throw new Error('Failed to open large object for reading');
|
|
104
|
-
}
|
|
105
|
-
const chunks = [];
|
|
106
|
-
stream.on('data', (c) => chunks.push(c));
|
|
107
|
-
await new Promise((res, rej) => stream.on('end', res).on('error', rej));
|
|
108
|
-
await client.query('COMMIT');
|
|
109
|
-
client.release();
|
|
110
|
-
return { meta, payload: Buffer.concat(chunks) };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (options.as === 'path') {
|
|
114
|
-
throw new Error('options.as="path" is not supported by Postgres provider');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// default: stream – wrap LO stream so we can close txn when done
|
|
118
|
-
await client.query('BEGIN');
|
|
119
|
-
const lom = new LargeObjectManager({ pg: client });
|
|
120
|
-
const ro = await lom.openAndReadableStreamAsync(meta.loid, bufSize);
|
|
121
|
-
const totalSize = ro[0];
|
|
122
|
-
const stream = ro[1];
|
|
123
|
-
const pass = new PassThrough();
|
|
124
|
-
stream.pipe(pass);
|
|
125
|
-
const done = new Promise((res, rej) => pass.on('end', res).on('error', rej));
|
|
126
|
-
done.finally(async () => {
|
|
127
|
-
await client.query('COMMIT').catch(() => {});
|
|
128
|
-
client.release();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Do not release here; we release in finally of wrapper. We return early, so prevent double release.
|
|
132
|
-
return { meta, payload: pass };
|
|
133
|
-
} catch (err) {
|
|
134
|
-
|
|
135
|
-
throw err;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async delete(id) {
|
|
140
|
-
const client = await this.pool.connect();
|
|
141
|
-
try {
|
|
142
|
-
await client.query('BEGIN');
|
|
143
|
-
const { rows } = await client.query(`DELETE FROM ${this.schema}.${this.table} WHERE id=$1 RETURNING loid`, [
|
|
144
|
-
id,
|
|
145
|
-
]);
|
|
146
|
-
if (rows.length) {
|
|
147
|
-
const lom = new LargeObjectManager({ pg: client });
|
|
148
|
-
await lom.unlinkAsync(rows[0].loid);
|
|
149
|
-
}
|
|
150
|
-
await client.query('COMMIT');
|
|
151
|
-
} catch (err) {
|
|
152
|
-
await client.query('ROLLBACK').catch(() => {});
|
|
153
|
-
throw err;
|
|
154
|
-
} finally {
|
|
155
|
-
client.release();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
module.exports = PgProvider;
|