From ffe3b69d295f1edf689caa60035b2d7545333683 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Tue, 27 Jul 2021 01:32:40 +0200
Subject: [PATCH] Implement initial PoC of S3 based image processing and
 delivery

---
 api/image_get.go               |  23 +++++-
 api/image_upload.go            | 142 ++++++++++++++++++++++++++++++---
 configuration/conversion.go    |  19 +++--
 environment/backend.go         |   3 +
 environment/frontend.go        |   4 +
 environment/repositories.go    |   1 +
 model/image_info.go            |   7 ++
 repo/album_images.go           |  71 +++++++++++------
 repo/albums.go                 |  55 ++++++++-----
 repo/image_state.go            |  56 +++++++++++++
 repo/images.go                 |  66 +++++++++------
 storage/storage.go             |  24 +++++-
 task/image_resize.go           |  16 ++--
 task/image_resize_processor.go |  45 +++++++++--
 14 files changed, 423 insertions(+), 109 deletions(-)
 create mode 100644 model/image_info.go
 create mode 100644 repo/image_state.go

diff --git a/api/image_get.go b/api/image_get.go
index b18a010..9927a03 100644
--- a/api/image_get.go
+++ b/api/image_get.go
@@ -1,8 +1,10 @@
 package api
 
 import (
+	"context"
 	"database/sql"
 	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
 	"git.kuschku.de/justjanne/imghost-frontend/util"
 	"github.com/gorilla/mux"
 	"net/http"
@@ -10,8 +12,11 @@ import (
 
 func GetImage(env environment.FrontendEnvironment) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		var err error
+
 		vars := mux.Vars(request)
-		image, err := env.Repositories.Images.Get(vars["imageId"])
+		var info model.ImageInfo
+		info.Image, err = env.Repositories.Images.Get(vars["imageId"])
 		if err == sql.ErrNoRows {
 			http.NotFound(writer, request)
 			return
@@ -19,7 +24,21 @@ func GetImage(env environment.FrontendEnvironment) http.Handler {
 			http.Error(writer, err.Error(), http.StatusInternalServerError)
 			return
 		}
+		info.State, err = env.Repositories.ImageStates.Get(vars["imageId"])
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		imageUrl, err := env.Storage.UrlFor(context.Background(), env.Configuration.Storage.ImageBucket, info.Image.Id)
+		if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		info.Url = imageUrl.String()
 
-		util.ReturnJson(writer, image)
+		util.ReturnJson(writer, info)
 	})
 }
diff --git a/api/image_upload.go b/api/image_upload.go
index e1b8e5f..595a49d 100644
--- a/api/image_upload.go
+++ b/api/image_upload.go
@@ -2,13 +2,63 @@ package api
 
 import (
 	"context"
+	"crypto/rand"
+	"encoding/base32"
+	"errors"
+	"fmt"
 	"git.kuschku.de/justjanne/imghost-frontend/auth"
 	"git.kuschku.de/justjanne/imghost-frontend/environment"
 	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"git.kuschku.de/justjanne/imghost-frontend/repo"
+	"git.kuschku.de/justjanne/imghost-frontend/task"
 	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"mime"
 	"net/http"
+	"path/filepath"
+	"strings"
 )
 
