@hasna/connectors 1.3.22 → 1.3.23
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/bin/index.js +109 -59
- package/bin/mcp.js +236 -182
- package/bin/serve.js +77 -33
- package/connectors/connect-dropbox/src/api/bulk.ts +285 -0
- package/connectors/connect-dropbox/src/api/index.ts +4 -0
- package/connectors/connect-dropbox/src/cli/index.ts +151 -0
- package/connectors/connect-github/src/api/bulk.ts +328 -0
- package/connectors/connect-github/src/api/index.ts +4 -0
- package/connectors/connect-github/src/cli/index.ts +145 -0
- package/connectors/connect-gmail/src/api/history.ts +82 -0
- package/connectors/connect-gmail/src/api/index.ts +12 -0
- package/connectors/connect-gmail/src/api/settings.ts +280 -0
- package/connectors/connect-gmail/src/api/watch.ts +40 -0
- package/connectors/connect-gmail/src/cli/index.ts +237 -0
- package/connectors/connect-googleads/src/api/bulk.ts +354 -0
- package/connectors/connect-googleads/src/api/index.ts +4 -0
- package/connectors/connect-googleads/src/cli/index.ts +192 -0
- package/connectors/connect-googlecalendar/src/api/acl.ts +66 -0
- package/connectors/connect-googlecalendar/src/api/bulk.ts +452 -0
- package/connectors/connect-googlecalendar/src/api/freebusy.ts +35 -0
- package/connectors/connect-googlecalendar/src/api/index.ts +9 -0
- package/connectors/connect-googlecalendar/src/cli/index.ts +522 -0
- package/connectors/connect-googlecalendar/src/types/index.ts +90 -0
- package/connectors/connect-googlecontacts/src/api/bulk.ts +365 -0
- package/connectors/connect-googlecontacts/src/api/groups.ts +164 -0
- package/connectors/connect-googlecontacts/src/api/index.ts +8 -0
- package/connectors/connect-googlecontacts/src/cli/index.ts +340 -0
- package/connectors/connect-googledocs/src/api/bulk.ts +301 -0
- package/connectors/connect-googledocs/src/api/index.ts +4 -0
- package/connectors/connect-googledocs/src/cli/index.ts +187 -0
- package/connectors/connect-googledrive/src/api/bulk.ts +505 -0
- package/connectors/connect-googledrive/src/api/changes.ts +139 -0
- package/connectors/connect-googledrive/src/api/client.ts +34 -0
- package/connectors/connect-googledrive/src/api/index.ts +12 -0
- package/connectors/connect-googledrive/src/api/revisions.ts +132 -0
- package/connectors/connect-googledrive/src/cli/index.ts +624 -0
- package/connectors/connect-googledrive/src/index.ts +2 -0
- package/connectors/connect-googlephotos/src/api/bulk.ts +455 -0
- package/connectors/connect-googlephotos/src/api/index.ts +8 -0
- package/connectors/connect-googlephotos/src/cli/index.ts +185 -0
- package/connectors/connect-googletasks/src/api/bulk.ts +359 -0
- package/connectors/connect-googletasks/src/api/index.ts +1 -0
- package/connectors/connect-googletasks/src/cli/index.ts +178 -0
- package/connectors/connect-linear/src/api/bulk.ts +219 -0
- package/connectors/connect-linear/src/api/index.ts +4 -0
- package/connectors/connect-linear/src/cli/index.ts +116 -0
- package/connectors/connect-shopify/src/api/bulk.ts +288 -0
- package/connectors/connect-shopify/src/api/index.ts +4 -0
- package/connectors/connect-shopify/src/cli/index.ts +108 -0
- package/connectors/connect-slack/src/api/bulk.ts +237 -0
- package/connectors/connect-slack/src/api/index.ts +4 -0
- package/connectors/connect-slack/src/cli/index.ts +157 -0
- package/connectors/connect-stripe/src/api/bulk.ts +488 -0
- package/connectors/connect-stripe/src/api/index.ts +4 -0
- package/connectors/connect-stripe/src/cli/index.ts +206 -0
- package/dist/index.js +75 -33
- package/dist/lib/runner.d.ts +15 -5
- package/package.json +2 -2
- package/connectors/connect-cloudflare/bun.lock +0 -32
- package/connectors/connect-gmail/bin/index.js +0 -7027
- package/connectors/connect-gmail/dist/cli/index.js +0 -7035
- package/connectors/connect-gmail/dist/index.js +0 -2174
- package/dist/server/server.test.d.ts +0 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { GoogleDocs } from '../api';
|
|
5
|
+
import type { Request } from '../types';
|
|
5
6
|
import {
|
|
6
7
|
getApiKey,
|
|
7
8
|
setApiKey,
|
|
@@ -278,6 +279,192 @@ program
|
|
|
278
279
|
}
|
|
279
280
|
});
|
|
280
281
|
|
|
282
|
+
// ============================================
|
|
283
|
+
// Bulk Commands
|
|
284
|
+
// ============================================
|
|
285
|
+
|
|
286
|
+
const bulkCmd = program
|
|
287
|
+
.command('bulk')
|
|
288
|
+
.description('Bulk operations on documents');
|
|
289
|
+
|
|
290
|
+
bulkCmd
|
|
291
|
+
.command('create')
|
|
292
|
+
.description('Bulk create documents from titles')
|
|
293
|
+
.requiredOption('--titles <titles>', 'Comma-separated list of document titles')
|
|
294
|
+
.option('--concurrency <number>', 'Max concurrent requests', '10')
|
|
295
|
+
.option('--dry-run', 'Preview without making changes')
|
|
296
|
+
.action(async (opts) => {
|
|
297
|
+
try {
|
|
298
|
+
const client = await getClient();
|
|
299
|
+
if (!client.hasWriteAccess()) {
|
|
300
|
+
error('Creating documents requires an OAuth access token.');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const titles = opts.titles.split(',').map((t: string) => t.trim()).filter(Boolean);
|
|
305
|
+
const concurrency = parseInt(opts.concurrency, 10);
|
|
306
|
+
|
|
307
|
+
if (titles.length === 0) {
|
|
308
|
+
error('No titles provided');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
info(`Creating ${titles.length} document(s) (concurrency: ${concurrency})...`);
|
|
313
|
+
if (opts.dryRun) info('(dry run)');
|
|
314
|
+
|
|
315
|
+
const result = await client.bulk.createDocuments({
|
|
316
|
+
titles,
|
|
317
|
+
concurrency,
|
|
318
|
+
dryRun: opts.dryRun,
|
|
319
|
+
onProgress: (current, total) => {
|
|
320
|
+
process.stdout.write(`\rProgress: ${current}/${total}`);
|
|
321
|
+
},
|
|
322
|
+
onError: (err, title) => {
|
|
323
|
+
warn(`\n Failed: ${title} - ${err.message}`);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log(); // newline after progress
|
|
328
|
+
success(`Done: ${result.success} created, ${result.failed} failed`);
|
|
329
|
+
if (result.errors.length > 0) {
|
|
330
|
+
warn('Errors:');
|
|
331
|
+
for (const e of result.errors) {
|
|
332
|
+
console.log(` - ${e.title}: ${e.error}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (result.createdDocuments.length > 0) {
|
|
336
|
+
print(result.createdDocuments, getFormat(program));
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
error(String(err));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
bulkCmd
|
|
345
|
+
.command('find-replace')
|
|
346
|
+
.description('Bulk find and replace across documents')
|
|
347
|
+
.requiredOption('--entries <entries>', 'JSON array of {documentId, find, replace, matchCase?}')
|
|
348
|
+
.option('--concurrency <number>', 'Max concurrent requests', '10')
|
|
349
|
+
.option('--dry-run', 'Preview without making changes')
|
|
350
|
+
.action(async (opts) => {
|
|
351
|
+
try {
|
|
352
|
+
const client = await getClient();
|
|
353
|
+
if (!client.hasWriteAccess()) {
|
|
354
|
+
error('Modifying documents requires an OAuth access token.');
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let entries: Array<{documentId: string; find: string; replace: string; matchCase?: boolean}>;
|
|
359
|
+
try {
|
|
360
|
+
entries = JSON.parse(opts.entries) as Array<{documentId: string; find: string; replace: string; matchCase?: boolean}>;
|
|
361
|
+
} catch {
|
|
362
|
+
error('Invalid JSON for --entries');
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const concurrency = parseInt(opts.concurrency, 10);
|
|
367
|
+
|
|
368
|
+
if (entries.length === 0) {
|
|
369
|
+
error('No entries provided');
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
info(`Processing ${entries.length} find/replace operation(s)...`);
|
|
374
|
+
if (opts.dryRun) info('(dry run)');
|
|
375
|
+
|
|
376
|
+
const result = await client.bulk.findReplace({
|
|
377
|
+
entries,
|
|
378
|
+
concurrency,
|
|
379
|
+
dryRun: opts.dryRun,
|
|
380
|
+
onProgress: (current, total) => {
|
|
381
|
+
process.stdout.write(`\rProgress: ${current}/${total}`);
|
|
382
|
+
},
|
|
383
|
+
onError: (err, entry) => {
|
|
384
|
+
const e = entry as { documentId: string };
|
|
385
|
+
warn(`\n Failed: ${e.documentId} - ${err.message}`);
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
console.log(); // newline after progress
|
|
390
|
+
success(`Done: ${result.success} succeeded, ${result.failed} failed`);
|
|
391
|
+
if (result.errors.length > 0) {
|
|
392
|
+
warn('Errors:');
|
|
393
|
+
for (const e of result.errors) {
|
|
394
|
+
console.log(` - ${e.documentId} ("${e.find}"): ${e.error}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (result.results.length > 0 && !opts.dryRun) {
|
|
398
|
+
info('Results:');
|
|
399
|
+
for (const r of result.results) {
|
|
400
|
+
console.log(` ${r.documentId}: ${r.occurrencesChanged} occurrence(s) of "${r.find}" replaced with "${r.replace}"`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
error(String(err));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
bulkCmd
|
|
410
|
+
.command('batch-update')
|
|
411
|
+
.description('Bulk batch updates across documents')
|
|
412
|
+
.requiredOption('--entries <entries>', 'JSON array of {documentId, requests: [...]}')
|
|
413
|
+
.option('--concurrency <number>', 'Max concurrent requests', '10')
|
|
414
|
+
.option('--dry-run', 'Preview without making changes')
|
|
415
|
+
.action(async (opts) => {
|
|
416
|
+
try {
|
|
417
|
+
const client = await getClient();
|
|
418
|
+
if (!client.hasWriteAccess()) {
|
|
419
|
+
error('Modifying documents requires an OAuth access token.');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let entries: Array<{documentId: string; requests: Request[]}>;
|
|
424
|
+
try {
|
|
425
|
+
entries = JSON.parse(opts.entries) as Array<{documentId: string; requests: Request[]}>;
|
|
426
|
+
} catch {
|
|
427
|
+
error('Invalid JSON for --entries');
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const concurrency = parseInt(opts.concurrency, 10);
|
|
432
|
+
|
|
433
|
+
if (entries.length === 0) {
|
|
434
|
+
error('No entries provided');
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
info(`Processing ${entries.length} batch update(s)...`);
|
|
439
|
+
if (opts.dryRun) info('(dry run)');
|
|
440
|
+
|
|
441
|
+
const result = await client.bulk.batchUpdate({
|
|
442
|
+
entries,
|
|
443
|
+
concurrency,
|
|
444
|
+
dryRun: opts.dryRun,
|
|
445
|
+
onProgress: (current, total) => {
|
|
446
|
+
process.stdout.write(`\rProgress: ${current}/${total}`);
|
|
447
|
+
},
|
|
448
|
+
onError: (err, entry) => {
|
|
449
|
+
const e = entry as { documentId: string };
|
|
450
|
+
warn(`\n Failed: ${e.documentId} - ${err.message}`);
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
console.log(); // newline after progress
|
|
455
|
+
success(`Done: ${result.success} succeeded, ${result.failed} failed`);
|
|
456
|
+
if (result.errors.length > 0) {
|
|
457
|
+
warn('Errors:');
|
|
458
|
+
for (const e of result.errors) {
|
|
459
|
+
console.log(` - ${e.documentId}: ${e.error}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
error(String(err));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
281
468
|
program
|
|
282
469
|
.command('replace <documentId> <find> <replace>')
|
|
283
470
|
.description('Find and replace text in a document (requires OAuth token)')
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import type { DriveClient } from './client.ts';
|
|
2
|
+
import type { DriveFile, FileListResponse, ListFilesOptions } from '../types/index.ts';
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// Bulk Operation Types
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
export interface BulkOperationOptions {
|
|
9
|
+
/** Drive search query (e.g., "name contains 'report'", "mimeType='application/pdf'") */
|
|
10
|
+
query: string;
|
|
11
|
+
/** Maximum files to process (default: 100) */
|
|
12
|
+
maxResults?: number;
|
|
13
|
+
/** Maximum concurrent API calls (default: 10) */
|
|
14
|
+
concurrency?: number;
|
|
15
|
+
/** Dry run - don't actually modify, just preview */
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
/** Progress callback */
|
|
18
|
+
onProgress?: (current: number, total: number, file: FileSummary) => void;
|
|
19
|
+
/** Error callback */
|
|
20
|
+
onError?: (error: Error, file: FileSummary) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BulkDeleteOptions extends BulkOperationOptions {
|
|
24
|
+
/** Skip permanently deleting, move to trash instead */
|
|
25
|
+
trash?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BulkMoveOptions extends BulkOperationOptions {
|
|
29
|
+
/** Destination folder ID */
|
|
30
|
+
destinationFolderId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BulkRenameOptions extends BulkOperationOptions {
|
|
34
|
+
/** Rename pattern: use 'prefix', 'suffix', or 'replace' */
|
|
35
|
+
mode: 'prefix' | 'suffix' | 'replace' | 'lowercase' | 'uppercase';
|
|
36
|
+
/** New prefix (for mode=prefix) */
|
|
37
|
+
prefix?: string;
|
|
38
|
+
/** New suffix (for mode=suffix, without the dot) */
|
|
39
|
+
suffix?: string;
|
|
40
|
+
/** Text to find (for mode=replace) */
|
|
41
|
+
find?: string;
|
|
42
|
+
/** Replacement text (for mode=replace) */
|
|
43
|
+
replace?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BulkShareOptions extends BulkOperationOptions {
|
|
47
|
+
/** Email address to share with */
|
|
48
|
+
email: string;
|
|
49
|
+
/** Permission role */
|
|
50
|
+
role: 'reader' | 'writer' | 'commenter';
|
|
51
|
+
/** Whether to send notification email */
|
|
52
|
+
sendNotification?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FileSummary {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
mimeType: string;
|
|
59
|
+
size?: string;
|
|
60
|
+
parents?: string[];
|
|
61
|
+
webViewLink?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BulkOperationResult {
|
|
65
|
+
total: number;
|
|
66
|
+
success: number;
|
|
67
|
+
failed: number;
|
|
68
|
+
skipped: number;
|
|
69
|
+
errors: Array<{ fileId: string; error: string }>;
|
|
70
|
+
processedFiles: FileSummary[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PreviewResult {
|
|
74
|
+
files: FileSummary[];
|
|
75
|
+
total: number;
|
|
76
|
+
query: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// Bulk Operations API
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
export class BulkApi {
|
|
84
|
+
private readonly client: DriveClient;
|
|
85
|
+
|
|
86
|
+
constructor(client: DriveClient) {
|
|
87
|
+
this.client = client;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// Preview Operations
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Preview files matching a query without making changes
|
|
96
|
+
*/
|
|
97
|
+
async preview(query: string, maxResults: number = 50): Promise<PreviewResult> {
|
|
98
|
+
const files = await this.fetchFiles(query, maxResults);
|
|
99
|
+
return { files, total: files.length, query };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// Trash/Delete Operations
|
|
104
|
+
// ============================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Bulk move files to trash
|
|
108
|
+
*/
|
|
109
|
+
async trash(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
110
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
111
|
+
|
|
112
|
+
return this.executeBatch(files, {
|
|
113
|
+
dryRun: options.dryRun || false,
|
|
114
|
+
concurrency: options.concurrency || 10,
|
|
115
|
+
onProgress: options.onProgress,
|
|
116
|
+
onError: options.onError,
|
|
117
|
+
operation: async (file) => {
|
|
118
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { trashed: true }, {
|
|
119
|
+
fields: 'id,name',
|
|
120
|
+
supportsAllDrives: true,
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Bulk permanently delete files (DANGER!)
|
|
128
|
+
*/
|
|
129
|
+
async delete(options: BulkDeleteOptions): Promise<BulkOperationResult> {
|
|
130
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
131
|
+
|
|
132
|
+
return this.executeBatch(files, {
|
|
133
|
+
dryRun: options.dryRun || false,
|
|
134
|
+
concurrency: options.concurrency || 10,
|
|
135
|
+
onProgress: options.onProgress,
|
|
136
|
+
onError: options.onError,
|
|
137
|
+
operation: async (file) => {
|
|
138
|
+
if (options.trash) {
|
|
139
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { trashed: true }, {
|
|
140
|
+
fields: 'id,name',
|
|
141
|
+
supportsAllDrives: true,
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
await this.client.delete('/files/' + file.id, { supportsAllDrives: true });
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Bulk restore files from trash
|
|
152
|
+
*/
|
|
153
|
+
async untrash(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
154
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
155
|
+
|
|
156
|
+
return this.executeBatch(files, {
|
|
157
|
+
dryRun: options.dryRun || false,
|
|
158
|
+
concurrency: options.concurrency || 10,
|
|
159
|
+
onProgress: options.onProgress,
|
|
160
|
+
onError: options.onError,
|
|
161
|
+
operation: async (file) => {
|
|
162
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { trashed: false }, {
|
|
163
|
+
fields: 'id,name',
|
|
164
|
+
supportsAllDrives: true,
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================
|
|
171
|
+
// Move Operations
|
|
172
|
+
// ============================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Bulk move files to a different folder
|
|
176
|
+
*/
|
|
177
|
+
async move(options: BulkMoveOptions): Promise<BulkOperationResult> {
|
|
178
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
179
|
+
|
|
180
|
+
return this.executeBatch(files, {
|
|
181
|
+
dryRun: options.dryRun || false,
|
|
182
|
+
concurrency: options.concurrency || 10,
|
|
183
|
+
onProgress: options.onProgress,
|
|
184
|
+
onError: options.onError,
|
|
185
|
+
operation: async (file) => {
|
|
186
|
+
// Get current parents to remove them
|
|
187
|
+
const fullFile = await this.client.get<DriveFile>('/files/' + file.id, {
|
|
188
|
+
fields: 'parents',
|
|
189
|
+
supportsAllDrives: true,
|
|
190
|
+
});
|
|
191
|
+
const currentParents = fullFile.parents?.join(',') || '';
|
|
192
|
+
|
|
193
|
+
await this.client.patch<DriveFile>('/files/' + file.id, {}, {
|
|
194
|
+
addParents: options.destinationFolderId,
|
|
195
|
+
removeParents: currentParents,
|
|
196
|
+
fields: 'id,name',
|
|
197
|
+
supportsAllDrives: true,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================
|
|
204
|
+
// Rename Operations
|
|
205
|
+
// ============================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Bulk rename files based on a pattern
|
|
209
|
+
*/
|
|
210
|
+
async rename(options: BulkRenameOptions): Promise<BulkOperationResult> {
|
|
211
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
212
|
+
|
|
213
|
+
return this.executeBatch(files, {
|
|
214
|
+
dryRun: options.dryRun || false,
|
|
215
|
+
concurrency: options.concurrency || 10,
|
|
216
|
+
onProgress: options.onProgress,
|
|
217
|
+
onError: options.onError,
|
|
218
|
+
operation: async (file) => {
|
|
219
|
+
const newName = this.applyRenamePattern(file.name, options);
|
|
220
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { name: newName }, {
|
|
221
|
+
fields: 'id,name',
|
|
222
|
+
supportsAllDrives: true,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================
|
|
229
|
+
// Share Operations
|
|
230
|
+
// ============================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Bulk share files with a user
|
|
234
|
+
*/
|
|
235
|
+
async share(options: BulkShareOptions): Promise<BulkOperationResult> {
|
|
236
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
237
|
+
|
|
238
|
+
return this.executeBatch(files, {
|
|
239
|
+
dryRun: options.dryRun || false,
|
|
240
|
+
concurrency: options.concurrency || 10,
|
|
241
|
+
onProgress: options.onProgress,
|
|
242
|
+
onError: options.onError,
|
|
243
|
+
operation: async (file) => {
|
|
244
|
+
const permission = {
|
|
245
|
+
type: 'user' as const,
|
|
246
|
+
role: options.role,
|
|
247
|
+
emailAddress: options.email,
|
|
248
|
+
};
|
|
249
|
+
const params: Record<string, string | number | boolean | undefined> = {
|
|
250
|
+
supportsAllDrives: true,
|
|
251
|
+
sendNotificationEmail: options.sendNotification ?? true,
|
|
252
|
+
};
|
|
253
|
+
await this.client.post('/files/' + file.id + '/permissions', permission as Record<string, unknown>, params);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Bulk make files publicly accessible
|
|
260
|
+
*/
|
|
261
|
+
async makePublic(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
262
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
263
|
+
|
|
264
|
+
return this.executeBatch(files, {
|
|
265
|
+
dryRun: options.dryRun || false,
|
|
266
|
+
concurrency: options.concurrency || 10,
|
|
267
|
+
onProgress: options.onProgress,
|
|
268
|
+
onError: options.onError,
|
|
269
|
+
operation: async (file) => {
|
|
270
|
+
await this.client.post('/files/' + file.id + '/permissions', {
|
|
271
|
+
type: 'anyone',
|
|
272
|
+
role: 'reader',
|
|
273
|
+
}, {
|
|
274
|
+
supportsAllDrives: true,
|
|
275
|
+
sendNotificationEmail: false,
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Bulk remove public access from files
|
|
283
|
+
*/
|
|
284
|
+
async removePublicAccess(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
285
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
286
|
+
|
|
287
|
+
return this.executeBatch(files, {
|
|
288
|
+
dryRun: options.dryRun || false,
|
|
289
|
+
concurrency: options.concurrency || 10,
|
|
290
|
+
onProgress: options.onProgress,
|
|
291
|
+
onError: options.onError,
|
|
292
|
+
operation: async (file) => {
|
|
293
|
+
// List permissions to find the "anyone" permission
|
|
294
|
+
const perms = await this.client.get<{ permissions: Array<{ id: string; type: string }> }>(
|
|
295
|
+
'/files/' + file.id + '/permissions',
|
|
296
|
+
{ supportsAllDrives: true, fields: 'permissions(id,type)' }
|
|
297
|
+
);
|
|
298
|
+
const anyonePerm = perms.permissions.find(p => p.type === 'anyone');
|
|
299
|
+
if (anyonePerm) {
|
|
300
|
+
await this.client.delete('/files/' + file.id + '/permissions/' + anyonePerm.id, {
|
|
301
|
+
supportsAllDrives: true,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ============================================
|
|
309
|
+
// Star Operations
|
|
310
|
+
// ============================================
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Bulk star files
|
|
314
|
+
*/
|
|
315
|
+
async star(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
316
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
317
|
+
|
|
318
|
+
return this.executeBatch(files, {
|
|
319
|
+
dryRun: options.dryRun || false,
|
|
320
|
+
concurrency: options.concurrency || 10,
|
|
321
|
+
onProgress: options.onProgress,
|
|
322
|
+
onError: options.onError,
|
|
323
|
+
operation: async (file) => {
|
|
324
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { starred: true }, {
|
|
325
|
+
fields: 'id,name',
|
|
326
|
+
supportsAllDrives: true,
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Bulk unstar files
|
|
334
|
+
*/
|
|
335
|
+
async unstar(options: BulkOperationOptions): Promise<BulkOperationResult> {
|
|
336
|
+
const files = await this.fetchFiles(options.query, options.maxResults || 100);
|
|
337
|
+
|
|
338
|
+
return this.executeBatch(files, {
|
|
339
|
+
dryRun: options.dryRun || false,
|
|
340
|
+
concurrency: options.concurrency || 10,
|
|
341
|
+
onProgress: options.onProgress,
|
|
342
|
+
onError: options.onError,
|
|
343
|
+
operation: async (file) => {
|
|
344
|
+
await this.client.patch<DriveFile>('/files/' + file.id, { starred: false }, {
|
|
345
|
+
fields: 'id,name',
|
|
346
|
+
supportsAllDrives: true,
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================
|
|
353
|
+
// Helper Methods
|
|
354
|
+
// ============================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Fetch files matching a query with metadata
|
|
358
|
+
*/
|
|
359
|
+
private async fetchFiles(query: string, maxResults: number): Promise<FileSummary[]> {
|
|
360
|
+
const files: FileSummary[] = [];
|
|
361
|
+
let pageToken: string | undefined;
|
|
362
|
+
const fields = 'id,name,mimeType,size,parents,webViewLink';
|
|
363
|
+
|
|
364
|
+
while (files.length < maxResults) {
|
|
365
|
+
const params: Record<string, string | number | boolean | undefined> = {
|
|
366
|
+
pageSize: Math.min(100, maxResults - files.length),
|
|
367
|
+
pageToken,
|
|
368
|
+
q: query,
|
|
369
|
+
fields: 'nextPageToken,files(' + fields + ')',
|
|
370
|
+
supportsAllDrives: true,
|
|
371
|
+
includeItemsFromAllDrives: true,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const response = await this.client.get<FileListResponse>('/files', params);
|
|
375
|
+
|
|
376
|
+
if (!response.files || response.files.length === 0) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const f of response.files) {
|
|
381
|
+
files.push({
|
|
382
|
+
id: f.id,
|
|
383
|
+
name: f.name,
|
|
384
|
+
mimeType: f.mimeType,
|
|
385
|
+
size: f.size,
|
|
386
|
+
parents: f.parents,
|
|
387
|
+
webViewLink: f.webViewLink,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
pageToken = response.nextPageToken;
|
|
392
|
+
if (!pageToken) break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return files;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Apply rename pattern to a file name
|
|
400
|
+
*/
|
|
401
|
+
private applyRenamePattern(name: string, options: BulkRenameOptions): string {
|
|
402
|
+
switch (options.mode) {
|
|
403
|
+
case 'prefix':
|
|
404
|
+
return (options.prefix || '') + name;
|
|
405
|
+
case 'suffix': {
|
|
406
|
+
const dotIndex = name.lastIndexOf('.');
|
|
407
|
+
if (dotIndex > 0 && !name.startsWith('.')) {
|
|
408
|
+
return name.slice(0, dotIndex) + (options.suffix || '') + name.slice(dotIndex);
|
|
409
|
+
}
|
|
410
|
+
return name + (options.suffix || '');
|
|
411
|
+
}
|
|
412
|
+
case 'replace':
|
|
413
|
+
return options.find ? name.replace(new RegExp(options.find, 'g'), options.replace || '') : name;
|
|
414
|
+
case 'lowercase': {
|
|
415
|
+
const dotIndex = name.lastIndexOf('.');
|
|
416
|
+
if (dotIndex > 0 && !name.startsWith('.')) {
|
|
417
|
+
return name.slice(0, dotIndex).toLowerCase() + name.slice(dotIndex);
|
|
418
|
+
}
|
|
419
|
+
return name.toLowerCase();
|
|
420
|
+
}
|
|
421
|
+
case 'uppercase': {
|
|
422
|
+
const dotIndex = name.lastIndexOf('.');
|
|
423
|
+
if (dotIndex > 0 && !name.startsWith('.')) {
|
|
424
|
+
return name.slice(0, dotIndex).toUpperCase() + name.slice(dotIndex);
|
|
425
|
+
}
|
|
426
|
+
return name.toUpperCase();
|
|
427
|
+
}
|
|
428
|
+
default:
|
|
429
|
+
return name;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Execute operations in batches with concurrency control
|
|
435
|
+
*/
|
|
436
|
+
private async executeBatch(
|
|
437
|
+
files: FileSummary[],
|
|
438
|
+
options: {
|
|
439
|
+
dryRun: boolean;
|
|
440
|
+
concurrency: number;
|
|
441
|
+
onProgress?: (current: number, total: number, file: FileSummary) => void;
|
|
442
|
+
onError?: (error: Error, file: FileSummary) => void;
|
|
443
|
+
operation: (file: FileSummary) => Promise<void>;
|
|
444
|
+
}
|
|
445
|
+
): Promise<BulkOperationResult> {
|
|
446
|
+
const { dryRun, concurrency, onProgress, onError, operation } = options;
|
|
447
|
+
|
|
448
|
+
const result: BulkOperationResult = {
|
|
449
|
+
total: files.length,
|
|
450
|
+
success: 0,
|
|
451
|
+
failed: 0,
|
|
452
|
+
skipped: 0,
|
|
453
|
+
errors: [],
|
|
454
|
+
processedFiles: [],
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
if (files.length === 0) {
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const chunks = this.chunkArray(files, concurrency);
|
|
462
|
+
|
|
463
|
+
for (const chunk of chunks) {
|
|
464
|
+
await Promise.all(
|
|
465
|
+
chunk.map(async (file) => {
|
|
466
|
+
try {
|
|
467
|
+
if (dryRun) {
|
|
468
|
+
result.success++;
|
|
469
|
+
result.processedFiles.push(file);
|
|
470
|
+
} else {
|
|
471
|
+
await operation(file);
|
|
472
|
+
result.success++;
|
|
473
|
+
result.processedFiles.push(file);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (onProgress) {
|
|
477
|
+
onProgress(result.success + result.failed, result.total, file);
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
result.failed++;
|
|
481
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
482
|
+
result.errors.push({ fileId: file.id, error: errorMessage });
|
|
483
|
+
|
|
484
|
+
if (onError) {
|
|
485
|
+
onError(err instanceof Error ? err : new Error(errorMessage), file);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Split array into chunks
|
|
497
|
+
*/
|
|
498
|
+
private chunkArray<T>(array: T[], size: number): T[][] {
|
|
499
|
+
const chunks: T[][] = [];
|
|
500
|
+
for (let i = 0; i < array.length; i += size) {
|
|
501
|
+
chunks.push(array.slice(i, i + size));
|
|
502
|
+
}
|
|
503
|
+
return chunks;
|
|
504
|
+
}
|
|
505
|
+
}
|