@sinoia/hubdoc-tools 1.3.2 → 1.3.4

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.
Files changed (68) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/plugins/alfresco/plugin.json +12 -0
  3. package/dist/plugins/aws-s3/plugin.json +12 -0
  4. package/dist/plugins/azure-blob/plugin.json +12 -0
  5. package/dist/plugins/box/plugin.json +12 -0
  6. package/dist/plugins/core/index.d.ts +25 -0
  7. package/dist/plugins/core/index.d.ts.map +1 -0
  8. package/dist/plugins/core/index.js +400 -0
  9. package/dist/plugins/core/index.js.map +1 -0
  10. package/dist/plugins/core/plugin.json +26 -0
  11. package/dist/plugins/dropbox/plugin.json +12 -0
  12. package/dist/plugins/filesystem/index.d.ts +22 -0
  13. package/dist/plugins/filesystem/index.d.ts.map +1 -0
  14. package/dist/plugins/filesystem/index.js +306 -0
  15. package/dist/plugins/filesystem/index.js.map +1 -0
  16. package/dist/plugins/filesystem/plugin.json +12 -0
  17. package/dist/plugins/googledrive/plugin.json +12 -0
  18. package/dist/plugins/nuxeo/plugin.json +12 -0
  19. package/dist/plugins/onedrive/plugin.json +12 -0
  20. package/dist/plugins/opentext/plugin.json +12 -0
  21. package/dist/plugins/sharepoint/plugin.json +12 -0
  22. package/dist/services/hubdoc-api.d.ts +1 -1
  23. package/dist/services/hubdoc-api.js +1 -1
  24. package/dist/services/oauth-token-service.d.ts +1 -1
  25. package/dist/services/oauth-token-service.js +2 -2
  26. package/dist/services/permission-manager.d.ts +1 -1
  27. package/dist/services/permission-manager.js +1 -1
  28. package/dist/src/types/plugins.d.ts +111 -0
  29. package/dist/src/types/plugins.d.ts.map +1 -0
  30. package/dist/src/types/plugins.js +3 -0
  31. package/dist/src/types/plugins.js.map +1 -0
  32. package/dist/src/utils/concurrent-processor.d.ts +63 -0
  33. package/dist/src/utils/concurrent-processor.d.ts.map +1 -0
  34. package/dist/src/utils/concurrent-processor.js +240 -0
  35. package/dist/src/utils/concurrent-processor.js.map +1 -0
  36. package/dist/src/utils/xml-metadata.d.ts +47 -0
  37. package/dist/src/utils/xml-metadata.d.ts.map +1 -0
  38. package/dist/src/utils/xml-metadata.js +200 -0
  39. package/dist/src/utils/xml-metadata.js.map +1 -0
  40. package/dist/types/index.d.ts +1 -1
  41. package/package.json +6 -2
  42. package/plugins/alfresco/index.ts +518 -0
  43. package/plugins/alfresco/plugin.json +12 -0
  44. package/plugins/aws-s3/index.ts +471 -0
  45. package/plugins/aws-s3/plugin.json +12 -0
  46. package/plugins/azure-blob/index.ts +420 -0
  47. package/plugins/azure-blob/plugin.json +12 -0
  48. package/plugins/box/index.ts +495 -0
  49. package/plugins/box/plugin.json +12 -0
  50. package/plugins/core/README.md +122 -0
  51. package/plugins/core/TESTING.md +155 -0
  52. package/plugins/core/index.ts +510 -0
  53. package/plugins/core/plugin.json +26 -0
  54. package/plugins/dropbox/index.ts +451 -0
  55. package/plugins/dropbox/plugin.json +12 -0
  56. package/plugins/filesystem/index.ts +360 -0
  57. package/plugins/filesystem/plugin.json +12 -0
  58. package/plugins/googledrive/index.ts +463 -0
  59. package/plugins/googledrive/plugin.json +12 -0
  60. package/plugins/nuxeo/index.ts +512 -0
  61. package/plugins/nuxeo/plugin.json +12 -0
  62. package/plugins/onedrive/TESTING.md +197 -0
  63. package/plugins/onedrive/index.ts +447 -0
  64. package/plugins/onedrive/plugin.json +12 -0
  65. package/plugins/opentext/index.ts +542 -0
  66. package/plugins/opentext/plugin.json +12 -0
  67. package/plugins/sharepoint/index.ts +509 -0
  68. package/plugins/sharepoint/plugin.json +12 -0
