2021-07-05 11:23:03 +00:00
/ *
GoToSocial
2023-01-05 11:43:00 +00:00
Copyright ( C ) 2021 - 2023 GoToSocial Authors admin @ gotosocial . org
2021-07-05 11:23:03 +00: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 13:34:33 +00:00
"context"
2021-07-05 11:23:03 +00:00
"fmt"
2022-03-07 10:08:26 +00:00
"io"
"net/url"
2021-07-05 11:23:03 +00: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-11-23 21:40:07 +00:00
"github.com/superseriousbusiness/gotosocial/internal/transport"
2022-10-13 13:16:24 +00:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
2021-07-05 11:23:03 +00:00
)
2023-02-22 15:05:26 +00:00
// GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content.
func ( p * Processor ) GetFile ( ctx context . Context , requestingAccount * gtsmodel . Account , form * apimodel . GetContentRequestForm ) ( * apimodel . Content , gtserror . WithCode ) {
2021-07-05 11:23:03 +00:00
// parse the form fields
2023-02-22 15:05:26 +00:00
mediaSize , err := parseSize ( form . MediaSize )
2021-07-05 11:23:03 +00:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media size %s not valid" , form . MediaSize ) )
}
2023-02-22 15:05:26 +00:00
mediaType , err := parseType ( form . MediaType )
2021-07-05 11:23:03 +00:00
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 13:16:24 +00:00
owningAccountID := form . AccountID
2021-07-05 11:23:03 +00:00
// get the account that owns the media and make sure it's not suspended
2022-10-13 13:16:24 +00:00
owningAccount , err := p . db . GetAccountByID ( ctx , owningAccountID )
2021-08-25 13:34:33 +00:00
if err != nil {
2022-10-13 13:16:24 +00:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s could not be selected from the db: %s" , owningAccountID , err ) )
2021-07-05 11:23:03 +00:00
}
2022-10-13 13:16:24 +00:00
if ! owningAccount . SuspendedAt . IsZero ( ) {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s is suspended" , owningAccountID ) )
2021-07-05 11:23:03 +00:00
}
// make sure the requesting account and the media account don't block each other
2022-10-13 13:16:24 +00:00
if requestingAccount != nil {
blocked , err := p . db . IsBlocked ( ctx , requestingAccount . ID , owningAccountID , true )
2021-07-05 11:23:03 +00:00
if err != nil {
2022-10-13 13:16:24 +00: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 11:23:03 +00:00
}
if blocked {
2022-10-13 13:16:24 +00:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "block exists between accounts %s and %s" , owningAccountID , requestingAccount . ID ) )
2021-07-05 11:23:03 +00: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 14:19:53 +00:00
case media . TypeEmoji :
2022-10-13 13:16:24 +00:00
return p . getEmojiContent ( ctx , wantedMediaID , owningAccountID , mediaSize )
2021-12-20 14:19:53 +00:00
case media . TypeAttachment , media . TypeHeader , media . TypeAvatar :
2022-10-13 13:16:24 +00:00
return p . getAttachmentContent ( ctx , requestingAccount , wantedMediaID , owningAccountID , mediaSize )
2022-03-07 10:08:26 +00:00
default :
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media type %s not recognized" , mediaType ) )
}
}
2023-02-22 15:05:26 +00:00
/ *
UTIL FUNCTIONS
* /
func parseType ( s string ) ( media . Type , error ) {
switch s {
case string ( media . TypeAttachment ) :
return media . TypeAttachment , nil
case string ( media . TypeHeader ) :
return media . TypeHeader , nil
case string ( media . TypeAvatar ) :
return media . TypeAvatar , nil
case string ( media . TypeEmoji ) :
return media . TypeEmoji , nil
}
return "" , fmt . Errorf ( "%s not a recognized media.Type" , s )
}
func parseSize ( s string ) ( media . Size , error ) {
switch s {
case string ( media . SizeSmall ) :
return media . SizeSmall , nil
case string ( media . SizeOriginal ) :
return media . SizeOriginal , nil
case string ( media . SizeStatic ) :
return media . SizeStatic , nil
}
return "" , fmt . Errorf ( "%s not a recognized media.Size" , s )
}
func ( p * Processor ) getAttachmentContent ( ctx context . Context , requestingAccount * gtsmodel . Account , wantedMediaID string , owningAccountID string , mediaSize media . Size ) ( * apimodel . Content , gtserror . WithCode ) {
2022-03-07 10:08:26 +00:00
// 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 13:16:24 +00:00
if a . AccountID != owningAccountID {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "attachment %s is not owned by %s" , wantedMediaID , owningAccountID ) )
2022-03-07 10:08:26 +00:00
}
2023-01-11 11:13:13 +00:00
if ! * a . Cached {
// 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 ) )
}
2022-03-07 10:08:26 +00:00
2023-01-11 11:13:13 +00:00
// 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
}
2022-03-07 10:08:26 +00:00
2023-01-11 11:13:13 +00:00
// Pour one out for tobi's original streamed recache
// (streaming data both to the client and storage).
// Gone and forever missed <3
//
// [
// the reason it was removed was because a slow
// client connection could hold open a storage
// recache operation, and so holding open a media
// worker worker.
// ]
dataFn := func ( innerCtx context . Context ) ( io . ReadCloser , int64 , error ) {
2022-11-23 21:40:07 +00:00
t , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
2022-03-07 10:08:26 +00:00
if err != nil {
return nil , 0 , err
}
2022-11-23 21:40:07 +00:00
return t . DereferenceMedia ( transport . WithFastfail ( innerCtx ) , remoteMediaIRI )
2021-07-05 11:23:03 +00:00
}
2022-03-07 10:08:26 +00:00
2023-01-11 11:13:13 +00:00
// Start recaching this media with the prepared data function.
2023-02-13 18:40:48 +00:00
processingMedia , err := p . mediaManager . PreProcessMediaRecache ( ctx , dataFn , nil , wantedMediaID )
2023-01-11 11:13:13 +00:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error recaching media: %s" , err ) )
2022-03-07 10:08:26 +00:00
}
2023-01-11 11:13:13 +00:00
// Load attachment and block until complete
a , err = processingMedia . LoadAttachment ( ctx )
if err != nil {
2022-03-07 10:08:26 +00:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error loading recached attachment: %s" , err ) )
2021-07-05 11:23:03 +00:00
}
}
2023-01-16 15:19:17 +00:00
var (
storagePath string
attachmentContent = & apimodel . Content {
ContentUpdated : a . UpdatedAt ,
}
)
2023-01-11 11:13:13 +00: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 ) )
}
// ... so now we can safely return it
return p . retrieveFromStorage ( ctx , storagePath , attachmentContent )
2022-03-07 10:08:26 +00:00
}
2023-02-22 15:05:26 +00:00
func ( p * Processor ) getEmojiContent ( ctx context . Context , fileName string , owningAccountID string , emojiSize media . Size ) ( * apimodel . Content , gtserror . WithCode ) {
2022-03-07 10:08:26 +00:00
emojiContent := & apimodel . Content { }
var storagePath string
2022-10-13 13:16:24 +00: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 10:42:55 +00:00
if err != nil {
2022-10-13 13:16:24 +00:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s could not be taken from the db: %s" , fileName , err ) )
2022-03-07 10:08:26 +00:00
}
2022-08-15 10:35:05 +00:00
if * e . Disabled {
2022-10-13 13:16:24 +00:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s has been disabled" , fileName ) )
2022-03-07 10:08:26 +00: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 10:08:30 +00:00
return p . retrieveFromStorage ( ctx , storagePath , emojiContent )
2022-03-07 10:08:26 +00:00
}
2023-02-22 15:05:26 +00:00
func ( p * Processor ) retrieveFromStorage ( ctx context . Context , storagePath string , content * apimodel . Content ) ( * apimodel . Content , gtserror . WithCode ) {
2023-02-12 13:42:28 +00:00
// If running on S3 storage with proxying disabled then
// just fetch a pre-signed URL instead of serving the content.
2022-07-03 10:08:30 +00:00
if url := p . storage . URL ( ctx , storagePath ) ; url != nil {
content . URL = url
return content , nil
}
2023-02-12 13:42:28 +00:00
2022-07-03 10:08:30 +00:00
reader , err := p . storage . GetStream ( ctx , storagePath )
2021-07-05 11:23:03 +00:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error retrieving from storage: %s" , err ) )
}
2022-02-19 10:44:56 +00:00
content . Content = reader
2021-07-05 11:23:03 +00:00
return content , nil
}