fix: search files recursively within current folder subtree (#17779)

This commit is contained in:
euvre
2026-08-11 23:43:06 -07:00
committed by GitHub
parent a530a3a170
commit fdb6b5fdd2
3 changed files with 254 additions and 28 deletions

View File

@@ -45,17 +45,26 @@ func (dao *FileDAO) GetByID(ctx context.Context, db *gorm.DB, id string) (*entit
return &file, nil
}
// GetByPfID gets files by parent folder ID with pagination and filtering
// GetByPfID gets files by parent folder ID with pagination and filtering.
// When keywords is empty, only direct children of pfID are listed; when
// keywords is non-empty, the search covers the whole subtree under pfID so
// files and folders nested in sub-folders can be found too.
func (dao *FileDAO) GetByPfID(ctx context.Context, db *gorm.DB, tenantID, pfID string, page, pageSize int, orderBy string, desc bool, keywords string) ([]*entity.File, int64, error) {
var files []*entity.File
var total int64
query := db.WithContext(ctx).Model(&entity.File{}).
Where("tenant_id = ? AND parent_id = ? AND id != ?", tenantID, pfID, pfID)
Where("tenant_id = ? AND id != ?", tenantID, pfID)
// Apply keyword filter
if keywords != "" {
query = query.Where("LOWER(name) LIKE ?", "%"+strings.ToLower(keywords)+"%")
descendantIDs, err := dao.GetSubtreeIDs(ctx, db, tenantID, pfID)
if err != nil {
return nil, 0, err
}
query = query.Where("parent_id IN ?", descendantIDs).
Where("LOWER(name) LIKE ?", "%"+strings.ToLower(keywords)+"%")
} else {
query = query.Where("parent_id = ?", pfID)
}
// Count total
@@ -85,6 +94,43 @@ func (dao *FileDAO) GetByPfID(ctx context.Context, db *gorm.DB, tenantID, pfID s
return files, total, nil
}
// GetSubtreeIDs returns pfID itself plus the IDs of all entries nested under
// it (folders and files), used to scope recursive keyword searches.
func (dao *FileDAO) GetSubtreeIDs(ctx context.Context, db *gorm.DB, tenantID, pfID string) ([]string, error) {
var rows []struct {
ID string
ParentID string
}
if err := db.WithContext(ctx).Model(&entity.File{}).
Select("id", "parent_id").
Where("tenant_id = ?", tenantID).
Find(&rows).Error; err != nil {
return nil, err
}
children := make(map[string][]string, len(rows))
for _, row := range rows {
children[row.ParentID] = append(children[row.ParentID], row.ID)
}
ids := []string{pfID}
inTree := map[string]struct{}{pfID: {}}
queue := []string{pfID}
for len(queue) > 0 {
cur := queue[0]
queue = queue[1:]
for _, child := range children[cur] {
if _, ok := inTree[child]; ok {
continue
}
inTree[child] = struct{}{}
ids = append(ids, child)
queue = append(queue, child)
}
}
return ids, nil
}
// GetRootFolder gets or creates root folder for tenant
func (dao *FileDAO) GetRootFolder(ctx context.Context, db *gorm.DB, tenantID string) (*entity.File, error) {
var file entity.File

150
internal/dao/file_test.go Normal file
View File

@@ -0,0 +1,150 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package dao
import (
"context"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"ragflow/internal/entity"
)
func setupFileTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
TranslateError: true,
})
if err != nil {
t.Fatalf("failed to open sqlite: %v", err)
}
if err := db.AutoMigrate(&entity.File{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
return db
}
func testFile(t *testing.T, db *gorm.DB, id, parentID, tenantID, name, fileType string) {
t.Helper()
f := &entity.File{
ID: id,
ParentID: parentID,
TenantID: tenantID,
CreatedBy: tenantID,
Name: name,
Type: fileType,
}
if err := db.Create(f).Error; err != nil {
t.Fatalf("failed to create file %s: %v", id, err)
}
}
// seedFileTree builds: root -> {dirA -> {subB -> [notes-deep.txt]}, top-report.pdf}, other -> [outside-report.txt]
func seedFileTree(t *testing.T, db *gorm.DB) {
t.Helper()
testFile(t, db, "root", "root", "t1", "/", "folder")
testFile(t, db, "dirA", "root", "t1", "dirA", "folder")
testFile(t, db, "subB", "dirA", "t1", "subB", "folder")
testFile(t, db, "f-deep", "subB", "t1", "notes-deep.txt", "doc")
testFile(t, db, "f-top", "root", "t1", "top-report.pdf", "doc")
testFile(t, db, "other", "other", "t1", "other", "folder")
testFile(t, db, "f-out", "other", "t1", "outside-report.txt", "doc")
testFile(t, db, "f-t2", "root", "t2", "report-t2.txt", "doc")
}
func TestFileDAO_GetByPfID_KeywordsSearchesSubtree(t *testing.T) {
db := setupFileTestDB(t)
seedFileTree(t, db)
d := NewFileDAO()
ctx := context.Background()
files, total, err := d.GetByPfID(ctx, db, "t1", "root", 1, 15, "create_time", true, "report")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 1 || len(files) != 1 || files[0].ID != "f-top" {
t.Fatalf("expected only f-top, got total=%d files=%v", total, files)
}
// Nested file two levels down must be found from the root folder.
files, total, err = d.GetByPfID(ctx, db, "t1", "root", 1, 15, "create_time", true, "notes")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 1 || len(files) != 1 || files[0].ID != "f-deep" {
t.Fatalf("expected nested f-deep, got total=%d files=%v", total, files)
}
// Folders themselves are searchable by name.
files, total, err = d.GetByPfID(ctx, db, "t1", "root", 1, 15, "create_time", true, "sub")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 1 || len(files) != 1 || files[0].ID != "subB" {
t.Fatalf("expected folder subB, got total=%d files=%v", total, files)
}
}
func TestFileDAO_GetByPfID_KeywordsScopedToSubtree(t *testing.T) {
db := setupFileTestDB(t)
seedFileTree(t, db)
d := NewFileDAO()
ctx := context.Background()
// Searching inside dirA must not match files outside that subtree.
files, total, err := d.GetByPfID(ctx, db, "t1", "dirA", 1, 15, "create_time", true, "report")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 0 || len(files) != 0 {
t.Fatalf("expected no results outside subtree, got total=%d files=%v", total, files)
}
// Tenant isolation still applies.
files, total, err = d.GetByPfID(ctx, db, "t2", "root", 1, 15, "create_time", true, "report")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 1 || len(files) != 1 || files[0].ID != "f-t2" {
t.Fatalf("expected only tenant t2 file, got total=%d files=%v", total, files)
}
}
func TestFileDAO_GetByPfID_NoKeywordsListsDirectChildren(t *testing.T) {
db := setupFileTestDB(t)
seedFileTree(t, db)
d := NewFileDAO()
ctx := context.Background()
files, total, err := d.GetByPfID(ctx, db, "t1", "root", 1, 15, "create_time", true, "")
if err != nil {
t.Fatalf("GetByPfID failed: %v", err)
}
if total != 2 || len(files) != 2 {
t.Fatalf("expected 2 direct children, got total=%d files=%v", total, files)
}
for _, f := range files {
if f.ParentID != "root" || f.ID == "root" {
t.Fatalf("unexpected entry in direct listing: %+v", f)
}
}
}