348 lines
10 KiB
Go
348 lines
10 KiB
Go
package zoptions
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"gitlab.bareksa.com/backend/zen/internal/bufferpool"
|
|
)
|
|
|
|
type NotifyParameters struct {
|
|
// AlwaysSend makes sure this message when set to true
|
|
// is always sent no matter the condition.
|
|
//
|
|
// It will cause `zen` to ignore every other
|
|
// parameters and just send the message.
|
|
AlwaysSend bool
|
|
|
|
// InitialBackoff is the starting backoff.
|
|
//
|
|
// If not specified, the default duration is time.Minute.
|
|
InitialBackoff time.Duration
|
|
// MaxBackoff is the maximum backoff. If unset, the default value
|
|
// is time.Hour * 24.
|
|
MaxBackoff time.Duration
|
|
|
|
// BackoffFormula is a string that represents the formula to calculate the backoff time
|
|
// when multiple message with the same key is fired off repeatedly.
|
|
//
|
|
// It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go
|
|
// for the language spec.
|
|
//
|
|
// Expression must return a duration type.
|
|
//
|
|
// If invalid cel expression, empty string, or wrong return value, default formula will be used.
|
|
//
|
|
// Available variables:
|
|
//
|
|
// - `repeat`: the number of times the notification has been sent. type: int.
|
|
// - `last`: the last time the notification was sent. type: timestamp.
|
|
// - `prev_backoff`: the backoff value from previous evaluation. 0 if first seen. Type: duration.
|
|
// - `initial_backoff`: the initial_backoff value. Type: duration
|
|
// - `max_backoff`: the maximum backoff value. Type: duration
|
|
//
|
|
// Available Non-Standard functions:
|
|
//
|
|
// // pow multiplies the base with the exponent
|
|
// pow(base: double, exp: double) -> double
|
|
//
|
|
// // mult_dur_double multiplies the duration with the double value
|
|
// mult_dur_double(dur: duration, mult: double) -> duration
|
|
//
|
|
//
|
|
// Example (and also default formula):
|
|
//
|
|
// mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff
|
|
// ? max_backoff
|
|
// : mult_dur_double(initial_backoff, pow(1.5, double(repeat)))
|
|
BackoffFormula string
|
|
|
|
// Attachments is a list of NamedReader (files or buffer) that will be sent as attachments.
|
|
//
|
|
// Attachments are guaranteed to be closed wether the notification is sent successfully or not
|
|
// to avoid resource leaks.
|
|
Attachments []NamedReadCloser
|
|
}
|
|
|
|
type NotifyOption interface {
|
|
Apply(parameters *NotifyParameters)
|
|
}
|
|
|
|
// NotifyOptionFunc is a shortcut to create a NotifyOption from a function.
|
|
type NotifyOptionFunc func(parameters *NotifyParameters)
|
|
|
|
func (f NotifyOptionFunc) Apply(parameters *NotifyParameters) {
|
|
f(parameters)
|
|
}
|
|
|
|
type NotifiyOptionBuilder []NotifyOption
|
|
|
|
func (no NotifiyOptionBuilder) Apply(parameters *NotifyParameters) {
|
|
for _, opt := range no {
|
|
opt.Apply(parameters)
|
|
}
|
|
}
|
|
|
|
// AlwaysSend makes sure this message when set to true
|
|
// is always sent no matter the condition.
|
|
//
|
|
// It will cause `zen` to ignore every other
|
|
// parameters and just send the message.
|
|
func (no NotifiyOptionBuilder) AlwaysSend(alwaysSend bool) NotifiyOptionBuilder {
|
|
return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) {
|
|
parameters.AlwaysSend = alwaysSend
|
|
}))
|
|
}
|
|
|
|
// InitialBackoff is the starting backoff.
|
|
//
|
|
// If not specified, the default duration is time.Minute.
|
|
func (no NotifiyOptionBuilder) InitialBackoff(backoff time.Duration) NotifiyOptionBuilder {
|
|
return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) {
|
|
parameters.InitialBackoff = backoff
|
|
}))
|
|
}
|
|
|
|
// MaxBackoff is the maximum backoff. If unset, the default value
|
|
// is time.Hour * 24.
|
|
func (no NotifiyOptionBuilder) MaxBackoff(maxBackoff time.Duration) NotifiyOptionBuilder {
|
|
return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) {
|
|
parameters.MaxBackoff = maxBackoff
|
|
}))
|
|
}
|
|
|
|
// Attachments is a list attachments (files or buffers) that will be sent with the notification.
|
|
//
|
|
// Attachments are guaranteed to be closed wether the notification is sent successfully or not
|
|
// to avoid resource leaks.
|
|
func (no NotifiyOptionBuilder) Attachments(attachments ...NamedReadCloser) NotifiyOptionBuilder {
|
|
return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) {
|
|
parameters.Attachments = append(parameters.Attachments, attachments...)
|
|
}))
|
|
}
|
|
|
|
// BackoffFormula is a string that represents the formula to calculate the backoff time
|
|
// when multiple message with the same key is fired off repeatedly.
|
|
//
|
|
// It uses CEL (Common Expression Language) syntax. See: https://github.com/google/cel-go
|
|
// for the language spec.
|
|
//
|
|
// Expression must return a duration type.
|
|
//
|
|
// If invalid cel expression, empty string, or wrong return value, default formula will be used.
|
|
//
|
|
// Available variables:
|
|
//
|
|
// - `repeat`: the number of times the notification has been sent. type: int.
|
|
// - `last`: the last time the notification was sent. type: timestamp.
|
|
// - `prev_backoff`: the backoff value from previous evaluation. 0 if this message is first seen. Type: duration.
|
|
// - `initial_backoff`: the initial_backoff value. Type: duration
|
|
// - `max_backoff`: the maximum backoff value. Type: duration
|
|
//
|
|
// Available Non-Standard functions:
|
|
//
|
|
// // pow multiplies the base with the exponent
|
|
// pow(base: double, exp: double) -> double
|
|
//
|
|
// // mult_dur_double multiplies the duration with the double value
|
|
// mult_dur_double(dur: duration, mult: double) -> duration
|
|
//
|
|
// Example (and also default formula):
|
|
//
|
|
// mult_dur_double(initial_backoff, pow(1.5, double(repeat))) > max_backoff
|
|
// ? max_backoff
|
|
// : mult_dur_double(initial_backoff, pow(1.5, double(repeat)))
|
|
func (no NotifiyOptionBuilder) BackoffFormula(formula string) NotifiyOptionBuilder {
|
|
return append(no, NotifyOptionFunc(func(parameters *NotifyParameters) {
|
|
parameters.BackoffFormula = formula
|
|
}))
|
|
}
|
|
|
|
func Notify() NotifiyOptionBuilder {
|
|
return NotifiyOptionBuilder{}
|
|
}
|
|
|
|
// NamedReadCloser is an interface that extends io.ReadCloser with
|
|
// Filename and MimeType methods.
|
|
//
|
|
// This is used to pass a file with its metadata to the notification
|
|
// system.
|
|
type NamedReadCloser interface {
|
|
io.ReadCloser
|
|
// Filename returns the name of the file.
|
|
//
|
|
// Filename will be sluggified and used as the filename
|
|
// in the notification.
|
|
Filename() string
|
|
// MimeType returns the mime type of the file.
|
|
//
|
|
// MimeType is used by Zen to determine the type of the file.
|
|
//
|
|
// Use constant values from: https://github.com/ldez/mimetype
|
|
// for maximum compatibility.
|
|
//
|
|
// Attachment like images, videos, etc, using well known mime types
|
|
// that are supported by browsers can
|
|
// be displayed by the browser.
|
|
//
|
|
// Mimetypes that are supported by browsers:
|
|
//
|
|
// - image/png
|
|
// - image/jpeg
|
|
// - image/gif
|
|
// - image/svg+xml
|
|
// - video/mp4
|
|
// - video/webm
|
|
// - video/ogg
|
|
// - audio/mpeg
|
|
// - audio/ogg
|
|
// - audio/wav
|
|
//
|
|
// Documents like pdf, docx, xlsx, will be given a download link and proper icons.
|
|
// How they are handled is up to the browser.
|
|
//
|
|
// If the mimetype is not supported, it will be given a download link with a generic icon.
|
|
MimeType() string
|
|
}
|
|
|
|
// NewNamedReadCloser creates a new NamedReader from the given filename, mimeType and reader.
|
|
//
|
|
// If reader implements io.Closer, it will be closed when the NamedReadCloser is closed.
|
|
//
|
|
// Otherwise, the reader will be wrapped with io.NopCloser.
|
|
//
|
|
// NewNamedReadCloser claims ownership of the reader and the caller should not use the reader
|
|
// after passing it to NewNamedReadCloser.
|
|
//
|
|
// To create a NamedReadCloser that can copy the reader as it's consumed, e.g. the reader
|
|
// is used for HTTP.Request.Body for example, use TeeNamedReader.
|
|
func NewNamedReadCloser(filename, mimeType string, reader io.Reader) NamedReadCloser {
|
|
var rc io.ReadCloser
|
|
if c, ok := reader.(io.ReadCloser); ok {
|
|
rc = c
|
|
} else {
|
|
rc = io.NopCloser(reader)
|
|
}
|
|
return &namedReadCloser{
|
|
filename: filename,
|
|
mimeType: mimeType,
|
|
reader: rc,
|
|
}
|
|
}
|
|
|
|
// NewNamedReadCloserFromFile same as NewNamedReader but reads the file from the filesystem.
|
|
//
|
|
// If the file does not exist or there is an error in permissions on reading the file,
|
|
// the error will be logged but otherwise ignored.
|
|
//
|
|
// Example:
|
|
//
|
|
// NewNamedReadCloserFromFile("path/to/file.txt", "text/plain")
|
|
func NewNamedReadCloserFromFile(path string, mimetype string) NamedReadCloser {
|
|
var rc io.ReadCloser
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
rc = &errorReader{err}
|
|
} else {
|
|
if stat, err := f.Stat(); err == nil && stat.IsDir() {
|
|
f.Close()
|
|
rc = &errorReader{fmt.Errorf("file %s is a directory", path)}
|
|
} else {
|
|
rc = f
|
|
}
|
|
}
|
|
return &namedReadCloser{
|
|
filename: path,
|
|
mimeType: mimetype,
|
|
reader: rc,
|
|
}
|
|
}
|
|
|
|
type readCloser struct {
|
|
tee io.Reader
|
|
close func() error
|
|
}
|
|
|
|
// Read implements io.Reader.
|
|
func (re *readCloser) Read(p []byte) (n int, err error) {
|
|
return re.tee.Read(p)
|
|
}
|
|
|
|
// Close implements io.Closer.
|
|
func (re *readCloser) Close() error {
|
|
return re.close()
|
|
}
|
|
|
|
// TeeNamedReadCloser copies everything read from returned rc to namedReadCloser as it's consumed.
|
|
//
|
|
// Useful for e.g. HTTP Request Body.
|
|
//
|
|
// if reader is io.ReadCloser, it will be closed when the returned rc is closed (NOT namedReadCloser!).
|
|
// Otherwise, the reader will be wrapped with io.NopCloser.
|
|
//
|
|
// Example HTTP Request Body:
|
|
//
|
|
// // tee will forward the close method if the original reader is a ReadCloser.
|
|
// tee, named := zoptions.TeeNamedReadCloser("file.txt", "text/plain", body)
|
|
// req, _ := http.NewRequest("POST", "http://example.com", tee) // use tee as the body.
|
|
//
|
|
// resp, err := http.DefaultClient.Do(req)
|
|
// // named now contains copy of values of the reader
|
|
// // and can be used to send the file as attachment.
|
|
func TeeNamedReadCloser(name, mimeType string, reader io.Reader) (rc io.ReadCloser, namedReadCloser NamedReadCloser) {
|
|
buf := bufferpool.Get()
|
|
if e, ok := reader.(interface{ Len() int }); ok {
|
|
buf.Grow(e.Len())
|
|
}
|
|
var closeFunc func() error
|
|
if c, ok := reader.(io.ReadCloser); ok {
|
|
closeFunc = c.Close
|
|
} else {
|
|
closeFunc = func() error { return nil }
|
|
}
|
|
|
|
tee := io.TeeReader(reader, buf)
|
|
teeReader := &readCloser{
|
|
tee: tee,
|
|
close: closeFunc,
|
|
}
|
|
nrc := NewNamedReadCloser(name, mimeType, buf)
|
|
return teeReader, nrc
|
|
}
|
|
|
|
type namedReadCloser struct {
|
|
filename string
|
|
mimeType string
|
|
reader io.ReadCloser
|
|
}
|
|
|
|
func (na *namedReadCloser) Read(p []byte) (n int, err error) {
|
|
return na.reader.Read(p)
|
|
}
|
|
|
|
func (na *namedReadCloser) Close() error {
|
|
return na.reader.Close()
|
|
}
|
|
|
|
func (na *namedReadCloser) Filename() string {
|
|
return na.filename
|
|
}
|
|
|
|
func (na *namedReadCloser) MimeType() string {
|
|
return na.mimeType
|
|
}
|
|
|
|
type errorReader struct {
|
|
err error
|
|
}
|
|
|
|
func (er *errorReader) Read(p []byte) (n int, err error) {
|
|
return 0, er.err
|
|
}
|
|
|
|
func (er *errorReader) Close() error {
|
|
return nil
|
|
}
|