mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
If an image token is present in an alert instance, the email notifier will attempt to find a public URL for the image token. If found, it will add that to the email as the `ImageLink` field. If only local file data is available, the notifier will attach the file to the outgoing email using the `EmbeddedImage` field.
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
)
|
|
|
|
var (
|
|
// ErrImageNotFound is returned when the image does not exist.
|
|
ErrImageNotFound = errors.New("image not found")
|
|
)
|
|
|
|
type Image struct {
|
|
ID int64 `xorm:"pk autoincr 'id'"`
|
|
Token string `xorm:"token"`
|
|
Path string `xorm:"path"`
|
|
URL string `xorm:"url"`
|
|
CreatedAt time.Time `xorm:"created_at"`
|
|
ExpiresAt time.Time `xorm:"expires_at"`
|
|
}
|
|
|
|
// A XORM interface that lets us clean up our SQL session definition.
|
|
func (i *Image) TableName() string {
|
|
return "alert_image"
|
|
}
|
|
|
|
type ImageStore interface {
|
|
// Get returns the image with the token or ErrImageNotFound.
|
|
GetImage(ctx context.Context, token string) (*Image, error)
|
|
|
|
// Saves the image or returns an error.
|
|
SaveImage(ctx context.Context, img *Image) error
|
|
|
|
GetURL(ctx context.Context, token string) (string, error)
|
|
|
|
GetFilepath(ctx context.Context, token string) (string, error)
|
|
|
|
// Returns an io.ReadCloser that reads out the image data for the provided
|
|
// token, if available. May return ErrImageNotFound.
|
|
GetData(ctx context.Context, token string) (io.ReadCloser, error)
|
|
}
|
|
|
|
func (st DBstore) GetImage(ctx context.Context, token string) (*Image, error) {
|
|
var img Image
|
|
if err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
|
exists, err := sess.Where("token = ?", token).Get(&img)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get image: %w", err)
|
|
}
|
|
if !exists {
|
|
return ErrImageNotFound
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return &img, nil
|
|
}
|
|
|
|
func (st DBstore) SaveImage(ctx context.Context, img *Image) error {
|
|
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
|
// TODO: Is this a good idea? Do we actually want to automatically expire
|
|
// rows? See issue https://github.com/grafana/grafana/issues/49366
|
|
img.ExpiresAt = TimeNow().Add(1 * time.Minute).UTC()
|
|
if img.ID == 0 { // xorm will fill this field on Insert.
|
|
token, err := uuid.NewV4()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create token: %w", err)
|
|
}
|
|
img.Token = token.String()
|
|
img.CreatedAt = TimeNow().UTC()
|
|
if _, err := sess.Insert(img); err != nil {
|
|
return fmt.Errorf("failed to insert screenshot: %w", err)
|
|
}
|
|
} else {
|
|
affected, err := sess.ID(img.ID).Update(img)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update screenshot: %v", err)
|
|
}
|
|
if affected == 0 {
|
|
return fmt.Errorf("update statement had no effect")
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (st *DBstore) GetURL(ctx context.Context, token string) (string, error) {
|
|
img, err := st.GetImage(ctx, token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return img.URL, nil
|
|
}
|
|
|
|
func (st *DBstore) GetFilepath(ctx context.Context, token string) (string, error) {
|
|
img, err := st.GetImage(ctx, token)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return img.Path, nil
|
|
}
|
|
|
|
func (st *DBstore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
|
// TODO: Should we support getting data from image.URL? One could configure
|
|
// the system to upload to S3 while still reading data for notifiers like
|
|
// Slack that take multipart uploads.
|
|
img, err := st.GetImage(ctx, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(img.Path) == 0 {
|
|
return nil, ErrImageNotFound
|
|
}
|
|
|
|
f, err := os.Open(img.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return f, nil
|
|
}
|
|
|
|
//nolint:unused
|
|
func (st DBstore) DeleteExpiredImages(ctx context.Context) error {
|
|
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
|
n, err := sess.Where("expires_at < ?", TimeNow()).Delete(&Image{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete expired images: %w", err)
|
|
}
|
|
st.Logger.Info("deleted expired images", "n", n)
|
|
return err
|
|
})
|
|
}
|