From 995a06d765a1b6cb74fecc5ec160b778411ee747 Mon Sep 17 00:00:00 2001 From: Nicolas-ggd Date: Thu, 19 Dec 2024 21:46:28 +0400 Subject: [PATCH] [UPDATE] add exif removal function (#4) --- .gitignore | 5 +++- README.md | 31 +++++++++++++-------- file.go | 77 ++++++++++++++++++++++++++++++---------------------- file_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 5 ++++ 6 files changed, 139 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index fb193ad..96662c6 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,10 @@ vendor/ *.cache *.bak *.old -/test +/_test +test +/uploads +uploads # Build directories /dist/ diff --git a/README.md b/README.md index 1a31dde..180058d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/Nicolas-ggd/filestream)](https://goreportcard.com/report/github.com/Nicolas-ggd/filestream) ![Go Version](https://img.shields.io/github/go-mod/go-version/Nicolas-ggd/filestream) +[![Go Reference](https://pkg.go.dev/badge/github.com/Nicolas-ggd/filestream.svg)](https://pkg.go.dev/github.com/Nicolas-ggd/filestream) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FNicolas-ggd%2Ffilestream.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2FNicolas-ggd%2Ffilestream?ref=badge_shield&issueType=license) ![License](https://img.shields.io/github/license/Nicolas-ggd/filestream) ![Issues Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat) @@ -11,7 +12,10 @@ FileStream is an open-source project, which is aim to gain more experience in go ## Purpose of this project -This project was born from a desire to learn Go deeply while building something practical and useful. My aim is to enhance my skills and gain real world experience by working on an open-source project that can also attract contributions from others who share the same passion. This project is learning journey for me and developers also which decide to collaborating and create a reusable engine. +This project was born from a desire to learn Go deeply while building something practical and useful. The aim is to: +- Enhance skills in Go by working on a real-world project. +- Build a reusable, efficient engine for file uploads. +- Attract contributions from developers passionate about open-source collaboration. ## Roadmap @@ -35,16 +39,9 @@ Here’s how you can get involved: ``` git clone https://github.com/Nicolas-ggd/filestream ``` -2. Install dependencies: - ``` - make dependencies - ``` -3. Run tests: - ``` - make test - ``` ## Usage +Basic File Upload Example: ```go import( "github.com/gin-gonic/gin" @@ -102,8 +99,7 @@ func Upload(c *gin.Context) { } ``` -`fstream` offers extension check, all you need is that to provide which extension do you want to allow - +Validate File Extensions: ```go import( "github.com/Nicolas-ggd/filestream" @@ -119,6 +115,19 @@ func Upload(c *gin.Context) { // your logic goes here... } ``` + +Remove EXIF Metadata: +```go + import( + "github.com/Nicolas-ggd/filestream" + ) + + // filePath is the upload direction + filename + err := fstream.RemoveExifMetadata(filePath) + if err != nil { + log.Fatalln(err) + } +``` ## License FileStream is open-source software licensed under the MIT License. diff --git a/file.go b/file.go index 520a02c..02e5336 100644 --- a/file.go +++ b/file.go @@ -2,6 +2,8 @@ package fstream import ( "fmt" + "image" + "image/jpeg" "io" "log" "math" @@ -18,7 +20,7 @@ type File struct { // Original uploaded file name FileName string // FileUniqueName is unique name - FileUniqueName *string + FileUniqueName string // Uploaded file path FilePath string // Uploaded file extension @@ -55,16 +57,8 @@ func uniqueName(fileName string) string { return fmt.Sprintf("%s%s", id.String(), ext) } -// RemoveUploadedFile function removes uploaded file from uploaded directory and returns error if something went wrong: -// -// Takes: -// -// - uploadDir (string) - upload directory where file lives -// - fileName (string) - file name -// -// Returns: -// - error if something went wrong, in this case if file doesn't removed function returns error -// +// RemoveUploadedFile removes uploaded file from uploaded directory and returns error if something went wrong, +// it takes upload directory and fileName. // Use this function in your handler after file is uploaded func RemoveUploadedFile(uploadDir, fileName string) error { filePath := filepath.Join(uploadDir, fileName) @@ -91,13 +85,9 @@ func prettyByteSize(b int) string { return fmt.Sprintf("%.1fYiB", bf) } -// StoreChunk cares slice of chunks and returns final results and error -// -// - File - struct is final version about file information -// -// - error - functions cares about occurred errors and returns it. -// -// Function creates new directory for chunks if it doesn't exist, if directory already exists it appends received chunks in current chunks and if entire file is uploaded then File struct is returned +// StoreChunk cares slice of chunks and returns final results and error. +// Functions creates new directory for chunks if it doesn't exist, +// if directory already exists it appends received chunks in current chunks and if entire file is uploaded then File struct is returned func StoreChunk(r *RFileRequest) (*File, error) { var rFile *File @@ -138,12 +128,6 @@ func StoreChunk(r *RFileRequest) (*File, error) { // Calculate file size in bytes size := prettyByteSize(int(fileInfo.Size())) - // Check if FileUniqueName field is true to generate unique name for file - if r.FileUniqueName { - uName := uniqueName(r.UploadFile.Filename) - rFile.FileUniqueName = &uName - } - // Bind File struct and return rFile = &File{ FileName: r.UploadFile.Filename, @@ -151,20 +135,18 @@ func StoreChunk(r *RFileRequest) (*File, error) { FileExtension: filepath.Ext(r.UploadFile.Filename), FileSize: size, } + + // Check if FileUniqueName field is true to generate unique name for file + if r.FileUniqueName { + uName := uniqueName(r.UploadFile.Filename) + rFile.FileUniqueName = uName + } } return rFile, nil } -// IsAllowExtension function checks if file extension is allowed to upload, it takes following params -// -// - fileExtensions - array of strings, which is looks like: []string{".jpg", ".jpeg"}, note that this is fileExtensions which is allowed to receive -// -// - fileName - string, this parameter is file name which is like ".jpeg", ".jpg" -// -// Returns: -// -// - bool - function returns false if extension isn't allowed to receive, it returns true if extension is allowed to receive +// IsAllowExtension checks if a given file's extension is allowed based on a provided list of acceptable extensions. func IsAllowExtension(fileExtensions []string, fileName string) bool { ext := strings.ToLower(filepath.Ext(fileName)) @@ -177,3 +159,32 @@ func IsAllowExtension(fileExtensions []string, fileName string) bool { return false } + +// RemoveExifMetadata returns error if something went wrong during the exif metadata removal process, functions takes inputPath which is location of the image. +// purpose of this function is that to open and re-encode image without metadata +func RemoveExifMetadata(inputPath string) error { + // open input path file + file, err := os.Open(inputPath) + if err != nil { + return fmt.Errorf("failed to open image: %v", err) + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return fmt.Errorf("failed to decode image: %v", err) + } + + output, err := os.Create(inputPath) + if err != nil { + return fmt.Errorf("failed to create output file: %v", err) + } + defer output.Close() + + // re-encode image without metadata + if err = jpeg.Encode(output, img, &jpeg.Options{Quality: 100}); err != nil { + return fmt.Errorf("failed to encode image: %v", err) + } + + return nil +} diff --git a/file_test.go b/file_test.go index 71416f9..e4fad51 100644 --- a/file_test.go +++ b/file_test.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "image" + "image/jpeg" "mime/multipart" "os" "path/filepath" @@ -180,3 +182,65 @@ func TestStoreChunk(t *testing.T) { }) } } + +func TestRemoveExifMetadata(t *testing.T) { + testCases := []struct { + name string + setupFunc func() (string, error) + expectError bool + }{ + { + name: "Exif metadata removed successfully", + setupFunc: func() (string, error) { + file, err := os.CreateTemp("", "*.jpg") + if err != nil { + return "", err + } + defer file.Close() + + // create dummy image + img := image.NewRGBA(image.Rect(0, 0, 100, 100)) + if err := jpeg.Encode(file, img, nil); err != nil { + return "", err + } + return file.Name(), nil + }, + expectError: false, + }, + { + name: "failed to open image", + setupFunc: func() (string, error) { + return "invalid_path.jpg", nil + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + inputPath, err := tc.setupFunc() + if err != nil { + t.Fatal(err) + } + + err = RemoveExifMetadata(inputPath) + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + + // Validate output file + file, err := os.Open(inputPath) + assert.NoError(t, err) + defer file.Close() + + _, _, err = image.Decode(file) + assert.NoError(t, err) + } + + if _, err = os.Stat(inputPath); err == nil { + os.Remove(inputPath) + } + }) + } +} diff --git a/go.mod b/go.mod index 9b61065..daebe2a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,9 @@ require github.com/google/uuid v1.6.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/disintegration/imaging v1.6.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 068e12a..3034419 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,16 @@ 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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=