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