Preview URL

This commit is contained in:
Philipp Heckel 2022-01-04 19:45:29 +01:00
parent 38788bb2e9
commit 2930c4ff62
5 changed files with 63 additions and 38 deletions

View File

@ -26,6 +26,7 @@ const (
attachment_type TEXT NOT NULL, attachment_type TEXT NOT NULL,
attachment_size INT NOT NULL, attachment_size INT NOT NULL,
attachment_expires INT NOT NULL, attachment_expires INT NOT NULL,
attachment_preview_url TEXT NOT NULL,
attachment_url TEXT NOT NULL, attachment_url TEXT NOT NULL,
published INT NOT NULL published INT NOT NULL
); );
@ -33,24 +34,24 @@ const (
COMMIT; COMMIT;
` `
insertMessageQuery = ` insertMessageQuery = `
INSERT INTO messages (id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, published) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectMessagesSinceTimeQuery = ` 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 FROM messages
WHERE topic = ? AND time >= ? AND published = 1 WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time ASC ORDER BY time ASC
` `
selectMessagesSinceTimeIncludeScheduledQuery = ` 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 FROM messages
WHERE topic = ? AND time >= ? WHERE topic = ? AND time >= ?
ORDER BY time ASC ORDER BY time ASC
` `
selectMessagesDueQuery = ` 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 FROM messages
WHERE time <= ? AND published = 0 WHERE time <= ? AND published = 0
` `
@ -124,13 +125,14 @@ func (c *sqliteCache) AddMessage(m *message) error {
} }
published := m.Time <= time.Now().Unix() published := m.Time <= time.Now().Unix()
tags := strings.Join(m.Tags, ",") tags := strings.Join(m.Tags, ",")
var attachmentName, attachmentType, attachmentURL string var attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string
var attachmentSize, attachmentExpires int64 var attachmentSize, attachmentExpires int64
if m.Attachment != nil { if m.Attachment != nil {
attachmentName = m.Attachment.Name attachmentName = m.Attachment.Name
attachmentType = m.Attachment.Type attachmentType = m.Attachment.Type
attachmentSize = m.Attachment.Size attachmentSize = m.Attachment.Size
attachmentExpires = m.Attachment.Expires attachmentExpires = m.Attachment.Expires
attachmentPreviewURL = m.Attachment.PreviewURL
attachmentURL = m.Attachment.URL attachmentURL = m.Attachment.URL
} }
_, err := c.db.Exec( _, err := c.db.Exec(
@ -146,6 +148,7 @@ func (c *sqliteCache) AddMessage(m *message) error {
attachmentType, attachmentType,
attachmentSize, attachmentSize,
attachmentExpires, attachmentExpires,
attachmentPreviewURL,
attachmentURL, attachmentURL,
published, published,
) )
@ -231,8 +234,8 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
for rows.Next() { for rows.Next() {
var timestamp, attachmentSize, attachmentExpires int64 var timestamp, attachmentSize, attachmentExpires int64
var priority int var priority int
var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentURL string var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentPreviewURL, attachmentURL string
if err := rows.Scan(&id, &timestamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentURL); err != nil { if err := rows.Scan(&id, &timestamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentPreviewURL, &attachmentURL); err != nil {
return nil, err return nil, err
} }
var tags []string var tags []string
@ -242,11 +245,12 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
var att *attachment var att *attachment
if attachmentName != "" && attachmentURL != "" { if attachmentName != "" && attachmentURL != "" {
att = &attachment{ att = &attachment{
Name: attachmentName, Name: attachmentName,
Type: attachmentType, Type: attachmentType,
Size: attachmentSize, Size: attachmentSize,
Expires: attachmentExpires, Expires: attachmentExpires,
URL: attachmentURL, PreviewURL: attachmentPreviewURL,
URL: attachmentURL,
} }
} }
messages = append(messages, &message{ messages = append(messages, &message{

View File

@ -13,8 +13,9 @@ const (
DefaultAtSenderInterval = 10 * time.Second DefaultAtSenderInterval = 10 * time.Second
DefaultMinDelay = 10 * time.Second DefaultMinDelay = 10 * time.Second
DefaultMaxDelay = 3 * 24 * time.Hour DefaultMaxDelay = 3 * 24 * time.Hour
DefaultMessageLimit = 4096 DefaultMessageLimit = 4096 // Bytes
DefaultAttachmentSizeLimit = 5 * 1024 * 1024 DefaultAttachmentSizeLimit = 15 * 1024 * 1024
DefaultAttachmentSizePreviewMax = 20 * 1024 * 1024 // Bytes
DefaultAttachmentExpiryDuration = 3 * time.Hour DefaultAttachmentExpiryDuration = 3 * time.Hour
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
) )
@ -48,6 +49,7 @@ type Config struct {
CacheDuration time.Duration CacheDuration time.Duration
AttachmentCacheDir string AttachmentCacheDir string
AttachmentSizeLimit int64 AttachmentSizeLimit int64
AttachmentSizePreviewMax int64
AttachmentExpiryDuration time.Duration AttachmentExpiryDuration time.Duration
KeepaliveInterval time.Duration KeepaliveInterval time.Duration
ManagerInterval time.Duration ManagerInterval time.Duration
@ -88,6 +90,7 @@ func NewConfig() *Config {
CacheDuration: DefaultCacheDuration, CacheDuration: DefaultCacheDuration,
AttachmentCacheDir: "", AttachmentCacheDir: "",
AttachmentSizeLimit: DefaultAttachmentSizeLimit, AttachmentSizeLimit: DefaultAttachmentSizeLimit,
AttachmentSizePreviewMax: DefaultAttachmentSizePreviewMax,
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration, AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
KeepaliveInterval: DefaultKeepaliveInterval, KeepaliveInterval: DefaultKeepaliveInterval,
ManagerInterval: DefaultManagerInterval, ManagerInterval: DefaultManagerInterval,

View File

@ -30,11 +30,12 @@ type message struct {
} }
type attachment struct { type attachment struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Size int64 `json:"size"` Size int64 `json:"size"`
Expires int64 `json:"expires"` Expires int64 `json:"expires"`
URL string `json:"url"` PreviewURL string `json:"preview_url"`
URL string `json:"url"`
} }
// messageEncoder is a function that knows how to encode a message // messageEncoder is a function that knows how to encode a message

View File

@ -16,7 +16,6 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"mime"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -233,6 +232,7 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) {
data["attachment_type"] = m.Attachment.Type data["attachment_type"] = m.Attachment.Type
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size) data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires) data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
data["attachment_preview_url"] = m.Attachment.PreviewURL
data["attachment_url"] = m.Attachment.URL 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) { } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
return s.handleDocs(w, r) return s.handleDocs(w, r)
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" { } 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 != "" { } 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 { } else if r.Method == http.MethodOptions {
return s.handleOptions(w, r) return s.handleOptions(w, r)
} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) { } 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 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 == "" { if s.config.AttachmentCacheDir == "" {
return errHTTPInternalError return errHTTPInternalError
} }
@ -408,7 +408,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error {
return err 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 == "" { if s.config.AttachmentCacheDir == "" {
return errHTTPInternalError return errHTTPInternalError
} }
@ -422,12 +422,12 @@ func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return errHTTPNotFound return errHTTPNotFound
} }
if stat.Size() > 20*1024*1024 { if stat.Size() > s.config.AttachmentSizePreviewMax {
return errHTTPInternalError return errHTTPNotFoundTooLarge
} }
img, err := imaging.Open(file) img, err := imaging.Open(file)
if err != nil { if err != nil {
return errHTTPNotFoundTooLarge return err
} }
var width, height int var width, height int
if width >= height { 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)) width = int(float32(img.Bounds().Dx()) / float32(img.Bounds().Dy()) * float32(height))
} }
preview := imaging.Resize(img, width, height, imaging.Lanczos) 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 { 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 return errHTTPBadRequestInvalidMessage
} }
contentType := http.DetectContentType(body.PeakedBytes) contentType := http.DetectContentType(body.PeakedBytes)
ext := ".bin" ext := util.ExtensionByType(contentType)
exts, err := mime.ExtensionsByType(contentType) fileURL := fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext)
if err == nil && len(exts) > 0 { previewURL := ""
ext = exts[0] 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") filename := readParam(r, "x-filename", "filename", "file", "f")
if filename == "" { 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.Message = fmt.Sprintf("You received a file: %s", filename) // May be overwritten later
m.Attachment = &attachment{ m.Attachment = &attachment{
Name: filename, Name: filename,
Type: contentType, Type: contentType,
Size: size, Size: size,
Expires: time.Now().Add(s.config.AttachmentExpiryDuration).Unix(), Expires: time.Now().Add(s.config.AttachmentExpiryDuration).Unix(),
URL: fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext), PreviewURL: previewURL,
URL: fileURL,
} }
return nil return nil
} }

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
"mime"
"os" "os"
"strings" "strings"
"sync" "sync"
@ -163,3 +164,17 @@ func ExpandHome(path string) string {
func ShortTopicURL(s string) string { func ShortTopicURL(s string) string {
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://") 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"
}
}