2

I'm trying to read files from a struct array but I'm unable to read it. Here is the postman.

Here is my code:

type MasterTemplate struct {
    ID       uuid.UUID `form:"id" json:"id"`
    IsVoice  bool      `form:"is_voice" json:"is_voice"`
    Title    string    `form:"title" json:"title"`
    IsSms    bool      `form:"is_sms" json:"is_sms"`
    FilePath string    `form:"file_path" json:"file_path"`
    Content  []struct {
        File             *multipart.FileHeader `form:"file" json:"file"`
        MsgTmplateLangId string                `form:"msg_template_lang_id" json:"msg_template_lang_id"`
        SMSContent       string                `form:"sms_content" json:"sms_content"`
        MsgContentID     string                `form:"msg_content_id" json:"msg_content_id"`
    } `form:"content" json:"content"`
    UserID uuid.UUID `form:"user_id" json:"user_id"`
}

func (tmplController *MessageTemplateController) CreateMsgTemplateController(ctx *gin.Context) {
    userID := ctx.Param("user_id")

    userUUID, err := uuid.Parse(userID)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        ctx.Abort()
        return
    }

    if ctx.Request.ContentLength == 0 {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": "Invalid/Empty body request",
        })
        ctx.Abort()
        return
    }

    err = ctx.Request.ParseMultipartForm(10 * 1024 * 1024)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    var msgTemplate *models.MasterTemplate

    if err := ctx.ShouldBind(&msgTemplate); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "Gin Error": err.Error(),
        })
        return
    }

    config, err := config.LoadConfig("credentials")
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "rerror": err.Error(),
        })
        return
    }

    
    for _, sms := range msgTemplate.Content {

        lanaguageID, err := uuid.Parse(sms.MsgTmplateLangId)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }

        templateContent := models.MessageTemplateContenet{
            ID:                uuid.New(),
            LanguageID:        lanaguageID,
            IsSms:             true,
            IsVoice:           false,
            FilePath:          "",
            Content:           sms.SMSContent,
            UserID:            userUUID,
            // MessageTemplateID: data.ID,
            IsDeleted:         false,
        }

        _, err = tmplController.contents.CreateMessageTemplateContent(&templateContent, ctx)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }
    }

    msgTemplate.IsVoice = true
    msgTemplate.IsSms = false

    msgTemplate.IsSms = false
    msgTemplate.IsVoice = true

    for x, voice := range msgTemplate.Content {
        fmt.Println(voice.MsgTmplateLangId)
        fmt.Println(x)
        fileHeader, err := ctx.FormFile(fmt.Sprintf("content[%c][file]", x))
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        file, err := voice.File.Open()
        if err != nil {
            ctx.JSON(400, gin.H{"error": "Failed to open uploaded file"})
            return
        }
        defer file.Close()
        fmt.Println(file)

        if !utilities.IsValidFile(fileHeader) {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": "Invalid file format. Only .mp3 and .wav files are allowed.",
            })
            return
        }
        if fileHeader.Size > 3*1024*1024 {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": "File size exceeds the limit. Maximum file size is 3MB.",
            })
            return
        }
        audioPath := uuid.New().String() + fileHeader.Filename
        uploadPath := filepath.Join("./msgtemplate", audioPath)
        // Save the uploaded file
        if err := ctx.SaveUploadedFile(fileHeader, uploadPath); err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        session, err := awsservice.S3BucketSessionHandler()
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        fileURL, err := awsservice.UploadFiles(session, uploadPath, config.AWS_AUDIO_BUCKET)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }
        lanaguageID, err := uuid.Parse(voice.MsgTmplateLangId)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }
        templateContent := models.MessageTemplateContenet{
            ID:         uuid.New(),
            LanguageID: lanaguageID,
            IsSms:      false,
            IsVoice:    true,
            IsDeleted:  false,
            FilePath:   uploadPath,
            Content:    *fileURL,
            UserID:     userUUID,
            // MessageTemplateID: data.ID,
        }

        _, err = tmplController.contents.CreateMessageTemplateContent(&templateContent, ctx)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "rerror": err.Error(),
            })
            return
        }

    }
}
eglease
  • 2,445
  • 11
  • 18
  • 28

3 Answers3

1

postman requestI see:

var msgTemplate *models.MasterTemplate

if err := ctx.ShouldBind(&msgTemplate); err != nil {
    ctx.JSON(http.StatusBadRequest, gin.H{
        "Gin Error": err.Error(),
    })
    return
}

