@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.
- package/dist/services/hubdoc-api.js +1 -1
- package/dist/services/oauth-token-service.js +2 -2
- package/dist/services/permission-manager.js +1 -1
- package/package.json +10 -5
- package/plugins/alfresco/index.ts +0 -518
- package/plugins/alfresco/plugin.json +0 -12
- package/plugins/aws-s3/index.ts +0 -471
- package/plugins/aws-s3/plugin.json +0 -12
- package/plugins/azure-blob/index.ts +0 -420
- package/plugins/azure-blob/plugin.json +0 -12
- package/plugins/box/index.ts +0 -495
- package/plugins/box/plugin.json +0 -12
- package/plugins/core/README.md +0 -122
- package/plugins/core/TESTING.md +0 -155
- package/plugins/core/index.ts +0 -510
- package/plugins/core/plugin.json +0 -26
- package/plugins/dropbox/index.ts +0 -451
- package/plugins/dropbox/plugin.json +0 -12
- package/plugins/filesystem/index.ts +0 -360
- package/plugins/filesystem/plugin.json +0 -12
- package/plugins/googledrive/index.ts +0 -463
- package/plugins/googledrive/plugin.json +0 -12
- package/plugins/nuxeo/index.ts +0 -512
- package/plugins/nuxeo/plugin.json +0 -12
- package/plugins/onedrive/TESTING.md +0 -197
- package/plugins/onedrive/index.ts +0 -447
- package/plugins/onedrive/plugin.json +0 -12
- package/plugins/opentext/index.ts +0 -542
- package/plugins/opentext/plugin.json +0 -12
- package/plugins/sharepoint/index.ts +0 -509
- package/plugins/sharepoint/plugin.json +0 -12
|
@@ -1,463 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
}
|