diff --git a/README.md b/README.md index fa1856f..fff9894 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,86 @@ Basic usage example: See godoc for more info. + +Basic usage example: Image Validation with different formats + + func uploadFile(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(10 << 20) + file, fileHeader, err := r.FormFile("myFile") + if err != nil { + fmt.Println("Error Retrieving the File") + fmt.Println(err) + return + } + defer file.Close() + validator := validate.New() + validator.IsImage("myFile", fileHeader, "") OR + validator.IsImage("myFile", fileHeader, "jpeg") OR + validator.IsImage("myFile", fileHeader, "jpeg, png") + if validator.HasErrors() { + fmt.Fprintf(w, validator.String()) + } + } + +For Image Dimensions (Width and Heights) + + func uploadFile(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(10 << 20) + file, fileHeader, err := r.FormFile("myFile") + if err != nil { + fmt.Println("Error Retrieving the File") + fmt.Println(err) + return + } + defer file.Close() + validator := validate.New() + minDim := validate.ImageDimension{Width: 200, Height: 300} //minimum dimension + maxDim := validate.ImageDimension{Width: 300, Height: 350} //maximum dimension + validator.ImageDimensions("myFile", fileHeader, &minDim, &maxDim, "") + if validator.HasErrors() { + fmt.Fprintf(w, validator.String()) + } + } + + +You can also validate different file mime types. +For more information on mime types, visit: + +https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types +https://www.iana.org/assignments/media-types/media-types.xhtml + +File Mime type Validation: + + func uploadFile(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(10 << 20) + file, fileHeader, err := r.FormFile("myFile") + if err != nil { + fmt.Println("Error Retrieving the File") + fmt.Println(err) + return + } + defer file.Close() + validator := validate.New() + validator.FileMimeType("myFile", fileHeader, "application/pdf", "") + if validator.HasErrors() { + fmt.Fprintf(w, validator.String()) + } + } + +For File Sizes in bytes: + + func uploadFile(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(10 << 20) + file, fileHeader, err := r.FormFile("myFile") + if err != nil { + fmt.Println("Error Retrieving the File") + fmt.Println(err) + return + } + defer file.Close() + validator := validate.New() + validator.FileSize("myFile", fileHeader, 100000, 200000, "") + if validator.HasErrors() { + fmt.Fprintf(w, validator.String()) + } + } \ No newline at end of file diff --git a/files.go b/files.go new file mode 100644 index 0000000..d027742 --- /dev/null +++ b/files.go @@ -0,0 +1,111 @@ +package validate + +import ( + "fmt" + "image" + + //For image encoding + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "math" + "mime/multipart" + + "strings" +) + +//Supported Image Formats/Mime Types +var ( + supportedImageFormats = map[string]string{ + "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "jpg": "image/jpeg", + } +) + +//ImageDimension represents width and height of an image dimension in pixels +//This is required by image dimension validation +type ImageDimension struct { + Width int + Height int +} + +//isFileImage confirms if this file is and Image of jpeg, png, gif +//format should be separated by comma +func isFileImage(uploadedType, format string) bool { + + //Required format supplied + if format != "" { + //Check format is defined in supported format map + requiredFormat, ok := supportedImageFormats[format] + + if ok && (strings.TrimSpace(requiredFormat) == uploadedType) { + return true + } + //Try splitting the required format in case of multiple formats + formatsArray := strings.Split(format, ",") + //Iterate through splitted formats + for _, val := range formatsArray { + requiredFormat, ok := supportedImageFormats[val] + if ok && (strings.TrimSpace(requiredFormat) == uploadedType) { + return true + } + } + //return false if not match is found + return false + } + //Check if the file is an image + for _, requiredFormat := range supportedImageFormats { + + if requiredFormat == uploadedType { + return true + } + } + + return false +} + +//getDimensions returns the dimensions of the uploaded image +func getDimension(fileHeader *multipart.FileHeader) (*ImageDimension, error) { + file, err := fileHeader.Open() + if err != nil { + return nil, fmt.Errorf("Error getting image dimension." + err.Error()) + } + //Reset File + _, err = file.Seek(0, 0) + if err != nil { + panic(err) + } + // buf := bufio.NewReader(file) + + img, _, err := image.DecodeConfig(file) + + if err != nil { + fmt.Println(err.Error(), file) + return nil, fmt.Errorf("Error getting image dimension." + err.Error()) + } + + return &ImageDimension{img.Width, img.Height}, nil +} + +//isFileMimeTypeValid confirms if the supplied mime type matches that of the image +func isFileMimeTypeValid(uploadedMimeType, requiresMimeType string) bool { + //Split mimetype to individual values + mimeTypeArray := strings.Split(requiresMimeType, ",") + //Check for single value mimetype + if len(mimeTypeArray) == 0 && uploadedMimeType == strings.TrimSpace(requiresMimeType) { + return true + } + + //Iterate through splitted types to determine if mimeType matches + for _, mimeType := range mimeTypeArray { + if strings.TrimSpace(mimeType) == uploadedMimeType { + return true + } + } + //return false on no match + return false +} + +//Convert bytes to kilobytes +func bytesToKiloBytes(byteData int64) float64 { + return math.Ceil(float64(byteData) / 1024) +} diff --git a/files_test.go b/files_test.go new file mode 100644 index 0000000..704a843 --- /dev/null +++ b/files_test.go @@ -0,0 +1,569 @@ +package validate + +import ( + "bytes" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" + "reflect" + "testing" + + "github.com/jung-kurt/gofpdf" +) + +var ( + testImageDir = "test_images/" + png2000x2000 = "test_png_2000_2000.png" + jpeg2000x2000 = "test_jpeg_2000_2000.jpg" + gif2000x2000 = "test_gif_2000_2000.gif" + jpeg1000x2000 = "test_jpeg_1000_1000.jpg" +) + +//Test and Confirm Images Formats +func TestImageFormatValidation(t *testing.T) { + //Test Images + jpegFile, pngFile, gifFile := getTestImages(2000, 2000) + + textFile := prepareFileHeader(makeOtherFiles("text_1.txt", "text/plain", "New text")) + + tests := []struct { + testname string + val func(Validator) + wantErrors map[string][]string + }{ + { + "jpeg ok", + func(v Validator) { v.IsImage("k", jpegFile, "JPEG", "") }, + make(map[string][]string), + }, + { + "png ok", + func(v Validator) { v.IsImage("k", pngFile, "PNG", "") }, + make(map[string][]string), + }, + { + "gif ok", + func(v Validator) { v.IsImage("k", gifFile, "Gif", "") }, + make(map[string][]string), + }, + + //Wrong Image Format + { + "jpeg in, png wanted", + func(v Validator) { v.IsImage("k", jpegFile, "PNG", "") }, + map[string][]string{"k": {"must be an image of 'PNG' format"}}, + }, + { + "png in, jpeg wanted", + func(v Validator) { v.IsImage("k", pngFile, "JPEG", "") }, + map[string][]string{"k": {"must be an image of 'JPEG' format"}}, + }, + { + "gif in, png wanted", + func(v Validator) { v.IsImage("k", gifFile, "PNG", "") }, + map[string][]string{"k": {"must be an image of 'PNG' format"}}, + }, + + { + "textfile in, png wanted", + func(v Validator) { v.IsImage("k", textFile, "PNG", "") }, + map[string][]string{"k": {"must be an image of 'PNG' format"}}, + }, + { + "textfile in, png wanted, custom error", + func(v Validator) { v.IsImage("k", textFile, "PNG", "Error") }, + map[string][]string{"k": {"Error"}}, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + v := New() + tt.val(v) + + if !reflect.DeepEqual(v.Errors, tt.wantErrors) { + t.Errorf("\nname:%s \nout: %#v\nwant: %#v\n", tt.testname, v.Errors, tt.wantErrors) + } + }) + } +} + +func TestImageMaxDimensionValidation(t *testing.T) { + //Test Images + jpegFile, pngFile, gifFile := getTestImages(2000, 2000) + + jpegFile1000x2000 := prepareFileHeader(makeTestImage("image/jpeg", jpeg1000x2000, 1000, 2000)) + + textFile := prepareFileHeader(makeOtherFiles("text_1.txt", "text/plain", "New text")) + + tests := []struct { + testname string + val func(Validator) + wantErrors map[string][]string + }{ + { + "jpeg ok", + func(v Validator) { + v.ImageDimensions("k", jpegFile, &ImageDimension{2000, 2000}, nil, "") + }, + make(map[string][]string), + }, + { + "jpeg 1000x2000 ok", + func(v Validator) { + v.ImageDimensions("k", jpegFile1000x2000, nil, &ImageDimension{1000, 2000}, "") + }, + make(map[string][]string), + }, + { + "png ok", + func(v Validator) { + v.ImageDimensions("k", pngFile, &ImageDimension{2000, 2000}, &ImageDimension{2000, 2000}, "") + }, + make(map[string][]string), + }, + { + "gif ok", + func(v Validator) { + v.ImageDimensions("k", gifFile, nil, &ImageDimension{2000, 2000}, "") + }, + make(map[string][]string), + }, + //Wrong Image dimension + { + "jpeg 2000x2000 in, 5000x5000 wanted", + func(v Validator) { + v.ImageDimensions("k", jpegFile, &ImageDimension{5000, 5000}, nil, "") + }, + map[string][]string{"k": {"image dimension (W x H) cannot be less than '5000 x 5000' pixels"}}, + }, + { + "png 2000x2000 in, 3000x500 wanted", + func(v Validator) { + v.ImageDimensions("k", pngFile, &ImageDimension{3000, 500}, &ImageDimension{3000, 1000}, "") + }, + map[string][]string{"k": {"image dimension (W x H) must be between '3000 x 500' and '3000 x 1000' pixels"}}, + }, + { + "gif 2000x2000 in, 1000x1000 max wanted", + func(v Validator) { + v.ImageDimensions("k", gifFile, nil, &ImageDimension{1000, 1000}, "") + }, + map[string][]string{"k": {"image dimension (W x H) cannot be more than '1000 x 1000' pixels"}}, + }, + + { + "jpeg 1000x2000 in, with custome error", + func(v Validator) { + v.ImageDimensions("k", jpegFile1000x2000, &ImageDimension{3000, 2000}, nil, "Error") + }, + map[string][]string{"k": {"Error"}}, + }, + + { + "textfile in, png 1000x1000 wanted", + func(v Validator) { + v.ImageDimensions("k", textFile, nil, &ImageDimension{1000, 1000}, "") + }, + map[string][]string{"k": {"File is not an image. Only dimensions of image files can be determined."}}, + }, + { + "textfile in, image wanted, custom error", + func(v Validator) { + v.ImageDimensions("k", textFile, nil, &ImageDimension{1000, 1000}, "Error") + }, + map[string][]string{"k": {"File is not an image. Only dimensions of image files can be determined."}}, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + v := New() + tt.val(v) + + if !reflect.DeepEqual(v.Errors, tt.wantErrors) { + t.Errorf("\nname:%s \nout: %#v\nwant: %#v\n", tt.testname, v.Errors, tt.wantErrors) + } + }) + } +} + +func TestFileSizeValidation(t *testing.T) { + //Test Images + jpegFile, pngFile, gifFile := getTestImages(2000, 2000) + + //Create test Text File + textFile := prepareFileHeader(makeOtherFiles("text_1.txt", "text/plain", "New text")) + + tests := []struct { + testname string + val func(Validator) + wantErrors map[string][]string + }{ + { + "jpeg ok", + func(v Validator) { v.FileSize("k", jpegFile, jpegFile.Size, -1, "") }, + make(map[string][]string), + }, + { + "png ok", + func(v Validator) { v.FileSize("k", pngFile, 0, -1, "") }, + make(map[string][]string), + }, + { + "gif ok", + func(v Validator) { v.FileSize("k", gifFile, 0, 100000000, "") }, + make(map[string][]string), + }, + { + "text ok", + func(v Validator) { v.FileSize("k", textFile, 2, 100000000, "") }, + make(map[string][]string), + }, + { + "text no min&max sizes", + func(v Validator) { v.FileSize("k", textFile, -1, -1, "") }, + make(map[string][]string), + }, + + //Wrong File sizes + { + "jpeg needs twice the size", + func(v Validator) { v.FileSize("k", jpegFile, 2*jpegFile.Size, -1, "") }, + map[string][]string{"k": {fmt.Sprintf("file size cannot be less than '%.1f'KB", + bytesToKiloBytes(2*jpegFile.Size))}}, + }, + { + "png 1000 bytes max", + func(v Validator) { v.FileSize("k", pngFile, 100, 1000, "") }, + map[string][]string{"k": {fmt.Sprintf("file size cannot be larger than '%.1f'KB", bytesToKiloBytes(1000))}}, + }, + { + "text 10 bytes max, custom error", + func(v Validator) { v.FileSize("k", textFile, -1, 10, "Error") }, + map[string][]string{"k": {"Error"}}, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + v := New() + tt.val(v) + + if !reflect.DeepEqual(v.Errors, tt.wantErrors) { + t.Errorf("\nname:%s \nout: %#v\nwant: %#v\n", tt.testname, v.Errors, tt.wantErrors) + } + }) + } +} + +func TestFileMimeTypeValidation(t *testing.T) { + //Test Images + jpegFile, pngFile, gifFile := getTestImages(2000, 2000) + + //Create test Text File + textFile := prepareFileHeader(makeOtherFiles("text_1.txt", "text/plain", "New text")) + + pdfFile := prepareFileHeader(makeOtherFiles("test_pdf.pdf", "application/pdf", + " Lorem ipsum dolor sit amet, consectetur adipiscing elit.")) + + tests := []struct { + testname string + val func(Validator) + wantErrors map[string][]string + }{ + { + "jpeg ok", + func(v Validator) { v.FileMimeType("k", jpegFile, "image/jpeg, image/png", "") }, + make(map[string][]string), + }, + { + "png ok", + func(v Validator) { v.FileMimeType("k", pngFile, "image/png", "") }, + make(map[string][]string), + }, + { + "gif ok", + func(v Validator) { v.FileMimeType("k", gifFile, "image/gif, image/png", "") }, + make(map[string][]string), + }, + { + "text ok", + func(v Validator) { v.FileMimeType("k", textFile, "text/plain", "") }, + make(map[string][]string), + }, + { + "pdf ok", + func(v Validator) { v.FileMimeType("k", pdfFile, "application/pdf", "") }, + make(map[string][]string), + }, + + { + "jpeg, in image/png, want image/jpeg", + func(v Validator) { v.FileMimeType("k", pngFile, "image/jpeg", "") }, + map[string][]string{"k": {"must be a file of type 'image/jpeg'"}}, + }, + { + "png, in image/png, want image/jpeg,application/octet-stream", + func(v Validator) { v.FileMimeType("k", pngFile, "image/jpeg,application/octet-stream", "") }, + map[string][]string{"k": {"must be a file of type 'image/jpeg,application/octet-stream'"}}, + }, + { + "jpeg, in image/jpeg, want application/pdf", + func(v Validator) { v.FileMimeType("k", jpegFile, "application/pdf", "") }, + map[string][]string{"k": {"must be a file of type 'application/pdf'"}}, + }, + { + "pdf, in image/jpeg, want application/pd", + func(v Validator) { v.FileMimeType("k", jpegFile, "application/pdf", "Error") }, + map[string][]string{"k": {"Error"}}, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + v := New() + tt.val(v) + + if !reflect.DeepEqual(v.Errors, tt.wantErrors) { + t.Errorf("\nname:%s \nout: %#v\nwant: %#v\n", tt.testname, v.Errors, tt.wantErrors) + } + }) + } +} + +//Empty File struct to implement file interface for multipart +type emptyFile struct{} + +//Mock empty reader for multipart file +func (f *emptyFile) Read(p []byte) (n int, err error) { + return 0, nil +} + +//Mock empty seek for multipart file +func (f *emptyFile) Seek(offset int64, whence int) (int64, error) { + return 0, nil +} //Mock empty close for multipart file +func (f *emptyFile) Close() error { + return nil +} //Mock empty readAt for multipart file +func (f *emptyFile) ReadAt(p []byte, off int64) (n int, err error) { + return 0, nil +} + +func TestFileRequired(t *testing.T) { + textFile := prepareFileHeader(makeOtherFiles("text_1.txt", "text/plain", "New text")) + + file, err := textFile.Open() + if err != nil { + panic(err) + } + defer closeFiles(file) + + tests := []struct { + testname string + val func(Validator) + wantErrors map[string][]string + }{ + { + "text ok", + func(v Validator) { v.Required("k", textFile, "") }, + make(map[string][]string), + }, + { + "File ok", + func(v Validator) { v.Required("k", file) }, + make(map[string][]string), + }, + { + "Data required", + func(v Validator) { v.Required("k", &multipart.FileHeader{}) }, + map[string][]string{"k": {"must be set"}}, + }, + + { + "Data required, custom error", + func(v Validator) { v.Required("k", &multipart.FileHeader{}, "Error") }, + map[string][]string{"k": {"Error"}}, + }, + { + "File empty", + func(v Validator) { v.Required("k", &emptyFile{}) }, + map[string][]string{"k": {"must be set"}}, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + v := New() + tt.val(v) + + if !reflect.DeepEqual(v.Errors, tt.wantErrors) { + t.Errorf("\nname:%s \nout: %#v\nwant: %#v\n", tt.testname, v.Errors, tt.wantErrors) + } + }) + } +} + +// +//--------------------------------------------------------- HELPER FUNCTIONS --------------------------- +// +//Create Files +func getTestImages(w, h int) (*multipart.FileHeader, *multipart.FileHeader, *multipart.FileHeader) { + // jpegFile, err := os.Open(makeTestImage("JPEG", jpeg2000x2000, w, h)) + + jpegFile := prepareFileHeader(makeTestImage("image/jpeg", jpeg2000x2000, w, h)) + + //Create PNG + pngFile := prepareFileHeader(makeTestImage("image/png", png2000x2000, w, h)) + + //Create GIF + gifFile := prepareFileHeader(makeTestImage("image/gif", gif2000x2000, w, h)) + + return jpegFile, pngFile, gifFile +} + +//Prepare multipart header from File +//This creates file request and returns multipart Header for testing +func prepareFileHeader(req *http.Request) *multipart.FileHeader { + + err := req.ParseMultipartForm(10 << 20) + if err != nil { + panic("Cannot parse request object: " + err.Error()) + } + + _, header, err := req.FormFile("test_file") + if err != nil { + panic("Erro retrieving file: " + err.Error()) + } + return header +} + +//Make For a Test +func makeTestImage(format, name string, w, h int) *http.Request { + + newImage := image.NewRGBA(image.Rect(0, 0, w, h)) + fullName := testImageDir + name + + file, err := os.Create(fullName) + if err != nil { + panic("Error creating image: \n" + err.Error()) + } + defer closeFiles(file) + + switch format { + case "GIF": + o := &gif.Options{NumColors: 10} + err := gif.Encode(file, newImage, o) + if err != nil { + panic(err) + } + case "JPEG": + o := jpeg.Options{Quality: 80} + err := jpeg.Encode(file, newImage, &o) + if err != nil { + panic(err) + } + default: + err := png.Encode(file, newImage) + if err != nil { + panic(err) + } + } + + return convertToRequest(fullName, format, file) +} + +//Create other files types for testing +func makeOtherFiles(name, format, content string) *http.Request { + fullName := testImageDir + name + + //Process PDF + if format == "pdf" { + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.AddPage() + pdf.Text(20, 20, content) + err := pdf.OutputFileAndClose(fullName) + if err != nil { + panic(err) + } + file, err := os.Open(fullName) + if err != nil { + panic("Error creating file: \n" + err.Error()) + } + defer closeFiles(file) + + return convertToRequest(fullName, format, file) + } + + //Create test file on file on Disk + file, err := os.Create(fullName) + if err != nil { + panic("Error creating file: \n" + err.Error()) + } + + defer closeFiles(file) + + _, err = file.Write([]byte(content)) + if err != nil { + panic("Error creating file: \n" + err.Error()) + } + + return convertToRequest(fullName, format, file) +} + +func convertToRequest(name, format string, file *os.File) *http.Request { + _, err := file.Seek(0, 0) + if err != nil { + panic(err) + } + //Convert to Request + var buff bytes.Buffer + + mw := multipart.NewWriter(&buff) + //header + hd := make(textproto.MIMEHeader) + hd.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "test_file", name)) + hd.Set("Content-Type", format) + + formFile, err := mw.CreatePart(hd) + + if err != nil { + panic("Error creating form file: " + err.Error()) + } + + _, err = file.Seek(0, 0) + if err != nil { + panic(err) + } + //Copy Files to form file + if _, err = io.Copy(formFile, file); err != nil { + panic("Error copying form file: " + err.Error()) + } + //Set Request Data + req, err := http.NewRequest("POST", "localhost", &buff) + if err != nil { + panic("Error creating request object: " + err.Error()) + } + // Don't forget to set the content type, this will contain the boundary. + req.Header.Set("Content-Type", mw.FormDataContentType()) + + closeFiles(mw) + + return req +} + +//Close Files +func closeFiles(c io.Closer) { + err := c.Close() + if err != nil { + panic("Error closing file:" + err.Error()) + } +} diff --git a/go.mod b/go.mod index fa25da2..3398262 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.2.0 github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/jung-kurt/gofpdf v1.16.2 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/teamwork/mailaddress v0.0.0-20180417011037-e0bce973c1a8 github.com/teamwork/test v0.0.0-20181126061546-2ff8918eb6a4 // indirect diff --git a/go.sum b/go.sum index 1827b08..3f6bcb7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= @@ -6,8 +8,15 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= +github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= +github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/teamwork/mailaddress v0.0.0-20180417011037-e0bce973c1a8 h1:/Dxrw8SmAa3TJHSQ437F2FUCj+9LpBIVNVbFcMfwIek= github.com/teamwork/mailaddress v0.0.0-20180417011037-e0bce973c1a8/go.mod h1:qvvPz5FrYe32YdNfKaeMGVBxDhQ2NKiIS8Y94O5ENVU= github.com/teamwork/test v0.0.0-20181126061546-2ff8918eb6a4 h1:/ujiGN1Gf1yBNvRoXSn/c24mbyjQN+r3nXOKLXfUt+A= @@ -16,5 +25,6 @@ github.com/teamwork/toutf8 v0.0.0-20180417010523-908c4b127591 h1:TzEYsThXaLGk7jO github.com/teamwork/toutf8 v0.0.0-20180417010523-908c4b127591/go.mod h1:3yhreNgI5hJ7gjWarHhHu59m31qe5oSL82QaBk9MAV8= github.com/teamwork/utils v0.0.0-20190114034940-d6a1f27ce92c h1:5/hkqtufOyLP25taIlo7BX9kLhw21unfjjdrOlwvFJk= github.com/teamwork/utils v0.0.0-20190114034940-d6a1f27ce92c/go.mod h1:rmPaJUVv426LGg3QR31m1N0bfpCdCVyh3dCWsJTQeDA= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/messages.go b/messages.go index d09817b..a96652f 100644 --- a/messages.go +++ b/messages.go @@ -2,22 +2,31 @@ package validate // Messages for the checkers; this can be changed for i18n. var ( - MessageRequired = "must be set" - MessageDomain = "must be a valid domain" - MessageURL = "must be a valid url" - MessageEmail = "must be a valid email address" - MessageIPv4 = "must be a valid IPv4 address" - MessageHexColor = "must be a valid color code" - MessageLenLonger = "must be longer than %d characters" - MessageLenShorter = "must be shorter than %d characters" - MessageExclude = "cannot be ‘%s’" - MessageInclude = "must be one of ‘%s’" - MessageInteger = "must be a whole number" - MessageBool = "must be a boolean" - MessageDate = "must be a date as ‘%s’" - MessagePhone = "must be a valid phone number" - MessageRangeHigher = "must be higher than %d" - MessageRangeLower = "must be lower than %d" + MessageRequired = "must be set" + MessageDomain = "must be a valid domain" + MessageURL = "must be a valid url" + MessageEmail = "must be a valid email address" + MessageIPv4 = "must be a valid IPv4 address" + MessageHexColor = "must be a valid color code" + MessageLenLonger = "must be longer than %d characters" + MessageLenShorter = "must be shorter than %d characters" + MessageExclude = "cannot be ‘%s’" + MessageInclude = "must be one of ‘%s’" + MessageInteger = "must be a whole number" + MessageBool = "must be a boolean" + MessageDate = "must be a date as ‘%s’" + MessagePhone = "must be a valid phone number" + MessageRangeHigher = "must be higher than %d" + MessageRangeLower = "must be lower than %d" + MessageNotAnImage = "must be an image" + MessageImageFormat = "must be an image of '%s' format" + MessageImageDimension = "image dimension (W x H) must be between '%d x %d' and '%d x %d' pixels" + MessageImageMinDimension = "image dimension (W x H) cannot be less than '%d x %d' pixels" + MessageImageMaxDimension = "image dimension (W x H) cannot be more than '%d x %d' pixels" + MessageFileMimeType = "must be a file of type '%s'" + MessageFileSize = "file size must be between '%.1f'KB and '%.1f'KB" + MessageFileMaxSize = "file size cannot be larger than '%.1f'KB" + MessageFileMinSize = "file size cannot be less than '%.1f'KB" ) func getMessage(in []string, def string) string { diff --git a/test_images/test_gif_2000_2000.gif b/test_images/test_gif_2000_2000.gif new file mode 100644 index 0000000..25891ef Binary files /dev/null and b/test_images/test_gif_2000_2000.gif differ diff --git a/test_images/test_jpeg_1000_1000.jpg b/test_images/test_jpeg_1000_1000.jpg new file mode 100644 index 0000000..856f2df Binary files /dev/null and b/test_images/test_jpeg_1000_1000.jpg differ diff --git a/test_images/test_jpeg_2000_2000.jpg b/test_images/test_jpeg_2000_2000.jpg new file mode 100644 index 0000000..25891ef Binary files /dev/null and b/test_images/test_jpeg_2000_2000.jpg differ diff --git a/test_images/test_pdf.pdf b/test_images/test_pdf.pdf new file mode 100644 index 0000000..b71a6b1 Binary files /dev/null and b/test_images/test_pdf.pdf differ diff --git a/test_images/test_png_2000_2000.png b/test_images/test_png_2000_2000.png new file mode 100644 index 0000000..25891ef Binary files /dev/null and b/test_images/test_png_2000_2000.png differ diff --git a/test_images/text_1.txt b/test_images/text_1.txt new file mode 100644 index 0000000..1f3a8cd --- /dev/null +++ b/test_images/text_1.txt @@ -0,0 +1 @@ +New text \ No newline at end of file diff --git a/validate.go b/validate.go index d4c54cd..28ed853 100644 --- a/validate.go +++ b/validate.go @@ -45,6 +45,7 @@ package validate // import "github.com/teamwork/validate" import ( "encoding/json" "fmt" + "mime/multipart" "net" "net/url" "reflect" @@ -257,6 +258,19 @@ func (v *Validator) Required(key string, value interface{}, message ...string) { if !nonEmpty { v.Append(key, msg) } + //Added Required for File + case *multipart.FileHeader: + _, err := val.Open() + if val == nil || val.Size <= 0 || err != nil { + v.Append(key, msg) + } + + case multipart.File: + b := make([]byte, 10) + num, _ := val.Read(b) + if num == 0 { + v.Append(key, msg) + } default: if vv := reflect.ValueOf(value); vv.Kind() == reflect.Ptr { if value == reflect.Zero(vv.Type()).Interface() { @@ -557,3 +571,239 @@ func (v *Validator) Range(key string, value, min, max int64, message ...string) } } } + +//IsImage checks if the file is an image. +//This only checks for JPEG, PNG or GIF MIME Header: +//It accepts pointer to multipart.FileHeader and formats(string) +//Multiple formats can be stated and should be separated by comma(','), eg: "jpeg, png, gif" +// For Example: +// func uploadFile(w http.ResponseWriter, r *http.Request) { +// r.ParseMultipartForm(10 << 20) +// file, fileHeader, err := r.FormFile("myFile") +// if err != nil { +// fmt.Println("Error Retrieving the File") +// fmt.Println(err) +// return +// } +// defer file.Close() +// validator := validate.New() +// validator.IsImage("myFile", fileHeader, "") OR +// validator.IsImage("myFile", fileHeader, "jpeg") OR +// validator.IsImage("myFile", fileHeader, "jpeg, png") +// if validator.HasErrors() { +// fmt.Fprintf(w, validator.String()) +// } +// } +func (v *Validator) IsImage(key string, + fileHeader *multipart.FileHeader, + formats string, message ...string) bool { + uploadedTypes := fileHeader.Header["Content-Type"] + msg := getMessage(message, "") + + for _, uploadedType := range uploadedTypes { + if isFileImage(uploadedType, strings.ToLower(formats)) { + return true + } + } + + //Append a different error if Image format is present + if msg != "" { + v.Append(key, msg) + } else if formats != "" { + v.Append(key, fmt.Sprintf(MessageImageFormat, formats)) + } else { + v.Append(key, MessageNotAnImage) + } + return false +} + +//ImageDimensions validates the maximum and minimum dimensions of an image in Pixels +//minDimension speficies the lower limit of the image dimension while maxDimension specifies the upper limit +//You can use nil to replace any of minDimension or maxDimension to indicate no limit +//It accepts pointer to multipart.FileHeader as the first parameter +//Both minDimension & maxDimension are of pointer to ImageDimension +// For Example: +// func uploadFile(w http.ResponseWriter, r *http.Request) { +// r.ParseMultipartForm(10 << 20) +// file, fileHeader, err := r.FormFile("myFile") +// if err != nil { +// fmt.Println("Error Retrieving the File") +// fmt.Println(err) +// return +// } +// defer file.Close() +// validator := validate.New() +// minDim := validate.ImageDimension{Width: 200, Height: 300} +// maxDim := validate.ImageDimension{Width: 300, Height: 350} +// validator.ImageDimensions("myFile", fileHeader, &minDim, &maxDim, "") +// if validator.HasErrors() { +// fmt.Fprintf(w, validator.String()) +// } +// } +func (v *Validator) ImageDimensions( + key string, + fileHeader *multipart.FileHeader, + minDimension, maxDimension *ImageDimension, + message ...string) { + + if maxDimension == nil && minDimension == nil { + panic("You must specify either minimum dimension or maximum dimension!") + } + + msg := getMessage(message, "") + + //Confirm Uploaded file is Image + if !v.IsImage(key, fileHeader, "", "File is not an image. Only dimensions of image files can be determined.") { + return + } + + //Get real dimension + realDim, err := getDimension(fileHeader) + + if err != nil { + v.Append(key, fmt.Sprintf("Error getting uploaded image dimension. Please try again.")) + return + } + //Init Error Message + var minDimErrorMsg, maxDimErrorMsg string = "", "" + + //Check for minimum dimension requirement + if minDimension != nil && (realDim.Width < minDimension.Width || realDim.Height < minDimension.Height) { + minDimErrorMsg = fmt.Sprintf(MessageImageMinDimension, minDimension.Width, minDimension.Height) + } + + //Check for maximum dimension requirements + if maxDimension != nil && (realDim.Width > maxDimension.Width || realDim.Height > maxDimension.Height) { + maxDimErrorMsg = fmt.Sprintf(MessageImageMaxDimension, maxDimension.Width, maxDimension.Height) + } + + //Append appropriate errors + if msg != "" { + v.Append(key, msg) + } else if minDimErrorMsg != "" && maxDimErrorMsg != "" { + v.Append(key, fmt.Sprintf(MessageImageDimension, + minDimension.Width, + minDimension.Height, + maxDimension.Width, + maxDimension.Height)) + } else if maxDimErrorMsg != "" { + v.Append(key, maxDimErrorMsg) + } else if minDimErrorMsg != "" { + v.Append(key, minDimErrorMsg) + } + +} + +//FileSize validates the minimum size ytes a file can be +//This can be used with any type of files as well as images +//It accepts pointer to multipart.FileHeader as the first parameter. +//Minimum and maximum sizes of files in integer +//Use '-1' for any size +// For Example: +// func uploadFile(w http.ResponseWriter, r *http.Request) { +// r.ParseMultipartForm(10 << 20) +// file, fileHeader, err := r.FormFile("myFile") +// if err != nil { +// fmt.Println("Error Retrieving the File") +// fmt.Println(err) +// return +// } +// defer file.Close() +// validator := validate.New() +// validator.FileSize("myFile", fileHeader, 100000, 200000, "") +// if validator.HasErrors() { +// fmt.Fprintf(w, validator.String()) +// } +// } +func (v *Validator) FileSize(key string, + fileHeader *multipart.FileHeader, + minSizeInBytes, maxSizeInBytes int64, + message ...string) { + + if minSizeInBytes == 0 && maxSizeInBytes == 0 { + panic("File size cannot be zeros. Minimum or maximum size in bytes must be specified") + } + //Return if all negative + if minSizeInBytes < 0 && maxSizeInBytes < 0 { + return + } + + msg := getMessage(message, "") + //initialise error messages + var minSizeErrMsg, maxSizeErrMsg = "", "" + + //For mimum size requirement + if minSizeInBytes >= 0 && fileHeader.Size < minSizeInBytes { + minSizeErrMsg = fmt.Sprintf(MessageFileMinSize, bytesToKiloBytes(minSizeInBytes)) + } + + //For maximum size requirement + if maxSizeInBytes >= 0 && fileHeader.Size > maxSizeInBytes { + maxSizeErrMsg = fmt.Sprintf(MessageFileMaxSize, bytesToKiloBytes(maxSizeInBytes)) + } + + //Append appropriate errors + if msg != "" { + v.Append(key, msg) + } else if minSizeErrMsg != "" && maxSizeErrMsg != "" { + v.Append(key, + fmt.Sprintf(MessageFileSize, bytesToKiloBytes(minSizeInBytes), + bytesToKiloBytes(maxSizeInBytes))) + } else if minSizeErrMsg != "" { + v.Append(key, minSizeErrMsg) + } else if maxSizeErrMsg != "" { + v.Append(key, maxSizeErrMsg) + } + +} + +//FileMimeType validates file mime type +//It accepts pointer to multipart.FileHeader as the first parameter, mime type(s) +//Multiple mimeType formats can be separated by comma Eg: "image/jpeg, text/csv, applications/pdf" +//For more information on mime types, visit: +//https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types +//https://www.iana.org/assignments/media-types/media-types.xhtml +// For Example: +// func uploadFile(w http.ResponseWriter, r *http.Request) { +// r.ParseMultipartForm(10 << 20) +// file, fileHeader, err := r.FormFile("myFile") +// if err != nil { +// fmt.Println("Error Retrieving the File") +// fmt.Println(err) +// return +// } +// defer file.Close() +// validator := validate.New() +// validator.FileMimeType("myFile", fileHeader, "application/pdf", "") +// if validator.HasErrors() { +// fmt.Fprintf(w, validator.String()) +// } +// } +func (v *Validator) FileMimeType(key string, + fileHeader *multipart.FileHeader, + requiredMimeTypes string, + message ...string) { + + if requiredMimeTypes == "" { + panic("Required mime type cannot be empty.") + } + //Get uploaded content type + uploadedTypes := fileHeader.Header["Content-Type"] + msg := getMessage(message, "") + + //Compare each with supllied Mime Type + for _, uploadedType := range uploadedTypes { + fmt.Println(uploadedTypes) + //Return once there is a match + if isFileMimeTypeValid(uploadedType, requiredMimeTypes) { + return + } + } + + //Append a different error if Image format is present + if msg != "" { + v.Append(key, msg) + } else { + v.Append(key, fmt.Sprintf(MessageFileMimeType, requiredMimeTypes)) + } +}