@sinoia/hubdoc-tools 1.3.6 → 1.3.8

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,495 +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 BoxConfig extends PluginConfig {
16
- accessToken: string;
17
- clientId?: string;
18
- clientSecret?: string;
19
- rootFolderId?: string; // Default to '0' (root)
20
- limit?: number;
21
- }
22
-
23
- interface BoxItem {
24
- type: 'file' | 'folder';
25
- id: string;
26
- name: string;
27
- size?: number;
28
- modified_at: string;
29
- created_at: string;
30
- description?: string;
31
- path_collection?: {
32
- entries: Array<{
33
- type: string;
34
- id: string;
35
- name: string;
36
- }>;
37
- };
38
- created_by?: {
39
- type: string;
40
- id: string;
41
- name: string;
42
- login: string;
43
- };
44
- modified_by?: {
45
- type: string;
46
- id: string;
47
- name: string;
48
- login: string;
49
- };
50
- shared_link?: {
51
- url: string;
52
- download_url: string;
53
- };
54
- tags?: string[];
55
- }
56
-
57
- export default class BoxPlugin implements DocumentSourcePlugin {
58
- readonly name = 'box';
59
- readonly version = '1.0.0';
60
- readonly description = 'Box cloud storage document source';
61
- readonly supportedOperations = ['import', 'export', 'both'] as const;
62
-
63
- private config?: BoxConfig;
64
- private apiClient?: AxiosInstance;
65
- private readonly baseUrl = 'https://api.box.com/2.0';
66
-
67
- async testConnection(config: PluginConfig): Promise<boolean> {
68
- try {
69
- const client = this.createApiClient(config as BoxConfig);
70
- const response = await client.get('/users/me');
71
- return response.status === 200;
72
- } catch (error: any) {
73
- console.error(`Box connection test failed: ${error.message}`);
74
- return false;
75
- }
76
- }
77
-
78
- async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
79
- this.config = config as BoxConfig;
80
- this.apiClient = this.createApiClient(this.config);
81
-
82
- const sources: DocumentSource[] = [];
83
- const errors: string[] = [];
84
- let totalSize = 0;
85
-
86
- try {
87
- const limit = (this.config as any).limit || (options as any)?.limit;
88
- console.log(`🔍 Scanning Box documents${limit ? ` (limit: ${limit})` : ''}...`);
89
-
90
- const rootFolderId = this.config.rootFolderId || '0';
91
- const items = await this.scanFolder(rootFolderId, '', options);
92
-
93
- let processedCount = 0;
94
- for (const item of items) {
95
- if (item.type === 'file') {
96
- const source: DocumentSource = {
97
- id: item.id,
98
- name: item.name,
99
- path: this.getItemPath(item),
100
- size: item.size || 0,
101
- mimeType: this.getMimeType(item.name),
102
- lastModified: new Date(item.modified_at),
103
- metadata: {
104
- boxId: item.id,
105
- description: item.description,
106
- createdAt: item.created_at,
107
- createdBy: item.created_by?.name,
108
- modifiedBy: item.modified_by?.name,
109
- tags: item.tags,
110
- sharedLink: item.shared_link?.url,
111
- pathCollection: item.path_collection
112
- }
113
- };
114
-
115
- // Apply filters
116
- if (this.shouldIncludeSource(source, options)) {
117
- sources.push(source);
118
- totalSize += source.size;
119
- processedCount++;
120
-
121
- // Check limit
122
- if (limit && processedCount >= limit) {
123
- console.log(`📏 Reached limit of ${limit} files`);
124
- break;
125
- }
126
- }
127
- }
128
- }
129
-
130
- return {
131
- sources,
132
- totalCount: sources.length,
133
- totalSize,
134
- errors
135
- };
136
- } catch (error: any) {
137
- return {
138
- sources: [],
139
- totalCount: 0,
140
- totalSize: 0,
141
- errors: [`Box scan failed: ${error.message}`]
142
- };
143
- }
144
- }
145
-
146
- private async scanFolder(folderId: string, parentPath: string, options?: PluginImportOptions): Promise<BoxItem[]> {
147
- if (!this.apiClient) throw new Error('API client not initialized');
148
-
149
- const allItems: BoxItem[] = [];
150
- let offset = 0;
151
- const limit = 1000; // Box API max
152
-
153
- try {
154
- while (true) {
155
- const response = await this.apiClient.get(`/folders/${folderId}/items`, {
156
- params: {
157
- fields: 'id,name,type,size,modified_at,created_at,description,path_collection,created_by,modified_by,shared_link,tags',
158
- limit,
159
- offset
160
- }
161
- });
162
-
163
- const items: BoxItem[] = response.data.entries || [];
164
-
165
- for (const item of items) {
166
- const itemPath = parentPath ? `${parentPath}/${item.name}` : item.name;
167
-
168
- if (item.type === 'file') {
169
- // Add path information to the item
170
- (item as any).fullPath = itemPath;
171
- allItems.push(item);
172
- } else if (item.type === 'folder') {
173
- // Recursively scan subfolders
174
- const subItems = await this.scanFolder(item.id, itemPath, options);
175
- allItems.push(...subItems);
176
- }
177
- }
178
-
179
- // Check if there are more items
180
- if (items.length < limit) {
181
- break;
182
- }
183
- offset += limit;
184
- }
185
-
186
- return allItems;
187
- } catch (error: any) {
188
- if (error.response?.status === 404) {
189
- console.warn(`Warning: Folder not found: ${folderId}`);
190
- return [];
191
- }
192
- throw error;
193
- }
194
- }
195
-
196
- async import(
197
- config: PluginConfig,
198
- sources: DocumentSource[],
199
- targetDir: string,
200
- options?: PluginImportOptions
201
- ): Promise<ImportResult[]> {
202
- this.config = config as BoxConfig;
203
- this.apiClient = this.createApiClient(this.config);
204
-
205
- const results: ImportResult[] = [];
206
- const batchSize = options?.batchSize || 5;
207
-
208
- // Process in batches to respect API limits
209
- for (let i = 0; i < sources.length; i += batchSize) {
210
- const batch = sources.slice(i, i + batchSize);
211
-
212
- for (const source of batch) {
213
- const result = await this.importSingle(source, targetDir);
214
- results.push(result);
215
-
216
- // Small delay to respect rate limits
217
- await this.sleep(200);
218
- }
219
- }
220
-
221
- return results;
222
- }
223
-
224
- private async importSingle(source: DocumentSource, targetDir: string): Promise<ImportResult> {
225
- try {
226
- if (!this.apiClient) throw new Error('API client not initialized');
227
-
228
- const targetPath = path.join(targetDir, source.path);
229
- const targetDirectory = path.dirname(targetPath);
230
-
231
- await fs.ensureDir(targetDirectory);
232
-
233
- // Download file from Box
234
- const response = await this.apiClient.get(`/files/${source.id}/content`, {
235
- responseType: 'stream',
236
- maxRedirects: 5 // Box returns redirects to download URLs
237
- });
238
-
239
- const writer = fs.createWriteStream(targetPath);
240
- response.data.pipe(writer);
241
-
242
- return new Promise((resolve) => {
243
- writer.on('finish', () => {
244
- resolve({
245
- success: true,
246
- source,
247
- localPath: targetPath,
248
- bytesTransferred: source.size
249
- });
250
- });
251
-
252
- writer.on('error', (error) => {
253
- resolve({
254
- success: false,
255
- source,
256
- error: error.message
257
- });
258
- });
259
- });
260
- } catch (error: any) {
261
- return {
262
- success: false,
263
- source,
264
- error: error.message
265
- };
266
- }
267
- }
268
-
269
- async export?(
270
- config: PluginConfig,
271
- localSources: DocumentSource[],
272
- options?: PluginExportOptions
273
- ): Promise<ExportResult[]> {
274
- this.config = config as BoxConfig;
275
- this.apiClient = this.createApiClient(this.config);
276
-
277
- const results: ExportResult[] = [];
278
- const rootFolderId = this.config.rootFolderId || '0';
279
-
280
- for (const source of localSources) {
281
- try {
282
- // Determine target folder
283
- let targetFolderId = rootFolderId;
284
-
285
- if (options?.preserveStructure && source.path.includes('/')) {
286
- const folderPath = path.dirname(source.path);
287
- targetFolderId = await this.createFolderStructure(folderPath, rootFolderId);
288
- }
289
-
290
- // Upload file to Box
291
- const fileContent = await fs.readFile(source.id);
292
- // Normalize filename to NFC to handle accented characters consistently across platforms
293
- const fileName = (options?.preserveStructure ? path.basename(source.path) : source.name).normalize('NFC');
294
-
295
- // Box requires multipart form data for uploads
296
- const FormData = require('form-data');
297
- const form = new FormData();
298
-
299
- form.append('attributes', JSON.stringify({
300
- name: fileName,
301
- parent: { id: targetFolderId }
302
- }));
303
- form.append('file', fileContent, fileName);
304
-
305
- const response = await this.apiClient!.post('/files/content', form, {
306
- headers: {
307
- ...form.getHeaders()
308
- }
309
- });
310
-
311
- const targetPath = options?.preserveStructure ? source.path : source.name;
312
-
313
- results.push({
314
- success: true,
315
- targetPath,
316
- source,
317
- bytesTransferred: source.size
318
- });
319
- } catch (error: any) {
320
- results.push({
321
- success: false,
322
- targetPath: options?.targetPath || '',
323
- source,
324
- error: error.message
325
- });
326
- }
327
- }
328
-
329
- return results;
330
- }
331
-
332
- private async createFolderStructure(folderPath: string, parentId: string): Promise<string> {
333
- if (!this.apiClient) throw new Error('API client not initialized');
334
-
335
- const parts = folderPath.split('/').filter(part => part.length > 0);
336
- let currentParentId = parentId;
337
-
338
- for (const folderName of parts) {
339
- // Check if folder already exists
340
- try {
341
- const response = await this.apiClient.get(`/folders/${currentParentId}/items`, {
342
- params: {
343
- fields: 'id,name,type',
344
- limit: 1000
345
- }
346
- });
347
-
348
- const existingFolder = response.data.entries.find(
349
- (item: any) => item.type === 'folder' && item.name === folderName
350
- );
351
-
352
- if (existingFolder) {
353
- currentParentId = existingFolder.id;
354
- } else {
355
- // Create new folder
356
- const createResponse = await this.apiClient.post('/folders', {
357
- name: folderName,
358
- parent: { id: currentParentId }
359
- });
360
- currentParentId = createResponse.data.id;
361
- }
362
- } catch (error: any) {
363
- throw new Error(`Failed to create folder structure: ${error.message}`);
364
- }
365
- }
366
-
367
- return currentParentId;
368
- }
369
-
370
- getConfigSchema(): Record<string, any> {
371
- return {
372
- type: 'object',
373
- properties: {
374
- accessToken: {
375
- type: 'string',
376
- description: 'Box Access Token (get from Box Developer Console)',
377
- required: true
378
- },
379
- clientId: {
380
- type: 'string',
381
- description: 'Box Client ID (optional, for OAuth flow)',
382
- required: false
383
- },
384
- clientSecret: {
385
- type: 'string',
386
- description: 'Box Client Secret (optional, for OAuth flow)',
387
- required: false
388
- },
389
- rootFolderId: {
390
- type: 'string',
391
- description: 'Root folder ID to scan (default: 0 for root)',
392
- default: '0'
393
- },
394
- limit: {
395
- type: 'number',
396
- description: 'Maximum number of documents to scan (useful for testing)',
397
- required: false
398
- }
399
- },
400
- required: ['accessToken']
401
- };
402
- }
403
-
404
- async initialize(config: PluginConfig): Promise<void> {
405
- this.config = config as BoxConfig;
406
-
407
- if (!this.config.accessToken) {
408
- throw new Error('Box access token is required');
409
- }
410
-
411
- this.apiClient = this.createApiClient(this.config);
412
- }
413
-
414
- async destroy(): Promise<void> {
415
- this.config = undefined;
416
- this.apiClient = undefined;
417
- }
418
-
419
- private createApiClient(config: BoxConfig): AxiosInstance {
420
- return axios.create({
421
- baseURL: this.baseUrl,
422
- headers: {
423
- 'Authorization': `Bearer ${config.accessToken}`,
424
- 'Content-Type': 'application/json'
425
- },
426
- timeout: 30000
427
- });
428
- }
429
-
430
- private getItemPath(item: BoxItem): string {
431
- // Use the full path if available, otherwise construct from path_collection
432
- if ((item as any).fullPath) {
433
- return (item as any).fullPath;
434
- }
435
-
436
- if (item.path_collection && item.path_collection.entries) {
437
- const pathParts = item.path_collection.entries
438
- .filter(entry => entry.type === 'folder' && entry.id !== '0')
439
- .map(entry => entry.name);
440
- pathParts.push(item.name);
441
- return pathParts.join('/');
442
- }
443
-
444
- return item.name;
445
- }
446
-
447
- private getMimeType(fileName: string): string {
448
- const ext = path.extname(fileName).toLowerCase();
449
- const mimeTypes: Record<string, string> = {
450
- '.pdf': 'application/pdf',
451
- '.doc': 'application/msword',
452
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
453
- '.xls': 'application/vnd.ms-excel',
454
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
455
- '.ppt': 'application/vnd.ms-powerpoint',
456
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
457
- '.txt': 'text/plain',
458
- '.csv': 'text/csv',
459
- '.json': 'application/json',
460
- '.xml': 'application/xml',
461
- '.jpg': 'image/jpeg',
462
- '.jpeg': 'image/jpeg',
463
- '.png': 'image/png',
464
- '.gif': 'image/gif',
465
- '.zip': 'application/zip'
466
- };
467
-
468
- return mimeTypes[ext] || 'application/octet-stream';
469
- }
470
-
471
- private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
472
- // Apply size filter
473
- if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
474
- return false;
475
- }
476
-
477
- // Apply date range filter
478
- if (options?.filters?.dateRange) {
479
- const { from, to } = options.filters.dateRange;
480
- if (from && source.lastModified < from) return false;
481
- if (to && source.lastModified > to) return false;
482
- }
483
-
484
- // Apply MIME type filter
485
- if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
486
- return false;
487
- }
488
-
489
- return true;
490
- }
491
-
492
- private sleep(ms: number): Promise<void> {
493
- return new Promise(resolve => setTimeout(resolve, ms));
494
- }
495
- }
@@ -1,12 +0,0 @@
1
- {
2
- "name": "box",
3
- "version": "1.0.0",
4
- "description": "Box document source plugin",
5
- "author": "HubDoc Tools",
6
- "main": "index.ts",
7
- "hubdocToolVersion": "^1.0.0",
8
- "dependencies": {
9
- "box-node-sdk": "^2.0.0",
10
- "fs-extra": "^11.1.0"
11
- }
12
- }
@@ -1,122 +0,0 @@
1
- # Core Plugin
2
-
3
- Plugin pour importer des documents depuis le système Core vers HubDoc.
4
-
5
- ## Description
6
-
7
- Ce plugin permet d'importer des documents stockés dans un système Core (Document Management System) vers HubDoc. Il utilise l'API REST de Core pour lister et télécharger les documents.
8
-
9
- ## Fonctionnalités
10
-
11
- - **Scan** : Liste tous les documents disponibles dans Core avec pagination
12
- - **Import** : Télécharge les documents vers un buffer local puis les transfère vers HubDoc
13
- - **Authentification** : Support de l'authentification Bearer Token avec Core
14
- - **Filtrage** : Support des filtres par taille, date, et type MIME
15
- - **Métadonnées** : Préservation des métadonnées Core (tags, type, sous-type, etc.)
16
-
17
- ## Configuration
18
-
19
- Le plugin nécessite les paramètres suivants :
20
-
21
- ### Paramètres obligatoires
22
-
23
- - **baseUrl** : URL de base de l'API Core (ex: `https://your-core-instance.com`)
24
- - **username** : Nom d'utilisateur pour l'authentification Core
25
- - **password** : Mot de passe pour l'authentification Core
26
-
27
- ### Paramètres optionnels
28
-
29
- - **bearerToken** : Token Bearer pré-obtenu (optionnel, sera récupéré automatiquement si non fourni)
30
- - **tempDir** : Répertoire temporaire pour les fichiers buffer (par défaut : `{targetDir}/.temp`)
31
-
32
- ## Exemple de configuration
33
-
34
- ```json
35
- {
36
- "baseUrl": "https://my-core-instance.com",
37
- "username": "myuser",
38
- "password": "mypassword",
39
- "tempDir": "/tmp/core-import"
40
- }
41
- ```
42
-
43
- ## Stratégie d'import
44
-
45
- Le plugin utilise une stratégie de buffer local pour l'import :
46
-
47
- 1. **Scan** : Liste les documents via `/api/d2/docs` avec pagination
48
- 2. **Fetch** : Récupère le détail de chaque document via `/api/d2/docs/{id}`
49
- 3. **Buffer** : Crée un fichier temporaire local avec le contenu du document
50
- 4. **Transfer** : Déplace le fichier vers le répertoire cible avec la structure appropriée
51
- 5. **Cleanup** : Supprime les fichiers temporaires
52
-
53
- ## Structure des documents
54
-
55
- Les documents Core sont organisés selon cette structure :
56
- ```
57
- {type}/{subtype}/{document_title}.txt
58
- ```
59
-
60
- Exemples :
61
- - `document/general/My_Document.txt`
62
- - `html/report/Monthly_Report.txt`
63
- - `unknown/general/Document_123.txt`
64
-
65
- ## Métadonnées préservées
66
-
67
- Le plugin préserve les métadonnées suivantes de Core :
68
-
69
- - **coreId** : ID unique du document dans Core
70
- - **coreType** : Type de document Core
71
- - **coreSubtype** : Sous-type de document Core
72
- - **corePath** : Chemin original dans Core
73
- - **tags** : Tags associés au document
74
- - **hasAttachments** : Présence de pièces jointes
75
- - **signed** : Statut de signature
76
- - **percent** : Pourcentage de completion
77
- - **note** : Note attribuée
78
- - **body** : Extrait du contenu (500 premiers caractères)
79
-
80
- ## Limitations
81
-
82
- - **Export non supporté** : L'export vers Core n'est pas implémenté car les documents doivent être gérés dans Core même
83
- - **Contenu textuel uniquement** : Les documents sont extraits en format texte
84
- - **Pièces jointes** : Les attachments Core ne sont pas téléchargés directement
85
-
86
- ## API Core utilisée
87
-
88
- Le plugin utilise les endpoints suivants de l'API Core :
89
-
90
- - `POST /api/token` : Authentification et récupération du token
91
- - `GET /api/d2/docs` : Liste des documents avec pagination
92
- - `GET /api/d2/docs/{id}` : Détail d'un document spécifique
93
-
94
- ## Gestion des erreurs
95
-
96
- Le plugin gère les erreurs suivantes :
97
-
98
- - **Erreurs d'authentification** : Token invalide ou expiré
99
- - **Erreurs de pagination** : Pages manquantes ou inaccessibles
100
- - **Erreurs de téléchargement** : Documents inaccessibles
101
- - **Erreurs de filesystem** : Problèmes de buffer local
102
-
103
- ## Utilisation
104
-
105
- ```bash
106
- # Configuration du plugin
107
- hubdoc-tool connect core
108
-
109
- # Scan des documents Core
110
- hubdoc-tool plugin-scan core-connection
111
-
112
- # Import vers HubDoc
113
- hubdoc-tool plugin-import core-mapping.csv
114
- ```
115
-
116
- ## Exemple de mapping CSV généré
117
-
118
- ```csv
119
- File Path,Target Folder,Workspace,Metadata (JSON),Permissions
120
- document/general/My_Document.txt,Core Documents,Core Project,"{\"coreId\":\"123\",\"coreType\":\"document\"}",
121
- html/report/Monthly_Report.txt,Core Reports,Core Project,"{\"coreId\":\"456\",\"coreType\":\"html\"}",
122
- ```