From f7fc2e5d9b9b907973b55bda51c180814a7a92a2 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Sun, 25 Jul 2021 21:23:22 +0200
Subject: [PATCH] Significant cleanup and improvements

Signed-off-by: Janne Mareike Koschinski <janne@kuschku.de>
---
 .gitignore                      |   3 +-
 api/album_get.go                |  25 ++++++++
 api/album_list.go               |  28 +++++++++
 api/albumimage_get.go           |  25 ++++++++
 api/albumimage_list.go          |  25 ++++++++
 api/image_get.go                |  25 ++++++++
 api/image_list.go               |  28 +++++++++
 auth/parse_user.go              |  19 ++++++
 cmd/backend/main.go             |  36 +++++++++++
 cmd/frontend/main.go            |  62 +++++++++++++++++++
 config.example.yaml             |  65 ++++++++++++++++++++
 configuration/auth.go           |   5 ++
 configuration/configuration.go  |   9 +++
 configuration/conversion.go     |  16 +++++
 configuration/database.go       |   6 ++
 configuration/queue.go          |  10 +++
 configuration/redis.go          |   6 ++
 configuration/types/loglevel.go |  24 ++++++++
 configuration/types/timeout.go  |  21 +++++++
 environment/environment.go      | 104 ++++++++++++++++++++------------
 environment/repositories.go     |   9 +++
 go.mod                          |   1 +
 go.sum                          |   2 +
 main.go                         |  58 ------------------
 repo/album_images.go            |   8 +--
 s3/upload_source.go             |  19 ++++++
 task/image_resize.go            |  31 +++++++---
 util/return_json.go             |  13 ++++
 28 files changed, 571 insertions(+), 112 deletions(-)
 create mode 100644 api/album_get.go
 create mode 100644 api/album_list.go
 create mode 100644 api/albumimage_get.go
 create mode 100644 api/albumimage_list.go
 create mode 100644 api/image_get.go
 create mode 100644 api/image_list.go
 create mode 100644 auth/parse_user.go
 create mode 100644 cmd/backend/main.go
 create mode 100644 cmd/frontend/main.go
 create mode 100644 config.example.yaml
 create mode 100644 configuration/auth.go
 create mode 100644 configuration/configuration.go
 create mode 100644 configuration/conversion.go
 create mode 100644 configuration/database.go
 create mode 100644 configuration/queue.go
 create mode 100644 configuration/redis.go
 create mode 100644 configuration/types/loglevel.go
 create mode 100644 configuration/types/timeout.go
 create mode 100644 environment/repositories.go
 delete mode 100644 main.go
 create mode 100644 s3/upload_source.go
 create mode 100644 util/return_json.go

diff --git a/.gitignore b/.gitignore
index f8570a4..0155fe2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 /.idea/
 /vendor/
