mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-29 04:22:24 -05:00 
			
		
		
		
	further refinements
This commit is contained in:
		
					parent
					
						
							
								7ebe0f6a15
							
						
					
				
			
			
				commit
				
					
						c2ff8f392b
					
				
			
		
					 4 changed files with 133 additions and 133 deletions
				
			
		|  | @ -20,22 +20,16 @@ package media | |||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"image/gif" | ||||
| 	"image/jpeg" | ||||
| 	"image/png" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/buckket/go-blurhash" | ||||
| 	"github.com/nfnt/resize" | ||||
| 	"github.com/superseriousbusiness/exifremove/pkg/exifremove" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -53,70 +47,6 @@ type ImageMeta struct { | |||
| 	blurhash    string | ||||
| } | ||||
| 
 | ||||
| func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string) (*Media, error) { | ||||
| 	if !supportedImage(contentType) { | ||||
| 		return nil, fmt.Errorf("image type %s not supported", contentType) | ||||
| 	} | ||||
| 	 | ||||
| 	if len(data) == 0 { | ||||
| 		return nil, errors.New("image was of size 0") | ||||
| 	} | ||||
| 	 | ||||
| 	id, err := id.NewRandomULID() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	extension := strings.Split(contentType, "/")[1] | ||||
| 
 | ||||
| 	attachment := >smodel.MediaAttachment{ | ||||
| 		ID:         id, | ||||
| 		UpdatedAt:  time.Now(), | ||||
| 		URL:        uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeOriginal), id, extension), | ||||
| 		Type:       gtsmodel.FileTypeImage, | ||||
| 		AccountID:  accountID, | ||||
| 		Processing: 0, | ||||
| 		File: gtsmodel.File{ | ||||
| 			Path:        fmt.Sprintf("%s/%s/%s/%s.%s", accountID, TypeAttachment, SizeOriginal, id, extension), | ||||
| 			ContentType: contentType, | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 		}, | ||||
| 		Thumbnail: gtsmodel.Thumbnail{ | ||||
| 			URL:         uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeSmall), id, mimeJpeg), // all thumbnails are encoded as jpeg, | ||||
| 			Path:        fmt.Sprintf("%s/%s/%s/%s.%s", accountID, TypeAttachment, SizeSmall, id, mimeJpeg),                 // all thumbnails are encoded as jpeg, | ||||
| 			ContentType: mimeJpeg, | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 		}, | ||||
| 		Avatar: false, | ||||
| 		Header: false, | ||||
| 	} | ||||
| 
 | ||||
| 	media := &Media{ | ||||
| 		attachment: attachment, | ||||
| 	} | ||||
| 
 | ||||
| 	return media, nil | ||||
| 
 | ||||
| 	var clean []byte | ||||
| 	var original *ImageMeta | ||||
| 	var small *ImageMeta | ||||
| 
 | ||||
| 	 | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	small, err = deriveThumbnail(clean, contentType) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error deriving thumbnail: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// now put it in storage, take a new id for the name of the file so we don't store any unnecessary info about it | ||||
| 
 | ||||
| 	return attachment, nil | ||||
| } | ||||
| 
 | ||||
| func decodeGif(b []byte) (*ImageMeta, error) { | ||||
| 	gif, err := gif.DecodeAll(bytes.NewReader(b)) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -24,11 +24,15 @@ import ( | |||
| 	"fmt" | ||||
| 	"runtime" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"codeberg.org/gruf/go-runners" | ||||
| 	"codeberg.org/gruf/go-store/kv" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||
| ) | ||||
| 
 | ||||
| // Manager provides an interface for managing media: parsing, storing, and retrieving media objects like photos, videos, and gifs. | ||||
|  | @ -92,6 +96,7 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin | |||
| 				// if the inner context is done that means the worker pool is closing, so we should just return | ||||
| 				return | ||||
| 			default: | ||||
| 				// start preloading the media for the caller's convenience | ||||
| 				media.PreLoad(innerCtx) | ||||
| 			} | ||||
| 		}) | ||||
|  | @ -101,3 +106,54 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin | |||
| 		return nil, fmt.Errorf("content type %s not (yet) supported", contentType) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // preProcessImage initializes processing | ||||
| func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string) (*Media, error) { | ||||
| 	if !supportedImage(contentType) { | ||||
| 		return nil, fmt.Errorf("image type %s not supported", contentType) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(data) == 0 { | ||||
| 		return nil, errors.New("image was of size 0") | ||||
| 	} | ||||
| 
 | ||||
| 	id, err := id.NewRandomULID() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	extension := strings.Split(contentType, "/")[1] | ||||
| 
 | ||||
| 	attachment := >smodel.MediaAttachment{ | ||||
| 		ID:         id, | ||||
| 		UpdatedAt:  time.Now(), | ||||
| 		URL:        uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeOriginal), id, extension), | ||||
| 		Type:       gtsmodel.FileTypeImage, | ||||
| 		AccountID:  accountID, | ||||
| 		Processing: 0, | ||||
| 		File: gtsmodel.File{ | ||||
| 			Path:        fmt.Sprintf("%s/%s/%s/%s.%s", accountID, TypeAttachment, SizeOriginal, id, extension), | ||||
| 			ContentType: contentType, | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 		}, | ||||
| 		Thumbnail: gtsmodel.Thumbnail{ | ||||
| 			URL:         uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeSmall), id, mimeJpeg), // all thumbnails are encoded as jpeg, | ||||
| 			Path:        fmt.Sprintf("%s/%s/%s/%s.%s", accountID, TypeAttachment, SizeSmall, id, mimeJpeg),                 // all thumbnails are encoded as jpeg, | ||||
| 			ContentType: mimeJpeg, | ||||
| 			UpdatedAt:   time.Now(), | ||||
| 		}, | ||||
| 		Avatar: false, | ||||
| 		Header: false, | ||||
| 	} | ||||
| 
 | ||||
