Skip to content

Commit

Permalink
[UPDATE] add exif removal function (#4) (#5)
Browse files Browse the repository at this point in the history
### What kind of change does this PR introduce?
Well this PR intoruces following minor changes:

- Updated documentation
- Add `Exif` metadata removal function, which is possible to use, add
test for that function
  • Loading branch information
Nicolas-ggd authored Dec 19, 2024
2 parents 48f26d9 + 995a06d commit ad8fc00
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 45 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ vendor/
*.cache
*.bak
*.old
/test
/_test
test
/uploads
uploads

# Build directories
/dist/
Expand Down
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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.
77 changes: 44 additions & 33 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package fstream

import (
"fmt"
"image"
"image/jpeg"
"io"
"log"
"math"
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -138,33 +128,25 @@ 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,
FilePath: filePath,
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))

Expand All @@ -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
}
64 changes: 64 additions & 0 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"image"
"image/jpeg"
"mime/multipart"
"os"
"path/filepath"
Expand Down Expand Up @@ -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)
}
})
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=

0 comments on commit ad8fc00

Please sign in to comment.