From 2930c4ff62f47c2aa1086c966ceadc805421e784 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 4 Jan 2022 19:45:29 +0100 Subject: [PATCH] Preview URL --- server/cache_sqlite.go | 30 +++++++++++++++++------------- server/config.go | 7 +++++-- server/message.go | 11 ++++++----- server/server.go | 38 ++++++++++++++++++++------------------ util/util.go | 15 +++++++++++++++ 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/server/cache_sqlite.go b/server/cache_sqlite.go index 352ad1af..ee668f20 100644 --- a/server/cache_sqlite.go +++ b/server/cache_sqlite.go @@ -26,6 +26,7 @@ const ( attachment_type TEXT NOT NULL, attachment_size INT NOT NULL, attachment_expires INT NOT NULL, + attachment_preview_url TEXT NOT NULL, attachment_url TEXT NOT NULL, published INT NOT NULL ); @@ -33,24 +34,24 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url, published) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectMessagesSinceTimeQuery = ` - SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url + SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time ASC ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url + SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url FROM messages WHERE topic = ? AND time >= ? ORDER BY time ASC ` selectMessagesDueQuery = ` - SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url + SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_preview_url, attachment_url FROM messages WHERE time <= ? AND published = 0 ` @@ -124,13 +125,14 @@ func (c *sqliteCache) AddMessage(m *message) error { } published := m.Time <= time.Now().Unix() tags := strings.Join(m.Tags, ",") - var attachmentName, attachmentType, attachmentURL string + var attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string var attachmentSize, attachmentExpires int64 if m.Attachment != nil { attachmentName = m.Attachment.Name attachmentType = m.Attachment.Type attachmentSize = m.Attachment.Size attachmentExpires = m.Attachment.Expires + attachmentPreviewURL = m.Attachment.PreviewURL attachmentURL = m.Attachment.URL } _, err := c.db.Exec( @@ -146,6 +148,7 @@ func (c *sqliteCache) AddMessage(m *message) error { attachmentType, attachmentSize, attachmentExpires, + attachmentPreviewURL, attachmentURL, published, ) @@ -231,8 +234,8 @@ func readMessages(rows *sql.Rows) ([]*message, error) { for rows.Next() { var timestamp, attachmentSize, attachmentExpires int64 var priority int - var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentURL string - if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentURL); err != nil { + var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string + if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentPreviewURL, &attachmentURL); err != nil { return nil, err } var tags []string @@ -242,11 +245,12 @@ func readMessages(rows *sql.Rows) ([]*message, error) { var att *attachment if attachmentName != "" && attachmentURL != "" { att = &attachment{ - Name: attachmentName, - Type: attachmentType, - Size: attachmentSize, - Expires: attachmentExpires, - URL: attachmentURL, + Name: attachmentName, + Type: attachmentType, + Size: attachmentSize, + Expires: attachmentExpires, + PreviewURL: attachmentPreviewURL, + URL: attachmentURL, } } messages = append(messages, &message{ diff --git a/server/config.go b/server/config.go index d23109b3..d997f800 100644 --- a/server/config.go +++ b/server/config.go @@ -13,8 +13,9 @@ const ( DefaultAtSenderInterval = 10 * time.Second DefaultMinDelay = 10 * time.Second DefaultMaxDelay = 3 * 24 * time.Hour - DefaultMessageLimit = 4096 - DefaultAttachmentSizeLimit = 5 * 1024 * 1024 + DefaultMessageLimit = 4096 // Bytes + DefaultAttachmentSizeLimit = 15 * 1024 * 1024 + DefaultAttachmentSizePreviewMax = 20 * 1024 * 1024 // Bytes DefaultAttachmentExpiryDuration = 3 * time.Hour DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery ) @@ -48,6 +49,7 @@ type Config struct { CacheDuration time.Duration AttachmentCacheDir string AttachmentSizeLimit int64 + AttachmentSizePreviewMax int64 AttachmentExpiryDuration time.Duration KeepaliveInterval time.Duration ManagerInterval time.Duration @@ -88,6 +90,7 @@ func NewConfig() *Config { CacheDuration: DefaultCacheDuration, AttachmentCacheDir: "", AttachmentSizeLimit: DefaultAttachmentSizeLimit, + AttachmentSizePreviewMax: DefaultAttachmentSizePreviewMax, AttachmentExpiryDuration: DefaultAttachmentExpiryDuration, KeepaliveInterval: DefaultKeepaliveInterval, ManagerInterval: DefaultManagerInterval, diff --git a/server/message.go b/server/message.go index d7baf59e..8cd9cbb5 100644 --- a/server/message.go +++ b/server/message.go @@ -30,11 +30,12 @@ type message struct { } type attachment struct { - Name string `json:"name"` - Type string `json:"type"` - Size int64 `json:"size"` - Expires int64 `json:"expires"` - URL string `json:"url"` + Name string `json:"name"` + Type string `json:"type"` + Size int64 `json:"size"` + Expires int64 `json:"expires"` + PreviewURL string `json:"preview_url"` + URL string `json:"url"` } // messageEncoder is a function that knows how to encode a message diff --git a/server/server.go b/server/server.go index c5db6e38..49d4689b 100644 --- a/server/server.go +++ b/server/server.go @@ -16,7 +16,6 @@ import ( "html/template" "io" "log" - "mime" "net" "net/http" "net/http/httptest" @@ -233,6 +232,7 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) { data["attachment_type"] = m.Attachment.Type data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) + data["attachment_preview_url"] = m.Attachment.PreviewURL data["attachment_url"] = m.Attachment.URL } } @@ -326,9 +326,9 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) { return s.handleDocs(w, r) } else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" { - return s.handleFile(w, r) + return s.withRateLimit(w, r, s.handleFile) } else if r.Method == http.MethodGet && previewRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" { - return s.handlePreview(w, r) + return s.withRateLimit(w, r, s.handlePreview) } else if r.Method == http.MethodOptions { return s.handleOptions(w, r) } else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) { @@ -384,7 +384,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error { return nil } -func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error { +func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, _ *visitor) error { if s.config.AttachmentCacheDir == "" { return errHTTPInternalError } @@ -408,7 +408,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error { return err } -func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) error { +func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request, _ *visitor) error { if s.config.AttachmentCacheDir == "" { return errHTTPInternalError } @@ -422,12 +422,12 @@ func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) error { if err != nil { return errHTTPNotFound } - if stat.Size() > 20*1024*1024 { - return errHTTPInternalError + if stat.Size() > s.config.AttachmentSizePreviewMax { + return errHTTPNotFoundTooLarge } img, err := imaging.Open(file) if err != nil { - return errHTTPNotFoundTooLarge + return err } var width, height int if width >= height { @@ -438,7 +438,7 @@ func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) error { width = int(float32(img.Bounds().Dx()) / float32(img.Bounds().Dy()) * float32(height)) } preview := imaging.Resize(img, width, height, imaging.Lanczos) - return imaging.Encode(w, preview, imaging.PNG) + return imaging.Encode(w, preview, imaging.JPEG, imaging.JPEGQuality(80)) } func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error { @@ -575,10 +575,11 @@ func (s *Server) writeAttachment(r *http.Request, v *visitor, m *message, body * return errHTTPBadRequestInvalidMessage } contentType := http.DetectContentType(body.PeakedBytes) - ext := ".bin" - exts, err := mime.ExtensionsByType(contentType) - if err == nil && len(exts) > 0 { - ext = exts[0] + ext := util.ExtensionByType(contentType) + fileURL := fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext) + previewURL := "" + if strings.HasPrefix(contentType, "image/") { + previewURL = fmt.Sprintf("%s/preview/%s%s", s.config.BaseURL, m.ID, ext) } filename := readParam(r, "x-filename", "filename", "file", "f") if filename == "" { @@ -606,11 +607,12 @@ func (s *Server) writeAttachment(r *http.Request, v *visitor, m *message, body * } m.Message = fmt.Sprintf("You received a file: %s", filename) // May be overwritten later m.Attachment = &attachment{ - Name: filename, - Type: contentType, - Size: size, - Expires: time.Now().Add(s.config.AttachmentExpiryDuration).Unix(), - URL: fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext), + Name: filename, + Type: contentType, + Size: size, + Expires: time.Now().Add(s.config.AttachmentExpiryDuration).Unix(), + PreviewURL: previewURL, + URL: fileURL, } return nil } diff --git a/util/util.go b/util/util.go index 38e28d5b..160852ce 100644 --- a/util/util.go +++ b/util/util.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "math/rand" + "mime" "os" "strings" "sync" @@ -163,3 +164,17 @@ func ExpandHome(path string) string { func ShortTopicURL(s string) string { return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://") } + +// ExtensionByType is a wrapper around mime.ExtensionByType with a few sensible corrections +func ExtensionByType(contentType string) string { + switch contentType { + case "image/jpeg": + return ".jpg" + default: + exts, err := mime.ExtensionsByType(contentType) + if err == nil && len(exts) > 0 { + return exts[0] + } + return ".bin" + } +}