// Google Drive API service wrapper
import { google, drive_v3 } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import { resolveFolderId } from '../utils/paths.js';
import { FOLDER_MIME_TYPE, getMimeTypeFromFilename } from '../utils/mime.js';
import { NotFoundError, withErrorHandling } from '../utils/errors.js';
export class DriveService {
private drive: drive_v3.Drive;
constructor(authClient: OAuth2Client) {
this.drive = google.drive({ version: 'v3', auth: authClient });
}
/**
* Get the underlying drive API for advanced operations
*/
get api(): drive_v3.Drive {
return this.drive;
}
// ─────────────────────────────────────────────────────────────────────────────
// SEARCH & LIST
// ─────────────────────────────────────────────────────────────────────────────
async search(query: string, pageSize = 10, pageToken?: string) {
return withErrorHandling(async () => {
const response = await this.drive.files.list({
q: query,
pageSize,
pageToken,
fields: 'nextPageToken, files(id, name, mimeType, modifiedTime, size, parents)',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
});
return {
files: response.data.files || [],
nextPageToken: response.data.nextPageToken,
};
}, 'Search failed');
}
async listFolder(folderId: string, pageSize = 100, pageToken?: string) {
const resolvedId = await resolveFolderId({ files: this.drive.files }, folderId);
return withErrorHandling(async () => {
const response = await this.drive.files.list({
q: `'${resolvedId}' in parents and trashed = false`,
pageSize,
pageToken,
fields: 'nextPageToken, files(id, name, mimeType, modifiedTime, size)',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
});
return {
files: response.data.files || [],
nextPageToken: response.data.nextPageToken,
};
}, 'List folder failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// FILE OPERATIONS
// ─────────────────────────────────────────────────────────────────────────────
async getFile(fileId: string) {
return withErrorHandling(async () => {
const response = await this.drive.files.get({
fileId,
fields: 'id, name, mimeType, modifiedTime, size, parents, webViewLink',
supportsAllDrives: true,
});
if (!response.data.id) {
throw new NotFoundError('File', fileId);
}
return response.data;
}, 'Get file failed');
}
async createTextFile(name: string, content: string, parentFolder?: string) {
const parentId = await resolveFolderId({ files: this.drive.files }, parentFolder);
const mimeType = getMimeTypeFromFilename(name);
return withErrorHandling(async () => {
const response = await this.drive.files.create({
requestBody: {
name,
mimeType,
parents: [parentId],
},
media: {
mimeType,
body: content,
},
fields: 'id, name, mimeType, webViewLink',
supportsAllDrives: true,
});
return response.data;
}, 'Create text file failed');
}
async updateTextFile(fileId: string, content: string) {
return withErrorHandling(async () => {
const file = await this.getFile(fileId);
const response = await this.drive.files.update({
fileId,
media: {
mimeType: file.mimeType || 'text/plain',
body: content,
},
fields: 'id, name, mimeType, modifiedTime',
supportsAllDrives: true,
});
return response.data;
}, 'Update text file failed');
}
async deleteFile(fileId: string) {
return withErrorHandling(async () => {
await this.drive.files.delete({
fileId,
supportsAllDrives: true,
});
return { success: true, fileId };
}, 'Delete file failed');
}
async renameFile(fileId: string, newName: string) {
return withErrorHandling(async () => {
const response = await this.drive.files.update({
fileId,
requestBody: { name: newName },
fields: 'id, name, mimeType',
supportsAllDrives: true,
});
return response.data;
}, 'Rename file failed');
}
async moveFile(fileId: string, newParentFolder: string) {
const newParentId = await resolveFolderId({ files: this.drive.files }, newParentFolder);
return withErrorHandling(async () => {
// Get current parents
const file = await this.getFile(fileId);
const previousParents = file.parents?.join(',') || '';
const response = await this.drive.files.update({
fileId,
addParents: newParentId,
removeParents: previousParents,
fields: 'id, name, parents',
supportsAllDrives: true,
});
return response.data;
}, 'Move file failed');
}
async copyFile(fileId: string, newName?: string, destinationFolder?: string) {
const parentId = destinationFolder
? await resolveFolderId({ files: this.drive.files }, destinationFolder)
: undefined;
return withErrorHandling(async () => {
const response = await this.drive.files.copy({
fileId,
requestBody: {
name: newName,
parents: parentId ? [parentId] : undefined,
},
fields: 'id, name, mimeType, webViewLink',
supportsAllDrives: true,
});
return response.data;
}, 'Copy file failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// FOLDER OPERATIONS
// ─────────────────────────────────────────────────────────────────────────────
async createFolder(name: string, parentFolder?: string) {
const parentId = await resolveFolderId({ files: this.drive.files }, parentFolder);
return withErrorHandling(async () => {
const response = await this.drive.files.create({
requestBody: {
name,
mimeType: FOLDER_MIME_TYPE,
parents: [parentId],
},
fields: 'id, name, mimeType, webViewLink',
supportsAllDrives: true,
});
return response.data;
}, 'Create folder failed');
}
/**
* Create a Google Workspace file (Doc, Sheet, Slides)
*/
async createGoogleWorkspaceFile(name: string, mimeType: string, parentFolder?: string) {
const parentId = await resolveFolderId({ files: this.drive.files }, parentFolder);
return withErrorHandling(async () => {
const response = await this.drive.files.create({
requestBody: {
name,
mimeType,
parents: [parentId],
},
fields: 'id, name, mimeType, webViewLink',
supportsAllDrives: true,
});
return response.data;
}, 'Create Google Workspace file failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// BINARY FILE UPLOAD
// ─────────────────────────────────────────────────────────────────────────────
async uploadBinaryFile(
name: string,
content: Buffer,
mimeType: string,
parentFolder?: string
): Promise<drive_v3.Schema$File> {
const parentId = parentFolder
? await resolveFolderId({ files: this.drive.files }, parentFolder)
: undefined;
return withErrorHandling(async () => {
const { Readable } = await import('stream');
const stream = Readable.from(content);
const response = await this.drive.files.create({
requestBody: {
name,
mimeType,
parents: parentId ? [parentId] : undefined,
},
media: {
mimeType,
body: stream,
},
fields: 'id, name, mimeType, size, webViewLink',
supportsAllDrives: true,
});
return response.data;
}, 'Upload binary file failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// DOWNLOAD / EXPORT
// ─────────────────────────────────────────────────────────────────────────────
async downloadFile(fileId: string): Promise<Buffer> {
return withErrorHandling(async () => {
const response = await this.drive.files.get(
{ fileId, alt: 'media', supportsAllDrives: true },
{ responseType: 'arraybuffer' }
);
return Buffer.from(response.data as ArrayBuffer);
}, 'Download file failed');
}
async exportFile(fileId: string, mimeType: string): Promise<Buffer> {
return withErrorHandling(async () => {
const response = await this.drive.files.export(
{ fileId, mimeType },
{ responseType: 'arraybuffer' }
);
return Buffer.from(response.data as ArrayBuffer);
}, 'Export file failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// ACCOUNT INFO
// ─────────────────────────────────────────────────────────────────────────────
async getAbout(): Promise<drive_v3.Schema$About> {
return withErrorHandling(async () => {
const response = await this.drive.about.get({
fields: 'user, storageQuota',
});
return response.data;
}, 'Get account info failed');
}
// ─────────────────────────────────────────────────────────────────────────────
// PERMISSIONS (SHARING)
// ─────────────────────────────────────────────────────────────────────────────
/**
* List permissions for a file or folder
*/
async listPermissions(fileId: string): Promise<drive_v3.Schema$Permission[]> {
return withErrorHandling(async () => {
const response = await this.drive.permissions.list({
fileId,
fields: 'permissions(id, type, role, emailAddress, domain, displayName, expirationTime, deleted)',
supportsAllDrives: true,
});
return response.data.permissions || [];
}, 'List permissions failed');
}
/**
* Get a specific permission
*/
async getPermission(fileId: string, permissionId: string): Promise<drive_v3.Schema$Permission> {
return withErrorHandling(async () => {
const response = await this.drive.permissions.get({
fileId,
permissionId,
fields: 'id, type, role, emailAddress, domain, displayName, expirationTime, deleted',
supportsAllDrives: true,
});
return response.data;
}, 'Get permission failed');
}
/**
* Share a file with a user, group, domain, or anyone
*/
async createPermission(
fileId: string,
type: 'user' | 'group' | 'domain' | 'anyone',
role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader',
options?: {
emailAddress?: string;
domain?: string;
sendNotificationEmail?: boolean;
emailMessage?: string;
transferOwnership?: boolean;
moveToNewOwnersRoot?: boolean;
expirationTime?: string;
}
): Promise<drive_v3.Schema$Permission> {
return withErrorHandling(async () => {
const requestBody: drive_v3.Schema$Permission = {
type,
role,
};
if (options?.emailAddress) {
requestBody.emailAddress = options.emailAddress;
}
if (options?.domain) {
requestBody.domain = options.domain;
}
if (options?.expirationTime) {
requestBody.expirationTime = options.expirationTime;
}
const response = await this.drive.permissions.create({
fileId,
requestBody,
sendNotificationEmail: options?.sendNotificationEmail ?? true,
emailMessage: options?.emailMessage,
transferOwnership: options?.transferOwnership,
moveToNewOwnersRoot: options?.moveToNewOwnersRoot,
fields: 'id, type, role, emailAddress, domain, displayName, expirationTime',
supportsAllDrives: true,
});
return response.data;
}, 'Create permission failed');
}
/**
* Update a permission (change role)
*/
async updatePermission(
fileId: string,
permissionId: string,
role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader',
options?: {
expirationTime?: string;
transferOwnership?: boolean;
}
): Promise<drive_v3.Schema$Permission> {
return withErrorHandling(async () => {
const response = await this.drive.permissions.update({
fileId,
permissionId,
requestBody: {
role,
expirationTime: options?.expirationTime,
},
transferOwnership: options?.transferOwnership,
fields: 'id, type, role, emailAddress, domain, displayName, expirationTime',
supportsAllDrives: true,
});
return response.data;
}, 'Update permission failed');
}
/**
* Remove a permission (unshare)
*/
async deletePermission(fileId: string, permissionId: string): Promise<void> {
return withErrorHandling(async () => {
await this.drive.permissions.delete({
fileId,
permissionId,
supportsAllDrives: true,
});
}, 'Delete permission failed');
}
}