UNPKG

@b2y/document-module

Version:

A flexible multi-provider storage adapter for file operations across S3, Azure Blob, Google Drive, and local storage

217 lines (183 loc) 6.55 kB
const { google } = require('googleapis'); const fs = require('fs'); const stream = require('stream'); const BaseStorageProvider = require('./BaseStorageProvider'); const logger = require('../../logger'); class GoogleDriveStorageProvider extends BaseStorageProvider { constructor() { super(); // Initialize Google Drive API client this.auth = new google.auth.GoogleAuth({ keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS, scopes: ['https://www.googleapis.com/auth/drive'] }); this.driveClient = null; this.parentFolderId = process.env.GOOGLE_DRIVE_PARENT_FOLDER_ID; this.urlExpiresIn = process.env.GDRIVE_URL_EXPIRES_IN ? parseInt(process.env.GDRIVE_URL_EXPIRES_IN) : 86400; } async getDriveClient() { if (!this.driveClient) { const authClient = await this.auth.getClient(); this.driveClient = google.drive({ version: 'v3', auth: authClient }); } return this.driveClient; } // Create folder hierarchy if it doesn't exist async ensureFolderPath(folderPath) { const drive = await this.getDriveClient(); const folders = folderPath.split('/').filter(Boolean); let parentId = this.parentFolderId; for (const folderName of folders) { // Check if folder exists const response = await drive.files.list({ q: `name = '${folderName}' and '${parentId}' in parents and trashed = false and mimeType = 'application/vnd.google-apps.folder'`, fields: 'files(id, name)' }); if (response.data.files.length > 0) { // Folder exists, use it as parent for next iteration parentId = response.data.files[0].id; } else { // Create folder const folderMetadata = { name: folderName, mimeType: 'application/vnd.google-apps.folder', parents: [parentId] }; const folder = await drive.files.create({ resource: folderMetadata, fields: 'id' }); parentId = folder.data.id; } } return parentId; } async uploadFile(file, destinationPath) { try { const drive = await this.getDriveClient(); // Parse destination path const pathParts = destinationPath.split('/'); const fileName = pathParts.pop(); const folderPath = pathParts.join('/'); // Ensure folder exists and get final folder ID const folderId = await this.ensureFolderPath(folderPath); let fileContent; let contentType = file.mimetype || 'application/octet-stream'; // If file is a stream if (file.stream) { fileContent = file.stream; } // If file is a path, read from fs else if (typeof file === 'string') { fileContent = fs.createReadStream(file); } // If file is from multer else if (file.path) { fileContent = fs.createReadStream(file.path); contentType = file.mimetype; } // If file is a buffer else if (Buffer.isBuffer(file)) { const bufferStream = new stream.PassThrough(); bufferStream.end(file); fileContent = bufferStream; } const fileMetadata = { name: fileName, parents: [folderId] }; const media = { mimeType: contentType, body: fileContent }; const response = await drive.files.create({ resource: fileMetadata, media: media, fields: 'id, name, webViewLink' }); // Store file ID along with path to retrieve it later const fileId = response.data.id; const storedPath = `${destinationPath}:${fileId}`; return { success: true, path: storedPath, fileId: fileId }; } catch (error) { logger.error('Error uploading file to Google Drive:', error); throw error; } } async getFile(filePath) { try { const drive = await this.getDriveClient(); // Extract file ID from path if it exists let fileId; if (filePath.includes(':')) { fileId = filePath.split(':').pop(); } else { // Need to look up the file by path - this is more complex and less efficient // For simplicity, we recommend storing the fileId with the path throw new Error('File ID not found in path. Please use path:fileId format'); } const response = await drive.files.get({ fileId: fileId, alt: 'media' }, { responseType: 'stream' }); return response.data; } catch (error) { logger.error('Error getting file from Google Drive:', error); throw error; } } async deleteFile(filePath) { try { const drive = await this.getDriveClient(); // Extract file ID from path let fileId; if (filePath.includes(':')) { fileId = filePath.split(':').pop(); } else { throw new Error('File ID not found in path. Please use path:fileId format'); } await drive.files.delete({ fileId }); return { success: true }; } catch (error) { logger.error('Error deleting file from Google Drive:', error); throw error; } } async getPublicUrl(filePath) { try { const drive = await this.getDriveClient(); // Extract file ID from path let fileId; if (filePath.includes(':')) { fileId = filePath.split(':').pop(); } else { throw new Error('File ID not found in path. Please use path:fileId format'); } // First make sure the file has appropriate sharing settings await drive.permissions.create({ fileId: fileId, requestBody: { role: 'reader', type: 'anyone' } }); // Get the web view link const response = await drive.files.get({ fileId: fileId, fields: 'webContentLink, webViewLink' }); // Prefer direct download link if available return response.data.webContentLink || response.data.webViewLink; } catch (error) { logger.error('Error generating public URL for Google Drive file:', error); throw error; } } } module.exports = GoogleDriveStorageProvider;