Recipe - File Upload & Download Service
Full-file service with multipart upload, type/size validation, streaming download with Range support, and static serving.
File Upload & Download Service
Build a complete file service with upload validation, streaming download, and directory serving.
Upload handler with validation
const maxUploadSize = 32 << 20 // 32 MB
func uploadHandler(c *kern.Context) {
c.Request.Body = http.MaxBytesReader(c.Response, c.Request.Body, maxUploadSize)
file, err := c.File("file")
if err != nil {
c.Error(http.StatusBadRequest, "file is required")
return
}
// Validate file type by extension
allowed := map[string]bool{
".jpg": true, ".jpeg": true, ".png": true,
".gif": true, ".pdf": true, ".zip": true,
}
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowed[ext] {
c.Error(http.StatusUnprocessableEntity, "file type not allowed: "+ext)
return
}
// Validate MIME type
f, err := file.Open()
if err != nil {
c.Error(http.StatusInternalServerError, "failed to read file")
return
}
defer f.Close()
buf := make([]byte, 512)
if _, err := f.Read(buf); err != nil {
c.Error(http.StatusInternalServerError, "failed to read file header")
return
}
mimeType := http.DetectContentType(buf)
if !strings.HasPrefix(mimeType, "image/") && mimeType != "application/pdf" {
c.Error(http.StatusUnprocessableEntity, "invalid content type: "+mimeType)
return
}
f.Close()Save file to disk with unique name
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.Error(http.StatusInternalServerError, "failed to create upload dir")
return
}
uniqueName := fmt.Sprintf("%d_%s", time.Now().UnixNano(), file.Filename)
dst := filepath.Join(uploadDir, uniqueName)
if err := c.SaveFile(file, dst); err != nil {
c.Error(http.StatusInternalServerError, "failed to save file")
return
}
c.Created(map[string]string{
"filename": uniqueName,
"size": fmt.Sprintf("%d bytes", file.Size),
"url": "/files/" + uniqueName,
})
}Download handler with Range support
func downloadHandler(c *kern.Context) {
filename := c.Param("file")
filepath := filepath.Join("./uploads", filename)
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.Error(http.StatusNotFound, "file not found")
return
}
// Force download with Content-Disposition
c.DownloadFile(filepath, filename)
}
func streamHandler(c *kern.Context) {
filename := c.Param("file")
filepath := filepath.Join("./uploads", filename)
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.Error(http.StatusNotFound, "file not found")
return
}
// Inline streaming with Range support (partial content, seeking)
c.StreamFile(filepath)
}Route setup
func main() {
app := kern.New(
kern.WithRecovery(),
kern.WithBodyLimit(32 << 20),
)
api := app.Group("/api/v1")
api.POST("/upload", uploadHandler)
api.GET("/files/{file}", streamHandler)
api.GET("/download/{file}", downloadHandler)
// Serve uploaded files as static assets
app.Static("/uploads", "./uploads")
app.Run(":8080",
kern.WithReadTimeout(30*time.Second),
kern.WithWriteTimeout(120*time.Second), // allow slow uploads
kern.WithGracefulShutdown(10*time.Second),
)
}Key patterns
- Size enforcement:
http.MaxBytesReaderbefore reading multipart form - Double validation: Extension check + MIME type sniffing
- Streaming:
c.StreamFilesupportsRangeheader for video/audio seeking - Download:
c.DownloadFilesetsContent-Disposition: attachment - Static serving:
app.Staticserves the uploads directory - Timeouts: Longer write timeout accommodates slow upload connections
Recipe - CRUD REST API with Validation
Full CRUD REST API using xvalidator.BodyValidator[T] middleware for route-level decode + validate, path constraints, and an in-memory store.
Recipe - OpenAPI Documentation with Swagger UI
Document your Kern API routes with OpenAPI 3.0.3 using the extensions/xopenapi package, served as JSON and Swagger UI.