summaryrefslogtreecommitdiff
path: root/internal/apimodule/media
diff options
context:
space:
mode:
Diffstat (limited to 'internal/apimodule/media')
-rw-r--r--internal/apimodule/media/media.go76
-rw-r--r--internal/apimodule/media/mediacreate.go193
-rw-r--r--internal/apimodule/media/test/mediacreate_test.go194
3 files changed, 0 insertions, 463 deletions
diff --git a/internal/apimodule/media/media.go b/internal/apimodule/media/media.go
deleted file mode 100644
index 8fb9f16ec..000000000
--- a/internal/apimodule/media/media.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- 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"
-)
-
-// BasePath is the base API path for making media requests
-const BasePath = "/api/v1/media"
-
-// Module implements the ClientAPIModule interface for media
-type Module struct {
- mediaHandler media.Handler
- 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.Handler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
- return &Module{
- mediaHandler: mediaHandler,
- config: config,
- db: db,
- mastoConverter: mastoConverter,
- log: log,
- }
-}
-
-// Route satisfies the RESTAPIModule interface
-func (m *Module) Route(s router.Router) error {
- s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler)
- return nil
-}
-
-// CreateTables populates necessary tables in the given DB
-func (m *Module) CreateTables(db db.DB) error {
- models := []interface{}{
- &gtsmodel.MediaAttachment{},
- }
-
- for _, m := range models {
- if err := db.CreateTable(m); err != nil {
- return fmt.Errorf("error creating table: %s", err)
- }
- }
- return nil
-}
diff --git a/internal/apimodule/media/mediacreate.go b/internal/apimodule/media/mediacreate.go
deleted file mode 100644
index ee713a471..000000000
--- a/internal/apimodule/media/mediacreate.go
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- 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"
-)
-
-// MediaCreatePOSTHandler handles requests to create/upload media attachments
-func (m *Module) 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
- }
-
- // open the attachment and extract the bytes from it
- 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
- }
- 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
- }
-
- // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
- attachment, err := m.mediaHandler.ProcessLocalAttachment(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
- }
-
- // now we need to add extra fields that the attachment processor doesn't know (from the form)
- // TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
-
- // first description
- attachment.Description = form.Description
-
- // now parse the focus parameter
- // TODO: tidy this up into a separate function and just return an error so all the c.JSON and return calls are obviated
- 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 == "" || yStr == "" {
- 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, 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, 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
-
- // prepare the frontend representation now -- if there are any errors here at least we can bail without
- // having already put something in the database and then having to clean it up again (eugh)
- 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
- }
-
- // now we can confidently put the attachment in the database
- 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
- }
-
- // and return its frontend representation
- 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 size 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)
- }
-
- if len(form.Description) < config.MinDescriptionChars || len(form.Description) > config.MaxDescriptionChars {
- return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(form.Description))
- }
-
- // TODO: validate focus here
-
- return nil
-}
diff --git a/internal/apimodule/media/test/mediacreate_test.go b/internal/apimodule/media/test/mediacreate_test.go
deleted file mode 100644
index 30bbb117a..000000000
--- a/internal/apimodule/media/test/mediacreate_test.go
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- 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 test
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/sirupsen/logrus"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
- mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
- "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"
- mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type MediaCreateTestSuite struct {
- // standard suite interfaces
- suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- storage storage.Storage
- mastoConverter mastotypes.Converter
- mediaHandler media.Handler
- oauthServer oauth.Server
-
- // standard suite models
- testTokens map[string]*oauth.Token
- testClients map[string]*oauth.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
-
- // item being tested
- mediaModule *mediamodule.Module
-}
-
-/*
- TEST INFRASTRUCTURE
-*/
-
-func (suite *MediaCreateTestSuite) SetupSuite() {
- // setup standard items
- suite.config = testrig.NewTestConfig()
- suite.db = testrig.NewTestDB()
- suite.log = testrig.NewTestLog()
- suite.storage = testrig.NewTestStorage()
- suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
- suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
-
- // setup module being tested
- suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediamodule.Module)
-}
-
-func (suite *MediaCreateTestSuite) TearDownSuite() {
- if err := suite.db.Stop(context.Background()); err != nil {
- logrus.Panicf("error closing db connection: %s", err)
- }
-}
-
-func (suite *MediaCreateTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
- testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
- suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
-}
-
-func (suite *MediaCreateTestSuite) TearDownTest() {
- testrig.StandardDBTeardown(suite.db)
- testrig.StandardStorageTeardown(suite.storage)
-}
-
-/*
- ACTUAL TESTS
-*/
-
-func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() {
-
- // set up the context for the request
- t := suite.testTokens["local_account_1"]
- oauthToken := oauth.TokenToOauthToken(t)
- recorder := httptest.NewRecorder()
- ctx, _ := gin.CreateTestContext(recorder)
- ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
- ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
- ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
- ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
-
- // see what's in storage *before* the request
- storageKeysBeforeRequest, err := suite.storage.ListKeys()
- if err != nil {
- panic(err)
- }
-
- // create the request
- buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{
- "description": "this is a test image -- a cool background from somewhere",
- "focus": "-0.5,0.5",
- })
- if err != nil {
- panic(err)
- }
- ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting
- ctx.Request.Header.Set("Content-Type", w.FormDataContentType())
-
- // do the actual request
- suite.mediaModule.MediaCreatePOSTHandler(ctx)
-
- // check what's in storage *after* the request
- storageKeysAfterRequest, err := suite.storage.ListKeys()
- if err != nil {
- panic(err)
- }
-
- // check response
- suite.EqualValues(http.StatusAccepted, recorder.Code)
-
- result := recorder.Result()
- defer result.Body.Close()
- b, err := ioutil.ReadAll(result.Body)
- assert.NoError(suite.T(), err)
- fmt.Println(string(b))
-
- attachmentReply := &mastomodel.Attachment{}
- err = json.Unmarshal(b, attachmentReply)
- assert.NoError(suite.T(), err)
-
- assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description)
- assert.Equal(suite.T(), "image", attachmentReply.Type)
- assert.EqualValues(suite.T(), mastomodel.MediaMeta{
- Original: mastomodel.MediaDimensions{
- Width: 1920,
- Height: 1080,
- Size: "1920x1080",
- Aspect: 1.7777778,
- },
- Small: mastomodel.MediaDimensions{
- Width: 256,
- Height: 144,
- Size: "256x144",
- Aspect: 1.7777778,
- },
- Focus: mastomodel.MediaFocus{
- X: -0.5,
- Y: 0.5,
- },
- }, attachmentReply.Meta)
- assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", attachmentReply.Blurhash)
- assert.NotEmpty(suite.T(), attachmentReply.ID)
- assert.NotEmpty(suite.T(), attachmentReply.URL)
- assert.NotEmpty(suite.T(), attachmentReply.PreviewURL)
- assert.Equal(suite.T(), len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail
-}
-
-func TestMediaCreateTestSuite(t *testing.T) {
- suite.Run(t, new(MediaCreateTestSuite))
-}