2022-01-11 17:49:14 +01: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 18:52:55 +01:00
"bytes"
2022-01-11 17:49:14 +01:00
"context"
"fmt"
2022-01-16 18:52:55 +01:00
"io"
2022-01-11 17:49:14 +01:00
"strings"
"sync"
"time"
"codeberg.org/gruf/go-store/kv"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"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 17:51:45 +01:00
// id of this instance's account -- pinned for convenience here so we only need to fetch it once
instanceAccountID string
2022-01-11 17:49:14 +01:00
/ *
below fields should be set on newly created media ;
emoji will be updated incrementally as media goes through processing
* /
emoji * gtsmodel . Emoji
data DataFunc
2022-01-16 18:52:55 +01:00
read bool // bool indicating that data function has been triggered already
2022-01-11 17:49:14 +01:00
/ *
2022-01-16 18:52:55 +01:00
below fields represent the processing state of the static of the emoji
2022-01-11 17:49:14 +01:00
* /
2022-01-16 18:52:55 +01:00
staticState processState
2022-01-11 17:49:14 +01:00
fullSizeState processState
/ *
below pointers to database and storage are maintained so that
the media can store and update itself during processing steps
* /
database db . DB
storage * kv . KVStore
err error // error created during processing, if any
2022-01-15 17:36:15 +01:00
// track whether this emoji has already been put in the databse
insertedInDB bool
2022-01-11 17:49:14 +01: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 18:52:55 +01:00
p . mu . Lock ( )
defer p . mu . Unlock ( )
2022-01-11 17:49:14 +01:00
2022-01-16 18:52:55 +01:00
if err := p . store ( ctx ) ; err != nil {
2022-01-11 17:49:14 +01:00
return nil , err
}
2022-01-16 18:52:55 +01:00
if err := p . loadStatic ( ctx ) ; err != nil {
2022-01-11 17:49:14 +01:00
return nil , err
}
2022-01-15 17:36:15 +01:00
// store the result in the database before returning it
if ! p . insertedInDB {
if err := p . database . Put ( ctx , p . emoji ) ; err != nil {
return nil , err
}
p . insertedInDB = true
}
2022-01-11 17:49:14 +01: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 {
return p . staticState == complete && p . fullSizeState == complete
}
2022-01-16 18:52:55 +01:00
func ( p * ProcessingEmoji ) loadStatic ( ctx context . Context ) error {
2022-01-11 17:49:14 +01:00
switch p . staticState {
case received :
2022-01-16 18:52:55 +01:00
// stream the original file out of storage...
stored , err := p . storage . GetStream ( p . emoji . ImagePath )
2022-01-11 17:49:14 +01:00
if err != nil {
2022-01-16 18:52:55 +01:00
p . err = fmt . Errorf ( "loadStatic: error fetching file from storage: %s" , err )
2022-01-11 17:49:14 +01:00
p . staticState = errored
2022-01-16 18:52:55 +01:00
return p . err
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01: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-01-11 17:49:14 +01:00
p . staticState = errored
2022-01-16 18:52:55 +01:00
return p . err
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
if err := stored . Close ( ) ; err != nil {
p . err = fmt . Errorf ( "loadStatic: error closing stored full size: %s" , err )
p . staticState = errored
return p . err
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
// put the static in storage
if err := p . storage . Put ( p . emoji . ImageStaticPath , static . small ) ; err != nil {
p . err = fmt . Errorf ( "loadStatic: error storing static: %s" , err )
p . staticState = errored
return p . err
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
p . emoji . ImageStaticFileSize = len ( static . small )
2022-01-11 17:49:14 +01:00
2022-01-16 18:52:55 +01:00
// we're done processing the static version of the emoji!
p . staticState = complete
2022-01-11 17:49:14 +01:00
fallthrough
case complete :
2022-01-16 18:52:55 +01:00
return nil
2022-01-11 17:49:14 +01:00
case errored :
2022-01-16 18:52:55 +01:00
return p . err
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
return fmt . Errorf ( "static processing status %d unknown" , p . staticState )
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01: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 17:49:14 +01:00
// check if we've already done this and bail early if we have
2022-01-16 18:52:55 +01:00
if p . read {
2022-01-11 17:49:14 +01:00
return nil
}
2022-01-16 18:52:55 +01:00
// execute the data function to get the reader out of it
2022-01-23 14:41:58 +01:00
reader , fileSize , err := p . data ( ctx )
2022-01-11 17:49:14 +01:00
if err != nil {
2022-01-16 18:52:55 +01:00
return fmt . Errorf ( "store: error executing data function: %s" , err )
}
// extract no more than 261 bytes from the beginning of the file -- this is the header
firstBytes := make ( [ ] byte , maxFileHeaderBytes )
if _ , err := reader . Read ( firstBytes ) ; err != nil {
return fmt . Errorf ( "store: error reading initial %d bytes: %s" , maxFileHeaderBytes , err )
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
// now we have the file header we can work out the content type from it
contentType , err := parseContentType ( firstBytes )
2022-01-11 17:49:14 +01:00
if err != nil {
2022-01-16 18:52:55 +01:00
return fmt . Errorf ( "store: error parsing content type: %s" , err )
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
// bail if this is a type we can't process
2022-01-11 17:49:14 +01:00
if ! supportedEmoji ( contentType ) {
2022-01-16 18:52:55 +01:00
return fmt . Errorf ( "store: content type %s was not valid for an emoji" , contentType )
2022-01-11 17:49:14 +01:00
}
2022-01-16 18:52:55 +01:00
// extract the file extension
2022-01-11 17:49:14 +01: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-01-15 14:33:58 +01:00
p . emoji . ImageURL = uris . GenerateURIForAttachment ( p . instanceAccountID , string ( TypeEmoji ) , string ( SizeOriginal ) , p . emoji . ID , extension )
p . emoji . ImagePath = fmt . Sprintf ( "%s/%s/%s/%s.%s" , p . instanceAccountID , TypeEmoji , SizeOriginal , p . emoji . ID , extension )
p . emoji . ImageContentType = contentType
2022-01-23 14:41:58 +01:00
p . emoji . ImageFileSize = fileSize
2022-01-11 17:49:14 +01:00
2022-01-16 18:52:55 +01:00
// concatenate the first bytes with the existing bytes still in the reader (thanks Mara)
multiReader := io . MultiReader ( bytes . NewBuffer ( firstBytes ) , reader )
// store this for now -- other processes can pull it out of storage as they please
if err := p . storage . PutStream ( p . emoji . ImagePath , multiReader ) ; err != nil {
return fmt . Errorf ( "store: error storing stream: %s" , err )
}
// if the original reader is a readcloser, close it since we're done with it now
if rc , ok := reader . ( io . ReadCloser ) ; ok {
if err := rc . Close ( ) ; err != nil {
return fmt . Errorf ( "store: error closing readcloser: %s" , err )
}
}
p . read = true
2022-01-11 17:49:14 +01:00
return nil
}
2022-01-15 14:33:58 +01:00
func ( m * manager ) preProcessEmoji ( ctx context . Context , data DataFunc , shortcode string , id string , uri string , ai * AdditionalEmojiInfo ) ( * ProcessingEmoji , error ) {
2022-01-11 17:49:14 +01: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 )
}
// populate initial fields on the emoji -- some of these will be overwritten as we proceed
emoji := & gtsmodel . Emoji {
ID : id ,
CreatedAt : time . Now ( ) ,
UpdatedAt : 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 ) , id , 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 , id , 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 ,
ImageUpdatedAt : time . Now ( ) ,
Disabled : false ,
2022-01-15 14:33:58 +01:00
URI : uri ,
2022-01-11 17:49:14 +01:00
VisibleInPicker : true ,
CategoryID : "" ,
}
// 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 14:33:58 +01:00
emoji . CreatedAt = * ai . CreatedAt
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . Domain != nil {
emoji . Domain = * ai . Domain
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . ImageRemoteURL != nil {
emoji . ImageRemoteURL = * ai . ImageRemoteURL
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . ImageStaticRemoteURL != nil {
emoji . ImageStaticRemoteURL = * ai . ImageStaticRemoteURL
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . Disabled != nil {
emoji . Disabled = * ai . Disabled
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . VisibleInPicker != nil {
emoji . VisibleInPicker = * ai . VisibleInPicker
2022-01-11 17:49:14 +01:00
}
2022-01-15 14:33:58 +01:00
if ai . CategoryID != nil {
emoji . CategoryID = * ai . CategoryID
2022-01-11 17:49:14 +01:00
}
}
processingEmoji := & ProcessingEmoji {
2022-01-11 17:51:45 +01:00
instanceAccountID : instanceAccount . ID ,
emoji : emoji ,
data : data ,
staticState : received ,
fullSizeState : received ,
database : m . db ,
storage : m . storage ,
2022-01-11 17:49:14 +01:00
}
return processingEmoji , nil
}