mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-24 10:15:47 +03:00
compiling now
This commit is contained in:
parent
c2ff8f392b
commit
f61c3ddcf7
18 changed files with 345 additions and 226 deletions
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
@ -133,10 +132,5 @@ func validateCreateEmoji(form *model.EmojiCreateRequest) error {
|
|||
return errors.New("no emoji given")
|
||||
}
|
||||
|
||||
// a very superficial check to see if the media size limit is exceeded
|
||||
if form.Image.Size > media.EmojiMaxBytes {
|
||||
return fmt.Errorf("file size limit exceeded: limit is %d bytes but emoji was %d bytes", media.EmojiMaxBytes, form.Image.Size)
|
||||
}
|
||||
|
||||
return validate.EmojiShortcode(form.Shortcode)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func processSQLiteError(err error) db.Error {
|
|||
|
||||
// Handle supplied error code:
|
||||
switch sqliteErr.Code() {
|
||||
case sqlite3.SQLITE_CONSTRAINT_UNIQUE:
|
||||
case sqlite3.SQLITE_CONSTRAINT_UNIQUE, sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY:
|
||||
return db.ErrAlreadyExists
|
||||
default:
|
||||
return err
|
||||
|
|
|
@ -246,25 +246,49 @@ func (d *deref) fetchHeaderAndAviForAccount(ctx context.Context, targetAccount *
|
|||
}
|
||||
|
||||
if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
|
||||
a, err := d.mediaManager.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.AvatarRemoteURL,
|
||||
Avatar: true,
|
||||
}, targetAccount.ID)
|
||||
avatarIRI, err := url.Parse(targetAccount.AvatarRemoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing avatar for user: %s", err)
|
||||
return err
|
||||
}
|
||||
targetAccount.AvatarMediaAttachmentID = a.ID
|
||||
|
||||
data, err := t.DereferenceMedia(ctx, avatarIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
media, err := d.mediaManager.ProcessMedia(ctx, data, targetAccount.ID, targetAccount.AvatarRemoteURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := media.SetAsAvatar(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetAccount.AvatarMediaAttachmentID = media.AttachmentID()
|
||||
}
|
||||
|
||||
if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
|
||||
a, err := d.mediaManager.ProcessRemoteHeaderOrAvatar(ctx, t, >smodel.MediaAttachment{
|
||||
RemoteURL: targetAccount.HeaderRemoteURL,
|
||||
Header: true,
|
||||
}, targetAccount.ID)
|
||||
headerIRI, err := url.Parse(targetAccount.HeaderRemoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error processing header for user: %s", err)
|
||||
return err
|
||||
}
|
||||
targetAccount.HeaderMediaAttachmentID = a.ID
|
||||
|
||||
data, err := t.DereferenceMedia(ctx, headerIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
media, err := d.mediaManager.ProcessMedia(ctx, data, targetAccount.ID, targetAccount.HeaderRemoteURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := media.SetAsHeader(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetAccount.HeaderMediaAttachmentID = media.AttachmentID()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 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 dereferencing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
func (d *deref) GetRemoteAttachment(ctx context.Context, requestingUsername string, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) {
|
||||
if minAttachment.RemoteURL == "" {
|
||||
return nil, fmt.Errorf("GetRemoteAttachment: minAttachment remote URL was empty")
|
||||
}
|
||||
remoteAttachmentURL := minAttachment.RemoteURL
|
||||
|
||||
l := logrus.WithFields(logrus.Fields{
|
||||
"username": requestingUsername,
|
||||
"remoteAttachmentURL": remoteAttachmentURL,
|
||||
})
|
||||
|
||||
// return early if we already have the attachment somewhere
|
||||
maybeAttachment := >smodel.MediaAttachment{}
|
||||
where := []db.Where{
|
||||
{
|
||||
Key: "remote_url",
|
||||
Value: remoteAttachmentURL,
|
||||
},
|
||||
}
|
||||
|
||||
if err := d.db.GetWhere(ctx, where, maybeAttachment); err == nil {
|
||||
// we already the attachment in the database
|
||||
l.Debugf("GetRemoteAttachment: attachment already exists with id %s", maybeAttachment.ID)
|
||||
return maybeAttachment, nil
|
||||
}
|
||||
|
||||
a, err := d.RefreshAttachment(ctx, requestingUsername, minAttachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetRemoteAttachment: error refreshing attachment: %s", err)
|
||||
}
|
||||
|
||||
if err := d.db.Put(ctx, a); err != nil {
|
||||
if err != db.ErrAlreadyExists {
|
||||
return nil, fmt.Errorf("GetRemoteAttachment: error inserting attachment: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (d *deref) RefreshAttachment(ctx context.Context, requestingUsername string, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error) {
|
||||
// it just doesn't exist or we have to refresh
|
||||
if minAttachment.AccountID == "" {
|
||||
return nil, fmt.Errorf("RefreshAttachment: minAttachment account ID was empty")
|
||||
}
|
||||
|
||||
if minAttachment.File.ContentType == "" {
|
||||
return nil, fmt.Errorf("RefreshAttachment: minAttachment.file.contentType was empty")
|
||||
}
|
||||
|
||||
t, err := d.transportController.NewTransportForUsername(ctx, requestingUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error creating transport: %s", err)
|
||||
}
|
||||
|
||||
derefURI, err := url.Parse(minAttachment.RemoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attachmentBytes, err := t.DereferenceMedia(ctx, derefURI, minAttachment.File.ContentType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error dereferencing media: %s", err)
|
||||
}
|
||||
|
||||
a, err := d.mediaManager.ProcessAttachment(ctx, attachmentBytes, minAttachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error processing attachment: %s", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
|
@ -41,34 +41,7 @@ type Dereferencer interface {
|
|||
|
||||
GetRemoteInstance(ctx context.Context, username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error)
|
||||
|
||||
// GetRemoteAttachment takes a minimal attachment struct and converts it into a fully fleshed out attachment, stored in the database and instance storage.
|
||||
//
|
||||
// The parameter minAttachment must have at least the following fields defined:
|
||||
// * minAttachment.RemoteURL
|
||||
// * minAttachment.AccountID
|
||||
// * minAttachment.File.ContentType
|
||||
//
|
||||
// The returned attachment will have an ID generated for it, so no need to generate one beforehand.
|
||||
// A blurhash will also be generated for the attachment.
|
||||
//
|
||||
// Most other fields will be preserved on the passed attachment, including:
|
||||
// * minAttachment.StatusID
|
||||
// * minAttachment.CreatedAt
|
||||
// * minAttachment.UpdatedAt
|
||||
// * minAttachment.FileMeta
|
||||
// * minAttachment.AccountID
|
||||
// * minAttachment.Description
|
||||
// * minAttachment.ScheduledStatusID
|
||||
// * minAttachment.Thumbnail.RemoteURL
|
||||
// * minAttachment.Avatar
|
||||
// * minAttachment.Header
|
||||
//
|
||||
// GetRemoteAttachment will return early if an attachment with the same value as minAttachment.RemoteURL
|
||||
// is found in the database -- then that attachment will be returned and nothing else will be changed or stored.
|
||||
GetRemoteAttachment(ctx context.Context, requestingUsername string, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error)
|
||||
// RefreshAttachment is like GetRemoteAttachment, but the attachment will always be dereferenced again,
|
||||
// whether or not it was already stored in the database.
|
||||
RefreshAttachment(ctx context.Context, requestingUsername string, minAttachment *gtsmodel.MediaAttachment) (*gtsmodel.MediaAttachment, error)
|
||||
GetRemoteMedia(ctx context.Context, requestingUsername string, accountID string, remoteURL string) (*media.Media, error)
|
||||
|
||||
DereferenceAnnounce(ctx context.Context, announce *gtsmodel.Status, requestingUsername string) error
|
||||
DereferenceThread(ctx context.Context, username string, statusIRI *url.URL) error
|
||||
|
|
55
internal/federation/dereferencing/media.go
Normal file
55
internal/federation/dereferencing/media.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 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 dereferencing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
)
|
||||
|
||||
func (d *deref) GetRemoteMedia(ctx context.Context, requestingUsername string, accountID string, remoteURL string) (*media.Media, error) {
|
||||
if accountID == "" {
|
||||
return nil, fmt.Errorf("RefreshAttachment: minAttachment account ID was empty")
|
||||
}
|
||||
|
||||
t, err := d.transportController.NewTransportForUsername(ctx, requestingUsername)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error creating transport: %s", err)
|
||||
}
|
||||
|
||||
derefURI, err := url.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := t.DereferenceMedia(ctx, derefURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error dereferencing media: %s", err)
|
||||
}
|
||||
|
||||
m, err := d.mediaManager.ProcessMedia(ctx, data, accountID, remoteURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RefreshAttachment: error processing attachment: %s", err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
|
@ -31,6 +31,8 @@ type AttachmentTestSuite struct {
|
|||
}
|
||||
|
||||
func (suite *AttachmentTestSuite) TestDereferenceAttachmentOK() {
|
||||
ctx := context.Background()
|
||||
|
||||
fetchingAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
attachmentOwner := "01FENS9F666SEQ6TYQWEEY78GM"
|
||||
|
@ -39,18 +41,12 @@ func (suite *AttachmentTestSuite) TestDereferenceAttachmentOK() {
|
|||
attachmentURL := "https://s3-us-west-2.amazonaws.com/plushcity/media_attachments/files/106/867/380/219/163/828/original/88e8758c5f011439.jpg"
|
||||
attachmentDescription := "It's a cute plushie."
|
||||
|
||||
minAttachment := >smodel.MediaAttachment{
|
||||
RemoteURL: attachmentURL,
|
||||
AccountID: attachmentOwner,
|
||||
StatusID: attachmentStatus,
|
||||
File: gtsmodel.File{
|
||||
ContentType: attachmentContentType,
|
||||
},
|
||||
Description: attachmentDescription,
|
||||
}
|
||||
|
||||
attachment, err := suite.dereferencer.GetRemoteAttachment(context.Background(), fetchingAccount.Username, minAttachment)
|
||||
media, err := suite.dereferencer.GetRemoteMedia(ctx, fetchingAccount.Username, attachmentOwner, attachmentURL)
|
||||
suite.NoError(err)
|
||||
|
||||
attachment, err := media.LoadAttachment(ctx)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.NotNil(attachment)
|
||||
|
||||
suite.Equal(attachmentOwner, attachment.AccountID)
|
|
@ -393,9 +393,15 @@ func (d *deref) populateStatusAttachments(ctx context.Context, status *gtsmodel.
|
|||
a.AccountID = status.AccountID
|
||||
a.StatusID = status.ID
|
||||
|
||||
attachment, err := d.GetRemoteAttachment(ctx, requestingUsername, a)
|
||||
media, err := d.GetRemoteMedia(ctx, requestingUsername, a.AccountID, a.RemoteURL)
|
||||
if err != nil {
|
||||
logrus.Errorf("populateStatusAttachments: couldn't get remote attachment %s: %s", a.RemoteURL, err)
|
||||
logrus.Errorf("populateStatusAttachments: couldn't get remote media %s: %s", a.RemoteURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
attachment, err := media.LoadAttachment(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("populateStatusAttachments: couldn't load remote attachment %s: %s", a.RemoteURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,17 @@ import (
|
|||
|
||||
// Manager provides an interface for managing media: parsing, storing, and retrieving media objects like photos, videos, and gifs.
|
||||
type Manager interface {
|
||||
ProcessMedia(ctx context.Context, data []byte, accountID string) (*Media, error)
|
||||
// ProcessMedia begins the process of decoding and storing the given data as a piece of media (aka an attachment).
|
||||
// It will return a pointer to a Media struct upon which further actions can be performed, such as getting
|
||||
// the finished media, thumbnail, decoded bytes, attachment, and setting additional fields.
|
||||
//
|
||||
// accountID should be the account that the media belongs to.
|
||||
//
|
||||
// RemoteURL is optional, and can be an empty string. Setting this to a non-empty string indicates that
|
||||
// the piece of media originated on a remote instance and has been dereferenced to be cached locally.
|
||||
ProcessMedia(ctx context.Context, data []byte, accountID string, remoteURL string) (*Media, error)
|
||||
|
||||
ProcessEmoji(ctx context.Context, data []byte, accountID string, remoteURL string) (*Media, error)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
|
@ -70,7 +80,7 @@ func New(database db.DB, storage *kv.KVStore) (Manager, error) {
|
|||
INTERFACE FUNCTIONS
|
||||
*/
|
||||
|
||||
func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID string) (*Media, error) {
|
||||
func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID string, remoteURL string) (*Media, error) {
|
||||
contentType, err := parseContentType(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -85,7 +95,7 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin
|
|||
|
||||
switch mainType {
|
||||
case mimeImage:
|
||||
media, err := m.preProcessImage(ctx, data, contentType, accountID)
|
||||
media, err := m.preProcessImage(ctx, data, contentType, accountID, remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -97,7 +107,7 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin
|
|||
return
|
||||
default:
|
||||
// start preloading the media for the caller's convenience
|
||||
media.PreLoad(innerCtx)
|
||||
media.preLoad(innerCtx)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -107,8 +117,12 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin
|
|||
}
|
||||
}
|
||||
|
||||
func (m *manager) ProcessEmoji(ctx context.Context, data []byte, accountID string, remoteURL string) (*Media, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// preProcessImage initializes processing
|
||||
func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string) (*Media, error) {
|
||||
func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string, remoteURL string) (*Media, error) {
|
||||
if !supportedImage(contentType) {
|
||||
return nil, fmt.Errorf("image type %s not supported", contentType)
|
||||
}
|
||||
|
@ -128,6 +142,7 @@ func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType
|
|||
ID: id,
|
||||
UpdatedAt: time.Now(),
|
||||
URL: uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeOriginal), id, extension),
|
||||
RemoteURL: remoteURL,
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
AccountID: accountID,
|
||||
Processing: 0,
|
||||
|
|
4
internal/media/manager_test.go
Normal file
4
internal/media/manager_test.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package media_test
|
||||
|
||||
|
||||
|
|
@ -1,9 +1,28 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-store/kv"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -26,7 +45,8 @@ type Media struct {
|
|||
attachment will be updated incrementally as media goes through processing
|
||||
*/
|
||||
|
||||
attachment *gtsmodel.MediaAttachment
|
||||
attachment *gtsmodel.MediaAttachment // will only be set if the media is an attachment
|
||||
emoji *gtsmodel.Emoji // will only be set if the media is an emoji
|
||||
rawData []byte
|
||||
|
||||
/*
|
||||
|
@ -86,17 +106,10 @@ func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) {
|
|||
m.attachment.Thumbnail.FileSize = thumb.size
|
||||
|
||||
// put or update the attachment in the database
|
||||
if err := m.database.Put(ctx, m.attachment); err != nil {
|
||||
if err != db.ErrAlreadyExists {
|
||||
m.err = fmt.Errorf("error putting attachment: %s", err)
|
||||
m.thumbstate = errored
|
||||
return nil, m.err
|
||||
}
|
||||
if err := m.database.UpdateByPrimaryKey(ctx, m.attachment); err != nil {
|
||||
m.err = fmt.Errorf("error updating attachment: %s", err)
|
||||
m.thumbstate = errored
|
||||
return nil, m.err
|
||||
}
|
||||
if err := putOrUpdateAttachment(ctx, m.database, m.attachment); err != nil {
|
||||
m.err = err
|
||||
m.thumbstate = errored
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the thumbnail of this media
|
||||
|
@ -148,6 +161,30 @@ func (m *Media) FullSize(ctx context.Context) (*ImageMeta, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// put the full size in storage
|
||||
if err := m.storage.Put(m.attachment.File.Path, decoded.image); err != nil {
|
||||
m.err = fmt.Errorf("error storing full size image: %s", err)
|
||||
m.fullSizeState = errored
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
// set appropriate fields on the attachment based on the image we derived
|
||||
m.attachment.FileMeta.Original = gtsmodel.Original{
|
||||
Width: decoded.width,
|
||||
Height: decoded.height,
|
||||
Size: decoded.size,
|
||||
Aspect: decoded.aspect,
|
||||
}
|
||||
m.attachment.File.FileSize = decoded.size
|
||||
m.attachment.File.UpdatedAt = time.Now()
|
||||
|
||||
// put or update the attachment in the database
|
||||
if err := putOrUpdateAttachment(ctx, m.database, m.attachment); err != nil {
|
||||
m.err = err
|
||||
m.fullSizeState = errored
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the fullsize of this media
|
||||
m.fullSize = decoded
|
||||
|
||||
|
@ -163,17 +200,46 @@ func (m *Media) FullSize(ctx context.Context) (*ImageMeta, error) {
|
|||
return nil, fmt.Errorf("full size processing status %d unknown", m.fullSizeState)
|
||||
}
|
||||
|
||||
// PreLoad begins the process of deriving the thumbnail and encoding the full-size image.
|
||||
func (m *Media) SetAsAvatar(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.attachment.Avatar = true
|
||||
return putOrUpdateAttachment(ctx, m.database, m.attachment)
|
||||
}
|
||||
|
||||
func (m *Media) SetAsHeader(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.attachment.Header = true
|
||||
return putOrUpdateAttachment(ctx, m.database, m.attachment)
|
||||
}
|
||||
|
||||
func (m *Media) SetStatusID(ctx context.Context, statusID string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.attachment.StatusID = statusID
|
||||
return putOrUpdateAttachment(ctx, m.database, m.attachment)
|
||||
}
|
||||
|
||||
// AttachmentID returns the ID of the underlying media attachment without blocking processing.
|
||||
func (m *Media) AttachmentID() string {
|
||||
return m.attachment.ID
|
||||
}
|
||||
|
||||
// preLoad begins the process of deriving the thumbnail and encoding the full-size image.
|
||||
// It does this in a non-blocking way, so you can call it and then come back later and check
|
||||
// if it's finished.
|
||||
func (m *Media) PreLoad(ctx context.Context) {
|
||||
func (m *Media) preLoad(ctx context.Context) {
|
||||
go m.Thumb(ctx)
|
||||
go m.FullSize(ctx)
|
||||
}
|
||||
|
||||
// Load is the blocking equivalent of pre-load. It makes sure the thumbnail and full-size image
|
||||
// have been processed, then it returns the full-size image.
|
||||
func (m *Media) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
|
||||
func (m *Media) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
|
||||
if _, err := m.Thumb(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -184,3 +250,20 @@ func (m *Media) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) {
|
|||
|
||||
return m.attachment, nil
|
||||
}
|
||||
|
||||
func (m *Media) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func putOrUpdateAttachment(ctx context.Context, database db.DB, attachment *gtsmodel.MediaAttachment) error {
|
||||
if err := database.Put(ctx, attachment); err != nil {
|
||||
if err != db.ErrAlreadyExists {
|
||||
return fmt.Errorf("putOrUpdateAttachment: proper error while putting attachment: %s", err)
|
||||
}
|
||||
if err := database.UpdateByPrimaryKey(ctx, attachment); err != nil {
|
||||
return fmt.Errorf("putOrUpdateAttachment: error while updating attachment: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
65
internal/media/media_test.go
Normal file
65
internal/media/media_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 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_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"codeberg.org/gruf/go-store/kv"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type MediaStandardTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
db db.DB
|
||||
storage *kv.KVStore
|
||||
manager media.Manager
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) SetupSuite() {
|
||||
testrig.InitTestLog()
|
||||
testrig.InitTestConfig()
|
||||
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.storage = testrig.NewTestStorage()
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) SetupTest() {
|
||||
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
|
||||
m, err := media.New(suite.db, suite.storage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
suite.manager = m
|
||||
}
|
||||
|
||||
func (suite *MediaStandardTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
}
|
||||
|
||||
func TestMediaStandardTestSuite(t *testing.T) {
|
||||
suite.Run(t, &MediaStandardTestSuite{})
|
||||
}
|
|
@ -33,7 +33,6 @@ import (
|
|||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
@ -140,31 +139,40 @@ func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHead
|
|||
var err error
|
||||
maxImageSize := viper.GetInt(config.Keys.MediaImageMaxSize)
|
||||
if int(avatar.Size) > maxImageSize {
|
||||
err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, maxImageSize)
|
||||
err = fmt.Errorf("UpdateAvatar: avatar with size %d exceeded max image size of %d bytes", avatar.Size, maxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := avatar.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
return nil, fmt.Errorf("UpdateAvatar: could not read provided avatar: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided avatar: %s", err)
|
||||
return nil, fmt.Errorf("UpdateAvatar: could not read provided avatar: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided avatar: size 0 bytes")
|
||||
return nil, errors.New("UpdateAvatar: could not read provided avatar: size 0 bytes")
|
||||
}
|
||||
|
||||
// we're done with the FileHeader now
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, fmt.Errorf("UpdateAvatar: error closing multipart fileheader: %s", err)
|
||||
}
|
||||
|
||||
// do the setting
|
||||
avatarInfo, err := p.mediaManager.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeAvatar, "")
|
||||
media, err := p.mediaManager.ProcessMedia(ctx, buf.Bytes(), accountID, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing avatar: %s", err)
|
||||
return nil, fmt.Errorf("UpdateAvatar: error processing avatar: %s", err)
|
||||
}
|
||||
|
||||
return avatarInfo, f.Close()
|
||||
if err := media.SetAsAvatar(ctx); err != nil {
|
||||
return nil, fmt.Errorf("UpdateAvatar: error setting media as avatar: %s", err)
|
||||
}
|
||||
|
||||
return media.LoadAttachment(ctx)
|
||||
}
|
||||
|
||||
// UpdateHeader does the dirty work of checking the header part of an account update form,
|
||||
|
@ -174,31 +182,40 @@ func (p *processor) UpdateHeader(ctx context.Context, header *multipart.FileHead
|
|||
var err error
|
||||
maxImageSize := viper.GetInt(config.Keys.MediaImageMaxSize)
|
||||
if int(header.Size) > maxImageSize {
|
||||
err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, maxImageSize)
|
||||
err = fmt.Errorf("UpdateHeader: header with size %d exceeded max image size of %d bytes", header.Size, maxImageSize)
|
||||
return nil, err
|
||||
}
|
||||
f, err := header.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
return nil, fmt.Errorf("UpdateHeader: could not read provided header: %s", err)
|
||||
}
|
||||
|
||||
// extract the bytes
|
||||
buf := new(bytes.Buffer)
|
||||
size, err := io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read provided header: %s", err)
|
||||
return nil, fmt.Errorf("UpdateHeader: could not read provided header: %s", err)
|
||||
}
|
||||
if size == 0 {
|
||||
return nil, errors.New("could not read provided header: size 0 bytes")
|
||||
return nil, errors.New("UpdateHeader: could not read provided header: size 0 bytes")
|
||||
}
|
||||
|
||||
// we're done with the FileHeader now
|
||||
if err := f.Close(); err != nil {
|
||||
return nil, fmt.Errorf("UpdateHeader: error closing multipart fileheader: %s", err)
|
||||
}
|
||||
|
||||
// do the setting
|
||||
headerInfo, err := p.mediaManager.ProcessHeaderOrAvatar(ctx, buf.Bytes(), accountID, media.TypeHeader, "")
|
||||
media, err := p.mediaManager.ProcessMedia(ctx, buf.Bytes(), accountID, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error processing header: %s", err)
|
||||
return nil, fmt.Errorf("UpdateHeader: error processing header: %s", err)
|
||||
}
|
||||
|
||||
return headerInfo, f.Close()
|
||||
if err := media.SetAsHeader(ctx); err != nil {
|
||||
return nil, fmt.Errorf("UpdateHeader: error setting media as header: %s", err)
|
||||
}
|
||||
|
||||
return media.LoadAttachment(ctx)
|
||||
}
|
||||
|
||||
func (p *processor) processNote(ctx context.Context, note string, accountID string) (string, error) {
|
||||
|
|
|
@ -27,7 +27,6 @@ import (
|
|||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
|
||||
|
@ -49,26 +48,20 @@ func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account,
|
|||
return nil, errors.New("could not read provided emoji: size 0 bytes")
|
||||
}
|
||||
|
||||
// allow the mediaManager to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
|
||||
emoji, err := p.mediaManager.ProcessLocalEmoji(ctx, buf.Bytes(), form.Shortcode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading emoji: %s", err)
|
||||
}
|
||||
|
||||
emojiID, err := id.NewULID()
|
||||
media, err := p.mediaManager.ProcessEmoji(ctx, buf.Bytes(), account.ID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emoji, err := media.LoadEmoji(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emoji.ID = emojiID
|
||||
|
||||
apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting emoji to apitype: %s", err)
|
||||
}
|
||||
|
||||
if err := p.db.Put(ctx, emoji); err != nil {
|
||||
return nil, fmt.Errorf("database error while processing emoji: %s", err)
|
||||
}
|
||||
|
||||
return &apiEmoji, nil
|
||||
}
|
||||
|
|
|
@ -44,13 +44,13 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
|||
return nil, errors.New("could not read provided attachment: size 0 bytes")
|
||||
}
|
||||
|
||||
// process the media and load it immediately
|
||||
media, err := p.mediaManager.ProcessMedia(ctx, buf.Bytes(), account.ID)
|
||||
// process the media attachment and load it immediately
|
||||
media, err := p.mediaManager.ProcessMedia(ctx, buf.Bytes(), account.ID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attachment, err := media.Load(ctx)
|
||||
attachment, err := media.LoadAttachment(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -62,10 +62,5 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
|||
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(ctx, attachment); err != nil {
|
||||
return nil, fmt.Errorf("error storing media attachment in db: %s", err)
|
||||
}
|
||||
|
||||
return &apiAttachment, nil
|
||||
}
|
||||
|
|
|
@ -28,18 +28,15 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (t *transport) DereferenceMedia(ctx context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
|
||||
func (t *transport) DereferenceMedia(ctx context.Context, iri *url.URL) ([]byte, error) {
|
||||
l := logrus.WithField("func", "DereferenceMedia")
|
||||
l.Debugf("performing GET to %s", iri.String())
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", iri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expectedContentType == "" {
|
||||
req.Header.Add("Accept", "*/*")
|
||||
} else {
|
||||
req.Header.Add("Accept", expectedContentType)
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "*/*") // we don't know what kind of media we're going to get here
|
||||
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
|
||||
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
|
||||
req.Header.Set("Host", iri.Host)
|
||||
|
|
|
@ -34,7 +34,7 @@ import (
|
|||
type Transport interface {
|
||||
pub.Transport
|
||||
// DereferenceMedia fetches the bytes of the given media attachment IRI, with the expectedContentType.
|
||||
DereferenceMedia(ctx context.Context, iri *url.URL, expectedContentType string) ([]byte, error)
|
||||
DereferenceMedia(ctx context.Context, iri *url.URL) ([]byte, error)
|
||||
// DereferenceInstance dereferences remote instance information, first by checking /api/v1/instance, and then by checking /.well-known/nodeinfo.
|
||||
DereferenceInstance(ctx context.Context, iri *url.URL) (*gtsmodel.Instance, error)
|
||||
// Finger performs a webfinger request with the given username and domain, and returns the bytes from the response body.
|
||||
|
|
|
@ -26,5 +26,9 @@ import (
|
|||
|
||||
// NewTestMediaManager returns a media handler with the default test config, and the given db and storage.
|
||||
func NewTestMediaManager(db db.DB, storage *kv.KVStore) media.Manager {
|
||||
return media.New(db, storage)
|
||||
m, err := media.New(db, storage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue