@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.
- package/dist/index.d.ts +1 -1
- package/dist/plugins/alfresco/plugin.json +12 -0
- package/dist/plugins/aws-s3/plugin.json +12 -0
- package/dist/plugins/azure-blob/plugin.json +12 -0
- package/dist/plugins/box/plugin.json +12 -0
- package/dist/plugins/core/index.d.ts +25 -0
- package/dist/plugins/core/index.d.ts.map +1 -0
- package/dist/plugins/core/index.js +400 -0
- package/dist/plugins/core/index.js.map +1 -0
- package/dist/plugins/core/plugin.json +26 -0
- package/dist/plugins/dropbox/plugin.json +12 -0
- package/dist/plugins/filesystem/index.d.ts +22 -0
- package/dist/plugins/filesystem/index.d.ts.map +1 -0
- package/dist/plugins/filesystem/index.js +306 -0
- package/dist/plugins/filesystem/index.js.map +1 -0
- package/dist/plugins/filesystem/plugin.json +12 -0
- package/dist/plugins/googledrive/plugin.json +12 -0
- package/dist/plugins/nuxeo/plugin.json +12 -0
- package/dist/plugins/onedrive/plugin.json +12 -0
- package/dist/plugins/opentext/plugin.json +12 -0
- package/dist/plugins/sharepoint/plugin.json +12 -0
- package/dist/services/hubdoc-api.d.ts +1 -1
- package/dist/services/hubdoc-api.js +1 -1
- package/dist/services/oauth-token-service.d.ts +1 -1
- package/dist/services/oauth-token-service.js +2 -2
- package/dist/services/permission-manager.d.ts +1 -1
- package/dist/services/permission-manager.js +1 -1
- package/dist/src/types/plugins.d.ts +111 -0
- package/dist/src/types/plugins.d.ts.map +1 -0
- package/dist/src/types/plugins.js +3 -0
- package/dist/src/types/plugins.js.map +1 -0
- package/dist/src/utils/concurrent-processor.d.ts +63 -0
- package/dist/src/utils/concurrent-processor.d.ts.map +1 -0
- package/dist/src/utils/concurrent-processor.js +240 -0
- package/dist/src/utils/concurrent-processor.js.map +1 -0
- package/dist/src/utils/xml-metadata.d.ts +47 -0
- package/dist/src/utils/xml-metadata.d.ts.map +1 -0
- package/dist/src/utils/xml-metadata.js +200 -0
- package/dist/src/utils/xml-metadata.js.map +1 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +6 -2
- package/plugins/alfresco/index.ts +518 -0
- package/plugins/alfresco/plugin.json +12 -0
- package/plugins/aws-s3/index.ts +471 -0
- package/plugins/aws-s3/plugin.json +12 -0
- package/plugins/azure-blob/index.ts +420 -0
- package/plugins/azure-blob/plugin.json +12 -0
- package/plugins/box/index.ts +495 -0
- package/plugins/box/plugin.json +12 -0
- package/plugins/core/README.md +122 -0
- package/plugins/core/TESTING.md +155 -0
- package/plugins/core/index.ts +510 -0
- package/plugins/core/plugin.json +26 -0
- package/plugins/dropbox/index.ts +451 -0
- package/plugins/dropbox/plugin.json +12 -0
- package/plugins/filesystem/index.ts +360 -0
- package/plugins/filesystem/plugin.json +12 -0
- package/plugins/googledrive/index.ts +463 -0
- package/plugins/googledrive/plugin.json +12 -0
- package/plugins/nuxeo/index.ts +512 -0
- package/plugins/nuxeo/plugin.json +12 -0
- package/plugins/onedrive/TESTING.md +197 -0
- package/plugins/onedrive/index.ts +447 -0
- package/plugins/onedrive/plugin.json +12 -0
- package/plugins/opentext/index.ts +542 -0
- package/plugins/opentext/plugin.json +12 -0
- package/plugins/sharepoint/index.ts +509 -0
- package/plugins/sharepoint/plugin.json +12 -0
|
@@ -0,0 +1,495 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
```
|