File Upload Example
Handle file uploads with validation, storage, and serving
Complete File Upload Server
This example demonstrates secure file upload handling:
package main
import (
"crypto/md5"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/user001/squirrel"
)
// File metadata
type FileInfo struct {
ID string `json:"id"`
OriginalName string `json:"original_name"`
Filename string `json:"filename"`
Size int64 `json:"size"`
ContentType string `json:"content_type"`
UploadedAt time.Time `json:"uploaded_at"`
MD5Hash string `json:"md5_hash"`
}
// Configuration
const (
MaxFileSize = 10 << 20 // 10MB
UploadDir = "./uploads"
ThumbnailDir = "./thumbnails"
)
// Allowed file types
var allowedTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"text/plain": true,
"application/pdf": true,
}
// File extensions
var allowedExtensions = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".txt": true,
".pdf": true,
}
func main() {
// Create upload directories
os.MkdirAll(UploadDir, 0755)
os.MkdirAll(ThumbnailDir, 0755)
mux := squirrel.NewSqurlMux()
// Middleware
mux.Use(squirrel.LoggingMiddleware())
mux.Use(squirrel.RecoveryMiddleware())
mux.Use(corsMiddleware())
// Upload endpoints
mux.HandleFunc("POST /upload", uploadHandler)
mux.HandleFunc("POST /upload/multiple", multipleUploadHandler)
mux.HandleFunc("GET /files/{id}", serveFileHandler)
mux.HandleFunc("GET /files/{id}/info", fileInfoHandler)
mux.HandleFunc("DELETE /files/{id}", deleteFileHandler)
// List files
mux.HandleFunc("GET /files", listFilesHandler)
// Upload form (for testing)
mux.HandleFunc("GET /", uploadFormHandler)
fmt.Println("File upload server starting on http://localhost:8080")
log.Fatal(squirrel.ListenAndServe(":8080", mux))
}
func corsMiddleware() squirrel.Middleware {
return func(next squirrel.HandlerFunc) squirrel.HandlerFunc {
return func(w *squirrel.Response, r *squirrel.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteStatus(200)
return
}
next(w, r)
}
}
}
// Single file upload handler
func uploadHandler(w *squirrel.Response, r *squirrel.Request) {
// Limit request size
r.Body = http.MaxBytesReader(w, r.Body, MaxFileSize)
// Parse multipart form
if err := r.ParseMultipartForm(MaxFileSize); err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "File too large or invalid form"})
return
}
// Get the file
file, header, err := r.FormFile("file")
if err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "No file provided"})
return
}
defer file.Close()
// Validate and save file
fileInfo, err := validateAndSaveFile(file, header)
if err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": err.Error()})
return
}
w.WriteStatus(201)
w.JSON(fileInfo)
}
// Multiple file upload handler
func multipleUploadHandler(w *squirrel.Response, r *squirrel.Request) {
// Limit request size
r.Body = http.MaxBytesReader(w, r.Body, MaxFileSize*5) // 50MB total
// Parse multipart form
if err := r.ParseMultipartForm(MaxFileSize * 5); err != nil {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "Request too large or invalid form"})
return
}
files := r.MultipartForm.File["files"]
if len(files) == 0 {
w.WriteStatus(400)
w.JSON(map[string]string{"error": "No files provided"})
return
}
var uploadedFiles []*FileInfo
var errors []string
for _, header := range files {
file, err := header.Open()
if err != nil {
errors = append(errors, fmt.Sprintf("Error opening %s: %v", header.Filename, err))
continue
}
fileInfo, err := validateAndSaveFile(file, header)
file.Close()
if err != nil {
errors = append(errors, fmt.Sprintf("Error saving %s: %v", header.Filename, err))
continue
}
uploadedFiles = append(uploadedFiles, fileInfo)
}
response := map[string]interface{}{
"uploaded_files": uploadedFiles,
"uploaded_count": len(uploadedFiles),
}
if len(errors) > 0 {
response["errors"] = errors
}
w.WriteStatus(201)
w.JSON(response)
}
// Validate and save file
func validateAndSaveFile(file multipart.File, header *multipart.FileHeader) (*FileInfo, error) {
// Check file size
if header.Size > MaxFileSize {
return nil, fmt.Errorf("file too large: %d bytes (max %d)", header.Size, MaxFileSize)
}
// Check file extension
ext := strings.ToLower(filepath.Ext(header.Filename))
if !allowedExtensions[ext] {
return nil, fmt.Errorf("file type not allowed: %s", ext)
}
// Read file content for validation
buffer := make([]byte, 512)
_, err := file.Read(buffer)
if err != nil {
return nil, fmt.Errorf("error reading file: %v", err)
}
// Detect content type
contentType := http.DetectContentType(buffer)
if !allowedTypes[contentType] {
return nil, fmt.Errorf("content type not allowed: %s", contentType)
}
// Reset file pointer
file.Seek(0, 0)
// Generate unique filename
timestamp := time.Now().UnixNano()
filename := fmt.Sprintf("%d_%s", timestamp, header.Filename)
filepath := filepath.Join(UploadDir, filename)
// Create destination file
dst, err := os.Create(filepath)
if err != nil {
return nil, fmt.Errorf("error creating file: %v", err)
}
defer dst.Close()
// Calculate MD5 hash while copying
hash := md5.New()
multiWriter := io.MultiWriter(dst, hash)
// Copy file content
_, err = io.Copy(multiWriter, file)
if err != nil {
os.Remove(filepath) // Clean up on error
return nil, fmt.Errorf("error saving file: %v", err)
}
// Create file info
fileInfo := &FileInfo{
ID: fmt.Sprintf("%d", timestamp),
OriginalName: header.Filename,
Filename: filename,
Size: header.Size,
ContentType: contentType,
UploadedAt: time.Now(),
MD5Hash: fmt.Sprintf("%x", hash.Sum(nil)),
}
return fileInfo, nil
}
// Serve file handler
func serveFileHandler(w *squirrel.Response, r *squirrel.Request) {
fileID := r.PathValue("id")
// Find file by ID (in production, use a database)
files, err := filepath.Glob(filepath.Join(UploadDir, fileID+"_*"))
if err != nil || len(files) == 0 {
w.WriteStatus(404)
w.JSON(map[string]string{"error": "File not found"})
return
}
filePath := files[0]
// Get file info
fileInfo, err := os.Stat(filePath)
if err != nil {
w.WriteStatus(404)
w.JSON(map[string]string{"error": "File not found"})
return
}
// Set appropriate headers
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".pdf":
w.Header().Set("Content-Type", "application/pdf")
case ".txt":
w.Header().Set("Content-Type", "text/plain")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
w.Header().Set("Cache-Control", "public, max-age=86400") // 1 day
// Serve the file
squirrel.ServeFile(w, r, filePath)
}
// File info handler
func fileInfoHandler(w *squirrel.Response, r *squirrel.Request) {
fileID := r.PathValue("id")
// Find file by ID
files, err := filepath.Glob(filepath.Join(UploadDir, fileID+"_*"))
if err != nil || len(files) == 0 {
w.WriteStatus(404)
w.JSON(map[string]string{"error": "File not found"})
return
}
filePath := files[0]
fileInfo, err := os.Stat(filePath)
if err != nil {
w.WriteStatus(404)
w.JSON(map[string]string{"error": "File not found"})
return
}
// Extract original filename from stored filename
filename := filepath.Base(filePath)
parts := strings.SplitN(filename, "_", 2)
originalName := filename
if len(parts) == 2 {
originalName = parts[1]
}
response := FileInfo{
ID: fileID,
OriginalName: originalName,
Filename: filename,
Size: fileInfo.Size(),
UploadedAt: fileInfo.ModTime(),
}
w.JSON(response)
}
// Delete file handler
func deleteFileHandler(w *squirrel.Response, r *squirrel.Request) {
fileID := r.PathValue("id")
// Find file by ID
files, err := filepath.Glob(filepath.Join(UploadDir, fileID+"_*"))
if err != nil || len(files) == 0 {
w.WriteStatus(404)
w.JSON(map[string]string{"error": "File not found"})
return
}
// Delete the file
if err := os.Remove(files[0]); err != nil {
w.WriteStatus(500)
w.JSON(map[string]string{"error": "Error deleting file"})
return
}
w.WriteStatus(204)
}
// List files handler
func listFilesHandler(w *squirrel.Response, r *squirrel.Request) {
files, err := filepath.Glob(filepath.Join(UploadDir, "*"))
if err != nil {
w.WriteStatus(500)
w.JSON(map[string]string{"error": "Error listing files"})
return
}
var fileList []map[string]interface{}
for _, filePath := range files {
fileInfo, err := os.Stat(filePath)
if err != nil {
continue
}
filename := filepath.Base(filePath)
parts := strings.SplitN(filename, "_", 2)
if len(parts) != 2 {
continue
}
fileData := map[string]interface{}{
"id": parts[0],
"original_name": parts[1],
"filename": filename,
"size": fileInfo.Size(),
"uploaded_at": fileInfo.ModTime(),
}
fileList = append(fileList, fileData)
}
w.JSON(map[string]interface{}{
"files": fileList,
"count": len(fileList),
})
}
// Upload form handler (for testing)
func uploadFormHandler(w *squirrel.Response, r *squirrel.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteString(`
<!DOCTYPE html>
<html>
<head>
<title>File Upload Example</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.upload-form { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 5px; }
input[type="file"] { margin: 10px 0; }
button { background: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 3px; cursor: pointer; }
button:hover { background: #005a87; }
.file-list { margin-top: 20px; }
.file-item { padding: 10px; border: 1px solid #eee; margin: 5px 0; border-radius: 3px; }
</style>
</head>
<body>
<h1>File Upload Example</h1>
<div class="upload-form">
<h3>Single File Upload</h3>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<br>
<button type="submit">Upload File</button>
</form>
</div>
<div class="upload-form">
<h3>Multiple File Upload</h3>
<form action="/upload/multiple" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple required>
<br>
<button type="submit">Upload Files</button>
</form>
</div>
<div class="file-list">
<h3>Uploaded Files</h3>
<p><a href="/files">View all files (JSON)</a></p>
</div>
<div>
<h3>API Endpoints</h3>
<ul>
<li>POST /upload - Single file upload</li>
<li>POST /upload/multiple - Multiple file upload</li>
<li>GET /files - List all files</li>
<li>GET /files/{id} - Download file</li>
<li>GET /files/{id}/info - Get file info</li>
<li>DELETE /files/{id} - Delete file</li>
</ul>
</div>
</body>
</html>
`)
}Testing the File Upload
Web Interface
Visit the upload form in your browser:
http://localhost:8080/Single File Upload (cURL)
curl -X POST -F "e;file=@example.jpg"e; http://localhost:8080/uploadMultiple File Upload (cURL)
curl -X POST -F "e;files=@file1.jpg"e; -F "e;files=@file2.png"e; http://localhost:8080/upload/multipleList Files
curl http://localhost:8080/filesDownload File
curl http://localhost:8080/files/1234567890 -o downloaded_file.jpgSecurity Features
File Validation
- • File size limits (10MB)
- • Content type validation
- • File extension checking
- • MIME type detection
Security Measures
- • Unique filename generation
- • Directory traversal prevention
- • Request size limiting
- • MD5 hash calculation
File Management
- • Organized file storage
- • File metadata tracking
- • Proper error handling
- • Cleanup on errors
HTTP Features
- • Proper content types
- • Cache headers
- • CORS support
- • RESTful endpoints
Production Enhancements
🚀 Production Recommendations
- • Use cloud storage (AWS S3, Google Cloud Storage)
- • Implement virus scanning
- • Add image resizing/thumbnail generation
- • Use a database for file metadata
- • Implement user authentication
- • Add file access permissions
- • Use CDN for file serving
- • Implement file versioning
Next Steps
- Add image processing and thumbnail generation
- Implement user authentication and file ownership
- Add database integration for file metadata
- Explore cloud storage integration
- Learn about static file serving