diff --git a/http/filestream/filestream.go b/http/filestream/filestream.go index f5f6eb5..a6b2ca2 100644 --- a/http/filestream/filestream.go +++ b/http/filestream/filestream.go @@ -1,6 +1,8 @@ package filestream import ( + "bytes" + "encoding/json" "errors" "fmt" "io" @@ -11,9 +13,15 @@ import ( ) const ( - FileType = "file" + FileType = "file" + ErrorType = "error" ) +type MultipartError struct { + FileName string `json:"file_name"` + ErrMessage string `json:"error_message"` +} + // The expected type of function that should be provided to the ReadFilesFromStream func, that returns the writer that should handle each file type FileWriterFunc func(fileName string) (writers []io.WriteCloser, err error) @@ -60,27 +68,44 @@ type FileInfo struct { } func WriteFilesToStream(multipartWriter *multipart.Writer, filesList []*FileInfo) (err error) { + // Close finishes the multipart message and writes the trailing + // boundary end line to the output, thereby marking the EOF. + defer ioutils.Close(multipartWriter, &err) for _, file := range filesList { if err = writeFile(multipartWriter, file); err != nil { - return + return writeErr(multipartWriter, file, err) } } - // Close finishes the multipart message and writes the trailing - // boundary end line to the output. - return multipartWriter.Close() + return nil } func writeFile(multipartWriter *multipart.Writer, file *FileInfo) (err error) { fileReader, err := os.Open(file.Path) if err != nil { - return fmt.Errorf("failed opening %q: %w", file, err) + return fmt.Errorf("failed opening file %q: %w", file.Name, err) } defer ioutils.Close(fileReader, &err) fileWriter, err := multipartWriter.CreateFormFile(FileType, file.Name) if err != nil { - return fmt.Errorf("failed to CreateFormFile: %w", err) + return fmt.Errorf("failed to create form file for %q: %w", file.Name, err) } _, err = io.Copy(fileWriter, fileReader) return err } + +func writeErr(multipartWriter *multipart.Writer, file *FileInfo, writeFileErr error) error { + fileWriter, err := multipartWriter.CreateFormField(ErrorType) + if err != nil { + return fmt.Errorf("failed to create form field: %w", err) + } + + multipartErr := MultipartError{FileName: file.Name, ErrMessage: writeFileErr.Error()} + multipartErrJSON, err := json.Marshal(multipartErr) + if err != nil { + return fmt.Errorf("failed to marshal multipart error for file %q: %w", file.Name, err) + } + + _, err = io.Copy(fileWriter, bytes.NewReader(multipartErrJSON)) + return err +} diff --git a/http/filestream/filestream_test.go b/http/filestream/filestream_test.go index a5203b4..8d9e520 100644 --- a/http/filestream/filestream_test.go +++ b/http/filestream/filestream_test.go @@ -2,6 +2,7 @@ package filestream import ( "bytes" + "encoding/json" "io" "mime/multipart" "os" @@ -9,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var targetDir string @@ -46,6 +48,31 @@ func TestWriteFilesToStreamAndReadFilesFromStream(t *testing.T) { assert.Equal(t, file2Content, content) } +func TestWriteFilesToStreamWithError(t *testing.T) { + nonExistentFileName := "nonexistent.txt" + // Create a FileInfo with a non-existent file + file := &FileInfo{Name: nonExistentFileName, Path: nonExistentFileName} + + // Create a buffer and a multipart writer + body := &bytes.Buffer{} + multipartWriter := multipart.NewWriter(body) + + // Call WriteFilesToStream and expect an error + err := WriteFilesToStream(multipartWriter, []*FileInfo{file}) + require.NoError(t, err) + + multipartReader := multipart.NewReader(body, multipartWriter.Boundary()) + form, err := multipartReader.ReadForm(10 * 1024) + require.NoError(t, err) + + assert.Len(t, form.Value[ErrorType], 1) + var multipartErr MultipartError + assert.NoError(t, json.Unmarshal([]byte(form.Value[ErrorType][0]), &multipartErr)) + + assert.Equal(t, nonExistentFileName, multipartErr.FileName) + assert.NotEmpty(t, multipartErr.ErrMessage) +} + func simpleFileWriter(fileName string) (fileWriter []io.WriteCloser, err error) { writer, err := os.Create(filepath.Join(targetDir, fileName)) if err != nil {