diff --git a/.gitignore b/.gitignore index a300c84..fb193ad 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ vendor/ *.cache *.bak *.old +/test # Build directories /dist/ diff --git a/README.md b/README.md index 9813337..1a31dde 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,10 @@ func Upload(c *gin.Context) { if isLast { // You can perform your own logic here, before return 200 status, // it's better to remove chunks which is uploaded and doesn't use anymore - fstream.RemoveUploadedFile(&fileReq) + err = fstream.RemoveUploadedFile("/dir", "filename.jpeg") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + } } c.JSON(http.StatusOK, gin.H{"message": "file chunk processed"}) diff --git a/file.go b/file.go index adc0cf2..520a02c 100644 --- a/file.go +++ b/file.go @@ -44,8 +44,8 @@ type RFileRequest struct { } // uniqueName function generates unique string using UUID -func uniqueName(request *RFileRequest) string { - ext := filepath.Ext(request.UploadFile.Filename) +func uniqueName(fileName string) string { + ext := filepath.Ext(fileName) id, err := uuid.NewUUID() if err != nil { @@ -55,20 +55,26 @@ func uniqueName(request *RFileRequest) string { return fmt.Sprintf("%s%s", id.String(), ext) } -// RemoveUploadedFile function removes uploaded file from uploaded directory, it takes param and returns nothing: +// RemoveUploadedFile function removes uploaded file from uploaded directory and returns error if something went wrong: // // Takes: // -// - RFileRequest struct +// - 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 // // Use this function in your handler after file is uploaded -func RemoveUploadedFile(r *RFileRequest) { - filePath := filepath.Join(r.UploadDirectory, r.UploadFile.Filename) +func RemoveUploadedFile(uploadDir, fileName string) error { + filePath := filepath.Join(uploadDir, fileName) - e := os.Remove(filePath) - if e != nil { - log.Printf("error removing file: %v", e) + err := os.Remove(filePath) + if err != nil { + return err } + + return nil } // prettyByteSize function is used to concrete the file size @@ -134,7 +140,7 @@ func StoreChunk(r *RFileRequest) (*File, error) { // Check if FileUniqueName field is true to generate unique name for file if r.FileUniqueName { - uName := uniqueName(r) + uName := uniqueName(r.UploadFile.Filename) rFile.FileUniqueName = &uName } diff --git a/file_test.go b/file_test.go index e78dece..71416f9 100644 --- a/file_test.go +++ b/file_test.go @@ -1,8 +1,182 @@ package fstream -import "testing" +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "mime/multipart" + "os" + "path/filepath" + "testing" +) -func FuzzStoreChunk(f *testing.F) { - f.Fuzz(func(t *testing.T, data []byte) { - }) +func TestIsAllowedExtension(t *testing.T) { + testCases := []struct { + fileExtension []string + fileName string + expected bool + expectedName string + }{ + { + fileExtension: []string{".jpeg", ".jpg"}, + fileName: "test.jpeg", + expected: true, + expectedName: "Success", + }, + { + fileExtension: []string{".png", ".webp"}, + fileName: "test.jpg", + expected: false, + expectedName: "Failed", + }, + } + + for _, tc := range testCases { + t.Run(tc.expectedName, func(t *testing.T) { + res := IsAllowExtension(tc.fileExtension, tc.fileName) + assert.Equal(t, tc.expected, res) + }) + } +} + +func TestRemoveUploadedFile(t *testing.T) { + // create test directory + err := os.MkdirAll("test", 0777) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("test") + + // simulate file upload + testFileName := "testfile.txt" + testFilePath := filepath.Join("test", testFileName) + _, err = os.Create(testFilePath) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + uploadDir string + fileName string + expected error + expectedName string + }{ + { + uploadDir: "test", + fileName: testFileName, + expected: nil, + expectedName: "Success", + }, + } + + for _, tc := range testCases { + t.Run(tc.expectedName, func(t *testing.T) { + err = RemoveUploadedFile(tc.uploadDir, tc.fileName) + assert.Equal(t, tc.expected, err) + + if _, statErr := os.Stat(testFilePath); !os.IsNotExist(statErr) { + t.Errorf("File %s was not removed", testFilePath) + } + }) + } +} + +func TestStoreChunk(t *testing.T) { + testCases := []struct { + name string + fileContent []byte + maxRange int + fileUniqueName bool + expectError bool + expectedFileSize int + }{ + { + name: "Successful file upload with unique name", + fileContent: []byte("This is a test chunk"), + maxRange: 19, // Full file uploaded + fileUniqueName: true, + expectError: false, + expectedFileSize: 19, + }, + { + name: "Successful file upload without unique name", + fileContent: []byte("Another test chunk"), + maxRange: 20, // Full file uploaded + fileUniqueName: false, + expectError: false, + expectedFileSize: 20, + }, + { + name: "Partial upload", + fileContent: []byte("Partial chunk"), + maxRange: 7, // Partial file uploaded + fileUniqueName: false, + expectError: false, + expectedFileSize: 0, // File should not be finalized + }, + { + name: "Error due to maxRange exceeding file size", + fileContent: []byte("Invalid max range"), + maxRange: 50, // maxRange > FileSize + fileUniqueName: false, + expectError: true, + expectedFileSize: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + file, err := os.CreateTemp("", "") + if err != nil { + assert.Error(t, err) + } + defer os.Remove(file.Name()) + + _, err = file.Write(tc.fileContent) + if err != nil { + assert.Error(t, err) + } + defer file.Close() + + multipartFile, err := os.Open(file.Name()) + if err != nil { + assert.Error(t, err) + } + defer multipartFile.Close() + + fileHeader := &multipart.FileHeader{ + Filename: filepath.Base(file.Name()), + Size: int64(len(tc.fileContent)), + } + + r := &RFileRequest{ + File: multipartFile, + UploadFile: fileHeader, + MaxRange: tc.maxRange, + FileSize: len(tc.fileContent), + UploadDirectory: t.TempDir(), // Temporary directory for test + FileUniqueName: tc.fileUniqueName, + } + + resFile, err := StoreChunk(r) + if err != nil { + assert.Error(t, err) + } + + expectedFilePath := filepath.Join(r.UploadDirectory + fileHeader.Filename) + fmt.Printf("Expected Path: %s\n", expectedFilePath) + + assert.FileExists(t, expectedFilePath) + + actualContent, err := os.ReadFile(expectedFilePath) + require.NoError(t, err) + assert.Equal(t, tc.fileContent, actualContent) + + if resFile != nil { + assert.Equal(t, fileHeader.Filename, resFile.FileName) + assert.Equal(t, expectedFilePath, resFile.FilePath) + assert.Equal(t, filepath.Ext(fileHeader.Filename), resFile.FileExtension) + } + }) + } } diff --git a/go.mod b/go.mod index c22a034..9b61065 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module github.com/Nicolas-ggd/filestream go 1.23.0 require github.com/google/uuid v1.6.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 7790d7c..068e12a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,11 @@ +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/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= +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=