You are trying to bind the incoming multipart file to your MasterTemplate struct.

Specifically, the following part of your struct is likely to be problematic:

Content  []struct {
    File             *multipart.FileHeader `form:"file" json:"file"`
    // other fields
} `form:"content" json:"content"`

The ShouldBind method may not be able to properly bind multipart files when they are included in a slice of structs in this way.

And when you are trying to open the file with:

file, err := voice.File.Open()
if err != nil {
    ctx.JSON(400, gin.H{"error": "Failed to open uploaded file"})
    return
}

voice.File comes from the previous struct binding, which may not have bound correctly, leading to issues when you try to open the file.

As seen in how to upload multipart file and json in Go with gin gonic?, for simple cases (when there is no slice of structs involved), the form binding works well with both text fields and files. But, while ShouldBind can handle multipart files within a struct, it might not correctly bind them when they are within a slice of structs, which is (from your code):

Content  []struct {
    File             *multipart.FileHeader `form:"file" json:"file"`
    MsgTmplateLangId string                `form:"msg_template_lang_id" json:"msg_template_lang_id"`
    SMSContent       string                `form:"sms_content" json:"sms_content"`
    MsgContentID     string                `form:"msg_content_id" json:"msg_content_id"`
} `form:"content" json:"content"`

When dealing with more complex scenarios, it may be safer to handle file data separately from other form data.
Generic example: "File Upload in Go with multipart/form-data" from Tanveer Shafee Prottoy .

You can fetch the files directly from the form data in the request using the ctx.MultipartForm() method and the form.File field:

form, _ := ctx.MultipartForm()
files := form.File["content[file]"]

You then iterate over files to process each file as required.
The full code would be:

func (tmplController *MessageTemplateController) CreateMsgTemplateController(ctx *gin.Context) {
    ...
    // Parse your form data manually
    form, _ := ctx.MultipartForm()
    files := form.File["content[file]"]

    // Iterate over the files in the form data
    for _, file := range files {
        // You can now access each file's data
        fmt.Println(file.Filename)
    }
    ...
}

In the above code, ctx.MultipartForm() is used to fetch the multipart form data from the request, and form.File["content[file]"] is used to access the files in the form data.

You will have to improve that code (for instance, I do not even check the files slice to ensure it is not empty before attempting to access its elements)


User So, how do I get the other JSON text appended or attached to the file?
Because with this I'm able to read the files, I'm also concerned with reading each text in the struct with the file.

Given that you are sending both files and associated JSON data in a multipart/form-data request, you would need a way to bind the data parts of the request and handle the files separately.

First, use ctx.ShouldBind to bind all textual form data to the MasterTemplate struct.

var msgTemplate models.MasterTemplate

if err := ctx.ShouldBind(&msgTemplate); err != nil {
    ctx.JSON(http.StatusBadRequest, gin.H{
        "Gin Error": err.Error(),
    })
    return
}

Then fetch files directly from the form data using ctx.MultipartForm() and associate them with the corresponding entry in the MasterTemplate.Content slice.

form, _ := ctx.MultipartForm()
files := form.File

// Iterating over files and binding them to msgTemplate.Content
for index, fileHeaderSlice := range files {
    if strings.HasPrefix(index, "content[") {
        // Extract the index from the string
        positionStr := strings.TrimSuffix(strings.TrimPrefix(index, "content["), "][file]")
        position, err := strconv.Atoi(positionStr)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{
                "error": "Invalid file index",
            })
            return
        }
        if position < len(msgTemplate.Content) && len(fileHeaderSlice) > 0 {
            msgTemplate.Content[position].File = fileHeaderSlice[0]
        }
    }
}

That should extract files from the request and associating them with the correct position in the msgTemplate.Content slice.

After this setup, the rest of your code where you work with msgTemplate remains mostly the same. You can access the File in each Content item, as they have now been correctly associated.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • So how do I get the other json text appended or attached to the file,because with this I'm able to read the files,I'm also concerned with reading each text in the struct with the file – Daniel Tenkorang Aug 06 '23 at 08:21
  • @DanielTenkorang I have edited the answer to address your comment. – VonC Aug 06 '23 at 10:25
  • How will the post request for the other request for the content look like in the postman? – Daniel Tenkorang Aug 06 '23 at 10:33
  • @DanielTenkorang You mean, create a POST request in Postman to send both files and the associated JSON data? – VonC Aug 06 '23 at 10:36
  • I have added the postman request – Daniel Tenkorang Aug 06 '23 at 10:39
  • @DanielTenkorang No problem, I have validated the edit. But don't forget to edit your own question, with image and (more importantly) text to illustrate what is missing. – VonC Aug 06 '23 at 10:43
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254820/discussion-between-vonc-and-daniel-tenkorang). – VonC Aug 06 '23 at 10:44
-1

