mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 17:01:00 +03:00
Feature/s3 storage (#2226)
* centralizing file writing.... * allowing s3 as storage option * allowing s3 as storage option * update s3 storage --------- Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
@@ -7,9 +7,7 @@ import { getBaseClasses } from '../../../src/utils'
|
||||
import { LoadPyodide, finalSystemPrompt, systemPrompt } from './core'
|
||||
import { checkInputs, Moderation } from '../../moderation/Moderation'
|
||||
import { formatResponse } from '../../outputparsers/OutputParserHelpers'
|
||||
import path from 'path'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class CSV_Agents implements INode {
|
||||
label: string
|
||||
@@ -114,8 +112,7 @@ class CSV_Agents implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
base64String += fileData.toString('base64')
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -203,7 +203,7 @@ const prepareAgent = async (
|
||||
|
||||
if (llmSupportsVision(model)) {
|
||||
const visionChatModel = model as IVisionChatModal
|
||||
const messageContent = addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
const messageContent = await addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
|
||||
if (messageContent?.length) {
|
||||
visionChatModel.setVisionModel()
|
||||
|
||||
@@ -98,7 +98,7 @@ class ReActAgentChat_Agents implements INode {
|
||||
|
||||
if (llmSupportsVision(model)) {
|
||||
const visionChatModel = model as IVisionChatModal
|
||||
const messageContent = addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
const messageContent = await addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
|
||||
if (messageContent?.length) {
|
||||
// Change model to vision supported
|
||||
|
||||
@@ -106,7 +106,7 @@ class ToolAgent_Agents implements INode {
|
||||
}
|
||||
}
|
||||
|
||||
const executor = prepareAgent(nodeData, options, { sessionId: this.sessionId, chatId: options.chatId, input })
|
||||
const executor = await prepareAgent(nodeData, options, { sessionId: this.sessionId, chatId: options.chatId, input })
|
||||
|
||||
const loggerHandler = new ConsoleCallbackHandler(options.logger)
|
||||
const callbacks = await additionalCallbacks(nodeData, options)
|
||||
@@ -178,7 +178,11 @@ class ToolAgent_Agents implements INode {
|
||||
}
|
||||
}
|
||||
|
||||
const prepareAgent = (nodeData: INodeData, options: ICommonObject, flowObj: { sessionId?: string; chatId?: string; input?: string }) => {
|
||||
const prepareAgent = async (
|
||||
nodeData: INodeData,
|
||||
options: ICommonObject,
|
||||
flowObj: { sessionId?: string; chatId?: string; input?: string }
|
||||
) => {
|
||||
const model = nodeData.inputs?.model as BaseChatModel
|
||||
const maxIterations = nodeData.inputs?.maxIterations as string
|
||||
const memory = nodeData.inputs?.memory as FlowiseMemory
|
||||
@@ -197,7 +201,7 @@ const prepareAgent = (nodeData: INodeData, options: ICommonObject, flowObj: { se
|
||||
|
||||
if (llmSupportsVision(model)) {
|
||||
const visionChatModel = model as IVisionChatModal
|
||||
const messageContent = addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
const messageContent = await addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
|
||||
if (messageContent?.length) {
|
||||
visionChatModel.setVisionModel()
|
||||
|
||||
@@ -5,9 +5,7 @@ import { getBaseClasses } from '../../../src/utils'
|
||||
import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
|
||||
import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation'
|
||||
import { formatResponse } from '../../outputparsers/OutputParserHelpers'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class OpenApiChain_Chains implements INode {
|
||||
label: string
|
||||
@@ -111,8 +109,7 @@ const initChain = async (nodeData: INodeData, options: ICommonObject) => {
|
||||
if (yamlFileBase64.startsWith('FILE-STORAGE::')) {
|
||||
const file = yamlFileBase64.replace('FILE-STORAGE::', '')
|
||||
const chatflowid = options.chatflowid
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
yamlString = fileData.toString()
|
||||
} else {
|
||||
const splitDataURI = yamlFileBase64.split(',')
|
||||
|
||||
@@ -111,7 +111,7 @@ class ConversationChain_Chains implements INode {
|
||||
async run(nodeData: INodeData, input: string, options: ICommonObject): Promise<string | object> {
|
||||
const memory = nodeData.inputs?.memory
|
||||
|
||||
const chain = prepareChain(nodeData, options, this.sessionId)
|
||||
const chain = await prepareChain(nodeData, options, this.sessionId)
|
||||
const moderations = nodeData.inputs?.inputModeration as Moderation[]
|
||||
|
||||
if (moderations && moderations.length > 0) {
|
||||
@@ -216,14 +216,14 @@ const prepareChatPrompt = (nodeData: INodeData, humanImageMessages: MessageConte
|
||||
return chatPrompt
|
||||
}
|
||||
|
||||
const prepareChain = (nodeData: INodeData, options: ICommonObject, sessionId?: string) => {
|
||||
const prepareChain = async (nodeData: INodeData, options: ICommonObject, sessionId?: string) => {
|
||||
let model = nodeData.inputs?.model as BaseChatModel
|
||||
const memory = nodeData.inputs?.memory as FlowiseMemory
|
||||
const memoryKey = memory.memoryKey ?? 'chat_history'
|
||||
|
||||
let messageContent: MessageContentImageUrl[] = []
|
||||
if (llmSupportsVision(model)) {
|
||||
messageContent = addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
messageContent = await addImagesToMessages(nodeData, options, model.multiModalOption)
|
||||
const visionChatModel = model as IVisionChatModal
|
||||
if (messageContent?.length) {
|
||||
visionChatModel.setVisionModel()
|
||||
|
||||
@@ -184,7 +184,7 @@ const runPrediction = async (
|
||||
|
||||
if (llmSupportsVision(chain.llm)) {
|
||||
const visionChatModel = chain.llm as IVisionChatModal
|
||||
const messageContent = addImagesToMessages(nodeData, options, visionChatModel.multiModalOption)
|
||||
const messageContent = await addImagesToMessages(nodeData, options, visionChatModel.multiModalOption)
|
||||
if (messageContent?.length) {
|
||||
// Change model to gpt-4-vision && max token to higher when using gpt-4-vision
|
||||
visionChatModel.setVisionModel()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { CSVLoader } from 'langchain/document_loaders/fs/csv'
|
||||
import path from 'path'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Csv_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -75,8 +73,7 @@ class Csv_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
const loader = new CSVLoader(blob, columnName.trim().length === 0 ? undefined : columnName.trim())
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { DocxLoader } from 'langchain/document_loaders/fs/docx'
|
||||
import path from 'path'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Docx_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -66,8 +64,7 @@ class Docx_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
const loader = new DocxLoader(blob)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { JSONLoader } from 'langchain/document_loaders/fs/json'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Json_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -82,8 +80,7 @@ class Json_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { JSONLinesLoader } from 'langchain/document_loaders/fs/json'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Jsonlines_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -76,8 +74,7 @@ class Jsonlines_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
const loader = new JSONLinesLoader(blob, pointer)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { PDFLoader } from 'langchain/document_loaders/fs/pdf'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Pdf_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -92,8 +90,7 @@ class Pdf_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const bf = Buffer.from(fileData)
|
||||
await this.extractDocs(usage, bf, legacyBuild, textSplitter, alldocs)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from
|
||||
import { TextSplitter } from 'langchain/text_splitter'
|
||||
import { TextLoader } from 'langchain/document_loaders/fs/text'
|
||||
import { Document } from '@langchain/core/documents'
|
||||
import { getStoragePath, handleEscapeCharacters } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage, handleEscapeCharacters } from '../../../src'
|
||||
|
||||
class Text_DocumentLoaders implements INode {
|
||||
label: string
|
||||
@@ -85,8 +83,7 @@ class Text_DocumentLoaders implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
const loader = new TextLoader(blob)
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { BaseLanguageModel } from '@langchain/core/language_models/base'
|
||||
import { OpenApiToolkit } from 'langchain/agents'
|
||||
import { JsonSpec, JsonObject } from './core'
|
||||
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getCredentialData, getCredentialParam } from '../../../src'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getCredentialData, getCredentialParam, getFileFromStorage } from '../../../src'
|
||||
|
||||
class OpenAPIToolkit_Tools implements INode {
|
||||
label: string
|
||||
@@ -63,9 +60,9 @@ class OpenAPIToolkit_Tools implements INode {
|
||||
if (yamlFileBase64.startsWith('FILE-STORAGE::')) {
|
||||
const file = yamlFileBase64.replace('FILE-STORAGE::', '')
|
||||
const chatflowid = options.chatflowid
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const utf8String = fileData.toString('utf-8')
|
||||
|
||||
data = load(utf8String) as JsonObject
|
||||
} else {
|
||||
const splitDataURI = yamlFileBase64.split(',')
|
||||
|
||||
@@ -11,9 +11,7 @@ import { Document } from '@langchain/core/documents'
|
||||
import { Embeddings } from '@langchain/core/embeddings'
|
||||
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams, IndexingResult } from '../../../src/Interface'
|
||||
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class Vectara_VectorStores implements INode {
|
||||
label: string
|
||||
@@ -197,8 +195,7 @@ class Vectara_VectorStores implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
vectaraFiles.push({ blob: blob, fileName: getFileName(file) })
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { VectaraStore, VectaraLibArgs, VectaraFilter, VectaraContextConfig, VectaraFile } from '@langchain/community/vectorstores/vectara'
|
||||
import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
|
||||
import path from 'path'
|
||||
import { getStoragePath } from '../../../src'
|
||||
import fs from 'fs'
|
||||
import { getFileFromStorage } from '../../../src'
|
||||
|
||||
class VectaraUpload_VectorStores implements INode {
|
||||
label: string
|
||||
@@ -144,8 +142,7 @@ class VectaraUpload_VectorStores implements INode {
|
||||
const chatflowid = options.chatflowid
|
||||
|
||||
for (const file of files) {
|
||||
const fileInStorage = path.join(getStoragePath(), chatflowid, file)
|
||||
const fileData = fs.readFileSync(fileInStorage)
|
||||
const fileData = await getFileFromStorage(file, chatflowid)
|
||||
const blob = new Blob([fileData])
|
||||
vectaraFiles.push({ blob: blob, fileName: getFileName(file) })
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@ dotenv.config({ path: envPath, override: true })
|
||||
export * from './Interface'
|
||||
export * from './utils'
|
||||
export * from './speechToText'
|
||||
export * from './storageUtils'
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { IVisionChatModal, ICommonObject, IFileUpload, IMultiModalOption, INodeData, MessageContentImageUrl } from './Interface'
|
||||
import path from 'path'
|
||||
import { getStoragePath } from './utils'
|
||||
import fs from 'fs'
|
||||
import { getFileFromStorage } from './storageUtils'
|
||||
|
||||
export const addImagesToMessages = (
|
||||
export const addImagesToMessages = async (
|
||||
nodeData: INodeData,
|
||||
options: ICommonObject,
|
||||
multiModalOption?: IMultiModalOption
|
||||
): MessageContentImageUrl[] => {
|
||||
): Promise<MessageContentImageUrl[]> => {
|
||||
const imageContent: MessageContentImageUrl[] = []
|
||||
let model = nodeData.inputs?.model
|
||||
|
||||
@@ -18,10 +16,8 @@ export const addImagesToMessages = (
|
||||
for (const upload of imageUploads) {
|
||||
let bf = upload.data
|
||||
if (upload.type == 'stored-file') {
|
||||
const filePath = path.join(getStoragePath(), options.chatflowid, options.chatId, upload.name)
|
||||
|
||||
const contents = await getFileFromStorage(upload.name, options.chatflowid, options.chatId)
|
||||
// as the image is stored in the server, read the file and convert it to base64
|
||||
const contents = fs.readFileSync(filePath)
|
||||
bf = 'data:' + upload.mime + ';base64,' + contents.toString('base64')
|
||||
|
||||
imageContent.push({
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { ICommonObject, IFileUpload } from './Interface'
|
||||
import { getCredentialData, getStoragePath } from './utils'
|
||||
import { getCredentialData } from './utils'
|
||||
import { type ClientOptions, OpenAIClient } from '@langchain/openai'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { AssemblyAI } from 'assemblyai'
|
||||
import { getFileFromStorage } from './storageUtils'
|
||||
|
||||
export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfig: ICommonObject, options: ICommonObject) => {
|
||||
if (speechToTextConfig) {
|
||||
const credentialId = speechToTextConfig.credentialId as string
|
||||
const credentialData = await getCredentialData(credentialId ?? '', options)
|
||||
const filePath = path.join(getStoragePath(), options.chatflowid, options.chatId, upload.name)
|
||||
const audio_file = fs.createReadStream(filePath)
|
||||
const audio_file = await getFileFromStorage(upload.name, options.chatflowid, options.chatId)
|
||||
|
||||
if (speechToTextConfig.name === 'openAIWhisper') {
|
||||
const openAIClientOptions: ClientOptions = {
|
||||
@@ -18,7 +16,7 @@ export const convertSpeechToText = async (upload: IFileUpload, speechToTextConfi
|
||||
}
|
||||
const openAIClient = new OpenAIClient(openAIClientOptions)
|
||||
const transcription = await openAIClient.audio.transcriptions.create({
|
||||
file: audio_file,
|
||||
file: new File([new Blob([audio_file])], upload.name),
|
||||
model: 'whisper-1',
|
||||
language: speechToTextConfig?.language,
|
||||
temperature: speechToTextConfig?.temperature ? parseFloat(speechToTextConfig.temperature) : undefined,
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { DeleteObjectsCommand, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
|
||||
import { Readable } from 'node:stream'
|
||||
import { getUserHome } from './utils'
|
||||
|
||||
export const addBase64FilesToStorage = async (file: string, chatflowid: string, fileNames: string[]) => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
const { s3Client, Bucket } = getS3Config()
|
||||
|
||||
const splitDataURI = file.split(',')
|
||||
const filename = splitDataURI.pop()?.split(':')[1] ?? ''
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
const mime = splitDataURI[0].split(':')[1].split(';')[0]
|
||||
|
||||
const key = chatflowid + '/' + filename
|
||||
const putObjCmd = new PutObjectCommand({
|
||||
Bucket,
|
||||
Key: key,
|
||||
ContentEncoding: 'base64', // required for binary data
|
||||
ContentType: mime,
|
||||
Body: bf
|
||||
})
|
||||
await s3Client.send(putObjCmd)
|
||||
|
||||
fileNames.push(filename)
|
||||
return 'FILE-STORAGE::' + JSON.stringify(fileNames)
|
||||
} else {
|
||||
const dir = path.join(getStoragePath(), chatflowid)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const splitDataURI = file.split(',')
|
||||
const filename = splitDataURI.pop()?.split(':')[1] ?? ''
|
||||
const bf = Buffer.from(splitDataURI.pop() || '', 'base64')
|
||||
|
||||
const filePath = path.join(dir, filename)
|
||||
fs.writeFileSync(filePath, bf)
|
||||
fileNames.push(filename)
|
||||
return 'FILE-STORAGE::' + JSON.stringify(fileNames)
|
||||
}
|
||||
}
|
||||
|
||||
export const addFileToStorage = async (mime: string, bf: Buffer, fileName: string, ...paths: string[]) => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
const { s3Client, Bucket } = getS3Config()
|
||||
|
||||
let Key = paths.reduce((acc, cur) => acc + '/' + cur, '') + '/' + fileName
|
||||
if (Key.startsWith('/')) {
|
||||
Key = Key.substring(1)
|
||||
}
|
||||
|
||||
const putObjCmd = new PutObjectCommand({
|
||||
Bucket,
|
||||
Key,
|
||||
ContentEncoding: 'base64', // required for binary data
|
||||
ContentType: mime,
|
||||
Body: bf
|
||||
})
|
||||
await s3Client.send(putObjCmd)
|
||||
} else {
|
||||
const dir = path.join(getStoragePath(), ...paths)
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
|
||||
const filePath = path.join(dir, fileName)
|
||||
fs.writeFileSync(filePath, bf)
|
||||
}
|
||||
}
|
||||
|
||||
export const getFileFromStorage = async (file: string, ...paths: string[]): Promise<Buffer> => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
const { s3Client, Bucket } = getS3Config()
|
||||
|
||||
let Key = paths.reduce((acc, cur) => acc + '/' + cur, '') + '/' + file
|
||||
if (Key.startsWith('/')) {
|
||||
Key = Key.substring(1)
|
||||
}
|
||||
|
||||
const getParams = {
|
||||
Bucket,
|
||||
Key
|
||||
}
|
||||
|
||||
const response = await s3Client.send(new GetObjectCommand(getParams))
|
||||
const body = response.Body
|
||||
if (body instanceof Readable) {
|
||||
const streamToString = await body.transformToString('base64')
|
||||
if (streamToString) {
|
||||
return Buffer.from(streamToString, 'base64')
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const buffer = Buffer.concat(response.Body.toArray())
|
||||
return buffer
|
||||
} else {
|
||||
const fileInStorage = path.join(getStoragePath(), ...paths, file)
|
||||
return fs.readFileSync(fileInStorage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path
|
||||
*/
|
||||
export const getStoragePath = (): string => {
|
||||
return process.env.BLOB_STORAGE_PATH ? path.join(process.env.BLOB_STORAGE_PATH) : path.join(getUserHome(), '.flowise', 'storage')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage type - local or s3
|
||||
*/
|
||||
export const getStorageType = (): string => {
|
||||
return process.env.STORAGE_TYPE ? process.env.STORAGE_TYPE : 'local'
|
||||
}
|
||||
|
||||
export const removeFilesFromStorage = async (...paths: string[]) => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
let Key = paths.reduce((acc, cur) => acc + '/' + cur, '')
|
||||
// remove the first '/' if it exists
|
||||
if (Key.startsWith('/')) {
|
||||
Key = Key.substring(1)
|
||||
}
|
||||
await _deleteS3Folder(Key)
|
||||
} else {
|
||||
const directory = path.join(getStoragePath(), ...paths)
|
||||
_deleteLocalFolderRecursive(directory)
|
||||
}
|
||||
}
|
||||
|
||||
export const removeFolderFromStorage = async (...paths: string[]) => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
let Key = paths.reduce((acc, cur) => acc + '/' + cur, '')
|
||||
// remove the first '/' if it exists
|
||||
if (Key.startsWith('/')) {
|
||||
Key = Key.substring(1)
|
||||
}
|
||||
await _deleteS3Folder(Key)
|
||||
} else {
|
||||
const directory = path.join(getStoragePath(), ...paths)
|
||||
_deleteLocalFolderRecursive(directory, true)
|
||||
}
|
||||
}
|
||||
|
||||
const _deleteLocalFolderRecursive = (directory: string, deleteParentChatflowFolder?: boolean) => {
|
||||
// Console error here as failing is not destructive operation
|
||||
if (fs.existsSync(directory)) {
|
||||
if (deleteParentChatflowFolder) {
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
} else {
|
||||
fs.readdir(directory, (error, files) => {
|
||||
if (error) console.error('Could not read directory')
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i]
|
||||
const file_path = path.join(directory, file)
|
||||
|
||||
fs.stat(file_path, (error, stat) => {
|
||||
if (error) console.error('File do not exist')
|
||||
|
||||
if (!stat.isDirectory()) {
|
||||
fs.unlink(file_path, (error) => {
|
||||
if (error) console.error('Could not delete file')
|
||||
})
|
||||
if (i === files.length - 1) {
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
} else {
|
||||
_deleteLocalFolderRecursive(file_path)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _deleteS3Folder = async (location: string) => {
|
||||
let count = 0 // number of files deleted
|
||||
const { s3Client, Bucket } = getS3Config()
|
||||
async function recursiveS3Delete(token?: any) {
|
||||
// get the files
|
||||
const listCommand = new ListObjectsV2Command({
|
||||
Bucket: Bucket,
|
||||
Prefix: location,
|
||||
ContinuationToken: token
|
||||
})
|
||||
let list = await s3Client.send(listCommand)
|
||||
if (list.KeyCount) {
|
||||
const deleteCommand = new DeleteObjectsCommand({
|
||||
Bucket: Bucket,
|
||||
Delete: {
|
||||
Objects: list.Contents?.map((item) => ({ Key: item.Key })),
|
||||
Quiet: false
|
||||
}
|
||||
})
|
||||
let deleted = await s3Client.send(deleteCommand)
|
||||
// @ts-ignore
|
||||
count += deleted.Deleted.length
|
||||
|
||||
if (deleted.Errors) {
|
||||
deleted.Errors.map((error: any) => console.error(`${error.Key} could not be deleted - ${error.Code}`))
|
||||
}
|
||||
}
|
||||
// repeat if more files to delete
|
||||
if (list.NextContinuationToken) {
|
||||
await recursiveS3Delete(list.NextContinuationToken)
|
||||
}
|
||||
// return total deleted count when finished
|
||||
return `${count} files deleted from S3`
|
||||
}
|
||||
// start the recursive function
|
||||
return recursiveS3Delete()
|
||||
}
|
||||
|
||||
export const streamStorageFile = async (
|
||||
chatflowId: string,
|
||||
chatId: string,
|
||||
fileName: string
|
||||
): Promise<fs.ReadStream | Buffer | undefined> => {
|
||||
const storageType = getStorageType()
|
||||
if (storageType === 's3') {
|
||||
const { s3Client, Bucket } = getS3Config()
|
||||
|
||||
const Key = chatflowId + '/' + chatId + '/' + fileName
|
||||
const getParams = {
|
||||
Bucket,
|
||||
Key
|
||||
}
|
||||
const response = await s3Client.send(new GetObjectCommand(getParams))
|
||||
const body = response.Body
|
||||
if (body instanceof Readable) {
|
||||
const blob = await body.transformToByteArray()
|
||||
return Buffer.from(blob)
|
||||
}
|
||||
} else {
|
||||
const filePath = path.join(getStoragePath(), chatflowId, chatId, fileName)
|
||||
//raise error if file path is not absolute
|
||||
if (!path.isAbsolute(filePath)) throw new Error(`Invalid file path`)
|
||||
//raise error if file path contains '..'
|
||||
if (filePath.includes('..')) throw new Error(`Invalid file path`)
|
||||
//only return from the storage folder
|
||||
if (!filePath.startsWith(getStoragePath())) throw new Error(`Invalid file path`)
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
return fs.createReadStream(filePath)
|
||||
} else {
|
||||
throw new Error(`File ${fileName} not found`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getS3Config = () => {
|
||||
const accessKeyId = process.env.S3_STORAGE_ACCESS_KEY_ID
|
||||
const secretAccessKey = process.env.S3_STORAGE_SECRET_ACCESS_KEY
|
||||
const region = process.env.S3_STORAGE_REGION
|
||||
const Bucket = process.env.S3_STORAGE_BUCKET_NAME
|
||||
if (!accessKeyId || !secretAccessKey || !region || !Bucket) {
|
||||
throw new Error('S3 storage configuration is missing')
|
||||
}
|
||||
const s3Client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey
|
||||
},
|
||||
region
|
||||
})
|
||||
return { s3Client, Bucket }
|
||||
}
|
||||
@@ -769,10 +769,3 @@ export const prepareSandboxVars = (variables: IVariable[]) => {
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare storage path
|
||||
*/
|
||||
export const getStoragePath = (): string => {
|
||||
return process.env.BLOB_STORAGE_PATH ? path.join(process.env.BLOB_STORAGE_PATH) : path.join(getUserHome(), '.flowise', 'storage')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user