@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
JavaScript
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;