UNPKG

@b2y/document-module

Version:

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

208 lines (176 loc) 7.86 kB
// src/providers/AzureBlobStorageProvider.js const { BlobServiceClient, StorageSharedKeyCredential, generateBlobSASQueryParameters, BlobSASPermissions } = require('@azure/storage-blob'); const fs = require('fs'); const BaseStorageProvider = require('./BaseStorageProvider'); const logger = require('../../logger'); class AzureBlobStorageProvider extends BaseStorageProvider { constructor(config = {}) { super(); // Get Azure credentials from config or environment variables const accountName = config.accountName || process.env.AZURE_STORAGE_ACCOUNT_NAME; const accountKey = config.accountKey || process.env.AZURE_STORAGE_ACCOUNT_KEY; const connectionString = config.connectionString || process.env.AZURE_STORAGE_CONNECTION_STRING; // Initialize client either with connection string or with credentials if (connectionString) { this.blobServiceClient = BlobServiceClient.fromConnectionString(connectionString); } else if (accountName && accountKey) { const sharedKeyCredential = new StorageSharedKeyCredential(accountName, accountKey); const blobServiceUrl = `https://${accountName}.blob.core.windows.net`; this.blobServiceClient = new BlobServiceClient(blobServiceUrl, sharedKeyCredential); this.sharedKeyCredential = sharedKeyCredential; // Store for SAS generation } else { throw new Error('Azure Storage credentials not provided. Set AZURE_STORAGE_CONNECTION_STRING or both AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY'); } this.containerName = config.containerName || process.env.AZURE_STORAGE_CONTAINER_NAME; this.containerClient = this.blobServiceClient.getContainerClient(this.containerName); this.accountName = accountName || this.extractAccountNameFromConnectionString(connectionString); // SAS token expiry duration in seconds (default 24 hours) this.sasExpiryTime = config.sasExpiryTime || process.env.AZURE_SAS_EXPIRY_SECONDS ? parseInt(config.sasExpiryTime || process.env.AZURE_SAS_EXPIRY_SECONDS) : 86400; this.publicAccess = config.publicAccess || process.env.AZURE_PUBLIC_ACCESS === 'true'; } // Helper to extract account name from connection string if needed extractAccountNameFromConnectionString(connectionString) { if (!connectionString) return null; const accountNameMatch = connectionString.match(/AccountName=([^;]+)/i); return accountNameMatch ? accountNameMatch[1] : null; } async ensureContainerExists() { try { const containerExists = await this.containerClient.exists(); if (!containerExists) { logger.info(`Creating container ${this.containerName}`); await this.containerClient.create({ access: 'blob' }); } } catch (error) { logger.error(`Error ensuring container exists: ${error.message}`); throw error; } } async uploadFile(file, destinationPath) { try { // Ensure container exists await this.ensureContainerExists(); // Get a block blob client const blockBlobClient = this.containerClient.getBlockBlobClient(destinationPath); let fileContent; let contentType = 'application/octet-stream'; // If file is a stream if (file.stream) { fileContent = file.stream; contentType = file.mimetype || contentType; } // 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 || contentType; } // If file is a buffer else if (Buffer.isBuffer(file)) { fileContent = file; } // Upload the file const uploadOptions = { blobHTTPHeaders: { blobContentType: contentType } }; // If it's a stream, use uploadStream if (fileContent.pipe && typeof fileContent.pipe === 'function') { await blockBlobClient.uploadStream(fileContent, undefined, undefined, uploadOptions); } else { // Otherwise use uploadData await blockBlobClient.uploadData(fileContent, uploadOptions); } // Store the complete path in the format containerName/blobName const storedPath = `${this.containerName}/${destinationPath}`; return { success: true, path: storedPath }; } catch (error) { logger.error('Error uploading file to Azure Blob Storage:', error); throw error; } } async getFile(filePath) { try { // Extract container and blob path if stored in combined format let blobPath = filePath; if (filePath.startsWith(`${this.containerName}/`)) { blobPath = filePath.substring(this.containerName.length + 1); } const blockBlobClient = this.containerClient.getBlockBlobClient(blobPath); const downloadResponse = await blockBlobClient.download(0); return downloadResponse.readableStreamBody; } catch (error) { logger.error('Error getting file from Azure Blob Storage:', error); throw error; } } async deleteFile(filePath) { try { // Extract container and blob path if stored in combined format let blobPath = filePath; if (filePath.startsWith(`${this.containerName}/`)) { blobPath = filePath.substring(this.containerName.length + 1); } const blockBlobClient = this.containerClient.getBlockBlobClient(blobPath); await blockBlobClient.delete(); return { success: true }; } catch (error) { logger.error('Error deleting file from Azure Blob Storage:', error); throw error; } } async getPublicUrl(filePath) { try { // Extract container and blob path if stored in combined format let blobPath = filePath; if (filePath.startsWith(`${this.containerName}/`)) { blobPath = filePath.substring(this.containerName.length + 1); } const blockBlobClient = this.containerClient.getBlockBlobClient(blobPath); // If container is configured for public access if (this.publicAccess) { return blockBlobClient.url; } // Otherwise generate a SAS token if (!this.sharedKeyCredential && !process.env.AZURE_STORAGE_ACCOUNT_KEY) { throw new Error('Shared key credential required for SAS token generation'); } // Create shared key credential if not already created if (!this.sharedKeyCredential) { this.sharedKeyCredential = new StorageSharedKeyCredential( this.accountName, process.env.AZURE_STORAGE_ACCOUNT_KEY ); } // Set expiry time const expiryTime = new Date(); expiryTime.setSeconds(expiryTime.getSeconds() + this.sasExpiryTime); // Set permissions const permissions = new BlobSASPermissions(); permissions.read = true; // Generate SAS token const sasToken = generateBlobSASQueryParameters({ containerName: this.containerName, blobName: blobPath, permissions: permissions, startsOn: new Date(), expiresOn: expiryTime, }, this.sharedKeyCredential).toString(); // Return full URL with SAS token return `${blockBlobClient.url}?${sasToken}`; } catch (error) { logger.error('Error generating public URL for Azure Blob Storage:', error); throw error; } } } module.exports = AzureBlobStorageProvider;