@sinoia/hubdoc-tools 1.3.1 → 1.3.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.
Files changed (133) hide show
  1. package/dist/api/api/chunked-uploads-api.d.ts +214 -0
  2. package/dist/api/api/chunked-uploads-api.d.ts.map +1 -0
  3. package/dist/api/api/chunked-uploads-api.js +420 -0
  4. package/dist/api/api/chunked-uploads-api.js.map +1 -0
  5. package/dist/api/api.d.ts +1 -0
  6. package/dist/api/api.d.ts.map +1 -1
  7. package/dist/api/api.js +1 -0
  8. package/dist/api/api.js.map +1 -1
  9. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response-data.d.ts +19 -0
  10. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response-data.d.ts.map +1 -0
  11. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response-data.js +20 -0
  12. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response-data.js.map +1 -0
  13. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response.d.ts +17 -0
  14. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response.d.ts.map +1 -0
  15. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response.js +16 -0
  16. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete200-response.js.map +1 -0
  17. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete422-response.d.ts +16 -0
  18. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete422-response.d.ts.map +1 -0
  19. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete422-response.js +16 -0
  20. package/dist/api/models/api-v1-documents-chunked-uploads-id-cancel-delete422-response.js.map +1 -0
  21. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch200-response.d.ts +17 -0
  22. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch200-response.d.ts.map +1 -0
  23. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch200-response.js +16 -0
  24. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch200-response.js.map +1 -0
  25. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch400-response.d.ts +16 -0
  26. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch400-response.d.ts.map +1 -0
  27. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch400-response.js +16 -0
  28. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch400-response.js.map +1 -0
  29. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch410-response.d.ts +16 -0
  30. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch410-response.d.ts.map +1 -0
  31. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch410-response.js +16 -0
  32. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch410-response.js.map +1 -0
  33. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch422-response.d.ts +16 -0
  34. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch422-response.d.ts.map +1 -0
  35. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch422-response.js +16 -0
  36. package/dist/api/models/api-v1-documents-chunked-uploads-id-chunks-chunk-number-patch422-response.js.map +1 -0
  37. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post200-response.d.ts +17 -0
  38. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post200-response.d.ts.map +1 -0
  39. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post200-response.js +16 -0
  40. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post200-response.js.map +1 -0
  41. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post422-response.d.ts +16 -0
  42. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post422-response.d.ts.map +1 -0
  43. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post422-response.js +16 -0
  44. package/dist/api/models/api-v1-documents-chunked-uploads-id-complete-post422-response.js.map +1 -0
  45. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get200-response.d.ts +17 -0
  46. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get200-response.d.ts.map +1 -0
  47. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get200-response.js +16 -0
  48. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get200-response.js.map +1 -0
  49. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get404-response.d.ts +16 -0
  50. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get404-response.d.ts.map +1 -0
  51. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get404-response.js +16 -0
  52. package/dist/api/models/api-v1-documents-chunked-uploads-id-status-get404-response.js.map +1 -0
  53. package/dist/api/models/api-v1-documents-chunked-uploads-post201-response.d.ts +17 -0
  54. package/dist/api/models/api-v1-documents-chunked-uploads-post201-response.d.ts.map +1 -0
  55. package/dist/api/models/api-v1-documents-chunked-uploads-post201-response.js +16 -0
  56. package/dist/api/models/api-v1-documents-chunked-uploads-post201-response.js.map +1 -0
  57. package/dist/api/models/chunked-upload-chunk-response.d.ts +46 -0
  58. package/dist/api/models/chunked-upload-chunk-response.d.ts.map +1 -0
  59. package/dist/api/models/chunked-upload-chunk-response.js +21 -0
  60. package/dist/api/models/chunked-upload-chunk-response.js.map +1 -0
  61. package/dist/api/models/chunked-upload-complete-request.d.ts +21 -0
  62. package/dist/api/models/chunked-upload-complete-request.d.ts.map +1 -0
  63. package/dist/api/models/chunked-upload-complete-request.js +16 -0
  64. package/dist/api/models/chunked-upload-complete-request.js.map +1 -0
  65. package/dist/api/models/chunked-upload-complete-response.d.ts +37 -0
  66. package/dist/api/models/chunked-upload-complete-response.d.ts.map +1 -0
  67. package/dist/api/models/chunked-upload-complete-response.js +20 -0
  68. package/dist/api/models/chunked-upload-complete-response.js.map +1 -0
  69. package/dist/api/models/chunked-upload-mutation.d.ts +47 -0
  70. package/dist/api/models/chunked-upload-mutation.d.ts.map +1 -0
  71. package/dist/api/models/chunked-upload-mutation.js +16 -0
  72. package/dist/api/models/chunked-upload-mutation.js.map +1 -0
  73. package/dist/api/models/chunked-upload-session-response.d.ts +33 -0
  74. package/dist/api/models/chunked-upload-session-response.d.ts.map +1 -0
  75. package/dist/api/models/chunked-upload-session-response.js +16 -0
  76. package/dist/api/models/chunked-upload-session-response.js.map +1 -0
  77. package/dist/api/models/chunked-upload-status-response.d.ts +54 -0
  78. package/dist/api/models/chunked-upload-status-response.d.ts.map +1 -0
  79. package/dist/api/models/chunked-upload-status-response.js +25 -0
  80. package/dist/api/models/chunked-upload-status-response.js.map +1 -0
  81. package/dist/api/models/chunked-upload.d.ts +98 -0
  82. package/dist/api/models/chunked-upload.d.ts.map +1 -0
  83. package/dist/api/models/chunked-upload.js +25 -0
  84. package/dist/api/models/chunked-upload.js.map +1 -0
  85. package/dist/api/models/index.d.ts +19 -0
  86. package/dist/api/models/index.d.ts.map +1 -1
  87. package/dist/api/models/index.js +19 -0
  88. package/dist/api/models/index.js.map +1 -1
  89. package/dist/commands/import.d.ts.map +1 -1
  90. package/dist/commands/import.js +7 -3
  91. package/dist/commands/import.js.map +1 -1
  92. package/dist/services/chunked-uploader.d.ts +83 -0
  93. package/dist/services/chunked-uploader.d.ts.map +1 -0
  94. package/dist/services/chunked-uploader.js +321 -0
  95. package/dist/services/chunked-uploader.js.map +1 -0
  96. package/dist/services/hubdoc-api.d.ts +5 -2
  97. package/dist/services/hubdoc-api.d.ts.map +1 -1
  98. package/dist/services/hubdoc-api.js +49 -12
  99. package/dist/services/hubdoc-api.js.map +1 -1
  100. package/dist/types/index.d.ts +1 -0
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/dist/utils/csv.d.ts +6 -1
  103. package/dist/utils/csv.d.ts.map +1 -1
  104. package/dist/utils/csv.js +30 -1
  105. package/dist/utils/csv.js.map +1 -1
  106. package/package.json +2 -1
  107. package/plugins/alfresco/index.ts +518 -0
  108. package/plugins/alfresco/plugin.json +12 -0
  109. package/plugins/aws-s3/index.ts +471 -0
  110. package/plugins/aws-s3/plugin.json +12 -0
  111. package/plugins/azure-blob/index.ts +420 -0
  112. package/plugins/azure-blob/plugin.json +12 -0
  113. package/plugins/box/index.ts +495 -0
  114. package/plugins/box/plugin.json +12 -0
  115. package/plugins/core/README.md +122 -0
  116. package/plugins/core/TESTING.md +155 -0
  117. package/plugins/core/index.ts +510 -0
  118. package/plugins/core/plugin.json +26 -0
  119. package/plugins/dropbox/index.ts +451 -0
  120. package/plugins/dropbox/plugin.json +12 -0
  121. package/plugins/filesystem/index.ts +360 -0
  122. package/plugins/filesystem/plugin.json +12 -0
  123. package/plugins/googledrive/index.ts +463 -0
  124. package/plugins/googledrive/plugin.json +12 -0
  125. package/plugins/nuxeo/index.ts +512 -0
  126. package/plugins/nuxeo/plugin.json +12 -0
  127. package/plugins/onedrive/TESTING.md +197 -0
  128. package/plugins/onedrive/index.ts +447 -0
  129. package/plugins/onedrive/plugin.json +12 -0
  130. package/plugins/opentext/index.ts +542 -0
  131. package/plugins/opentext/plugin.json +12 -0
  132. package/plugins/sharepoint/index.ts +509 -0
  133. package/plugins/sharepoint/plugin.json +12 -0
