mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 03:22:24 -05:00 
			
		
		
		
	return very partial image on first upload
This commit is contained in:
		
					parent
					
						
							
								e08c0e55ee
							
						
					
				
			
			
				commit
				
					
						8abfa7751a
					
				
			
		
					 5 changed files with 109 additions and 165 deletions
				
			
		|  | @ -43,8 +43,9 @@ const ( | ||||||
| 	thumbnailMaxHeight = 512 | 	thumbnailMaxHeight = 512 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type imageAndMeta struct { | type ImageMeta struct { | ||||||
| 	image       []byte | 	image       []byte | ||||||
|  | 	contentType string | ||||||
| 	width       int | 	width       int | ||||||
| 	height      int | 	height      int | ||||||
| 	size        int | 	size        int | ||||||
|  | @ -52,12 +53,45 @@ type imageAndMeta struct { | ||||||
| 	blurhash    string | 	blurhash    string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *manager) processImage(ctx context.Context, data []byte, contentType string, accountID string) { | func (m *manager) preProcessImage(ctx context.Context, data []byte, contentType string, accountID string) (*Media, error) { | ||||||
|  | 	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 clean []byte | ||||||
| 	var err error | 	var original *ImageMeta | ||||||
| 	var original *imageAndMeta | 	var small *ImageMeta | ||||||
| 	var small *imageAndMeta |  | ||||||
| 
 | 
 | ||||||
| 	switch contentType { | 	switch contentType { | ||||||
| 	case mimeImageJpeg, mimeImagePng: | 	case mimeImageJpeg, mimeImagePng: | ||||||
|  | @ -79,82 +113,17 @@ func (m *manager) processImage(ctx context.Context, data []byte, contentType str | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	small, err = deriveThumbnail(clean, contentType, thumbnailMaxWidth, thumbnailMaxHeight) | 	small, err = deriveThumbnail(clean, contentType) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("error deriving thumbnail: %s", err) | 		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 | 	// 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 | ||||||
| 	extension := strings.Split(contentType, "/")[1] |  | ||||||
| 	attachmentID, err := id.NewRandomULID() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	originalURL := uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeOriginal), attachmentID, extension) |  | ||||||
| 	smallURL := uris.GenerateURIForAttachment(accountID, string(TypeAttachment), string(SizeSmall), attachmentID, "jpeg") // all thumbnails/smalls are encoded as jpeg |  | ||||||
| 
 |  | ||||||
| 	// we store the original... |  | ||||||
| 	originalPath := fmt.Sprintf("%s/%s/%s/%s.%s", accountID, TypeAttachment, SizeOriginal, attachmentID, extension) |  | ||||||
| 	if err := m.storage.Put(originalPath, original.image); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// and a thumbnail... |  | ||||||
| 	smallPath := fmt.Sprintf("%s/%s/%s/%s.jpeg", accountID, TypeAttachment, SizeSmall, attachmentID) // all thumbnails/smalls are encoded as jpeg |  | ||||||
| 	if err := m.storage.Put(smallPath, small.image); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("storage error: %s", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	attachment := >smodel.MediaAttachment{ |  | ||||||
| 		ID:        attachmentID, |  | ||||||
| 		StatusID:  "", |  | ||||||
| 		URL:       originalURL, |  | ||||||
| 		RemoteURL: "", |  | ||||||
| 		CreatedAt: time.Time{}, |  | ||||||
| 		UpdatedAt: time.Time{}, |  | ||||||
| 		Type:      gtsmodel.FileTypeImage, |  | ||||||
| 		FileMeta: gtsmodel.FileMeta{ |  | ||||||
| 			Original: gtsmodel.Original{ |  | ||||||
| 				Width:  original.width, |  | ||||||
| 				Height: original.height, |  | ||||||
| 				Size:   original.size, |  | ||||||
| 				Aspect: original.aspect, |  | ||||||
| 			}, |  | ||||||
| 			Small: gtsmodel.Small{ |  | ||||||
| 				Width:  small.width, |  | ||||||
| 				Height: small.height, |  | ||||||
| 				Size:   small.size, |  | ||||||
| 				Aspect: small.aspect, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		AccountID:         accountID, |  | ||||||
| 		Description:       "", |  | ||||||
| 		ScheduledStatusID: "", |  | ||||||
| 		Blurhash:          small.blurhash, |  | ||||||
| 		Processing:        2, |  | ||||||
| 		File: gtsmodel.File{ |  | ||||||
| 			Path:        originalPath, |  | ||||||
| 			ContentType: contentType, |  | ||||||
| 			FileSize:    len(original.image), |  | ||||||
| 			UpdatedAt:   time.Now(), |  | ||||||
| 		}, |  | ||||||
| 		Thumbnail: gtsmodel.Thumbnail{ |  | ||||||
| 			Path:        smallPath, |  | ||||||
| 			ContentType: mimeJpeg, // all thumbnails/smalls are encoded as jpeg |  | ||||||
| 			FileSize:    len(small.image), |  | ||||||
| 			UpdatedAt:   time.Now(), |  | ||||||
| 			URL:         smallURL, |  | ||||||
| 			RemoteURL:   "", |  | ||||||
| 		}, |  | ||||||
| 		Avatar: false, |  | ||||||
| 		Header: false, |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	return attachment, nil | 	return attachment, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func decodeGif(b []byte) (*imageAndMeta, error) { | func decodeGif(b []byte) (*ImageMeta, error) { | ||||||
| 	gif, err := gif.DecodeAll(bytes.NewReader(b)) | 	gif, err := gif.DecodeAll(bytes.NewReader(b)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -166,7 +135,7 @@ func decodeGif(b []byte) (*imageAndMeta, error) { | ||||||
| 	size := width * height | 	size := width * height | ||||||
| 	aspect := float64(width) / float64(height) | 	aspect := float64(width) / float64(height) | ||||||
| 
 | 
 | ||||||
| 	return &imageAndMeta{ | 	return &ImageMeta{ | ||||||
| 		image:  b, | 		image:  b, | ||||||
| 		width:  width, | 		width:  width, | ||||||
| 		height: height, | 		height: height, | ||||||
|  | @ -175,7 +144,7 @@ func decodeGif(b []byte) (*imageAndMeta, error) { | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func decodeImage(b []byte, contentType string) (*imageAndMeta, error) { | func decodeImage(b []byte, contentType string) (*ImageMeta, error) { | ||||||
| 	var i image.Image | 	var i image.Image | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
|  | @ -201,7 +170,7 @@ func decodeImage(b []byte, contentType string) (*imageAndMeta, error) { | ||||||
| 	size := width * height | 	size := width * height | ||||||
| 	aspect := float64(width) / float64(height) | 	aspect := float64(width) / float64(height) | ||||||
| 
 | 
 | ||||||
| 	return &imageAndMeta{ | 	return &ImageMeta{ | ||||||
| 		image:  b, | 		image:  b, | ||||||
| 		width:  width, | 		width:  width, | ||||||
| 		height: height, | 		height: height, | ||||||
|  | @ -210,12 +179,12 @@ func decodeImage(b []byte, contentType string) (*imageAndMeta, error) { | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // deriveThumbnail returns a byte slice and metadata for a thumbnail of width x and height y, | // deriveThumbnail returns a byte slice and metadata for a thumbnail | ||||||
| // of a given jpeg, png, or gif, or an error if something goes wrong. | // of a given jpeg, png, or gif, or an error if something goes wrong. | ||||||
| // | // | ||||||
| // Note that the aspect ratio of the image will be retained, | // Note that the aspect ratio of the image will be retained, | ||||||
| // so it will not necessarily be a square, even if x and y are set as the same value. | // so it will not necessarily be a square, even if x and y are set as the same value. | ||||||
| func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMeta, error) { | func deriveThumbnail(b []byte, contentType string) (*ImageMeta, error) { | ||||||
| 	var i image.Image | 	var i image.Image | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
|  | @ -239,7 +208,7 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet | ||||||
| 		return nil, fmt.Errorf("content type %s not recognised", contentType) | 		return nil, fmt.Errorf("content type %s not recognised", contentType) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	thumb := resize.Thumbnail(x, y, i, resize.NearestNeighbor) | 	thumb := resize.Thumbnail(thumbnailMaxWidth, thumbnailMaxHeight, i, resize.NearestNeighbor) | ||||||
| 	width := thumb.Bounds().Size().X | 	width := thumb.Bounds().Size().X | ||||||
| 	height := thumb.Bounds().Size().Y | 	height := thumb.Bounds().Size().Y | ||||||
| 	size := width * height | 	size := width * height | ||||||
|  | @ -257,7 +226,7 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return &imageAndMeta{ | 	return &ImageMeta{ | ||||||
| 		image:    out.Bytes(), | 		image:    out.Bytes(), | ||||||
| 		width:    width, | 		width:    width, | ||||||
| 		height:   height, | 		height:   height, | ||||||
|  | @ -268,7 +237,7 @@ func deriveThumbnail(b []byte, contentType string, x uint, y uint) (*imageAndMet | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | // deriveStaticEmojji takes a given gif or png of an emoji, decodes it, and re-encodes it as a static png. | ||||||
| func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) { | func deriveStaticEmoji(b []byte, contentType string) (*ImageMeta, error) { | ||||||
| 	var i image.Image | 	var i image.Image | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
|  | @ -291,7 +260,7 @@ func deriveStaticEmoji(b []byte, contentType string) (*imageAndMeta, error) { | ||||||
| 	if err := png.Encode(out, i); err != nil { | 	if err := png.Encode(out, i); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return &imageAndMeta{ | 	return &ImageMeta{ | ||||||
| 		image: out.Bytes(), | 		image: out.Bytes(), | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,7 +25,9 @@ import ( | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"codeberg.org/gruf/go-runners" | ||||||
| 	"codeberg.org/gruf/go-store/kv" | 	"codeberg.org/gruf/go-store/kv" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -37,18 +39,27 @@ type Manager interface { | ||||||
| type manager struct { | type manager struct { | ||||||
| 	db      db.DB | 	db      db.DB | ||||||
| 	storage *kv.KVStore | 	storage *kv.KVStore | ||||||
| 	pool    *workerPool | 	pool    runners.WorkerPool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // New returns a media manager with the given db and underlying storage. | // New returns a media manager with the given db and underlying storage. | ||||||
| func New(database db.DB, storage *kv.KVStore) Manager { | func New(database db.DB, storage *kv.KVStore) (Manager, error) { | ||||||
| 	workers := runtime.NumCPU() / 2 | 	workers := runtime.NumCPU() / 2 | ||||||
|  | 	queue := workers * 10 | ||||||
|  | 	pool := runners.NewWorkerPool(workers, queue) | ||||||
| 
 | 
 | ||||||
| 	return &manager{ | 	if start := pool.Start(); !start { | ||||||
|  | 		return nil, errors.New("could not start worker pool") | ||||||
|  | 	} | ||||||
|  | 	logrus.Debugf("started media manager worker pool with %d workers and queue capacity of %d", workers, queue) | ||||||
|  | 
 | ||||||
|  | 	m := &manager{ | ||||||
| 		db:      database, | 		db:      database, | ||||||
| 		storage: storage, | 		storage: storage, | ||||||
| 		pool:    newWorkerPool(workers), | 		pool:    pool, | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return m, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
|  | @ -77,9 +88,16 @@ func (m *manager) ProcessMedia(ctx context.Context, data []byte, accountID strin | ||||||
| 			return nil, errors.New("image was of size 0") | 			return nil, errors.New("image was of size 0") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return m.pool.run(func(ctx context.Context, data []byte, contentType string, accountID string) { | 		media, err := m.preProcessImage(ctx, data, contentType, accountID) | ||||||
| 			m.processImage(ctx, data, contentType, accountID) | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		m.pool.Enqueue(func(innerCtx context.Context) { | ||||||
|  | 			 | ||||||
| 		}) | 		}) | ||||||
|  | 
 | ||||||
|  | 		return nil, nil | ||||||
| 	default: | 	default: | ||||||
| 		return nil, fmt.Errorf("content type %s not (yet) supported", contentType) | 		return nil, fmt.Errorf("content type %s not (yet) supported", contentType) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,7 +1,34 @@ | ||||||
| package media | package media | ||||||
| 
 | 
 | ||||||
| import gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20211113114307_init" | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| type Media struct { | type Media struct { | ||||||
| 	Attachment *gtsmodel.MediaAttachment | 	mu         sync.Mutex | ||||||
|  | 	attachment *gtsmodel.MediaAttachment | ||||||
|  | 	rawData    []byte | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *Media) Thumb() (*ImageMeta, error) { | ||||||
|  | 	m.mu.Lock() | ||||||
|  | 	thumb, err := deriveThumbnail(m.rawData, m.attachment.File.ContentType) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error deriving thumbnail: %s", err) | ||||||
|  | 	} | ||||||
|  | 	m.attachment.Blurhash = thumb.blurhash | ||||||
|  | 	aaaaaaaaaaaaaaaa | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *Media) PreLoad() { | ||||||
|  | 	m.mu.Lock() | ||||||
|  | 	defer m.mu.Unlock() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *Media) Load() { | ||||||
|  | 	m.mu.Lock() | ||||||
|  | 	defer m.mu.Unlock() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,65 +0,0 @@ | ||||||
| package media |  | ||||||
| 
 |  | ||||||
| import "context" |  | ||||||
| 
 |  | ||||||
| func newWorkerPool(workers int) *workerPool { |  | ||||||
| 	// make a pool with the given worker capacity |  | ||||||
| 	pool := &workerPool{ |  | ||||||
| 		workerQueue: make(chan *worker, workers), |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// fill the pool with workers |  | ||||||
| 	for i := 0; i < workers; i++ { |  | ||||||
| 		pool.workerQueue <- &worker{ |  | ||||||
| 			// give each worker a reference to the pool so it |  | ||||||
| 			// can put itself back in when it's finished |  | ||||||
| 			workerQueue: pool.workerQueue, |  | ||||||
| 			data:        []byte{}, |  | ||||||
| 			contentType: "", |  | ||||||
| 			accountID:   "", |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return pool |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type workerPool struct { |  | ||||||
| 	workerQueue chan *worker |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (p *workerPool) run(fn func(ctx context.Context, data []byte, contentType string, accountID string)) (*Media, error) { |  | ||||||
| 
 |  | ||||||
| 	m := &Media{} |  | ||||||
| 
 |  | ||||||
| 	go func() { |  | ||||||
| 		// take a worker from the worker pool |  | ||||||
| 		worker := <-p.workerQueue |  | ||||||
| 		// tell it to work |  | ||||||
| 		worker.work(fn) |  | ||||||
| 	}() |  | ||||||
| 
 |  | ||||||
| 	return m, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type worker struct { |  | ||||||
| 	workerQueue chan *worker |  | ||||||
| 	data        []byte |  | ||||||
| 	contentType string |  | ||||||
| 	accountID   string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (w *worker) work(fn func(ctx context.Context, data []byte, contentType string, accountID string)) { |  | ||||||
| 	// return self to pool when finished |  | ||||||
| 	defer w.finish() |  | ||||||
| 	// do the work |  | ||||||
| 	fn(context.Background(), w.data, w.contentType, w.accountID) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (w *worker) finish() { |  | ||||||
| 	// clear self |  | ||||||
| 	w.data = []byte{} |  | ||||||
| 	w.contentType = "" |  | ||||||
| 	w.accountID = "" |  | ||||||
| 	// put self back in the worker pool |  | ||||||
| 	w.workerQueue <- w |  | ||||||
| } |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| package media |  | ||||||
| 
 |  | ||||||
| import "context" |  | ||||||
| 
 |  | ||||||
| type mediaProcessingFunction func(ctx context.Context, data []byte, contentType string, accountID string) |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue