mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-03 16:20:27 +00:00
improve error handling
This commit is contained in:
parent
d7e589a10d
commit
29d7c277d3
10 changed files with 184 additions and 79 deletions
|
@ -274,6 +274,15 @@ const docTemplate = `{
|
|||
"schema": {
|
||||
"$ref": "#/definitions/types.ItemOut"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/server.ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1025,6 +1034,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -266,6 +266,15 @@
|
|||
"schema": {
|
||||
"$ref": "#/definitions/types.ItemOut"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Entity",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/server.ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1017,6 +1026,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"server.ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"types.ApiSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -14,6 +14,13 @@ definitions:
|
|||
items:
|
||||
type: any
|
||||
type: object
|
||||
server.ValidationError:
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
type: object
|
||||
types.ApiSummary:
|
||||
properties:
|
||||
build:
|
||||
|
@ -543,6 +550,12 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/types.ItemOut'
|
||||
"422":
|
||||
description: Unprocessable Entity
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/server.ValidationError'
|
||||
type: array
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: imports items into the database
|
||||
|
|
|
@ -43,7 +43,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
|||
// API Version 1
|
||||
|
||||
v1Base := v1.BaseUrlFunc(prefix)
|
||||
v1Ctrl := v1.NewControllerV1(a.services)
|
||||
v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
|
||||
{
|
||||
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{
|
||||
Version: Version,
|
||||
|
|
|
@ -8,8 +8,15 @@ import (
|
|||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
)
|
||||
|
||||
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
|
||||
return func(ctrl *V1Controller) {
|
||||
ctrl.maxUploadSize = maxUploadSize
|
||||
}
|
||||
}
|
||||
|
||||
type V1Controller struct {
|
||||
svc *services.AllServices
|
||||
svc *services.AllServices
|
||||
maxUploadSize int64
|
||||
}
|
||||
|
||||
func BaseUrlFunc(prefix string) func(s string) string {
|
||||
|
@ -21,7 +28,7 @@ func BaseUrlFunc(prefix string) func(s string) string {
|
|||
return prefixFunc
|
||||
}
|
||||
|
||||
func NewControllerV1(svc *services.AllServices) *V1Controller {
|
||||
func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) *V1Controller {
|
||||
ctrl := &V1Controller{
|
||||
svc: svc,
|
||||
}
|
||||
|
|
|
@ -2,10 +2,8 @@ package v1
|
|||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
|
@ -191,65 +189,3 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
|||
server.Respond(w, http.StatusNoContent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Max upload size of 10 MB - TODO: Set via config
|
||||
err := r.ParseMultipartForm(10 << 20)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to parse multipart form")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to get file from form")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
attachmentName := r.FormValue("name")
|
||||
if attachmentName == "" {
|
||||
log.Err(err).Msg("failed to get name from form")
|
||||
server.RespondError(w, http.StatusBadRequest, errors.New("name is required"))
|
||||
}
|
||||
|
||||
attachmentType := r.FormValue("type")
|
||||
if attachmentType == "" {
|
||||
attachmentName = "attachment"
|
||||
}
|
||||
|
||||
uid, _, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
item, err := ctrl.svc.Items.AttachmentAdd(
|
||||
ctx,
|
||||
uid,
|
||||
attachmentName,
|
||||
attachment.Type(attachmentType),
|
||||
file,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to add attachment")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
|
||||
server.Respond(w, http.StatusCreated, item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,97 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/hay-kot/homebox/backend/ent/attachment"
|
||||
"github.com/hay-kot/homebox/backend/internal/services"
|
||||
"github.com/hay-kot/homebox/backend/internal/types"
|
||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// HandleItemsImport godocs
|
||||
// @Summary imports items into the database
|
||||
// @Tags Items
|
||||
// @Produce json
|
||||
// @Param id path string true "Item ID"
|
||||
// @Param file formData file true "File attachment"
|
||||
// @Param type formData string true "Type of file"
|
||||
// @Param name formData string true "name of the file including extension"
|
||||
// @Success 200 {object} types.ItemOut
|
||||
// @Failure 422 {object} []server.ValidationError
|
||||
// @Router /v1/items/{id}/attachments [POST]
|
||||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleItemAttachmentCreate() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(ctrl.maxUploadSize << 20)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to parse multipart form")
|
||||
server.RespondError(w, http.StatusBadRequest, errors.New("failed to parse multipart form"))
|
||||
return
|
||||
}
|
||||
|
||||
errs := make(server.ValidationErrors, 0)
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, http.ErrMissingFile):
|
||||
log.Debug().Msg("file for attachment is missing")
|
||||
errs = errs.Append("file", "file is required")
|
||||
default:
|
||||
log.Err(err).Msg("failed to get file from form")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
attachmentName := r.FormValue("name")
|
||||
if attachmentName == "" {
|
||||
log.Debug().Msg("failed to get name from form")
|
||||
errs = errs.Append("name", "name is required")
|
||||
}
|
||||
|
||||
if errs.HasErrors() {
|
||||
server.Respond(w, http.StatusUnprocessableEntity, errs)
|
||||
return
|
||||
}
|
||||
|
||||
attachmentType := r.FormValue("type")
|
||||
if attachmentType == "" {
|
||||
attachmentType = attachment.TypeAttachment.String()
|
||||
}
|
||||
|
||||
id, _, err := ctrl.partialParseIdAndUser(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := services.NewContext(r.Context())
|
||||
|
||||
item, err := ctrl.svc.Items.AttachmentAdd(
|
||||
ctx,
|
||||
id,
|
||||
attachmentName,
|
||||
attachment.Type(attachmentType),
|
||||
file,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to add attachment")
|
||||
server.RespondServerError(w)
|
||||
return
|
||||
}
|
||||
|
||||
server.Respond(w, http.StatusCreated, item)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleItemAttachmentGet godocs
|
||||
// @Summary retrieves an attachment for an item
|
||||
// @Tags Items
|
||||
|
|
|
@ -58,7 +58,7 @@ func (svc *ItemService) AttachmentToken(ctx Context, itemId, attachmentId uuid.U
|
|||
}
|
||||
|
||||
if _, err := os.Stat(attachment.Edges.Document.Path); os.IsNotExist(err) {
|
||||
svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
|
||||
_ = svc.AttachmentDelete(ctx, ctx.GID, itemId, attachmentId)
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
|
|
|
@ -4,22 +4,47 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type ValidationErrors []ValidationError
|
||||
|
||||
func (ve *ValidationErrors) HasErrors() bool {
|
||||
if (ve == nil) || (len(*ve) == 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, err := range *ve {
|
||||
if err.Field != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ve ValidationErrors) Append(field, reasons string) ValidationErrors {
|
||||
return append(ve, ValidationError{
|
||||
Field: field,
|
||||
Reason: reasons,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorBuilder is a helper type to build a response that contains an array of errors.
|
||||
// Typical use cases are for returning an array of validation errors back to the user.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
//
|
||||
// {
|
||||
// "errors": [
|
||||
// "invalid id",
|
||||
// "invalid name",
|
||||
// "invalid description"
|
||||
// ],
|
||||
// "message": "Unprocessable Entity",
|
||||
// "status": 422
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "errors": [
|
||||
// "invalid id",
|
||||
// "invalid name",
|
||||
// "invalid description"
|
||||
// ],
|
||||
// "message": "Unprocessable Entity",
|
||||
// "status": 422
|
||||
// }
|
||||
type ErrorBuilder struct {
|
||||
errs []string
|
||||
}
|
||||
|
|
|
@ -21,6 +21,11 @@ export interface ServerResults {
|
|||
items: any;
|
||||
}
|
||||
|
||||
export interface ServerValidationError {
|
||||
field: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface ApiSummary {
|
||||
build: Build;
|
||||
health: boolean;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue