The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/store/http.go

285 lines
8.8 KiB

package store
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/middleware"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
func UploadErrorToStatusCode(err error) int {
switch {
case errors.Is(err, ErrStorageNotFound):
return 404
case errors.Is(err, ErrUnsupportedStorage):
return 400
case errors.Is(err, ErrValidationFailed):
return 400
case errors.Is(err, ErrQuotaReached):
return 400
case errors.Is(err, ErrFileAlreadyExists):
return 400
case errors.Is(err, ErrAccessDenied):
return 403
default:
return 500
}
}
func (s *standardStorageService) RegisterHTTPRoutes(storageRoute routing.RouteRegister) {
storageRoute.Get("/list/", routing.Wrap(s.list))
storageRoute.Get("/list/*", routing.Wrap(s.list))
storageRoute.Get("/read/*", routing.Wrap(s.read))
storageRoute.Get("/options/*", routing.Wrap(s.getOptions))
// Write paths
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
storageRoute.Post("/write/*", reqGrafanaAdmin, routing.Wrap(s.doWrite))
storageRoute.Post("/delete/*", reqGrafanaAdmin, routing.Wrap(s.doDelete))
storageRoute.Post("/upload", reqGrafanaAdmin, routing.Wrap(s.doUpload))
storageRoute.Post("/createFolder", reqGrafanaAdmin, routing.Wrap(s.doCreateFolder))
storageRoute.Post("/deleteFolder", reqGrafanaAdmin, routing.Wrap(s.doDeleteFolder))
storageRoute.Get("/config", reqGrafanaAdmin, routing.Wrap(s.getConfig))
}
func (s *standardStorageService) doWrite(c *contextmodel.ReqContext) response.Response {
scope, path := getPathAndScope(c)
cmd := &WriteValueRequest{}
if err := web.Bind(c.Req, cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
cmd.Path = scope + "/" + path
rsp, err := s.write(c.Req.Context(), c.SignedInUser, cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "save error", err)
}
return response.JSON(http.StatusOK, rsp)
}
func (s *standardStorageService) doUpload(c *contextmodel.ReqContext) response.Response {
type rspInfo struct {
Message string `json:"message,omitempty"`
Path string `json:"path,omitempty"`
Count int `json:"count,omitempty"`
Bytes int `json:"bytes,omitempty"`
Error bool `json:"err,omitempty"`
}
rsp := &rspInfo{Message: "uploaded"}
c.Req.Body = http.MaxBytesReader(c.Resp, c.Req.Body, MAX_UPLOAD_SIZE)
if err := c.Req.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
rsp.Message = fmt.Sprintf("Please limit file uploaded under %s", util.ByteCountSI(MAX_UPLOAD_SIZE))
rsp.Error = true
return response.JSON(http.StatusBadRequest, rsp)
}
message := getMultipartFormValue(c.Req, "message")
overwriteExistingFile := getMultipartFormValue(c.Req, "overwriteExistingFile") != "false" // must explicitly overwrite
folder := getMultipartFormValue(c.Req, "folder")
for k, fileHeaders := range c.Req.MultipartForm.File {
path := getMultipartFormValue(c.Req, k+".path") // match the path with a file
if len(fileHeaders) > 1 {
path = ""
}
if path == "" && folder == "" {
rsp.Message = "please specify the upload folder or full path"
rsp.Error = true
return response.JSON(http.StatusBadRequest, rsp)
}
for _, fileHeader := range fileHeaders {
// restrict file size based on file size
// open each file to copy contents
file, err := fileHeader.Open()
if err != nil {
return response.Error(http.StatusInternalServerError, "Internal Server Error", err)
}
err = file.Close()
if err != nil {
return response.Error(http.StatusInternalServerError, "Internal Server Error", err)
}
data, err := io.ReadAll(file)
if err != nil {
return response.Error(http.StatusInternalServerError, "Internal Server Error", err)
}
if path == "" {
path = folder + "/" + fileHeader.Filename
}
entityType := EntityTypeJSON
mimeType := http.DetectContentType(data)
if strings.HasPrefix(mimeType, "image") || strings.HasSuffix(path, ".svg") {
entityType = EntityTypeImage
}
err = s.Upload(c.Req.Context(), c.SignedInUser, &UploadRequest{
Contents: data,
EntityType: entityType,
Path: path,
OverwriteExistingFile: overwriteExistingFile,
Properties: map[string]string{
"message": message, // the commit/changelog entry
},
})
if err != nil {
return response.Error(UploadErrorToStatusCode(err), err.Error(), err)
}
rsp.Count++
rsp.Bytes += len(data)
rsp.Path = path
}
}
return response.JSON(http.StatusOK, rsp)
}
func getMultipartFormValue(req *http.Request, key string) string {
v, ok := req.MultipartForm.Value[key]
if !ok || len(v) != 1 {
return ""
}
return v[0]
}
func (s *standardStorageService) read(c *contextmodel.ReqContext) response.Response {
// full path is api/storage/read/upload/example.jpg, but we only want the part after read
scope, path := getPathAndScope(c)
file, err := s.Read(c.Req.Context(), c.SignedInUser, scope+"/"+path)
if err != nil {
return response.Error(http.StatusBadRequest, "cannot call read", err)
}
if file == nil || file.Contents == nil {
return response.Error(http.StatusNotFound, "file does not exist", err)
}
// set the correct content type for svg
if strings.HasSuffix(path, ".svg") {
c.Resp.Header().Set("Content-Type", "image/svg+xml")
}
return response.Respond(200, file.Contents)
}
func (s *standardStorageService) getOptions(c *contextmodel.ReqContext) response.Response {
scope, path := getPathAndScope(c)
opts, err := s.getWorkflowOptions(c.Req.Context(), c.SignedInUser, scope+"/"+path)
if err != nil {
return response.Error(http.StatusBadRequest, err.Error(), err)
}
return response.JSON(http.StatusOK, opts)
}
func (s *standardStorageService) doDelete(c *contextmodel.ReqContext) response.Response {
// full path is api/storage/delete/upload/example.jpg, but we only want the part after upload
scope, path := getPathAndScope(c)
err := s.Delete(c.Req.Context(), c.SignedInUser, scope+"/"+path)
if err != nil {
return response.Error(http.StatusBadRequest, "failed to delete the file: "+err.Error(), err)
}
return response.JSON(http.StatusOK, map[string]any{
"message": "Removed file from storage",
"success": true,
"path": path,
})
}
func (s *standardStorageService) doDeleteFolder(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(http.StatusInternalServerError, "error reading bytes", err)
}
cmd := &DeleteFolderCmd{}
err = json.Unmarshal(body, cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "error parsing body", err)
}
if cmd.Path == "" {
return response.Error(http.StatusBadRequest, "empty path", err)
}
// full path is api/storage/delete/upload/example.jpg, but we only want the part after upload
_, path := getPathAndScope(c)
if err := s.DeleteFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil {
return response.Error(http.StatusBadRequest, "failed to delete the folder: "+err.Error(), err)
}
return response.JSON(http.StatusOK, map[string]any{
"message": "Removed folder from storage",
"success": true,
"path": path,
})
}
func (s *standardStorageService) doCreateFolder(c *contextmodel.ReqContext) response.Response {
body, err := io.ReadAll(c.Req.Body)
if err != nil {
return response.Error(http.StatusInternalServerError, "error reading bytes", err)
}
cmd := &CreateFolderCmd{}
err = json.Unmarshal(body, cmd)
if err != nil {
return response.Error(http.StatusBadRequest, "error parsing body", err)
}
if cmd.Path == "" {
return response.Error(http.StatusBadRequest, "empty path", err)
}
if err := s.CreateFolder(c.Req.Context(), c.SignedInUser, cmd); err != nil {
return response.Error(http.StatusBadRequest, "failed to create the folder: "+err.Error(), err)
}
return response.JSON(http.StatusOK, map[string]any{
"message": "Folder created",
"success": true,
"path": cmd.Path,
})
}
func (s *standardStorageService) list(c *contextmodel.ReqContext) response.Response {
params := web.Params(c.Req)
path := params["*"]
// maxFiles of 0 will result in default behaviour from wrapper
frame, err := s.List(c.Req.Context(), c.SignedInUser, path, 0)
if err != nil {
return response.Error(http.StatusBadRequest, "error reading path", err)
}
if frame == nil {
return response.Error(http.StatusNotFound, "not found", nil)
}
return response.JSONStreaming(http.StatusOK, frame)
}
func (s *standardStorageService) getConfig(c *contextmodel.ReqContext) response.Response {
roots := make([]RootStorageMeta, 0)
orgId := c.SignedInUser.GetOrgID()
t := s.tree
t.assureOrgIsInitialized(orgId)
storages := t.getStorages(orgId)
for _, s := range storages {
roots = append(roots, s.Meta())
}
return response.JSON(http.StatusOK, roots)
}