2022-01-11 16:49:14 +00:00
/ *
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 (
2022-01-16 17:52:55 +00:00
"bytes"
2022-01-11 16:49:14 +00:00
"context"
2022-10-13 13:16:24 +00:00
"errors"
2022-01-11 16:49:14 +00:00
"fmt"
2022-01-16 17:52:55 +00:00
"io"
2022-01-11 16:49:14 +00:00
"strings"
"sync"
2022-02-08 12:38:44 +00:00
"sync/atomic"
2022-01-11 16:49:14 +00:00
"time"
2022-11-05 11:10:19 +00:00
gostore "codeberg.org/gruf/go-store/v2/storage"
2022-09-12 11:03:23 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2022-01-11 16:49:14 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2022-10-13 13:16:24 +00:00
"github.com/superseriousbusiness/gotosocial/internal/id"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2022-07-03 10:08:30 +00:00
"github.com/superseriousbusiness/gotosocial/internal/storage"
2022-01-11 16:49:14 +00:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
// ProcessingEmoji represents an emoji currently processing. It exposes
// various functions for retrieving data from the process.
type ProcessingEmoji struct {
mu sync . Mutex
2022-01-11 16:51:45 +00:00
// id of this instance's account -- pinned for convenience here so we only need to fetch it once
instanceAccountID string
2022-01-11 16:49:14 +00:00
/ *
below fields should be set on newly created media ;
emoji will be updated incrementally as media goes through processing
* /
2022-02-22 12:50:33 +00:00
emoji * gtsmodel . Emoji
data DataFunc
postData PostDataCallbackFunc
read bool // bool indicating that data function has been triggered already
2022-01-11 16:49:14 +00:00
/ *
2022-01-16 17:52:55 +00:00
below fields represent the processing state of the static of the emoji
2022-01-11 16:49:14 +00:00
* /
2022-02-08 12:38:44 +00:00
staticState int32
2022-01-11 16:49:14 +00:00
/ *
below pointers to database and storage are maintained so that
the media can store and update itself during processing steps
* /
database db . DB
2022-11-24 08:35:46 +00:00
storage * storage . Driver
2022-01-11 16:49:14 +00:00
err error // error created during processing, if any
2022-01-15 16:36:15 +00:00
// track whether this emoji has already been put in the databse
insertedInDB bool
2022-10-13 13:16:24 +00:00
// is this a refresh of an existing emoji?
refresh bool
// if it is a refresh, which alternate ID should we use in the storage and URL paths?
newPathID string
2022-01-11 16:49:14 +00:00
}
// EmojiID returns the ID of the underlying emoji without blocking processing.
func ( p * ProcessingEmoji ) EmojiID ( ) string {
return p . emoji . ID
}
// LoadEmoji blocks until the static and fullsize image
// has been processed, and then returns the completed emoji.
func ( p * ProcessingEmoji ) LoadEmoji ( ctx context . Context ) ( * gtsmodel . Emoji , error ) {
2022-01-16 17:52:55 +00:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
2022-01-11 16:49:14 +00:00
2022-01-16 17:52:55 +00:00
if err := p . store ( ctx ) ; err != nil {
2022-01-11 16:49:14 +00:00
return nil , err
}
2022-01-16 17:52:55 +00:00
if err := p . loadStatic ( ctx ) ; err != nil {
2022-01-11 16:49:14 +00:00
return nil , err
}
2022-01-15 16:36:15 +00:00
// store the result in the database before returning it
if ! p . insertedInDB {
2022-10-13 13:16:24 +00:00
if p . refresh {
columns := [ ] string {
"updated_at" ,
"image_remote_url" ,
"image_static_remote_url" ,
"image_url" ,
"image_static_url" ,
"image_path" ,
"image_static_path" ,
"image_content_type" ,
"image_file_size" ,
"image_static_file_size" ,
"image_updated_at" ,
2022-11-24 18:12:07 +00:00
"shortcode" ,
2022-10-13 13:16:24 +00:00
"uri" ,
}
if _ , err := p . database . UpdateEmoji ( ctx , p . emoji , columns ... ) ; err != nil {
return nil , err
}
} else {
if err := p . database . PutEmoji ( ctx , p . emoji ) ; err != nil {
return nil , err
}
2022-01-15 16:36:15 +00:00
}
p . insertedInDB = true
}
2022-01-11 16:49:14 +00:00
return p . emoji , nil
}
// Finished returns true if processing has finished for both the thumbnail
// and full fized version of this piece of media.
func ( p * ProcessingEmoji ) Finished ( ) bool {
2022-02-08 12:38:44 +00:00
return atomic . LoadInt32 ( & p . staticState ) == int32 ( complete )
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
func ( p * ProcessingEmoji ) loadStatic ( ctx context . Context ) error {
2022-02-08 12:38:44 +00:00
staticState := atomic . LoadInt32 ( & p . staticState )
switch processState ( staticState ) {
2022-01-11 16:49:14 +00:00
case received :
2022-01-16 17:52:55 +00:00
// stream the original file out of storage...
2022-07-03 10:08:30 +00:00
stored , err := p . storage . GetStream ( ctx , p . emoji . ImagePath )
2022-01-11 16:49:14 +00:00
if err != nil {
2022-01-16 17:52:55 +00:00
p . err = fmt . Errorf ( "loadStatic: error fetching file from storage: %s" , err )
2022-02-08 12:38:44 +00:00
atomic . StoreInt32 ( & p . staticState , int32 ( errored ) )
2022-01-16 17:52:55 +00:00
return p . err
2022-01-11 16:49:14 +00:00
}
2022-11-11 11:01:53 +00:00
defer stored . Close ( )
2022-09-19 11:43:22 +00:00
2022-01-16 17:52:55 +00:00
// we haven't processed a static version of this emoji yet so do it now
static , err := deriveStaticEmoji ( stored , p . emoji . ImageContentType )
if err != nil {
p . err = fmt . Errorf ( "loadStatic: error deriving static: %s" , err )
2022-02-08 12:38:44 +00:00
atomic . StoreInt32 ( & p . staticState , int32 ( errored ) )
2022-01-16 17:52:55 +00:00
return p . err
2022-01-11 16:49:14 +00:00
}
2022-11-11 11:01:53 +00:00
// Close stored emoji now we're done
if err := stored . Close ( ) ; err != nil {
log . Errorf ( "loadStatic: error closing stored full size: %s" , err )
}
// put the static image in storage
2022-09-19 11:59:11 +00:00
if err := p . storage . Put ( ctx , p . emoji . ImageStaticPath , static . small ) ; err != nil && err != storage . ErrAlreadyExists {
2022-01-16 17:52:55 +00:00
p . err = fmt . Errorf ( "loadStatic: error storing static: %s" , err )
2022-02-08 12:38:44 +00:00
atomic . StoreInt32 ( & p . staticState , int32 ( errored ) )
2022-01-16 17:52:55 +00:00
return p . err
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
p . emoji . ImageStaticFileSize = len ( static . small )
2022-01-11 16:49:14 +00:00
2022-01-16 17:52:55 +00:00
// we're done processing the static version of the emoji!
2022-02-08 12:38:44 +00:00
atomic . StoreInt32 ( & p . staticState , int32 ( complete ) )
2022-01-11 16:49:14 +00:00
fallthrough
case complete :
2022-01-16 17:52:55 +00:00
return nil
2022-01-11 16:49:14 +00:00
case errored :
2022-01-16 17:52:55 +00:00
return p . err
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
return fmt . Errorf ( "static processing status %d unknown" , p . staticState )
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
// store calls the data function attached to p if it hasn't been called yet,
// and updates the underlying attachment fields as necessary. It will then stream
// bytes from p's reader directly into storage so that it can be retrieved later.
func ( p * ProcessingEmoji ) store ( ctx context . Context ) error {
2022-01-11 16:49:14 +00:00
// check if we've already done this and bail early if we have
2022-01-16 17:52:55 +00:00
if p . read {
2022-01-11 16:49:14 +00:00
return nil
}
2022-11-03 14:03:12 +00:00
// execute the data function to get the readcloser out of it
rc , fileSize , err := p . data ( ctx )
2022-01-11 16:49:14 +00:00
if err != nil {
2022-01-16 17:52:55 +00:00
return fmt . Errorf ( "store: error executing data function: %s" , err )
}
2022-03-21 12:41:44 +00:00
// defer closing the reader when we're done with it
defer func ( ) {
2022-11-03 14:03:12 +00:00
if err := rc . Close ( ) ; err != nil {
log . Errorf ( "store: error closing readcloser: %s" , err )
2022-03-21 12:41:44 +00:00
}
} ( )
2022-11-11 19:27:37 +00:00
// execute the postData function no matter what happens
defer func ( ) {
if p . postData != nil {
if err := p . postData ( ctx ) ; err != nil {
log . Errorf ( "store: error executing postData: %s" , err )
}
}
} ( )
2022-01-16 17:52:55 +00:00
// extract no more than 261 bytes from the beginning of the file -- this is the header
firstBytes := make ( [ ] byte , maxFileHeaderBytes )
2022-11-03 14:03:12 +00:00
if _ , err := rc . Read ( firstBytes ) ; err != nil {
2022-01-16 17:52:55 +00:00
return fmt . Errorf ( "store: error reading initial %d bytes: %s" , maxFileHeaderBytes , err )
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
// now we have the file header we can work out the content type from it
contentType , err := parseContentType ( firstBytes )
2022-01-11 16:49:14 +00:00
if err != nil {
2022-01-16 17:52:55 +00:00
return fmt . Errorf ( "store: error parsing content type: %s" , err )
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
// bail if this is a type we can't process
2022-01-11 16:49:14 +00:00
if ! supportedEmoji ( contentType ) {
2022-01-16 17:52:55 +00:00
return fmt . Errorf ( "store: content type %s was not valid for an emoji" , contentType )
2022-01-11 16:49:14 +00:00
}
2022-01-16 17:52:55 +00:00
// extract the file extension
2022-01-11 16:49:14 +00:00
split := strings . Split ( contentType , "/" )
extension := split [ 1 ] // something like 'gif'
// set some additional fields on the emoji now that
// we know more about what the underlying image actually is
2022-10-13 13:16:24 +00:00
var pathID string
if p . refresh {
pathID = p . newPathID
} else {
pathID = p . emoji . ID
}
p . emoji . ImageURL = uris . GenerateURIForAttachment ( p . instanceAccountID , string ( TypeEmoji ) , string ( SizeOriginal ) , pathID , extension )
p . emoji . ImagePath = fmt . Sprintf ( "%s/%s/%s/%s.%s" , p . instanceAccountID , TypeEmoji , SizeOriginal , pathID , extension )
2022-01-15 13:33:58 +00:00
p . emoji . ImageContentType = contentType
2022-01-11 16:49:14 +00:00
2022-01-16 17:52:55 +00:00
// concatenate the first bytes with the existing bytes still in the reader (thanks Mara)
2022-11-03 14:03:12 +00:00
readerToStore := io . MultiReader ( bytes . NewBuffer ( firstBytes ) , rc )
2022-09-24 09:11:47 +00:00
2022-09-29 20:50:43 +00:00
var maxEmojiSize int64
2022-09-24 09:11:47 +00:00
if p . emoji . Domain == "" {
2022-09-29 20:50:43 +00:00
maxEmojiSize = int64 ( config . GetMediaEmojiLocalMaxSize ( ) )
2022-09-24 09:11:47 +00:00
} else {
2022-09-29 20:50:43 +00:00
maxEmojiSize = int64 ( config . GetMediaEmojiRemoteMaxSize ( ) )
2022-09-24 09:11:47 +00:00
}
// if we know the fileSize already, make sure it's not bigger than our limit
var checkedSize bool
if fileSize > 0 {
checkedSize = true
if fileSize > maxEmojiSize {
return fmt . Errorf ( "store: given emoji fileSize (%db) is larger than allowed size (%db)" , fileSize , maxEmojiSize )
}
}
2022-01-16 17:52:55 +00:00
// store this for now -- other processes can pull it out of storage as they please
2022-11-11 19:27:37 +00:00
if fileSize , err = putStream ( ctx , p . storage , p . emoji . ImagePath , readerToStore , fileSize ) ; err != nil {
if ! errors . Is ( err , storage . ErrAlreadyExists ) {
return fmt . Errorf ( "store: error storing stream: %s" , err )
}
log . Warnf ( "emoji %s already exists at storage path: %s" , p . emoji . ID , p . emoji . ImagePath )
2022-01-16 17:52:55 +00:00
}
2022-09-24 09:11:47 +00:00
// if we didn't know the fileSize yet, we do now, so check if we need to
if ! checkedSize && fileSize > maxEmojiSize {
2022-11-11 19:27:37 +00:00
err = fmt . Errorf ( "store: discovered emoji fileSize (%db) is larger than allowed emojiRemoteMaxSize (%db), will delete from the store now" , fileSize , maxEmojiSize )
log . Warn ( err )
if deleteErr := p . storage . Delete ( ctx , p . emoji . ImagePath ) ; deleteErr != nil {
log . Errorf ( "store: error removing too-large emoji from the store: %s" , deleteErr )
}
return err
2022-09-24 09:11:47 +00:00
}
2022-09-29 20:50:43 +00:00
p . emoji . ImageFileSize = int ( fileSize )
2022-01-16 17:52:55 +00:00
p . read = true
2022-02-22 12:50:33 +00:00
2022-01-11 16:49:14 +00:00
return nil
}
2022-10-13 13:16:24 +00:00
func ( m * manager ) preProcessEmoji ( ctx context . Context , data DataFunc , postData PostDataCallbackFunc , shortcode string , emojiID string , uri string , ai * AdditionalEmojiInfo , refresh bool ) ( * ProcessingEmoji , error ) {
2022-01-11 16:49:14 +00:00
instanceAccount , err := m . db . GetInstanceAccount ( ctx , "" )
if err != nil {
return nil , fmt . Errorf ( "preProcessEmoji: error fetching this instance account from the db: %s" , err )
}
2022-10-13 13:16:24 +00:00
var newPathID string
var emoji * gtsmodel . Emoji
if refresh {
emoji , err = m . db . GetEmojiByID ( ctx , emojiID )
if err != nil {
return nil , fmt . Errorf ( "preProcessEmoji: error fetching emoji to refresh from the db: %s" , err )
}
// if this is a refresh, we will end up with new images
// stored for this emoji, so we can use the postData function
// to perform clean up of the old images from storage
originalPostData := postData
originalImagePath := emoji . ImagePath
originalImageStaticPath := emoji . ImageStaticPath
2022-11-11 19:27:37 +00:00
postData = func ( innerCtx context . Context ) error {
2022-10-13 13:16:24 +00:00
// trigger the original postData function if it was provided
if originalPostData != nil {
2022-11-11 19:27:37 +00:00
if err := originalPostData ( innerCtx ) ; err != nil {
2022-10-13 13:16:24 +00:00
return err
}
}
l := log . WithField ( "shortcode@domain" , emoji . Shortcode + "@" + emoji . Domain )
l . Debug ( "postData: cleaning up old emoji files for refreshed emoji" )
2022-11-11 19:27:37 +00:00
if err := m . storage . Delete ( innerCtx , originalImagePath ) ; err != nil && ! errors . Is ( err , gostore . ErrNotFound ) {
2022-10-13 13:16:24 +00:00
l . Errorf ( "postData: error cleaning up old emoji image at %s for refreshed emoji: %s" , originalImagePath , err )
}
2022-11-11 19:27:37 +00:00
if err := m . storage . Delete ( innerCtx , originalImageStaticPath ) ; err != nil && ! errors . Is ( err , gostore . ErrNotFound ) {
2022-10-13 13:16:24 +00:00
l . Errorf ( "postData: error cleaning up old emoji static image at %s for refreshed emoji: %s" , originalImageStaticPath , err )
}
return nil
}
newPathID , err = id . NewRandomULID ( )
if err != nil {
return nil , fmt . Errorf ( "preProcessEmoji: error generating alternateID for emoji refresh: %s" , err )
}
// store + serve static image at new path ID
emoji . ImageStaticURL = uris . GenerateURIForAttachment ( instanceAccount . ID , string ( TypeEmoji ) , string ( SizeStatic ) , newPathID , mimePng )
emoji . ImageStaticPath = fmt . Sprintf ( "%s/%s/%s/%s.%s" , instanceAccount . ID , TypeEmoji , SizeStatic , newPathID , mimePng )
2022-11-24 18:12:07 +00:00
emoji . Shortcode = shortcode
2022-10-13 13:16:24 +00:00
emoji . URI = uri
} else {
disabled := false
visibleInPicker := true
// populate initial fields on the emoji -- some of these will be overwritten as we proceed
emoji = & gtsmodel . Emoji {
ID : emojiID ,
CreatedAt : time . Now ( ) ,
Shortcode : shortcode ,
Domain : "" , // assume our own domain unless told otherwise
ImageRemoteURL : "" ,
ImageStaticRemoteURL : "" ,
ImageURL : "" , // we don't know yet
ImageStaticURL : uris . GenerateURIForAttachment ( instanceAccount . ID , string ( TypeEmoji ) , string ( SizeStatic ) , emojiID , mimePng ) , // all static emojis are encoded as png
ImagePath : "" , // we don't know yet
ImageStaticPath : fmt . Sprintf ( "%s/%s/%s/%s.%s" , instanceAccount . ID , TypeEmoji , SizeStatic , emojiID , mimePng ) , // all static emojis are encoded as png
ImageContentType : "" , // we don't know yet
ImageStaticContentType : mimeImagePng , // all static emojis are encoded as png
ImageFileSize : 0 ,
ImageStaticFileSize : 0 ,
Disabled : & disabled ,
URI : uri ,
VisibleInPicker : & visibleInPicker ,
CategoryID : "" ,
}
2022-01-11 16:49:14 +00:00
}
2022-10-13 13:16:24 +00:00
emoji . ImageUpdatedAt = time . Now ( )
emoji . UpdatedAt = time . Now ( )
2022-01-11 16:49:14 +00:00
// check if we have additional info to add to the emoji,
// and overwrite some of the emoji fields if so
if ai != nil {
if ai . CreatedAt != nil {
2022-01-15 13:33:58 +00:00
emoji . CreatedAt = * ai . CreatedAt
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . Domain != nil {
emoji . Domain = * ai . Domain
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . ImageRemoteURL != nil {
emoji . ImageRemoteURL = * ai . ImageRemoteURL
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . ImageStaticRemoteURL != nil {
emoji . ImageStaticRemoteURL = * ai . ImageStaticRemoteURL
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . Disabled != nil {
2022-08-15 10:35:05 +00:00
emoji . Disabled = ai . Disabled
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . VisibleInPicker != nil {
2022-08-15 10:35:05 +00:00
emoji . VisibleInPicker = ai . VisibleInPicker
2022-01-11 16:49:14 +00:00
}
2022-01-15 13:33:58 +00:00
if ai . CategoryID != nil {
emoji . CategoryID = * ai . CategoryID
2022-01-11 16:49:14 +00:00
}
}
processingEmoji := & ProcessingEmoji {
2022-01-11 16:51:45 +00:00
instanceAccountID : instanceAccount . ID ,
emoji : emoji ,
data : data ,
2022-02-22 12:50:33 +00:00
postData : postData ,
2022-02-08 12:38:44 +00:00
staticState : int32 ( received ) ,
2022-01-11 16:51:45 +00:00
database : m . db ,
storage : m . storage ,
2022-10-13 13:16:24 +00:00
refresh : refresh ,
newPathID : newPathID ,
2022-01-11 16:49:14 +00:00
}
return processingEmoji , nil
}