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:
Sofia Papagiannaki
2024-01-25 09:27:13 +02:00
committed by GitHub
parent f154b2b855
commit 5e88d29814
11 changed files with 579 additions and 83 deletions
+163 -10
View File
@@ -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
}