n8n-nodes-smbclient 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/.editorconfig +20 -0
- package/.github/workflows/npm-publish.yml +32 -0
- package/.github/workflows/one-shot-npm-publish.yml +30 -0
- package/.prettierrc.js +51 -0
- package/LICENSE.md +19 -0
- package/Readme.md +131 -0
- package/credentials/Smb2Api.credentials.ts +72 -0
- package/credentials/smb2.svg +36 -0
- package/dist/credentials/Smb2Api.credentials.d.ts +7 -0
- package/dist/credentials/Smb2Api.credentials.js +72 -0
- package/dist/credentials/Smb2Api.credentials.js.map +1 -0
- package/dist/credentials/smb2.svg +36 -0
- package/dist/nodes/Smb2/Smb2.node.d.ts +5 -0
- package/dist/nodes/Smb2/Smb2.node.js +174 -0
- package/dist/nodes/Smb2/Smb2.node.js.map +1 -0
- package/dist/nodes/Smb2/Smb2.node.json +19 -0
- package/dist/nodes/Smb2/SmbClientWrapper.d.ts +22 -0
- package/dist/nodes/Smb2/SmbClientWrapper.js +231 -0
- package/dist/nodes/Smb2/SmbClientWrapper.js.map +1 -0
- package/dist/nodes/Smb2/SmbEntryHelpers.d.ts +6 -0
- package/dist/nodes/Smb2/SmbEntryHelpers.js +102 -0
- package/dist/nodes/Smb2/SmbEntryHelpers.js.map +1 -0
- package/dist/nodes/Smb2/interfaces.d.ts +31 -0
- package/dist/nodes/Smb2/interfaces.js +3 -0
- package/dist/nodes/Smb2/interfaces.js.map +1 -0
- package/dist/nodes/Smb2/smb2.svg +36 -0
- package/dist/package.json +67 -0
- package/eslint.config.mjs +174 -0
- package/gulpfile.js +16 -0
- package/index.js +0 -0
- package/nodes/Smb2/Smb2.node.json +19 -0
- package/nodes/Smb2/Smb2.node.ts +184 -0
- package/nodes/Smb2/SmbClientWrapper.ts +285 -0
- package/nodes/Smb2/SmbEntryHelpers.ts +129 -0
- package/nodes/Smb2/interfaces.ts +39 -0
- package/nodes/Smb2/smb2.svg +36 -0
- package/package.json +67 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IExecuteFunctions,
|
|
3
|
+
INode, ITriggerFunctions, NodeApiError,
|
|
4
|
+
NodeOperationError
|
|
5
|
+
} from "n8n-workflow";
|
|
6
|
+
import {promisify} from "node:util";
|
|
7
|
+
import {execFile} from "node:child_process";
|
|
8
|
+
import {Smb2Credentials, SmbListEntry, SmbStat} from "./interfaces";
|
|
9
|
+
import {debuglog} from "util";
|
|
10
|
+
|
|
11
|
+
const debug = debuglog("n8n-nodes-smbclient");
|
|
12
|
+
// Refaire une map d’erreurs si besoin
|
|
13
|
+
const SMB_ERROR_HINTS: Array<[RegExp, string]> = [
|
|
14
|
+
[/EACCES/i, "Access Denied - Check your permissions for this file/folder"],
|
|
15
|
+
[/ENOENT/i, "File/Path Not Found"],
|
|
16
|
+
[/ENOTDIR/i, "Not a directory"],
|
|
17
|
+
[/ETIMEOUT/i, "Connection timed out"],
|
|
18
|
+
[/ECONNREFUSED/i, "Could not connect to SMB server - Connection refused"],
|
|
19
|
+
[/LOGON failure/i, "Logon Failure - Check your username, password, and domain"],
|
|
20
|
+
[/bad network name/i, "Bad Network Name - The specified share does not exist on the server"],
|
|
21
|
+
// etc.
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function getReadableError(error: any): string {
|
|
25
|
+
const msg = error?.message ?? String(error);
|
|
26
|
+
for (const [re, friendly] of SMB_ERROR_HINTS) {
|
|
27
|
+
if (re.test(msg)) {
|
|
28
|
+
return `${friendly} (${msg})`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return msg;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const execFileAsync = promisify(execFile);
|
|
36
|
+
const redactCmd = (s: string): string => {
|
|
37
|
+
if (!s) return s;
|
|
38
|
+
s = s.replace(/-U\s+(\S=)/g, '-U ***');
|
|
39
|
+
s = s.replace(/-A\s=\S+/g, '-A ***');
|
|
40
|
+
s = s.replace(/\/\/[^/\s]+\/\S+/g, '//***');
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SmbClientWrapper {
|
|
45
|
+
constructor(
|
|
46
|
+
private auth: Smb2Credentials,
|
|
47
|
+
private smbclientPath: string = 'smbclient',
|
|
48
|
+
private node: INode
|
|
49
|
+
) {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
private buildBaseArgs(): string[] {
|
|
54
|
+
const {host, username, password, domain, share} = this.auth;
|
|
55
|
+
|
|
56
|
+
// Anonymous if no username supplied
|
|
57
|
+
let userPart = '%';
|
|
58
|
+
if (username) {
|
|
59
|
+
if (!domain) {
|
|
60
|
+
userPart = `${username}%${password ?? ''}`
|
|
61
|
+
} else {
|
|
62
|
+
userPart = `${domain}/${username}%${password ?? ''}`
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const hostPart = `\\\\${host}\\${share}`;
|
|
66
|
+
return [hostPart, '-U', userPart, '-g'];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async runOne(cmd: string): Promise<string> {
|
|
70
|
+
const args = [...this.buildBaseArgs(), '-c', cmd];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const {stdout, stderr} = await execFileAsync(this.smbclientPath, args, {
|
|
74
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
75
|
+
});
|
|
76
|
+
// smbclient sometimes prints warnings to stderr; detect real errors
|
|
77
|
+
if (stderr && /NT_STATUS|Error|failed/i.test(stderr)) {
|
|
78
|
+
const {username, password} = this.auth;
|
|
79
|
+
const safeCmd = redactCmd([this.smbclientPath, ...args].join(' ')).replace(username, '***').replace(password, '***');
|
|
80
|
+
throw new NodeOperationError(this.node, {
|
|
81
|
+
message: `smbclient failed. cmd="${safeCmd}" stderr="${stderr.trim()}"`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return stdout ?? '';
|
|
85
|
+
} catch (err) {
|
|
86
|
+
|
|
87
|
+
const {username, password} = this.auth;
|
|
88
|
+
const safeCmd = redactCmd(err?.cmd || [this.smbclientPath, ...args].join(' ')).replace(username, '***').replace(password, '***');
|
|
89
|
+
const msg = (err?.stderr?.trim?.() || err?.message || 'smbclient command failed').replace(username, '***').replace(password, '***');
|
|
90
|
+
throw new NodeOperationError(this.node, {
|
|
91
|
+
message: `smbclient failed. cmd="${safeCmd}" stderr="${String(msg)}`
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async stat(remotePath: string): Promise<SmbStat> {
|
|
98
|
+
const out = await this.runOne(`allinfo "${remotePath}"`);
|
|
99
|
+
const info = new Map<string, string>();
|
|
100
|
+
for (const line of out.split('\n')) {
|
|
101
|
+
const [k, v] = line.split('|');
|
|
102
|
+
if (k && v !== undefined) info.set(k.trim().toUpperCase(), v.trim());
|
|
103
|
+
}
|
|
104
|
+
const attrs = (info.get('ATTRIBUTES') ?? '').split('').filter(Boolean);
|
|
105
|
+
return {
|
|
106
|
+
size: info.get('SIZE') ? Number(info.get('SIZE')) : undefined,
|
|
107
|
+
createTime: info.get('CREATE_TIME') ?? undefined,
|
|
108
|
+
accessTime: info.get('ACCESS_TIME') ?? undefined,
|
|
109
|
+
writeTime: info.get('WRITE_TIME') ?? undefined,
|
|
110
|
+
changeTime: info.get('CHANGE_TIME') ?? undefined,
|
|
111
|
+
attributes: attrs,
|
|
112
|
+
isDirectory: attrs.includes('D'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async list(dir: string): Promise<SmbListEntry[]> {
|
|
117
|
+
const out = await this.runOne(`ls "${dir}"`);
|
|
118
|
+
|
|
119
|
+
const lines = out
|
|
120
|
+
.split('\n')
|
|
121
|
+
.map((l) => l.trim())
|
|
122
|
+
.filter(Boolean);
|
|
123
|
+
|
|
124
|
+
const parsed = lines.map((line) => {
|
|
125
|
+
// ---------- Case 1: pipe-delimited ----------
|
|
126
|
+
if (line.includes('|')) {
|
|
127
|
+
const parts = line.split('|');
|
|
128
|
+
|
|
129
|
+
// Try common pattern: name|size|date|time|attr
|
|
130
|
+
if (parts.length >= 5 && /^\d+$/.test(parts[1])) {
|
|
131
|
+
const name = parts[0];
|
|
132
|
+
const size = Number(parts[1]) || 0;
|
|
133
|
+
const date = parts[2];
|
|
134
|
+
const time = parts[3];
|
|
135
|
+
const attr = parts[4] ?? '';
|
|
136
|
+
const attributes = attr.split('').filter(Boolean);
|
|
137
|
+
return {
|
|
138
|
+
name,
|
|
139
|
+
size,
|
|
140
|
+
date,
|
|
141
|
+
time,
|
|
142
|
+
attributes,
|
|
143
|
+
isDirectory: attributes.includes('D'),
|
|
144
|
+
} as SmbListEntry;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Alternate pattern seen on some builds: attr|size|date|time|name
|
|
148
|
+
if (parts.length >= 5 && /^\d+$/.test(parts[1])) {
|
|
149
|
+
const attr = parts[0] ?? '';
|
|
150
|
+
const size = Number(parts[1]) || 0;
|
|
151
|
+
const date = parts[2];
|
|
152
|
+
const time = parts[3];
|
|
153
|
+
const name = parts.slice(4).join('|'); // keep any extra pipes in name
|
|
154
|
+
const attributes = attr.split('').filter(Boolean);
|
|
155
|
+
return {
|
|
156
|
+
name,
|
|
157
|
+
size,
|
|
158
|
+
date,
|
|
159
|
+
time,
|
|
160
|
+
attributes,
|
|
161
|
+
isDirectory: attributes.includes('D'),
|
|
162
|
+
} as SmbListEntry;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback if unexpected pipe layout: treat whole line as name
|
|
166
|
+
return {
|
|
167
|
+
name: line,
|
|
168
|
+
size: 0,
|
|
169
|
+
attributes: [],
|
|
170
|
+
isDirectory: false,
|
|
171
|
+
} as SmbListEntry;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------- Case 2: space-aligned columns (your example) ----------
|
|
175
|
+
// Example:
|
|
176
|
+
// 11100 MY FILE_NAME R-1234.pdf A 2058 Thu Sep 4 14:39:03 2025
|
|
177
|
+
const compact = line.replace(/\s+/g, ' ').trim();
|
|
178
|
+
const tokens = compact.split(' ');
|
|
179
|
+
|
|
180
|
+
// We expect: ... <ATTR> <SIZE> <Wkd> <Mon> <dd> <hh:mm:ss> <yyyy>
|
|
181
|
+
if (tokens.length >= 8) {
|
|
182
|
+
const year = tokens[tokens.length - 1];
|
|
183
|
+
const time = tokens[tokens.length - 2];
|
|
184
|
+
const day = tokens[tokens.length - 3];
|
|
185
|
+
const month = tokens[tokens.length - 4];
|
|
186
|
+
const weekday = tokens[tokens.length - 5];
|
|
187
|
+
const sizeStr = tokens[tokens.length - 6];
|
|
188
|
+
const attr = tokens[tokens.length - 7];
|
|
189
|
+
|
|
190
|
+
const name = tokens.slice(0, tokens.length - 7).join(' ');
|
|
191
|
+
const size = Number(sizeStr) || 0;
|
|
192
|
+
const attributes = (attr ?? '').split('').filter(Boolean);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
name,
|
|
196
|
+
size,
|
|
197
|
+
date: `${weekday} ${month} ${day} ${time} ${year}`,
|
|
198
|
+
time,
|
|
199
|
+
attributes,
|
|
200
|
+
isDirectory: attributes.includes('D'),
|
|
201
|
+
} as SmbListEntry;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Minimal fallback
|
|
205
|
+
return {
|
|
206
|
+
name: line,
|
|
207
|
+
size: 0,
|
|
208
|
+
attributes: [],
|
|
209
|
+
isDirectory: false,
|
|
210
|
+
} as SmbListEntry;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return parsed.filter((entry) => {
|
|
214
|
+
if (!entry) return false;
|
|
215
|
+
|
|
216
|
+
// Skip . and ..
|
|
217
|
+
if (entry.name === '.' || entry.name === '..') return false;
|
|
218
|
+
|
|
219
|
+
// Skip smbclient footer lines like "blocks available"
|
|
220
|
+
if (/blocks available/i.test(entry.date || '') || /blocks available/i.test(entry.name)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Skip stray numeric-only names (these are usually size/blocks artifacts)
|
|
225
|
+
if (/^\d+$/.test(entry.name)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async get(remotePath: string, localPath: string): Promise<void> {
|
|
235
|
+
await this.runOne(`get "${remotePath}" "${localPath}"`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async put(localPath: string, remotePath: string): Promise<void> {
|
|
239
|
+
await this.runOne(`put "${localPath}" "${remotePath}"`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async mkdir(remoteDir: string): Promise<void> {
|
|
243
|
+
await this.runOne(`mkdir "${remoteDir}"`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async rmdir(remoteDir: string): Promise<void> {
|
|
247
|
+
await this.runOne(`rmdir "${remoteDir}"`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async del(remotePath: string): Promise<void> {
|
|
251
|
+
await this.runOne(`del "${remotePath}"`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// No persistent connection to close for smbclient CLI, but keep API parity.
|
|
255
|
+
async close(): Promise<void> {
|
|
256
|
+
/* no-op */
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function connectToSmbServer(
|
|
261
|
+
this: IExecuteFunctions | ITriggerFunctions
|
|
262
|
+
): Promise<{ client: SmbClientWrapper }> {
|
|
263
|
+
try {
|
|
264
|
+
const credentials = (await this.getCredentials("smb2Api")) as unknown as Smb2Credentials;
|
|
265
|
+
|
|
266
|
+
debug(
|
|
267
|
+
"Connecting to //%s/%s as (%s\\%s)",
|
|
268
|
+
credentials.host,
|
|
269
|
+
credentials.share,
|
|
270
|
+
credentials.domain ?? "",
|
|
271
|
+
credentials.username,
|
|
272
|
+
);
|
|
273
|
+
const smbclientPath = this.getNodeParameter('smbclientPath', 0, 'smbclient') as string;
|
|
274
|
+
const client = new SmbClientWrapper(credentials, smbclientPath, this.getNode());
|
|
275
|
+
|
|
276
|
+
// Optionnel: tester existence ou list root pour vérifier la connexion
|
|
277
|
+
await client.list(""); // ou client.exists(".");
|
|
278
|
+
|
|
279
|
+
return {client};
|
|
280
|
+
} catch (error: any) {
|
|
281
|
+
debug("Connect error: %O", error);
|
|
282
|
+
const readableError = getReadableError(error);
|
|
283
|
+
throw new NodeApiError(this.getNode(), error, {message: `Failed to connect to SMB server: ${readableError}`});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {debuglog} from "util";
|
|
2
|
+
import {IExecuteFunctions, INodeExecutionData, NodeOperationError} from "n8n-workflow";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import {SmbClientWrapper} from "./SmbClientWrapper";
|
|
7
|
+
import {Operation, OpHandler, Smb2Credentials, SmbListEntry} from "./interfaces";
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
|
|
10
|
+
const debug = debuglog("n8n-nodes-smbclient");
|
|
11
|
+
const str = (ctx: IExecuteFunctions, i: number, name: string, def = ''): string =>
|
|
12
|
+
ctx.getNodeParameter(name, i, def) as string;
|
|
13
|
+
|
|
14
|
+
const tmpFile = (prefix: 'get' | 'put') =>
|
|
15
|
+
path.join(os.tmpdir(), `n8n-smb-${prefix}-${Date.now()}-${crypto.randomUUID()}`);
|
|
16
|
+
|
|
17
|
+
const toBinary = async (
|
|
18
|
+
ctx: IExecuteFunctions,
|
|
19
|
+
buf: Buffer,
|
|
20
|
+
fileName: string,
|
|
21
|
+
mime: string,
|
|
22
|
+
outProp: string,
|
|
23
|
+
extraJson: Record<string, unknown> = {},
|
|
24
|
+
): Promise<INodeExecutionData> => {
|
|
25
|
+
const binary = await ctx.helpers.prepareBinaryData(buf, fileName, mime);
|
|
26
|
+
return {json: {fileName, ...extraJson}, binary: {[outProp]: binary}};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const buildClient = async (ctx: IExecuteFunctions): Promise<SmbClientWrapper> => {
|
|
30
|
+
const smbclientPath = str(ctx, 0, 'smbclientPath', 'smbclient');
|
|
31
|
+
const {host, username, password, domain, share} = (await ctx.getCredentials('smb2Api')) as unknown as Smb2Credentials;
|
|
32
|
+
debug(
|
|
33
|
+
"Connecting to //%s/%s as (%s\\%s)",
|
|
34
|
+
host,
|
|
35
|
+
share,
|
|
36
|
+
domain ?? "",
|
|
37
|
+
username,
|
|
38
|
+
);
|
|
39
|
+
return new SmbClientWrapper({host, username, password, domain, share}, smbclientPath, ctx.getNode());
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/* ---------- Handlers (take ctx instead of this) ---------- */
|
|
43
|
+
const handleStat: OpHandler = async (ctx, i, client) => {
|
|
44
|
+
const remotePath = str(ctx, i, 'remotePath');
|
|
45
|
+
const stat = await client.stat(remotePath);
|
|
46
|
+
return {json: {remotePath, ...stat}};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleList: OpHandler = async (ctx, i, client) => {
|
|
50
|
+
const directory = str(ctx, i, 'directory', '/');
|
|
51
|
+
const entries = await client.list(directory);
|
|
52
|
+
const filtered = entries.filter(
|
|
53
|
+
(e: SmbListEntry) =>
|
|
54
|
+
e &&
|
|
55
|
+
e.name !== '.' &&
|
|
56
|
+
e.name !== '..' &&
|
|
57
|
+
!/blocks available/i.test(e?.date || '') &&
|
|
58
|
+
!/^\d+$/.test(e.name),
|
|
59
|
+
);
|
|
60
|
+
return {json: {directory, entries: filtered}};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleGet: OpHandler = async (ctx, i, client) => {
|
|
64
|
+
const remotePath = str(ctx, i, 'remotePath');
|
|
65
|
+
const outProp = str(ctx, i, 'outBinaryPropertyName', 'data');
|
|
66
|
+
const outFile = str(ctx, i, 'outFileName') || path.basename(remotePath);
|
|
67
|
+
const outMime = str(ctx, i, 'outMimeType', 'application/octet-stream');
|
|
68
|
+
|
|
69
|
+
const tmp = tmpFile('get');
|
|
70
|
+
await client.get(remotePath, tmp);
|
|
71
|
+
const buf = await fs.readFile(tmp);
|
|
72
|
+
await fs.unlink(tmp).catch(() => {
|
|
73
|
+
});
|
|
74
|
+
return toBinary(ctx, buf, outFile, outMime, outProp, {remotePath});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handlePut: OpHandler = async (ctx, i, client) => {
|
|
78
|
+
const remotePath = str(ctx, i, 'remotePath');
|
|
79
|
+
const source = str(ctx, i, 'putSource', 'binary'); // 'binary' | 'text'
|
|
80
|
+
|
|
81
|
+
const tmp = tmpFile('put');
|
|
82
|
+
if (source === 'binary') {
|
|
83
|
+
const binProp = str(ctx, i, 'binaryPropertyName', 'data');
|
|
84
|
+
const item = ctx.getInputData()[i];
|
|
85
|
+
if (!item.binary || !item.binary[binProp]) {
|
|
86
|
+
throw new NodeOperationError(ctx.getNode(), `Binary property "${binProp}" not found on item ${i}`);
|
|
87
|
+
}
|
|
88
|
+
const buffer = await ctx.helpers.getBinaryDataBuffer(i, binProp);
|
|
89
|
+
await fs.writeFile(tmp, buffer);
|
|
90
|
+
} else {
|
|
91
|
+
const text = str(ctx, i, 'textContent', '');
|
|
92
|
+
await fs.writeFile(tmp, text, 'utf8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await client.put(tmp, remotePath);
|
|
96
|
+
await fs.unlink(tmp).catch(() => {
|
|
97
|
+
});
|
|
98
|
+
return {json: {remotePath, uploaded: true}};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleMkdir: OpHandler = async (ctx, i, client) => {
|
|
102
|
+
const directory = str(ctx, i, 'directory');
|
|
103
|
+
await client.mkdir(directory);
|
|
104
|
+
return {json: {directory, created: true}};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleRmdir: OpHandler = async (ctx, i, client) => {
|
|
108
|
+
const directory = str(ctx, i, 'directory');
|
|
109
|
+
await client.rmdir(directory);
|
|
110
|
+
return {json: {directory, removed: true}};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDel: OpHandler = async (ctx, i, client) => {
|
|
114
|
+
const remotePath = str(ctx, i, 'remotePath');
|
|
115
|
+
await client.del(remotePath);
|
|
116
|
+
return {json: {remotePath, deleted: true}};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handlers: Record<Operation, OpHandler> = {
|
|
120
|
+
stat: handleStat,
|
|
121
|
+
list: handleList,
|
|
122
|
+
get: handleGet,
|
|
123
|
+
put: handlePut,
|
|
124
|
+
mkdir: handleMkdir,
|
|
125
|
+
rmdir: handleRmdir,
|
|
126
|
+
del: handleDel,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export {handlers, buildClient}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {IExecuteFunctions, INodeExecutionData} from "n8n-workflow";
|
|
2
|
+
import {SmbClientWrapper} from "./SmbClientWrapper";
|
|
3
|
+
|
|
4
|
+
interface Smb2Credentials {
|
|
5
|
+
host: string;
|
|
6
|
+
share: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
port?: number; // samba-client may not support explicit port
|
|
11
|
+
maxProtocol?: string; // passed through
|
|
12
|
+
// timeout options (if supported)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SmbListEntry = {
|
|
16
|
+
name: string;
|
|
17
|
+
size: number;
|
|
18
|
+
date?: string;
|
|
19
|
+
time?: string;
|
|
20
|
+
attributes: string[];
|
|
21
|
+
isDirectory: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SmbStat = {
|
|
25
|
+
size?: number;
|
|
26
|
+
createTime?: string;
|
|
27
|
+
accessTime?: string;
|
|
28
|
+
writeTime?: string;
|
|
29
|
+
changeTime?: string;
|
|
30
|
+
attributes?: string[];
|
|
31
|
+
isDirectory?: boolean;
|
|
32
|
+
};
|
|
33
|
+
type Operation = 'stat' | 'list' | 'get' | 'put' | 'mkdir' | 'rmdir' | 'del';
|
|
34
|
+
type OpHandler = (ctx: IExecuteFunctions, i: number, client: SmbClientWrapper) => Promise<INodeExecutionData>;
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
SmbStat, SmbListEntry, Smb2Credentials, Operation
|
|
38
|
+
, OpHandler
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512">
|
|
3
|
+
<!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
|
|
4
|
+
<defs>
|
|
5
|
+
<style>
|
|
6
|
+
.st0 {
|
|
7
|
+
fill: url(#Dégradé_sans_nom1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.st1 {
|
|
11
|
+
fill: url(#Dégradé_sans_nom2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.st2 {
|
|
15
|
+
fill: url(#Dégradé_sans_nom);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.st3 {
|
|
19
|
+
fill: url(#Dégradé_sans_nom3);
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
<linearGradient id="Dégradé_sans_nom" data-name="Dégradé sans nom" x1="-1301.41" y1="140.82" x2="-1300.41" y2="141.82" gradientTransform="translate(174550 -18839.5) scale(134)" gradientUnits="userSpaceOnUse">
|
|
23
|
+
<stop offset="0" stop-color="#4c9ded"/>
|
|
24
|
+
<stop offset="1" stop-color="#3178b5"/>
|
|
25
|
+
</linearGradient>
|
|
26
|
+
<linearGradient id="Dégradé_sans_nom1" data-name="Dégradé sans nom" x1="-1305.78" y1="138.8" x2="-1304.78" y2="139.8" gradientTransform="translate(418568.08 -11797.5) scale(320.43 87)" xlink:href="#Dégradé_sans_nom"/>
|
|
27
|
+
<linearGradient id="Dégradé_sans_nom2" data-name="Dégradé sans nom" x1="214.73" y1="-57.27" x2="452.27" y2="180.27" gradientTransform="translate(0 514) scale(1 -1)" xlink:href="#Dégradé_sans_nom"/>
|
|
28
|
+
<linearGradient id="Dégradé_sans_nom3" data-name="Dégradé sans nom" x1="0" y1="217" x2="325" y2="217" gradientTransform="matrix(1,0,0,1,0,0)" xlink:href="#Dégradé_sans_nom"/>
|
|
29
|
+
</defs>
|
|
30
|
+
<g id="Calque_1-2">
|
|
31
|
+
<path class="st2" d="M325,134v-6.6c0-3.18-1.26-6.23-3.5-8.48L206.37,3.52c-2.25-2.26-5.31-3.52-8.5-3.52h-6.87v134h134Z"/>
|
|
32
|
+
<path class="st0" d="M228.3,274c-5.89,0-11.3,3.23-14.09,8.42l-42.21,78.58h320.38l-48-79.28c-2.89-4.78-8.07-7.71-13.66-7.72h-202.42Z"/>
|
|
33
|
+
<path class="st1" d="M280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM281.09,448.82c1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.57-2.81-1.78-3.58-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.57,0,4.35-.42,5.34-1.27h.03ZM496,393H171c-8.84,0-16,7.16-16,16v87c0,8.84,7.16,16,16,16h325c8.84,0,16-7.16,16-16v-87c0-8.84-7.16-16-16-16ZM211.39,466.38c-1.07,1.55-2.58,2.74-4.33,3.43-2.14.84-4.42,1.24-6.72,1.19-1.22,0-2.43-.07-3.64-.21-1.1-.14-2.19-.35-3.27-.63-.97-.25-1.91-.59-2.82-1v-5.37c1.55.66,3.14,1.22,4.76,1.67,1.73.5,3.53.75,5.33.75,1.25.04,2.49-.16,3.66-.58.88-.32,1.64-.9,2.18-1.67.49-.75.74-1.63.72-2.53.04-.94-.26-1.86-.83-2.6-.69-.83-1.55-1.51-2.51-2-1.37-.71-2.78-1.36-4.22-1.92-1.12-.44-2.21-.96-3.26-1.55-1-.57-1.93-1.27-2.76-2.07-.82-.81-1.48-1.77-1.93-2.83-.5-1.21-.74-2.52-.71-3.83-.05-1.79.44-3.56,1.42-5.06,1-1.44,2.38-2.55,4-3.22,1.9-.78,3.94-1.16,6-1.12,1.74-.01,3.47.18,5.16.58,1.62.4,3.21.94,4.74,1.61l-1.77,4.58c-1.36-.56-2.75-1.03-4.16-1.41-1.33-.36-2.71-.54-4.09-.54-1.05-.03-2.1.16-3.07.55-2.21,1-3.18,3.61-2.18,5.81.1.22.22.43.36.64.63.79,1.42,1.43,2.33,1.88,1.06.57,2.42,1.2,4.08,1.9,1.7.68,3.34,1.52,4.89,2.5,1.26.78,2.32,1.86,3.08,3.13.76,1.39,1.13,2.96,1.07,4.55.06,1.9-.47,3.78-1.51,5.37ZM255.39,470.52h-5.44v-22.8c0-1.23.08-2.4.13-3.51s.11-2,.16-2.67h-.24l-10.37,29h-5l-10-29h-.21c0,.65.1,1.53.16,2.65s.12,2.32.17,3.62.07,2.51.07,3.67v19.08h-5.19v-34.82h8l9.64,27.77h.14l10-27.77h8l-.02,34.78ZM287.94,466c-1.07,1.53-2.57,2.72-4.31,3.4-2.11.82-4.36,1.22-6.63,1.16h-12.49v-34.82h10.35c4.49,0,7.88.65,10.16,1.95,2.28,1.3,3.42,3.55,3.43,6.76.01,1.23-.22,2.45-.7,3.59-.44,1.03-1.12,1.93-2,2.63-.97.72-2.09,1.2-3.28,1.39v.24c1.26.21,2.48.64,3.6,1.26,1.06.61,1.93,1.51,2.5,2.59.66,1.36.98,2.87.92,4.38.05,1.94-.49,3.85-1.55,5.47ZM317.81,470.56h-23.58v-4.21l9-9.13c1.74-1.78,3.18-3.31,4.3-4.61,1-1.11,1.85-2.34,2.53-3.67.56-1.19.84-2.48.83-3.79.1-1.41-.43-2.8-1.45-3.79-1.07-.9-2.43-1.37-3.83-1.3-1.44-.01-2.87.29-4.18.89-1.43.68-2.77,1.53-4,2.54l-3.07-3.71c.97-.82,2-1.57,3.08-2.24,1.16-.71,2.4-1.26,3.7-1.64,1.52-.44,3.1-.65,4.69-.62,1.97-.05,3.92.35,5.71,1.18,1.52.72,2.81,1.85,3.7,3.28.88,1.47,1.33,3.16,1.29,4.87.02,1.79-.36,3.56-1.11,5.19-.83,1.71-1.91,3.29-3.19,4.7-1.38,1.56-3,3.24-4.93,5.05l-6,5.85v.26h16.47l.04,4.9ZM372.74,476.78c-13.25,0-24-10.75-24-24s10.75-24,24-24,24,10.75,24,24c-.02,13.24-10.76,23.96-24,23.96v.04ZM444.74,476.78c-13.25,0-24-10.75-24-24s10.75-24,24-24,24,10.75,24,24c-.02,13.24-10.76,23.96-24,23.96v.04ZM280.38,455.37c-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.22-1.96-.71-2.81-.56-.85-1.38-1.49-2.34-1.82ZM281.06,448.82c1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.57-2.81-1.78-3.58-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58h0ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58h0ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58h0ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM282.77,457.2c-.56-.86-1.39-1.5-2.36-1.83-1.42-.48-2.92-.7-4.42-.64h-5.74v11h6c2.67,0,4.53-.51,5.59-1.54,1.11-1.11,1.7-2.63,1.62-4.19.02-.98-.23-1.95-.72-2.8h.03ZM280.77,441.58c-1.74-.84-3.67-1.21-5.6-1.08h-4.95v9.59h5.47c2.59,0,4.38-.42,5.37-1.27,1.03-.93,1.58-2.28,1.49-3.66.12-1.43-.56-2.81-1.78-3.58h0Z"/>
|
|
34
|
+
<path class="st3" d="M325,166v76h-96.44c-17.67,0-33.91,9.7-42.28,25.26l-57.08,106.15c-4.06,7.56-6.19,16.01-6.2,24.59v36H12c-6.63,0-12-5.37-12-12V12C0,5.37,5.37,0,12,0h147v154c0,6.63,5.37,12,12,12h154Z"/>
|
|
35
|
+
</g>
|
|
36
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-smbclient",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Transfer files via Samba (SMB2) and smbclient package with n8n, some of the base work are based on https://github.com/drudge/n8n-nodes-smb work.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n-nodes-smbclient",
|
|
8
|
+
"smb",
|
|
9
|
+
"smb",
|
|
10
|
+
"samba",
|
|
11
|
+
"binary",
|
|
12
|
+
"file",
|
|
13
|
+
"transfer",
|
|
14
|
+
"node",
|
|
15
|
+
"n8n"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20.15"
|
|
19
|
+
},
|
|
20
|
+
"repository": "https://github.com/JimmyMtl/n8n-nodes-smbclient",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "npx rimraf dist && tsc && gulp build:icons",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"format": "prettier nodes credentials --write",
|
|
26
|
+
"lint": "eslint nodes credentials package.json",
|
|
27
|
+
"lintfix": "eslint nodes credentials package.json --fix",
|
|
28
|
+
"prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes credentials package.json",
|
|
29
|
+
"release": "npx standard-version"
|
|
30
|
+
},
|
|
31
|
+
"n8n": {
|
|
32
|
+
"n8nNodesApiVersion": 1,
|
|
33
|
+
"credentials": [
|
|
34
|
+
"dist/credentials/Smb2Api.credentials.js"
|
|
35
|
+
],
|
|
36
|
+
"nodes": [
|
|
37
|
+
"dist/nodes/Smb2/Smb2.node.js"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
42
|
+
"@eslint/js": "^9.35.0",
|
|
43
|
+
"@types/node": "^24.9.2",
|
|
44
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
45
|
+
"eslint": "^9.38.0",
|
|
46
|
+
"eslint-plugin-n8n-nodes-base": "^1.16.4",
|
|
47
|
+
"globals": "^16.4.0",
|
|
48
|
+
"gulp": "^5.0.1",
|
|
49
|
+
"prettier": "^3.6.2",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"n8n-workflow": "^1.115.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"tmp-promise": "^3.0.3"
|
|
57
|
+
},
|
|
58
|
+
"overrides": {
|
|
59
|
+
"form-data": "^4.0.4",
|
|
60
|
+
"axios": "1.12.0"
|
|
61
|
+
},
|
|
62
|
+
"pnpm": {
|
|
63
|
+
"onlyBuiltDependencies": [
|
|
64
|
+
"eslint-plugin-n8n-nodes-base"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"target": "es2019",
|
|
7
|
+
"lib": ["es2019", "es2020", "es2022.error"],
|
|
8
|
+
"removeComments": true,
|
|
9
|
+
"useUnknownInCatchVariables": false,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"strictNullChecks": true,
|
|
15
|
+
"preserveConstEnums": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"incremental": true,
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
"skipLibCheck": true,
|
|
22
|
+
"outDir": "./dist/",
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"credentials/**/*",
|
|
26
|
+
"nodes/**/*",
|
|
27
|
+
"nodes/**/*.json",
|
|
28
|
+
"package.json",
|
|
29
|
+
],
|
|
30
|
+
}
|