Philipp Heckel 4 gadi atpakaļ
vecāks
revīzija
2930c4ff62
5 mainītis faili ar 63 papildinājumiem un 38 dzēšanām
  1. 17 13
      server/cache_sqlite.go
  2. 5 2
      server/config.go
  3. 6 5
      server/message.go
  4. 20 18
      server/server.go
  5. 15 0
      util/util.go

+ 17 - 13
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, &timestamp, &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, &timestamp, &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{

+ 5 - 2
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,

+ 6 - 5
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

+ 20 - 18
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
 }

+ 15 - 0
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"
+	}
+}