@sinoia/hubdoc-tools 1.3.2 → 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.
- package/package.json +2 -1
- 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,451 @@
|
|
|
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 DropboxConfig extends PluginConfig {
|
|
16
|
+
accessToken: string;
|
|
17
|
+
appKey?: string;
|
|
18
|
+
appSecret?: string;
|
|
19
|
+
rootPath?: string; // e.g., '/Documents' or ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DropboxFileMetadata {
|
|
23
|
+
'.tag': 'file' | 'folder';
|
|
24
|
+
name: string;
|
|
25
|
+
path_lower: string;
|
|
26
|
+
path_display: string;
|
|
27
|
+
id: string;
|
|
28
|
+
size?: number;
|
|
29
|
+
server_modified?: string;
|
|
30
|
+
client_modified?: string;
|
|
31
|
+
content_hash?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default class DropboxPlugin implements DocumentSourcePlugin {
|
|
35
|
+
readonly name = 'dropbox';
|
|
36
|
+
readonly version = '1.0.0';
|
|
37
|
+
readonly description = 'Dropbox document source';
|
|
38
|
+
readonly supportedOperations = ['import', 'export', 'both'] as const;
|
|
39
|
+
|
|
40
|
+
private config?: DropboxConfig;
|
|
41
|
+
private apiClient?: AxiosInstance;
|
|
42
|
+
private readonly apiUrl = 'https://api.dropboxapi.com/2';
|
|
43
|
+
private readonly contentUrl = 'https://content.dropboxapi.com/2';
|
|
44
|
+
|
|
45
|
+
async testConnection(config: PluginConfig): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
const client = this.createApiClient(config as DropboxConfig);
|
|
48
|
+
const response = await client.post('/users/get_current_account');
|
|
49
|
+
return response.status === 200;
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
console.error(`Dropbox connection test failed: ${error.message}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async scan(config: PluginConfig, options?: PluginImportOptions): Promise<ScanResult> {
|
|
57
|
+
this.config = config as DropboxConfig;
|
|
58
|
+
this.apiClient = this.createApiClient(this.config);
|
|
59
|
+
|
|
60
|
+
const sources: DocumentSource[] = [];
|
|
61
|
+
const errors: string[] = [];
|
|
62
|
+
let totalSize = 0;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const rootPath = this.config.rootPath || '';
|
|
66
|
+
const items = await this.scanFolder(rootPath, options);
|
|
67
|
+
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
if (item['.tag'] === 'file') {
|
|
70
|
+
const source: DocumentSource = {
|
|
71
|
+
id: item.path_display,
|
|
72
|
+
name: item.name,
|
|
73
|
+
path: this.getRelativePath(item.path_display, rootPath),
|
|
74
|
+
size: item.size || 0,
|
|
75
|
+
mimeType: this.getMimeType(item.name),
|
|
76
|
+
lastModified: new Date(item.server_modified || item.client_modified || Date.now()),
|
|
77
|
+
metadata: {
|
|
78
|
+
dropboxId: item.id,
|
|
79
|
+
pathLower: item.path_lower,
|
|
80
|
+
contentHash: item.content_hash
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Apply filters
|
|
85
|
+
if (this.shouldIncludeSource(source, options)) {
|
|
86
|
+
sources.push(source);
|
|
87
|
+
totalSize += source.size;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sources,
|
|
94
|
+
totalCount: sources.length,
|
|
95
|
+
totalSize,
|
|
96
|
+
errors
|
|
97
|
+
};
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
return {
|
|
100
|
+
sources: [],
|
|
101
|
+
totalCount: 0,
|
|
102
|
+
totalSize: 0,
|
|
103
|
+
errors: [`Dropbox scan failed: ${error.message}`]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async scanFolder(folderPath: string, options?: PluginImportOptions): Promise<DropboxFileMetadata[]> {
|
|
109
|
+
if (!this.apiClient) throw new Error('API client not initialized');
|
|
110
|
+
|
|
111
|
+
const allItems: DropboxFileMetadata[] = [];
|
|
112
|
+
let cursor: string | undefined;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Initial request
|
|
116
|
+
const response = await this.apiClient.post('/files/list_folder', {
|
|
117
|
+
path: folderPath || '',
|
|
118
|
+
recursive: true,
|
|
119
|
+
limit: 2000,
|
|
120
|
+
include_media_info: false,
|
|
121
|
+
include_deleted: false,
|
|
122
|
+
include_has_explicit_shared_members: false
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
allItems.push(...response.data.entries);
|
|
126
|
+
|
|
127
|
+
// Handle pagination
|
|
128
|
+
while (response.data.has_more) {
|
|
129
|
+
cursor = response.data.cursor;
|
|
130
|
+
const continueResponse = await this.apiClient.post('/files/list_folder/continue', {
|
|
131
|
+
cursor
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
allItems.push(...continueResponse.data.entries);
|
|
135
|
+
|
|
136
|
+
if (!continueResponse.data.has_more) break;
|
|
137
|
+
response.data = continueResponse.data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return allItems;
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
if (error.response?.data?.error?.['.tag'] === 'path_not_found') {
|
|
143
|
+
console.warn(`Warning: Folder not found: ${folderPath}`);
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async import(
|
|
151
|
+
config: PluginConfig,
|
|
152
|
+
sources: DocumentSource[],
|
|
153
|
+
targetDir: string,
|
|
154
|
+
options?: PluginImportOptions
|
|
155
|
+
): Promise<ImportResult[]> {
|
|
156
|
+
this.config = config as DropboxConfig;
|
|
157
|
+
this.apiClient = this.createApiClient(this.config);
|
|
158
|
+
|
|
159
|
+
const results: ImportResult[] = [];
|
|
160
|
+
const batchSize = options?.batchSize || 10;
|
|
161
|
+
|
|
162
|
+
// Process in batches
|
|
163
|
+
for (let i = 0; i < sources.length; i += batchSize) {
|
|
164
|
+
const batch = sources.slice(i, i + batchSize);
|
|
165
|
+
|
|
166
|
+
for (const source of batch) {
|
|
167
|
+
const result = await this.importSingle(source, targetDir);
|
|
168
|
+
results.push(result);
|
|
169
|
+
|
|
170
|
+
// Small delay to respect rate limits
|
|
171
|
+
await this.sleep(100);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async importSingle(source: DocumentSource, targetDir: string): Promise<ImportResult> {
|
|
179
|
+
try {
|
|
180
|
+
if (!this.apiClient) throw new Error('API client not initialized');
|
|
181
|
+
|
|
182
|
+
const targetPath = path.join(targetDir, source.path);
|
|
183
|
+
const targetDirectory = path.dirname(targetPath);
|
|
184
|
+
|
|
185
|
+
await fs.ensureDir(targetDirectory);
|
|
186
|
+
|
|
187
|
+
// Download file from Dropbox
|
|
188
|
+
const downloadClient = axios.create({
|
|
189
|
+
baseURL: this.contentUrl,
|
|
190
|
+
headers: {
|
|
191
|
+
'Authorization': `Bearer ${this.config!.accessToken}`,
|
|
192
|
+
'Dropbox-API-Arg': JSON.stringify({
|
|
193
|
+
path: source.id
|
|
194
|
+
})
|
|
195
|
+
},
|
|
196
|
+
responseType: 'stream'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const response = await downloadClient.post('/files/download');
|
|
200
|
+
|
|
201
|
+
const writer = fs.createWriteStream(targetPath);
|
|
202
|
+
response.data.pipe(writer);
|
|
203
|
+
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
writer.on('finish', () => {
|
|
206
|
+
resolve({
|
|
207
|
+
success: true,
|
|
208
|
+
source,
|
|
209
|
+
localPath: targetPath,
|
|
210
|
+
bytesTransferred: source.size
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
writer.on('error', (error) => {
|
|
215
|
+
resolve({
|
|
216
|
+
success: false,
|
|
217
|
+
source,
|
|
218
|
+
error: error.message
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
} catch (error: any) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
source,
|
|
226
|
+
error: error.message
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async export?(
|
|
232
|
+
config: PluginConfig,
|
|
233
|
+
localSources: DocumentSource[],
|
|
234
|
+
options?: PluginExportOptions
|
|
235
|
+
): Promise<ExportResult[]> {
|
|
236
|
+
this.config = config as DropboxConfig;
|
|
237
|
+
this.apiClient = this.createApiClient(this.config);
|
|
238
|
+
|
|
239
|
+
const results: ExportResult[] = [];
|
|
240
|
+
|
|
241
|
+
for (const source of localSources) {
|
|
242
|
+
try {
|
|
243
|
+
const targetPath = options?.preserveStructure
|
|
244
|
+
? `${this.config.rootPath || ''}/${options.targetPath}/${source.path}`
|
|
245
|
+
: `${this.config.rootPath || ''}/${options?.targetPath || ''}/${source.name}`;
|
|
246
|
+
|
|
247
|
+
// Read file content
|
|
248
|
+
const fileContent = await fs.readFile(source.id);
|
|
249
|
+
const fileSize = fileContent.length;
|
|
250
|
+
|
|
251
|
+
// Upload file to Dropbox
|
|
252
|
+
const uploadClient = axios.create({
|
|
253
|
+
baseURL: this.contentUrl,
|
|
254
|
+
headers: {
|
|
255
|
+
'Authorization': `Bearer ${this.config.accessToken}`,
|
|
256
|
+
'Content-Type': 'application/octet-stream',
|
|
257
|
+
'Dropbox-API-Arg': JSON.stringify({
|
|
258
|
+
path: targetPath,
|
|
259
|
+
mode: options?.overwrite ? 'overwrite' : 'add',
|
|
260
|
+
autorename: !options?.overwrite,
|
|
261
|
+
mute: false
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Use upload sessions for large files (>150MB)
|
|
267
|
+
if (fileSize > 150 * 1024 * 1024) {
|
|
268
|
+
await this.uploadLargeFile(uploadClient, fileContent, targetPath);
|
|
269
|
+
} else {
|
|
270
|
+
await uploadClient.post('/files/upload', fileContent);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
results.push({
|
|
274
|
+
success: true,
|
|
275
|
+
targetPath,
|
|
276
|
+
source,
|
|
277
|
+
bytesTransferred: fileSize
|
|
278
|
+
});
|
|
279
|
+
} catch (error: any) {
|
|
280
|
+
results.push({
|
|
281
|
+
success: false,
|
|
282
|
+
targetPath: options?.targetPath || '',
|
|
283
|
+
source,
|
|
284
|
+
error: error.message
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async uploadLargeFile(uploadClient: AxiosInstance, content: Buffer, targetPath: string): Promise<void> {
|
|
293
|
+
const chunkSize = 4 * 1024 * 1024; // 4MB chunks
|
|
294
|
+
let offset = 0;
|
|
295
|
+
let sessionId: string | undefined;
|
|
296
|
+
|
|
297
|
+
while (offset < content.length) {
|
|
298
|
+
const chunk = content.slice(offset, offset + chunkSize);
|
|
299
|
+
const isLastChunk = offset + chunkSize >= content.length;
|
|
300
|
+
|
|
301
|
+
if (!sessionId) {
|
|
302
|
+
// Start upload session
|
|
303
|
+
const startResponse = await uploadClient.post('/files/upload_session/start', chunk, {
|
|
304
|
+
headers: {
|
|
305
|
+
'Dropbox-API-Arg': JSON.stringify({ close: false })
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
sessionId = startResponse.data.session_id;
|
|
309
|
+
} else if (!isLastChunk) {
|
|
310
|
+
// Continue upload session
|
|
311
|
+
await uploadClient.post('/files/upload_session/append_v2', chunk, {
|
|
312
|
+
headers: {
|
|
313
|
+
'Dropbox-API-Arg': JSON.stringify({
|
|
314
|
+
cursor: { session_id: sessionId, offset },
|
|
315
|
+
close: false
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
// Finish upload session
|
|
321
|
+
await uploadClient.post('/files/upload_session/finish', chunk, {
|
|
322
|
+
headers: {
|
|
323
|
+
'Dropbox-API-Arg': JSON.stringify({
|
|
324
|
+
cursor: { session_id: sessionId, offset },
|
|
325
|
+
commit: {
|
|
326
|
+
path: targetPath,
|
|
327
|
+
mode: 'add',
|
|
328
|
+
autorename: true
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
offset += chunkSize;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getConfigSchema(): Record<string, any> {
|
|
340
|
+
return {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
accessToken: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
description: 'Dropbox Access Token',
|
|
346
|
+
required: true
|
|
347
|
+
},
|
|
348
|
+
appKey: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'Dropbox App Key (optional)',
|
|
351
|
+
required: false
|
|
352
|
+
},
|
|
353
|
+
appSecret: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Dropbox App Secret (optional)',
|
|
356
|
+
required: false
|
|
357
|
+
},
|
|
358
|
+
rootPath: {
|
|
359
|
+
type: 'string',
|
|
360
|
+
description: 'Root path to scan (e.g., /Documents)',
|
|
361
|
+
default: ''
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
required: ['accessToken']
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async initialize(config: PluginConfig): Promise<void> {
|
|
369
|
+
this.config = config as DropboxConfig;
|
|
370
|
+
|
|
371
|
+
if (!this.config.accessToken) {
|
|
372
|
+
throw new Error('Dropbox access token is required');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.apiClient = this.createApiClient(this.config);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async destroy(): Promise<void> {
|
|
379
|
+
this.config = undefined;
|
|
380
|
+
this.apiClient = undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private createApiClient(config: DropboxConfig): AxiosInstance {
|
|
384
|
+
return axios.create({
|
|
385
|
+
baseURL: this.apiUrl,
|
|
386
|
+
headers: {
|
|
387
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
388
|
+
'Content-Type': 'application/json'
|
|
389
|
+
},
|
|
390
|
+
timeout: 30000
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private getRelativePath(fullPath: string, rootPath: string): string {
|
|
395
|
+
if (!rootPath) return fullPath.startsWith('/') ? fullPath.substring(1) : fullPath;
|
|
396
|
+
|
|
397
|
+
const relativePath = fullPath.startsWith(rootPath)
|
|
398
|
+
? fullPath.substring(rootPath.length)
|
|
399
|
+
: fullPath;
|
|
400
|
+
|
|
401
|
+
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private getMimeType(fileName: string): string {
|
|
405
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
406
|
+
const mimeTypes: Record<string, string> = {
|
|
407
|
+
'.pdf': 'application/pdf',
|
|
408
|
+
'.doc': 'application/msword',
|
|
409
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
410
|
+
'.xls': 'application/vnd.ms-excel',
|
|
411
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
412
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
413
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
414
|
+
'.txt': 'text/plain',
|
|
415
|
+
'.csv': 'text/csv',
|
|
416
|
+
'.json': 'application/json',
|
|
417
|
+
'.jpg': 'image/jpeg',
|
|
418
|
+
'.jpeg': 'image/jpeg',
|
|
419
|
+
'.png': 'image/png',
|
|
420
|
+
'.gif': 'image/gif',
|
|
421
|
+
'.zip': 'application/zip'
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private shouldIncludeSource(source: DocumentSource, options?: PluginImportOptions): boolean {
|
|
428
|
+
// Apply size filter
|
|
429
|
+
if (options?.filters?.maxSize && source.size > options.filters.maxSize) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Apply date range filter
|
|
434
|
+
if (options?.filters?.dateRange) {
|
|
435
|
+
const { from, to } = options.filters.dateRange;
|
|
436
|
+
if (from && source.lastModified < from) return false;
|
|
437
|
+
if (to && source.lastModified > to) return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Apply MIME type filter
|
|
441
|
+
if (options?.filters?.mimeTypes && !options.filters.mimeTypes.includes(source.mimeType)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private sleep(ms: number): Promise<void> {
|
|
449
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
450
|
+
}
|
|
451
|
+
}
|