mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 07:52:25 -06:00 
			
		
		
		
	media handling
This commit is contained in:
		
					parent
					
						
							
								f210d39891
							
						
					
				
			
			
				commit
				
					
						a30a1a267b
					
				
			
		
					 8 changed files with 431 additions and 1 deletions
				
			
		
							
								
								
									
										73
									
								
								internal/apimodule/media/media.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								internal/apimodule/media/media.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,73 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					   GoToSocial
 | 
				
			||||||
 | 
					   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					   it under the terms of the GNU Affero General Public License as published by
 | 
				
			||||||
 | 
					   the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					   (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					   GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/apimodule"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/db"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/mastotypes"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/media"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/router"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mediaPath = "/api/v1/media"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type mediaModule struct {
 | 
				
			||||||
 | 
						mediaHandler   media.MediaHandler
 | 
				
			||||||
 | 
						config         *config.Config
 | 
				
			||||||
 | 
						db             db.DB
 | 
				
			||||||
 | 
						mastoConverter mastotypes.Converter
 | 
				
			||||||
 | 
						log            *logrus.Logger
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// New returns a new auth module
 | 
				
			||||||
 | 
					func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
 | 
				
			||||||
 | 
						return &mediaModule{
 | 
				
			||||||
 | 
							mediaHandler:   mediaHandler,
 | 
				
			||||||
 | 
							config: config,
 | 
				
			||||||
 | 
							db:             db,
 | 
				
			||||||
 | 
							mastoConverter: mastoConverter,
 | 
				
			||||||
 | 
							log:            log,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Route satisfies the RESTAPIModule interface
 | 
				
			||||||
 | 
					func (m *mediaModule) Route(s router.Router) error {
 | 
				
			||||||
 | 
						s.AttachHandler(http.MethodPost, mediaPath, m.mediaCreatePOSTHandler)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *mediaModule) CreateTables(db db.DB) error {
 | 
				
			||||||
 | 
						models := []interface{}{
 | 
				
			||||||
 | 
							>smodel.MediaAttachment{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, m := range models {
 | 
				
			||||||
 | 
							if err := db.CreateTable(m); err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("error creating table: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										175
									
								
								internal/apimodule/media/mediacreate.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								internal/apimodule/media/mediacreate.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,175 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					   GoToSocial
 | 
				
			||||||
 | 
					   Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This program is free software: you can redistribute it and/or modify
 | 
				
			||||||
 | 
					   it under the terms of the GNU Affero General Public License as published by
 | 
				
			||||||
 | 
					   the Free Software Foundation, either version 3 of the License, or
 | 
				
			||||||
 | 
					   (at your option) any later version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   This program is distributed in the hope that it will be useful,
 | 
				
			||||||
 | 
					   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
				
			||||||
 | 
					   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
				
			||||||
 | 
					   GNU Affero General Public License for more details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/config"
 | 
				
			||||||
 | 
						mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
 | 
				
			||||||
 | 
						"github.com/superseriousbusiness/gotosocial/internal/oauth"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
 | 
				
			||||||
 | 
						l := m.log.WithField("func", "statusCreatePOSTHandler")
 | 
				
			||||||
 | 
						authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything*
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							l.Debugf("couldn't auth: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// First check this user/account is permitted to create media
 | 
				
			||||||
 | 
						// There's no point continuing otherwise.
 | 
				
			||||||
 | 
						if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
 | 
				
			||||||
 | 
							l.Debugf("couldn't auth: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// extract the media create form from the request context
 | 
				
			||||||
 | 
						l.Tracef("parsing request form: %s", c.Request.Form)
 | 
				
			||||||
 | 
						form := &mastotypes.AttachmentRequest{}
 | 
				
			||||||
 | 
						if err := c.ShouldBind(form); err != nil || form == nil {
 | 
				
			||||||
 | 
							l.Debugf("could not parse form from request: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Give the fields on the request form a first pass to make sure the request is superficially valid.
 | 
				
			||||||
 | 
						l.Tracef("validating form %+v", form)
 | 
				
			||||||
 | 
						if err := validateCreateMedia(form, m.config.MediaConfig); err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error validating form: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						f, err := form.File.Open()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error opening attachment: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided attachment: %s", err)})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// extract the bytes
 | 
				
			||||||
 | 
						buf := new(bytes.Buffer)
 | 
				
			||||||
 | 
						size, err := io.Copy(buf, f)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error reading attachment: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not read provided attachment: %s", err)})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if size == 0 {
 | 
				
			||||||
 | 
							l.Debug("could not read provided attachment: size 0 bytes")
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": "could not read provided attachment: size 0 bytes"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						attachment, err := m.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error reading attachment: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not process attachment: %s", err)})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						attachment.Description = form.Description
 | 
				
			||||||
 | 
						var focusx, focusy float32
 | 
				
			||||||
 | 
						if form.Focus != "" {
 | 
				
			||||||
 | 
							spl := strings.Split(form.Focus, ",")
 | 
				
			||||||
 | 
							if len(spl) != 2 {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s", form.Focus)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							xStr := spl[0]
 | 
				
			||||||
 | 
							yStr := spl[1]
 | 
				
			||||||
 | 
							if xStr == "" || xStr == "" {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s", form.Focus)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fx, err := strconv.ParseFloat(xStr[:4], 32)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fx > 1 || fx < -1 {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s", form.Focus)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							focusx = float32(fx)
 | 
				
			||||||
 | 
							fy, err := strconv.ParseFloat(yStr[:4], 32)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if fy > 1 || fy < -1 {
 | 
				
			||||||
 | 
								l.Debugf("improperly formatted focus %s", form.Focus)
 | 
				
			||||||
 | 
								c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							focusy = float32(fy)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						attachment.FileMeta.Focus.X = focusx
 | 
				
			||||||
 | 
						attachment.FileMeta.Focus.Y = focusy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mastoAttachment, err := m.mastoConverter.AttachmentToMasto(attachment)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error parsing media attachment to frontend type: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error parsing media attachment to frontend type: %s", err)})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := m.db.Put(attachment); err != nil {
 | 
				
			||||||
 | 
							l.Debugf("error storing media attachment in db: %s", err)
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error storing media attachment in db: %s", err)})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusAccepted, mastoAttachment)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func validateCreateMedia(form *mastotypes.AttachmentRequest, config *config.MediaConfig) error {
 | 
				
			||||||
 | 
						// check there actually is a file attached and it's not size 0
 | 
				
			||||||
 | 
						if form.File == nil || form.File.Size == 0 {
 | 
				
			||||||
 | 
							return errors.New("no attachment given")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// a very superficial check to see if no limits are exceeded
 | 
				
			||||||
 | 
						// we still don't actually know which media types we're dealing with but the other handlers will go into more detail there
 | 
				
			||||||
 | 
						maxSize := config.MaxVideoSize
 | 
				
			||||||
 | 
						if config.MaxImageSize > maxSize {
 | 
				
			||||||
 | 
							maxSize = config.MaxImageSize
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if form.File.Size > int64(maxSize) {
 | 
				
			||||||
 | 
							return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -123,6 +123,7 @@ const (
 | 
				
			||||||
type FileMeta struct {
 | 
					type FileMeta struct {
 | 
				
			||||||
	Original Original
 | 
						Original Original
 | 
				
			||||||
	Small    Small
 | 
						Small    Small
 | 
				
			||||||
 | 
						Focus    Focus
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Small can be used for a thumbnail of any media type
 | 
					// Small can be used for a thumbnail of any media type
 | 
				
			||||||
| 
						 | 
					@ -140,3 +141,8 @@ type Original struct {
 | 
				
			||||||
	Size   int
 | 
						Size   int
 | 
				
			||||||
	Aspect float64
 | 
						Aspect float64
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Focus struct {
 | 
				
			||||||
 | 
						X float32
 | 
				
			||||||
 | 
						Y float32
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -255,6 +255,10 @@ func (c *converter) AttachmentToMasto(a *gtsmodel.MediaAttachment) (mastotypes.A
 | 
				
			||||||
				Size: fmt.Sprintf("%dx%d", a.FileMeta.Small.Width, a.FileMeta.Small.Height),
 | 
									Size: fmt.Sprintf("%dx%d", a.FileMeta.Small.Width, a.FileMeta.Small.Height),
 | 
				
			||||||
				Aspect: float32(a.FileMeta.Small.Aspect),
 | 
									Aspect: float32(a.FileMeta.Small.Aspect),
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
 | 
								Focus: mastotypes.MediaFocus{
 | 
				
			||||||
 | 
									X: a.FileMeta.Focus.X,
 | 
				
			||||||
 | 
									Y: a.FileMeta.Focus.Y,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		Description: a.Description,
 | 
							Description: a.Description,
 | 
				
			||||||
		Blurhash: a.Blurhash,
 | 
							Blurhash: a.Blurhash,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,11 @@ type MediaHandler interface {
 | 
				
			||||||
	// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
 | 
						// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new image,
 | 
				
			||||||
	// and then returns information to the caller about the new header.
 | 
						// and then returns information to the caller about the new header.
 | 
				
			||||||
	SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error)
 | 
						SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ProcessAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
 | 
				
			||||||
 | 
						// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
 | 
				
			||||||
 | 
						// and then returns information to the caller about the attachment.
 | 
				
			||||||
 | 
						ProcessAttachment(img []byte, accountID string) (*gtsmodel.MediaAttachment, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type mediaHandler struct {
 | 
					type mediaHandler struct {
 | 
				
			||||||
| 
						 | 
					@ -103,10 +108,147 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri
 | 
				
			||||||
	return ma, nil
 | 
						return ma, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mh *mediaHandler) ProcessAttachment(data []byte, accountID string) (*gtsmodel.MediaAttachment, error) {
 | 
				
			||||||
 | 
						contentType, err := parseContentType(data)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						mainType := strings.Split(contentType, "/")[0] 
 | 
				
			||||||
 | 
						switch mainType {
 | 
				
			||||||
 | 
						case "video":
 | 
				
			||||||
 | 
							if !supportedVideoType(contentType) {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("video type %s not supported", contentType)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(data) == 0 {
 | 
				
			||||||
 | 
								return nil, errors.New("video was of size 0")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(data) > mh.config.MediaConfig.MaxVideoSize {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(data), mh.config.MediaConfig.MaxVideoSize)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return mh.processVideo(data, accountID, contentType)
 | 
				
			||||||
 | 
						case "image":
 | 
				
			||||||
 | 
							if !supportedImageType(contentType) {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("image type %s not supported", contentType)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(data) == 0 {
 | 
				
			||||||
 | 
								return nil, errors.New("image was of size 0")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(data) > mh.config.MediaConfig.MaxImageSize {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(data), mh.config.MediaConfig.MaxImageSize)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return mh.processImage(data, accountID, contentType)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, fmt.Errorf("content type %s not (yet) supported", contentType)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
	HELPER FUNCTIONS
 | 
						HELPER FUNCTIONS
 | 
				
			||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mh *mediaHandler) processVideo(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) {
 | 
				
			||||||
 | 
						return nil, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (mh *mediaHandler) processImage(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) {
 | 
				
			||||||
 | 
						var clean []byte
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch contentType {
 | 
				
			||||||
 | 
						case "image/jpeg":
 | 
				
			||||||
 | 
							if clean, err = purgeExif(data); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("error cleaning exif data: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "image/png":
 | 
				
			||||||
 | 
							if clean, err = purgeExif(data); err != nil {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("error cleaning exif data: %s", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "image/gif":
 | 
				
			||||||
 | 
							clean = data
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil, errors.New("media type unrecognized")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						original, err := deriveImage(clean, contentType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error parsing image: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						small, err := deriveThumbnail(clean, contentType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("error deriving thumbnail: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it
 | 
				
			||||||
 | 
						extension := strings.Split(contentType, "/")[1]
 | 
				
			||||||
 | 
						newMediaID := uuid.NewString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
 | 
				
			||||||
 | 
						originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
 | 
				
			||||||
 | 
						smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.%s", URLbase, accountID, newMediaID, extension)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we store the original...
 | 
				
			||||||
 | 
						originalPath := fmt.Sprintf("%s/%s/attachment/original/%s.%s", mh.config.StorageConfig.BasePath, accountID, newMediaID, extension)
 | 
				
			||||||
 | 
						if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("storage error: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// and a thumbnail...
 | 
				
			||||||
 | 
						smallPath := fmt.Sprintf("%s/%s/attachment/small/%s.%s", mh.config.StorageConfig.BasePath, accountID, newMediaID, extension)
 | 
				
			||||||
 | 
						if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("storage error: %s", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ma := >smodel.MediaAttachment{
 | 
				
			||||||
 | 
							ID:        newMediaID,
 | 
				
			||||||
 | 
							StatusID:  "",
 | 
				
			||||||
 | 
							URL:       originalURL,
 | 
				
			||||||
 | 
							RemoteURL: "",
 | 
				
			||||||
 | 
							CreatedAt: time.Now(),
 | 
				
			||||||
 | 
							UpdatedAt: time.Now(),
 | 
				
			||||||
 | 
							Type:      gtsmodel.FileTypeImage,
 | 
				
			||||||
 | 
							FileMeta: gtsmodel.FileMeta{
 | 
				
			||||||
 | 
								Original: gtsmodel.Original{
 | 
				
			||||||
 | 
									Width:  original.width,
 | 
				
			||||||
 | 
									Height: original.height,
 | 
				
			||||||
 | 
									Size:   original.size,
 | 
				
			||||||
 | 
									Aspect: original.aspect,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Small: gtsmodel.Small{
 | 
				
			||||||
 | 
									Width:  small.width,
 | 
				
			||||||
 | 
									Height: small.height,
 | 
				
			||||||
 | 
									Size:   small.size,
 | 
				
			||||||
 | 
									Aspect: small.aspect,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							AccountID:         accountID,
 | 
				
			||||||
 | 
							Description:       "",
 | 
				
			||||||
 | 
							ScheduledStatusID: "",
 | 
				
			||||||
 | 
							Blurhash:          original.blurhash,
 | 
				
			||||||
 | 
							Processing:        2,
 | 
				
			||||||
 | 
							File: gtsmodel.File{
 | 
				
			||||||
 | 
								Path:        originalPath,
 | 
				
			||||||
 | 
								ContentType: contentType,
 | 
				
			||||||
 | 
								FileSize:    len(original.image),
 | 
				
			||||||
 | 
								UpdatedAt:   time.Now(),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Thumbnail: gtsmodel.Thumbnail{
 | 
				
			||||||
 | 
								Path:        smallPath,
 | 
				
			||||||
 | 
								ContentType: contentType,
 | 
				
			||||||
 | 
								FileSize:    len(small.image),
 | 
				
			||||||
 | 
								UpdatedAt:   time.Now(),
 | 
				
			||||||
 | 
								URL:         smallURL,
 | 
				
			||||||
 | 
								RemoteURL:   "",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Avatar: false,
 | 
				
			||||||
 | 
							Header: false,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ma, nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*gtsmodel.MediaAttachment, error) {
 | 
					func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*gtsmodel.MediaAttachment, error) {
 | 
				
			||||||
	var isHeader bool
 | 
						var isHeader bool
 | 
				
			||||||
	var isAvatar bool
 | 
						var isAvatar bool
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,22 @@ func supportedImageType(mimeType string) bool {
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// supportedVideoType checks mime type of a video against a slice of accepted types,
 | 
				
			||||||
 | 
					// and returns True if the mime type is accepted.
 | 
				
			||||||
 | 
					func supportedVideoType(mimeType string) bool {
 | 
				
			||||||
 | 
						acceptedVideoTypes := []string{
 | 
				
			||||||
 | 
							"video/mp4",
 | 
				
			||||||
 | 
							"video/mpeg",
 | 
				
			||||||
 | 
							"video/webm",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, accepted := range acceptedVideoTypes {
 | 
				
			||||||
 | 
							if mimeType == accepted {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// purgeExif is a little wrapper for the action of removing exif data from an image.
 | 
					// purgeExif is a little wrapper for the action of removing exif data from an image.
 | 
				
			||||||
// Only pass pngs or jpegs to this function.
 | 
					// Only pass pngs or jpegs to this function.
 | 
				
			||||||
func purgeExif(b []byte) ([]byte, error) {
 | 
					func purgeExif(b []byte) ([]byte, error) {
 | 
				
			||||||
| 
						 | 
					@ -134,7 +150,7 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// deriveThumbnailFromImage returns a byte slice and metadata for a 256-pixel-width thumbnail
 | 
					// deriveThumbnail returns a byte slice and metadata for a 256-pixel-width thumbnail
 | 
				
			||||||
// of a given jpeg, png, or gif, or an error if something goes wrong.
 | 
					// of a given jpeg, png, or gif, or an error if something goes wrong.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Note that the aspect ratio of the image will be retained,
 | 
					// Note that the aspect ratio of the image will be retained,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,6 +61,12 @@ func StandardDBSetup(db db.DB) error {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, v := range TestAttachments() {
 | 
				
			||||||
 | 
							if err := db.Put(v); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, v := range TestStatuses() {
 | 
						for _, v := range TestStatuses() {
 | 
				
			||||||
		if err := db.Put(v); err != nil {
 | 
							if err := db.Put(v); err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -440,6 +440,14 @@ func TestAccounts() map[string]*gtsmodel.Account {
 | 
				
			||||||
	return accounts
 | 
						return accounts
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAttachments() map[string]*gtsmodel.MediaAttachment {
 | 
				
			||||||
 | 
						return map[string]*gtsmodel.MediaAttachment{
 | 
				
			||||||
 | 
							"admin_account_status_1": {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestStatuses() map[string]*gtsmodel.Status {
 | 
					func TestStatuses() map[string]*gtsmodel.Status {
 | 
				
			||||||
	return map[string]*gtsmodel.Status{
 | 
						return map[string]*gtsmodel.Status{
 | 
				
			||||||
		"admin_account_status_1": {
 | 
							"admin_account_status_1": {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue