Skip to content

Commit 01b9b5a

Browse files
authored
feat(drive): add trashFile and renameFile tools (#254)
1 parent c967ade commit 01b9b5a

3 files changed

Lines changed: 226 additions & 11 deletions

File tree

workspace-server/src/__tests__/services/DriveService.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,123 @@ describe('DriveService', () => {
961961
});
962962
});
963963

964+
describe('trashFile', () => {
965+
it('should trash a file by ID', async () => {
966+
mockDriveAPI.files.update.mockResolvedValue({
967+
data: { id: 'file-id-123', name: 'My File.pdf' },
968+
});
969+
970+
const result = await driveService.trashFile({ fileId: 'file-id-123' });
971+
972+
expect(mockDriveAPI.files.update).toHaveBeenCalledWith({
973+
fileId: 'file-id-123',
974+
requestBody: { trashed: true },
975+
fields: 'id, name',
976+
supportsAllDrives: true,
977+
});
978+
expect(JSON.parse(result.content[0].text)).toEqual({
979+
id: 'file-id-123',
980+
name: 'My File.pdf',
981+
trashed: true,
982+
});
983+
});
984+
985+
it('should extract ID from a Drive URL', async () => {
986+
mockDriveAPI.files.update.mockResolvedValue({
987+
data: { id: 'file-url-id', name: 'URL File.pdf' },
988+
});
989+
990+
const result = await driveService.trashFile({
991+
fileId: 'https://drive.google.com/file/d/file-url-id/view',
992+
});
993+
994+
expect(mockDriveAPI.files.update).toHaveBeenCalledWith({
995+
fileId: 'file-url-id',
996+
requestBody: { trashed: true },
997+
fields: 'id, name',
998+
supportsAllDrives: true,
999+
});
1000+
expect(JSON.parse(result.content[0].text)).toEqual({
1001+
id: 'file-url-id',
1002+
name: 'URL File.pdf',
1003+
trashed: true,
1004+
});
1005+
});
1006+
1007+
it('should handle API errors gracefully', async () => {
1008+
mockDriveAPI.files.update.mockRejectedValue(
1009+
new Error('Permission denied'),
1010+
);
1011+
1012+
const result = await driveService.trashFile({ fileId: 'file-id-123' });
1013+
1014+
expect(JSON.parse(result.content[0].text)).toEqual({
1015+
error: 'Permission denied',
1016+
});
1017+
});
1018+
});
1019+
1020+
describe('renameFile', () => {
1021+
it('should rename a file by ID', async () => {
1022+
mockDriveAPI.files.update.mockResolvedValue({
1023+
data: { id: 'file-id-123', name: 'New Name' },
1024+
});
1025+
1026+
const result = await driveService.renameFile({
1027+
fileId: 'file-id-123',
1028+
newName: 'New Name',
1029+
});
1030+
1031+
expect(mockDriveAPI.files.update).toHaveBeenCalledWith({
1032+
fileId: 'file-id-123',
1033+
requestBody: { name: 'New Name' },
1034+
fields: 'id, name',
1035+
supportsAllDrives: true,
1036+
});
1037+
1038+
expect(JSON.parse(result.content[0].text)).toEqual({
1039+
id: 'file-id-123',
1040+
name: 'New Name',
1041+
});
1042+
});
1043+
1044+
it('should extract ID from a Drive URL', async () => {
1045+
mockDriveAPI.files.update.mockResolvedValue({
1046+
data: { id: 'doc-url-id', name: 'Renamed Doc' },
1047+
});
1048+
1049+
const result = await driveService.renameFile({
1050+
fileId: 'https://docs.google.com/document/d/doc-url-id/edit',
1051+
newName: 'Renamed Doc',
1052+
});
1053+
1054+
expect(mockDriveAPI.files.update).toHaveBeenCalledWith({
1055+
fileId: 'doc-url-id',
1056+
requestBody: { name: 'Renamed Doc' },
1057+
fields: 'id, name',
1058+
supportsAllDrives: true,
1059+
});
1060+
1061+
expect(JSON.parse(result.content[0].text)).toEqual({
1062+
id: 'doc-url-id',
1063+
name: 'Renamed Doc',
1064+
});
1065+
});
1066+
1067+
it('should handle API errors gracefully', async () => {
1068+
mockDriveAPI.files.update.mockRejectedValue(new Error('File not found'));
1069+
1070+
const result = await driveService.renameFile({
1071+
fileId: 'file-id-123',
1072+
newName: 'New Name',
1073+
});
1074+
1075+
expect(JSON.parse(result.content[0].text)).toEqual({
1076+
error: 'File not found',
1077+
});
1078+
});
1079+
});
1080+
9641081
describe('Shared Drive Support', () => {
9651082
it('findFolder should include shared drive flags', async () => {
9661083
mockDriveAPI.files.list.mockResolvedValue({ data: { files: [] } });

workspace-server/src/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,34 @@ async function main() {
579579
driveService.downloadFile,
580580
);
581581

582+
server.registerTool(
583+
'drive.trashFile',
584+
{
585+
description:
586+
'Moves a file or folder to the trash in Google Drive. This is a safe, reversible operation.',
587+
inputSchema: {
588+
fileId: z.string().describe('The ID or URL of the file to trash.'),
589+
},
590+
},
591+
driveService.trashFile,
592+
);
593+
594+
server.registerTool(
595+
'drive.renameFile',
596+
{
597+
description: 'Renames a file or folder in Google Drive.',
598+
inputSchema: {
599+
fileId: z.string().describe('The ID or URL of the file to rename.'),
600+
newName: z
601+
.string()
602+
.trim()
603+
.min(1)
604+
.describe('The new name for the file.'),
605+
},
606+
},
607+
driveService.renameFile,
608+
);
609+
582610
server.registerTool(
583611
'calendar.list',
584612
{

workspace-server/src/services/DriveService.ts

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ export class DriveService {
3535
return google.drive({ version: 'v3', ...options });
3636
}
3737

38+
private handleError(context: string, error: unknown) {
39+
const errorMessage = error instanceof Error ? error.message : String(error);
40+
logToFile(`Error during ${context}: ${errorMessage}`);
41+
return {
42+
content: [
43+
{
44+
type: 'text' as const,
45+
text: JSON.stringify({ error: errorMessage }),
46+
},
47+
],
48+
};
49+
}
50+
3851
public findFolder = async ({ folderName }: { folderName: string }) => {
3952
logToFile(`Searching for folder with name: ${folderName}`);
4053
try {
@@ -62,17 +75,7 @@ export class DriveService {
6275
],
6376
};
6477
} catch (error) {
65-
const errorMessage =
66-
error instanceof Error ? error.message : String(error);
67-
logToFile(`Error during drive.findFolder: ${errorMessage}`);
68-
return {
69-
content: [
70-
{
71-
type: 'text' as const,
72-
text: JSON.stringify({ error: errorMessage }),
73-
},
74-
],
75-
};
78+
return this.handleError('drive.findFolder', error);
7679
}
7780
};
7881

@@ -346,6 +349,73 @@ export class DriveService {
346349
}
347350
};
348351

352+
public trashFile = async ({ fileId }: { fileId: string }) => {
353+
logToFile(`Trashing Drive file: ${fileId}`);
354+
try {
355+
const drive = await this.getDriveClient();
356+
const id = extractDocumentId(fileId);
357+
358+
const file = await drive.files.update({
359+
fileId: id,
360+
requestBody: { trashed: true },
361+
fields: 'id, name',
362+
supportsAllDrives: true,
363+
});
364+
365+
logToFile(`Successfully trashed file: ${id}`);
366+
return {
367+
content: [
368+
{
369+
type: 'text' as const,
370+
text: JSON.stringify({
371+
id: file.data.id,
372+
name: file.data.name,
373+
trashed: true,
374+
}),
375+
},
376+
],
377+
};
378+
} catch (error) {
379+
return this.handleError('drive.trashFile', error);
380+
}
381+
};
382+
383+
public renameFile = async ({
384+
fileId,
385+
newName,
386+
}: {
387+
fileId: string;
388+
newName: string;
389+
}) => {
390+
logToFile(`Renaming Drive file: ${fileId} to "${newName}"`);
391+
try {
392+
const drive = await this.getDriveClient();
393+
const id = extractDocumentId(fileId);
394+
395+
const file = await drive.files.update({
396+
fileId: id,
397+
requestBody: { name: newName },
398+
fields: 'id, name',
399+
supportsAllDrives: true,
400+
});
401+
402+
logToFile(`Successfully renamed file: ${id} to "${file.data.name}"`);
403+
return {
404+
content: [
405+
{
406+
type: 'text' as const,
407+
text: JSON.stringify({
408+
id: file.data.id,
409+
name: file.data.name,
410+
}),
411+
},
412+
],
413+
};
414+
} catch (error) {
415+
return this.handleError('drive.renameFile', error);
416+
}
417+
};
418+
349419
public downloadFile = async ({
350420
fileId,
351421
localPath,

0 commit comments

Comments
 (0)