From 38bb05ae469cbe428148a863d3bb26415ade9935 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Sun, 28 Mar 2021 18:48:07 +0200 Subject: [PATCH] start building media interface --- internal/media/media.go | 133 +++++++++++++++++++++++++++- internal/media/mock_API.go | 10 --- internal/media/mock_MediaHandler.go | 37 ++++++++ 3 files changed, 168 insertions(+), 12 deletions(-) delete mode 100644 internal/media/mock_API.go create mode 100644 internal/media/mock_MediaHandler.go diff --git a/internal/media/media.go b/internal/media/media.go index 644edb8..d3ab2b2 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -18,6 +18,135 @@ package media -// API provides an interface for parsing, storing, and retrieving media objects like photos and videos -type API interface { +import ( + "errors" + "fmt" + "mime/multipart" + + "github.com/google/uuid" + "github.com/h2non/filetype" + exifremove "github.com/scottleedavis/go-exif-remove" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/storage" +) + +var ( + acceptedImageTypes = []string{ + "jpeg", + "gif", + "png", + } +) + +// MediaHandler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs. +type MediaHandler interface { + // SetHeaderForAccountID takes a new header image for an 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 image, + // and then returns information to the caller about the new header's web location. + SetHeaderForAccountID(f multipart.File, id string) (*HeaderInfo, error) +} + +type mediaHandler struct { + config *config.Config + db db.DB + storage storage.Storage + log *logrus.Logger +} + +func New(config *config.Config, database db.DB, storage storage.Storage, log *logrus.Logger) MediaHandler { + return &mediaHandler{ + config: config, + db: database, + storage: storage, + log: log, + } +} + +// HeaderInfo wraps the urls at which a Header and a StaticHeader is available from the server. +type HeaderInfo struct { + // URL to the header + Header string + // Static version of the above (eg., a path to a still image if the header is a gif) + HeaderStatic string +} + +func (mh *mediaHandler) SetHeaderForAccountID(f multipart.File, accountID string) (*HeaderInfo, error) { + l := mh.log.WithField("func", "SetHeaderForAccountID") + + // make sure we can handle this + extension, err := processableHeaderOrAvi(f) + if err != nil { + return nil, err + } + + // extract the bytes + imageBytes := []byte{} + size, err := f.Read(imageBytes) + if err != nil { + return nil, fmt.Errorf("error reading file bytes: %s", err) + } + l.Tracef("read %d bytes of file", size) + + // close the open file--we don't need it anymore now we have the bytes + if err := f.Close(); err != nil { + return nil, fmt.Errorf("error closing file: %s", err) + } + + // remove exif data from images because fuck that shit + cleanBytes := []byte{} + if extension == "jpeg" || extension == "png" { + cleanBytes, err = exifremove.Remove(imageBytes) + if err != nil { + return nil, fmt.Errorf("error removing exif from image: %s", err) + } + } else { + // our only other accepted image type (gif) doesn't need cleaning + cleanBytes = imageBytes + } + + // now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it + path := fmt.Sprintf("/%s/media/headers/%s.%s", accountID, uuid.NewString(), extension) + if err := mh.storage.StoreFileAt(path, cleanBytes); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + return nil, nil +} + +func processableHeaderOrAvi(f multipart.File) (string, error) { + extension := "" + + head := make([]byte, 261) + _, err := f.Read(head) + if err != nil { + return extension, fmt.Errorf("could not read first magic bytes of file: %s", err) + } + + kind, err := filetype.Match(head) + if err != nil { + return extension, err + } + + if kind == filetype.Unknown || !filetype.IsImage(head) { + return extension, errors.New("filetype is not an image") + } + + if !supportedImageType(kind.MIME.Subtype) { + return extension, fmt.Errorf("%s is not an accepted image type", kind.MIME.Value) + } + + extension = kind.MIME.Subtype + + return extension, nil +} + +func supportedImageType(have string) bool { + for _, accepted := range acceptedImageTypes { + if have == accepted { + return true + } + } + return false } diff --git a/internal/media/mock_API.go b/internal/media/mock_API.go deleted file mode 100644 index bceede3..0000000 --- a/internal/media/mock_API.go +++ /dev/null @@ -1,10 +0,0 @@ -// Code generated by mockery v2.7.4. DO NOT EDIT. - -package media - -import mock "github.com/stretchr/testify/mock" - -// MockAPI is an autogenerated mock type for the API type -type MockAPI struct { - mock.Mock -} diff --git a/internal/media/mock_MediaHandler.go b/internal/media/mock_MediaHandler.go new file mode 100644 index 0000000..1f11abd --- /dev/null +++ b/internal/media/mock_MediaHandler.go @@ -0,0 +1,37 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package media + +import ( + multipart "mime/multipart" + + mock "github.com/stretchr/testify/mock" +) + +// MockMediaHandler is an autogenerated mock type for the MediaHandler type +type MockMediaHandler struct { + mock.Mock +} + +// SetHeaderForAccountID provides a mock function with given fields: f, id +func (_m *MockMediaHandler) SetHeaderForAccountID(f multipart.File, id string) (*HeaderInfo, error) { + ret := _m.Called(f, id) + + var r0 *HeaderInfo + if rf, ok := ret.Get(0).(func(multipart.File, string) *HeaderInfo); ok { + r0 = rf(f, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*HeaderInfo) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(multipart.File, string) error); ok { + r1 = rf(f, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +}