Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ validate := validator.New(validator.WithRequiredStructEnabled())
| file | Existing File |
| filepath | File Path |
| image | Image |
| audio | Audio |
| mimetype | MIME Type |
| isdefault | Is Default |
| len | Length |
| max | Maximum |
Expand Down
100 changes: 72 additions & 28 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ var (
"startsnotwith": startsNotWith,
"endsnotwith": endsNotWith,
"image": isImage,
"audio": isAudio,
"mimetype": isMIMEType,
"isbn": isISBN,
"isbn10": isISBN10,
"isbn13": isISBN13,
Expand Down Expand Up @@ -1670,6 +1672,63 @@ func isFile(fl FieldLevel) bool {
panic(fmt.Sprintf("Bad field type %s", field.Type()))
}

func detectFileMIMEType(field reflect.Value) (string, bool) {
switch field.Kind() {
case reflect.String:
filePath := field.String()
fileInfo, err := os.Stat(filePath)
if err != nil || fileInfo.IsDir() {
return "", false
}

file, err := os.Open(filePath)
if err != nil {
return "", false
}
defer func() {
_ = file.Close()
}()

mime, err := mimetype.DetectReader(file)
if err != nil {
return "", false
}

return mime.String(), true
}

panic(fmt.Sprintf("Bad field type %s", field.Type()))
}

func matchesMIMEType(mime, expected string) bool {
expectedType, expectedSubtype, ok := strings.Cut(strings.TrimSpace(expected), "/")
if !ok || expectedType == "" || expectedSubtype == "" {
return false
}

mimeType, mimeSubtype, ok := strings.Cut(mime, "/")
if !ok || mimeType == "" || mimeSubtype == "" {
return false
}

if expectedSubtype == "*" {
return mimeType == expectedType
}

return mimeType == expectedType && mimeSubtype == expectedSubtype
}

// isMIMEType is the validation function for validating if the current field's value contains the path to a file
// whose detected MIME type matches the provided validator param in the form type/subtype or type/*.
func isMIMEType(fl FieldLevel) bool {
mime, ok := detectFileMIMEType(fl.Field())
if !ok {
return false
}

return matchesMIMEType(mime, fl.Param())
}

// isImage is the validation function for validating if the current field's value contains the path to a valid image file
func isImage(fl FieldLevel) bool {
mimetypes := map[string]bool{
Expand Down Expand Up @@ -1698,39 +1757,24 @@ func isImage(fl FieldLevel) bool {
"image/x-xpixmap": true,
"image/x-xwindowdump": true,
}
field := fl.Field()

switch field.Kind() {
case reflect.String:
filePath := field.String()
fileInfo, err := os.Stat(filePath)
if err != nil {
return false
}

if fileInfo.IsDir() {
return false
}

file, err := os.Open(filePath)
if err != nil {
return false
}
defer func() {
_ = file.Close()
}()
mime, ok := detectFileMIMEType(fl.Field())
if !ok {
return false
}

mime, err := mimetype.DetectReader(file)
if err != nil {
return false
}
_, ok = mimetypes[mime]
return ok
}

if _, ok := mimetypes[mime.String()]; ok {
return true
}
// isAudio is the validation function for validating if the current field's value contains the path to a valid audio file
func isAudio(fl FieldLevel) bool {
mime, ok := detectFileMIMEType(fl.Field())
if !ok {
return false
}

panic(fmt.Sprintf("Bad field type %s", field.Type()))
return strings.HasPrefix(mime, "audio/")
}

// isFilePath is the validation function for validating if the current field's value is a valid file path.
Expand Down
18 changes: 18 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,24 @@ This is done using os.Stat and github.com/gabriel-vasile/mimetype

Usage: image

# MIME type path

This validates that a string value contains a valid file path and that
the file exists on the machine and matches the provided MIME type in the
form type/subtype or type/*.
This is done using os.Stat and github.com/gabriel-vasile/mimetype

Usage: mimetype=image/png
Usage: mimetype=image/*

# Audio path

This validates that a string value contains a valid file path and that
the file exists on the machine and is an audio file.
This is done using os.Stat and github.com/gabriel-vasile/mimetype

Usage: audio

# File Path

This validates that a string value contains a valid file path but does not
Expand Down
10 changes: 10 additions & 0 deletions translations/ar/ar.go
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,16 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
translation: "يجب أن تكون {0} صورة صالحة",
override: false,
},
{
tag: "audio",
translation: "يجب أن يكون {0} ملفًا صوتيًا صالحًا",
override: false,
},
{
tag: "mimetype",
translation: "يجب أن يكون {0} نوع MIME صالحًا",
override: false,
},
{
tag: "cve",
translation: "يجب أن يكون {0} معرف CVE صالح",
Expand Down
10 changes: 10 additions & 0 deletions translations/ar/ar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ func TestTranslations(t *testing.T) {
PostCodeCountry string
PostCodeByField string `validate:"postcode_iso3166_alpha2_field=PostCodeCountry"`
Image string `validate:"image"`
Audio string `validate:"audio"`
MIMEType string `validate:"mimetype=image/png"`
BooleanString string `validate:"boolean"`
CveString string `validate:"cve"`
FQDN string `validate:"fqdn"`
Expand Down Expand Up @@ -709,6 +711,14 @@ func TestTranslations(t *testing.T) {
ns: "Test.Image",
expected: "يجب أن تكون Image صورة صالحة",
},
{
ns: "Test.Audio",
expected: "يجب أن يكون Audio ملفًا صوتيًا صالحًا",
},
{
ns: "Test.MIMEType",
expected: "يجب أن يكون MIMEType نوع MIME صالحًا",
},
{
ns: "Test.BooleanString",
expected: "يجب أن يكون BooleanString قيمة منطقية صالحة",
Expand Down
10 changes: 10 additions & 0 deletions translations/de/de.go
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,16 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
translation: "{0} muss ein Bild sein",
override: false,
},
{
tag: "audio",
translation: "{0} muss eine gultige Audiodatei sein",
override: false,
},
{
tag: "mimetype",
translation: "{0} muss ein gueltiger MIME-Typ sein",
override: false,
},
{
tag: "cve",
translation: "{0} muss eine gültige CVE-Kennung sein",
Expand Down
10 changes: 10 additions & 0 deletions translations/de/de_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ func TestTranslations(t *testing.T) {
PostCodeByField string `validate:"postcode_iso3166_alpha2_field=PostCodeCountry"`
BooleanString string `validate:"boolean"`
Image string `validate:"image"`
Audio string `validate:"audio"`
MIMEType string `validate:"mimetype=image/png"`
CveString string `validate:"cve"`
}

Expand Down Expand Up @@ -788,6 +790,14 @@ func TestTranslations(t *testing.T) {
ns: "Test.Image",
expected: "Image muss ein Bild sein",
},
{
ns: "Test.Audio",
expected: "Audio muss eine gultige Audiodatei sein",
},
{
ns: "Test.MIMEType",
expected: "MIMEType muss ein gueltiger MIME-Typ sein",
},
{
ns: "Test.CveString",
expected: "CveString muss eine gültige CVE-Kennung sein",
Expand Down
10 changes: 10 additions & 0 deletions translations/en/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,16 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er
translation: "{0} must be a valid image",
override: false,
},
{
tag: "audio",
translation: "{0} must be a valid audio file",
override: false,
},
{
tag: "mimetype",
translation: "{0} must be a valid MIME type",
override: false,
},
{
tag: "cve",
translation: "{0} must be a valid cve identifier",
Expand Down
Loading