From b893fcc35a55f978af0c5a854d40806fed1ee906 Mon Sep 17 00:00:00 2001 From: TheWisePigeon Date: Wed, 10 Jan 2024 16:32:05 +0000 Subject: [PATCH 1/2] add function to handle file uploads --- internal/handlers/faces.go | 40 ++++++++++++++++++++++++ internal/types/errors.go | 2 ++ main.go | 5 +++ pkg/uploads.go | 62 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 internal/handlers/faces.go create mode 100644 pkg/uploads.go diff --git a/internal/handlers/faces.go b/internal/handlers/faces.go new file mode 100644 index 0000000..4bd85ec --- /dev/null +++ b/internal/handlers/faces.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + "visio/internal/types" + "visio/pkg" +) + +type FaceHandler struct { + logger *slog.Logger +} + +func NewFaceHandler(logger *slog.Logger) *FaceHandler { + return &FaceHandler{ + logger: logger, + } +} + +func (h *FaceHandler) SaveFace(w http.ResponseWriter, r *http.Request) { + filePath, err := pkg.HandleFileUpload(w, r) + if err != nil { + if errors.Is(err, types.ErrFileNotFound) { + w.WriteHeader(http.StatusBadRequest) + return + } + if errors.Is(err, types.ErrUnsupportedFormat) { + w.WriteHeader(http.StatusBadRequest) + return + } + h.logger.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + fmt.Println(filePath) + w.WriteHeader(http.StatusOK) + return +} diff --git a/internal/types/errors.go b/internal/types/errors.go index 7ab8b13..3eca34c 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -6,4 +6,6 @@ var ( ErrUserNotFound = errors.New("No user found") ErrSessionNotFound = errors.New("Session not found") ErrDuplicatePrefix = errors.New("Duplicate key prefix") + ErrFileNotFound = errors.New("File field not found in request body") + ErrUnsupportedFormat = errors.New("Field type not supported") ) diff --git a/main.go b/main.go index fd44296..55e0352 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,7 @@ func main() { appHandler := handlers.NewAppHandler(keys, appLogger) authHandler := handlers.NewAuthHandler(users, sessions, appLogger) keyHandler := handlers.NewKeyHandler(keys, sessions, appLogger) + faceHandler := handlers.NewFaceHandler(appLogger) authMiddleware := middlewares.NewAuthMiddleware(sessions, users, appLogger) r := chi.NewRouter() @@ -68,6 +69,10 @@ func main() { r.With(authMiddleware.CookieAuth).Delete("/{prefix}", keyHandler.Revoke) }) + r.Route("/faces", func(r chi.Router) { + r.Post("/", faceHandler.SaveFace) + }) + port := os.Getenv("PORT") if port == "" { panic("Unable to read PORT environment variable") diff --git a/pkg/uploads.go b/pkg/uploads.go new file mode 100644 index 0000000..c406a49 --- /dev/null +++ b/pkg/uploads.go @@ -0,0 +1,62 @@ +package pkg + +import ( + "bufio" + "fmt" + "io" + "net/http" + "os" + "visio/internal/types" +) + +var imageMimeTypes = []string{ + "image/jpeg", + "image/png", +} + +func HandleFileUpload(w http.ResponseWriter, r *http.Request) (string, error) { + r.Body = http.MaxBytesReader(w, r.Body, 20<<20+1024) + reader, err := r.MultipartReader() + if err != nil { + return "", fmt.Errorf("Error while reading multipart request body: %w", err) + } + p, err := reader.NextPart() + if err != nil && err != io.EOF { + return "", fmt.Errorf("Error while reading multipart body part: %w", err) + } + if p.FormName() != "face" { + return "", types.ErrFileNotFound + } + buffer := bufio.NewReader(p) + sniffedBytes, err := buffer.Peek(512) + if err != nil { + return "", fmt.Errorf("Error while peeking through bytes: %w", err) + } + contentType := http.DetectContentType(sniffedBytes) + fileIsNotImage := true + for _, mimeType := range imageMimeTypes { + if contentType == mimeType { + fileIsNotImage = false + break + } + } + if fileIsNotImage { + return "", types.ErrUnsupportedFormat + } + f, err := os.CreateTemp("", "") + if err != nil { + return "", fmt.Errorf("Error while creating file: %w", err) + } + defer f.Close() + var maxSize int64 = 20 << 20 + lmt := io.MultiReader(buffer, io.LimitReader(p, maxSize-511)) + written, err := io.Copy(f, lmt) + if err != nil && err != io.EOF { + return "", fmt.Errorf("Error while copying bytes to file: %w", err) + } + if written > maxSize { + os.Remove(f.Name()) + return "", fmt.Errorf("Error while deleting file: %w", err) + } + return f.Name(), nil +} From a85b0bdbea954ab6c3b524996a7bf587b12bd008 Mon Sep 17 00:00:00 2001 From: TheWisePigeon Date: Wed, 10 Jan 2024 20:05:35 +0000 Subject: [PATCH 2/2] add file upload handler --- internal/handlers/faces.go | 8 ++++++++ internal/types/errors.go | 17 ++++++++++++----- pkg/uploads.go | 8 +++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/internal/handlers/faces.go b/internal/handlers/faces.go index 4bd85ec..6df8ce4 100644 --- a/internal/handlers/faces.go +++ b/internal/handlers/faces.go @@ -24,10 +24,17 @@ func (h *FaceHandler) SaveFace(w http.ResponseWriter, r *http.Request) { if err != nil { if errors.Is(err, types.ErrFileNotFound) { w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(types.ErrFileNotFoundMessage)) return } if errors.Is(err, types.ErrUnsupportedFormat) { w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(types.ErrUnsupportedFormatMessage)) + return + } + if errors.Is(err, types.ErrBodyTooLarge) { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(types.ErrBodyTooLargeMessage)) return } h.logger.Error(err.Error()) @@ -36,5 +43,6 @@ func (h *FaceHandler) SaveFace(w http.ResponseWriter, r *http.Request) { } fmt.Println(filePath) w.WriteHeader(http.StatusOK) + w.Write([]byte("Ok")) return } diff --git a/internal/types/errors.go b/internal/types/errors.go index 3eca34c..f2e8e52 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -3,9 +3,16 @@ package types import "errors" var ( - ErrUserNotFound = errors.New("No user found") - ErrSessionNotFound = errors.New("Session not found") - ErrDuplicatePrefix = errors.New("Duplicate key prefix") - ErrFileNotFound = errors.New("File field not found in request body") - ErrUnsupportedFormat = errors.New("Field type not supported") + ErrFileNotFoundMessage = "'face' field not found in request body" + ErrUnsupportedFormatMessage = "File type not supported. Only image/jpeg and image/png are." + ErrBodyTooLargeMessage = "Request body too large. Only 5Mb is allowed." +) + +var ( + ErrUserNotFound = errors.New("No user found") + ErrSessionNotFound = errors.New("Session not found") + ErrDuplicatePrefix = errors.New("Duplicate key prefix") + ErrFileNotFound = errors.New(ErrFileNotFoundMessage) + ErrUnsupportedFormat = errors.New(ErrUnsupportedFormatMessage) + ErrBodyTooLarge = errors.New(ErrBodyTooLargeMessage) ) diff --git a/pkg/uploads.go b/pkg/uploads.go index c406a49..fb96c5a 100644 --- a/pkg/uploads.go +++ b/pkg/uploads.go @@ -14,8 +14,10 @@ var imageMimeTypes = []string{ "image/png", } +const MaxFileSize = 5 + func HandleFileUpload(w http.ResponseWriter, r *http.Request) (string, error) { - r.Body = http.MaxBytesReader(w, r.Body, 20<<20+1024) + r.Body = http.MaxBytesReader(w, r.Body, MaxFileSize<<20) reader, err := r.MultipartReader() if err != nil { return "", fmt.Errorf("Error while reading multipart request body: %w", err) @@ -48,11 +50,11 @@ func HandleFileUpload(w http.ResponseWriter, r *http.Request) (string, error) { return "", fmt.Errorf("Error while creating file: %w", err) } defer f.Close() - var maxSize int64 = 20 << 20 + var maxSize int64 = MaxFileSize << 20 lmt := io.MultiReader(buffer, io.LimitReader(p, maxSize-511)) written, err := io.Copy(f, lmt) if err != nil && err != io.EOF { - return "", fmt.Errorf("Error while copying bytes to file: %w", err) + return "", types.ErrBodyTooLarge } if written > maxSize { os.Remove(f.Name())