@@ -0,0 +1,463 @@
1
+ import { google, drive_v3 } from 'googleapis';
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 GoogleDriveConfig extends PluginConfig {
16
+ clientId: string;
17
+ clientSecret: string;
18
+ redirectUri: string;
19
+ accessToken?: string;
20
+ refreshToken?: string;
21
+ rootFolderId?: string; // Specific folder ID to scan, defaults to 'root'
22
+ }
23
+
24
+ export default class GoogleDrivePlugin implements DocumentSourcePlugin {
25
+ readonly name = 'googledrive';
26
+ readonly version = '1.0.0';
27
+ readonly description = 'Google Drive document source';
28
+ readonly supportedOperations = ['import', 'export', 'both'] as const;
29
+
30
+ private config?: GoogleDriveConfig;
31
+ private drive?: drive_v3.Drive;
32
+ private auth?: any;
33
+
34
+ async testConnection(config: PluginConfig): Promise<boolean> {
35
+ try {
36
+ const driveConfig = config as GoogleDriveConfig;
37
+ const auth = await this.createAuth(driveConfig);
38
+ const drive = google.drive({ version: 'v3', auth });
39
+
40
+ const response = await drive.about.get({ fields: 'user' });
41
+ return !!response.data.user;
42
+ } catch (error: any) {
43
+ console.error(`Google Drive connection test failed: ${error.message}`);
44
+ return false;
45
+ }
46
+ }
47
+
48
+ async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
49
+ this.config = config as GoogleDriveConfig;
50
+ this.auth = await this.createAuth(this.config);
51
+ this.drive = google.drive({ version: 'v3', auth: this.auth });
52
+
53
+ const sources: DocumentSource[] = [];
54
+ const errors: string[] = [];
55
+ let totalSize = 0;
56
+
57
+ try {
58
+ const rootFolderId = this.config.rootFolderId || 'root';
59
+ const files = await this.scanFolder(rootFolderId, '', options);
60
+
61
+ for (const file of files) {
62
+ if (file.mimeType && !file.mimeType.includes('folder')) {
63
+ const source: DocumentSource = {
64
+ id: file.id!,
65
+ name: file.name!,
66
+ path: (file as any).path!,
67
+ size: parseInt(file.size || '0'),
68
+ mimeType: file.mimeType,
69
+ lastModified: new Date(file.modifiedTime!),
70
+ metadata: {
71
+ googleDriveId: file.id,
72
+ parents: file.parents,
73
+ webViewLink: file.webViewLink
74
+ }
75
+ };
76
+
77
+ // Apply filters
78
+ if (this.shouldIncludeSource(source, options)) {
79
+ sources.push(source);
80
+ totalSize += source.size;
81
+ }
82
+ }
83
+ }
84
+
85
+ return {
86
+ sources,
87
+ totalCount: sources.length,
88
+ totalSize,
89
+ errors
90
+ };
91
+ } catch (error: any) {
92
+ return {
93
+ sources: [],
94
+ totalCount: 0,
95
+ totalSize: 0,
96
+ errors: [`Google Drive scan failed: ${error.message}`]
97
+ };
98
+ }
99
+ }
100
+
101
+ private async scanFolder(folderId: string, parentPath: string, options?: PluginImportOptions): Promise<any[]> {
102
+ if (!this.drive) throw new Error('Drive client not initialized');
103
+
104
+ const allFiles: any[] = [];
105
+ let pageToken: string | undefined;
106
+
107
+ do {
108
+ try {
109
+ const response = await this.drive.files.list({
110
+ q: `'${folderId}' in parents and trashed=false`,
111
+ fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, parents, webViewLink)',
112
+ pageSize: 1000,
113
+ pageToken
114
+ });
115
+
116
+ const files = response.data.files || [];
117
+
118
+ for (const file of files) {
119
+ const filePath = parentPath ? `${parentPath}/${file.name}` : file.name!;
120
+ (file as any).path = filePath;
121
+
122
+ if (file.mimeType === 'application/vnd.google-apps.folder') {
123
+ // Recursively scan subfolders
124
+ const subFiles = await this.scanFolder(file.id!, filePath, options);
125
+ allFiles.push(...subFiles);
126
+ } else {
127
+ allFiles.push(file);
128
+ }
129
+ }
130
+
131
+ pageToken = response.data.nextPageToken || undefined;
132
+ } catch (error: any) {
133
+ console.warn(`Warning: Failed to scan folder ${folderId}: ${error.message}`);
134
+ break;
135
+ }
136
+ } while (pageToken);
137
+
138
+ return allFiles;
139
+ }
140
+
141
+ async import(
142
+ config: PluginConfig,
143
+ sources: DocumentSource[],
144
+ targetDir: string,
145
+ options?: PluginImportOptions
146
+ ): Promise<ImportResult[]> {
147
+ this.config = config as GoogleDriveConfig;
148
+ this.auth = await this.createAuth(this.config);
149
+ this.drive = google.drive({ version: 'v3', auth: this.auth });
150
+
151
+ const results: ImportResult[] = [];
152
+ const batchSize = options?.batchSize || 5; // Google Drive has rate limits
153
+
154
+ // Process in batches
155
+ for (let i = 0; i < sources.length; i += batchSize) {
156
+ const batch = sources.slice(i, i + batchSize);
157
+
158
+ for (const source of batch) {
159
+ const result = await this.importSingle(source, targetDir);
160
+ results.push(result);
161
+
162
+ // Small delay to respect rate limits
163
+ await this.sleep(200);
164
+ }
165
+ }
166
+
167
+ return results;
168
+ }
169
+
170
+ private async importSingle(source: DocumentSource, targetDir: string): Promise<ImportResult> {
171
+ try {
172
+ if (!this.drive) throw new Error('Drive client not initialized');
173
+
174
+ const targetPath = path.join(targetDir, source.path);
175
+ const targetDirectory = path.dirname(targetPath);
176
+
177
+ await fs.ensureDir(targetDirectory);
178
+
179
+ // Handle Google Workspace documents (export to common formats)
180
+ if (this.isGoogleWorkspaceDoc(source.mimeType)) {
181
+ const exportMimeType = this.getExportMimeType(source.mimeType);
182
+ const exportExt = this.getExportExtension(exportMimeType);
183
+ const exportPath = targetPath.replace(path.extname(targetPath), exportExt);
184
+
185
+ const response = await this.drive.files.export({
186
+ fileId: source.id,
187
+ mimeType: exportMimeType
188
+ }, { responseType: 'stream' });
189
+
190
+ const writer = fs.createWriteStream(exportPath);
191
+ response.data.pipe(writer);
192
+
193
+ return new Promise((resolve) => {
194
+ writer.on('finish', async () => {
195
+ const stats = await fs.stat(exportPath);
196
+ resolve({
197
+ success: true,
198
+ source,
199
+ localPath: exportPath,
200
+ bytesTransferred: stats.size
201
+ });
202
+ });
203
+
204
+ writer.on('error', (error) => {
205
+ resolve({
206
+ success: false,
207
+ source,
208
+ error: error.message
209
+ });
210
+ });
211
+ });
212
+ } else {
213
+ // Regular file download
214
+ const response = await this.drive.files.get({
215
+ fileId: source.id,
216
+ alt: 'media'
217
+ }, { responseType: 'stream' });
218
+
219
+ const writer = fs.createWriteStream(targetPath);
220
+ response.data.pipe(writer);
221
+
222
+ return new Promise((resolve) => {
223
+ writer.on('finish', () => {
224
+ resolve({
225
+ success: true,
226
+ source,
227
+ localPath: targetPath,
228
+ bytesTransferred: source.size
229
+ });
230
+ });
231
+
232
+ writer.on('error', (error) => {
233
+ resolve({
234
+ success: false,
235
+ source,
236
+ error: error.message
237
+ });
238
+ });
239
+ });
240
+ }
241
+ } catch (error: any) {
242
+ return {
243
+ success: false,
244
+ source,
245
+ error: error.message
246
+ };
247
+ }
248
+ }
249
+
250
+ async export?(
251
+ config: PluginConfig,
252
+ localSources: DocumentSource[],
253
+ options?: PluginExportOptions
254
+ ): Promise<ExportResult[]> {
255
+ this.config = config as GoogleDriveConfig;
256
+ this.auth = await this.createAuth(this.config);
257
+ this.drive = google.drive({ version: 'v3', auth: this.auth });
258
+
259
+ const results: ExportResult[] = [];
260
+ const rootFolderId = this.config.rootFolderId || 'root';
261
+
262
+ for (const source of localSources) {
263
+ try {
264
+ // Determine target folder
265
+ let targetFolderId = rootFolderId;
266
+
267
+ if (options?.preserveStructure && source.path.includes('/')) {
268
+ const folderPath = path.dirname(source.path);
269
+ targetFolderId = await this.createFolderStructure(folderPath, rootFolderId);
270
+ }
271
+
272
+ // Upload file
273
+ const fileContent = await fs.readFile(source.id);
274
+
275
+ const response = await this.drive!.files.create({
276
+ requestBody: {
277
+ name: source.name,
278
+ parents: [targetFolderId]
279
+ },
280
+ media: {
281
+ mimeType: source.mimeType,
282
+ body: fileContent
283
+ }
284
+ });
285
+
286
+ const targetPath = options?.preserveStructure ? source.path : source.name;
287
+
288
+ results.push({
289
+ success: true,
290
+ targetPath,
291
+ source,
292
+ bytesTransferred: source.size
293
+ });
294
+ } catch (error: any) {
295
+ results.push({
296
+ success: false,
297
+ targetPath: options?.targetPath || '',
298
+ source,
299
+ error: error.message
300
+ });
301
+ }
302
+ }
303
+
304
+ return results;
305
+ }
306
+
307
+ private async createFolderStructure(folderPath: string, parentId: string): Promise<string> {
308
+ if (!this.drive) throw new Error('Drive client not initialized');
309
+
310
+ const parts = folderPath.split('/').filter(part => part.length > 0);
311
+ let currentParentId = parentId;
312
+
313
+ for (const folderName of parts) {
314
+ // Check if folder already exists
315
+ const existingFolder = await this.drive.files.list({
316
+ q: `name='${folderName}' and '${currentParentId}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false`,
317
+ fields: 'files(id)'
318
+ });
319
+
320
+ if (existingFolder.data.files && existingFolder.data.files.length > 0) {
321
+ currentParentId = existingFolder.data.files[0].id!;
322
+ } else {
323
+ // Create new folder
324
+ const newFolder = await this.drive.files.create({
325
+ requestBody: {
326
+ name: folderName,
327
+ mimeType: 'application/vnd.google-apps.folder',
328
+ parents: [currentParentId]
329
+ }
330
+ });
331
+ currentParentId = newFolder.data.id!;
332
+ }
333
+ }
334
+
335
+ return currentParentId;
336
+ }
337
+
338
+ getConfigSchema(): Record<string, any> {
339
+ return {
340
+ type: 'object',
341
+ properties: {
342
+ clientId: {
343
+ type: 'string',
344
+ description: 'Google OAuth2 Client ID',
345
+ required: true
346
+ },
347
+ clientSecret: {
348
+ type: 'string',
349
+ description: 'Google OAuth2 Client Secret',
350
+ required: true
351
+ },
352
+ redirectUri: {
353
+ type: 'string',
354
+ description: 'OAuth2 Redirect URI',
355
+ required: true
356
+ },
357
+ accessToken: {
358
+ type: 'string',
359
+ description: 'OAuth2 Access Token (will be obtained automatically)',
360
+ required: false
361
+ },
362
+ refreshToken: {
363
+ type: 'string',
364
+ description: 'OAuth2 Refresh Token (will be obtained automatically)',
365
+ required: false
366
+ },
367
+ rootFolderId: {
368
+ type: 'string',
369
+ description: 'Root folder ID to scan (defaults to root)',
370
+ default: 'root'
371
+ }
372
+ },
373
+ required: ['clientId', 'clientSecret', 'redirectUri']
374
+ };
375
+ }
376
+
377
+ async initialize(config: PluginConfig): Promise<void> {
378
+ this.config = config as GoogleDriveConfig;
379
+
380
+ if (!this.config.accessToken) {
381
+ throw new Error('Google Drive authentication required. Please run authentication flow first.');
382
+ }
383
+
384
+ this.auth = await this.createAuth(this.config);
385
+ }
386
+
387
+ async destroy(): Promise<void> {
388
+ this.config = undefined;
389
+ this.drive = undefined;
390
+ this.auth = undefined;
391
+ }
392
+
393
+ private async createAuth(config: GoogleDriveConfig) {
394
+ const auth = new google.auth.OAuth2(
395
+ config.clientId,
396
+ config.clientSecret,
397
+ config.redirectUri
398
+ );
399
+
400
+ if (config.accessToken) {
401
+ auth.setCredentials({
402
+ access_token: config.accessToken,
403
+ refresh_token: config.refreshToken
404
+ });
405
+ }
406
+
407
+ return auth;
408
+ }
409
+
410
+ private isGoogleWorkspaceDoc(mimeType: string): boolean {
411
+ return [
412
+ 'application/vnd.google-apps.document',
413
+ 'application/vnd.google-apps.spreadsheet',
414
+ 'application/vnd.google-apps.presentation'
415
+ ].includes(mimeType);
416
+ }
417
+
418
+ private getExportMimeType(googleMimeType: string): string {
419
+ const exportMap: Record<string, string> = {
420
+ 'application/vnd.google-apps.document': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
421
+ 'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
422
+ 'application/vnd.google-apps.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
423
+ };
424
+
425
+ return exportMap[googleMimeType] || 'application/pdf';
426
+ }
427
+
428
+ private getExportExtension(mimeType: string): string {
429
+ const extensionMap: Record<string, string> = {
430
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
431
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
432
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
433
+ 'application/pdf': '.pdf'
434
+ };
435
+
436
+ return extensionMap[mimeType] || '.pdf';
437
+ }
438
+
439
+ private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
440
+ // Apply size filter
441
+ if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
442
+ return false;
443
+ }
444
+
445
+ // Apply date range filter
446
+ if (options?.filters?.dateRange) {
447
+ const { from, to } = options.filters.dateRange;
448
+ if (from && source.lastModified < from) return false;
449
+ if (to && source.lastModified > to) return false;
450
+ }
451
+
452
+ // Apply MIME type filter
453
+ if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
454
+ return false;
455
+ }
456
+
457
+ return true;
458
+ }
459
+
460
+ private sleep(ms: number): Promise<void> {
461
+ return new Promise(resolve => setTimeout(resolve, ms));
462
+ }
463
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "googledrive",
3
+ "version": "1.0.0",
4
+ "description": "Google Drive document source plugin",
5
+ "author": "HubDoc Tools",
6
+ "main": "index.js",
7
+ "hubdocToolVersion": "^1.0.0",
8
+ "dependencies": {
9
+ "googleapis": "^128.0.0",
10
+ "fs-extra": "^11.1.0"
11
+ }
12
+ }