+func generateId() string {
+	token := make([]byte, 4)
+	fmt.Printf("%v\n", token)
+	n, err := rand.Read(token)
+	fmt.Printf("%v\n", token)
+	if err != nil {
+		panic(err)
+	}
+	if n != 4 {
+		panic(errors.New("not enough bytes read"))
+	}
+	return strings.TrimSuffix(base32.StdEncoding.EncodeToString(token), "=")
+}
+
+func determineMimeType(header string, filename string) string {
+	mediaType, _, err := mime.ParseMediaType(header)
+	if err == nil {
+		return mediaType
+	}
+	mediaType = mime.TypeByExtension(filepath.Ext(filename))
+	return mediaType
+}
+
+func determineExtension(filename string, mimeType string) (extension string, err error) {
+	extension = filepath.Ext(filename)
+	if extension != "" {
+		return
+	}
+
+	extensions, err := mime.ExtensionsByType(mimeType)
+	if err != nil {
+		return
+	}
+	if len(extensions) == 0 {
+		err = errors.New("no extensions for type " + mimeType + " found")
+		return
+	}
+	extension = extensions[0]
+	return
+}
+
 func UploadImage(env environment.FrontendEnvironment) http.Handler {
 	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
 		user, err := auth.ParseUser(request, env)
@@ -16,25 +66,91 @@ func UploadImage(env environment.FrontendEnvironment) http.Handler {
 			http.Error(writer, err.Error(), http.StatusUnauthorized)
 			return
 		}
+		println("parsed user: " + user.Name)
 
-		var image model.Image
-		image.Id = "testid"
-		image.Owner = user.Id
-		err = env.Repositories.Images.Create(image)
+		err = request.ParseMultipartForm(1 * 1024 * 1024)
 		if err != nil {
+			println("could not parse multiline form")
 			http.Error(writer, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		err = env.Storage.Upload(
-			context.Background(),
-			env.Configuration.Storage.ConversionBucket,
-			image.Id,
-			request.Body)
-		if err != nil {
-			http.Error(writer, err.Error(), http.StatusInternalServerError)
-			return
+		println("parsed multiline form")
+
+		for key := range request.MultipartForm.File {
+			println("found file: " + key)
+		}
+
+		var files []model.Image
+		for _, file := range request.MultipartForm.File["images[]"] {
+			println("processing file")
+			var image model.Image
+			image.Id = generateId()
+			image.Owner = user.Id
+			image.OriginalName = file.Filename
+			image.MimeType = determineMimeType(file.Header.Get("Content-Type"), file.Filename)
+			err = env.Repositories.Images.Create(image)
+			if err != nil {
+				println("failed creating image: " + image.Id)
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			println("created image: " + image.Id)
+			data, err := file.Open()
+			if err != nil {
+				println("failed opening image: " + file.Filename)
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			fmt.Printf("Read image: %d\n", file.Size)
+			err = env.Storage.Upload(
+				context.Background(),
+				env.Configuration.Storage.ConversionBucket,
+				image.Id,
+				image.MimeType,
+				data)
+			if err != nil {
+				println("failed uploading image: " + file.Filename)
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			println("uploaded image: " + env.Configuration.Storage.Endpoint +
+				"/" + env.Configuration.Storage.ConversionBucket +
+				"/" + image.Id)
+			extension, err := determineExtension(image.OriginalName, image.MimeType)
+			if err != nil {
+				println("failed to determine extension for file")
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			resizeTask, err := task.NewResizeTask(
+				image.Id,
+				extension,
+				env.Configuration)
+			if err != nil {
+				println("failed creating resize task")
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			println("created task: " + resizeTask.Type())
+			_, err = env.QueueClient.Enqueue(resizeTask)
+			if err != nil {
+				println("failed enqueuing resize task")
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			println("Enqueued task")
+			err = env.Repositories.ImageStates.Update(image.Id, repo.StateQueued)
+			if err != nil {
+				println("failed updating image state")
+				http.Error(writer, err.Error(), http.StatusInternalServerError)
+				return
+			}
+			println("Updated state")
+
+			files = append(files, image)
 		}
+		fmt.Printf("Processed all files: %d\n", len(files))
 
-		util.ReturnJson(writer, image)
+		util.ReturnJson(writer, files)
 	})
 }
diff --git a/configuration/conversion.go b/configuration/conversion.go
index 00c1ab9..6a3112a 100644
--- a/configuration/conversion.go
+++ b/configuration/conversion.go
@@ -6,11 +6,16 @@ import (
 )
 
 type ConversionConfiguration struct {
-	TaskId        string          `json:"task_id" yaml:"task-id"`
-	MaxRetry      int             `json:"max_retry" yaml:"max-retry"`
-	Timeout       types.Timeout   `json:"timeout" yaml:"timeout"`
-	Queue         string          `json:"queue" yaml:"queue"`
-	UniqueTimeout types.Timeout   `json:"unique_timeout" yaml:"unique-timeout"`
-	Quality       imgconv.Quality `json:"quality" yaml:"quality"`
-	Sizes         []imgconv.Size  `json:"sizes" yaml:"sizes"`
+	TaskId        string           `json:"task_id" yaml:"task-id"`
+	MaxRetry      int              `json:"max_retry" yaml:"max-retry"`
+	Timeout       types.Timeout    `json:"timeout" yaml:"timeout"`
+	Queue         string           `json:"queue" yaml:"queue"`
+	UniqueTimeout types.Timeout    `json:"unique_timeout" yaml:"unique-timeout"`
+	Quality       imgconv.Quality  `json:"quality" yaml:"quality"`
+	Sizes         []SizeDefinition `json:"sizes" yaml:"sizes"`
+}
+
+type SizeDefinition struct {
+	Suffix string       `json:"suffix" yaml:"suffix"`
+	Size   imgconv.Size `json:"size" yaml:"size"`
 }
diff --git a/environment/backend.go b/environment/backend.go
index 73fd20a..cde2a74 100644
--- a/environment/backend.go
+++ b/environment/backend.go
@@ -24,6 +24,9 @@ func NewBackendEnvironment(config configuration.BackendConfiguration) (env Backe
 	if env.Repositories.Images, err = repo.NewImageRepo(env.Database); err != nil {
 		return
 	}
+	if env.Repositories.ImageStates, err = repo.NewImageStateRepo(env.Database); err != nil {
+		return
+	}
 	if env.Repositories.Albums, err = repo.NewAlbumRepo(env.Database); err != nil {
 		return
 	}
diff --git a/environment/frontend.go b/environment/frontend.go
index b05e301..b29e552 100644
--- a/environment/frontend.go
+++ b/environment/frontend.go
@@ -15,6 +15,7 @@ type FrontendEnvironment struct {
 	Database      *sqlx.DB
 	Repositories  Repositories
 	Storage       storage.Storage
+	InProgress    map[string]*asynq.TaskInfo
 }
 
 func NewFrontendEnvironment(config configuration.FrontendConfiguration) (env FrontendEnvironment, err error) {
@@ -25,6 +26,9 @@ func NewFrontendEnvironment(config configuration.FrontendConfiguration) (env Fro
 	if env.Repositories.Images, err = repo.NewImageRepo(env.Database); err != nil {
 		return
 	}
+	if env.Repositories.ImageStates, err = repo.NewImageStateRepo(env.Database); err != nil {
+		return
+	}
 	if env.Repositories.Albums, err = repo.NewAlbumRepo(env.Database); err != nil {
 		return
 	}
diff --git a/environment/repositories.go b/environment/repositories.go
index ff2a7ec..c307ff7 100644
--- a/environment/repositories.go
+++ b/environment/repositories.go
@@ -4,6 +4,7 @@ import "git.kuschku.de/justjanne/imghost-frontend/repo"
 
 type Repositories struct {
 	Images      repo.Images
+	ImageStates repo.ImageStates
 	Albums      repo.Albums
 	AlbumImages repo.AlbumImages
 }
diff --git a/model/image_info.go b/model/image_info.go
new file mode 100644
index 0000000..00a62e5
--- /dev/null
+++ b/model/image_info.go
@@ -0,0 +1,7 @@
+package model
+
+type ImageInfo struct {
+	Image Image  `json:"image"`
+	State string `json:"state"`
+	Url   string `json:"url"`
+}
diff --git a/repo/album_images.go b/repo/album_images.go
index b58a0a1..fb95079 100644
--- a/repo/album_images.go
+++ b/repo/album_images.go
@@ -19,54 +19,75 @@ type AlbumImages struct {
 func NewAlbumImageRepo(db *sqlx.DB) (repo AlbumImages, err error) {
 	repo.db = db
 	repo.queryList, err = db.PrepareNamed(`
-			SELECT album_images.album,
-			       album_images.image,
-			       album_images.title,
-			       album_images.description
+			SELECT album,
+			       image,
+			       title,
+			       description
 			FROM album_images
-			WHERE album_images.album = :albumId
-			ORDER BY album_images.position
+			WHERE album = :albumId
+			ORDER BY position
 		`)
+	if err != nil {
+		return
+	}
 	repo.queryGet, err = db.PrepareNamed(`
-			SELECT album_images.album,
-			       album_images.image,
-			       album_images.title,
-			       album_images.description
+			SELECT album,
+			       image,
+			       title,
+			       description
 			FROM album_images
-			WHERE album_images.album = :albumId
-			AND album_images.image = :imageId
-			ORDER BY album_images.position
+			WHERE album = :albumId
+			AND image = :imageId
+			ORDER BY position
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtCreate, err = db.PrepareNamed(`
 			INSERT INTO album_images (album, image, title, description, position)
 			VALUES (:albumId, :imageId, :title, :description, (
-			    SELECT COUNT(album_images.image)
+			    SELECT COUNT(image)
 			    FROM album_images
-			    WHERE album_images.album = :albumId
+			    WHERE album = :albumId
 			))
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtUpdate, err = db.PrepareNamed(`
 			UPDATE album_images 
-			SET album_images.title = :title, 
-			    album_images.description = :description
-			WHERE album_images.album = :albumId
-		    AND album_images.image = :imageId
+			SET title = :title, 
+			    description = :description
+			WHERE album = :albumId
+		    AND image = :imageId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtDelete, err = db.PrepareNamed(`
 			DELETE FROM album_images
-			WHERE album_images.album = :albumId
-			AND album_images.image = :imageID
+			WHERE album = :albumId
+			AND image = :imageID
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtDeleteAll, err = db.PrepareNamed(`
 			DELETE FROM album_images
-			WHERE album_images.album = :albumId
+			WHERE album = :albumId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtReorder, err = db.PrepareNamed(`
 			UPDATE album_images 
-			SET album_images.position = :position
-			WHERE album_images.album = :albumId
-		    AND album_images.image = :imageId
+			SET position = :position
+			WHERE album = :albumId
+		    AND image = :imageId
 		`)
+	if err != nil {
+		return
+	}
 
 	return repo, nil
 }
diff --git a/repo/albums.go b/repo/albums.go
index 6be98f1..e443a02 100644
--- a/repo/albums.go
+++ b/repo/albums.go
@@ -17,41 +17,56 @@ type Albums struct {
 func NewAlbumRepo(db *sqlx.DB) (repo Albums, err error) {
 	repo.db = db
 	repo.queryList, err = db.PrepareNamed(`
-			SELECT albums.id,
-			       albums.owner,
-			       albums.title,
-			       albums.description,
-			       albums.created_at,
-			       albums.updated_at
+			SELECT id,
+			       owner,
+			       title,
+			       description,
+			       created_at,
+			       updated_at
 			FROM albums
-			WHERE albums.owner = :userId
-			ORDER BY albums.created_at DESC
+			WHERE owner = :userId
+			ORDER BY created_at DESC
 		`)
+	if err != nil {
+		return
+	}
 	repo.queryGet, err = db.PrepareNamed(`
-			SELECT albums.id,
-			       albums.owner,
-			       albums.title,
-			       albums.description,
-			       albums.created_at,
-			       albums.updated_at
+			SELECT id,
+			       owner,
+			       title,
+			       description,
+			       created_at,
+			       updated_at
 			FROM albums
-			WHERE albums.id = :albumId
+			WHERE id = :albumId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtCreate, err = db.PrepareNamed(`
 			INSERT INTO albums (id, owner, title, description, created_at, updated_at)
 			VALUES (:albumId, :userId, :title, :description, NOW(), NOW())
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtUpdate, err = db.PrepareNamed(`
 			UPDATE albums 
-			SET albums.title = :title, 
-			    albums.description = :description, 
-			    albums.updated_at = NOW()
-			WHERE albums.id = :albumId
+			SET title = :title, 
+			    description = :description, 
+			    updated_at = NOW()
+			WHERE id = :albumId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtDelete, err = db.PrepareNamed(`
 			DELETE FROM albums
-			WHERE albums.id = :albums
+			WHERE id = :albums
 		`)
+	if err != nil {
+		return
+	}
 
 	return repo, nil
 }
diff --git a/repo/image_state.go b/repo/image_state.go
new file mode 100644
index 0000000..2c0026b
--- /dev/null
+++ b/repo/image_state.go
@@ -0,0 +1,56 @@
+package repo
+
+import (
+	"github.com/jmoiron/sqlx"
+)
+
+const (
+	StateCreated    = "created"
+	StateQueued     = "queued"
+	StateInProgress = "in_progress"
+	StateDone       = "done"
+	StateError      = "error"
+)
+
+type ImageStates struct {
+	db         *sqlx.DB
+	queryGet   *sqlx.NamedStmt
+	stmtUpdate *sqlx.NamedStmt
+}
+
+func NewImageStateRepo(db *sqlx.DB) (repo ImageStates, err error) {
+	repo.db = db
+	repo.queryGet, err = db.PrepareNamed(`
+			SELECT state
+			FROM images
+			WHERE id = :imageId
+		`)
+	if err != nil {
+		return
+	}
+	repo.stmtUpdate, err = db.PrepareNamed(`
+			UPDATE images
+			SET state = :state
+			WHERE id = :imageId
+		`)
+	if err != nil {
+		return
+	}
+
+	return repo, nil
+}
+
+func (repo ImageStates) Get(imageId string) (state string, err error) {
+	err = repo.queryGet.Get(&state, map[string]interface{}{
+		"imageId": imageId,
+	})
+	return
+}
+
+func (repo ImageStates) Update(imageId string, state string) (err error) {
+	_, err = repo.stmtUpdate.Exec(map[string]interface{}{
+		"imageId": imageId,
+		"state":   state,
+	})
+	return
+}
diff --git a/repo/images.go b/repo/images.go
index a29c01b..a8b1531 100644
--- a/repo/images.go
+++ b/repo/images.go
@@ -17,43 +17,58 @@ type Images struct {
 func NewImageRepo(db *sqlx.DB) (repo Images, err error) {
 	repo.db = db
 	repo.queryList, err = db.PrepareNamed(`
-			SELECT images.id,
-			       images.owner,
-			       images.title,
-			       images.description,
-			       images.original_name,
-			       images.created_at,
-			       images.updated_at
+			SELECT id,
+			       owner,
+			       title,
+			       description,
+			       original_name,
+			       created_at,
+			       updated_at
 			FROM images
-			WHERE images.owner = :userId
-			ORDER BY images.created_at DESC
+			WHERE owner = :userId
+			ORDER BY created_at DESC
 		`)
+	if err != nil {
+		return
+	}
 	repo.queryGet, err = db.PrepareNamed(`
-			SELECT images.id,
-			       images.owner,
-			       images.title,
-			       images.description,
-			       images.original_name,
-			       images.created_at,
-			       images.updated_at
+			SELECT id,
+			       owner,
+			       title,
+			       description,
+			       original_name,
+			       created_at,
+			       updated_at
 			FROM images
-			WHERE images.id = :imageId
+			WHERE id = :imageId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtCreate, err = db.PrepareNamed(`
-			INSERT INTO images (id, owner, title, description, original_name, type, created_at, updated_at)
-			VALUES (:imageId, :userId, :title, :description, :originalName, :mimeType, NOW(), NOW())
+			INSERT INTO images (id, owner, title, description, original_name, type, created_at, updated_at, state)
+			VALUES (:imageId, :userId, :title, :description, :originalName, :mimeType, NOW(), NOW(), :state)
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtUpdate, err = db.PrepareNamed(`
 			UPDATE images 
-			SET images.title = :title, 
-			    images.description = :description, 
-			    images.updated_at = NOW()
-			WHERE images.id = :imageId
+			SET title = :title, 
+			    description = :description, 
+			    updated_at = NOW()
+			WHERE id = :imageId
 		`)
+	if err != nil {
+		return
+	}
 	repo.stmtDelete, err = db.PrepareNamed(`
 			DELETE FROM images
-			WHERE images.id = :imageId
+			WHERE id = :imageId
 		`)
+	if err != nil {
+		return
+	}
 
 	return repo, nil
 }
@@ -84,13 +99,14 @@ func (repo Images) Get(imageId string) (image model.Image, err error) {
 }
 
 func (repo Images) Create(new model.Image) (err error) {
-	_, err = repo.stmtUpdate.Exec(map[string]interface{}{
+	_, err = repo.stmtCreate.Exec(map[string]interface{}{
 		"imageId":      new.Id,
 		"userId":       new.Owner,
 		"title":        new.Title,
 		"description":  new.Description,
 		"originalName": new.OriginalName,
 		"mimeType":     new.MimeType,
+		"state":        StateCreated,
 	})
 	return
 }
diff --git a/storage/storage.go b/storage/storage.go
index 8a4065d..ab535a5 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -6,7 +6,9 @@ import (
 	"github.com/minio/minio-go/v7"
 	"github.com/minio/minio-go/v7/pkg/credentials"
 	"io"
+	"net/url"
 	"os"
+	"time"
 )
 
 type Storage struct {
@@ -23,24 +25,28 @@ func NewStorage(config configuration.StorageConfiguration) (storage Storage, err
 	return
 }
 
-func (storage Storage) UploadFile(ctx context.Context, bucketName string, fileName string, file *os.File) (err error) {
+func (storage Storage) UploadFile(ctx context.Context, bucketName string, fileName string, mimeType string, file *os.File) (err error) {
 	_, err = storage.s3client.FPutObject(
 		ctx,
 		bucketName,
 		fileName,
 		file.Name(),
-		minio.PutObjectOptions{})
+		minio.PutObjectOptions{
+			ContentType: mimeType,
+		})
 	return
 }
 
-func (storage Storage) Upload(ctx context.Context, bucketName string, fileName string, reader io.ReadCloser) (err error) {
+func (storage Storage) Upload(ctx context.Context, bucketName string, fileName string, mimeType string, reader io.Reader) (err error) {
 	_, err = storage.s3client.PutObject(
 		ctx,
 		bucketName,
 		fileName,
 		reader,
 		-1,
-		minio.PutObjectOptions{})
+		minio.PutObjectOptions{
+			ContentType: mimeType,
+		})
 	return
 }
 
@@ -53,3 +59,13 @@ func (storage Storage) DownloadFile(ctx context.Context, bucketName string, file
 		minio.GetObjectOptions{})
 	return
 }
+
+func (storage Storage) UrlFor(ctx context.Context, bucketName string, fileName string) (url *url.URL, err error) {
+	url, err = storage.s3client.PresignedGetObject(
+		ctx,
+		bucketName,
+		fileName,
+		7*24*time.Hour,
+		map[string][]string{})
+	return
+}
diff --git a/task/image_resize.go b/task/image_resize.go
index 28d5e28..9af8293 100644
--- a/task/image_resize.go
+++ b/task/image_resize.go
@@ -8,16 +8,18 @@ import (
 )
 
 type ImageResizePayload struct {
-	ImageId string
-	Sizes   []imgconv.Size
-	Quality imgconv.Quality
+	ImageId   string
+	Extension string
+	Sizes     []configuration.SizeDefinition
+	Quality   imgconv.Quality
 }
 
-func NewResizeTask(imageId string, config configuration.FrontendConfiguration) (task *asynq.Task, err error) {
+func NewResizeTask(imageId string, extension string, config configuration.FrontendConfiguration) (task *asynq.Task, err error) {
 	payload, err := json.Marshal(ImageResizePayload{
-		ImageId: imageId,
-		Sizes:   config.Conversion.Sizes,
-		Quality: config.Conversion.Quality,
+		ImageId:   imageId,
+		Extension: extension,
+		Sizes:     config.Conversion.Sizes,
+		Quality:   config.Conversion.Quality,
 	})
 	if err != nil {
 		return
diff --git a/task/image_resize_processor.go b/task/image_resize_processor.go
index a3071b4..20eaf98 100644
--- a/task/image_resize_processor.go
+++ b/task/image_resize_processor.go
@@ -3,7 +3,9 @@ package task
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/repo"
 	"git.kuschku.de/justjanne/imghost-frontend/util"
 	"github.com/hibiken/asynq"
 	"github.com/justjanne/imgconv"
@@ -27,11 +29,22 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
 		return
 	}
 
+	println("parsed task: " + payload.ImageId)
+
+	if err = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateInProgress); err != nil {
+		println("failed to set image state: " + payload.ImageId)
+		println(err.Error())
+		return
+	}
+
 	wand := imagick.NewMagickWand()
 	defer wand.Destroy()
 
-	sourceFile, err := ioutil.TempFile("", payload.ImageId)
+	sourceFile, err := ioutil.TempFile("", payload.ImageId+"*."+payload.Extension)
 	if err != nil {
+		println("failed to create temp file: " + payload.ImageId)
+		println(err.Error())
+		_ = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateError)
 		return
 	}
 	err = processor.env.Storage.DownloadFile(
@@ -40,28 +53,37 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
 		payload.ImageId,
 		sourceFile)
 	if err != nil {
+		println("failed to download file: " + sourceFile.Name())
+		println(err.Error())
+		_ = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateError)
 		return
 	}
-	if err = wand.ReadImageFile(sourceFile); err != nil {
+	if err = wand.ReadImage(sourceFile.Name()); err != nil {
+		println("failed to read file: " + sourceFile.Name())
+		println(err.Error())
+		_ = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateError)
 		return
 	}
 	var originalImage imgconv.ImageHandle
 	if originalImage, err = imgconv.NewImage(wand); err != nil {
+		println("failed to load file: " + sourceFile.Name())
+		println(err.Error())
+		_ = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateError)
 		return err
 	}
 
 	err = util.LaunchGoroutines(len(payload.Sizes), func(index int) error {
-		outputFile, err := ioutil.TempFile("", payload.ImageId)
+		outputFile, err := ioutil.TempFile("", payload.ImageId+"*.png")
 		if err != nil {
 			return err
 		}
 
 		size := payload.Sizes[index]
 		image := originalImage.CloneImage()
-		if err := image.Crop(size); err != nil {
+		if err := image.Crop(size.Size); err != nil {
 			return err
 		}
-		if err := image.Resize(size); err != nil {
+		if err := image.Resize(size.Size); err != nil {
 			return err
 		}
 		if err := image.Write(payload.Quality, outputFile); err != nil {
@@ -70,12 +92,23 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
 		if err := processor.env.Storage.UploadFile(
 			ctx,
 			processor.env.Configuration.Storage.ImageBucket,
-			payload.ImageId,
+			fmt.Sprintf("%s%s", payload.ImageId, size.Suffix),
+			"image/png",
 			outputFile); err != nil {
 			return err
 		}
 		return nil
 	})
+	if err != nil {
+		println("failed to convert image file")
+		println(err.Error())
+		_ = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateError)
+		return
+	}
+
+	if err = processor.env.Repositories.ImageStates.Update(payload.ImageId, repo.StateDone); err != nil {
+		return
+	}
 
 	return
 }
-- 
GitLab