feat[Go]: implement POST /api/v1/files/link-to-datasets (#15674)

### What problem does this PR solve?

Closes #15673 — ports the Python `file2document_api.py` `convert()`
endpoint to Go.

| Method | Path | Handler |
|--------|------|---------|
| POST | `/api/v1/files/link-to-datasets` | `FileHandler.LinkToDatasets`
|

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---

#### Implementation notes

**Files changed:**

```
internal/service/file2document.go  – new service (File2DocumentService)
internal/dao/file2document.go      – added Create method
internal/handler/file.go           – FileHandler gains file2DocumentService;
                                     LinkToDatasets HTTP handler
internal/router/router.go          – route registered
```

**Functional parity table:**

| Concern | Go behaviour |
|---------|-------------|
| Required fields | `file_ids` and `kb_ids` both required; missing
either → `CodeDataError` mirroring Python `@validate_request` |
| File existence | `fileDAO.GetByIDs(fileIDs)` builds a set; any missing
ID → `"File not found!"` |
| KB existence | `kbDAO.GetByID(kbID)` per KB; missing → `"Can't find
this dataset!"` |
| Folder expansion | `getAllInnermostFileIDs` recursively calls
`fileDAO.ListByParentID` — mirrors
`FileService.get_all_innermost_file_ids` |
| File permissions | `checkFileTeamPermission`: `file.TenantID ==
userID` OR user in tenant's team — mirrors `check_file_team_permission`
|
| KB permissions | `checkKBTeamPermission`: `kb.TenantID == userID` OR
user in tenant's team — mirrors `check_kb_team_permission` |
| Fire-and-forget | `go convertFiles(...)` goroutine after all
validation passes — mirrors `loop.run_in_executor(None, _convert_files,
…)` |
| Conversion | `convertFiles`: for each file → delete existing mappings
+ hard-delete old documents → create new `Document` in each target KB →
create `File2Document` mapping — mirrors Python `_convert_files` |
| `getParser` | Extension-based lookup with fallback to `kb.ParserID` —
mirrors `FileService.get_parser` |
| Immediate return | `true` returned to caller as soon as goroutine is
scheduled |

---------

Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
This commit is contained in:
Hunnyboy1217
2026-06-10 01:46:55 -07:00
committed by GitHub
parent 3796835c4d
commit 16d5b4fa02
5 changed files with 460 additions and 4 deletions

View File

@@ -17,6 +17,8 @@
package handler
import (
"errors"
"fmt"
"net/http"
"net/url"
"ragflow/internal/common"
@@ -32,15 +34,17 @@ import (
// FileHandler file handler
type FileHandler struct {
fileService *service.FileService
userService *service.UserService
fileService *service.FileService
userService *service.UserService
file2DocumentService *service.File2DocumentService
}
// NewFileHandler create file handler
func NewFileHandler(fileService *service.FileService, userService *service.UserService) *FileHandler {
return &FileHandler{
fileService: fileService,
userService: userService,
fileService: fileService,
userService: userService,
file2DocumentService: service.NewFile2DocumentService(),
}
}
@@ -552,3 +556,69 @@ func (h *FileHandler) Download(c *gin.Context) {
// Send file data
c.Data(http.StatusOK, contentType, blob)
}
// LinkToDatasets links files (or folder trees) to one or more datasets.
// Mirrors Python POST /api/v1/files/link-to-datasets (convert).
// @Summary Link files to datasets
// @Description Associate files with target knowledge-base datasets, re-indexing
// as needed. Folder inputs are expanded to their innermost files.
// The heavy DB work runs in a goroutine; the endpoint returns immediately.
// @Tags file
// @Accept json
// @Produce json
// @Param request body service.LinkToDatasetsRequest true "file_ids and kb_ids"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/files/link-to-datasets [post]
func (h *FileHandler) LinkToDatasets(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
var req service.LinkToDatasetsRequest
// Tolerate bind errors: a malformed or empty body simply leaves the fields
// empty, which the validate_request-style check below reports as missing
// arguments — matching Python's @validate_request behaviour and code.
_ = c.ShouldBindJSON(&req)
// Mirror Python @validate_request("file_ids", "kb_ids"): missing arguments
// return ARGUMENT_ERROR (101) with data=null and the aggregated message.
var missing []string
if len(req.FileIDs) == 0 {
missing = append(missing, "file_ids")
}
if len(req.KbIDs) == 0 {
missing = append(missing, "kb_ids")
}
if len(missing) > 0 {
jsonError(c, common.CodeArgumentError, fmt.Sprintf("required argument are missing: %s; ", strings.Join(missing, ",")))
return
}
if err := h.file2DocumentService.LinkToDatasets(user.ID, &req); err != nil {
jsonError(c, linkToDatasetsErrorCode(err), err.Error())
return
}
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"data": true,
"message": "success",
})
}
// linkToDatasetsErrorCode maps File2DocumentService sentinel errors to
// Python-compatible response codes. File/dataset-not-found and no-authorization
// use DATA_ERROR (102), matching Python's get_data_error_result in convert();
// any other (internal) error is reported as a server error.
func linkToDatasetsErrorCode(err error) common.ErrorCode {
switch {
case errors.Is(err, service.ErrLinkFileNotFound),
errors.Is(err, service.ErrLinkDatasetNotFound),
errors.Is(err, service.ErrLinkNoAuthorization):
return common.CodeDataError
default:
return common.CodeServerError
}
}