Multipart Form
Handle file uploads and multipart form data with Forge's built-in support
Forge provides comprehensive support for multipart form data, including file uploads, form fields, and advanced file handling. The framework handles parsing, validation, and storage of multipart data with built-in security features.
Multipart Form Interface
Forge's multipart form interface provides a clean API for handling form data:
type MultipartForm interface {
// Form fields
GetValue(key string) string
GetValues(key string) []string
// File handling
GetFile(key string) (*FileHeader, error)
GetFiles(key string) ([]*FileHeader, error)
// Form processing
Parse() error
RemoveAll() error
// Validation
Validate() error
}Basic File Upload
Simple File Upload Handler
func uploadHandler(ctx forge.Context) error {
// Parse multipart form
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid multipart form")
}
// Get uploaded file
file, err := form.GetFile("file")
if err != nil {
return forge.BadRequest("file is required")
}
// Validate file
if file.Size > 10*1024*1024 { // 10MB limit
return forge.BadRequest("file too large")
}
// Save file
dst, err := os.Create("uploads/" + file.Filename)
if err != nil {
return forge.InternalError("failed to save file")
}
defer dst.Close()
src, err := file.Open()
if err != nil {
return forge.InternalError("failed to open file")
}
defer src.Close()
_, err = io.Copy(dst, src)
if err != nil {
return forge.InternalError("failed to copy file")
}
return ctx.JSON(200, map[string]string{
"message": "File uploaded successfully",
"filename": file.Filename,
"size": fmt.Sprintf("%d bytes", file.Size),
})
}
// Register upload endpoint
app.Router().POST("/upload", uploadHandler)Multiple File Upload
func multipleUploadHandler(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid multipart form")
}
files, err := form.GetFiles("files")
if err != nil {
return forge.BadRequest("files are required")
}
var uploadedFiles []map[string]interface{}
for _, file := range files {
// Validate file
if file.Size > 5*1024*1024 { // 5MB limit per file
continue
}
// Save file
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), file.Filename)
dst, err := os.Create("uploads/" + filename)
if err != nil {
continue
}
defer dst.Close()
src, err := file.Open()
if err != nil {
continue
}
defer src.Close()
io.Copy(dst, src)
uploadedFiles = append(uploadedFiles, map[string]interface{}{
"original_name": file.Filename,
"saved_name": filename,
"size": file.Size,
"content_type": file.Header.Get("Content-Type"),
})
}
return ctx.JSON(200, map[string]interface{}{
"message": "Files uploaded successfully",
"files": uploadedFiles,
"count": len(uploadedFiles),
})
}Form Data Handling
Mixed Form Data and Files
type UploadRequest struct {
Title string `form:"title" validate:"required"`
Description string `form:"description"`
Category string `form:"category" validate:"required"`
}
func mixedUploadHandler(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid multipart form")
}
// Parse form fields
var req UploadRequest
if err := form.Bind(&req); err != nil {
return forge.BadRequest("invalid form data")
}
// Validate form data
if err := validate.Struct(req); err != nil {
return forge.BadRequest("validation failed")
}
// Get uploaded file
file, err := form.GetFile("file")
if err != nil {
return forge.BadRequest("file is required")
}
// Process upload
uploadResult := processUpload(req, file)
return ctx.JSON(200, uploadResult)
}Form Field Access
func formFieldsHandler(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid multipart form")
}
// Get single value
title := form.GetValue("title")
// Get multiple values (for checkboxes, etc.)
tags := form.GetValues("tags")
// Get all form data
formData := make(map[string]interface{})
for key, values := range form.Value {
if len(values) == 1 {
formData[key] = values[0]
} else {
formData[key] = values
}
}
return ctx.JSON(200, map[string]interface{}{
"title": title,
"tags": tags,
"form_data": formData,
})
}File Validation and Security
File Type Validation
func validateFileType(file *forge.FileHeader) error {
// Check file extension
ext := strings.ToLower(filepath.Ext(file.Filename))
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt"}
for _, allowedExt := range allowedExts {
if ext == allowedExt {
return nil
}
}
return errors.New("file type not allowed")
}
func secureUploadHandler(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid multipart form")
}
file, err := form.GetFile("file")
if err != nil {
return forge.BadRequest("file is required")
}
// Validate file type
if err := validateFileType(file); err != nil {
return forge.BadRequest("invalid file type")
}
// Check file size
if file.Size > 2*1024*1024 { // 2MB limit
return forge.BadRequest("file too large")
}
// Generate secure filename
secureFilename := generateSecureFilename(file.Filename)
// Save file
err = saveFileSecurely(file, secureFilename)
if err != nil {
return forge.InternalError("failed to save file")
}
return ctx.JSON(200, map[string]string{
"message": "File uploaded securely",
"filename": secureFilename,
})
}File Content Validation
func validateFileContent(file *forge.FileHeader) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Read first few bytes to check file signature
buffer := make([]byte, 512)
n, err := src.Read(buffer)
if err != nil && err != io.EOF {
return err
}
// Check file signature
contentType := http.DetectContentType(buffer[:n])
// Validate against expected content types
allowedTypes := []string{
"image/jpeg",
"image/png",
"image/gif",
"application/pdf",
"text/plain",
}
for _, allowedType := range allowedTypes {
if contentType == allowedType {
return nil
}
}
return errors.New("invalid file content")
}Advanced File Handling
File Processing Pipeline
type FileProcessor struct {
validators []FileValidator
processors []FileProcessor
storage FileStorage
}
func (fp *FileProcessor) ProcessFile(file *forge.FileHeader) (*ProcessedFile, error) {
// Validate file
for _, validator := range fp.validators {
if err := validator.Validate(file); err != nil {
return nil, err
}
}
// Process file
processedFile := &ProcessedFile{
OriginalName: file.Filename,
Size: file.Size,
ContentType: file.Header.Get("Content-Type"),
}
for _, processor := range fp.processors {
if err := processor.Process(file, processedFile); err != nil {
return nil, err
}
}
// Store file
if err := fp.storage.Store(processedFile); err != nil {
return nil, err
}
return processedFile, nil
}Image Processing
func processImage(file *forge.FileHeader) (*ProcessedImage, error) {
src, err := file.Open()
if err != nil {
return nil, err
}
defer src.Close()
// Decode image
img, format, err := image.Decode(src)
if err != nil {
return nil, err
}
// Get image dimensions
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
// Create thumbnails
thumbnails := make(map[string]string)
// Small thumbnail (150x150)
smallThumb := resizeImage(img, 150, 150)
smallThumbPath := saveThumbnail(smallThumb, "small")
thumbnails["small"] = smallThumbPath
// Medium thumbnail (300x300)
mediumThumb := resizeImage(img, 300, 300)
mediumThumbPath := saveThumbnail(mediumThumb, "medium")
thumbnails["medium"] = mediumThumbPath
return &ProcessedImage{
OriginalName: file.Filename,
Format: format,
Width: width,
Height: height,
Size: file.Size,
Thumbnails: thumbnails,
}, nil
}Storage Integration
Local Storage
type LocalStorage struct {
basePath string
}
func (ls *LocalStorage) Store(file *forge.FileHeader) (string, error) {
// Generate unique filename
filename := generateUniqueFilename(file.Filename)
filePath := filepath.Join(ls.basePath, filename)
// Create directory if it doesn't exist
if err := os.MkdirAll(ls.basePath, 0755); err != nil {
return "", err
}
// Save file
dst, err := os.Create(filePath)
if err != nil {
return "", err
}
defer dst.Close()
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
_, err = io.Copy(dst, src)
if err != nil {
return "", err
}
return filename, nil
}Cloud Storage Integration
type CloudStorage struct {
bucket string
client *storage.Client
}
func (cs *CloudStorage) Store(file *forge.FileHeader) (string, error) {
// Generate unique filename
filename := generateUniqueFilename(file.Filename)
// Create cloud storage object
obj := cs.client.Bucket(cs.bucket).Object(filename)
writer := obj.NewWriter(context.Background())
defer writer.Close()
// Set metadata
writer.ObjectAttrs.ContentType = file.Header.Get("Content-Type")
writer.ObjectAttrs.Metadata = map[string]string{
"original-name": file.Filename,
"upload-time": time.Now().Format(time.RFC3339),
}
// Copy file data
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
_, err = io.Copy(writer, src)
if err != nil {
return "", err
}
return filename, nil
}Configuration
Multipart Form Configuration
app := forge.NewApp(forge.AppConfig{
MultipartConfig: forge.MultipartConfig{
MaxMemory: 32 << 20, // 32MB
MaxFileSize: 10 << 20, // 10MB
MaxFiles: 10,
AllowedTypes: []string{"image/jpeg", "image/png", "application/pdf"},
UploadPath: "uploads/",
SecureMode: true,
},
})File Upload Limits
// Global limits
app.Router().Use(forge.MultipartLimitMiddleware(forge.MultipartLimit{
MaxMemory: 32 << 20, // 32MB
MaxFileSize: 5 << 20, // 5MB
MaxFiles: 5,
}))
// Route-specific limits
app.Router().POST("/upload", uploadHandler,
forge.WithMultipartLimit(forge.MultipartLimit{
MaxMemory: 64 << 20, // 64MB
MaxFileSize: 20 << 20, // 20MB
MaxFiles: 10,
}),
)Error Handling
Upload Error Handling
func uploadWithErrorHandling(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
app.Logger().Error("multipart form error", forge.F("error", err))
return forge.BadRequest("invalid multipart form")
}
file, err := form.GetFile("file")
if err != nil {
return forge.BadRequest("file is required")
}
// Validate file
if err := validateFile(file); err != nil {
app.Logger().Warn("file validation failed",
forge.F("filename", file.Filename),
forge.F("error", err),
)
return forge.BadRequest("file validation failed")
}
// Process file
result, err := processFile(file)
if err != nil {
app.Logger().Error("file processing failed",
forge.F("filename", file.Filename),
forge.F("error", err),
)
return forge.InternalError("file processing failed")
}
return ctx.JSON(200, result)
}Testing Multipart Forms
Unit Testing
func TestFileUpload(t *testing.T) {
app := forge.NewTestApp(forge.TestAppConfig{
Name: "test-app",
})
// Register upload handler
app.Router().POST("/upload", func(ctx forge.Context) error {
form, err := ctx.MultipartForm()
if err != nil {
return forge.BadRequest("invalid form")
}
file, err := form.GetFile("file")
if err != nil {
return forge.BadRequest("file required")
}
return ctx.JSON(200, map[string]string{
"filename": file.Filename,
"size": fmt.Sprintf("%d", file.Size),
})
})
// Test file upload
resp := app.Test().POST("/upload").
MultipartForm(map[string]interface{}{
"file": forge.TestFile{
Filename: "test.txt",
Content: []byte("test content"),
},
}).
Expect(t)
resp.Status(200)
resp.JSON().Object().Value("filename").Equal("test.txt")
}Integration Testing
func TestFileUploadIntegration(t *testing.T) {
app := forge.NewTestApp(forge.TestAppConfig{
Name: "test-app",
})
// Start application
go func() {
app.Run()
}()
defer app.Stop(context.Background())
// Create test file
testFile := createTestFile("test.txt", "test content")
defer os.Remove(testFile.Name())
// Test file upload
resp, err := http.Post("http://localhost:8080/upload", "multipart/form-data", testFile)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
}Best Practices
- File Validation: Always validate file types and content
- Size Limits: Set appropriate file size limits
- Security: Use secure filenames and validate file content
- Storage: Choose appropriate storage backend
- Error Handling: Handle upload errors gracefully
- Monitoring: Monitor upload metrics and errors
- Testing: Test file upload functionality thoroughly
- Performance: Optimize for large file uploads
For more advanced file handling features, see the Storage Extension documentation.
How is this guide?
Last updated on