@sinoia/hubdoc-tools 1.3.6 → 1.3.7

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.
@@ -1,512 +0,0 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import fs from 'fs-extra';
3
- import path from 'path';
4
- import {
5
- DocumentSourcePlugin,
6
- DocumentSource,
7
- PluginConfig,
8
- ScanResult,
9
- PluginImportOptions,
10
- PluginExportOptions,
11
- ImportResult,
12
- ExportResult
13
- } from '../../src/types/plugins';
14
-
15
- interface NuxeoConfig extends PluginConfig {
16
- baseUrl: string;
17
- username: string;
18
- password: string;
19
- repository?: string; // Default repository name, usually 'default'
20
- rootPath?: string; // Starting path, defaults to '/default-domain/workspaces'
21
- limit?: number;
22
- }
23
-
24
- interface NuxeoDocument {
25
- uid: string;
26
- name: string;
27
- title: string;
28
- type: string;
29
- path: string;
30
- state: string;
31
- isFolder: boolean;
32
- lastModified: string;
33
- created: string;
34
- properties: {
35
- 'dc:title': string;
36
- 'dc:description'?: string;
37
- 'dc:creator': string;
38
- 'dc:lastContributor': string;
39
- 'dc:created': string;
40
- 'dc:modified': string;
41
- 'file:content'?: {
42
- name: string;
43
- 'mime-type': string;
44
- length: number;
45
- digest?: string;
46
- };
47
- [key: string]: any;
48
- };
49
- facets?: string[];
50
- changeToken?: string;
51
- }
52
-
53
- export default class NuxeoPlugin implements DocumentSourcePlugin {
54
- readonly name = 'nuxeo';
55
- readonly version = '1.0.0';
56
- readonly description = 'Nuxeo ECM document source';
57
- readonly supportedOperations = ['import', 'export', 'both'] as const;
58
-
59
- private config?: NuxeoConfig;
60
- private apiClient?: AxiosInstance;
61
-
62
- async testConnection(config: PluginConfig): Promise<boolean> {
63
- try {
64
- const client = this.createApiClient(config as NuxeoConfig);
65
- const response = await client.get('/user/Administrator');
66
- return response.status === 200;
67
- } catch (error: any) {
68
- console.error(`Nuxeo connection test failed: ${error.message}`);
69
- return false;
70
- }
71
- }
72
-
73
- async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
74
- this.config = config as NuxeoConfig;
75
- this.apiClient = this.createApiClient(this.config);
76
-
77
- const sources: DocumentSource[] = [];
78
- const errors: string[] = [];
79
- let totalSize = 0;
80
-
81
- try {
82
- const limit = (this.config as any).limit || (options as any)?.limit;
83
- console.log(`🔍 Scanning Nuxeo repository${limit ? ` (limit: ${limit})` : ''}...`);
84
-
85
- const rootPath = this.config.rootPath || '/default-domain/workspaces';
86
- const documents = await this.scanPath(rootPath);
87
-
88
- let processedCount = 0;
89
- for (const doc of documents) {
90
- if (!doc.isFolder && doc.properties['file:content']) {
91
- const fileContent = doc.properties['file:content'];
92
-
93
- const source: DocumentSource = {
94
- id: doc.uid,
95
- name: fileContent.name || doc.name,
96
- path: this.getDocumentPath(doc),
97
- size: fileContent.length || 0,
98
- mimeType: fileContent['mime-type'] || 'application/octet-stream',
99
- lastModified: new Date(doc.properties['dc:modified']),
100
- metadata: {
101
- nuxeoUid: doc.uid,
102
- nuxeoPath: doc.path,
103
- docType: doc.type,
104
- title: doc.properties['dc:title'],
105
- description: doc.properties['dc:description'],
106
- creator: doc.properties['dc:creator'],
107
- lastContributor: doc.properties['dc:lastContributor'],
108
- createdAt: doc.properties['dc:created'],
109
- state: doc.state,
110
- facets: doc.facets,
111
- changeToken: doc.changeToken,
112
- digest: fileContent.digest
113
- }
114
- };
115
-
116
- // Apply filters
117
- if (this.shouldIncludeSource(source, options)) {
118
- sources.push(source);
119
- totalSize += source.size;
120
- processedCount++;
121
-
122
- // Check limit
123
- if (limit && processedCount >= limit) {
124
- console.log(`📏 Reached limit of ${limit} files`);
125
- break;
126
- }
127
- }
128
- }
129
- }
130
-
131
- return {
132
- sources,
133
- totalCount: sources.length,
134
- totalSize,
135
- errors
136
- };
137
- } catch (error: any) {
138
- return {
139
- sources: [],
140
- totalCount: 0,
141
- totalSize: 0,
142
- errors: [`Nuxeo scan failed: ${error.message}`]
143
- };
144
- }
145
- }
146
-
147
- private async scanPath(docPath: string): Promise<NuxeoDocument[]> {
148
- if (!this.apiClient) throw new Error('API client not initialized');
149
-
150
- const allDocuments: NuxeoDocument[] = [];
151
- let currentPageIndex = 0;
152
- const pageSize = 100;
153
-
154
- try {
155
- while (true) {
156
- // Use NXQL query to get all documents under the specified path
157
- const query = `SELECT * FROM Document WHERE ecm:path STARTSWITH '${docPath}' AND ecm:isVersion = 0 AND ecm:isTrashed = 0`;
158
-
159
- const response = await this.apiClient.get('/search/lang/NXQL/execute', {
160
- params: {
161
- query: query,
162
- currentPageIndex: currentPageIndex,
163
- pageSize: pageSize,
164
- sortBy: 'dc:title',
165
- sortOrder: 'ASC'
166
- }
167
- });
168
-
169
- const documents: NuxeoDocument[] = response.data.entries || [];
170
-
171
- if (documents.length === 0) {
172
- break;
173
- }
174
-
175
- // Process both files and folders
176
- for (const doc of documents) {
177
- allDocuments.push(doc);
178
-
179
- // If it's a folder and has children, we could recursively scan
180
- // but the STARTSWITH query should already include all descendants
181
- }
182
-
183
- // Check if there are more pages
184
- if (documents.length < pageSize) {
185
- break;
186
- }
187
- currentPageIndex++;
188
- }
189
-
190
- return allDocuments;
191
- } catch (error: any) {
192
- if (error.response?.status === 404) {
193
- console.warn(`Warning: Path not found: ${docPath}`);
194
- return [];
195
- }
196
- throw error;
197
- }
198
- }
199
-
200
- async import(
201
- config: PluginConfig,
202
- sources: DocumentSource[],
203
- targetDir: string,
204
- options?: PluginImportOptions
205
- ): Promise<ImportResult[]> {
206
- this.config = config as NuxeoConfig;
207
- this.apiClient = this.createApiClient(this.config);
208
-
209
- const results: ImportResult[] = [];
210
- const batchSize = options?.batchSize || 5;
211
-
212
- // Process in batches to respect API limits
213
- for (let i = 0; i < sources.length; i += batchSize) {
214
- const batch = sources.slice(i, i + batchSize);
215
-
216
- for (const source of batch) {
217
- const result = await this.importSingle(source, targetDir);
218
- results.push(result);
219
-
220
- // Small delay to respect rate limits
221
- await this.sleep(200);
222
- }
223
- }
224
-
225
- return results;
226
- }
227
-
228
- private async importSingle(source: DocumentSource, targetDir: string): Promise<ImportResult> {
229
- try {
230
- if (!this.apiClient) throw new Error('API client not initialized');
231
-
232
- const targetPath = path.join(targetDir, source.path);
233
- const targetDirectory = path.dirname(targetPath);
234
-
235
- await fs.ensureDir(targetDirectory);
236
-
237
- // Download file content from Nuxeo using the blob download endpoint
238
- const response = await this.apiClient.get(`/id/${source.id}/@blob/file:content`, {
239
- responseType: 'stream'
240
- });
241
-
242
- const writer = fs.createWriteStream(targetPath);
243
- response.data.pipe(writer);
244
-
245
- return new Promise((resolve) => {
246
- writer.on('finish', () => {
247
- resolve({
248
- success: true,
249
- source,
250
- localPath: targetPath,
251
- bytesTransferred: source.size
252
- });
253
- });
254
-
255
- writer.on('error', (error) => {
256
- resolve({
257
- success: false,
258
- source,
259
- error: error.message
260
- });
261
- });
262
- });
263
- } catch (error: any) {
264
- return {
265
- success: false,
266
- source,
267
- error: error.message
268
- };
269
- }
270
- }
271
-
272
- async export?(
273
- config: PluginConfig,
274
- localSources: DocumentSource[],
275
- options?: PluginExportOptions
276
- ): Promise<ExportResult[]> {
277
- this.config = config as NuxeoConfig;
278
- this.apiClient = this.createApiClient(this.config);
279
-
280
- const results: ExportResult[] = [];
281
- const rootPath = this.config.rootPath || '/default-domain/workspaces';
282
-
283
- for (const source of localSources) {
284
- try {
285
- // Determine target path in Nuxeo
286
- let targetPath = rootPath;
287
-
288
- if (options?.preserveStructure && source.path.includes('/')) {
289
- const folderPath = path.dirname(source.path);
290
- const targetFolderId = await this.createFolderStructure(folderPath, rootPath);
291
- targetPath = targetFolderId;
292
- }
293
-
294
- // Read local file
295
- const fileContent = await fs.readFile(source.id);
296
- // Normalize filename to NFC to handle accented characters consistently across platforms
297
- const fileName = (options?.preserveStructure ? path.basename(source.path) : source.name).normalize('NFC');
298
-
299
- // Create document in Nuxeo using multipart form data
300
- const FormData = require('form-data');
301
- const form = new FormData();
302
-
303
- // Document metadata
304
- const docData = {
305
- 'entity-type': 'document',
306
- type: 'File',
307
- name: fileName,
308
- properties: {
309
- 'dc:title': fileName,
310
- 'dc:description': `Uploaded via hubdoc-tools`
311
- }
312
- };
313
-
314
- form.append('entity-type', 'document');
315
- form.append('type', 'File');
316
- form.append('name', fileName);
317
- form.append('properties', JSON.stringify({
318
- 'dc:title': fileName,
319
- 'dc:description': 'Uploaded via hubdoc-tools'
320
- }));
321
-
322
- // Attach file
323
- form.append('file', fileContent, {
324
- filename: fileName,
325
- contentType: source.mimeType || 'application/octet-stream'
326
- });
327
-
328
- const uploadResponse = await this.apiClient!.post(`/path${targetPath}`, form, {
329
- headers: {
330
- ...form.getHeaders()
331
- }
332
- });
333
-
334
- const resultTargetPath = options?.preserveStructure ? source.path : source.name;
335
-
336
- results.push({
337
- success: true,
338
- targetPath: resultTargetPath,
339
- source,
340
- bytesTransferred: source.size
341
- });
342
- } catch (error: any) {
343
- results.push({
344
- success: false,
345
- targetPath: options?.targetPath || '',
346
- source,
347
- error: error.message
348
- });
349
- }
350
- }
351
-
352
- return results;
353
- }
354
-
355
- private async createFolderStructure(folderPath: string, parentPath: string): Promise<string> {
356
- if (!this.apiClient) throw new Error('API client not initialized');
357
-
358
- const parts = folderPath.split('/').filter(part => part.length > 0);
359
- let currentPath = parentPath;
360
-
361
- for (const folderName of parts) {
362
- const expectedPath = `${currentPath}/${folderName}`;
363
-
364
- try {
365
- // Check if folder already exists
366
- const response = await this.apiClient.get(`/path${expectedPath}`);
367
-
368
- if (response.data && response.data.type === 'Folder') {
369
- currentPath = expectedPath;
370
- continue;
371
- }
372
- } catch (error: any) {
373
- // Folder doesn't exist, create it
374
- if (error.response?.status === 404) {
375
- try {
376
- const createResponse = await this.apiClient.post(`/path${currentPath}`, {
377
- 'entity-type': 'document',
378
- type: 'Folder',
379
- name: folderName,
380
- properties: {
381
- 'dc:title': folderName
382
- }
383
- }, {
384
- headers: {
385
- 'Content-Type': 'application/json'
386
- }
387
- });
388
-
389
- currentPath = expectedPath;
390
- } catch (createError: any) {
391
- throw new Error(`Failed to create folder ${folderName}: ${createError.message}`);
392
- }
393
- } else {
394
- throw error;
395
- }
396
- }
397
- }
398
-
399
- return currentPath;
400
- }
401
-
402
- getConfigSchema(): Record<string, any> {
403
- return {
404
- type: 'object',
405
- properties: {
406
- baseUrl: {
407
- type: 'string',
408
- description: 'Nuxeo base URL (e.g., http://localhost:8080/nuxeo)',
409
- required: true
410
- },
411
- username: {
412
- type: 'string',
413
- description: 'Nuxeo username',
414
- required: true
415
- },
416
- password: {
417
- type: 'string',
418
- description: 'Nuxeo password',
419
- required: true
420
- },
421
- repository: {
422
- type: 'string',
423
- description: 'Nuxeo repository name (default: default)',
424
- default: 'default'
425
- },
426
- rootPath: {
427
- type: 'string',
428
- description: 'Starting path in Nuxeo (default: /default-domain/workspaces)',
429
- default: '/default-domain/workspaces'
430
- },
431
- limit: {
432
- type: 'number',
433
- description: 'Maximum number of documents to scan (useful for testing)',
434
- required: false
435
- }
436
- },
437
- required: ['baseUrl', 'username', 'password']
438
- };
439
- }
440
-
441
- async initialize(config: PluginConfig): Promise<void> {
442
- this.config = config as NuxeoConfig;
443
-
444
- if (!this.config.baseUrl || !this.config.username || !this.config.password) {
445
- throw new Error('Nuxeo base URL, username, and password are required');
446
- }
447
-
448
- this.apiClient = this.createApiClient(this.config);
449
- }
450
-
451
- async destroy(): Promise<void> {
452
- this.config = undefined;
453
- this.apiClient = undefined;
454
- }
455
-
456
- private createApiClient(config: NuxeoConfig): AxiosInstance {
457
- const repository = config.repository || 'default';
458
- const baseURL = `${config.baseUrl.replace(/\/$/, '')}/api/v1/repo/${repository}`;
459
-
460
- return axios.create({
461
- baseURL,
462
- auth: {
463
- username: config.username,
464
- password: config.password
465
- },
466
- headers: {
467
- 'Content-Type': 'application/json',
468
- 'Accept': 'application/json'
469
- },
470
- timeout: 30000
471
- });
472
- }
473
-
474
- private getDocumentPath(doc: NuxeoDocument): string {
475
- // Remove the repository-specific prefix from the path
476
- let cleanPath = doc.path.replace(/^\/default-domain\/workspaces\//, '');
477
-
478
- // If the document has a file content, use the file name
479
- if (doc.properties['file:content']?.name) {
480
- const pathParts = cleanPath.split('/');
481
- pathParts[pathParts.length - 1] = doc.properties['file:content'].name;
482
- cleanPath = pathParts.join('/');
483
- }
484
-
485
- return cleanPath || doc.name;
486
- }
487
-
488
- private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
489
- // Apply size filter
490
- if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
491
- return false;
492
- }
493
-
494
- // Apply date range filter
495
- if (options?.filters?.dateRange) {
496
- const { from, to } = options.filters.dateRange;
497
- if (from && source.lastModified < from) return false;
498
- if (to && source.lastModified > to) return false;
499
- }
500
-
501
- // Apply MIME type filter
502
- if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
503
- return false;
504
- }
505
-
506
- return true;
507
- }
508
-
509
- private sleep(ms: number): Promise<void> {
510
- return new Promise(resolve => setTimeout(resolve, ms));
511
- }
512
- }
@@ -1,12 +0,0 @@
1
- {
2
- "name": "nuxeo",
3
- "version": "1.0.0",
4
- "description": "Nuxeo document source plugin",
5
- "author": "HubDoc Tools",
6
- "main": "index.ts",
7
- "hubdocToolVersion": "^1.0.0",
8
- "dependencies": {
9
- "axios": "^1.5.0",
10
- "fs-extra": "^11.1.0"
11
- }
12
- }