package file import ( "context" "encoding/base64" "encoding/json" "fmt" "ragflow/internal/dao" "ragflow/internal/entity" "ragflow/internal/parser/parser" "ragflow/internal/storage" "ragflow/internal/utility" "strings" ) // GetFileContent gets file metadata and checks permission for download // Matches Python's file_api_service.get_file_content function func (s *FileService) GetFileContent(uid, fileID string) (*entity.File, error) { file, err := s.fileDAO.GetByID(fileID) if err != nil || file == nil { return nil, fmt.Errorf("Document not found!") } if !s.checkFilePerm(s.fileDAO, file, uid) { return nil, fmt.Errorf("No authorization.") } return file, nil } // GetStorageAddress gets storage address for a file (fallback for when direct blob is empty) // Matches Python's File2DocumentService.get_storage_address function func (s *FileService) GetStorageAddress(fileID string) (*StorageAddress, error) { // Get file2document mapping f2d, err := s.file2DocumentDAO.GetByFileID(fileID) if err != nil || len(f2d) == 0 { return nil, fmt.Errorf("file2document mapping not found") } // Get the file if f2d[0].FileID == nil { return nil, fmt.Errorf("file_id is nil in file2document mapping") } file, err := s.fileDAO.GetByID(*f2d[0].FileID) if err != nil || file == nil { return nil, fmt.Errorf("file not found") } // If source_type is empty or local, return file's parent_id and location if file.SourceType == "" || entity.FileSource(file.SourceType) == entity.FileSourceLocal { if file.Location == nil || *file.Location == "" { return nil, fmt.Errorf("file location is empty") } return &StorageAddress{ Bucket: file.ParentID, Name: *file.Location, }, nil } // Otherwise, use document's kb_id and location if f2d[0].DocumentID == nil { return nil, fmt.Errorf("document_id is required") } documentDAO := dao.NewDocumentDAO() doc, err := documentDAO.GetByID(*f2d[0].DocumentID) if err != nil || doc == nil { return nil, fmt.Errorf("document not found") } if doc.Location == nil || *doc.Location == "" { return nil, fmt.Errorf("document location is empty") } return &StorageAddress{ Bucket: doc.KbID, Name: *doc.Location, }, nil } // DownloadAgentFile downloads an agent-generated file directly from MinIO without querying the database. func (s *FileService) DownloadAgentFile(tenantID, location string) ([]byte, error) { storageImpl := storage.GetStorageFactory().GetStorage() if storageImpl == nil { return nil, fmt.Errorf("storage not initialized") } bucketName := fmt.Sprintf("%s-downloads", tenantID) blob, err := storageImpl.Get(bucketName, location) if err != nil { return nil, fmt.Errorf("failed to read file from storage: %w", err) } return blob, nil } // GetFileContents fetches file contents (text + image) from storage // for the given file dicts. // // File dicts are the descriptors returned by the upload_info endpoint // (UploadInfos / storeUploadInfoBlob). They contain: // // - "id": storage location UUID (key in the downloads bucket) // - "created_by": the user ID who owns the downloads bucket // - "name": the original filename // - "mime_type": the content type // // Blobs are stored directly in "{created_by}-downloads/{id}" in object // storage WITHOUT a corresponding File entity row in the database. // Mirrors Python's FileService.get_files → get_blob(user_id, file_id). // // - raw=false: images returned as base64 data URIs in images; non-images parsed and returned as text. // - raw=true: images returned as raw bytes in images; non-images parsed and returned as text. func (s *FileService) GetFileContents(ctx context.Context, uid string, fileDicts []map[string]interface{}, raw bool) (texts []string, images []string, err error) { storageImpl := storage.GetStorageFactory().GetStorage() if storageImpl == nil { return nil, nil, fmt.Errorf("storage not initialized") } for _, fd := range fileDicts { id, _ := fd["id"].(string) if id == "" { continue } name, _ := fd["name"].(string) mimeType, _ := fd["mime_type"].(string) createdBy, _ := fd["created_by"].(string) if createdBy == "" { createdBy = uid } // Permission: only the owner can access their uploads bucket. if createdBy != uid { return nil, nil, fmt.Errorf("No authorization.") } data, derr := storageImpl.Get(createdBy+"-downloads", id) if derr != nil || len(data) == 0 { continue } ft := utility.FilenameType(name) if ft == utility.FileTypeVISUAL { if raw { images = append(images, string(data)) } else { mediaType := strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0])) if mediaType == "" { ext := utility.GetFileExtension(name) mediaType = utility.GetContentType(ext, string(ft)) } images = append(images, "data:"+mediaType+";base64,"+base64.StdEncoding.EncodeToString(data)) } } else { texts = append(texts, parseFileContent(ctx, name, data)) } } return texts, images, nil } // parseAgentUploads resolves descriptors returned by upload_info from the // caller's downloads bucket and converts them to sys.files values. func (s *FileService) ParseAgentUploads(ctx context.Context, userID string, fileDicts []map[string]interface{}, layoutRecognize string) ([]string, error) { storageImpl := storage.GetStorageFactory().GetStorage() if storageImpl == nil { return nil, fmt.Errorf("storage not initialized") } contents := make([]string, 0, len(fileDicts)) for i, fd := range fileDicts { id, _ := fd["id"].(string) name, _ := fd["name"].(string) mimeType, _ := fd["mime_type"].(string) createdBy, _ := fd["created_by"].(string) if id == "" || name == "" || mimeType == "" || createdBy == "" { return nil, fmt.Errorf("file %d: id, name, mime_type, and created_by are required", i) } if createdBy != userID { return nil, fmt.Errorf("file %q: created_by does not match the current user", name) } data, err := storageImpl.Get(createdBy+"-downloads", id) if err != nil { return nil, fmt.Errorf("file %q: read upload: %w", name, err) } if len(data) == 0 { return nil, fmt.Errorf("file %q: upload is empty", name) } mediaType := strings.ToLower(strings.TrimSpace(strings.Split(mimeType, ";")[0])) if strings.HasPrefix(mediaType, "image/") { contents = append(contents, "data:"+mediaType+";base64,"+base64.StdEncoding.EncodeToString(data)) continue } content, err := parseAgentUploadContent(ctx, name, data, layoutRecognize) if err != nil { return nil, fmt.Errorf("file %q: parse upload: %w", name, err) } contents = append(contents, content) } return contents, nil } func parseAgentUploadContent(ctx context.Context, filename string, data []byte, layoutRecognize string) (string, error) { content := string(data) fileType := utility.GetFileType(filename) if fileType != utility.FileTypeOTHER { fp, err := parser.GetParser(fileType) if err != nil { return "", err } if configurable, ok := fp.(interface{ ConfigureFromSetup(map[string]any) }); ok { configurable.ConfigureFromSetup(map[string]any{"layout_recognize": layoutRecognize}) } res := fp.ParseWithResult(ctx, filename, data) if res.Err != nil { return "", res.Err } switch res.OutputFormat { case "text": content = res.Text case "markdown": content = res.Markdown case "html": content = res.HTML case "json": parts := make([]string, 0, len(res.JSON)) for _, item := range res.JSON { if text, ok := item["text"].(string); ok { parts = append(parts, text) continue } raw, err := json.Marshal(item) if err != nil { return "", err } parts = append(parts, string(raw)) } content = strings.Join(parts, "\n") } } return fmt.Sprintf("\n -----------------\nFile: %s\nContent as following: \n%s", filename, content), nil } // parseFileContent tries to parse a file's contents using the appropriate parser. // Falls back to returning raw text if no parser is available. func parseFileContent(ctx context.Context, filename string, data []byte) string { fileType := utility.GetFileType(filename) if fileType == utility.FileTypeOTHER { return string(data) } fp, err := parser.GetParser(fileType) if err != nil { return string(data) } res := fp.ParseWithResult(ctx, filename, data) if res.Err != nil { return string(data) } switch res.OutputFormat { case "text": return res.Text case "markdown": return res.Markdown case "html": return res.HTML case "json": return string(data) default: return string(data) } }