-/node_modules/
\ No newline at end of file
+/node_modules/
+/config.yaml
diff --git a/api/album_get.go b/api/album_get.go
new file mode 100644
index 0000000..741a40a
--- /dev/null
+++ b/api/album_get.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"github.com/gorilla/mux"
+	"net/http"
+)
+
+func GetAlbum(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		vars := mux.Vars(request)
+		album, err := env.Repositories.Albums.Get(vars["albumId"])
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, album)
+	})
+}
diff --git a/api/album_list.go b/api/album_list.go
new file mode 100644
index 0000000..9429759
--- /dev/null
+++ b/api/album_list.go
@@ -0,0 +1,28 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/auth"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"net/http"
+)
+
+func ListAlbums(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		user, err := auth.ParseUser(request, env)
+		if err != nil {
+			http.Error(writer, err.Error(), http.StatusUnauthorized)
+		}
+		albums, err := env.Repositories.Albums.List(user)
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, albums)
+	})
+}
diff --git a/api/albumimage_get.go b/api/albumimage_get.go
new file mode 100644
index 0000000..f140856
--- /dev/null
+++ b/api/albumimage_get.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"github.com/gorilla/mux"
+	"net/http"
+)
+
+func GetAlbumImage(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		vars := mux.Vars(request)
+		albumImage, err := env.Repositories.AlbumImages.Get(vars["albumId"], vars["imageId"])
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, albumImage)
+	})
+}
diff --git a/api/albumimage_list.go b/api/albumimage_list.go
new file mode 100644
index 0000000..5b284df
--- /dev/null
+++ b/api/albumimage_list.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"github.com/gorilla/mux"
+	"net/http"
+)
+
+func ListAlbumImages(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		vars := mux.Vars(request)
+		albumImages, err := env.Repositories.AlbumImages.List(vars["albumId"])
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, albumImages)
+	})
+}
diff --git a/api/image_get.go b/api/image_get.go
new file mode 100644
index 0000000..9ec538a
--- /dev/null
+++ b/api/image_get.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"github.com/gorilla/mux"
+	"net/http"
+)
+
+func GetImage(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		vars := mux.Vars(request)
+		image, err := env.Repositories.Images.Get(vars["imageId"])
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, image)
+	})
+}
diff --git a/api/image_list.go b/api/image_list.go
new file mode 100644
index 0000000..4148b4c
--- /dev/null
+++ b/api/image_list.go
@@ -0,0 +1,28 @@
+package api
+
+import (
+	"database/sql"
+	"git.kuschku.de/justjanne/imghost-frontend/auth"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"net/http"
+)
+
+func ListImages(env environment.Environment) http.Handler {
+	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		user, err := auth.ParseUser(request, env)
+		if err != nil {
+			http.Error(writer, err.Error(), http.StatusUnauthorized)
+		}
+		images, err := env.Repositories.Images.List(user)
+		if err == sql.ErrNoRows {
+			http.NotFound(writer, request)
+			return
+		} else if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
+		util.ReturnJson(writer, images)
+	})
+}
diff --git a/auth/parse_user.go b/auth/parse_user.go
new file mode 100644
index 0000000..fab59f3
--- /dev/null
+++ b/auth/parse_user.go
@@ -0,0 +1,19 @@
+package auth
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"net/http"
+)
+
+func ParseUser(request *http.Request, env environment.Environment) (user model.User, err error) {
+	// TODO: Implement actual user auth
+	user = model.User{
+		Id:    "ad45284c-be4d-4546-8171-41cf126ac091",
+		Name:  "justJanne",
+		Email: "janne@kuschku.de",
+		Roles: []string{"imghost:user", "imghost:admin"},
+	}
+
+	return
+}
diff --git a/cmd/backend/main.go b/cmd/backend/main.go
new file mode 100644
index 0000000..102b6d2
--- /dev/null
+++ b/cmd/backend/main.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/configuration"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/task"
+	"github.com/hibiken/asynq"
+	_ "github.com/lib/pq"
+	"gopkg.in/yaml.v2"
+	"log"
+	"os"
+)
+
+func main() {
+	var config configuration.Configuration
+	configFile, err := os.Open("config.yaml")
+	if err != nil {
+		panic(err)
+	}
+	err = yaml.NewDecoder(configFile).Decode(&config)
+	if err != nil {
+		panic(err)
+	}
+
+	env, err := environment.NewServerEnvironment(config)
+	if err != nil {
+		panic(err)
+	}
+	defer env.Destroy()
+
+	mux := asynq.NewServeMux()
+	mux.HandleFunc(config.Conversion.ResizeTaskId, task.HandleImageResizeTask)
+	if err := env.QueueServer.Run(mux); err != nil {
+		log.Fatalf("could not run server: %v", err)
+	}
+}
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
new file mode 100644
index 0000000..d101b77
--- /dev/null
+++ b/cmd/frontend/main.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/api"
+	"git.kuschku.de/justjanne/imghost-frontend/configuration"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/util"
+	"github.com/gorilla/mux"
+	_ "github.com/lib/pq"
+	"gopkg.in/yaml.v2"
+	"net/http"
+	"os"
+)
+
+func main() {
+	var config configuration.Configuration
+	configFile, err := os.Open("config.yaml")
+	if err != nil {
+		panic(err)
+	}
+	err = yaml.NewDecoder(configFile).Decode(&config)
+	if err != nil {
+		panic(err)
+	}
+
+	env, err := environment.NewClientEnvironment(config)
+	if err != nil {
+		panic(err)
+	}
+	defer env.Destroy()
+
+	router := mux.NewRouter()
+	// Image API
+	router.Handle(
+		"/api/v1/images",
+		api.ListImages(env)).Methods(http.MethodGet)
+	router.Handle(
+		"/api/v1/images/{imageId}",
+		api.GetImage(env)).Methods(http.MethodGet)
+
+	// Album API
+	router.Handle(
+		"/api/v1/albums",
+		api.ListAlbums(env)).Methods(http.MethodGet)
+	router.Handle(
+		"/api/v1/albums/{imageId}",
+		api.GetAlbum(env)).Methods(http.MethodGet)
+
+	// Album Image API
+	router.Handle(
+		"/api/v1/albums/{albumId}/images",
+		api.ListAlbumImages(env)).Methods(http.MethodGet)
+	router.Handle(
+		"/api/v1/albums/{albumId}/images/{imageId}",
+		api.GetAlbumImage(env)).Methods(http.MethodGet)
+
+	// TODO: Implement mutating API methods
+
+	if err = http.ListenAndServe(":8080", util.MethodOverride(router)); err != nil {
+		panic(err)
+	}
+}
diff --git a/config.example.yaml b/config.example.yaml
new file mode 100644
index 0000000..1d7a960
--- /dev/null
+++ b/config.example.yaml
@@ -0,0 +1,65 @@
+auth:
+  role_prefix: "imghost"
+
+queue:
+  concurrency: 10
+  log_level: "info"
+  strict_priority: false
+  queues:
+    critical: 6
+    default: 3
+    low: 1
+
+database:
+  type: "postgres"
+  url: "postgresql://imghost:hunter2@db.example.com/imghost"
+
+redis:
+  address: ":6379"
+  password: ""
+
+conversion:
+  task_id: "image:resize"
+  max_retry: 10
+  timeout: "3m"
+  queue: "default"
+  unique_timeout: "3m"
+  quality:
+    compression_quality: 100
+    sampling_factors: [ 1,1 ]
+  sizes:
+    - suffix: "s"
+      size:
+        width: 90
+        height: 90
+        format: "cover"
+    - suffix: "b"
+      size:
+        width: 160
+        height: 160
+        format: "cover"
+    - suffix: "t"
+      size:
+        width: 160
+        height: 160
+        format: "contain"
+    - suffix: "m"
+      size:
+        width: 320
+        height: 320
+        format: "contain"
+    - suffix: "l"
+      size:
+        width: 640
+        height: 640
+        format: "contain"
+    - suffix: "h"
+      size:
+        width: 1024
+        height: 1024
+        format: "contain"
+    - suffix: ""
+      size:
+        width: 0
+        height: 0
+        format: "contain"
diff --git a/configuration/auth.go b/configuration/auth.go
new file mode 100644
index 0000000..d87ba42
--- /dev/null
+++ b/configuration/auth.go
@@ -0,0 +1,5 @@
+package configuration
+
+type AuthConfiguration struct {
+	RolePrefix string `json:"role_prefix"`
+}
diff --git a/configuration/configuration.go b/configuration/configuration.go
new file mode 100644
index 0000000..42558ae
--- /dev/null
+++ b/configuration/configuration.go
@@ -0,0 +1,9 @@
+package configuration
+
+type Configuration struct {
+	Queue      QueueConfiguration      `json:"queue"`
+	Database   DatabaseConfiguration   `json:"database"`
+	Redis      RedisConfiguration      `json:"redis"`
+	Conversion ConversionConfiguration `json:"conversion"`
+	Auth       AuthConfiguration       `json:"auth"`
+}
diff --git a/configuration/conversion.go b/configuration/conversion.go
new file mode 100644
index 0000000..11d896d
--- /dev/null
+++ b/configuration/conversion.go
@@ -0,0 +1,16 @@
+package configuration
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/configuration/types"
+	"github.com/justjanne/imgconv"
+)
+
+type ConversionConfiguration struct {
+	TaskId        string          `json:"task_id"`
+	MaxRetry      int             `json:"max_retry"`
+	Timeout       types.Timeout   `json:"timeout"`
+	Queue         string          `json:"queue"`
+	UniqueTimeout types.Timeout   `json:"unique_timeout"`
+	Quality       imgconv.Quality `json:"quality"`
+	Sizes         []imgconv.Size  `json:"sizes"`
+}
diff --git a/configuration/database.go b/configuration/database.go
new file mode 100644
index 0000000..2719add
--- /dev/null
+++ b/configuration/database.go
@@ -0,0 +1,6 @@
+package configuration
+
+type DatabaseConfiguration struct {
+	Type string `json:"type"`
+	Url  string `json:"url"`
+}
diff --git a/configuration/queue.go b/configuration/queue.go
new file mode 100644
index 0000000..b10105c
--- /dev/null
+++ b/configuration/queue.go
@@ -0,0 +1,10 @@
+package configuration
+
+import "git.kuschku.de/justjanne/imghost-frontend/configuration/types"
+
+type QueueConfiguration struct {
+	Concurrency    int            `json:"concurrency"`
+	LogLevel       types.Severity `json:"log_level"`
+	StrictPriority bool           `json:"strict_priority"`
+	Queues         map[string]int `json:"queues"`
+}
diff --git a/configuration/redis.go b/configuration/redis.go
new file mode 100644
index 0000000..cb93818
--- /dev/null
+++ b/configuration/redis.go
@@ -0,0 +1,6 @@
+package configuration
+
+type RedisConfiguration struct {
+	Address  string `json:"address"`
+	Password string `json:"password"`
+}
diff --git a/configuration/types/loglevel.go b/configuration/types/loglevel.go
new file mode 100644
index 0000000..8338ad7
--- /dev/null
+++ b/configuration/types/loglevel.go
@@ -0,0 +1,24 @@
+package types
+
+import (
+	"github.com/hibiken/asynq"
+)
+
+type Severity asynq.LogLevel
+
+func (severity *Severity) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var spec string
+	if err := unmarshal(&spec); err != nil {
+		return err
+	}
+
+	var loglevel asynq.LogLevel
+	err := loglevel.Set(spec)
+	if err != nil {
+		return err
+	}
+
+	*severity = Severity(loglevel)
+
+	return nil
+}
diff --git a/configuration/types/timeout.go b/configuration/types/timeout.go
new file mode 100644
index 0000000..8a988e3
--- /dev/null
+++ b/configuration/types/timeout.go
@@ -0,0 +1,21 @@
+package types
+
+import "time"
+
+type Timeout time.Duration
+
+func (timeout *Timeout) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var spec string
+	if err := unmarshal(&spec); err != nil {
+		return err
+	}
+
+	duration, err := time.ParseDuration(spec)
+	if err != nil {
+		return err
+	}
+
+	*timeout = Timeout(duration)
+
+	return nil
+}
diff --git a/environment/environment.go b/environment/environment.go
index 114924a..31a0e4e 100644
--- a/environment/environment.go
+++ b/environment/environment.go
@@ -1,62 +1,88 @@
 package environment
 
 import (
-	"encoding/json"
+	"git.kuschku.de/justjanne/imghost-frontend/configuration"
+	"git.kuschku.de/justjanne/imghost-frontend/repo"
 	"github.com/hibiken/asynq"
 	"github.com/jmoiron/sqlx"
-	"github.com/justjanne/imgconv"
-	"os"
+	"time"
 )
 
 type Environment struct {
-	Queue      *asynq.Client
-	Database   *sqlx.DB
-	RolePrefix string
-	Sizes      []imgconv.Size
-	Quality    imgconv.Quality
+	Configuration configuration.Configuration
+	QueueClient   *asynq.Client
+	QueueServer   *asynq.Server
+	Database      *sqlx.DB
+	Repositories  Repositories
 }
 
-func NewEnvironment(
-	redisAddress string,
-	dbType string,
-	dbUrl string,
-	rolePrefix string,
-	sizes []imgconv.Size,
-	quality imgconv.Quality,
-) (env Environment, err error) {
-	env.Queue = asynq.NewClient(asynq.RedisClientOpt{
-		Addr: redisAddress,
-	})
-	env.Database, err = sqlx.Open(dbType, dbUrl)
-	env.RolePrefix = rolePrefix
-	env.Sizes = sizes
-	env.Quality = quality
-	return
-}
-
-func InitializeEnvironment() (env Environment, err error) {
-	var sizes []imgconv.Size
-	if err = json.Unmarshal([]byte(os.Getenv("CONFIG_SIZES")), &sizes); err != nil {
+func newCommonEnvironment(config configuration.Configuration) (env Environment, err error) {
+	env.Configuration = config
+	if env.Database, err = sqlx.Open(config.Database.Type, config.Database.Url); err != nil {
+		return
+	}
+	if env.Repositories.Images, err = repo.NewImageRepo(env.Database); err != nil {
+		return
+	}
+	if env.Repositories.Albums, err = repo.NewAlbumRepo(env.Database); err != nil {
 		return
 	}
-	var quality imgconv.Quality
-	if err = json.Unmarshal([]byte(os.Getenv("CONFIG_QUALITY")), &quality); err != nil {
+	if env.Repositories.AlbumImages, err = repo.NewAlbumImageRepo(env.Database); err != nil {
 		return
 	}
-	rolePrefix := os.Getenv("CONFIG_ROLE_PREFIX")
+	return
+}
 
-	redisAddress := os.Getenv("REDIS_ADDRESS")
-	dbType := os.Getenv("DB_TYPE")
-	dbUrl := os.Getenv("DB_URL")
-	return NewEnvironment(redisAddress, dbType, dbUrl, rolePrefix, sizes, quality)
+func NewClientEnvironment(config configuration.Configuration) (Environment, error) {
+	env, err := newCommonEnvironment(config)
+	if err != nil {
+		return env, err
+	}
+	env.QueueClient = asynq.NewClient(asynq.RedisClientOpt{
+		Addr:     config.Redis.Address,
+		Password: config.Redis.Password,
+	})
+	env.QueueClient.SetDefaultOptions(
+		config.Conversion.TaskId,
+		asynq.MaxRetry(config.Conversion.MaxRetry),
+		asynq.Timeout(time.Duration(config.Conversion.Timeout)),
+		asynq.Queue(config.Conversion.Queue),
+		asynq.Unique(time.Duration(config.Conversion.UniqueTimeout)),
+	)
+	return env, err
 }
 
-func (env Environment) Destroy() error {
-	if err := env.Queue.Close(); err != nil {
-		return err
+func NewServerEnvironment(config configuration.Configuration) (Environment, error) {
+	env, err := newCommonEnvironment(config)
+	if err != nil {
+		return env, err
 	}
+	env.QueueServer = asynq.NewServer(
+		asynq.RedisClientOpt{
+			Addr:     config.Redis.Address,
+			Password: config.Redis.Password,
+		},
+		asynq.Config{
+			Concurrency:    config.Queue.Concurrency,
+			LogLevel:       asynq.LogLevel(config.Queue.LogLevel),
+			Queues:         config.Queue.Queues,
+			StrictPriority: config.Queue.StrictPriority,
+		},
+	)
+	return env, err
+}
+
+func (env Environment) Destroy() error {
 	if err := env.Database.Close(); err != nil {
 		return err
 	}
+	if env.QueueClient != nil {
+		if err := env.QueueClient.Close(); err != nil {
+			return err
+		}
+	}
+	if env.QueueServer != nil {
+		env.QueueServer.Shutdown()
+	}
 	return nil
 }
diff --git a/environment/repositories.go b/environment/repositories.go
new file mode 100644
index 0000000..ff2a7ec
--- /dev/null
+++ b/environment/repositories.go
@@ -0,0 +1,9 @@
+package environment
+
+import "git.kuschku.de/justjanne/imghost-frontend/repo"
+
+type Repositories struct {
+	Images      repo.Images
+	Albums      repo.Albums
+	AlbumImages repo.AlbumImages
+}
diff --git a/go.mod b/go.mod
index 96db1b4..3698c09 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.kuschku.de/justjanne/imghost-frontend
 go 1.13
 
 require (
+	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/hibiken/asynq v0.18.2
 	github.com/jmoiron/sqlx v1.3.4
 	github.com/justjanne/imgconv v1.0.3 // indirect
diff --git a/go.sum b/go.sum
index 190c08b..89853b5 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,8 @@ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/hibiken/asynq v0.18.2 h1:hbLVygnmQMc2evTmNildtUWaUPHIBtggA59MoaHp/bY=
 github.com/hibiken/asynq v0.18.2/go.mod h1:Sn3Ql1clxIO5kXoFOAE3W73tX81Hkau+2Kam9LfgymM=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
diff --git a/main.go b/main.go
deleted file mode 100644
index e5a5af7..0000000
--- a/main.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"git.kuschku.de/justjanne/imghost-frontend/environment"
-	"git.kuschku.de/justjanne/imghost-frontend/model"
-	"git.kuschku.de/justjanne/imghost-frontend/repo"
-	_ "github.com/lib/pq"
-)
-
-func main() {
-	user := model.User{
-		Id:    "ad45284c-be4d-4546-8171-41cf126ac091",
-		Name:  "justJanne",
-		Email: "janne@kuschku.de",
-		Roles: []string{"imghost:user", "imghost:admin"},
-	}
-
-	var env environment.Environment
-	env, err := environment.InitializeEnvironment()
-	if err != nil {
-		panic(err)
-	}
-	defer env.Destroy()
-
-	imageRepo, err := repo.NewImageRepo(env.Database)
-	if err != nil {
-		panic(err)
-	}
-
-	albumRepo, err := repo.NewAlbumRepo(env.Database)
-	if err != nil {
-		panic(err)
-	}
-
-	albumImageRepo, err := repo.NewAlbumImageRepo(env.Database)
-	if err != nil {
-		panic(err)
-	}
-
-	images, err := imageRepo.List(user)
-	if err != nil {
-		panic(err)
-	}
-	fmt.Printf("images: %v\n", len(images))
-
-	albums, err := albumRepo.List(user)
-	if err != nil {
-		panic(err)
-	}
-	fmt.Printf("albums: %v\n", len(albums))
-
-	albumImages, err := albumImageRepo.List(model.Album{})
-	if err != nil {
-		panic(err)
-	}
-	fmt.Printf("albumImages: %v\n", len(albumImages))
-}
diff --git a/repo/album_images.go b/repo/album_images.go
index 9cfdbc5..b58a0a1 100644
--- a/repo/album_images.go
+++ b/repo/album_images.go
@@ -71,9 +71,9 @@ func NewAlbumImageRepo(db *sqlx.DB) (repo AlbumImages, err error) {
 	return repo, nil
 }
 
-func (repo AlbumImages) List(album model.Album) (images []model.AlbumImage, err error) {
+func (repo AlbumImages) List(albumId string) (images []model.AlbumImage, err error) {
 	rows, err := repo.queryList.Queryx(map[string]interface{}{
-		"albumId": album.Id,
+		"albumId": albumId,
 	})
 	if err != nil {
 		return
@@ -89,9 +89,9 @@ func (repo AlbumImages) List(album model.Album) (images []model.AlbumImage, err
 	return
 }
 
-func (repo AlbumImages) Get(album model.Album, imageId string) (image model.AlbumImage, err error) {
+func (repo AlbumImages) Get(albumId string, imageId string) (image model.AlbumImage, err error) {
 	err = repo.queryGet.Get(&image, map[string]interface{}{
-		"albumId": album.Id,
+		"albumId": albumId,
 		"imageId": imageId,
 	})
 	return
diff --git a/s3/upload_source.go b/s3/upload_source.go
new file mode 100644
index 0000000..8393c17
--- /dev/null
+++ b/s3/upload_source.go
@@ -0,0 +1,19 @@
+package s3
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"github.com/justjanne/imgconv"
+)
+
+// TODO: Implement
+func UploadSource(env environment.Environment, imageId string, source string) error {
+	return nil
+}
+
+func DownloadSource(env environment.Environment, imageId string) (string, error) {
+	return "", nil
+}
+
+func UploadImage(env environment.Environment, imageId string, format imgconv.Size, source string) error {
+	return nil
+}
diff --git a/task/image_resize.go b/task/image_resize.go
index 1e140b2..99021cc 100644
--- a/task/image_resize.go
+++ b/task/image_resize.go
@@ -3,34 +3,38 @@ package task
 import (
 	"context"
 	"encoding/json"
+	"git.kuschku.de/justjanne/imghost-frontend/configuration"
+	"git.kuschku.de/justjanne/imghost-frontend/environment"
+	"git.kuschku.de/justjanne/imghost-frontend/s3"
 	"git.kuschku.de/justjanne/imghost-frontend/util"
 	"github.com/hibiken/asynq"
 	"github.com/justjanne/imgconv"
 	"gopkg.in/gographics/imagick.v2/imagick"
 )
 
-const TypeImageResize = "image:Resize"
-
 type ImageResizePayload struct {
 	ImageId string
 	Sizes   []imgconv.Size
 	Quality imgconv.Quality
 }
 
-func NewResizeTask(imageId string, sizes []imgconv.Size, quality imgconv.Quality) (task *asynq.Task, err error) {
+func NewResizeTask(imageId string, config configuration.Configuration) (task *asynq.Task, err error) {
 	payload, err := json.Marshal(ImageResizePayload{
 		ImageId: imageId,
-		Sizes:   sizes,
-		Quality: quality,
+		Sizes:   config.Conversion.Sizes,
+		Quality: config.Conversion.Quality,
 	})
 	if err != nil {
 		return
 	}
-	task = asynq.NewTask(TypeImageResize, payload)
+	task = asynq.NewTask(config.Conversion.ResizeTaskId, payload)
 	return
 }
 
 func HandleImageResizeTask(ctx context.Context, task *asynq.Task) (err error) {
+	// TODO: Handle environment for tasks
+	env := environment.Environment{}
+
 	var payload ImageResizePayload
 	if err = json.Unmarshal(task.Payload(), &payload); err != nil {
 		return
@@ -39,8 +43,11 @@ func HandleImageResizeTask(ctx context.Context, task *asynq.Task) (err error) {
 	wand := imagick.NewMagickWand()
 	defer wand.Destroy()
 
-	tmpFile := ""
-	if err = wand.ReadImage(tmpFile); err != nil {
+	file, err := s3.DownloadSource(env, payload.ImageId)
+	if err != nil {
+		return
+	}
+	if err = wand.ReadImage(file); err != nil {
 		return
 	}
 	var originalImage imgconv.ImageHandle
@@ -50,7 +57,8 @@ func HandleImageResizeTask(ctx context.Context, task *asynq.Task) (err error) {
 
 	err = util.LaunchGoroutines(len(payload.Sizes), func(index int) error {
 		size := payload.Sizes[index]
-		tmpTargetFile := ""
+		// TODO: Allocate temp file
+		tmpFile := ""
 		image := originalImage.CloneImage()
 		if err := image.Crop(size); err != nil {
 			return err
@@ -58,7 +66,10 @@ func HandleImageResizeTask(ctx context.Context, task *asynq.Task) (err error) {
 		if err := image.Resize(size); err != nil {
 			return err
 		}
-		if err := image.Write(payload.Quality, tmpTargetFile); err != nil {
+		if err := image.Write(payload.Quality, tmpFile); err != nil {
+			return err
+		}
+		if err := s3.UploadImage(env, payload.ImageId, size, tmpFile); err != nil {
 			return err
 		}
 		return nil
diff --git a/util/return_json.go b/util/return_json.go
new file mode 100644
index 0000000..aad85ef
--- /dev/null
+++ b/util/return_json.go
@@ -0,0 +1,13 @@
+package util
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+func ReturnJson(writer http.ResponseWriter, data interface{}) {
+	writer.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(writer).Encode(data); err != nil {
+		writer.WriteHeader(http.StatusInternalServerError)
+	}
+}
-- 
GitLab