diff --git a/api/image_delete.go b/api/image_delete.go
index fcb2cd9cfbe69842c26b7e21b6d1ea3c09117b8d..731798a7b01dcc8688861352dd5bd057f220b0be 100644
--- a/api/image_delete.go
+++ b/api/image_delete.go
@@ -1,6 +1,7 @@
 package api
 
 import (
+	"context"
 	"database/sql"
 	"git.kuschku.de/justjanne/imghost-frontend/environment"
 	"github.com/gorilla/mux"
@@ -27,6 +28,16 @@ func DeleteImage(env environment.FrontendEnvironment) http.Handler {
 			return
 		}
 
+		err = env.Storage.DeleteFiles(
+			context.Background(),
+			env.Configuration.Storage.ImageBucket,
+			image.Id,
+		)
+		if err != nil {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+			return
+		}
+
 		writer.WriteHeader(http.StatusNoContent)
 	})
 }
diff --git a/api/image_upload.go b/api/image_upload.go
index 4c8035e16057630fee3a23f8a8db3a58bd43f76c..d4b05aa80844d6ef2fc2563d1fa20e701a68db18 100644
--- a/api/image_upload.go
+++ b/api/image_upload.go
@@ -33,16 +33,21 @@ func generateId() string {
 }
 
 func determineMimeType(header string, filename string) string {
+	println("Determining mime header from " + header + " and " + filename)
 	mediaType, _, err := mime.ParseMediaType(header)
 	if err == nil {
+		println("mediatype is " + mediaType)
 		return mediaType
 	}
 	mediaType = mime.TypeByExtension(filepath.Ext(filename))
+	println("extensiontype is " + mediaType)
 	return mediaType
 }
 
 func determineExtension(filename string, mimeType string) (extension string, err error) {
+	println("determining extension for " + filename + " and " + mimeType)
 	extension = filepath.Ext(filename)
+	println("found file extension " + extension)
 	if extension != "" {
 		return
 	}
@@ -56,6 +61,7 @@ func determineExtension(filename string, mimeType string) (extension string, err
 		return
 	}
 	extension = extensions[0]
+	println("found mime extension " + extension)
 	return
 }
 
@@ -81,7 +87,7 @@ func UploadImage(env environment.FrontendEnvironment) http.Handler {
 		}
 
 		var files []model.Image