postman requestIt seems like you are using the Gin framework to handle a multipart request that includes both form fields and file uploads. In your code snippet, you are defining a struct to bind the incoming request data and processing it accordingly.My main issue is how to include the information associated with the file, msg_template_lang_id but unable to read since when I print i get empty even if I read the file with ctx.Request.MultipartForm and then read the other part with shouldbind.

From the provided code, there are a few things that might lead to issues:

Binding Issue with the Nested Struct and Multipart File: In Gin, binding multipart files with nested struct could be tricky, especially when your form has a non-flat structure.

Incorrect Indexing in Loop: You're using x as an index and trying to convert it to a character using fmt.Sprintf("content[%c][file]", x). Since x is an integer, you need to convert it to a string with %d instead of %c.

for x, voice := range msgTemplate.Content {
    fmt.Println(voice.MsgTmplateLangId)
    fmt.Println(x)
    fileHeader, err := ctx.FormFile(fmt.Sprintf("content[%d][file]", x))
    // rest of your code
}

Try to get the file directly from the request using the FormFile method. You might need to adjust the form's field names to match what you are sending from the client. Make sure the client is sending the file with the correct field name in the form-data. If you continue to face issues with binding, you might have to manually parse the multipart form data instead of relying on Gin's automatic binding. This would involve iterating through the multipart form data and manually handling each part, including the files.

pseudo-code:-

err = ctx.Request.ParseMultipartForm(10 * 1024 * 1024)
if err != nil {
    ctx.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
    })
    return
}

formdata := ctx.Request.MultipartForm

// Handle the fields
formfields := formdata.Value
// Extract values from formfields as needed

// Handle the files
files := formdata.File["content"]
for i, fileHeader := range files {
    file, err := fileHeader.Open()
    if err != nil {
        ctx.JSON(400, gin.H{"error": "Failed to open uploaded file"})
        return
    }
    defer file.Close()

    // Process the file as needed
}

Ensure that the Postman sends the multipart request properly, with each file part having the correct name according to what your server expects. You may want to print the request on the server side to see exactly what's being sent and adapt your code accordingly. Make sure that your Postman request is set up correctly. Make sure to select "form-data" under the "Body" tab and then enter the keys and values, including files, according to the structure expected by your server code.

Aman Khan
  • 34
  • 2
-1

postman request Below is the refactored code:

func (tmplController *MessageTemplateController) CreateMsgTemplateController(ctx *gin.Context) {
    err := ctx.Request.ParseMultipartForm(10 * 1024 * 1024)
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // We get the user ID from the route the we convert it to UUID 
    // We convert to UUID since that is the input type
    userUUID, err := uuid.Parse(ctx.Param("user_id"))
    if err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // We get the other parameters in the struct
    msgTemplate := models.MasterTemplate{
        ID:      ctx.Request.Form.Get("id"),
        Title:   ctx.Request.Form.Get("title"),
        IsSms:   ctx.Request.Form.Get("is_sms") == "true",
        IsVoice: ctx.Request.Form.Get("is_voice") == "true",
        UserID:  userUUID,
    }


    msgTemplate.Content = make([]models.Content, 0)
    for i := 0; ; i++ {
        fileInputName := fmt.Sprintf("content[%d][file]", i)
        if file, err := ctx.FormFile(fileInputName); err == nil {
            contentPrefix := fmt.Sprintf("content[%d]", i)
            msgTemplateLangIDKey := fmt.Sprintf("%s[msg_template_lang_id]", contentPrefix)
            smsContentKey := fmt.Sprintf("%s[sms_content]", contentPrefix)

            var fc models.Content
            fc.File = file
            fc.SMSContent = ctx.Request.Form.Get(smsContentKey)
            fc.MsgTmplateLangId = ctx.Request.Form.Get(msgTemplateLangIDKey)

            msgTemplate.Content = append(msgTemplate.Content, fc)
        } else {
            break
        }
    }

    // Now we can loop through the content to do what we want to now

    ctx.JSON(http.StatusOK, gin.H{
        "response": msgTemplate,
    })
}