mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🔧 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:
parent
d60f2ed50b
commit
bc039cea29
9 changed files with 93 additions and 27 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue