n8n-nodes-antony-cloud-office 0.1.3

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,1048 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NextcloudNode = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const fast_xml_parser_1 = require("fast-xml-parser");
6
+ // ──────────────────────────────────────────────────────────── helpers ──
7
+ function davPath(username, path) {
8
+ const parts = (path.startsWith('/') ? path.slice(1) : path)
9
+ .split('/')
10
+ .filter(Boolean)
11
+ .map(encodeURIComponent);
12
+ return `/remote.php/dav/files/${encodeURIComponent(username)}/${parts.join('/')}`;
13
+ }
14
+ function extractFileIdPath(path) {
15
+ const trimmed = path.trim();
16
+ const match = trimmed.match(/(?:^|\/)(?:nc\/)?f\/(\d+)(?:$|[/?#])/);
17
+ return match?.[1];
18
+ }
19
+ function pathFromDavHref(href) {
20
+ try {
21
+ return decodeURIComponent(new URL(href, 'https://nextcloud.local').pathname);
22
+ }
23
+ catch {
24
+ return decodeURIComponent(href);
25
+ }
26
+ }
27
+ function xmlEscape(value) {
28
+ return value
29
+ .replace(/&/g, '&')
30
+ .replace(/</g, '&lt;')
31
+ .replace(/>/g, '&gt;')
32
+ .replace(/"/g, '&quot;')
33
+ .replace(/'/g, '&apos;');
34
+ }
35
+ function getEnvironmentBaseUrl(baseUrl) {
36
+ return baseUrl
37
+ .trim()
38
+ .replace(/\/+$/, '')
39
+ .replace(/\/(?:nc|kc)$/i, '');
40
+ }
41
+ function getNextcloudBaseUrl(baseUrl) {
42
+ return `${getEnvironmentBaseUrl(baseUrl)}/nc`;
43
+ }
44
+ function getKeycloakBaseUrl(baseUrl) {
45
+ return `${getEnvironmentBaseUrl(baseUrl)}/kc`;
46
+ }
47
+ function isOcsUrl(url) {
48
+ return !!url && /(?:^|\/)ocs\/v[12]\.php\//.test(url);
49
+ }
50
+ function getHttpStatusCode(error) {
51
+ const response = error?.response;
52
+ const statusCode = error?.statusCode ?? error?.httpCode ?? response?.statusCode ?? response?.status;
53
+ return typeof statusCode === 'number' ? statusCode : undefined;
54
+ }
55
+ const xmlParser = new fast_xml_parser_1.XMLParser({
56
+ ignoreAttributes: false,
57
+ removeNSPrefix: true,
58
+ isArray: (name) => ['response', 'propstat'].includes(name),
59
+ });
60
+ function parseWebDavItems(xml, username) {
61
+ const parsed = xmlParser.parse(xml);
62
+ const multistatus = parsed.multistatus;
63
+ if (!multistatus)
64
+ return [];
65
+ const responses = multistatus.response || [];
66
+ const prefix = `/remote.php/dav/files/${username}`;
67
+ return responses.map((item) => {
68
+ const href = pathFromDavHref(String(item.href || ''));
69
+ const propstats = item.propstat || [];
70
+ const okPs = propstats.find((ps) => String(ps.status || '').includes('200'));
71
+ const props = okPs?.prop || {};
72
+ const resourcetype = props.resourcetype;
73
+ const isDirectory = !!(resourcetype && 'collection' in resourcetype);
74
+ const prefixIndex = href.indexOf(prefix);
75
+ const rawRelativePath = prefixIndex >= 0 ? href.slice(prefixIndex + prefix.length) : href;
76
+ const relativePath = rawRelativePath.replace(/\/$/, '') || '/';
77
+ return {
78
+ name: decodeURIComponent(href.split('/').filter(Boolean).pop() || ''),
79
+ path: relativePath.startsWith('/') ? relativePath : `/${relativePath}`,
80
+ isDirectory,
81
+ lastModified: String(props.getlastmodified || ''),
82
+ size: Number(props.size ?? props.getcontentlength ?? 0),
83
+ contentType: isDirectory ? '' : String(props.getcontenttype || ''),
84
+ fileId: String(props.fileid || ''),
85
+ permissions: String(props.permissions || ''),
86
+ etag: String(props.getetag || '').replace(/"/g, ''),
87
+ };
88
+ });
89
+ }
90
+ function extractOcsData(response) {
91
+ if (typeof response === 'string') {
92
+ throw new Error('OCS API returned a non-JSON response. Check authentication, the Nextcloud URL, and Bearer-token API access.');
93
+ }
94
+ if (!response || typeof response !== 'object' || Array.isArray(response)) {
95
+ throw new Error(`OCS API returned unexpected type: ${typeof response}`);
96
+ }
97
+ const obj = response;
98
+ const ocs = obj.ocs;
99
+ if (!ocs) {
100
+ const keys = Object.keys(obj).join(', ') || '(empty object)';
101
+ throw new Error(`OCS API response missing ".ocs" field (response keys: ${keys}). ` +
102
+ `Verify: Nextcloud base URL in the credential, Bearer-token auth is accepted by Nextcloud ` +
103
+ `(user_oidc app / token introspection), and the files_sharing app is enabled.`);
104
+ }
105
+ const meta = ocs.meta;
106
+ const statusCode = Number(meta?.statuscode ?? 200);
107
+ if (statusCode >= 400) {
108
+ throw new Error(`OCS error ${statusCode}: ${String(meta?.message ?? 'Unknown error')}`);
109
+ }
110
+ return ocs.data ?? {};
111
+ }
112
+ function getOcsUserId(response, fallback) {
113
+ const data = extractOcsData(response);
114
+ if (!Array.isArray(data) && data.id) {
115
+ return String(data.id);
116
+ }
117
+ return fallback;
118
+ }
119
+ function toDataObjectArray(value) {
120
+ if (!Array.isArray(value))
121
+ return [];
122
+ return value.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item));
123
+ }
124
+ function normalizeLookupValue(value) {
125
+ return String(value ?? '').trim().toLowerCase();
126
+ }
127
+ function shareeCategoryForType(shareType) {
128
+ if (shareType === 0)
129
+ return 'users';
130
+ if (shareType === 1)
131
+ return 'groups';
132
+ if (shareType === 4)
133
+ return 'emails';
134
+ return undefined;
135
+ }
136
+ function getShareeValue(sharee, key) {
137
+ const value = sharee.value;
138
+ const candidate = value?.[key] ?? sharee[key];
139
+ return String(candidate ?? '');
140
+ }
141
+ function getShareeId(sharee) {
142
+ return (getShareeValue(sharee, 'shareWith') ||
143
+ getShareeValue(sharee, 'shareWithId') ||
144
+ getShareeValue(sharee, 'id') ||
145
+ String(sharee.id ?? ''));
146
+ }
147
+ function shareeMatchesInput(sharee, input) {
148
+ const needle = normalizeLookupValue(input);
149
+ const value = sharee.value;
150
+ const candidates = [
151
+ sharee.label,
152
+ sharee.name,
153
+ sharee.id,
154
+ sharee.shareWith,
155
+ value?.shareWith,
156
+ value?.shareWithDisplayName,
157
+ value?.displayName,
158
+ value?.name,
159
+ value?.id,
160
+ ];
161
+ return candidates.some((candidate) => normalizeLookupValue(candidate) === needle);
162
+ }
163
+ function getShareeCandidates(data, category) {
164
+ const exact = data.exact;
165
+ return [
166
+ ...toDataObjectArray(exact?.[category]),
167
+ ...toDataObjectArray(data[category]),
168
+ ];
169
+ }
170
+ const PROPFIND_BODY = `<?xml version="1.0"?>
171
+ <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
172
+ <d:prop>
173
+ <d:resourcetype/><d:getlastmodified/><d:getcontentlength/>
174
+ <d:getcontenttype/><d:getetag/>
175
+ <oc:fileid/><oc:size/><oc:permissions/>
176
+ </d:prop>
177
+ </d:propfind>`;
178
+ function searchByFileIdBody(username, fileId) {
179
+ const escapedUsername = xmlEscape(username);
180
+ return `<?xml version="1.0"?>
181
+ <d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
182
+ <d:basicsearch>
183
+ <d:select>
184
+ <d:prop>
185
+ <d:resourcetype/><d:getlastmodified/><d:getcontentlength/>
186
+ <d:getcontenttype/><d:getetag/>
187
+ <oc:fileid/><oc:size/><oc:permissions/>
188
+ </d:prop>
189
+ </d:select>
190
+ <d:from>
191
+ <d:scope>
192
+ <d:href>/files/${escapedUsername}</d:href>
193
+ <d:depth>infinity</d:depth>
194
+ </d:scope>
195
+ </d:from>
196
+ <d:where>
197
+ <d:eq>
198
+ <d:prop><oc:fileid/></d:prop>
199
+ <d:literal>${fileId}</d:literal>
200
+ </d:eq>
201
+ </d:where>
202
+ <d:orderby/>
203
+ </d:basicsearch>
204
+ </d:searchrequest>`;
205
+ }
206
+ // ──────────────────────────────────────────────────────────── node ──
207
+ class NextcloudNode {
208
+ constructor() {
209
+ this.description = {
210
+ displayName: 'antony Nextcloud',
211
+ name: 'nextcloudNode',
212
+ icon: 'file:antony.svg',
213
+ group: ['transform'],
214
+ version: 1,
215
+ subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
216
+ description: 'Manage files, folders, shares, versions and search in Nextcloud',
217
+ defaults: { name: 'antony Nextcloud' },
218
+ inputs: ['main'],
219
+ outputs: ['main'],
220
+ credentials: [{ name: 'nextcloudApi', required: true, testedBy: 'testNextcloudCredentials' }],
221
+ properties: [
222
+ // ── Resource selector ──────────────────────────────────────────
223
+ {
224
+ displayName: 'Resource',
225
+ name: 'resource',
226
+ type: 'options',
227
+ noDataExpression: true,
228
+ options: [
229
+ { name: 'File', value: 'file' },
230
+ { name: 'Folder', value: 'folder' },
231
+ { name: 'Search', value: 'search' },
232
+ { name: 'Share', value: 'share' },
233
+ { name: 'Version', value: 'version' },
234
+ ],
235
+ default: 'file',
236
+ },
237
+ // ── File: operation selector ───────────────────────────────────
238
+ {
239
+ displayName: 'Operation',
240
+ name: 'operation',
241
+ type: 'options',
242
+ noDataExpression: true,
243
+ displayOptions: { show: { resource: ['file'] } },
244
+ options: [
245
+ { name: 'Copy', value: 'copy', action: 'Copy a file' },
246
+ { name: 'Delete', value: 'delete', action: 'Delete a file' },
247
+ { name: 'Download', value: 'download', action: 'Download a file' },
248
+ { name: 'Get Info', value: 'info', action: 'Get file metadata' },
249
+ { name: 'Move', value: 'move', action: 'Move or rename a file' },
250
+ { name: 'Upload', value: 'upload', action: 'Upload a file' },
251
+ ],
252
+ default: 'upload',
253
+ },
254
+ // ── Folder: operation selector ─────────────────────────────────
255
+ {
256
+ displayName: 'Operation',
257
+ name: 'operation',
258
+ type: 'options',
259
+ noDataExpression: true,
260
+ displayOptions: { show: { resource: ['folder'] } },
261
+ options: [
262
+ { name: 'Copy', value: 'copy', action: 'Copy a folder' },
263
+ { name: 'Create', value: 'create', action: 'Create a folder' },
264
+ { name: 'Delete', value: 'delete', action: 'Delete a folder' },
265
+ { name: 'Get Info', value: 'info', action: 'Get folder metadata' },
266
+ { name: 'List Contents', value: 'list', action: 'List contents of a folder' },
267
+ { name: 'Move', value: 'move', action: 'Move or rename a folder' },
268
+ ],
269
+ default: 'list',
270
+ },
271
+ // ── Share: operation selector ──────────────────────────────────
272
+ {
273
+ displayName: 'Operation',
274
+ name: 'operation',
275
+ type: 'options',
276
+ noDataExpression: true,
277
+ displayOptions: { show: { resource: ['share'] } },
278
+ options: [
279
+ { name: 'Create', value: 'create', action: 'Create a share' },
280
+ { name: 'Delete', value: 'delete', action: 'Delete a share' },
281
+ { name: 'Get', value: 'get', action: 'Get share details' },
282
+ { name: 'List', value: 'list', action: 'List shares (optionally for a path)' },
283
+ { name: 'Update', value: 'update', action: 'Update share settings' },
284
+ ],
285
+ default: 'list',
286
+ },
287
+ // ── Version: operation selector ────────────────────────────────
288
+ {
289
+ displayName: 'Operation',
290
+ name: 'operation',
291
+ type: 'options',
292
+ noDataExpression: true,
293
+ displayOptions: { show: { resource: ['version'] } },
294
+ options: [
295
+ { name: 'List', value: 'list', action: 'List versions of a file' },
296
+ { name: 'Restore', value: 'restore', action: 'Restore a file version' },
297
+ ],
298
+ default: 'list',
299
+ },
300
+ // ── Search: operation selector ─────────────────────────────────
301
+ {
302
+ displayName: 'Operation',
303
+ name: 'operation',
304
+ type: 'options',
305
+ noDataExpression: true,
306
+ displayOptions: { show: { resource: ['search'] } },
307
+ options: [
308
+ { name: 'Search Files', value: 'search', action: 'Search for files and folders' },
309
+ ],
310
+ default: 'search',
311
+ },
312
+ // ── Shared: path (single-path operations) ──────────────────────
313
+ {
314
+ displayName: 'Path',
315
+ name: 'path',
316
+ type: 'string',
317
+ default: '/',
318
+ placeholder: '/Documents/file.txt',
319
+ description: 'Path to the file or folder in Nextcloud',
320
+ displayOptions: {
321
+ show: {
322
+ resource: ['file', 'folder'],
323
+ operation: ['list', 'create', 'delete', 'download', 'info'],
324
+ },
325
+ },
326
+ },
327
+ // ── Shared: move / copy ────────────────────────────────────────
328
+ {
329
+ displayName: 'Source Path',
330
+ name: 'sourcePath',
331
+ type: 'string',
332
+ default: '',
333
+ placeholder: '/Documents/old-name.txt',
334
+ displayOptions: {
335
+ show: {
336
+ resource: ['file', 'folder'],
337
+ operation: ['move', 'copy'],
338
+ },
339
+ },
340
+ },
341
+ {
342
+ displayName: 'Destination Path',
343
+ name: 'destinationPath',
344
+ type: 'string',
345
+ default: '',
346
+ placeholder: '/Documents/new-name.txt',
347
+ displayOptions: {
348
+ show: {
349
+ resource: ['file', 'folder'],
350
+ operation: ['move', 'copy'],
351
+ },
352
+ },
353
+ },
354
+ {
355
+ displayName: 'Overwrite',
356
+ name: 'overwrite',
357
+ type: 'boolean',
358
+ default: false,
359
+ displayOptions: {
360
+ show: {
361
+ resource: ['file', 'folder'],
362
+ operation: ['move', 'copy'],
363
+ },
364
+ },
365
+ },
366
+ // ── Folder: list options ───────────────────────────────────────
367
+ {
368
+ displayName: 'List Subdirectories Recursively',
369
+ name: 'recursive',
370
+ type: 'boolean',
371
+ default: false,
372
+ displayOptions: { show: { resource: ['folder'], operation: ['list'] } },
373
+ },
374
+ // ── File: upload ───────────────────────────────────────────────
375
+ {
376
+ displayName: 'Remote Path',
377
+ name: 'remotePath',
378
+ type: 'string',
379
+ default: '',
380
+ placeholder: '/Documents/upload.pdf',
381
+ description: 'Target path in Nextcloud including the file name',
382
+ displayOptions: { show: { resource: ['file'], operation: ['upload'] } },
383
+ },
384
+ {
385
+ displayName: 'Input Binary Property',
386
+ name: 'binaryPropertyName',
387
+ type: 'string',
388
+ default: 'data',
389
+ description: 'Name of the binary property containing the file to upload',
390
+ displayOptions: { show: { resource: ['file'], operation: ['upload'] } },
391
+ },
392
+ // ── File: download ─────────────────────────────────────────────
393
+ {
394
+ displayName: 'Output Binary Property',
395
+ name: 'outputBinaryProperty',
396
+ type: 'string',
397
+ default: 'data',
398
+ description: 'Name of the binary property to write the downloaded file into',
399
+ displayOptions: { show: { resource: ['file'], operation: ['download'] } },
400
+ },
401
+ // ── Share: list ────────────────────────────────────────────────
402
+ {
403
+ displayName: 'Filter by Path',
404
+ name: 'sharePath',
405
+ type: 'string',
406
+ default: '',
407
+ placeholder: '/Documents/report.pdf',
408
+ description: 'Leave empty to list all your shares',
409
+ displayOptions: { show: { resource: ['share'], operation: ['list'] } },
410
+ },
411
+ {
412
+ displayName: 'Include Sub-file Shares',
413
+ name: 'subfiles',
414
+ type: 'boolean',
415
+ default: false,
416
+ description: 'Whether to list shares for all files inside a shared folder',
417
+ displayOptions: { show: { resource: ['share'], operation: ['list'] } },
418
+ },
419
+ // ── Share: get / delete ────────────────────────────────────────
420
+ {
421
+ displayName: 'Share ID',
422
+ name: 'shareId',
423
+ type: 'number',
424
+ default: 0,
425
+ displayOptions: { show: { resource: ['share'], operation: ['get', 'delete'] } },
426
+ },
427
+ // ── Share: create ──────────────────────────────────────────────
428
+ {
429
+ displayName: 'Path',
430
+ name: 'shareSrcPath',
431
+ type: 'string',
432
+ default: '',
433
+ placeholder: '/Documents/report.pdf',
434
+ description: 'Path to the file or folder to share',
435
+ displayOptions: { show: { resource: ['share'], operation: ['create'] } },
436
+ },
437
+ {
438
+ displayName: 'Share Type',
439
+ name: 'shareType',
440
+ type: 'options',
441
+ options: [
442
+ { name: 'Public Link', value: 3 },
443
+ { name: 'User (internal)', value: 0 },
444
+ { name: 'Group', value: 1 },
445
+ { name: 'Email (link + notification)', value: 4 },
446
+ ],
447
+ default: 3,
448
+ displayOptions: { show: { resource: ['share'], operation: ['create'] } },
449
+ },
450
+ {
451
+ displayName: 'Share With',
452
+ name: 'shareWith',
453
+ type: 'string',
454
+ default: '',
455
+ placeholder: 'username, group name, or email@example.com',
456
+ displayOptions: {
457
+ show: { resource: ['share'], operation: ['create'], shareType: [0, 1, 4] },
458
+ },
459
+ },
460
+ {
461
+ displayName: 'Permissions',
462
+ name: 'permissions',
463
+ type: 'options',
464
+ options: [
465
+ { name: 'Read Only', value: 1 },
466
+ { name: 'Read + Edit', value: 7 },
467
+ { name: 'Read + Edit + Share', value: 23 },
468
+ { name: 'All', value: 31 },
469
+ ],
470
+ default: 1,
471
+ displayOptions: {
472
+ show: { resource: ['share'], operation: ['create'], shareType: [0, 1, 3, 4] },
473
+ },
474
+ },
475
+ {
476
+ displayName: 'Additional Options',
477
+ name: 'shareOptions',
478
+ type: 'collection',
479
+ placeholder: 'Add Option',
480
+ default: {},
481
+ displayOptions: { show: { resource: ['share'], operation: ['create'] } },
482
+ options: [
483
+ {
484
+ displayName: 'Password',
485
+ name: 'password',
486
+ type: 'string',
487
+ typeOptions: { password: true },
488
+ default: '',
489
+ },
490
+ {
491
+ displayName: 'Expiry Date',
492
+ name: 'expireDate',
493
+ type: 'string',
494
+ default: '',
495
+ placeholder: 'YYYY-MM-DD',
496
+ },
497
+ {
498
+ displayName: 'Note for Recipient',
499
+ name: 'note',
500
+ type: 'string',
501
+ default: '',
502
+ },
503
+ {
504
+ displayName: 'Label (Public Link)',
505
+ name: 'label',
506
+ type: 'string',
507
+ default: '',
508
+ },
509
+ {
510
+ displayName: 'Allow Upload (Public Link on Folder)',
511
+ name: 'publicUpload',
512
+ type: 'boolean',
513
+ default: false,
514
+ },
515
+ ],
516
+ },
517
+ // ── Share: update ──────────────────────────────────────────────
518
+ {
519
+ displayName: 'Share ID',
520
+ name: 'shareId',
521
+ type: 'number',
522
+ default: 0,
523
+ displayOptions: { show: { resource: ['share'], operation: ['update'] } },
524
+ },
525
+ {
526
+ displayName: 'Update Fields',
527
+ name: 'updateFields',
528
+ type: 'collection',
529
+ placeholder: 'Add Field',
530
+ default: {},
531
+ displayOptions: { show: { resource: ['share'], operation: ['update'] } },
532
+ options: [
533
+ {
534
+ displayName: 'Password',
535
+ name: 'password',
536
+ type: 'string',
537
+ typeOptions: { password: true },
538
+ default: '',
539
+ },
540
+ {
541
+ displayName: 'Expiry Date',
542
+ name: 'expireDate',
543
+ type: 'string',
544
+ default: '',
545
+ placeholder: 'YYYY-MM-DD',
546
+ },
547
+ {
548
+ displayName: 'Permissions',
549
+ name: 'permissions',
550
+ type: 'number',
551
+ default: 1,
552
+ description: 'Permissions bitfield: 1=read, 2=update, 4=create, 8=delete, 16=share',
553
+ },
554
+ {
555
+ displayName: 'Note for Recipient',
556
+ name: 'note',
557
+ type: 'string',
558
+ default: '',
559
+ },
560
+ {
561
+ displayName: 'Allow Upload (Public Link on Folder)',
562
+ name: 'publicUpload',
563
+ type: 'boolean',
564
+ default: false,
565
+ },
566
+ {
567
+ displayName: 'Label',
568
+ name: 'label',
569
+ type: 'string',
570
+ default: '',
571
+ },
572
+ ],
573
+ },
574
+ // ── Version: file path ─────────────────────────────────────────
575
+ {
576
+ displayName: 'File Path',
577
+ name: 'filePath',
578
+ type: 'string',
579
+ default: '',
580
+ placeholder: '/Documents/report.pdf',
581
+ displayOptions: { show: { resource: ['version'], operation: ['list', 'restore'] } },
582
+ },
583
+ {
584
+ displayName: 'Version ID',
585
+ name: 'versionId',
586
+ type: 'string',
587
+ default: '',
588
+ description: 'Version identifier returned by the List operation (use the "name" field)',
589
+ displayOptions: { show: { resource: ['version'], operation: ['restore'] } },
590
+ },
591
+ // ── Search ─────────────────────────────────────────────────────
592
+ {
593
+ displayName: 'Search Term',
594
+ name: 'term',
595
+ type: 'string',
596
+ default: '',
597
+ displayOptions: { show: { resource: ['search'], operation: ['search'] } },
598
+ },
599
+ {
600
+ displayName: 'Max Results',
601
+ name: 'limit',
602
+ type: 'number',
603
+ default: 30,
604
+ displayOptions: { show: { resource: ['search'], operation: ['search'] } },
605
+ },
606
+ ],
607
+ };
608
+ }
609
+ async testNextcloudCredentials(credential) {
610
+ const d = (credential.data ?? {});
611
+ const base = getEnvironmentBaseUrl(String(d.baseUrl ?? ''));
612
+ const nextcloudUrl = getNextcloudBaseUrl(String(d.baseUrl ?? ''));
613
+ const authMethod = String(d.authMethod ?? 'appPassword');
614
+ let authHeader;
615
+ if (authMethod === 'appPassword') {
616
+ authHeader = 'Basic ' + Buffer.from(`${d.username}:${d.appPassword}`).toString('base64');
617
+ }
618
+ else {
619
+ // Schritt 1: Keycloak-Token holen
620
+ const tokenUrl = `${getKeycloakBaseUrl(base)}/realms/${encodeURIComponent(String(d.realm))}/protocol/openid-connect/token`;
621
+ const bearerTokenType = String(d.bearerTokenType ?? 'access_token');
622
+ let rawBody;
623
+ try {
624
+ rawBody = await this.helpers.request({
625
+ method: 'POST',
626
+ url: tokenUrl,
627
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
628
+ body: new URLSearchParams({
629
+ grant_type: 'password',
630
+ client_id: String(d.clientId),
631
+ username: String(d.username),
632
+ password: String(d.password),
633
+ scope: 'openid',
634
+ }).toString(),
635
+ simple: false,
636
+ });
637
+ }
638
+ catch (e) {
639
+ return { status: 'Error', message: `Schritt 1/2 – Keycloak nicht erreichbar (${tokenUrl}): ${e.message}` };
640
+ }
641
+ let tokenData;
642
+ try {
643
+ tokenData = JSON.parse(rawBody);
644
+ }
645
+ catch {
646
+ return { status: 'Error', message: `Schritt 1/2 – Keycloak antwortete kein JSON:\n${rawBody.slice(0, 300)}` };
647
+ }
648
+ if (tokenData.error) {
649
+ const desc = tokenData.error_description ? ` – ${tokenData.error_description}` : '';
650
+ return { status: 'Error', message: `Schritt 1/2 – Keycloak: ${tokenData.error}${desc}` };
651
+ }
652
+ const bearerToken = tokenData[bearerTokenType];
653
+ if (!bearerToken) {
654
+ return { status: 'Error', message: `Schritt 1/2 – Keycloak gab kein ${bearerTokenType} zurück` };
655
+ }
656
+ authHeader = `Bearer ${String(bearerToken)}`;
657
+ }
658
+ // Schritt 2: Nextcloud testen
659
+ const testUrl = `${nextcloudUrl}/ocs/v2.php/cloud/user?format=json`;
660
+ let ncStatus;
661
+ try {
662
+ const resp = await this.helpers.request({
663
+ method: 'GET',
664
+ url: testUrl,
665
+ headers: { Authorization: authHeader, 'OCS-APIRequest': 'true', Accept: 'application/json' },
666
+ simple: false,
667
+ resolveWithFullResponse: true,
668
+ });
669
+ ncStatus = resp.statusCode;
670
+ }
671
+ catch (e) {
672
+ return { status: 'Error', message: `Schritt 2/2 – Nextcloud nicht erreichbar (${testUrl}): ${e.message}` };
673
+ }
674
+ if (ncStatus === 401) {
675
+ const hint = authMethod === 'oidc'
676
+ ? 'Bearer-Token von Keycloak erhalten, aber Nextcloud lehnt ihn ab.\nPrüfe: user_oidc-App installiert + Keycloak als Provider eingetragen?'
677
+ : 'App-Passwort oder Benutzername falsch.';
678
+ return {
679
+ status: 'Error',
680
+ message: `Schritt 2/2 – Nextcloud HTTP 401 bei ${testUrl}. ${hint}`,
681
+ };
682
+ }
683
+ if (ncStatus >= 400) {
684
+ return { status: 'Error', message: `Schritt 2/2 – Nextcloud HTTP ${ncStatus} bei ${testUrl}` };
685
+ }
686
+ return { status: 'OK', message: 'Verbindung erfolgreich!' };
687
+ }
688
+ async execute() {
689
+ const items = this.getInputData();
690
+ const returnData = [];
691
+ const credentials = await this.getCredentials('nextcloudApi');
692
+ const baseUrl = getNextcloudBaseUrl(String(credentials.baseUrl));
693
+ const username = String(credentials.username);
694
+ const req = async (options) => {
695
+ const headers = isOcsUrl(options.url)
696
+ ? { 'OCS-APIRequest': 'true', Accept: 'application/json', ...(options.headers ?? {}) }
697
+ : options.headers;
698
+ const url = options.url?.startsWith('http') ? options.url : `${baseUrl}${options.url}`;
699
+ const merged = {
700
+ ...options,
701
+ headers,
702
+ url,
703
+ };
704
+ try {
705
+ return await this.helpers.httpRequestWithAuthentication.call(this, 'nextcloudApi', merged);
706
+ }
707
+ catch (error) {
708
+ if (getHttpStatusCode(error) === 404 && url.includes('/remote.php/dav/')) {
709
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Nextcloud HTTP 404 bei ${url}. Der Pfad wurde nicht gefunden oder die WebDAV-User-ID stimmt nicht zur Anmeldung.`);
710
+ }
711
+ if (getHttpStatusCode(error) === 401) {
712
+ const authMethod = String(credentials.authMethod ?? 'appPassword');
713
+ const hint = authMethod === 'oidc'
714
+ ? 'Keycloak-Token wurde geholt, aber Nextcloud akzeptiert ihn nicht. Prüfe user_oidc/Token-Validierung in Nextcloud oder nutze ein Nextcloud-App-Passwort.'
715
+ : 'Prüfe, ob der Username die Nextcloud User-ID ist und das Passwort ein Nextcloud-App-Passwort ist.';
716
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Nextcloud HTTP 401 bei ${url}. ${hint}`);
717
+ }
718
+ throw error;
719
+ }
720
+ };
721
+ const userResponse = (await req({
722
+ method: 'GET',
723
+ url: '/ocs/v2.php/cloud/user?format=json',
724
+ json: true,
725
+ }));
726
+ const davUsername = getOcsUserId(userResponse, username);
727
+ const propfind = async (path, depth) => {
728
+ const xml = (await req({
729
+ method: 'PROPFIND',
730
+ url: path,
731
+ headers: { 'Content-Type': 'application/xml', Depth: depth },
732
+ body: PROPFIND_BODY,
733
+ json: false,
734
+ }));
735
+ return parseWebDavItems(xml, davUsername);
736
+ };
737
+ const findByFileId = async (fileId) => {
738
+ const xml = (await req({
739
+ method: 'SEARCH',
740
+ url: '/remote.php/dav/',
741
+ headers: { 'Content-Type': 'text/xml' },
742
+ body: searchByFileIdBody(davUsername, fileId),
743
+ json: false,
744
+ }));
745
+ const matches = parseWebDavItems(xml, davUsername);
746
+ const match = matches.find((item) => item.fileId === fileId) ?? matches[0];
747
+ if (!match) {
748
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Nextcloud fileId ${fileId} wurde nicht gefunden oder ist fuer diesen Benutzer nicht sichtbar.`);
749
+ }
750
+ return match;
751
+ };
752
+ const resolveDavPath = async (path) => {
753
+ const fileId = extractFileIdPath(path);
754
+ if (!fileId) {
755
+ return davPath(davUsername, path);
756
+ }
757
+ const item = await findByFileId(fileId);
758
+ return davPath(davUsername, item.path);
759
+ };
760
+ const resolveFilePath = async (path) => {
761
+ const fileId = extractFileIdPath(path);
762
+ if (!fileId) {
763
+ return path;
764
+ }
765
+ const item = await findByFileId(fileId);
766
+ return item.path;
767
+ };
768
+ const resolveShareWith = async (shareWith, shareType, itemIndex) => {
769
+ const trimmed = shareWith.trim();
770
+ const category = shareeCategoryForType(shareType);
771
+ if (!trimmed || !category || category === 'emails') {
772
+ return trimmed;
773
+ }
774
+ const response = (await req({
775
+ method: 'GET',
776
+ url: '/ocs/v2.php/apps/files_sharing/api/v1/sharees',
777
+ qs: {
778
+ format: 'json',
779
+ search: trimmed,
780
+ itemType: 'file',
781
+ page: 1,
782
+ perPage: 50,
783
+ },
784
+ json: true,
785
+ }));
786
+ const data = extractOcsData(response);
787
+ const candidates = getShareeCandidates(data, category);
788
+ const typeLabel = category === 'users' ? 'Benutzer' : 'Gruppe';
789
+ if (candidates.length === 0) {
790
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Kein Nextcloud-${typeLabel} für "${trimmed}" gefunden. Bei OIDC-Benutzern bitte die interne Nextcloud-ID (GUID) direkt eingeben.`, { itemIndex });
791
+ }
792
+ const exactMatches = candidates.filter((candidate) => shareeMatchesInput(candidate, trimmed));
793
+ const matches = exactMatches.length > 0 ? exactMatches : candidates;
794
+ const uniqueIds = [...new Set(matches.map(getShareeId).filter(Boolean))];
795
+ if (uniqueIds.length === 1) {
796
+ return uniqueIds[0];
797
+ }
798
+ if (uniqueIds.length > 1) {
799
+ const labels = candidates
800
+ .slice(0, 5)
801
+ .map((candidate) => `${String(candidate.label ?? candidate.name ?? getShareeId(candidate) ?? '(without label)')} (${getShareeId(candidate)})`)
802
+ .join(', ');
803
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Share With "${trimmed}" ist nicht eindeutig. Nutze die interne Nextcloud-ID/GUID oder einen exakten Namen. Treffer: ${labels}`, { itemIndex });
804
+ }
805
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Konnte keine interne Nextcloud-ID für "${trimmed}" ermitteln. Bitte die GUID des ${typeLabel}s direkt eingeben.`, { itemIndex });
806
+ };
807
+ for (let i = 0; i < items.length; i++) {
808
+ try {
809
+ const resource = this.getNodeParameter('resource', i);
810
+ const operation = this.getNodeParameter('operation', i);
811
+ // ──────────────────────────────────────────── FOLDER ──
812
+ if (resource === 'folder') {
813
+ if (operation === 'list') {
814
+ const path = this.getNodeParameter('path', i);
815
+ const recursive = this.getNodeParameter('recursive', i);
816
+ const allItems = await propfind(await resolveDavPath(path), recursive ? 'infinity' : '1');
817
+ for (const item of allItems.slice(1)) {
818
+ returnData.push({ json: item, pairedItem: i });
819
+ }
820
+ }
821
+ else if (operation === 'create') {
822
+ const path = this.getNodeParameter('path', i);
823
+ await req({ method: 'MKCOL', url: await resolveDavPath(path), json: false });
824
+ returnData.push({ json: { success: true, path }, pairedItem: i });
825
+ }
826
+ else if (operation === 'delete') {
827
+ const path = this.getNodeParameter('path', i);
828
+ await req({ method: 'DELETE', url: await resolveDavPath(path), json: false });
829
+ returnData.push({ json: { success: true, path }, pairedItem: i });
830
+ }
831
+ else if (operation === 'move' || operation === 'copy') {
832
+ const src = this.getNodeParameter('sourcePath', i);
833
+ const dst = this.getNodeParameter('destinationPath', i);
834
+ const overwrite = this.getNodeParameter('overwrite', i);
835
+ await req({
836
+ method: (operation === 'move' ? 'MOVE' : 'COPY'),
837
+ url: await resolveDavPath(src),
838
+ headers: {
839
+ Destination: `${baseUrl}${await resolveDavPath(dst)}`,
840
+ Overwrite: overwrite ? 'T' : 'F',
841
+ },
842
+ json: false,
843
+ });
844
+ returnData.push({ json: { success: true, sourcePath: src, destinationPath: dst }, pairedItem: i });
845
+ }
846
+ else if (operation === 'info') {
847
+ const path = this.getNodeParameter('path', i);
848
+ const result = await propfind(await resolveDavPath(path), '0');
849
+ returnData.push({ json: result[0] ?? {}, pairedItem: i });
850
+ }
851
+ // ──────────────────────────────────────────── FILE ──
852
+ }
853
+ else if (resource === 'file') {
854
+ if (operation === 'upload') {
855
+ const remotePath = this.getNodeParameter('remotePath', i);
856
+ const binProp = this.getNodeParameter('binaryPropertyName', i);
857
+ const binaryData = this.helpers.assertBinaryData(i, binProp);
858
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binProp);
859
+ await req({
860
+ method: 'PUT',
861
+ url: await resolveDavPath(remotePath),
862
+ body: buffer,
863
+ headers: { 'Content-Type': binaryData.mimeType || 'application/octet-stream' },
864
+ json: false,
865
+ });
866
+ returnData.push({ json: { success: true, path: remotePath }, pairedItem: i });
867
+ }
868
+ else if (operation === 'download') {
869
+ const path = this.getNodeParameter('path', i);
870
+ const outProp = this.getNodeParameter('outputBinaryProperty', i);
871
+ const response = (await req({
872
+ method: 'GET',
873
+ url: await resolveDavPath(path),
874
+ json: false,
875
+ encoding: 'arraybuffer',
876
+ returnFullResponse: true,
877
+ }));
878
+ const fileName = path.split('/').pop() || 'download';
879
+ const contentType = response.headers['content-type'] || 'application/octet-stream';
880
+ const binary = await this.helpers.prepareBinaryData(Buffer.from(response.body), fileName, contentType);
881
+ returnData.push({ json: { path, fileName }, binary: { [outProp]: binary }, pairedItem: i });
882
+ }
883
+ else if (operation === 'delete') {
884
+ const path = this.getNodeParameter('path', i);
885
+ await req({ method: 'DELETE', url: await resolveDavPath(path), json: false });
886
+ returnData.push({ json: { success: true, path }, pairedItem: i });
887
+ }
888
+ else if (operation === 'move' || operation === 'copy') {
889
+ const src = this.getNodeParameter('sourcePath', i);
890
+ const dst = this.getNodeParameter('destinationPath', i);
891
+ const overwrite = this.getNodeParameter('overwrite', i);
892
+ await req({
893
+ method: (operation === 'move' ? 'MOVE' : 'COPY'),
894
+ url: await resolveDavPath(src),
895
+ headers: {
896
+ Destination: `${baseUrl}${await resolveDavPath(dst)}`,
897
+ Overwrite: overwrite ? 'T' : 'F',
898
+ },
899
+ json: false,
900
+ });
901
+ returnData.push({ json: { success: true, sourcePath: src, destinationPath: dst }, pairedItem: i });
902
+ }
903
+ else if (operation === 'info') {
904
+ const path = this.getNodeParameter('path', i);
905
+ const result = await propfind(await resolveDavPath(path), '0');
906
+ returnData.push({ json: result[0] ?? {}, pairedItem: i });
907
+ }
908
+ // ──────────────────────────────────────────── SHARE ──
909
+ }
910
+ else if (resource === 'share') {
911
+ if (operation === 'list') {
912
+ const sharePath = this.getNodeParameter('sharePath', i);
913
+ const subfiles = this.getNodeParameter('subfiles', i);
914
+ const qs = { format: 'json' };
915
+ if (sharePath)
916
+ qs.path = await resolveFilePath(sharePath);
917
+ if (subfiles)
918
+ qs.subfiles = 'true';
919
+ const response = (await req({
920
+ method: 'GET',
921
+ url: '/ocs/v2.php/apps/files_sharing/api/v1/shares',
922
+ qs,
923
+ json: true,
924
+ }));
925
+ const data = extractOcsData(response);
926
+ const shares = Array.isArray(data) ? data : [data];
927
+ for (const share of shares) {
928
+ returnData.push({ json: share, pairedItem: i });
929
+ }
930
+ }
931
+ else if (operation === 'create') {
932
+ const path = this.getNodeParameter('shareSrcPath', i);
933
+ const shareType = this.getNodeParameter('shareType', i);
934
+ const shareOptions = this.getNodeParameter('shareOptions', i);
935
+ const resolvedPath = await resolveFilePath(path);
936
+ const davItems = await propfind(await resolveDavPath(path), '0');
937
+ const itemType = (davItems[0]?.isDirectory ?? false) ? 'folder' : 'file';
938
+ const body = { path: resolvedPath, shareType, itemType, ...shareOptions };
939
+ if ([0, 1, 4].includes(shareType)) {
940
+ body.shareWith = await resolveShareWith(this.getNodeParameter('shareWith', i), shareType, i);
941
+ }
942
+ if ([0, 1, 3, 4].includes(shareType)) {
943
+ body.permissions = this.getNodeParameter('permissions', i);
944
+ }
945
+ const response = (await req({
946
+ method: 'POST',
947
+ url: '/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json',
948
+ body: new URLSearchParams(Object.fromEntries(Object.entries(body)
949
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
950
+ .map(([k, v]) => [k, String(v)]))).toString(),
951
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
952
+ json: true,
953
+ }));
954
+ const data = extractOcsData(response);
955
+ returnData.push({ json: (Array.isArray(data) ? data[0] : data), pairedItem: i });
956
+ }
957
+ else if (operation === 'get') {
958
+ const shareId = this.getNodeParameter('shareId', i);
959
+ const response = (await req({
960
+ method: 'GET',
961
+ url: `/ocs/v2.php/apps/files_sharing/api/v1/shares/${shareId}?format=json`,
962
+ json: true,
963
+ }));
964
+ const data = extractOcsData(response);
965
+ returnData.push({ json: (Array.isArray(data) ? data[0] : data), pairedItem: i });
966
+ }
967
+ else if (operation === 'update') {
968
+ const shareId = this.getNodeParameter('shareId', i);
969
+ const updateFields = this.getNodeParameter('updateFields', i);
970
+ const response = (await req({
971
+ method: 'PUT',
972
+ url: `/ocs/v2.php/apps/files_sharing/api/v1/shares/${shareId}?format=json`,
973
+ body: new URLSearchParams(Object.fromEntries(Object.entries(updateFields)
974
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
975
+ .map(([k, v]) => [k, String(v)]))).toString(),
976
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
977
+ json: true,
978
+ }));
979
+ const data = extractOcsData(response);
980
+ returnData.push({ json: (Array.isArray(data) ? data[0] : data), pairedItem: i });
981
+ }
982
+ else if (operation === 'delete') {
983
+ const shareId = this.getNodeParameter('shareId', i);
984
+ await req({
985
+ method: 'DELETE',
986
+ url: `/ocs/v2.php/apps/files_sharing/api/v1/shares/${shareId}?format=json`,
987
+ json: true,
988
+ });
989
+ returnData.push({ json: { success: true, shareId }, pairedItem: i });
990
+ }
991
+ // ──────────────────────────────────────────── VERSION ──
992
+ }
993
+ else if (resource === 'version') {
994
+ const filePath = this.getNodeParameter('filePath', i);
995
+ // Resolve fileId first (needed for versions WebDAV path)
996
+ const fileInfoItems = await propfind(await resolveDavPath(filePath), '0');
997
+ const fileId = fileInfoItems[0]?.fileId;
998
+ if (!fileId) {
999
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Could not resolve fileId for: ${filePath}`, { itemIndex: i });
1000
+ }
1001
+ if (operation === 'list') {
1002
+ const versionsPath = `/remote.php/dav/versions/${encodeURIComponent(davUsername)}/versions/${fileId}/`;
1003
+ const versionItems = await propfind(versionsPath, '1');
1004
+ for (const v of versionItems.slice(1)) {
1005
+ returnData.push({ json: { ...v, fileId, filePath }, pairedItem: i });
1006
+ }
1007
+ }
1008
+ else if (operation === 'restore') {
1009
+ const versionId = this.getNodeParameter('versionId', i);
1010
+ await req({
1011
+ method: 'COPY',
1012
+ url: `/remote.php/dav/versions/${encodeURIComponent(davUsername)}/versions/${fileId}/${versionId}`,
1013
+ headers: { Destination: `${baseUrl}${await resolveDavPath(filePath)}` },
1014
+ json: false,
1015
+ });
1016
+ returnData.push({ json: { success: true, filePath, versionId }, pairedItem: i });
1017
+ }
1018
+ // ──────────────────────────────────────────── SEARCH ──
1019
+ }
1020
+ else if (resource === 'search') {
1021
+ const term = this.getNodeParameter('term', i);
1022
+ const limit = this.getNodeParameter('limit', i);
1023
+ const response = (await req({
1024
+ method: 'GET',
1025
+ url: `/ocs/v2.php/search/providers/files/search?format=json&term=${encodeURIComponent(term)}&limit=${limit}`,
1026
+ json: true,
1027
+ }));
1028
+ const data = extractOcsData(response);
1029
+ const entries = data.entries || [];
1030
+ for (const entry of entries) {
1031
+ returnData.push({ json: entry, pairedItem: i });
1032
+ }
1033
+ }
1034
+ }
1035
+ catch (error) {
1036
+ if (this.continueOnFail()) {
1037
+ returnData.push({ json: { error: error.message }, pairedItem: i });
1038
+ }
1039
+ else {
1040
+ throw error;
1041
+ }
1042
+ }
1043
+ }
1044
+ return [returnData];
1045
+ }
1046
+ }
1047
+ exports.NextcloudNode = NextcloudNode;
1048
+ //# sourceMappingURL=NextcloudNode.node.js.map