summaryrefslogtreecommitdiff
path: root/internal/processing/media
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/media')
-rw-r--r--internal/processing/media/create.go79
-rw-r--r--internal/processing/media/delete.go51
-rw-r--r--internal/processing/media/getfile.go120
-rw-r--r--internal/processing/media/getmedia.go51
-rw-r--r--internal/processing/media/media.go63
-rw-r--r--internal/processing/media/update.go70
-rw-r--r--internal/processing/media/util.go63
7 files changed, 497 insertions, 0 deletions
diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go
new file mode 100644
index 000000000..f9e383504
--- /dev/null
+++ b/internal/processing/media/create.go
@@ -0,0 +1,79 @@
+/*
+ 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"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) {
+ // open the attachment and extract the bytes from it
+ f, err := form.File.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening attachment: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided attachment: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
+ attachment, err := p.mediaHandler.ProcessAttachment(buf.Bytes(), account.ID, "")
+ if err != nil {
+ return nil, fmt.Errorf("error reading attachment: %s", err)
+ }
+
+ // 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
+ focusx, focusy, err := parseFocus(form.Focus)
+ if err != nil {
+ return nil, err
+ }
+ 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 := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing media attachment to frontend type: %s", err)
+ }
+
+ // now we can confidently put the attachment in the database
+ if err := p.db.Put(attachment); err != nil {
+ return nil, fmt.Errorf("error storing media attachment in db: %s", err)
+ }
+
+ return &mastoAttachment, nil
+}
diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go
new file mode 100644
index 000000000..694d78ac3
--- /dev/null
+++ b/internal/processing/media/delete.go
@@ -0,0 +1,51 @@
+package media
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Delete(mediaAttachmentID string) gtserror.WithCode {
+ a := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaAttachmentID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // attachment already gone
+ return nil
+ }
+ // actual error
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ errs := []string{}
+
+ // delete the thumbnail from storage
+ if a.Thumbnail.Path != "" {
+ if err := p.storage.RemoveFileAt(a.Thumbnail.Path); err != nil {
+ errs = append(errs, fmt.Sprintf("remove thumbnail at path %s: %s", a.Thumbnail.Path, err))
+ }
+ }
+
+ // delete the file from storage
+ if a.File.Path != "" {
+ if err := p.storage.RemoveFileAt(a.File.Path); err != nil {
+ errs = append(errs, fmt.Sprintf("remove file at path %s: %s", a.File.Path, err))
+ }
+ }
+
+ // delete the attachment
+ if err := p.db.DeleteByID(mediaAttachmentID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ errs = append(errs, fmt.Sprintf("remove attachment: %s", err))
+ }
+ }
+
+ if len(errs) != 0 {
+ return gtserror.NewErrorInternalError(fmt.Errorf("Delete: one or more errors removing attachment with id %s: %s", mediaAttachmentID, strings.Join(errs, "; ")))
+ }
+
+ return nil
+}
diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go
new file mode 100644
index 000000000..1664306b8
--- /dev/null
+++ b/internal/processing/media/getfile.go
@@ -0,0 +1,120 @@
+/*
+ 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"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+func (p *processor) GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) {
+ // parse the form fields
+ mediaSize, err := media.ParseMediaSize(form.MediaSize)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
+ }
+
+ mediaType, err := media.ParseMediaType(form.MediaType)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
+ }
+
+ spl := strings.Split(form.FileName, ".")
+ if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
+ }
+ wantedMediaID := spl[0]
+
+ // get the account that owns the media and make sure it's not suspended
+ acct := &gtsmodel.Account{}
+ if err := p.db.GetByID(form.AccountID, acct); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
+ }
+ if !acct.SuspendedAt.IsZero() {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
+ }
+
+ // make sure the requesting account and the media account don't block each other
+ if account != nil {
+ blocked, err := p.db.Blocked(account.ID, form.AccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, account.ID, err))
+ }
+ if blocked {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, account.ID))
+ }
+ }
+
+ // the way we store emojis is a little different from the way we store other attachments,
+ // so we need to take different steps depending on the media type being requested
+ content := &apimodel.Content{}
+ var storagePath string
+ switch mediaType {
+ case media.Emoji:
+ e := &gtsmodel.Emoji{}
+ if err := p.db.GetByID(wantedMediaID, e); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if e.Disabled {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = e.ImageContentType
+ storagePath = e.ImagePath
+ case media.Static:
+ content.ContentType = e.ImageStaticContentType
+ storagePath = e.ImageStaticPath
+ default:
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
+ }
+ case media.Attachment, media.Header, media.Avatar:
+ a := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(wantedMediaID, a); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
+ }
+ if a.AccountID != form.AccountID {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
+ }
+ switch mediaSize {
+ case media.Original:
+ content.ContentType = a.File.ContentType
+ storagePath = a.File.Path
+ case media.Small:
+ content.ContentType = a.Thumbnail.ContentType
+ storagePath = a.Thumbnail.Path
+ default:
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
+ }
+ }
+
+ bytes, err := p.storage.RetrieveFileFrom(storagePath)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
+ }
+
+ content.ContentLength = int64(len(bytes))
+ content.Content = bytes
+ return content, nil
+}
diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go
new file mode 100644
index 000000000..c36370225
--- /dev/null
+++ b/internal/processing/media/getmedia.go
@@ -0,0 +1,51 @@
+/*
+ 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 (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
+ attachment := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // attachment doesn't exist
+ return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
+ }
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
+ }
+
+ if attachment.AccountID != account.ID {
+ return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
+ }
+
+ a, err := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
+ }
+
+ return &a, nil
+}
diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go
new file mode 100644
index 000000000..79c9a7e18
--- /dev/null
+++ b/internal/processing/media/media.go
@@ -0,0 +1,63 @@
+/*
+ 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 (
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/blob"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+// Processor wraps a bunch of functions for processing media actions.
+type Processor interface {
+ // Create creates a new media attachment belonging to the given account, using the request form.
+ Create(account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
+ // Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
+ Delete(mediaAttachmentID string) gtserror.WithCode
+ GetFile(account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
+ GetMedia(account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode)
+ Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ mediaHandler media.Handler
+ storage blob.Storage
+ db db.DB
+ log *logrus.Logger
+}
+
+// New returns a new media processor.
+func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, storage blob.Storage, config *config.Config, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ mediaHandler: mediaHandler,
+ storage: storage,
+ db: db,
+ log: log,
+ }
+}
diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go
new file mode 100644
index 000000000..aa3583054
--- /dev/null
+++ b/internal/processing/media/update.go
@@ -0,0 +1,70 @@
+/*
+ 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 (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Update(account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
+ attachment := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // attachment doesn't exist
+ return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
+ }
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
+ }
+
+ if attachment.AccountID != account.ID {
+ return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
+ }
+
+ if form.Description != nil {
+ attachment.Description = *form.Description
+ if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
+ }
+ }
+
+ if form.Focus != nil {
+ focusx, focusy, err := parseFocus(*form.Focus)
+ if err != nil {
+ return nil, gtserror.NewErrorBadRequest(err)
+ }
+ attachment.FileMeta.Focus.X = focusx
+ attachment.FileMeta.Focus.Y = focusy
+ if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
+ }
+ }
+
+ a, err := p.tc.AttachmentToMasto(attachment)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
+ }
+
+ return &a, nil
+}
diff --git a/internal/processing/media/util.go b/internal/processing/media/util.go
new file mode 100644
index 000000000..47ea4fccd
--- /dev/null
+++ b/internal/processing/media/util.go
@@ -0,0 +1,63 @@
+/*
+ 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"
+ "strconv"
+ "strings"
+)
+
+func parseFocus(focus string) (focusx, focusy float32, err error) {
+ if focus == "" {
+ return
+ }
+ spl := strings.Split(focus, ",")
+ if len(spl) != 2 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ xStr := spl[0]
+ yStr := spl[1]
+ if xStr == "" || yStr == "" {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ fx, err := strconv.ParseFloat(xStr, 32)
+ if err != nil {
+ err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
+ return
+ }
+ if fx > 1 || fx < -1 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ focusx = float32(fx)
+ fy, err := strconv.ParseFloat(yStr, 32)
+ if err != nil {
+ err = fmt.Errorf("improperly formatted focus %s: %s", focus, err)
+ return
+ }
+ if fy > 1 || fy < -1 {
+ err = fmt.Errorf("improperly formatted focus %s", focus)
+ return
+ }
+ focusy = float32(fy)
+ return
+}