diff --git a/core/zoptions/notify.go b/core/zoptions/notify.go index 9026744..d9601bd 100644 --- a/core/zoptions/notify.go +++ b/core/zoptions/notify.go @@ -1,9 +1,13 @@ package zoptions -import "time" +import ( + "io" + "os" + "time" +) type NotifyParameters struct { - // AlwaysSend makes sure this message + // AlwaysSend makes sure this message when set to true // is always sent no matter the condition. // // It will cause `zen` to ignore every other @@ -51,12 +55,19 @@ type NotifyParameters struct { // ? 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 []NamedReader } 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) { @@ -71,24 +82,44 @@ func (no NotifiyOptionBuilder) Apply(parameters *NotifyParameters) { } } +// 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 ...NamedReader) 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. // @@ -129,3 +160,122 @@ func (no NotifiyOptionBuilder) BackoffFormula(formula string) NotifiyOptionBuild func Notify() NotifiyOptionBuilder { return NotifiyOptionBuilder{} } + +// NamedReader 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 NamedReader 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 +} + +// NewNamedReader creates a new NamedReader from the given filename, mimeType and reader. +// +// If reader implements io.Closer, it will be closed when the NamedReader is closed. +// +// Otherwise, the reader will be wrapped with io.NopCloser. +func NewNamedReader(filename, mimeType string, reader io.Reader) NamedReader { + var rc io.ReadCloser + if c, ok := reader.(io.ReadCloser); ok { + rc = c + } else { + rc = io.NopCloser(reader) + } + return &namedReader{ + filename: filename, + mimeType: mimeType, + reader: rc, + } +} + +// NewNamedReaderFromFile 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: +// +// NewNamedReaderFromFile("path/to/file.txt", "text/plain") +func NewNamedReaderFromFile(path string, mimetype string) NamedReader { + var rc io.ReadCloser + f, err := os.Open(path) + if err != nil { + rc = &errorReader{err} + } else { + rc = f + } + return &namedReader{ + filename: path, + mimeType: mimetype, + reader: rc, + } +} + +type namedReader struct { + filename string + mimeType string + reader io.ReadCloser +} + +func (na *namedReader) Read(p []byte) (n int, err error) { + return na.reader.Read(p) +} + +func (na *namedReader) Close() error { + return na.reader.Close() +} + +func (na *namedReader) Filename() string { + return na.filename +} + +func (na *namedReader) 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 +}