| 	media := &Media{ | ||||
| 		attachment:    attachment, | ||||
| 		rawData:       data, | ||||
| 		thumbstate:    received, | ||||
| 		fullSizeState: received, | ||||
| 		database:      m.db, | ||||
| 		storage:       m.storage, | ||||
| 	} | ||||
| 
 | ||||
| 	return media, nil | ||||
| } | ||||
|  |  | |||
|  | @ -33,15 +33,15 @@ type Media struct { | |||
| 		below fields represent the processing state of the media thumbnail | ||||
| 	*/ | ||||
| 
 | ||||
| 	thumbing processState | ||||
| 	thumb    *ImageMeta | ||||
| 	thumbstate processState | ||||
| 	thumb      *ImageMeta | ||||
| 
 | ||||
| 	/* | ||||
| 		below fields represent the processing state of the full-sized media | ||||
| 	*/ | ||||
| 
 | ||||
| 	processing processState | ||||
| 	processed  *ImageMeta | ||||
| 	fullSizeState processState | ||||
| 	fullSize      *ImageMeta | ||||
| 
 | ||||
| 	/* | ||||
| 		below pointers to database and storage are maintained so that | ||||
|  | @ -58,20 +58,20 @@ func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) { | |||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| 
 | ||||
| 	switch m.thumbing { | ||||
| 	switch m.thumbstate { | ||||
| 	case received: | ||||
| 		// we haven't processed a thumbnail for this media yet so do it now | ||||
| 		thumb, err := deriveThumbnail(m.rawData, m.attachment.File.ContentType) | ||||
| 		if err != nil { | ||||
| 			m.err = fmt.Errorf("error deriving thumbnail: %s", err) | ||||
| 			m.thumbing = errored | ||||
| 			m.thumbstate = errored | ||||
| 			return nil, m.err | ||||
| 		} | ||||
| 
 | ||||
| 		// put the thumbnail in storage | ||||
| 		if err := m.storage.Put(m.attachment.Thumbnail.Path, thumb.image); err != nil { | ||||
| 			m.err = fmt.Errorf("error storing thumbnail: %s", err) | ||||
| 			m.thumbing = errored | ||||
| 			m.thumbstate = errored | ||||
| 			return nil, m.err | ||||
| 		} | ||||
| 
 | ||||
|  | @ -89,12 +89,12 @@ func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) { | |||
| 		if err := m.database.Put(ctx, m.attachment); err != nil { | ||||
| 			if err != db.ErrAlreadyExists { | ||||
| 				m.err = fmt.Errorf("error putting attachment: %s", err) | ||||
| 				m.thumbing = errored | ||||
| 				m.thumbstate = errored | ||||
| 				return nil, m.err | ||||
| 			} | ||||
| 			if err := m.database.UpdateByPrimaryKey(ctx, m.attachment); err != nil { | ||||
| 				m.err = fmt.Errorf("error updating attachment: %s", err) | ||||
| 				m.thumbing = errored | ||||
| 				m.thumbstate = errored | ||||
| 				return nil, m.err | ||||
| 			} | ||||
| 		} | ||||
|  | @ -103,7 +103,7 @@ func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) { | |||
| 		m.thumb = thumb | ||||
| 
 | ||||
| 		// we're done processing the thumbnail! | ||||
| 		m.thumbing = complete | ||||
| 		m.thumbstate = complete | ||||
| 		fallthrough | ||||
| 	case complete: | ||||
| 		return m.thumb, nil | ||||
|  | @ -111,46 +111,76 @@ func (m *Media) Thumb(ctx context.Context) (*ImageMeta, error) { | |||
| 		return nil, m.err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, fmt.Errorf("thumbnail processing status %d unknown", m.thumbing) | ||||
| 	return nil, fmt.Errorf("thumbnail processing status %d unknown", m.thumbstate) | ||||
| } | ||||
| 
 | ||||
| func (m *Media) Full(ctx context.Context) (*ImageMeta, error) { | ||||
| 	var clean []byte | ||||
| 	var err error | ||||
| 	var original *ImageMeta | ||||
| func (m *Media) FullSize(ctx context.Context) (*ImageMeta, error) { | ||||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| 
 | ||||
| 	ct := m.attachment.File.ContentType | ||||
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ||||
| 	switch ct { | ||||
| 	case mimeImageJpeg, mimeImagePng: | ||||
| 		// first 'clean' image by purging exif data from it | ||||
| 		var exifErr error | ||||
| 		if clean, exifErr = purgeExif(m.rawData); exifErr != nil { | ||||
| 			return nil, fmt.Errorf("error cleaning exif data: %s", exifErr) | ||||
| 	switch m.fullSizeState { | ||||
| 	case received: | ||||
| 		var clean []byte | ||||
| 		var err error | ||||
| 		var decoded *ImageMeta | ||||
| 
 | ||||
| 		ct := m.attachment.File.ContentType | ||||
| 		switch ct { | ||||
| 		case mimeImageJpeg, mimeImagePng: | ||||
| 			// first 'clean' image by purging exif data from it | ||||
| 			var exifErr error | ||||
| 			if clean, exifErr = purgeExif(m.rawData); exifErr != nil { | ||||
| 				err = exifErr | ||||
| 				break | ||||
| 			} | ||||
| 			decoded, err = decodeImage(clean, ct) | ||||
| 		case mimeImageGif: | ||||
| 			// gifs are already clean - no exif data to remove | ||||
| 			clean = m.rawData | ||||
| 			decoded, err = decodeGif(clean) | ||||
| 		default: | ||||
| 			err = fmt.Errorf("content type %s not a processible image type", ct) | ||||
| 		} | ||||
| 		original, err = decodeImage(clean, ct) | ||||
| 	case mimeImageGif: | ||||
| 		// gifs are already clean - no exif data to remove | ||||
| 		clean = m.rawData | ||||
| 		original, err = decodeGif(clean) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("content type %s not a processible image type", ct) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			m.err = err | ||||
| 			m.fullSizeState = errored | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		// set the fullsize of this media | ||||
| 		m.fullSize = decoded | ||||
| 
 | ||||
| 		// we're done processing the full-size image | ||||
| 		m.fullSizeState = complete | ||||
| 		fallthrough | ||||
| 	case complete: | ||||
| 		return m.fullSize, nil | ||||
| 	case errored: | ||||
| 		return nil, m.err | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 	return nil, fmt.Errorf("full size processing status %d unknown", m.fullSizeState) | ||||
| } | ||||
| 
 | ||||
| // PreLoad begins the process of deriving the thumbnail and encoding the full-size image. | ||||
| // It does this in a non-blocking way, so you can call it and then come back later and check | ||||
| // if it's finished. | ||||
| func (m *Media) PreLoad(ctx context.Context) { | ||||
| 	go m.Thumb(ctx) | ||||
| 	go m.FullSize(ctx) | ||||
| } | ||||
| 
 | ||||
| // Load is the blocking equivalent of pre-load. It makes sure the thumbnail and full-size image | ||||
| // have been processed, then it returns the full-size image. | ||||
| func (m *Media) Load(ctx context.Context) (*gtsmodel.MediaAttachment, error) { | ||||
| 	if _, err := m.Thumb(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return original, nil | ||||
| } | ||||
| 	if _, err := m.FullSize(ctx); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| func (m *Media) PreLoad(ctx context.Context) { | ||||
| 	go m.Thumb(ctx) | ||||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| } | ||||
| 
 | ||||
| func (m *Media) Load() { | ||||
| 	m.mu.Lock() | ||||
| 	defer m.mu.Unlock() | ||||
| 	return m.attachment, nil | ||||
| } | ||||
|  |  | |||
|  | @ -24,11 +24,9 @@ import ( | |||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"time" | ||||
| 
 | ||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/superseriousbusiness/gotosocial/internal/text" | ||||
| ) | ||||
| 
 | ||||
| func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) { | ||||
|  | @ -46,29 +44,15 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form | |||
| 		return nil, errors.New("could not read provided attachment: size 0 bytes") | ||||
| 	} | ||||
| 
 | ||||
| 	// now parse the focus parameter | ||||
| 	focusx, focusy, err := parseFocus(form.Focus) | ||||
| 	// process the media and load it immediately | ||||
| 	media, err := p.mediaManager.ProcessMedia(ctx, buf.Bytes(), account.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("couldn't parse attachment focus: %s", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	minAttachment := >smodel.MediaAttachment{ | ||||
| 		CreatedAt:   time.Now(), | ||||
| 		UpdatedAt:   time.Now(), | ||||
| 		AccountID:   account.ID, | ||||
| 		Description: text.SanitizeCaption(form.Description), | ||||
| 		FileMeta: gtsmodel.FileMeta{ | ||||
| 			Focus: gtsmodel.Focus{ | ||||
| 				X: focusx, | ||||
| 				Y: focusy, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// allow the mediaManager to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using | ||||
| 	attachment, err := p.mediaManager.ProcessAttachment(ctx, buf.Bytes(), minAttachment) | ||||
| 	attachment, err := media.Load(ctx) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error reading attachment: %s", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// prepare the frontend representation now -- if there are any errors here at least we can bail without | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue