Skip to content

Commit bc16ef8

Browse files
Merge pull request #82 from binwiederhier/attachments
Attachments
2 parents 014b561 + 6a7b20e commit bc16ef8

36 files changed

+2162
-342
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ build/
33
.idea/
44
server/docs/
55
tools/fbsend/fbsend
6+
playground/
67
*.iml

client/client.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,22 @@ func New(config *Config) *Client {
6767
}
6868

6969
// Publish sends a message to a specific topic, optionally using options.
70+
// See PublishReader for details.
71+
func (c *Client) Publish(topic, message string, options ...PublishOption) (*Message, error) {
72+
return c.PublishReader(topic, strings.NewReader(message), options...)
73+
}
74+
75+
// PublishReader sends a message to a specific topic, optionally using options.
7076
//
7177
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
7278
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
7379
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
7480
//
7581
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
7682
// WithNoFirebase, and the generic WithHeader.
77-
func (c *Client) Publish(topic, message string, options ...PublishOption) (*Message, error) {
83+
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
7884
topicURL := c.expandTopicURL(topic)
79-
req, _ := http.NewRequest("POST", topicURL, strings.NewReader(message))
85+
req, _ := http.NewRequest("POST", topicURL, body)
8086
for _, option := range options {
8187
if err := option(req); err != nil {
8288
return nil, err

client/options.go

+15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ type PublishOption = RequestOption
1616
// SubscribeOption is an option that can be passed to a Client.Subscribe or Client.Poll call
1717
type SubscribeOption = RequestOption
1818

19+
// WithMessage sets the notification message. This is an alternative way to passing the message body.
20+
func WithMessage(message string) PublishOption {
21+
return WithHeader("X-Message", message)
22+
}
23+
1924
// WithTitle adds a title to a message
2025
func WithTitle(title string) PublishOption {
2126
return WithHeader("X-Title", title)
@@ -50,6 +55,16 @@ func WithClick(url string) PublishOption {
5055
return WithHeader("X-Click", url)
5156
}
5257

58+
// WithAttach sets a URL that will be used by the client to download an attachment
59+
func WithAttach(attach string) PublishOption {
60+
return WithHeader("X-Attach", attach)
61+
}
62+
63+
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
64+
func WithFilename(filename string) PublishOption {
65+
return WithHeader("X-Filename", filename)
66+
}
67+
5368
// WithEmail instructs the server to also send the message to the given e-mail address
5469
func WithEmail(email string) PublishOption {
5570
return WithHeader("X-Email", email)

cmd/publish.go

+42-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"fmt"
66
"github.com/urfave/cli/v2"
77
"heckel.io/ntfy/client"
8+
"io"
9+
"os"
10+
"path/filepath"
811
"strings"
912
)
1013

@@ -21,6 +24,9 @@ var cmdPublish = &cli.Command{
2124
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"},
2225
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"},
2326
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, Usage: "URL to open when notification is clicked"},
27+
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, Usage: "URL to send as an external attachment"},
28+
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, Usage: "Filename for the attachment"},
29+
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "File to upload as an attachment"},
2430
&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"},
2531
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"},
2632
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
@@ -37,6 +43,9 @@ Examples:
3743
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
3844
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
3945
ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked
46+
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
47+
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
48+
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
4049
ntfy trigger mywebhook # Sending without message, useful for webhooks
4150
4251
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
@@ -59,6 +68,9 @@ func execPublish(c *cli.Context) error {
5968
tags := c.String("tags")
6069
delay := c.String("delay")
6170
click := c.String("click")
71+
attach := c.String("attach")
72+
filename := c.String("filename")
73+
file := c.String("file")
6274
email := c.String("email")
6375
noCache := c.Bool("no-cache")
6476
noFirebase := c.Bool("no-firebase")
@@ -82,7 +94,13 @@ func execPublish(c *cli.Context) error {
8294
options = append(options, client.WithDelay(delay))
8395
}
8496
if click != "" {
85-
options = append(options, client.WithClick(email))
97+
options = append(options, client.WithClick(click))
98+
}
99+
if attach != "" {
100+
options = append(options, client.WithAttach(attach))
101+
}
102+
if filename != "" {
103+
options = append(options, client.WithFilename(filename))
86104
}
87105
if email != "" {
88106
options = append(options, client.WithEmail(email))
@@ -93,8 +111,30 @@ func execPublish(c *cli.Context) error {
93111
if noFirebase {
94112
options = append(options, client.WithNoFirebase())
95113
}
114+
var body io.Reader
115+
if file == "" {
116+
body = strings.NewReader(message)
117+
} else {
118+
if message != "" {
119+
options = append(options, client.WithMessage(message))
120+
}
121+
if file == "-" {
122+
if filename == "" {
123+
options = append(options, client.WithFilename("stdin"))
124+
}
125+
body = c.App.Reader
126+
} else {
127+
if filename == "" {
128+
options = append(options, client.WithFilename(filepath.Base(file)))
129+
}
130+
body, err = os.Open(file)
131+
if err != nil {
132+
return err
133+
}
134+
}
135+
}
96136
cl := client.New(conf)
97-
m, err := cl.Publish(topic, message, options...)
137+
m, err := cl.PublishReader(topic, body, options...)
98138
if err != nil {
99139
return err
100140
}

cmd/serve.go

+56-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package cmd
22

33
import (
44
"errors"
5+
"fmt"
56
"github.com/urfave/cli/v2"
67
"github.com/urfave/cli/v2/altsrc"
78
"heckel.io/ntfy/server"
89
"heckel.io/ntfy/util"
910
"log"
11+
"math"
1012
"time"
1113
)
1214

@@ -20,6 +22,10 @@ var flagsServe = []cli.Flag{
2022
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
2123
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
2224
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
25+
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
26+
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
27+
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
28+
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
2329
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
2430
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
2531
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
@@ -29,8 +35,10 @@ var flagsServe = []cli.Flag{
2935
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
3036
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
3137
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
32-
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultGlobalTopicLimit, Usage: "total number of topics allowed"}),
38+
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
3339
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
40+
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
41+
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
3442
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
3543
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
3644
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
@@ -69,6 +77,10 @@ func execServe(c *cli.Context) error {
6977
firebaseKeyFile := c.String("firebase-key-file")
7078
cacheFile := c.String("cache-file")
7179
cacheDuration := c.Duration("cache-duration")
80+
attachmentCacheDir := c.String("attachment-cache-dir")
81+
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
82+
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
83+
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
7284
keepaliveInterval := c.Duration("keepalive-interval")
7385
managerInterval := c.Duration("manager-interval")
7486
smtpSenderAddr := c.String("smtp-sender-addr")
@@ -78,8 +90,10 @@ func execServe(c *cli.Context) error {
7890
smtpServerListen := c.String("smtp-server-listen")
7991
smtpServerDomain := c.String("smtp-server-domain")
8092
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
81-
globalTopicLimit := c.Int("global-topic-limit")
93+
totalTopicLimit := c.Int("global-topic-limit")
8294
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
95+
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
96+
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
8397
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
8498
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
8599
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
@@ -105,6 +119,28 @@ func execServe(c *cli.Context) error {
105119
return errors.New("if smtp-sender-addr is set, base-url, smtp-sender-user, smtp-sender-pass and smtp-sender-from must also be set")
106120
} else if smtpServerListen != "" && smtpServerDomain == "" {
107121
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
122+
} else if attachmentCacheDir != "" && baseURL == "" {
123+
return errors.New("if attachment-cache-dir is set, base-url must also be set")
124+
}
125+
126+
// Convert sizes to bytes
127+
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
128+
if err != nil {
129+
return err
130+
}
131+
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
132+
if err != nil {
133+
return err
134+
}
135+
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
136+
if err != nil {
137+
return err
138+
}
139+
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
140+
if err != nil {
141+
return err
142+
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
143+
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
108144
}
109145

110146
// Run server
@@ -117,6 +153,10 @@ func execServe(c *cli.Context) error {
117153
conf.FirebaseKeyFile = firebaseKeyFile
118154
conf.CacheFile = cacheFile
119155
conf.CacheDuration = cacheDuration
156+
conf.AttachmentCacheDir = attachmentCacheDir
157+
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
158+
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
159+
conf.AttachmentExpiryDuration = attachmentExpiryDuration
120160
conf.KeepaliveInterval = keepaliveInterval
121161
conf.ManagerInterval = managerInterval
122162
conf.SMTPSenderAddr = smtpSenderAddr
@@ -126,8 +166,10 @@ func execServe(c *cli.Context) error {
126166
conf.SMTPServerListen = smtpServerListen
127167
conf.SMTPServerDomain = smtpServerDomain
128168
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
129-
conf.GlobalTopicLimit = globalTopicLimit
169+
conf.TotalTopicLimit = totalTopicLimit
130170
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
171+
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
172+
conf.VisitorAttachmentDailyBandwidthLimit = int(visitorAttachmentDailyBandwidthLimit)
131173
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
132174
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
133175
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
@@ -143,3 +185,14 @@ func execServe(c *cli.Context) error {
143185
log.Printf("Exiting.")
144186
return nil
145187
}
188+
189+
func parseSize(s string, defaultValue int64) (v int64, err error) {
190+
if s == "" {
191+
return defaultValue, nil
192+
}
193+
v, err = util.ParseSize(s)
194+
if err != nil {
195+
return 0, err
196+
}
197+
return v, nil
198+
}

0 commit comments

Comments
 (0)