@@ -0,0 +1,542 @@
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 OpenTextConfig extends PluginConfig {
16
+ baseUrl: string;
17
+ username: string;
18
+ password: string;
19
+ ticket?: string; // Alternative authentication using OTDS ticket
20
+ startingFolderId?: number; // Starting folder ID, defaults to Enterprise workspace (2000)
21
+ limit?: number;
22
+ }
23
+
24
+ interface OpenTextNode {
25
+ id: number;
26
+ name: string;
27
+ type: number;
28
+ type_name: string;
29
+ container: boolean;
30
+ size?: number;
31
+ create_date: string;
32
+ modify_date: string;
33
+ created_by: number;
34
+ created_by_name: string;
35
+ modified_by: number;
36
+ modified_by_name: string;
37
+ parent_id: number;
38
+ mime_type?: string;
39
+ version_number?: number;
40
+ description?: string;
41
+ reserved?: boolean;
42
+ reserved_by?: number;
43
+ reserved_date?: string;
44
+ }
45
+
46
+ export default class OpenTextPlugin implements DocumentSourcePlugin {
47
+ readonly name = 'opentext';
48
+ readonly version = '1.0.0';
49
+ readonly description = 'OpenText Content Server document source';
50
+ readonly supportedOperations = ['import', 'export', 'both'] as const;
51
+
52
+ private config?: OpenTextConfig;
53
+ private apiClient?: AxiosInstance;
54
+ private authToken?: string;
55
+
56
+ async testConnection(config: PluginConfig): Promise<boolean> {
57
+ try {
58
+ const client = await this.createApiClient(config as OpenTextConfig);
59
+ const response = await client.get('/nodes/2000'); // Test access to Enterprise workspace
60
+ return response.status === 200;
61
+ } catch (error: any) {
62
+ console.error(`OpenText connection test failed: ${error.message}`);
63
+ return false;
64
+ }
65
+ }
66
+
67
+ async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
68
+ this.config = config as OpenTextConfig;
69
+ this.apiClient = await this.createApiClient(this.config);
70
+
71
+ const sources: DocumentSource[] = [];
72
+ const errors: string[] = [];
73
+ let totalSize = 0;
74
+
75
+ try {
76
+ const limit = (this.config as any).limit || (options as any)?.limit;
77
+ console.log(`🔍 Scanning OpenText Content Server${limit ? ` (limit: ${limit})` : ''}...`);
78
+
79
+ const startingFolderId = this.config.startingFolderId || 2000; // Enterprise workspace
80
+ const nodes = await this.scanNode(startingFolderId);
81
+
82
+ let processedCount = 0;
83
+ for (const node of nodes) {
84
+ if (!node.container && node.type === 144) { // Type 144 is Document
85
+ const source: DocumentSource = {
86
+ id: node.id.toString(),
87
+ name: node.name,
88
+ path: await this.getNodePath(node.id),
89
+ size: node.size || 0,
90
+ mimeType: node.mime_type || this.getMimeType(node.name),
91
+ lastModified: new Date(node.modify_date),
92
+ metadata: {
93
+ opentextId: node.id,
94
+ nodeType: node.type,
95
+ typeName: node.type_name,
96
+ createdAt: node.create_date,
97
+ createdBy: node.created_by_name,
98
+ modifiedBy: node.modified_by_name,
99
+ parentId: node.parent_id,
100
+ versionNumber: node.version_number,
101
+ description: node.description,
102
+ reserved: node.reserved,
103
+ reservedBy: node.reserved_by,
104
+ reservedDate: node.reserved_date
105
+ }
106
+ };
107
+
108
+ // Apply filters
109
+ if (this.shouldIncludeSource(source, options)) {
110
+ sources.push(source);
111
+ totalSize += source.size;
112
+ processedCount++;
113
+
114
+ // Check limit
115
+ if (limit && processedCount >= limit) {
116
+ console.log(`📏 Reached limit of ${limit} files`);
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ return {
124
+ sources,
125
+ totalCount: sources.length,
126
+ totalSize,
127
+ errors
128
+ };
129
+ } catch (error: any) {
130
+ return {
131
+ sources: [],
132
+ totalCount: 0,
133
+ totalSize: 0,
134
+ errors: [`OpenText scan failed: ${error.message}`]
135
+ };
136
+ }
137
+ }
138
+
139
+ private async scanNode(nodeId: number, parentPath: string = ''): Promise<OpenTextNode[]> {
140
+ if (!this.apiClient) throw new Error('API client not initialized');
141
+
142
+ const allNodes: OpenTextNode[] = [];
143
+ let page = 1;
144
+ const limit = 100;
145
+
146
+ try {
147
+ while (true) {
148
+ const response = await this.apiClient.get(`/nodes/${nodeId}/nodes`, {
149
+ params: {
150
+ limit: limit,
151
+ page: page,
152
+ expand: 'properties{original_id,parent_id,name,type,type_name,container,size,create_date,modify_date,created_by,created_by_name,modified_by,modified_by_name,mime_type,version_number,description,reserved,reserved_by,reserved_date}'
153
+ }
154
+ });
155
+
156
+ const nodes: OpenTextNode[] = response.data.results || [];
157
+
158
+ if (nodes.length === 0) {
159
+ break;
160
+ }
161
+
162
+ for (const node of nodes) {
163
+ const nodePath = parentPath ? `${parentPath}/${node.name}` : node.name;
164
+ (node as any).fullPath = nodePath;
165
+
166
+ if (!node.container) {
167
+ // It's a document
168
+ allNodes.push(node);
169
+ } else {
170
+ // It's a folder, recursively scan it
171
+ const subNodes = await this.scanNode(node.id, nodePath);
172
+ allNodes.push(...subNodes);
173
+ }
174
+ }
175
+
176
+ // Check if there are more pages
177
+ if (nodes.length < limit) {
178
+ break;
179
+ }
180
+ page++;
181
+ }
182
+
183
+ return allNodes;
184
+ } catch (error: any) {
185
+ if (error.response?.status === 404) {
186
+ console.warn(`Warning: Node not found: ${nodeId}`);
187
+ return [];
188
+ }
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ async import(
194
+ config: PluginConfig,
195
+ sources: DocumentSource[],
196
+ targetDir: string,
197
+ options?: PluginImportOptions
198
+ ): Promise<ImportResult[]> {
199
+ this.config = config as OpenTextConfig;
200
+ this.apiClient = await this.createApiClient(this.config);
201
+
202
+ const results: ImportResult[] = [];
203
+ const batchSize = options?.batchSize || 5;
204
+
205
+ // Process in batches to respect API limits
206
+ for (let i = 0; i < sources.length; i += batchSize) {
207
+ const batch = sources.slice(i, i + batchSize);
208
+
209
+ for (const source of batch) {
210
+ const result = await this.importSingle(source, targetDir);
211
+ results.push(result);
212
+
213
+ // Small delay to respect rate limits
214
+ await this.sleep(300);
215
+ }
216
+ }
217
+
218
+ return results;
219
+ }
220
+
221
+ private async importSingle(source: DocumentSource, targetDir: string): Promise<ImportResult> {
222
+ try {
223
+ if (!this.apiClient) throw new Error('API client not initialized');
224
+
225
+ const targetPath = path.join(targetDir, source.path);
226
+ const targetDirectory = path.dirname(targetPath);
227
+
228
+ await fs.ensureDir(targetDirectory);
229
+
230
+ // Download file content from OpenText
231
+ const response = await this.apiClient.get(`/nodes/${source.id}/content`, {
232
+ responseType: 'stream'
233
+ });
234
+
235
+ const writer = fs.createWriteStream(targetPath);
236
+ response.data.pipe(writer);
237
+
238
+ return new Promise((resolve) => {
239
+ writer.on('finish', () => {
240
+ resolve({
241
+ success: true,
242
+ source,
243
+ localPath: targetPath,
244
+ bytesTransferred: source.size
245
+ });
246
+ });
247
+
248
+ writer.on('error', (error) => {
249
+ resolve({
250
+ success: false,
251
+ source,
252
+ error: error.message
253
+ });
254
+ });
255
+ });
256
+ } catch (error: any) {
257
+ return {
258
+ success: false,
259
+ source,
260
+ error: error.message
261
+ };
262
+ }
263
+ }
264
+
265
+ async export?(
266
+ config: PluginConfig,
267
+ localSources: DocumentSource[],
268
+ options?: PluginExportOptions
269
+ ): Promise<ExportResult[]> {
270
+ this.config = config as OpenTextConfig;
271
+ this.apiClient = await this.createApiClient(this.config);
272
+
273
+ const results: ExportResult[] = [];
274
+ const rootFolderId = this.config.startingFolderId || 2000;
275
+
276
+ for (const source of localSources) {
277
+ try {
278
+ // Determine target folder
279
+ let targetFolderId = rootFolderId;
280
+
281
+ if (options?.preserveStructure && source.path.includes('/')) {
282
+ const folderPath = path.dirname(source.path);
283
+ targetFolderId = await this.createFolderStructure(folderPath, rootFolderId);
284
+ }
285
+
286
+ // Read local file
287
+ const fileContent = await fs.readFile(source.id);
288
+ // Normalize filename to NFC to handle accented characters consistently across platforms
289
+ const fileName = (options?.preserveStructure ? path.basename(source.path) : source.name).normalize('NFC');
290
+
291
+ // Upload file to OpenText using multipart form data
292
+ const FormData = require('form-data');
293
+ const form = new FormData();
294
+
295
+ form.append('type', '144'); // Document type
296
+ form.append('name', fileName);
297
+ form.append('description', `Uploaded via hubdoc-tools`);
298
+ form.append('file', fileContent, fileName);
299
+
300
+ const response = await this.apiClient!.post(`/nodes/${targetFolderId}/nodes`, form, {
301
+ headers: {
302
+ ...form.getHeaders()
303
+ }
304
+ });
305
+
306
+ const targetPath = options?.preserveStructure ? source.path : source.name;
307
+
308
+ results.push({
309
+ success: true,
310
+ targetPath,
311
+ source,
312
+ bytesTransferred: source.size
313
+ });
314
+ } catch (error: any) {
315
+ results.push({
316
+ success: false,
317
+ targetPath: options?.targetPath || '',
318
+ source,
319
+ error: error.message
320
+ });
321
+ }
322
+ }
323
+
324
+ return results;
325
+ }
326
+
327
+ private async createFolderStructure(folderPath: string, parentId: number): Promise<number> {
328
+ if (!this.apiClient) throw new Error('API client not initialized');
329
+
330
+ const parts = folderPath.split('/').filter(part => part.length > 0);
331
+ let currentParentId = parentId;
332
+
333
+ for (const folderName of parts) {
334
+ try {
335
+ // Check if folder already exists
336
+ const response = await this.apiClient.get(`/nodes/${currentParentId}/nodes`, {
337
+ params: {
338
+ where_name: folderName,
339
+ where_type: 0, // Folder type
340
+ limit: 1
341
+ }
342
+ });
343
+
344
+ const existingFolder = response.data.results?.[0];
345
+
346
+ if (existingFolder) {
347
+ currentParentId = existingFolder.id;
348
+ } else {
349
+ // Create new folder
350
+ const createResponse = await this.apiClient.post(`/nodes/${currentParentId}/nodes`, {
351
+ type: 0, // Folder type
352
+ name: folderName,
353
+ description: `Created by hubdoc-tools`
354
+ });
355
+ currentParentId = createResponse.data.id;
356
+ }
357
+ } catch (error: any) {
358
+ throw new Error(`Failed to create folder structure: ${error.message}`);
359
+ }
360
+ }
361
+
362
+ return currentParentId;
363
+ }
364
+
365
+ private async getNodePath(nodeId: number): Promise<string> {
366
+ if (!this.apiClient) throw new Error('API client not initialized');
367
+
368
+ try {
369
+ // Get the node's ancestors to build the full path
370
+ const response = await this.apiClient.get(`/nodes/${nodeId}/ancestors`);
371
+ const ancestors = response.data.ancestors || [];
372
+
373
+ // Filter out the Enterprise workspace and build path
374
+ const pathParts = ancestors
375
+ .filter((ancestor: any) => ancestor.id !== 2000) // Skip Enterprise workspace
376
+ .map((ancestor: any) => ancestor.name);
377
+
378
+ // Get the node itself to add its name
379
+ const nodeResponse = await this.apiClient.get(`/nodes/${nodeId}`);
380
+ const nodeName = nodeResponse.data.results.name;
381
+
382
+ pathParts.push(nodeName);
383
+
384
+ return pathParts.join('/');
385
+ } catch (error: any) {
386
+ // Fallback to just the node ID if path resolution fails
387
+ return `node_${nodeId}`;
388
+ }
389
+ }
390
+
391
+ getConfigSchema(): Record<string, any> {
392
+ return {
393
+ type: 'object',
394
+ properties: {
395
+ baseUrl: {
396
+ type: 'string',
397
+ description: 'OpenText Content Server base URL (e.g., http://localhost/otcs/cs.exe/api/v1)',
398
+ required: true
399
+ },
400
+ username: {
401
+ type: 'string',
402
+ description: 'OpenText username',
403
+ required: false
404
+ },
405
+ password: {
406
+ type: 'string',
407
+ description: 'OpenText password',
408
+ required: false
409
+ },
410
+ ticket: {
411
+ type: 'string',
412
+ description: 'OTDS authentication ticket (alternative to username/password)',
413
+ required: false
414
+ },
415
+ startingFolderId: {
416
+ type: 'number',
417
+ description: 'Starting folder ID (default: 2000 for Enterprise workspace)',
418
+ default: 2000
419
+ },
420
+ limit: {
421
+ type: 'number',
422
+ description: 'Maximum number of documents to scan (useful for testing)',
423
+ required: false
424
+ }
425
+ },
426
+ required: ['baseUrl'],
427
+ oneOf: [
428
+ { required: ['baseUrl', 'username', 'password'] },
429
+ { required: ['baseUrl', 'ticket'] }
430
+ ]
431
+ };
432
+ }
433
+
434
+ async initialize(config: PluginConfig): Promise<void> {
435
+ this.config = config as OpenTextConfig;
436
+
437
+ if (!this.config.baseUrl) {
438
+ throw new Error('OpenText base URL is required');
439
+ }
440
+
441
+ if (!this.config.username && !this.config.ticket) {
442
+ throw new Error('Either username/password or OTDS ticket is required');
443
+ }
444
+
445
+ if (this.config.username && !this.config.password) {
446
+ throw new Error('Password is required when using username authentication');
447
+ }
448
+
449
+ this.apiClient = await this.createApiClient(this.config);
450
+ }
451
+
452
+ async destroy(): Promise<void> {
453
+ // Log out if we have an auth token
454
+ if (this.authToken && this.apiClient) {
455
+ try {
456
+ await this.apiClient.post('/auth');
457
+ } catch (error) {
458
+ // Ignore logout errors
459
+ }
460
+ }
461
+
462
+ this.config = undefined;
463
+ this.apiClient = undefined;
464
+ this.authToken = undefined;
465
+ }
466
+
467
+ private async createApiClient(config: OpenTextConfig): Promise<AxiosInstance> {
468
+ const client = axios.create({
469
+ baseURL: config.baseUrl,
470
+ headers: {
471
+ 'Content-Type': 'application/json'
472
+ },
473
+ timeout: 30000
474
+ });
475
+
476
+ // Authenticate and get token
477
+ if (config.ticket) {
478
+ // Use OTDS ticket
479
+ client.defaults.headers.common['OTDSTicket'] = config.ticket;
480
+ } else if (config.username && config.password) {
481
+ // Login with username/password
482
+ const authResponse = await client.post('/auth', {
483
+ username: config.username,
484
+ password: config.password
485
+ });
486
+
487
+ this.authToken = authResponse.data.ticket;
488
+ client.defaults.headers.common['OTCSTicket'] = this.authToken;
489
+ }
490
+
491
+ return client;
492
+ }
493
+
494
+ private getMimeType(fileName: string): string {
495
+ const ext = path.extname(fileName).toLowerCase();
496
+ const mimeTypes: Record<string, string> = {
497
+ '.pdf': 'application/pdf',
498
+ '.doc': 'application/msword',
499
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
500
+ '.xls': 'application/vnd.ms-excel',
501
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
502
+ '.ppt': 'application/vnd.ms-powerpoint',
503
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
504
+ '.txt': 'text/plain',
505
+ '.csv': 'text/csv',
506
+ '.json': 'application/json',
507
+ '.xml': 'application/xml',
508
+ '.jpg': 'image/jpeg',
509
+ '.jpeg': 'image/jpeg',
510
+ '.png': 'image/png',
511
+ '.gif': 'image/gif',
512
+ '.zip': 'application/zip'
513
+ };
514
+
515
+ return mimeTypes[ext] || 'application/octet-stream';
516
+ }
517
+
518
+ private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
519
+ // Apply size filter
520
+ if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
521
+ return false;
522
+ }
523
+
524
+ // Apply date range filter
525
+ if (options?.filters?.dateRange) {
526
+ const { from, to } = options.filters.dateRange;
527
+ if (from && source.lastModified < from) return false;
528
+ if (to && source.lastModified > to) return false;
529
+ }
530
+
531
+ // Apply MIME type filter
532
+ if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
533
+ return false;
534
+ }
535
+
536
+ return true;
537
+ }
538
+
539
+ private sleep(ms: number): Promise<void> {
540
+ return new Promise(resolve => setTimeout(resolve, ms));
541
+ }
542
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "opentext",
3
+ "version": "1.0.0",
4
+ "description": "OpenText 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
+ }