|
| 1 | +import { randomBytes } from 'crypto'; |
| 2 | +import { |
| 3 | + defaultUploadConfig, |
| 4 | + type UploadFileConfig, |
| 5 | + type PresignedUrlInput, |
| 6 | + type PresignedUrlResponse, |
| 7 | + PluginTypeEnum, |
| 8 | + PLUGIN_TYPE_TO_PATH_MAP |
| 9 | +} from './config'; |
| 10 | +import { addLog } from '../../system/log'; |
| 11 | +import { ensureBucket } from '../../minio/init'; |
| 12 | +import * as path from 'path'; |
| 13 | +import { connectionMinio } from '../../minio/index'; |
| 14 | +import { MongoFastGPTPlugin } from './schema'; |
| 15 | +import { inferContentType } from './utils'; |
| 16 | + |
| 17 | +let globalConfig: UploadFileConfig = defaultUploadConfig; |
| 18 | + |
| 19 | +export const initFileUploadService = async (config?: Partial<UploadFileConfig>) => { |
| 20 | + globalConfig = { ...defaultUploadConfig, ...config }; |
| 21 | + |
| 22 | + try { |
| 23 | + addLog.info(`Initializing upload bucket: ${globalConfig.bucket}`); |
| 24 | + await ensureBucket(globalConfig.bucket, true); |
| 25 | + addLog.info(`Upload bucket initialized successfully: ${globalConfig.bucket}`); |
| 26 | + return true; |
| 27 | + } catch (error) { |
| 28 | + addLog.error(`Failed to initialize upload bucket: ${globalConfig.bucket}`, error); |
| 29 | + throw error; |
| 30 | + } |
| 31 | +}; |
| 32 | + |
| 33 | +const generateFileId = (): string => { |
| 34 | + return randomBytes(16).toString('hex'); |
| 35 | +}; |
| 36 | + |
| 37 | +export const generateDownloadUrl = (objectName: string, config: UploadFileConfig): string => { |
| 38 | + const pathParts = objectName.split('/'); |
| 39 | + const encodedParts = pathParts.map((part) => encodeURIComponent(part)); |
| 40 | + const encodedObjectName = encodedParts.join('/'); |
| 41 | + return `${config.bucket}/${encodedObjectName}`; |
| 42 | +}; |
| 43 | + |
| 44 | +const validatePresignedInput = (input: PresignedUrlInput, config: UploadFileConfig): void => { |
| 45 | + if (!input.filename) { |
| 46 | + throw new Error('Filename is required'); |
| 47 | + } |
| 48 | + |
| 49 | + const maxSize = input.maxSize || config.maxFileSize; |
| 50 | + if (maxSize > config.maxFileSize) { |
| 51 | + throw new Error(`Max size exceeds limit of ${config.maxFileSize} bytes`); |
| 52 | + } |
| 53 | + |
| 54 | + const ext = path.extname(input.filename).toLowerCase(); |
| 55 | + if (config.allowedExtensions && !config.allowedExtensions.includes(ext)) { |
| 56 | + throw new Error( |
| 57 | + `File extension ${ext} is not allowed. Supported extensions: ${config.allowedExtensions.join(', ')}` |
| 58 | + ); |
| 59 | + } |
| 60 | +}; |
| 61 | + |
| 62 | +//Generate a pre-signed URL for direct file upload |
| 63 | +export const generatePresignedUrl = async ( |
| 64 | + input: PresignedUrlInput |
| 65 | +): Promise<PresignedUrlResponse> => { |
| 66 | + const currentConfig = { ...globalConfig }; |
| 67 | + |
| 68 | + validatePresignedInput(input, currentConfig); |
| 69 | + |
| 70 | + const fileId = generateFileId(); |
| 71 | + const pluginType = input.pluginType || PluginTypeEnum.tool; |
| 72 | + const pluginPath = PLUGIN_TYPE_TO_PATH_MAP[pluginType]; |
| 73 | + const objectName = `${pluginPath}/${fileId}/${input.filename}`; |
| 74 | + const contentType = input.contentType || inferContentType(input.filename); |
| 75 | + const maxSize = input.maxSize || currentConfig.maxFileSize; |
| 76 | + |
| 77 | + try { |
| 78 | + const policy = connectionMinio.newPostPolicy(); |
| 79 | + policy.setBucket(currentConfig.bucket); |
| 80 | + policy.setKey(objectName); |
| 81 | + |
| 82 | + policy.setContentType(contentType); |
| 83 | + |
| 84 | + policy.setContentLengthRange(1, maxSize); |
| 85 | + |
| 86 | + policy.setExpires(new Date(Date.now() + 10 * 60 * 1000)); |
| 87 | + |
| 88 | + const metadata = { |
| 89 | + 'original-filename': encodeURIComponent(input.filename), |
| 90 | + 'upload-time': new Date().toISOString(), |
| 91 | + 'file-id': fileId, |
| 92 | + ...input.metadata |
| 93 | + }; |
| 94 | + policy.setUserMetaData(metadata); |
| 95 | + |
| 96 | + const { postURL, formData } = await connectionMinio.presignedPostPolicy(policy); |
| 97 | + |
| 98 | + const response: PresignedUrlResponse = { |
| 99 | + fileId, |
| 100 | + objectName, |
| 101 | + uploadUrl: postURL, |
| 102 | + formData |
| 103 | + }; |
| 104 | + |
| 105 | + console.log('Generated presigned URL successfully', response); |
| 106 | + |
| 107 | + return response; |
| 108 | + } catch (error) { |
| 109 | + addLog.error('Failed to generate presigned URL', error); |
| 110 | + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| 111 | + return Promise.reject(`Failed to generate presigned URL: ${errorMessage}`); |
| 112 | + } |
| 113 | +}; |
| 114 | + |
| 115 | +//Confirm the presigned URL upload is complete and save the file information to MongoDB |
| 116 | +export const confirmPresignedUpload = async (objectName: string, size: string): Promise<string> => { |
| 117 | + try { |
| 118 | + const currentConfig = { ...globalConfig }; |
| 119 | + const stat = await connectionMinio.statObject(currentConfig.bucket, objectName); |
| 120 | + |
| 121 | + if (stat.size !== Number(size)) { |
| 122 | + return Promise.reject(`File size mismatch. Expected: ${size}, Actual: ${stat.size}`); |
| 123 | + } |
| 124 | + |
| 125 | + const accessUrl = generateDownloadUrl(objectName, currentConfig); |
| 126 | + |
| 127 | + const pluginData = { |
| 128 | + url: accessUrl, |
| 129 | + type: 'tool' as const |
| 130 | + }; |
| 131 | + |
| 132 | + const result = await MongoFastGPTPlugin.create(pluginData); |
| 133 | + |
| 134 | + return result.url; |
| 135 | + } catch (error) { |
| 136 | + addLog.error('Failed to confirm presigned upload', error); |
| 137 | + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| 138 | + return Promise.reject(`Failed to confirm presigned upload: ${errorMessage}`); |
| 139 | + } |
| 140 | +}; |
0 commit comments