| | 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, |
| | }; |
| |
|