| const fs = require('fs'); |
| const path = require('path'); |
| const axios = require('axios'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { EModelEndpoint } = require('librechat-data-provider'); |
| const { generateShortLivedToken } = require('@librechat/api'); |
| const { resizeImageBuffer } = require('~/server/services/Files/images/resize'); |
| const { getBufferMetadata } = require('~/server/utils'); |
| const paths = require('~/config/paths'); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function saveLocalFile(file, outputPath, outputFilename) { |
| try { |
| if (!fs.existsSync(outputPath)) { |
| fs.mkdirSync(outputPath, { recursive: true }); |
| } |
|
|
| const fileExtension = path.extname(file.originalname); |
| const filenameWithExt = outputFilename + fileExtension; |
| const outputFilePath = path.join(outputPath, filenameWithExt); |
| fs.copyFileSync(file.path, outputFilePath); |
| fs.unlinkSync(file.path); |
|
|
| return outputFilePath; |
| } catch (error) { |
| logger.error('[saveFile] Error while saving the file:', error); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const saveLocalImage = async (req, file, filename) => { |
| const appConfig = req.config; |
| const imagePath = appConfig.paths.imageOutput; |
| const outputPath = path.join(imagePath, req.user.id ?? ''); |
| await saveLocalFile(file, outputPath, filename); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function saveLocalBuffer({ userId, buffer, fileName, basePath = 'images' }) { |
| try { |
| const { publicPath, uploads } = paths; |
|
|
| const directoryPath = path.join(basePath === 'images' ? publicPath : uploads, basePath, userId); |
|
|
| if (!fs.existsSync(directoryPath)) { |
| fs.mkdirSync(directoryPath, { recursive: true }); |
| } |
|
|
| fs.writeFileSync(path.join(directoryPath, fileName), buffer); |
|
|
| const filePath = path.posix.join('/', basePath, userId, fileName); |
|
|
| return filePath; |
| } catch (error) { |
| logger.error('[saveLocalBuffer] Error while saving the buffer:', error); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function saveFileFromURL({ userId, URL, fileName, basePath = 'images' }) { |
| try { |
| const response = await axios({ |
| url: URL, |
| responseType: 'arraybuffer', |
| }); |
|
|
| const buffer = Buffer.from(response.data, 'binary'); |
| const { bytes, type, dimensions, extension } = await getBufferMetadata(buffer); |
|
|
| |
| const outputPath = path.join(paths.publicPath, basePath, userId.toString()); |
|
|
| |
| if (!fs.existsSync(outputPath)) { |
| fs.mkdirSync(outputPath, { recursive: true }); |
| } |
|
|
| |
| const extRegExp = new RegExp(path.extname(fileName) + '$'); |
| fileName = fileName.replace(extRegExp, `.${extension}`); |
| if (!path.extname(fileName)) { |
| fileName += `.${extension}`; |
| } |
|
|
| |
| const outputFilePath = path.join(outputPath, fileName); |
| fs.writeFileSync(outputFilePath, buffer); |
|
|
| return { |
| bytes, |
| type, |
| dimensions, |
| }; |
| } catch (error) { |
| logger.error('[saveFileFromURL] Error while saving the file:', error); |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function getLocalFileURL({ fileName, basePath = 'images' }) { |
| return path.posix.join('/', basePath, fileName); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const isValidPath = (req, base, subfolder, filepath) => { |
| const normalizedBase = path.resolve(base, subfolder, req.user.id); |
| const normalizedFilepath = path.resolve(filepath); |
| return normalizedFilepath.startsWith(normalizedBase); |
| }; |
|
|
| |
| |
| |
| const unlinkFile = async (filepath) => { |
| try { |
| await fs.promises.unlink(filepath); |
| } catch (error) { |
| logger.error('Error deleting file:', error); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const deleteLocalFile = async (req, file) => { |
| const appConfig = req.config; |
| const { publicPath, uploads } = appConfig.paths; |
|
|
| |
| const cleanFilepath = file.filepath.split('?')[0]; |
|
|
| if (file.embedded && process.env.RAG_API_URL) { |
| const jwtToken = generateShortLivedToken(req.user.id); |
| try { |
| await axios.delete(`${process.env.RAG_API_URL}/documents`, { |
| headers: { |
| Authorization: `Bearer ${jwtToken}`, |
| 'Content-Type': 'application/json', |
| accept: 'application/json', |
| }, |
| data: [file.file_id], |
| }); |
| } catch (error) { |
| if (error.response?.status === 404) { |
| logger.warn( |
| `[deleteLocalFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, |
| ); |
| } else { |
| logger.error('[deleteLocalFile] Error deleting document from RAG API:', error); |
| } |
| } |
| } |
|
|
| if (cleanFilepath.startsWith(`/uploads/${req.user.id}`)) { |
| const userUploadDir = path.join(uploads, req.user.id); |
| const basePath = cleanFilepath.split(`/uploads/${req.user.id}/`)[1]; |
|
|
| if (!basePath) { |
| throw new Error(`Invalid file path: ${cleanFilepath}`); |
| } |
|
|
| const filepath = path.join(userUploadDir, basePath); |
|
|
| const rel = path.relative(userUploadDir, filepath); |
| if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { |
| throw new Error(`Invalid file path: ${cleanFilepath}`); |
| } |
|
|
| await unlinkFile(filepath); |
| return; |
| } |
|
|
| const parts = cleanFilepath.split(path.sep); |
| const subfolder = parts[1]; |
| if (!subfolder && parts[0] === EModelEndpoint.agents) { |
| logger.warn(`Agent File ${file.file_id} is missing filepath, may have been deleted already`); |
| return; |
| } |
| const filepath = path.join(publicPath, cleanFilepath); |
|
|
| if (!isValidPath(req, publicPath, subfolder, filepath)) { |
| throw new Error('Invalid file path'); |
| } |
|
|
| await unlinkFile(filepath); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function uploadLocalFile({ req, file, file_id }) { |
| const appConfig = req.config; |
| const inputFilePath = file.path; |
| const inputBuffer = await fs.promises.readFile(inputFilePath); |
| const bytes = Buffer.byteLength(inputBuffer); |
|
|
| const { uploads } = appConfig.paths; |
| const userPath = path.join(uploads, req.user.id); |
|
|
| if (!fs.existsSync(userPath)) { |
| fs.mkdirSync(userPath, { recursive: true }); |
| } |
|
|
| const fileName = `${file_id}__${path.basename(inputFilePath)}`; |
| const newPath = path.join(userPath, fileName); |
|
|
| await fs.promises.writeFile(newPath, inputBuffer); |
| const filepath = path.posix.join('/', 'uploads', req.user.id, path.basename(newPath)); |
|
|
| let height, width; |
| if (file.mimetype && file.mimetype.startsWith('image/')) { |
| try { |
| const { width: imgWidth, height: imgHeight } = await resizeImageBuffer(inputBuffer, 'high'); |
| height = imgHeight; |
| width = imgWidth; |
| } catch (error) { |
| logger.warn('[uploadLocalFile] Could not get image dimensions:', error.message); |
| } |
| } |
|
|
| return { filepath, bytes, height, width }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| async function getLocalFileStream(req, filepath) { |
| try { |
| const appConfig = req.config; |
| if (filepath.includes('/uploads/')) { |
| const basePath = filepath.split('/uploads/')[1]; |
|
|
| if (!basePath) { |
| logger.warn(`Invalid base path: ${filepath}`); |
| throw new Error(`Invalid file path: ${filepath}`); |
| } |
|
|
| const fullPath = path.join(appConfig.paths.uploads, basePath); |
| const uploadsDir = appConfig.paths.uploads; |
|
|
| const rel = path.relative(uploadsDir, fullPath); |
| if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { |
| logger.warn(`Invalid relative file path: ${filepath}`); |
| throw new Error(`Invalid file path: ${filepath}`); |
| } |
|
|
| return fs.createReadStream(fullPath); |
| } else if (filepath.includes('/images/')) { |
| const basePath = filepath.split('/images/')[1]; |
|
|
| if (!basePath) { |
| logger.warn(`Invalid base path: ${filepath}`); |
| throw new Error(`Invalid file path: ${filepath}`); |
| } |
|
|
| const fullPath = path.join(appConfig.paths.imageOutput, basePath); |
| const publicDir = appConfig.paths.imageOutput; |
|
|
| const rel = path.relative(publicDir, fullPath); |
| if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { |
| logger.warn(`Invalid relative file path: ${filepath}`); |
| throw new Error(`Invalid file path: ${filepath}`); |
| } |
|
|
| return fs.createReadStream(fullPath); |
| } |
| return fs.createReadStream(filepath); |
| } catch (error) { |
| logger.error('Error getting local file stream:', error); |
| throw error; |
| } |
| } |
|
|
| module.exports = { |
| saveLocalFile, |
| saveLocalImage, |
| saveLocalBuffer, |
| saveFileFromURL, |
| getLocalFileURL, |
| deleteLocalFile, |
| uploadLocalFile, |
| getLocalFileStream, |
| }; |
|
|