🔧 fix: Azure Blob Integration and File Source References (#6575)

* 🔧 fix: Update file source references to include 'azure_blob' for correct service initialization

* 🔧 fix: Add Azure Blob Storage Emulator entries to .gitignore

* fix: Update file source references to include 'azure_blob' for correct service initialization

* fix: Refactor Azure Blob Storage functions to use environment variables for access control and container name, fix deletion improper logging and improper params

* fix: Add basePath determination for agent file uploads based on MIME type

* fix: Implement file streaming to Azure Blob Storage to optimize memory usage during uploads (non-images)

* fix: Update SourceIcon to include 'azure_blob' class and adjust model setting in useSelectorEffects for assistants

* chore: import order

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Ruben Talstra 2025-03-31 19:44:20 +02:00 committed by GitHub
parent d60f2ed50b
commit bc039cea29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 93 additions and 27 deletions

4
.gitignore vendored
View file

@ -37,6 +37,10 @@ client/public/main.js
client/public/main.js.map client/public/main.js.map
client/public/main.js.LICENSE.txt client/public/main.js.LICENSE.txt
# Azure Blob Storage Emulator (Azurite)
__azurite**
__blobstorage__/**/*
# Dependency directorys # Dependency directorys
# Deployed apps should consider commenting these lines out: # Deployed apps should consider commenting these lines out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git

View file

@ -52,7 +52,7 @@ const AppService = async (app) => {
if (fileStrategy === FileSources.firebase) { if (fileStrategy === FileSources.firebase) {
initializeFirebase(); initializeFirebase();
} else if (fileStrategy === FileSources.azure) { } else if (fileStrategy === FileSources.azure_blob) {
initializeAzureBlobService(); initializeAzureBlobService();
} else if (fileStrategy === FileSources.s3) { } else if (fileStrategy === FileSources.s3) {
initializeS3(); initializeS3();

View file

@ -1,11 +1,13 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const mime = require('mime');
const axios = require('axios'); const axios = require('axios');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const { logger } = require('~/config'); const { logger } = require('~/config');
const { getAzureContainerClient } = require('./initialize'); const { getAzureContainerClient } = require('./initialize');
const defaultBasePath = 'images'; const defaultBasePath = 'images';
const { AZURE_STORAGE_PUBLIC_ACCESS = 'true', AZURE_CONTAINER_NAME = 'files' } = process.env;
/** /**
* Uploads a buffer to Azure Blob Storage. * Uploads a buffer to Azure Blob Storage.
@ -29,10 +31,9 @@ async function saveBufferToAzure({
}) { }) {
try { try {
const containerClient = getAzureContainerClient(containerName); const containerClient = getAzureContainerClient(containerName);
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
// Create the container if it doesn't exist. This is done per operation. // Create the container if it doesn't exist. This is done per operation.
await containerClient.createIfNotExists({ await containerClient.createIfNotExists({ access });
access: process.env.AZURE_STORAGE_PUBLIC_ACCESS ? 'blob' : undefined,
});
const blobPath = `${basePath}/${userId}/${fileName}`; const blobPath = `${basePath}/${userId}/${fileName}`;
const blockBlobClient = containerClient.getBlockBlobClient(blobPath); const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
await blockBlobClient.uploadData(buffer); await blockBlobClient.uploadData(buffer);
@ -97,25 +98,21 @@ async function getAzureURL({ fileName, basePath = defaultBasePath, userId, conta
* Deletes a blob from Azure Blob Storage. * Deletes a blob from Azure Blob Storage.
* *
* @param {Object} params * @param {Object} params
* @param {string} params.fileName - The name of the file. * @param {ServerRequest} params.req - The Express request object.
* @param {string} [params.basePath='images'] - The base folder where the file is stored. * @param {MongoFile} params.file - The file object.
* @param {string} params.userId - The user's id.
* @param {string} [params.containerName] - The Azure Blob container name.
*/ */
async function deleteFileFromAzure({ async function deleteFileFromAzure(req, file) {
fileName,
basePath = defaultBasePath,
userId,
containerName,
}) {
try { try {
const containerClient = getAzureContainerClient(containerName); const containerClient = getAzureContainerClient(AZURE_CONTAINER_NAME);
const blobPath = `${basePath}/${userId}/${fileName}`; const blobPath = file.filepath.split(`${AZURE_CONTAINER_NAME}/`)[1];
if (!blobPath.includes(req.user.id)) {
throw new Error('User ID not found in blob path');
}
const blockBlobClient = containerClient.getBlockBlobClient(blobPath); const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
await blockBlobClient.delete(); await blockBlobClient.delete();
logger.debug('[deleteFileFromAzure] Blob deleted successfully from Azure Blob Storage'); logger.debug('[deleteFileFromAzure] Blob deleted successfully from Azure Blob Storage');
} catch (error) { } catch (error) {
logger.error('[deleteFileFromAzure] Error deleting blob:', error.message); logger.error('[deleteFileFromAzure] Error deleting blob:', error);
if (error.statusCode === 404) { if (error.statusCode === 404) {
return; return;
} }
@ -123,6 +120,65 @@ async function deleteFileFromAzure({
} }
} }
/**
* Streams a file from disk directly to Azure Blob Storage without loading
* the entire file into memory.
*
* @param {Object} params
* @param {string} params.userId - The user's id.
* @param {string} params.filePath - The local file path to upload.
* @param {string} params.fileName - The name of the file in Azure.
* @param {string} [params.basePath='images'] - The base folder within the container.
* @param {string} [params.containerName] - The Azure Blob container name.
* @returns {Promise<string>} The URL of the uploaded blob.
*/
async function streamFileToAzure({
userId,
filePath,
fileName,
basePath = defaultBasePath,
containerName,
}) {
try {
const containerClient = getAzureContainerClient(containerName);
const access = AZURE_STORAGE_PUBLIC_ACCESS?.toLowerCase() === 'true' ? 'blob' : undefined;
// Create the container if it doesn't exist
await containerClient.createIfNotExists({ access });
const blobPath = `${basePath}/${userId}/${fileName}`;
const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
// Get file size for proper content length
const stats = await fs.promises.stat(filePath);
// Create read stream from the file
const fileStream = fs.createReadStream(filePath);
const blobContentType = mime.getType(fileName);
await blockBlobClient.uploadStream(
fileStream,
undefined, // Use default concurrency (5)
undefined, // Use default buffer size (8MB)
{
blobHTTPHeaders: {
blobContentType,
},
onProgress: (progress) => {
logger.debug(
`[streamFileToAzure] Upload progress: ${progress.loadedBytes} bytes of ${stats.size}`,
);
},
},
);
return blockBlobClient.url;
} catch (error) {
logger.error('[streamFileToAzure] Error streaming file:', error);
throw error;
}
}
/** /**
* Uploads a file from the local file system to Azure Blob Storage. * Uploads a file from the local file system to Azure Blob Storage.
* *
@ -146,18 +202,19 @@ async function uploadFileToAzure({
}) { }) {
try { try {
const inputFilePath = file.path; const inputFilePath = file.path;
const inputBuffer = await fs.promises.readFile(inputFilePath); const stats = await fs.promises.stat(inputFilePath);
const bytes = Buffer.byteLength(inputBuffer); const bytes = stats.size;
const userId = req.user.id; const userId = req.user.id;
const fileName = `${file_id}__${path.basename(inputFilePath)}`; const fileName = `${file_id}__${path.basename(inputFilePath)}`;
const fileURL = await saveBufferToAzure({
const fileURL = await streamFileToAzure({
userId, userId,
buffer: inputBuffer, filePath: inputFilePath,
fileName, fileName,
basePath, basePath,
containerName, containerName,
}); });
await fs.promises.unlink(inputFilePath);
return { filepath: fileURL, bytes }; return { filepath: fileURL, bytes };
} catch (error) { } catch (error) {
logger.error('[uploadFileToAzure] Error uploading file:', error); logger.error('[uploadFileToAzure] Error uploading file:', error);

View file

@ -37,7 +37,7 @@ const base64Only = new Set([
EModelEndpoint.bedrock, EModelEndpoint.bedrock,
]); ]);
const blobStorageSources = new Set([FileSources.azure, FileSources.s3]); const blobStorageSources = new Set([FileSources.azure_blob, FileSources.s3]);
/** /**
* Encodes and formats the given files. * Encodes and formats the given files.

View file

@ -492,7 +492,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
let fileInfoMetadata; let fileInfoMetadata;
const entity_id = messageAttachment === true ? undefined : agent_id; const entity_id = messageAttachment === true ? undefined : agent_id;
const basePath = mime.getType(file.originalname)?.startsWith('image') ? 'images' : 'uploads';
if (tool_resource === EToolResources.execute_code) { if (tool_resource === EToolResources.execute_code) {
const isCodeEnabled = await checkCapability(req, AgentCapabilities.execute_code); const isCodeEnabled = await checkCapability(req, AgentCapabilities.execute_code);
if (!isCodeEnabled) { if (!isCodeEnabled) {
@ -532,7 +532,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
images, images,
filename, filename,
filepath: ocrFileURL, filepath: ocrFileURL,
} = await handleFileUpload({ req, file, file_id, entity_id: agent_id }); } = await handleFileUpload({ req, file, file_id, entity_id: agent_id, basePath });
const fileInfo = removeNullishValues({ const fileInfo = removeNullishValues({
text, text,
@ -582,6 +582,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
file, file,
file_id, file_id,
entity_id, entity_id,
basePath,
}); });
let filepath = _filepath; let filepath = _filepath;

View file

@ -211,6 +211,8 @@ const getStrategyFunctions = (fileSource) => {
} else if (fileSource === FileSources.openai) { } else if (fileSource === FileSources.openai) {
return openAIStrategy(); return openAIStrategy();
} else if (fileSource === FileSources.azure) { } else if (fileSource === FileSources.azure) {
return openAIStrategy();
} else if (fileSource === FileSources.azure_blob) {
return azureStrategy(); return azureStrategy();
} else if (fileSource === FileSources.vectordb) { } else if (fileSource === FileSources.vectordb) {
return vectorStrategy(); return vectorStrategy();

View file

@ -10,7 +10,8 @@ const sourceToEndpoint = {
const sourceToClassname = { const sourceToClassname = {
[FileSources.openai]: 'bg-white/75 dark:bg-black/65', [FileSources.openai]: 'bg-white/75 dark:bg-black/65',
[FileSources.azure]: 'azure-bg-color opacity-85', [FileSources.azure]: 'azure-bg-color',
[FileSources.azure_blob]: 'azure-bg-color',
[FileSources.execute_code]: 'bg-black text-white opacity-85', [FileSources.execute_code]: 'bg-black text-white opacity-85',
[FileSources.text]: 'bg-blue-500 dark:bg-blue-900 opacity-85 text-white', [FileSources.text]: 'bg-blue-500 dark:bg-blue-900 opacity-85 text-white',
[FileSources.vectordb]: 'bg-yellow-700 dark:bg-yellow-900 opacity-85 text-white', [FileSources.vectordb]: 'bg-yellow-700 dark:bg-yellow-900 opacity-85 text-white',

View file

@ -61,7 +61,7 @@ export default function useSelectorEffects({
} }
const assistant = assistantsMap?.[endpoint ?? '']?.[assistant_id]; const assistant = assistantsMap?.[endpoint ?? '']?.[assistant_id];
if (assistant !== undefined) { if (assistant !== undefined) {
setOption('model')(''); setOption('model')(assistant.model);
setOption('assistant_id')(assistant_id); setOption('assistant_id')(assistant_id);
} }
} }

View file

@ -4,6 +4,7 @@ export enum FileSources {
local = 'local', local = 'local',
firebase = 'firebase', firebase = 'firebase',
azure = 'azure', azure = 'azure',
azure_blob = 'azure_blob',
openai = 'openai', openai = 'openai',
s3 = 's3', s3 = 's3',
vectordb = 'vectordb', vectordb = 'vectordb',