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 (
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"
2022-12-21 11:17:43 +01:00
"github.com/superseriousbusiness/gotosocial/internal/iotools"
2021-07-05 13:23:03 +02:00
"github.com/superseriousbusiness/gotosocial/internal/media"
2022-11-23 22:40:07 +01:00
"github.com/superseriousbusiness/gotosocial/internal/transport"
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
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-11-03 15:03:12 +01:00
data = func ( innerCtx context . Context ) ( io . ReadCloser , int64 , error ) {
2022-11-23 22:40:07 +01:00
t , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
2022-03-07 11:08:26 +01:00
if err != nil {
return nil , 0 , err
}
2022-11-23 22:40:07 +01:00
return t . DereferenceMedia ( transport . WithFastfail ( 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:
//
2022-12-21 11:17:43 +01:00
// http fetch pipe
2022-03-07 11:08:26 +01:00
// remote server ------------> data function ----------------> api caller
// |
// | tee
// |
// ▼
// instance storage
2022-12-21 11:17:43 +01:00
// This pipe will connect the caller to the in-process media retrieval...
2022-03-07 11:08:26 +01:00
pipeReader , pipeWriter := io . Pipe ( )
2022-12-21 11:17:43 +01:00
// Wrap the output pipe to silence any errors during the actual media
// streaming process. We catch the error later but they must be silenced
// during stream to prevent interruptions to storage of the actual media.
silencedWriter := iotools . SilenceWriter ( pipeWriter )
2022-03-07 11:08:26 +01:00
2022-12-21 11:17:43 +01:00
// Pass the reader side of the pipe to the caller to slurp from.
attachmentContent . Content = pipeReader
// Create a data function which injects the writer end of the pipe
// into the data retrieval process. If something goes wrong while
// doing the data retrieval, we hang up the underlying pipeReader
// to indicate to the caller that no data is available. It's up to
// the caller of this processor function to handle that gracefully.
2022-11-03 15:03:12 +01:00
data = func ( innerCtx context . Context ) ( io . ReadCloser , int64 , error ) {
2022-11-23 22:40:07 +01:00
t , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
2022-03-07 11:08:26 +01:00
if err != nil {
2022-12-21 11:17:43 +01:00
// propagate the transport error to read end of pipe.
_ = pipeWriter . CloseWithError ( fmt . Errorf ( "error getting transport for user: %w" , err ) )
2022-03-07 11:08:26 +01:00
return nil , 0 , err
}
2022-11-23 22:40:07 +01:00
readCloser , fileSize , err := t . DereferenceMedia ( transport . WithFastfail ( innerCtx ) , remoteMediaIRI )
2022-03-07 11:08:26 +01:00
if err != nil {
2022-12-21 11:17:43 +01:00
// propagate the dereference error to read end of pipe.
_ = pipeWriter . CloseWithError ( fmt . Errorf ( "error dereferencing media: %w" , err ) )
2022-03-07 11:08:26 +01:00
return nil , 0 , err
}
2022-12-21 11:17:43 +01:00
// Make a TeeReader so that everything read from the readCloser,
// aka the remote instance, will also be written into the pipe.
teeReader := io . TeeReader ( readCloser , silencedWriter )
// Wrap teereader to implement original readcloser's close,
// and also ensuring that we close the pipe from write end.
return iotools . ReadFnCloser ( teeReader , func ( ) error {
defer func ( ) {
// We use the error (if any) encountered by the
// silenced writer to close connection to make sure it
// gets propagated to the attachment.Content reader.
_ = pipeWriter . CloseWithError ( silencedWriter . Error ( ) )
} ( )
return readCloser . Close ( )
} ) , fileSize , nil
2022-03-07 11:08:26 +01:00
}
}
// put the media recached in the queue
2022-12-21 11:17:43 +01:00
processingMedia , err := p . mediaManager . RecacheMedia ( ctx , data , nil , wantedMediaID )
2022-03-07 11:08:26 +01:00
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
}