encode gifs properly

This commit is contained in:
tsmethurst 2021-04-12 16:48:31 +02:00
commit 2e7ac10d00
15 changed files with 304 additions and 66 deletions

View file

@ -149,7 +149,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
return return
} }
} }
newStatus.Mentions = menchies newStatus.GTSMentions = menchies
// convert tags to *gtsmodel.Tag // convert tags to *gtsmodel.Tag
tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID) tags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), authed.Account.ID, thisStatusID)
@ -158,7 +158,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating hashtags from status"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating hashtags from status"})
return return
} }
newStatus.Tags = tags newStatus.GTSTags = tags
// convert emojis to *gtsmodel.Emoji // convert emojis to *gtsmodel.Emoji
emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID) emojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), authed.Account.ID, thisStatusID)
@ -167,7 +167,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating emojis from status"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "error generating emojis from status"})
return return
} }
newStatus.Emojis = emojis newStatus.GTSEmojis = emojis
/* /*
FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE STATUS -- it is valid and we will try to create it
@ -180,7 +180,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
} }
// change the status ID of the media attachments to the new status // change the status ID of the media attachments to the new status
for _, a := range newStatus.Attachments { for _, a := range newStatus.GTSMediaAttachments {
a.StatusID = newStatus.ID a.StatusID = newStatus.ID
a.UpdatedAt = time.Now() a.UpdatedAt = time.Now()
if err := m.db.UpdateByID(a.ID, a); err != nil { if err := m.db.UpdateByID(a.ID, a); err != nil {
@ -207,7 +207,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
} }
mastoAttachments := []mastotypes.Attachment{} mastoAttachments := []mastotypes.Attachment{}
for _, a := range newStatus.Attachments { for _, a := range newStatus.GTSMediaAttachments {
ma, err := m.mastoConverter.AttachmentToMasto(a) ma, err := m.mastoConverter.AttachmentToMasto(a)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@ -217,7 +217,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
} }
mastoMentions := []mastotypes.Mention{} mastoMentions := []mastotypes.Mention{}
for _, gtsm := range newStatus.Mentions { for _, gtsm := range newStatus.GTSMentions {
mm, err := m.mastoConverter.MentionToMasto(gtsm) mm, err := m.mastoConverter.MentionToMasto(gtsm)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@ -433,7 +433,8 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
return nil return nil
} }
attachments := []*gtsmodel.MediaAttachment{} GTSMediaAttachments := []*gtsmodel.MediaAttachment{}
Attachments := []string{}
for _, mediaID := range form.MediaIDs { for _, mediaID := range form.MediaIDs {
// check these attachments exist // check these attachments exist
a := &gtsmodel.MediaAttachment{} a := &gtsmodel.MediaAttachment{}
@ -448,9 +449,11 @@ func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccount
if a.StatusID != "" || a.ScheduledStatusID != "" { if a.StatusID != "" || a.ScheduledStatusID != "" {
return fmt.Errorf("media with id %s is already attached to a status", mediaID) return fmt.Errorf("media with id %s is already attached to a status", mediaID)
} }
attachments = append(attachments, a) GTSMediaAttachments = append(GTSMediaAttachments, a)
Attachments = append(Attachments, a.ID)
} }
status.Attachments = attachments status.GTSMediaAttachments = GTSMediaAttachments
status.Attachments = Attachments
return nil return nil
} }

View file

@ -116,7 +116,8 @@ func (suite *StatusCreateTestSuite) TearDownTest() {
TESTING: StatusCreatePOSTHandler TESTING: StatusCreatePOSTHandler
*/ */
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() { // Post a new status with some custom visibility settings
func (suite *StatusCreateTestSuite) TestPostNewStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.PGTokenToOauthToken(t)
@ -160,7 +161,8 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerSuccessful() {
assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility) assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
} }
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() { // Try to reply to a status that doesn't exist
func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.PGTokenToOauthToken(t)
@ -190,7 +192,8 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToFail() {
assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b)) assert.Equal(suite.T(), `{"error":"status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
} }
func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToLocalSuccess() { // Post a reply to the status of a local user that allows replies.
func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
t := suite.testTokens["local_account_1"] t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t) oauthToken := oauth.PGTokenToOauthToken(t)
@ -229,6 +232,63 @@ func (suite *StatusCreateTestSuite) TestStatusCreatePOSTHandlerReplyToLocalSucce
assert.Len(suite.T(), statusReply.Mentions, 1) assert.Len(suite.T(), statusReply.Mentions, 1)
} }
// Take a media file which is currently not associated with a status, and attach it to a new status.
func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t)
// setup
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"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{
"status": {"here's an image attachment"},
"media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"},
}
suite.statusModule.statusCreatePOSTHandler(ctx)
// check response
suite.EqualValues(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
fmt.Println(string(b))
statusReply := &mastomodel.Status{}
err = json.Unmarshal(b, statusReply)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), "", statusReply.SpoilerText)
assert.Equal(suite.T(), "here's an image attachment", statusReply.Content)
assert.False(suite.T(), statusReply.Sensitive)
assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
// there should be one media attachment
assert.Len(suite.T(), statusReply.MediaAttachments, 1)
// get the updated media attachment from the database
gtsAttachment := &gtsmodel.MediaAttachment{}
err = suite.db.GetByID(statusReply.MediaAttachments[0].ID, gtsAttachment)
assert.NoError(suite.T(), err)
// convert it to a masto attachment
gtsAttachmentAsMasto, err := suite.mastoConverter.AttachmentToMasto(gtsAttachment)
assert.NoError(suite.T(), err)
// compare it with what we have now
assert.EqualValues(suite.T(), statusReply.MediaAttachments[0], gtsAttachmentAsMasto)
// the status id of the attachment should now be set to the id of the status we just created
assert.Equal(suite.T(), statusReply.ID, gtsAttachment.StatusID)
}
func TestStatusCreateTestSuite(t *testing.T) { func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite)) suite.Run(t, new(StatusCreateTestSuite))
} }

View file

@ -117,8 +117,8 @@ func GetDefaults() Defaults {
AccountsRequireApproval: true, AccountsRequireApproval: true,
AccountsReasonRequired: true, AccountsReasonRequired: true,
MediaMaxImageSize: 1048576, //1mb MediaMaxImageSize: 2097152, //2mb
MediaMaxVideoSize: 5242880, //5mb MediaMaxVideoSize: 10485760, //10mb
MediaMinDescriptionChars: 0, MediaMinDescriptionChars: 0,
MediaMaxDescriptionChars: 500, MediaMaxDescriptionChars: 500,

View file

@ -110,7 +110,7 @@ const (
// FileTypeImage is for jpegs and pngs // FileTypeImage is for jpegs and pngs
FileTypeImage FileType = "image" FileTypeImage FileType = "image"
// FileTypeGif is for native gifs and soundless videos that have been converted to gifs // FileTypeGif is for native gifs and soundless videos that have been converted to gifs
FileTypeGif FileType = "gifv" FileTypeGif FileType = "gif"
// FileTypeAudio is for audio-only files (no video) // FileTypeAudio is for audio-only files (no video)
FileTypeAudio FileType = "audio" FileTypeAudio FileType = "audio"
// FileTypeVideo is for files with audio + visual // FileTypeVideo is for files with audio + visual

View file

@ -30,6 +30,8 @@ type Status struct {
URL string `pg:",unique"` URL string `pg:",unique"`
// the html-formatted content of this status // the html-formatted content of this status
Content string Content string
// Database IDs of any media attachments associated with this status
Attachments []string
// when was this status created? // when was this status created?
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
// when was this status updated? // when was this status updated?
@ -62,21 +64,21 @@ type Status struct {
NON-DATABASE FIELDS NON-DATABASE FIELDS
These are for convenience while passing the status around internally, These are for convenience while passing the status around internally,
but these fields should never be put in the db. but these fields should *never* be put in the db.
*/ */
// Mentions created in this status // Mentions created in this status
Mentions []*Mention `pg:"-"` GTSMentions []*Mention `pg:"-"`
// Hashtags used in this status // Hashtags used in this status
Tags []*Tag `pg:"-"` GTSTags []*Tag `pg:"-"`
// Emojis used in this status // Emojis used in this status
Emojis []*Emoji `pg:"-"` GTSEmojis []*Emoji `pg:"-"`
// Attachments used in this status // MediaAttachments used in this status
Attachments []*MediaAttachment `pg:"-"` GTSMediaAttachments []*MediaAttachment `pg:"-"`
// Status being replied to // Status being replied to
ReplyToStatus *Status `pg:"-"` GTSReplyToStatus *Status `pg:"-"`
// Account being replied to // Account being replied to
ReplyToAccount *Account `pg:"-"` GTSReplyToAccount *Account `pg:"-"`
} }
// Visibility represents the visibility granularity of a status. // Visibility represents the visibility granularity of a status.

View file

@ -50,7 +50,7 @@ type MediaHandler interface {
// ProcessAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it, // ProcessAttachment takes a new attachment and the requesting account, checks it out, removes exif data from it,
// puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media, // puts it in whatever storage backend we're using, sets the relevant fields in the database for the new media,
// and then returns information to the caller about the attachment. // and then returns information to the caller about the attachment.
ProcessAttachment(img []byte, accountID string) (*gtsmodel.MediaAttachment, error) ProcessAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error)
} }
type mediaHandler struct { type mediaHandler struct {
@ -73,15 +73,15 @@ func New(config *config.Config, database db.DB, storage storage.Storage, log *lo
INTERFACE FUNCTIONS INTERFACE FUNCTIONS
*/ */
func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(attachment []byte, accountID string, headerOrAvi string) (*gtsmodel.MediaAttachment, error) {
l := mh.log.WithField("func", "SetHeaderForAccountID") l := mh.log.WithField("func", "SetHeaderForAccountID")
if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar { if headerOrAvi != MediaHeader && headerOrAvi != MediaAvatar {
return nil, errors.New("header or avatar not selected") return nil, errors.New("header or avatar not selected")
} }
// make sure we have an image we can handle // make sure we have a type we can handle
contentType, err := parseContentType(img) contentType, err := parseContentType(attachment)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -89,13 +89,13 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri
return nil, fmt.Errorf("%s is not an accepted image type", contentType) return nil, fmt.Errorf("%s is not an accepted image type", contentType)
} }
if len(img) == 0 { if len(attachment) == 0 {
return nil, fmt.Errorf("passed reader was of size 0") return nil, fmt.Errorf("passed reader was of size 0")
} }
l.Tracef("read %d bytes of file", len(img)) l.Tracef("read %d bytes of file", len(attachment))
// process it // process it
ma, err := mh.processHeaderOrAvi(img, contentType, headerOrAvi, accountID) ma, err := mh.processHeaderOrAvi(attachment, contentType, headerOrAvi, accountID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err) return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err)
} }
@ -108,8 +108,8 @@ func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID stri
return ma, nil return ma, nil
} }
func (mh *mediaHandler) ProcessAttachment(data []byte, accountID string) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) ProcessAttachment(attachment []byte, accountID string) (*gtsmodel.MediaAttachment, error) {
contentType, err := parseContentType(data) contentType, err := parseContentType(attachment)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -119,24 +119,24 @@ func (mh *mediaHandler) ProcessAttachment(data []byte, accountID string) (*gtsmo
if !supportedVideoType(contentType) { if !supportedVideoType(contentType) {
return nil, fmt.Errorf("video type %s not supported", contentType) return nil, fmt.Errorf("video type %s not supported", contentType)
} }
if len(data) == 0 { if len(attachment) == 0 {
return nil, errors.New("video was of size 0") return nil, errors.New("video was of size 0")
} }
if len(data) > mh.config.MediaConfig.MaxVideoSize { if len(attachment) > mh.config.MediaConfig.MaxVideoSize {
return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(data), mh.config.MediaConfig.MaxVideoSize) return nil, fmt.Errorf("video size %d bytes exceeded max video size of %d bytes", len(attachment), mh.config.MediaConfig.MaxVideoSize)
} }
return mh.processVideo(data, accountID, contentType) return mh.processVideo(attachment, accountID, contentType)
case "image": case "image":
if !supportedImageType(contentType) { if !supportedImageType(contentType) {
return nil, fmt.Errorf("image type %s not supported", contentType) return nil, fmt.Errorf("image type %s not supported", contentType)
} }
if len(data) == 0 { if len(attachment) == 0 {
return nil, errors.New("image was of size 0") return nil, errors.New("image was of size 0")
} }
if len(data) > mh.config.MediaConfig.MaxImageSize { if len(attachment) > mh.config.MediaConfig.MaxImageSize {
return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(data), mh.config.MediaConfig.MaxImageSize) return nil, fmt.Errorf("image size %d bytes exceeded max image size of %d bytes", len(attachment), mh.config.MediaConfig.MaxImageSize)
} }
return mh.processImage(data, accountID, contentType) return mh.processImage(attachment, accountID, contentType)
default: default:
break break
} }
@ -154,28 +154,29 @@ func (mh *mediaHandler) processVideo(data []byte, accountID string, contentType
func (mh *mediaHandler) processImage(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) { func (mh *mediaHandler) processImage(data []byte, accountID string, contentType string) (*gtsmodel.MediaAttachment, error) {
var clean []byte var clean []byte
var err error var err error
var original *imageAndMeta
var small *imageAndMeta
switch contentType { switch contentType {
case "image/jpeg": case "image/jpeg", "image/png":
if clean, err = purgeExif(data); err != nil { if clean, err = purgeExif(data); err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error cleaning exif data: %s", err)
} }
case "image/png": original, err = deriveImage(clean, contentType)
if clean, err = purgeExif(data); err != nil { if err != nil {
return nil, fmt.Errorf("error cleaning exif data: %s", err) return nil, fmt.Errorf("error parsing image: %s", err)
} }
case "image/gif": case "image/gif":
clean = data clean = data
original, err = deriveGif(clean, contentType)
if err != nil {
return nil, fmt.Errorf("error parsing gif: %s", err)
}
default: default:
return nil, errors.New("media type unrecognized") return nil, errors.New("media type unrecognized")
} }
original, err := deriveImage(clean, contentType) small, err = deriveThumbnail(clean, contentType)
if err != nil {
return nil, fmt.Errorf("error parsing image: %s", err)
}
small, err := deriveThumbnail(clean, contentType)
if err != nil { if err != nil {
return nil, fmt.Errorf("error deriving thumbnail: %s", err) return nil, fmt.Errorf("error deriving thumbnail: %s", err)
} }
@ -186,7 +187,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension) originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.%s", URLbase, accountID, newMediaID, extension) smallURL := fmt.Sprintf("%s/%s/attachment/small/%s.jpeg", URLbase, accountID, newMediaID) // all thumbnails/smalls are encoded as jpeg
// we store the original... // we store the original...
originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaOriginal, newMediaID, extension) originalPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaOriginal, newMediaID, extension)
@ -195,7 +196,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
} }
// and a thumbnail... // and a thumbnail...
smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaSmall, newMediaID, extension) smallPath := fmt.Sprintf("%s/%s/%s/%s/%s.jpeg", mh.config.StorageConfig.BasePath, accountID, MediaAttachment, MediaSmall, newMediaID) // all thumbnails/smalls are encoded as jpeg
if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil {
return nil, fmt.Errorf("storage error: %s", err) return nil, fmt.Errorf("storage error: %s", err)
} }
@ -235,7 +236,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType
}, },
Thumbnail: gtsmodel.Thumbnail{ Thumbnail: gtsmodel.Thumbnail{
Path: smallPath, Path: smallPath,
ContentType: contentType, ContentType: "image/jpeg", // all thumbnails/smalls are encoded as jpeg
FileSize: len(small.image), FileSize: len(small.image),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
URL: smallURL, URL: smallURL,

View file

@ -103,6 +103,45 @@ func purgeExif(b []byte) ([]byte, error) {
return clean, nil return clean, nil
} }
func deriveGif(b []byte, extension string) (*imageAndMeta, error) {
var g *gif.GIF
var err error
switch extension {
case "image/gif":
g, err = gif.DecodeAll(bytes.NewReader(b))
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("extension %s not recognised", extension)
}
// use the first frame to get the static characteristics
width := g.Config.Width
height := g.Config.Height
size := width * height
aspect := float64(width) / float64(height)
bh, err := blurhash.Encode(4, 3, g.Image[0])
if err != nil || bh == "" {
return nil, err
}
out := &bytes.Buffer{}
if err := gif.EncodeAll(out, g); err != nil {
return nil, err
}
return &imageAndMeta{
image: out.Bytes(),
width: width,
height: height,
size: size,
aspect: aspect,
blurhash: bh,
}, nil
}
func deriveImage(b []byte, extension string) (*imageAndMeta, error) { func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
var i image.Image var i image.Image
var err error var err error
@ -118,11 +157,6 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
case "image/gif":
i, err = gif.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
default: default:
return nil, fmt.Errorf("extension %s not recognised", extension) return nil, fmt.Errorf("extension %s not recognised", extension)
} }
@ -131,15 +165,17 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) {
height := i.Bounds().Size().Y height := i.Bounds().Size().Y
size := width * height size := width * height
aspect := float64(width) / float64(height) aspect := float64(width) / float64(height)
bh, err := blurhash.Encode(4, 3, i) bh, err := blurhash.Encode(4, 3, i)
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating blurhash: %s", err) return nil, err
} }
out := &bytes.Buffer{} out := &bytes.Buffer{}
if err := jpeg.Encode(out, i, nil); err != nil { if err := jpeg.Encode(out, i, nil); err != nil {
return nil, err return nil, err
} }
return &imageAndMeta{ return &imageAndMeta{
image: out.Bytes(), image: out.Bytes(),
width: width, width: width,

BIN
testrig/media/ohyou-original.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
testrig/media/ohyou-small.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
testrig/media/ohyou.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
testrig/media/trent-original.gif Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
testrig/media/trent-small.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -21,7 +21,6 @@ package testrig
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/storage"
) )
@ -39,13 +38,13 @@ func NewTestStorage() storage.Storage {
func StandardStorageSetup(s storage.Storage, relativePath string) { func StandardStorageSetup(s storage.Storage, relativePath string) {
stored := NewTestStored() stored := NewTestStored()
a := NewTestAttachments() a := NewTestAttachments()
for k, fileNameTemplate := range stored { for k, paths := range stored {
attachmentInfo, ok := a[k] attachmentInfo, ok := a[k]
if !ok { if !ok {
panic(fmt.Errorf("key %s not found in test attachments", k)) panic(fmt.Errorf("key %s not found in test attachments", k))
} }
filenameOriginal := strings.Replace(fileNameTemplate, "*", "original", 1) filenameOriginal := paths.original
filenameSmall := strings.Replace(fileNameTemplate, "*", "small", 1) filenameSmall := paths.small
pathOriginal := attachmentInfo.File.Path pathOriginal := attachmentInfo.File.Path
pathSmall := attachmentInfo.Thumbnail.Path pathSmall := attachmentInfo.Thumbnail.Path
bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal)) bOriginal, err := os.ReadFile(fmt.Sprintf("%s/%s", relativePath, filenameOriginal))

View file

@ -511,13 +511,125 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
Avatar: false, Avatar: false,
Header: false, Header: false,
}, },
"local_account_1_status_4_attachment_1": {
ID: "510f6033-798b-4390-81b1-c38ca2205ad3",
StatusID: "18524c05-97dc-46d7-b474-c811bd9e1e32",
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/510f6033-798b-4390-81b1-c38ca2205ad3.gif",
RemoteURL: "",
CreatedAt: time.Now().Add(-1 * time.Hour),
UpdatedAt: time.Now().Add(-1 * time.Hour),
Type: gtsmodel.FileTypeGif,
FileMeta: gtsmodel.FileMeta{
Original: gtsmodel.Original{
Width: 400,
Height: 280,
Size: 756000,
Aspect: 1.4285714285714286,
},
Small: gtsmodel.Small{
Width: 256,
Height: 179,
Size: 45824,
Aspect: 1.4301675977653632,
},
Focus: gtsmodel.Focus{
X: 0,
Y: 0,
},
},
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
Description: "90's Trent Reznor turning to the camera",
ScheduledStatusID: "",
Blurhash: "LEDara58O=t5EMSOENEN9]}?aK%0",
Processing: 2,
File: gtsmodel.File{
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/510f6033-798b-4390-81b1-c38ca2205ad3.gif",
ContentType: "image/gif",
FileSize: 1109138,
UpdatedAt: time.Now().Add(-1 * time.Hour),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/510f6033-798b-4390-81b1-c38ca2205ad3.jpeg",
ContentType: "image/jpeg",
FileSize: 8803,
UpdatedAt: time.Now().Add(-1 * time.Hour),
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/510f6033-798b-4390-81b1-c38ca2205ad3.jpeg",
RemoteURL: "",
},
Avatar: false,
Header: false,
},
"local_account_1_unattached_1": {
ID: "7a3b9f77-ab30-461e-bdd8-e64bd1db3008",
StatusID: "", // this attachment isn't connected to a status YET
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
RemoteURL: "",
CreatedAt: time.Now().Add(30 * time.Second),
UpdatedAt: time.Now().Add(30 * time.Second),
Type: gtsmodel.FileTypeGif,
FileMeta: gtsmodel.FileMeta{
Original: gtsmodel.Original{
Width: 800,
Height: 450,
Size: 360000,
Aspect: 1.7777777777777777,
},
Small: gtsmodel.Small{
Width: 256,
Height: 144,
Size: 36864,
Aspect: 1.7777777777777777,
},
Focus: gtsmodel.Focus{
X: 0,
Y: 0,
},
},
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
Description: "the oh you meme",
ScheduledStatusID: "",
Blurhash: "LSAd]9ogDge-R:M|j=xWIto0xXWX",
Processing: 2,
File: gtsmodel.File{
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/original/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
ContentType: "image/jpeg",
FileSize: 27759,
UpdatedAt: time.Now().Add(30 * time.Second),
},
Thumbnail: gtsmodel.Thumbnail{
Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
ContentType: "image/jpeg",
FileSize: 6177,
UpdatedAt: time.Now().Add(30 * time.Second),
URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/attachment/small/7a3b9f77-ab30-461e-bdd8-e64bd1db3008.jpeg",
RemoteURL: "",
},
Avatar: false,
Header: false,
},
} }
} }
type paths struct {
original string
small string
}
// NewTestStored returns a map of filenames, keyed according to which attachment they pertain to. // NewTestStored returns a map of filenames, keyed according to which attachment they pertain to.
func NewTestStored() map[string]string { func NewTestStored() map[string]paths {
return map[string]string{ return map[string]paths{
"admin_account_status_1_attachment_1": "welcome-*.jpeg", "admin_account_status_1_attachment_1": {
original: "welcome-original.jpeg",
small: "welcome-small.jpeg",
},
"local_account_1_status_4_attachment_1": {
original: "trent-original.gif",
small: "trent-small.jpeg",
},
"local_account_1_unattached_1": {
original: "ohyou-original.jpeg",
small: "ohyou-small.jpeg",
},
} }
} }
@ -530,6 +642,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd",
Content: "hello world! first post on the instance!", Content: "hello world! first post on the instance!",
Attachments: []string{"b052241b-f30f-4dc6-92fc-2bad0be1f8d8"},
CreatedAt: time.Now().Add(-71 * time.Hour), CreatedAt: time.Now().Add(-71 * time.Hour),
UpdatedAt: time.Now().Add(-71 * time.Hour), UpdatedAt: time.Now().Add(-71 * time.Hour),
Local: true, Local: true,
@ -640,6 +753,30 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
}, },
ActivityStreamsType: gtsmodel.ActivityStreamsNote, ActivityStreamsType: gtsmodel.ActivityStreamsNote,
}, },
"local_account_1_status_4": {
ID: "18524c05-97dc-46d7-b474-c811bd9e1e32",
URI: "http://localhost:8080/users/the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32",
URL: "http://localhost:8080/@the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32",
Content: "here's a little gif of trent",
Attachments: []string{"510f6033-798b-4390-81b1-c38ca2205ad3"},
CreatedAt: time.Now().Add(-1 * time.Hour),
UpdatedAt: time.Now().Add(-1 * time.Hour),
Local: true,
AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6",
InReplyToID: "",
BoostOfID: "",
ContentWarning: "eye contact, trent reznor gif",
Visibility: gtsmodel.VisibilityMutualsOnly,
Sensitive: false,
Language: "en",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true,
Boostable: true,
Replyable: true,
Likeable: true,
},
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
},
"local_account_2_status_1": { "local_account_2_status_1": {
ID: "8945ccf2-3873-45e9-aa13-fd7163f19775", ID: "8945ccf2-3873-45e9-aa13-fd7163f19775",
URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775",