diff --git a/package-lock.json b/package-lock.json index 3fd4dad6..41344608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -217,6 +217,7 @@ "integrity": "sha512-22SHEEVNjZfFWkFks3P6HilkR3rS7a6GjnCIqR22Zz4HNxdfT0FG+RE7efTcFVfLUkTTMQQybvaUcwMrHXYa7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.46.0", "@algolia/requester-browser-xhr": "5.46.0", @@ -395,6 +396,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -988,6 +990,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1032,6 +1035,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3396,6 +3400,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3487,6 +3492,7 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -4281,6 +4287,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4382,6 +4389,7 @@ "integrity": "sha512-7ML6fa2K93FIfifG3GMWhDEwT5qQzPTmoHKCTvhzGEwdbQ4n0yYUWZlLYT75WllTGJCJtNUI0C1ybN4BCegqvg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.12.0", "@algolia/client-abtesting": "5.46.0", @@ -5002,6 +5010,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6214,6 +6223,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6714,6 +6724,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6935,6 +6946,7 @@ "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.3.0" } @@ -8357,6 +8369,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10304,6 +10317,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11929,6 +11943,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12103,6 +12118,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12331,6 +12347,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12628,6 +12645,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -13160,6 +13178,7 @@ "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", @@ -13641,6 +13660,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/workspace-server/src/__tests__/services/DriveService.test.ts b/workspace-server/src/__tests__/services/DriveService.test.ts index f4e39a76..4d778c53 100644 --- a/workspace-server/src/__tests__/services/DriveService.test.ts +++ b/workspace-server/src/__tests__/services/DriveService.test.ts @@ -101,6 +101,8 @@ describe('DriveService', () => { q: "mimeType='application/vnd.google-apps.folder' and name = 'TestFolder'", fields: 'files(id, name)', spaces: 'drive', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); expect(JSON.parse(result.content[0].text)).toEqual(mockFolders); @@ -116,6 +118,10 @@ describe('DriveService', () => { const result = await driveService.findFolder({ folderName: 'NonExistentFolder' }); expect(mockDriveAPI.files.list).toHaveBeenCalledTimes(1); + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + supportsAllDrives: true, + includeItemsFromAllDrives: true + })); expect(JSON.parse(result.content[0].text)).toEqual([]); }); @@ -145,6 +151,7 @@ describe('DriveService', () => { mimeType: 'application/vnd.google-apps.folder', }, fields: 'id, name', + supportsAllDrives: true, }); expect(JSON.parse(result.content[0].text)).toEqual(mockFolder); @@ -166,6 +173,7 @@ describe('DriveService', () => { parents: ['parent-id'], }, fields: 'id, name', + supportsAllDrives: true, }); expect(JSON.parse(result.content[0].text)).toEqual(mockFolder); @@ -206,6 +214,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -237,6 +247,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -267,6 +279,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -296,6 +310,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -324,6 +340,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -351,6 +369,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -404,6 +424,8 @@ describe('DriveService', () => { pageToken: 'previous-token', corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); }); @@ -425,6 +447,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: 'domain', fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); }); @@ -454,6 +478,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); }); @@ -479,6 +505,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -508,6 +536,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -529,6 +559,7 @@ describe('DriveService', () => { expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: 'file456', fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + supportsAllDrives: true, }); expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); @@ -552,6 +583,7 @@ describe('DriveService', () => { expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: 'doc789', fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + supportsAllDrives: true, }); expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); @@ -597,6 +629,7 @@ describe('DriveService', () => { expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: 'folder789', fields: 'mimeType', + supportsAllDrives: true, }); expect(mockDriveAPI.files.list).toHaveBeenCalledWith({ q: "'folder789' in parents", @@ -604,6 +637,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -627,10 +662,12 @@ describe('DriveService', () => { expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: 'file123', fields: 'mimeType', + supportsAllDrives: true, }); expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: 'file123', fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + supportsAllDrives: true, }); expect(mockDriveAPI.files.list).not.toHaveBeenCalled(); @@ -661,6 +698,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -688,6 +727,8 @@ describe('DriveService', () => { pageToken: undefined, corpus: undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const responseData = JSON.parse(result.content[0].text); @@ -704,12 +745,12 @@ describe('DriveService', () => { mockDriveAPI.files.get.mockImplementation((params: any) => { if (params.alt === 'media') { - return Promise.resolve({ - data: mockBuffer, - }); + return Promise.resolve({ + data: mockBuffer, + }); } return Promise.resolve({ - data: { id: mockFileId, name: 'test.txt', mimeType: 'text/plain' }, + data: { id: mockFileId, name: 'test.txt', mimeType: 'text/plain' }, }); }); @@ -718,10 +759,11 @@ describe('DriveService', () => { expect(mockDriveAPI.files.get).toHaveBeenCalledWith({ fileId: mockFileId, fields: 'id, name, mimeType', + supportsAllDrives: true, }); expect(mockDriveAPI.files.get).toHaveBeenCalledWith( - { fileId: mockFileId, alt: 'media' }, + { fileId: mockFileId, alt: 'media', supportsAllDrives: true }, { responseType: 'arraybuffer' } ); @@ -753,4 +795,56 @@ describe('DriveService', () => { expect(JSON.parse(result.content[0].text)).toEqual({ error: 'API Error' }); }); }); + + describe('Shared Drive Support', () => { + it('findFolder should include shared drive flags (currently fails to do so)', async () => { + mockDriveAPI.files.list.mockResolvedValue({ data: { files: [] } }); + + await driveService.findFolder({ folderName: 'SharedFolder' }); + + // This test is intended to fail until we add shared drive support + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + supportsAllDrives: true, + includeItemsFromAllDrives: true + })); + }); + + it('search should include shared drive flags (currently fails to do so)', async () => { + mockDriveAPI.files.list.mockResolvedValue({ data: { files: [] } }); + + await driveService.search({ query: 'test' }); + + expect(mockDriveAPI.files.list).toHaveBeenCalledWith(expect.objectContaining({ + supportsAllDrives: true, + includeItemsFromAllDrives: true + })); + }); + + it('createFolder should include supportsAllDrives flag (currently fails to do so)', async () => { + mockDriveAPI.files.create.mockResolvedValue({ data: { id: 'new-id', name: 'new' } }); + + await driveService.createFolder({ name: 'New Folder', parentId: 'shared-drive-parent-id' }); + + expect(mockDriveAPI.files.create).toHaveBeenCalledWith(expect.objectContaining({ + supportsAllDrives: true + })); + }); + + it('downloadFile should include supportsAllDrives flag for metadata and media (currently fails to do so)', async () => { + mockDriveAPI.files.get.mockResolvedValueOnce({ data: { mimeType: 'text/plain', name: 'test.txt' } }); + mockDriveAPI.files.get.mockResolvedValueOnce({ data: { data: Buffer.from('content') } }); + + await driveService.downloadFile({ fileId: 'shared-file-id', localPath: 'test.txt' }); + + // First call for metadata + expect(mockDriveAPI.files.get).toHaveBeenNthCalledWith(1, expect.objectContaining({ + supportsAllDrives: true + })); + + // Second call for media + expect(mockDriveAPI.files.get).toHaveBeenNthCalledWith(2, expect.objectContaining({ + supportsAllDrives: true + }), expect.any(Object)); + }); + }); }); \ No newline at end of file diff --git a/workspace-server/src/services/DriveService.ts b/workspace-server/src/services/DriveService.ts index c937051b..7e6fb19f 100644 --- a/workspace-server/src/services/DriveService.ts +++ b/workspace-server/src/services/DriveService.ts @@ -45,6 +45,8 @@ export class DriveService { q: query, fields: 'files(id, name)', spaces: 'drive', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); const folders = res.data.files || []; @@ -85,6 +87,7 @@ export class DriveService { const file = await drive.files.create({ requestBody: fileMetadata, fields: 'id, name', + supportsAllDrives: true, }); logToFile(`Created folder: ${file.data.name} (${file.data.id})`); @@ -137,7 +140,11 @@ export class DriveService { if (urlType === 'unknown') { try { - const file = await drive.files.get({ fileId, fields: 'mimeType' }); + const file = await drive.files.get({ + fileId, + fields: 'mimeType', + supportsAllDrives: true, + }); if (file.data.mimeType === 'application/vnd.google-apps.folder') { isFolder = true; } @@ -155,6 +162,7 @@ export class DriveService { const res = await drive.files.get({ fileId: fileId, fields: 'id, name, modifiedTime, viewedByMeTime, mimeType, parents', + supportsAllDrives: true, }); return { content: [{ @@ -224,7 +232,7 @@ export class DriveService { q = "sharedWithMe"; } } - + logToFile(`Executing Drive search with query: ${q}`); if (corpus) { logToFile(`Using corpus: ${corpus}`); @@ -240,6 +248,8 @@ export class DriveService { pageToken: pageToken, corpus: corpus as 'user' | 'domain' | undefined, fields: 'nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)', + supportsAllDrives: true, + includeItemsFromAllDrives: true, }); let files = res.data.files || []; @@ -279,11 +289,12 @@ export class DriveService { logToFile(`Downloading Drive file ${fileId} to ${localPath}`); try { const drive = await this.getDriveClient(); - + // 1. Check if it's a Google Doc (special handling required, export instead of download) const metadata = await drive.files.get({ fileId: fileId, fields: 'id, name, mimeType', + supportsAllDrives: true, }); const mimeType = metadata.data.mimeType || ''; @@ -304,7 +315,7 @@ export class DriveService { } if (mimeType.includes('vnd.google-apps.')) { - return { + return { content: [{ type: "text" as const, text: `This is a Google Workspace file type (${mimeType}). Direct media download is not supported. Please use specific tools (docs.getText, slides.getText, etc.) or export it if supported.` @@ -316,6 +327,7 @@ export class DriveService { const response = await drive.files.get({ fileId: fileId, alt: 'media', + supportsAllDrives: true, }, { responseType: 'arraybuffer' }); const buffer = Buffer.from(response.data as unknown as ArrayBuffer); @@ -323,7 +335,7 @@ export class DriveService { // 3. Save to localPath const absolutePath = path.isAbsolute(localPath) ? localPath : path.resolve(PROJECT_ROOT, localPath); const dir = path.dirname(absolutePath); - + await fs.promises.mkdir(dir, { recursive: true }); await fs.promises.writeFile(absolutePath, buffer);