2023-03-12 18:00:57 +03:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// 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/>.
2021-07-05 14:23:03 +03:00
package media
import (
2021-08-25 16:34:33 +03:00
"context"
2021-07-05 14:23:03 +03:00
"fmt"
2022-03-07 13:08:26 +03:00
"io"
"net/url"
2021-07-05 14:23:03 +03: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-24 00:40:07 +03:00
"github.com/superseriousbusiness/gotosocial/internal/transport"
2022-10-13 16:16:24 +03:00
"github.com/superseriousbusiness/gotosocial/internal/uris"
2021-07-05 14:23:03 +03:00
)
2023-02-22 18:05:26 +03: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 14:23:03 +03:00
// parse the form fields
2023-02-22 18:05:26 +03:00
mediaSize , err := parseSize ( form . MediaSize )
2021-07-05 14:23:03 +03:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media size %s not valid" , form . MediaSize ) )
}
2023-02-22 18:05:26 +03:00
mediaType , err := parseType ( form . MediaType )
2021-07-05 14:23:03 +03: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 16:16:24 +03:00
owningAccountID := form . AccountID
2021-07-05 14:23:03 +03:00
// get the account that owns the media and make sure it's not suspended
2023-03-01 21:26:53 +03:00
owningAccount , err := p . state . DB . GetAccountByID ( ctx , owningAccountID )
2021-08-25 16:34:33 +03:00
if err != nil {
2022-10-13 16:16:24 +03:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s could not be selected from the db: %s" , owningAccountID , err ) )
2021-07-05 14:23:03 +03:00
}
2022-10-13 16:16:24 +03:00
if ! owningAccount . SuspendedAt . IsZero ( ) {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "account with id %s is suspended" , owningAccountID ) )
2021-07-05 14:23:03 +03:00
}
// make sure the requesting account and the media account don't block each other
2022-10-13 16:16:24 +03:00
if requestingAccount != nil {
2023-03-01 21:26:53 +03:00
blocked , err := p . state . DB . IsBlocked ( ctx , requestingAccount . ID , owningAccountID , true )
2021-07-05 14:23:03 +03:00
if err != nil {
2022-10-13 16:16:24 +03: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 14:23:03 +03:00
}
if blocked {
2022-10-13 16:16:24 +03:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "block exists between accounts %s and %s" , owningAccountID , requestingAccount . ID ) )
2021-07-05 14:23:03 +03: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 17:19:53 +03:00
case media . TypeEmoji :
2022-10-13 16:16:24 +03:00
return p . getEmojiContent ( ctx , wantedMediaID , owningAccountID , mediaSize )
2021-12-20 17:19:53 +03:00
case media . TypeAttachment , media . TypeHeader , media . TypeAvatar :
2022-10-13 16:16:24 +03:00
return p . getAttachmentContent ( ctx , requestingAccount , wantedMediaID , owningAccountID , mediaSize )
2022-03-07 13:08:26 +03:00
default :
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "media type %s not recognized" , mediaType ) )
}
}
2023-02-22 18:05:26 +03: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 13:08:26 +03:00
// retrieve attachment from the database and do basic checks on it
2023-03-01 21:26:53 +03:00
a , err := p . state . DB . GetAttachmentByID ( ctx , wantedMediaID )
2022-03-07 13:08:26 +03:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "attachment %s could not be taken from the db: %s" , wantedMediaID , err ) )
}
2022-10-13 16:16:24 +03:00
if a . AccountID != owningAccountID {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "attachment %s is not owned by %s" , wantedMediaID , owningAccountID ) )
2022-03-07 13:08:26 +03:00
}
2023-01-11 14:13:13 +03: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 13:08:26 +03:00
2023-01-11 14:13:13 +03: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 13:08:26 +03:00
2023-01-11 14:13:13 +03: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-24 00:40:07 +03:00
t , err := p . transportController . NewTransportForUsername ( innerCtx , requestingUsername )
2022-03-07 13:08:26 +03:00
if err != nil {
return nil , 0 , err
}
2022-11-24 00:40:07 +03:00
return t . DereferenceMedia ( transport . WithFastfail ( innerCtx ) , remoteMediaIRI )
2021-07-05 14:23:03 +03:00
}
2022-03-07 13:08:26 +03:00
2023-01-11 14:13:13 +03:00
// Start recaching this media with the prepared data function.
2023-02-13 21:40:48 +03:00
processingMedia , err := p . mediaManager . PreProcessMediaRecache ( ctx , dataFn , nil , wantedMediaID )
2023-01-11 14:13:13 +03:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error recaching media: %s" , err ) )
2022-03-07 13:08:26 +03:00
}
2023-01-11 14:13:13 +03:00
// Load attachment and block until complete
a , err = processingMedia . LoadAttachment ( ctx )
if err != nil {
2022-03-07 13:08:26 +03:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error loading recached attachment: %s" , err ) )
2021-07-05 14:23:03 +03:00
}
}
2023-01-16 18:19:17 +03:00
var (
storagePath string
attachmentContent = & apimodel . Content {
ContentUpdated : a . UpdatedAt ,
}
)
2023-01-11 14:13:13 +03: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 13:08:26 +03:00
}
2023-02-22 18:05:26 +03:00
func ( p * Processor ) getEmojiContent ( ctx context . Context , fileName string , owningAccountID string , emojiSize media . Size ) ( * apimodel . Content , gtserror . WithCode ) {
2022-03-07 13:08:26 +03:00
emojiContent := & apimodel . Content { }
var storagePath string
2022-10-13 16:16:24 +03: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" )
2023-03-01 21:26:53 +03:00
e , err := p . state . DB . GetEmojiByStaticURL ( ctx , imageStaticURL )
2022-09-06 13:42:55 +03:00
if err != nil {
2022-10-13 16:16:24 +03:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s could not be taken from the db: %s" , fileName , err ) )
2022-03-07 13:08:26 +03:00
}
2022-08-15 13:35:05 +03:00
if * e . Disabled {
2022-10-13 16:16:24 +03:00
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "emoji %s has been disabled" , fileName ) )
2022-03-07 13:08:26 +03: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 13:08:30 +03:00
return p . retrieveFromStorage ( ctx , storagePath , emojiContent )
2022-03-07 13:08:26 +03:00
}
2023-02-22 18:05:26 +03:00
func ( p * Processor ) retrieveFromStorage ( ctx context . Context , storagePath string , content * apimodel . Content ) ( * apimodel . Content , gtserror . WithCode ) {
2023-02-12 16:42:28 +03:00
// If running on S3 storage with proxying disabled then
// just fetch a pre-signed URL instead of serving the content.
2023-03-01 21:26:53 +03:00
if url := p . state . Storage . URL ( ctx , storagePath ) ; url != nil {
2022-07-03 13:08:30 +03:00
content . URL = url
return content , nil
}
2023-02-12 16:42:28 +03:00
2023-03-01 21:26:53 +03:00
reader , err := p . state . Storage . GetStream ( ctx , storagePath )
2021-07-05 14:23:03 +03:00
if err != nil {
return nil , gtserror . NewErrorNotFound ( fmt . Errorf ( "error retrieving from storage: %s" , err ) )
}
2022-02-19 13:44:56 +03:00
content . Content = reader
2021-07-05 14:23:03 +03:00
return content , nil
}