diff options
Diffstat (limited to 'internal/processing/media')
-rw-r--r-- | internal/processing/media/create.go | 79 | ||||
-rw-r--r-- | internal/processing/media/delete.go | 51 | ||||
-rw-r--r-- | internal/processing/media/getfile.go | 120 | ||||
-rw-r--r-- | internal/processing/media/getmedia.go | 51 | ||||
-rw-r--r-- | internal/processing/media/media.go | 63 | ||||
-rw-r--r-- | internal/processing/media/update.go | 70 | ||||
-rw-r--r-- | internal/processing/media/util.go | 63 |
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 := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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 +} |