2021-07-05 13:23:03 +02:00
/ *
GoToSocial
2021-12-20 18:42:19 +01:00
Copyright ( C ) 2021 - 2022 GoToSocial Authors admin @ gotosocial . org
2021-07-05 13:23:03 +02:00
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-03-07 11:08:26 +01:00
"bufio"
2021-08-25 15:34:33 +02:00
"context"
2021-07-05 13:23:03 +02:00
"fmt"
2022-03-07 11:08:26 +01:00
"io"
"net/url"
2021-07-05 13:23:03 +02:00
"strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
2022-10-13 15:16:24 +02:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
2021-07-05 13:23:03 +02:00
)
2022-10-13 15:16:24 +02:00
func ( p * processor ) GetFile ( ctx context . Context , requestingAccount * gtsmodel . Account , form * apimodel . GetContentRequestForm ) ( * apimodel . Content , gtserror . WithCode ) {
2021-07-05 13:23:03 +02:00
// parse the form fields
mediaSize , err := media . ParseMediaSize ( form . MediaSize )
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media size %s not valid" , form . MediaSize ) )
}
mediaType , err := media . ParseMediaType ( form . MediaType )
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media type %s not valid" , form . MediaType ) )
}
spl := strings . Split ( form . FileName , "." )
if len ( spl ) != 2 || spl [ 0 ] == "" || spl [ 1 ] == "" {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "file name %s not parseable" , form . FileName ) )
}
wantedMediaID := spl [ 0 ]
2022-10-13 15:16:24 +02:00
owningAccountID := form . AccountID
2021-07-05 13:23:03 +02:00
// get the account that owns the media and make sure it's not suspended
2022-10-13 15:16:24 +02:00
owningAccount , err := p . db . GetAccountByID ( ctx , owningAccountID )
2021-08-25 15:34:33 +02:00
if err != nil {
2022-10-13 15:16:24 +02:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s could not be selected from the db: %s" , owningAccountID , err ) )
2021-07-05 13:23:03 +02:00
}
2022-10-13 15:16:24 +02:00
if ! owningAccount . SuspendedAt . IsZero ( ) {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s is suspended" , owningAccountID ) )
2021-07-05 13:23:03 +02:00
}
// make sure the requesting account and the media account don't block each other
2022-10-13 15:16:24 +02:00
if requestingAccount != nil {
blocked , err := p . db . IsBlocked ( ctx , requestingAccount . ID , owningAccountID , true )
2021-07-05 13:23:03 +02:00
if err != nil {
2022-10-13 15:16:24 +02:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "block status could not be established between accounts %s and %s: %s" , owningAccountID , requestingAccount . ID , err ) )
2021-07-05 13:23:03 +02:00
}
if blocked {
2022-10-13 15:16:24 +02:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "block exists between accounts %s and %s" , owningAccountID , requestingAccount . ID ) )
2021-07-05 13:23:03 +02:00
}
}
// the way we store emojis is a little different from the way we store other attachments,
// so we need to take different steps depending on the media type being requested
switch mediaType {
2021-12-20 15:19:53 +01:00
case media . TypeEmoji :
2022-10-13 15:16:24 +02:00
return p . getEmojiContent ( ctx , wantedMediaID , owningAccountID , mediaSize )
2021-12-20 15:19:53 +01:00
case media . TypeAttachment , media . TypeHeader , media . TypeAvatar :
2022-10-13 15:16:24 +02:00
return p . getAttachmentContent ( ctx , requestingAccount , wantedMediaID , owningAccountID , mediaSize )
2022-03-07 11:08:26 +01:00
default :
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media type %s not recognized" , mediaType ) )
}
}
2022-10-13 15:16:24 +02:00
func ( p * processor ) getAttachmentContent ( ctx context . Context , requestingAccount * gtsmodel . Account , wantedMediaID string , owningAccountID string , mediaSize media . Size ) ( * apimodel . Content , gtserror . WithCode ) {
2022-03-07 11:08:26 +01:00
attachmentContent := & apimodel . Content { }
var storagePath string
// retrieve attachment from the database and do basic checks on it
a , err := p . db . GetAttachmentByID ( ctx , wantedMediaID )
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "attachment %s could not be taken from the db: %s" , wantedMediaID , err ) )
}
2022-10-13 15:16:24 +02:00
if a . AccountID != owningAccountID {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "attachment %s is not owned by %s" , wantedMediaID , owningAccountID ) )
2022-03-07 11:08:26 +01:00
}
// get file information from the attachment depending on the requested media size
switch mediaSize {
case media . SizeOriginal :
attachmentContent . ContentType = a . File . ContentType
attachmentContent . ContentLength = int64 ( a . File . FileSize )
storagePath = a . File . Path
case media . SizeSmall :
attachmentContent . ContentType = a . Thumbnail . ContentType
attachmentContent . ContentLength = int64 ( a . Thumbnail . FileSize )
storagePath = a . Thumbnail . Path
default :
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media size %s not recognized for attachment" , mediaSize ) )
}
// if we have the media cached on our server already, we can now simply return it from storage
2022-08-15 12:35:05 +02:00
if * a . Cached {
2022-07-03 12:08:30 +02:00
return p . retrieveFromStorage ( ctx , storagePath , attachmentContent )
2022-03-07 11:08:26 +01:00
}
// if we don't have it cached, then we can assume two things:
// 1. this is remote media, since local media should never be uncached
// 2. we need to fetch it again using a transport and the media manager
remoteMediaIRI , err := url . Parse ( a . RemoteURL )
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error parsing remote media iri %s: %s" , a . RemoteURL , err ) )
}
// use an empty string as requestingUsername to use the instance account, unless the request for this
// media has been http signed, then use the requesting account to make the request to remote server
var requestingUsername string
if requestingAccount != nil {
requestingUsername = requestingAccount . Username
}
var data media . DataFunc
var postDataCallback media . PostDataCallbackFunc
if mediaSize == media . SizeSmall {
// if it's the thumbnail that's requested then the user will have to wait a bit while we process the
// large version and derive a thumbnail from it, so use the normal recaching procedure: fetch the media,
// process it, then return the thumbnail data
2022-09-29 22:50:43 +02:00
data = func ( innerCtx context . Context ) ( io . Reader , int64 , error ) {
2022-03-07 11:08:26 +01:00
transport , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
if err != nil {
return nil , 0 , err
}
return transport . DereferenceMedia ( innerCtx , remoteMediaIRI )
2021-07-05 13:23:03 +02:00
}
2022-03-07 11:08:26 +01:00
} else {
// if it's the full-sized version being requested, we can cheat a bit by streaming data to the user as
// it's retrieved from the remote server, using tee; this saves the user from having to wait while
// we process the media on our side
//
// this looks a bit like this:
//
// http fetch buffered pipe
// remote server ------------> data function ----------------> api caller
// |
// | tee
// |
// ▼
// instance storage
// Buffer each end of the pipe, so that if the caller drops the connection during the flow, the tee
// reader can continue without having to worry about tee-ing into a closed or blocked pipe.
pipeReader , pipeWriter := io . Pipe ( )
bufferedWriter := bufio . NewWriterSize ( pipeWriter , int ( attachmentContent . ContentLength ) )
bufferedReader := bufio . NewReaderSize ( pipeReader , int ( attachmentContent . ContentLength ) )
// the caller will read from the buffered reader, so it doesn't matter if they drop out without reading everything
attachmentContent . Content = bufferedReader
2022-09-29 22:50:43 +02:00
data = func ( innerCtx context . Context ) ( io . Reader , int64 , error ) {
2022-03-07 11:08:26 +01:00
transport , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
if err != nil {
return nil , 0 , err
}
readCloser , fileSize , err := transport . DereferenceMedia ( innerCtx , remoteMediaIRI )
if err != nil {
return nil , 0 , err
}
2022-03-21 13:41:44 +01:00
// Make a TeeReader so that everything read from the readCloser by the media manager will be written into the bufferedWriter.
// We wrap this in a teeReadCloser which implements io.ReadCloser, so that whoever uses the teeReader can close the readCloser
// when they're done with it.
trc := teeReadCloser {
teeReader : io . TeeReader ( readCloser , bufferedWriter ) ,
close : readCloser . Close ,
}
return trc , fileSize , nil
2021-07-05 13:23:03 +02:00
}
2022-03-07 11:08:26 +01:00
// close the pipewriter after data has been piped into it, so the reader on the other side doesn't block;
// we don't need to close the reader here because that's the caller's responsibility
postDataCallback = func ( innerCtx context . Context ) error {
// flush the buffered writer into the buffer of the reader...
if err := bufferedWriter . Flush ( ) ; err != nil {
return err
}
// and close the underlying pipe writer
if err := pipeWriter . Close ( ) ; err != nil {
return err
}
return nil
}
}
// put the media recached in the queue
processingMedia , err := p . mediaManager . RecacheMedia ( ctx , data , postDataCallback , wantedMediaID )
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error recaching media: %s" , err ) )
}
// if it's the thumbnail, stream the processed thumbnail from storage, after waiting for processing to finish
if mediaSize == media . SizeSmall {
// below function call blocks until all processing on the attachment has finished...
if _ , err := processingMedia . LoadAttachment ( ctx ) ; err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error loading recached attachment: %s" , err ) )
2021-07-05 13:23:03 +02:00
}
2022-03-07 11:08:26 +01:00
// ... so now we can safely return it
2022-07-03 12:08:30 +02:00
return p . retrieveFromStorage ( ctx , storagePath , attachmentContent )
2021-07-05 13:23:03 +02:00
}
2022-03-07 11:08:26 +01:00
return attachmentContent , nil
}
2022-10-13 15:16:24 +02:00
func ( p * processor ) getEmojiContent ( ctx context . Context , fileName string , owningAccountID string , emojiSize media . Size ) ( * apimodel . Content , gtserror . WithCode ) {
2022-03-07 11:08:26 +01:00
emojiContent := & apimodel . Content { }
var storagePath string
2022-10-13 15:16:24 +02:00
// reconstruct the static emoji image url -- reason
// for using the static URL rather than full size url
// is that static emojis are always encoded as png,
// so this is more reliable than using full size url
imageStaticURL := uris . GenerateURIForAttachment ( owningAccountID , string ( media . TypeEmoji ) , string ( media . SizeStatic ) , fileName , "png" )
e , err := p . db . GetEmojiByStaticURL ( ctx , imageStaticURL )
2022-09-06 12:42:55 +02:00
if err != nil {
2022-10-13 15:16:24 +02:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s could not be taken from the db: %s" , fileName , err ) )
2022-03-07 11:08:26 +01:00
}
2022-08-15 12:35:05 +02:00
if * e . Disabled {
2022-10-13 15:16:24 +02:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s has been disabled" , fileName ) )
2022-03-07 11:08:26 +01:00
}
switch emojiSize {
case media . SizeOriginal :
emojiContent . ContentType = e . ImageContentType
emojiContent . ContentLength = int64 ( e . ImageFileSize )
storagePath = e . ImagePath
case media . SizeStatic :
emojiContent . ContentType = e . ImageStaticContentType
emojiContent . ContentLength = int64 ( e . ImageStaticFileSize )
storagePath = e . ImageStaticPath
default :
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media size %s not recognized for emoji" , emojiSize ) )
}
2022-07-03 12:08:30 +02:00
return p . retrieveFromStorage ( ctx , storagePath , emojiContent )
2022-03-07 11:08:26 +01:00
}
2022-07-03 12:08:30 +02:00
func ( p * processor ) retrieveFromStorage ( ctx context . Context , storagePath string , content * apimodel . Content ) ( * apimodel . Content , gtserror . WithCode ) {
if url := p . storage . URL ( ctx , storagePath ) ; url != nil {
content . URL = url
return content , nil
}
reader , err := p . storage . GetStream ( ctx , storagePath )
2021-07-05 13:23:03 +02:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error retrieving from storage: %s" , err ) )
}
2022-02-19 11:44:56 +01:00
content . Content = reader
2021-07-05 13:23:03 +02:00
return content , nil
}