-		for _, file := range request.MultipartForm.File["images[]"] {
+		for _, file := range request.MultipartForm.File["images"] {
 			println("processing file")
 			var image model.Image
 			image.Id = generateId()
diff --git a/environment/backend.go b/environment/backend.go
index 73fd20ad2f24438a9aded730c6413cf1ed1a6f6a..aa8e10e25047720ed0d9eefb0b1fb10c67085667 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.ImageMetadata, err = repo.NewImageMetadataRepo(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 607103d90afe3dda006db4b14b6e409107fae77e..42d5a5af38be091a0c0fda37e5565c4d8bd06bcd 100644
--- a/environment/frontend.go
+++ b/environment/frontend.go
@@ -26,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.ImageMetadata, err = repo.NewImageMetadataRepo(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 ff2a7ec5efc15f5bb91d508d5b03ff03d1c02cb8..55cca909ea6db87e2af25897ed6aea91c9cd30af 100644
--- a/environment/repositories.go
+++ b/environment/repositories.go
@@ -3,7 +3,8 @@ package environment
 import "git.kuschku.de/justjanne/imghost-frontend/repo"
 
 type Repositories struct {
-	Images      repo.Images
-	Albums      repo.Albums
-	AlbumImages repo.AlbumImages
+	Images        repo.Images
+	ImageMetadata repo.ImageMetadata
+	Albums        repo.Albums
+	AlbumImages   repo.AlbumImages
 }
diff --git a/model/image.go b/model/image.go
index a095a9ed8d4acb33ba8b0f0324f2b650682ccf35..79c204acfd131b4bd31c907ab7b02b2058f2c7ba 100644
--- a/model/image.go
+++ b/model/image.go
@@ -15,7 +15,7 @@ type Image struct {
 	CreatedAt    time.Time `json:"created_at" db:"created_at"`
 	UpdatedAt    time.Time `json:"updated_at" db:"updated_at"`
 	OriginalName string    `json:"original_name" db:"original_name"`
-	MimeType     string    `json:"mime_type" db:"mime_type"`
+	MimeType     string    `json:"mime_type" db:"type"`
 	State        string    `json:"state" db:"state"`
 	Url          string    `json:"url"`
 }
diff --git a/repo/image_metadata.go b/repo/image_metadata.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe9aabdc8f131180391ec345d1209601f0dc45e0
--- /dev/null
+++ b/repo/image_metadata.go
@@ -0,0 +1,95 @@
+package repo
+
+import (
+	"git.kuschku.de/justjanne/imghost-frontend/model"
+	"github.com/jmoiron/sqlx"
+)
+
+type ImageMetadata struct {
+	db         *sqlx.DB
+	queryList  *sqlx.NamedStmt
+	stmtCreate *sqlx.NamedStmt
+	stmtDelete *sqlx.NamedStmt
+}
+
+func NewImageMetadataRepo(db *sqlx.DB) (repo ImageMetadata, err error) {
+	repo.db = db
+	repo.queryList, err = db.PrepareNamed(`
+			SELECT name, content
+			FROM image_metadata
+			WHERE image = :imageId
+		`)
+	if err != nil {
+		return
+	}
+	repo.stmtCreate, err = db.PrepareNamed(`
+			INSERT INTO image_metadata (image, name, content)
+			VALUES (:imageId, :name, :content)
+		`)
+	if err != nil {
+		return
+	}
+	repo.stmtDelete, err = db.PrepareNamed(`
+			DELETE FROM image_metadata
+			WHERE image = :imageId
+		`)
+	if err != nil {
+		return
+	}
+
+	return repo, nil
+}
+
+func (repo ImageMetadata) List(image model.Image) (map[string]string, error) {
+	rows, err := repo.queryList.Queryx(map[string]interface{}{
+		"imageId": image.Id,
+	})
+	if err != nil {
+		return nil, err
+	}
+	metadata := make(map[string]string)
+	for rows.Next() {
+		var key string
+		var value string
+		err = rows.Scan(&key, &value)
+		if err != nil {
+			return nil, err
+		}
+		metadata[key] = value
+	}
+	return metadata, nil
+}
+
+func (repo ImageMetadata) Update(imageId string, metadata map[string]string) (err error) {
+	tx, err := repo.db.Beginx()
+	if err != nil {
+		return
+	}
+	println("Deleting metadata for " + imageId)
+	_, err = tx.NamedStmt(repo.stmtDelete).Exec(map[string]interface{}{
+		"imageId": imageId,
+	})
+	if err != nil {
+		return
+	}
+	for key, value := range metadata {
+		println("Adding metadata for " + imageId + " with " + key + "=" + value)
+		_, err = tx.NamedStmt(repo.stmtCreate).Exec(map[string]interface{}{
+			"imageId": imageId,
+			"name":    key,
+			"content": value,
+		})
+		if err != nil {
+			return
+		}
+	}
+	err = tx.Commit()
+	return
+}
+
+func (repo ImageMetadata) Delete(changed model.Image) (err error) {
+	_, err = repo.stmtDelete.Exec(map[string]interface{}{
+		"imageId": changed.Id,
+	})
+	return
+}
diff --git a/repo/images.go b/repo/images.go
index 57e3338a89226726f0d53cc19368aa0cfe3868fa..c6d7824c85d4d7a0ecdbb8093fa120b9b0f22bf8 100644
--- a/repo/images.go
+++ b/repo/images.go
@@ -31,6 +31,7 @@ func NewImageRepo(db *sqlx.DB) (repo Images, err error) {
 			       title,
 			       description,
 			       original_name,
+			       type,
 			       created_at,
 			       updated_at,
 			       state
@@ -47,6 +48,7 @@ func NewImageRepo(db *sqlx.DB) (repo Images, err error) {
 			       title,
 			       description,
 			       original_name,
+			       type,
 			       created_at,
 			       updated_at,
 			       state
diff --git a/storage/storage.go b/storage/storage.go
index ffb357105668f43f64008476081bea416849c7ef..a8a41bf211bcf72622337cf770301c5ae3e18ffa 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -60,6 +60,26 @@ func (storage Storage) DownloadFile(ctx context.Context, bucketName string, file
 	return
 }
 
+func (storage Storage) DeleteFiles(ctx context.Context, bucketName string, prefix string) error {
+	objects := storage.s3client.ListObjects(
+		ctx,
+		bucketName,
+		minio.ListObjectsOptions{Prefix: prefix},
+	)
+	errors := storage.s3client.RemoveObjects(
+		ctx,
+		bucketName,
+		objects,
+		minio.RemoveObjectsOptions{},
+	)
+	for err := range errors {
+		if err.Err != nil {
+			return err.Err
+		}
+	}
+	return nil
+}
+
 func (storage Storage) UrlFor(bucketName string, fileName string) *url.URL {
 	fileUrl := *storage.s3client.EndpointURL()
 	fileUrl.Path = filepath.Join(fileUrl.Path, bucketName, fileName)
diff --git a/task/image_resize_processor.go b/task/image_resize_processor.go
index 8b79df8656240d382930ef46386c0fb3fcb5a561..414478ee6332cd6c89d7f29933deaf7eee082104 100644
--- a/task/image_resize_processor.go
+++ b/task/image_resize_processor.go
@@ -11,6 +11,7 @@ import (
 	"github.com/justjanne/imgconv"
 	"gopkg.in/gographics/imagick.v2/imagick"
 	"io/ioutil"
+	"strings"
 )
 
 type ImageProcessor struct {
@@ -26,6 +27,8 @@ func NewImageProcessor(env environment.BackendEnvironment) *ImageProcessor {
 func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
 	var payload ImageResizePayload
 	if err = json.Unmarshal(task.Payload(), &payload); err != nil {
+		println("Could not unmarshal task")
+		println(err.Error())
 		return
 	}
 
@@ -69,7 +72,22 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
 		println("failed to load file: " + sourceFile.Name())
 		println(err.Error())
 		_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
-		return err
+		return
+	}
+
+	metadata := make(map[string]string)
+	for _, key := range wand.GetImageProperties("exif:*") {
+		if strings.HasPrefix(key, "exif:thumbnail:") {
+			continue
+		}
+		metadata[strings.TrimPrefix(key, "exif:")] = wand.GetImageProperty(key)
+	}
+	err = processor.env.Repositories.ImageMetadata.Update(payload.ImageId, metadata)
+	if err != nil {
+		println("failed to write metadata: " + payload.ImageId)
+		println(err.Error())
+		_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
+		return
 	}
 
 	err = util.LaunchGoroutines(len(payload.Sizes), func(index int) error {
@@ -106,6 +124,18 @@ func (processor *ImageProcessor) ProcessTask(ctx context.Context, task *asynq.Ta
 		return
 	}
 
+	err = processor.env.Storage.DeleteFiles(
+		ctx,
+		processor.env.Configuration.Storage.ConversionBucket,
+		payload.ImageId,
+	)
+	if err != nil {
+		println("failed to delete temp file: " + payload.ImageId)
+		println(err.Error())
+		_ = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateError)
+		return
+	}
+
 	if err = processor.env.Repositories.Images.UpdateState(payload.ImageId, repo.StateDone); err != nil {
 		return
 	}
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index 8fae9ddd27f5404dc92e8139c57d74b0e89f7481..b89a5bbc03d43c96c17b94578269ca27c13451cd 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -3,6 +3,7 @@ import './App.css';
 import ImageList from "./components/ImageList";
 import {BaseUrlProvider} from './api/baseUrlContext';
 import {QueryClient, QueryClientProvider} from "react-query";
+import UploadView from "./components/UploadView";
 
 const queryClient = new QueryClient();
 
@@ -10,6 +11,7 @@ function App() {
     return (
         <BaseUrlProvider value="http://localhost:8080/">
             <QueryClientProvider client={queryClient}>
+                <UploadView />
                 <ImageList/>
             </QueryClientProvider>
         </BaseUrlProvider>
diff --git a/ui/src/api/useDeleteImage.ts b/ui/src/api/useDeleteImage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..46cb2517af0f9f196bd3cbd4d4f1797d566e302c
--- /dev/null
+++ b/ui/src/api/useDeleteImage.ts
@@ -0,0 +1,19 @@
+import {Image} from "./model/Image";
+import {useBaseUrl} from "./baseUrlContext";
+import {useMutation, useQueryClient} from "react-query";
+import axios from "axios";
+
+export const useDeleteImage = () => {
+    const baseUrl = useBaseUrl();
+    const queryClient = useQueryClient();
+    return useMutation<unknown, unknown, Image>((image: Image) => axios.delete(
+        `api/v1/images/${image.id}`,
+        {
+            baseURL: baseUrl
+        }
+    ), {
+        onSuccess: () => {
+            queryClient.invalidateQueries('image')
+        },
+    })
+}
diff --git a/ui/src/api/useListAlbums.ts b/ui/src/api/useListAlbums.ts
index 6f408304ba670d975deaa071d6a910f7371b27ae..6e7b041be6ed0e26e63aedbcef07115759b8a057 100644
--- a/ui/src/api/useListAlbums.ts
+++ b/ui/src/api/useListAlbums.ts
@@ -6,7 +6,7 @@ import {Album} from "./model/Album";
 export const useListAlbums = () => {
     const baseUrl = useBaseUrl();
     return useQuery(
-        "albums",
+        "album",
         () => axios.get<Album[]>(
             "api/v1/albums",
             {
diff --git a/ui/src/api/useListImages.ts b/ui/src/api/useListImages.ts
index 76d74e0315118c647ef39299ecea19becf31e732..9d858b5635824b3b08b4d192909c94cc9beb39e0 100644
--- a/ui/src/api/useListImages.ts
+++ b/ui/src/api/useListImages.ts
@@ -6,7 +6,7 @@ import {Image} from "./model/Image";
 export const useListImages = () => {
     const baseUrl = useBaseUrl();
     return useQuery(
-        "images",
+        "image",
         () => axios.get<Image[]>(
             "api/v1/images",
             {
diff --git a/ui/src/api/useUpdateImage.ts b/ui/src/api/useUpdateImage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..98dcffa34ec210a5396185dd1d094286b7b047b2
--- /dev/null
+++ b/ui/src/api/useUpdateImage.ts
@@ -0,0 +1,20 @@
+import {Image} from "./model/Image";
+import {useBaseUrl} from "./baseUrlContext";
+import {useMutation, useQueryClient} from "react-query";
+import axios from "axios";
+
+export const useUpdateImage = () => {
+    const baseUrl = useBaseUrl();
+    const queryClient = useQueryClient();
+    return useMutation<void, unknown, Image>((image: Image) => axios.post(
+        `api/v1/images/${image.id}`,
+        image,
+        {
+            baseURL: baseUrl
+        }
+    ), {
+        onSuccess: () => {
+            queryClient.invalidateQueries('image')
+        },
+    })
+}
diff --git a/ui/src/api/useUploadImage.ts b/ui/src/api/useUploadImage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..925fc675363eaedee1551c0095fa9d18f521c135
--- /dev/null
+++ b/ui/src/api/useUploadImage.ts
@@ -0,0 +1,26 @@
+import {Image} from "./model/Image";
+import {useBaseUrl} from "./baseUrlContext";
+import {useMutation, useQueryClient} from "react-query";
+import axios from "axios";
+
+export const useUploadImage = () => {
+    const baseUrl = useBaseUrl();
+    const queryClient = useQueryClient();
+    return useMutation<Image, unknown, FileList>((files: FileList) => {
+        const formData = new FormData();
+        for (let i = 0; i < files.length; i++) {
+            formData.append("images", files[i]);
+        }
+        return axios.post<Image>(
+            `api/v1/images`,
+            formData,
+            {
+                baseURL: baseUrl
+            }
+        ).then(it => it.data);
+    }, {
+        onSuccess: () => {
+            queryClient.invalidateQueries('image')
+        },
+    })
+}
diff --git a/ui/src/components/ImageView.tsx b/ui/src/components/ImageView.tsx
index 83bb5c55e38243c509a559573d70297e76368181..e6e62e62b15b3a24336a3d581210e842d94cf0da 100644
--- a/ui/src/components/ImageView.tsx
+++ b/ui/src/components/ImageView.tsx
@@ -1,23 +1,66 @@
 import {Image} from "../api/model/Image";
-import React from "react";
+import React, {useState} from "react";
+import {useUpdateImage} from "../api/useUpdateImage";
+import {useDeleteImage} from "../api/useDeleteImage";
 
 export interface ImageProps {
     image: Image
 }
 
 export default function ImageView({image}: ImageProps) {
+    const {mutate: update, error: updateError, isLoading: updateLoading} = useUpdateImage();
+    const {mutate: remove, error: removeError, isLoading: removeLoading} = useDeleteImage();
+    const [title, setTitle] = useState<string>(image.title);
+    const [description, setDescription] = useState<string>(image.description);
+
     return (
         <div>
+            <p>UpdateError: {JSON.stringify(updateError, null, 2)}</p>
+            <p>RemoveError: {JSON.stringify(removeError, null, 2)}</p>
+            <p>UpdateLoading: {JSON.stringify(updateLoading, null, 2)}</p>
+            <p>RemoveLoading: {JSON.stringify(removeLoading, null, 2)}</p>
             <p>{image.id}</p>
             <p>{image.owner}</p>
-            <p>{image.title}</p>
-            <p>{image.description}</p>
+            <label>
+                Title
+                <input
+                    type="text"
+                    value={title}
+                    onChange={({target: {value}}) =>
+                        setTitle(value)}
+                />
+            </label>
+            <br/>
+            <label>
+                Description
+                <input
+                    type="text"
+                    value={description}
+                    onChange={({target: {value}}) =>
+                        setDescription(value)}
+                />
+            </label>
             <p>{image.original_name}</p>
             <p>{image.mime_type}</p>
             <p>{image.created_at}</p>
             <p>{image.updated_at}</p>
             <p>{image.state}</p>
             <img src={image.url + "t"} alt=""/>
+            <br/>
+            <input
+                type="submit"
+                value="Save"
+                onClick={() => update({
+                    ...image,
+                    title,
+                    description,
+                })}
+            />
+            <input
+                type="submit"
+                value="Delete"
+                onClick={() => remove(image)}
+            />
         </div>
     )
 }
diff --git a/ui/src/components/UploadView.tsx b/ui/src/components/UploadView.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5f4dee2906e0d2fc6a41ef1308590a403cf7ba47
--- /dev/null
+++ b/ui/src/components/UploadView.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import {useUploadImage} from "../api/useUploadImage";
+
+export default function UploadView() {
+    const {mutate: upload, error: uploadError, isLoading: uploadLoading} = useUploadImage();
+
+    return (
+        <div>
+            <pre>Error: {JSON.stringify(uploadError, null, 2)}</pre>
+            <pre>Loading: {JSON.stringify(uploadLoading, null, 2)}</pre>
+            <input
+                type="file"
+                onChange={async ({target}) => {
+                    if (target.files) {
+                        await upload(target.files)
+                        target.files = null;
+                    }
+                }}
+            />
+        </div>
+    )
+}
diff --git a/util/cors_wrapper.go b/util/cors_wrapper.go
index 89ee96e7562796e6617f914fdd0c20325db542b3..8bd32904ed8e5602a5ba8ce7c037b6f81c96e982 100644
--- a/util/cors_wrapper.go
+++ b/util/cors_wrapper.go
@@ -6,6 +6,7 @@ func CorsWrapper(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.Header().Set("Access-Control-Allow-Origin", "*")
 		w.Header().Set("Access-Control-Allow-Headers", "*")
+		w.Header().Set("Access-Control-Allow-Methods", "*")
 
 		if r.Method == http.MethodOptions {
 			w.WriteHeader(200)