From 71a49e2b43218d34f97b2276c43bdeb2df4a53d2 Mon Sep 17 00:00:00 2001 From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 1 Apr 2021 20:46:45 +0200 Subject: Api/v1/accounts (#8) * start work on accounts module * plodding away on the accounts endpoint * groundwork for other account routes * add password validator * validation utils * require account approval flags * comments * comments * go fmt * comments * add distributor stub * rename api to federator * tidy a bit * validate new account requests * rename r router * comments * add domain blocks * add some more shortcuts * add some more shortcuts * check email + username availability * email block checking for signups * chunking away at it * tick off a few more things * some fiddling with tests * add mock package * relocate repo * move mocks around * set app id on new signups * initialize oauth server properly * rename oauth server * proper mocking tests * go fmt ./... * add required fields * change name of func * move validation to account.go * more tests! * add some file utility tools * add mediaconfig * new shortcut * add some more fields * add followrequest model * add notify * update mastotypes * mock out storage interface * start building media interface * start on update credentials * mess about with media a bit more * test image manipulation * media more or less working * account update nearly working * rearranging my package ;) ;) ;) * phew big stuff!!!! * fix type checking * *fiddles* * Add CreateTables func * account registration flow working * tidy * script to step through auth flow * add a lil helper for generating user uris * fiddling with federation a bit * update progress * Tidying and linting --- internal/media/media.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 2 deletions(-) (limited to 'internal/media/media.go') diff --git a/internal/media/media.go b/internal/media/media.go index 644edb8e4..d25fd258d 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -18,6 +18,195 @@ package media -// API provides an interface for parsing, storing, and retrieving media objects like photos and videos -type API interface { +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/db/model" + "github.com/superseriousbusiness/gotosocial/internal/storage" +) + +// MediaHandler provides an interface for parsing, storing, and retrieving media objects like photos, videos, and gifs. +type MediaHandler interface { + // SetHeaderOrAvatarForAccountID 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. + SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*model.MediaAttachment, 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 +} + +/* + INTERFACE FUNCTIONS +*/ + +func (mh *mediaHandler) SetHeaderOrAvatarForAccountID(img []byte, accountID string, headerOrAvi string) (*model.MediaAttachment, error) { + l := mh.log.WithField("func", "SetHeaderForAccountID") + + if headerOrAvi != "header" && headerOrAvi != "avatar" { + return nil, errors.New("header or avatar not selected") + } + + // make sure we have an image we can handle + contentType, err := parseContentType(img) + if err != nil { + return nil, err + } + if !supportedImageType(contentType) { + return nil, fmt.Errorf("%s is not an accepted image type", contentType) + } + + if len(img) == 0 { + return nil, fmt.Errorf("passed reader was of size 0") + } + l.Tracef("read %d bytes of file", len(img)) + + // process it + ma, err := mh.processHeaderOrAvi(img, contentType, headerOrAvi, accountID) + if err != nil { + return nil, fmt.Errorf("error processing %s: %s", headerOrAvi, err) + } + + // set it in the database + if err := mh.db.SetHeaderOrAvatarForAccountID(ma, accountID); err != nil { + return nil, fmt.Errorf("error putting %s in database: %s", headerOrAvi, err) + } + + return ma, nil +} + +/* + HELPER FUNCTIONS +*/ + +func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, headerOrAvi string, accountID string) (*model.MediaAttachment, error) { + var isHeader bool + var isAvatar bool + + switch headerOrAvi { + case "header": + isHeader = true + case "avatar": + isAvatar = true + default: + return nil, errors.New("header or avatar not selected") + } + + var clean []byte + var err error + + switch contentType { + case "image/jpeg": + if clean, err = purgeExif(imageBytes); err != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", err) + } + case "image/png": + if clean, err = purgeExif(imageBytes); err != nil { + return nil, fmt.Errorf("error cleaning exif data: %s", err) + } + case "image/gif": + clean = imageBytes + default: + return nil, errors.New("media type unrecognized") + } + + original, err := deriveImage(clean, contentType) + if err != nil { + return nil, fmt.Errorf("error parsing image: %s", err) + } + + small, err := deriveThumbnail(clean, contentType) + if err != nil { + return nil, fmt.Errorf("error deriving thumbnail: %s", err) + } + + // 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 + extension := strings.Split(contentType, "/")[1] + newMediaID := uuid.NewString() + + base := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + + // we store the original... + originalPath := fmt.Sprintf("%s/%s/%s/original/%s.%s", base, accountID, headerOrAvi, newMediaID, extension) + if err := mh.storage.StoreFileAt(originalPath, original.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + // and a thumbnail... + smallPath := fmt.Sprintf("%s/%s/%s/small/%s.%s", base, accountID, headerOrAvi, newMediaID, extension) + if err := mh.storage.StoreFileAt(smallPath, small.image); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + ma := &model.MediaAttachment{ + ID: newMediaID, + StatusID: "", + RemoteURL: "", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Type: model.FileTypeImage, + FileMeta: model.FileMeta{ + Original: model.Original{ + Width: original.width, + Height: original.height, + Size: original.size, + Aspect: original.aspect, + }, + Small: model.Small{ + Width: small.width, + Height: small.height, + Size: small.size, + Aspect: small.aspect, + }, + }, + AccountID: accountID, + Description: "", + ScheduledStatusID: "", + Blurhash: original.blurhash, + Processing: 2, + File: model.File{ + Path: originalPath, + ContentType: contentType, + FileSize: len(original.image), + UpdatedAt: time.Now(), + }, + Thumbnail: model.Thumbnail{ + Path: smallPath, + ContentType: contentType, + FileSize: len(small.image), + UpdatedAt: time.Now(), + RemoteURL: "", + }, + Avatar: isAvatar, + Header: isHeader, + } + + return ma, nil } -- cgit v1.2.3