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": {
|
"schema": {
|
||||||
"$ref": "#/definitions/types.ItemOut"
|
"$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": {
|
"types.ApiSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -266,6 +266,15 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/types.ItemOut"
|
"$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": {
|
"types.ApiSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -14,6 +14,13 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: any
|
type: any
|
||||||
type: object
|
type: object
|
||||||
|
server.ValidationError:
|
||||||
|
properties:
|
||||||
|
field:
|
||||||
|
type: string
|
||||||
|
reason:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
types.ApiSummary:
|
types.ApiSummary:
|
||||||
properties:
|
properties:
|
||||||
build:
|
build:
|
||||||
|
@ -543,6 +550,12 @@ paths:
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/types.ItemOut'
|
$ref: '#/definitions/types.ItemOut'
|
||||||
|
"422":
|
||||||
|
description: Unprocessable Entity
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/server.ValidationError'
|
||||||
|
type: array
|
||||||
security:
|
security:
|
||||||
- Bearer: []
|
- Bearer: []
|
||||||
summary: imports items into the database
|
summary: imports items into the database
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||||
// API Version 1
|
// API Version 1
|
||||||
|
|
||||||
v1Base := v1.BaseUrlFunc(prefix)
|
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{
|
r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, types.Build{
|
||||||
Version: Version,
|
Version: Version,
|
||||||
|
|
|
@ -8,8 +8,15 @@ import (
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
|
||||||
|
return func(ctrl *V1Controller) {
|
||||||
|
ctrl.maxUploadSize = maxUploadSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type V1Controller struct {
|
type V1Controller struct {
|
||||||
svc *services.AllServices
|
svc *services.AllServices
|
||||||
|
maxUploadSize int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func BaseUrlFunc(prefix string) func(s string) string {
|
func BaseUrlFunc(prefix string) func(s string) string {
|
||||||
|
@ -21,7 +28,7 @@ func BaseUrlFunc(prefix string) func(s string) string {
|
||||||
return prefixFunc
|
return prefixFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewControllerV1(svc *services.AllServices) *V1Controller {
|
func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller)) *V1Controller {
|
||||||
ctrl := &V1Controller{
|
ctrl := &V1Controller{
|
||||||
svc: svc,
|
svc: svc,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,8 @@ package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"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/services"
|
||||||
"github.com/hay-kot/homebox/backend/internal/types"
|
"github.com/hay-kot/homebox/backend/internal/types"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
|
@ -191,65 +189,3 @@ func (ctrl *V1Controller) HandleItemsImport() http.HandlerFunc {
|
||||||
server.Respond(w, http.StatusNoContent, nil)
|
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
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"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/services"
|
||||||
"github.com/hay-kot/homebox/backend/internal/types"
|
"github.com/hay-kot/homebox/backend/internal/types"
|
||||||
"github.com/hay-kot/homebox/backend/pkgs/server"
|
"github.com/hay-kot/homebox/backend/pkgs/server"
|
||||||
"github.com/rs/zerolog/log"
|
"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
|
// HandleItemAttachmentGet godocs
|
||||||
// @Summary retrieves an attachment for an item
|
// @Summary retrieves an attachment for an item
|
||||||
// @Tags Items
|
// @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) {
|
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
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,22 +4,47 @@ import (
|
||||||
"net/http"
|
"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.
|
// 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.
|
// Typical use cases are for returning an array of validation errors back to the user.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
//
|
// {
|
||||||
// {
|
// "errors": [
|
||||||
// "errors": [
|
// "invalid id",
|
||||||
// "invalid id",
|
// "invalid name",
|
||||||
// "invalid name",
|
// "invalid description"
|
||||||
// "invalid description"
|
// ],
|
||||||
// ],
|
// "message": "Unprocessable Entity",
|
||||||
// "message": "Unprocessable Entity",
|
// "status": 422
|
||||||
// "status": 422
|
// }
|
||||||
// }
|
|
||||||
//
|
|
||||||
type ErrorBuilder struct {
|
type ErrorBuilder struct {
|
||||||
errs []string
|
errs []string
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ export interface ServerResults {
|
||||||
items: any;
|
items: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServerValidationError {
|
||||||
|
field: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiSummary {
|
export interface ApiSummary {
|
||||||
build: Build;
|
build: Build;
|
||||||
health: boolean;
|
health: boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue