Folders: Introduce folder service function for fetching folders by org and UIDs that contain optionally the folder full path (#80716)
* Folders: Expose function for getting all org folders with specific UIDs * Return all org folders if UIDs is empty * Filter out not accessible folders by the user * Modify query to optionally returning a string that contains the UIDs of all parent folders separated by slash.
This commit is contained in:
@@ -2,6 +2,7 @@ package folderimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -12,10 +13,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const DEFAULT_BATCH_SIZE = 999
|
||||
|
||||
type sqlStore struct {
|
||||
db db.DB
|
||||
log log.Logger
|
||||
@@ -342,27 +346,176 @@ func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64,
|
||||
return height, nil
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetFolders(ctx context.Context, orgID int64, uids []string) ([]*folder.Folder, error) {
|
||||
if len(uids) == 0 {
|
||||
return []*folder.Folder{}, nil
|
||||
// GetFolders returns org folders by their UIDs.
|
||||
// If UIDs is empty, it returns all folders in the org.
|
||||
// If WithFullpath is true it computes also the full path of a folder.
|
||||
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
||||
// For example, if the folder structure is:
|
||||
//
|
||||
// A
|
||||
// └── B
|
||||
// └── C
|
||||
//
|
||||
// The full path of C is "A/B/C".
|
||||
// The full path of B is "A/B".
|
||||
// The full path of A is "A".
|
||||
// If a folder contains a slash in its title, it is escaped with a backslash.
|
||||
// For example, if the folder structure is:
|
||||
//
|
||||
// A
|
||||
// └── B/C
|
||||
//
|
||||
// The full path of C is "A/B\/C".
|
||||
//
|
||||
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
|
||||
// For example, if the folder structure is:
|
||||
//
|
||||
// A (uid: "uid1")
|
||||
// └── B (uid: "uid2")
|
||||
// └── C (uid: "uid3")
|
||||
//
|
||||
// The full path UIDs of C is "uid1/uid2/uid3".
|
||||
// The full path UIDs of B is "uid1/uid2".
|
||||
// The full path UIDs of A is "uid1".
|
||||
func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) {
|
||||
if q.BatchSize == 0 {
|
||||
q.BatchSize = DEFAULT_BATCH_SIZE
|
||||
}
|
||||
|
||||
var folders []*folder.Folder
|
||||
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||
b := strings.Builder{}
|
||||
b.WriteString(`SELECT * FROM folder WHERE org_id=? AND uid IN (?` + strings.Repeat(", ?", len(uids)-1) + `)`)
|
||||
args := []any{orgID}
|
||||
for _, uid := range uids {
|
||||
args = append(args, uid)
|
||||
}
|
||||
return sess.SQL(b.String(), args...).Find(&folders)
|
||||
return batch(len(q.UIDs), int(q.BatchSize), func(start, end int) error {
|
||||
partialFolders := make([]*folder.Folder, 0, q.BatchSize)
|
||||
partialUIDs := q.UIDs[start:min(end, len(q.UIDs))]
|
||||
s := strings.Builder{}
|
||||
s.WriteString(`SELECT f0.id, f0.org_id, f0.uid, f0.parent_uid, f0.title, f0.description, f0.created, f0.updated`)
|
||||
// compute full path column if requested
|
||||
if q.WithFullpath {
|
||||
s.WriteString(fmt.Sprintf(`, %s AS fullpath`, getFullpathSQL(ss.db.GetDialect())))
|
||||
}
|
||||
// compute full path UIDs column if requested
|
||||
if q.WithFullpathUIDs {
|
||||
s.WriteString(fmt.Sprintf(`, %s AS fullpath_uids`, getFullapathUIDsSQL(ss.db.GetDialect())))
|
||||
}
|
||||
s.WriteString(` FROM folder f0`)
|
||||
// join the same table multiple times to compute the full path of a folder
|
||||
if q.WithFullpath || q.WithFullpathUIDs || len(q.ancestorUIDs) > 0 {
|
||||
s.WriteString(getFullpathJoinsSQL())
|
||||
}
|
||||
s.WriteString(` WHERE f0.org_id=?`)
|
||||
args := []any{q.OrgID}
|
||||
if len(partialUIDs) > 0 {
|
||||
s.WriteString(` AND f0.uid IN (?` + strings.Repeat(", ?", len(partialUIDs)-1) + `)`)
|
||||
for _, uid := range partialUIDs {
|
||||
args = append(args, uid)
|
||||
}
|
||||
}
|
||||
|
||||
if len(q.ancestorUIDs) == 0 {
|
||||
err := sess.SQL(s.String(), args...).Find(&partialFolders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
folders = append(folders, partialFolders...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// filter out folders if they are not in the subtree of the given ancestor folders
|
||||
if err := batch(len(q.ancestorUIDs), int(q.BatchSize), func(start2, end2 int) error {
|
||||
s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.ancestorUIDs, start2, end2, s.String(), args)
|
||||
err := sess.SQL(s2, args2...).Find(&partialFolders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
folders = append(folders, partialFolders...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add URLs
|
||||
for i, f := range folders {
|
||||
f.Fullpath = strings.TrimLeft(f.Fullpath, "/")
|
||||
f.FullpathUIDs = strings.TrimLeft(f.FullpathUIDs, "/")
|
||||
folders[i] = f.WithURL()
|
||||
}
|
||||
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func getFullpathSQL(dialect migrator.Dialect) string {
|
||||
concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||
concatCols = append(concatCols, "COALESCE(REPLACE(f0.title, '/', '\\/'), '')")
|
||||
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||
concatCols = append([]string{fmt.Sprintf("COALESCE(REPLACE(f%d.title, '/', '\\/'), '')", i), "'/'"}, concatCols...)
|
||||
}
|
||||
return dialect.Concat(concatCols...)
|
||||
}
|
||||
|
||||
func getFullapathUIDsSQL(dialect migrator.Dialect) string {
|
||||
concatCols := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||
concatCols = append(concatCols, "COALESCE(f0.uid, '')")
|
||||
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||
concatCols = append([]string{fmt.Sprintf("COALESCE(f%d.uid, '')", i), "'/'"}, concatCols...)
|
||||
}
|
||||
return dialect.Concat(concatCols...)
|
||||
}
|
||||
|
||||
// getFullpathJoinsSQL returns a SQL fragment that joins the same table multiple times to get the full path of a folder.
|
||||
func getFullpathJoinsSQL() string {
|
||||
joins := make([]string, 0, folder.MaxNestedFolderDepth)
|
||||
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||
joins = append(joins, fmt.Sprintf(` LEFT JOIN folder f%d ON f%d.org_id = f%d.org_id AND f%d.uid = f%d.parent_uid`, i, i, i-1, i, i-1))
|
||||
}
|
||||
return strings.Join(joins, "\n")
|
||||
}
|
||||
|
||||
func getAncestorsSQL(dialect migrator.Dialect, ancestorUIDs []string, start int, end int, origSQL string, origArgs []any) (string, []any) {
|
||||
s2 := strings.Builder{}
|
||||
s2.WriteString(origSQL)
|
||||
args2 := make([]any, 0, len(ancestorUIDs)*folder.MaxNestedFolderDepth)
|
||||
args2 = append(args2, origArgs...)
|
||||
|
||||
partialAncestorUIDs := ancestorUIDs[start:min(end, len(ancestorUIDs))]
|
||||
partialArgs := make([]any, 0, len(partialAncestorUIDs))
|
||||
for _, uid := range partialAncestorUIDs {
|
||||
partialArgs = append(partialArgs, uid)
|
||||
}
|
||||
s2.WriteString(` AND ( f0.uid IN (?` + strings.Repeat(", ?", len(partialAncestorUIDs)-1) + `)`)
|
||||
args2 = append(args2, partialArgs...)
|
||||
for i := 1; i <= folder.MaxNestedFolderDepth; i++ {
|
||||
s2.WriteString(fmt.Sprintf(` OR f%d.uid IN (?`+strings.Repeat(", ?", len(partialAncestorUIDs)-1)+`)`, i))
|
||||
args2 = append(args2, partialArgs...)
|
||||
}
|
||||
s2.WriteString(` )`)
|
||||
return s2.String(), args2
|
||||
}
|
||||
|
||||
func batch(count, batchSize int, eachFn func(start, end int) error) error {
|
||||
if count == 0 {
|
||||
if err := eachFn(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < count; {
|
||||
end := i + batchSize
|
||||
if end > count {
|
||||
end = count
|
||||
}
|
||||
|
||||
if err := eachFn(i, end); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i = end
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user