file-manager.ts•16.3 kB
import { google, drive_v3 } from 'googleapis';
import { AuthService } from '../auth/auth-service.js';
import {
FileManager,
DriveFile,
FileMetadata,
User,
Permission,
FileCapabilities
} from '../types/drive.js';
import { SearchQuery } from '../types/search.js';
import { ResilientExecutor } from '../utils/retry-handler.js';
import {
InvalidFileIdError,
PermissionDeniedError,
FileTooLargeError,
UnsupportedFileTypeError
} from '../types/errors.js';
/**
* Google Drive File Manager
* Handles file operations with Google Drive API including download, metadata retrieval, and folder listing
*/
export class GoogleDriveFileManager implements FileManager {
private driveApi: drive_v3.Drive;
private authService: AuthService;
private resilientExecutor: ResilientExecutor;
constructor(
authService: AuthService,
resilientExecutor?: ResilientExecutor
) {
this.authService = authService;
this.driveApi = google.drive({
version: 'v3',
auth: authService.getOAuthManager().getOAuth2Client()
});
this.resilientExecutor = resilientExecutor || new ResilientExecutor();
}
/**
* Search files on Google Drive
*/
async searchFiles(query: SearchQuery): Promise<DriveFile[]> {
return this.resilientExecutor.execute(async () => {
return this.authService.executeWithAuth(async () => {
const searchQuery = this.buildSearchQuery(query);
const response = await this.driveApi.files.list({
q: searchQuery,
pageSize: query.limit || 100,
fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents)',
orderBy: query.orderBy ? `${query.orderBy} ${query.orderDirection || 'desc'}` : 'modifiedTime desc',
supportsAllDrives: true,
includeItemsFromAllDrives: true
});
if (!response.data.files) {
return [];
}
return response.data.files.map(file => this.mapGoogleFileTodriveFile(file));
}, 'searchFiles');
}, { operation: 'searchFiles', query });
}
/**
* Get file information
*/
async getFile(fileId: string): Promise<DriveFile> {
// Validate file ID format
if (!this.isValidFileId(fileId)) {
throw new InvalidFileIdError(`Invalid Google Drive file ID format: ${fileId}`);
}
return this.resilientExecutor.execute(async () => {
return this.authService.executeWithAuth(async () => {
try {
const response = await this.driveApi.files.get({
fileId,
fields: 'id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents',
supportsAllDrives: true
});
return this.mapGoogleFileTodriveFile(response.data);
} catch (error: any) {
if (error.status === 404) {
throw new InvalidFileIdError(`File not found: ${fileId}. The file may have been deleted or you may not have access to it.`);
}
if (error.status === 403) {
throw new PermissionDeniedError(`Access denied to file: ${fileId}. You may not have permission to view this file.`);
}
throw error;
}
}, 'getFile');
}, { operation: 'getFile', fileId });
}
/**
* Download file content
*/
async downloadFile(fileId: string): Promise<Buffer> {
// Validate file ID format
if (!this.isValidFileId(fileId)) {
throw new InvalidFileIdError(`Invalid Google Drive file ID format: ${fileId}`);
}
return this.resilientExecutor.execute(async () => {
return this.authService.executeWithAuth(async () => {
try {
// First get file metadata to determine how to download
const fileMetadata = await this.driveApi.files.get({
fileId,
fields: 'mimeType, name, size',
supportsAllDrives: true
});
const mimeType = fileMetadata.data.mimeType;
const fileSize = parseInt(fileMetadata.data.size || '0');
// Check file size limits (100MB default)
const maxFileSize = 100 * 1024 * 1024; // 100MB
if (fileSize > maxFileSize) {
throw new FileTooLargeError(
`File is too large (${Math.round(fileSize / 1024 / 1024)}MB). Maximum supported size is ${Math.round(maxFileSize / 1024 / 1024)}MB.`,
{ fileId, fileSize, maxFileSize }
);
}
// Handle Google Workspace files (need to export)
if (mimeType?.startsWith('application/vnd.google-apps.')) {
return this.exportGoogleWorkspaceFile(fileId, mimeType);
}
// Check if file type is supported
if (!this.isSupportedFileType(mimeType || undefined)) {
throw new UnsupportedFileTypeError(
`File type not supported: ${mimeType}. Supported types include PDF, Word documents, Google Docs, and plain text files.`,
{ fileId, mimeType }
);
}
// Handle regular files (direct download)
const response = await this.driveApi.files.get({
fileId,
alt: 'media',
supportsAllDrives: true
}, {
responseType: 'arraybuffer'
});
return Buffer.from(response.data as ArrayBuffer);
} catch (error: any) {
if (error.status === 404) {
throw new InvalidFileIdError(`File not found: ${fileId}`);
}
if (error.status === 403) {
throw new PermissionDeniedError(`Access denied to file: ${fileId}`);
}
throw error;
}
}, 'downloadFile');
}, { operation: 'downloadFile', fileId });
}
/**
* Get comprehensive file metadata
*/
async getFileMetadata(fileId: string): Promise<FileMetadata> {
return this.authService.executeWithAuth(async () => {
const response = await this.driveApi.files.get({
fileId,
fields: 'id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents, description, starred, trashed, version, lastModifyingUser, sharingUser, capabilities',
supportsAllDrives: true
});
return this.mapGoogleFileToFileMetadata(response.data);
}, 'getFileMetadata');
}
/**
* List folder contents
*/
async listFolder(folderId: string): Promise<DriveFile[]> {
return this.authService.executeWithAuth(async () => {
const response = await this.driveApi.files.list({
q: `'${folderId}' in parents and trashed = false`,
fields: 'nextPageToken, files(id, name, mimeType, size, modifiedTime, createdTime, owners, permissions, webViewLink, webContentLink, parents)',
orderBy: 'folder, name',
supportsAllDrives: true,
includeItemsFromAllDrives: true
});
if (!response.data.files) {
return [];
}
return response.data.files.map(file => this.mapGoogleFileTodriveFile(file));
}, 'listFolder');
}
/**
* Build Google Drive search query from SearchQuery object
*/
private buildSearchQuery(query: SearchQuery): string {
const conditions: string[] = [];
// Text search in name and content
if (query.text) {
conditions.push(`(name contains '${this.escapeQueryString(query.text)}' or fullText contains '${this.escapeQueryString(query.text)}')`);
}
// File type filter
if (query.fileType && query.fileType.length > 0) {
const mimeTypeConditions = query.fileType.map((type: string) => {
// Handle common file type shortcuts
const mimeTypeMap: { [key: string]: string } = {
'pdf': 'application/pdf',
'doc': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'gdoc': 'application/vnd.google-apps.document',
'gsheet': 'application/vnd.google-apps.spreadsheet',
'gslide': 'application/vnd.google-apps.presentation',
'folder': 'application/vnd.google-apps.folder',
'image': 'image/',
'video': 'video/',
'audio': 'audio/'
};
const mimeType = mimeTypeMap[type.toLowerCase()] || type;
// For partial mime types (like 'image/'), use contains
if (mimeType.endsWith('/')) {
return `mimeType contains '${mimeType}'`;
}
return `mimeType = '${mimeType}'`;
});
conditions.push(`(${mimeTypeConditions.join(' or ')})`);
}
// Folder filter
if (query.folderId) {
conditions.push(`'${query.folderId}' in parents`);
}
// Date filters
if (query.modifiedAfter) {
conditions.push(`modifiedTime > '${query.modifiedAfter.toISOString()}'`);
}
if (query.modifiedBefore) {
conditions.push(`modifiedTime < '${query.modifiedBefore.toISOString()}'`);
}
// Owner filter
if (query.owner) {
conditions.push(`'${this.escapeQueryString(query.owner)}' in owners`);
}
// Always exclude trashed files unless explicitly requested
conditions.push('trashed = false');
return conditions.join(' and ');
}
/**
* Export Google Workspace files to appropriate formats
*/
private async exportGoogleWorkspaceFile(fileId: string, mimeType: string): Promise<Buffer> {
let exportMimeType: string;
// Determine export format based on Google Workspace file type
switch (mimeType) {
case 'application/vnd.google-apps.document':
exportMimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; // .docx
break;
case 'application/vnd.google-apps.spreadsheet':
exportMimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; // .xlsx
break;
case 'application/vnd.google-apps.presentation':
exportMimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; // .pptx
break;
case 'application/vnd.google-apps.drawing':
exportMimeType = 'image/png';
break;
default:
// Fallback to plain text for other Google Workspace files
exportMimeType = 'text/plain';
}
const response = await this.driveApi.files.export({
fileId,
mimeType: exportMimeType
}, {
responseType: 'arraybuffer'
});
return Buffer.from(response.data as ArrayBuffer);
}
/**
* Map Google Drive API file object to our DriveFile interface
*/
private mapGoogleFileTodriveFile(file: drive_v3.Schema$File): DriveFile {
return {
id: file.id!,
name: file.name || 'Untitled',
mimeType: file.mimeType || 'application/octet-stream',
size: parseInt(file.size || '0'),
modifiedTime: new Date(file.modifiedTime || Date.now()),
createdTime: new Date(file.createdTime || Date.now()),
owners: (file.owners || []).map(owner => this.mapGoogleUserToUser(owner)),
permissions: (file.permissions || []).map(permission => this.mapGooglePermissionToPermission(permission)),
webViewLink: file.webViewLink || '',
webContentLink: file.webContentLink || '',
parents: file.parents || []
};
}
/**
* Map Google Drive API file object to our FileMetadata interface
*/
private mapGoogleFileToFileMetadata(file: drive_v3.Schema$File): FileMetadata {
return {
id: file.id!,
name: file.name || 'Untitled',
mimeType: file.mimeType || 'application/octet-stream',
size: parseInt(file.size || '0'),
modifiedTime: new Date(file.modifiedTime || Date.now()),
createdTime: new Date(file.createdTime || Date.now()),
owners: (file.owners || []).map(owner => this.mapGoogleUserToUser(owner)),
permissions: (file.permissions || []).map(permission => this.mapGooglePermissionToPermission(permission)),
webViewLink: file.webViewLink || '',
webContentLink: file.webContentLink || '',
parents: file.parents || [],
description: file.description || '',
starred: file.starred || false,
trashed: file.trashed || false,
version: file.version || '1',
lastModifyingUser: this.mapGoogleUserToUser(file.lastModifyingUser || {}),
sharingUser: this.mapGoogleUserToUser(file.sharingUser || {}),
capabilities: this.mapGoogleCapabilitiesToFileCapabilities(file.capabilities || {})
};
}
/**
* Map Google Drive API user object to our User interface
*/
private mapGoogleUserToUser(user: drive_v3.Schema$User): User {
return {
displayName: user.displayName || 'Unknown User',
emailAddress: user.emailAddress || '',
photoLink: user.photoLink || '',
me: user.me || false
};
}
/**
* Map Google Drive API permission object to our Permission interface
*/
private mapGooglePermissionToPermission(permission: drive_v3.Schema$Permission): Permission {
return {
id: permission.id!,
type: (permission.type as any) || 'user',
role: (permission.role as any) || 'reader',
emailAddress: permission.emailAddress || '',
domain: permission.domain || '',
displayName: permission.displayName || ''
};
}
/**
* Map Google Drive API capabilities to our FileCapabilities interface
*/
private mapGoogleCapabilitiesToFileCapabilities(capabilities: any): FileCapabilities {
return {
canAddChildren: capabilities.canAddChildren || false,
canChangeCopyRequiresWriterPermission: capabilities.canChangeCopyRequiresWriterPermission || false,
canChangeViewersCanCopyContent: capabilities.canChangeViewersCanCopyContent || false,
canComment: capabilities.canComment || false,
canCopy: capabilities.canCopy || false,
canDelete: capabilities.canDelete || false,
canDownload: capabilities.canDownload || false,
canEdit: capabilities.canEdit || false,
canListChildren: capabilities.canListChildren || false,
canMoveItemIntoTeamDrive: capabilities.canMoveItemIntoTeamDrive || false,
canMoveItemOutOfTeamDrive: capabilities.canMoveItemOutOfTeamDrive || false,
canMoveItemWithinTeamDrive: capabilities.canMoveItemWithinTeamDrive || false,
canReadRevisions: capabilities.canReadRevisions || false,
canRemoveChildren: capabilities.canRemoveChildren || false,
canRename: capabilities.canRename || false,
canShare: capabilities.canShare || false,
canTrash: capabilities.canTrash || false,
canUntrash: capabilities.canUntrash || false
};
}
/**
* Escape special characters in search query strings
*/
private escapeQueryString(query: string): string {
// Escape single quotes and backslashes for Google Drive search
return query.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
/**
* Validate Google Drive file ID format
*/
private isValidFileId(fileId: string): boolean {
// Google Drive file IDs are typically 28-44 characters long and contain alphanumeric characters, hyphens, and underscores
const fileIdRegex = /^[a-zA-Z0-9_-]{10,50}$/;
return fileIdRegex.test(fileId);
}
/**
* Check if file type is supported for processing
*/
private isSupportedFileType(mimeType?: string): boolean {
if (!mimeType) return false;
const supportedTypes = [
// Google Workspace files
'application/vnd.google-apps.document',
'application/vnd.google-apps.spreadsheet',
'application/vnd.google-apps.presentation',
// Microsoft Office files
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
'application/msword', // .doc
// PDF files
'application/pdf',
// Text files
'text/plain',
'text/markdown',
'text/html',
'text/csv',
// Other common formats
'application/rtf',
'application/json'
];
return supportedTypes.includes(mimeType) || mimeType.startsWith('text/');
}
}