File Handling & Static Files

Serve static files, handle uploads, and stream large files with Suave.

Serving Static Files

Serve files from a directory:

open Suave
open Suave.Files
open Suave.Operators
open Suave.Filters

let app =
  choose [
    path "/" >=> Files.browseFileHome "index.html"
    Files.browseHome  // Serve all files from current directory
  ]

Serving from Specific Directory

Serve files from a specific folder:

open Suave
open Suave.Files

let app =
  choose [
    Files.browse "./public"  // Serve from ./public directory
    RequestErrors.NOT_FOUND "Not found"
  ]

Directory Listing

Allow directory browsing:

open Suave
open Suave.Files
open Suave.Operators

let app =
  choose [
    browseFileHome "index.html"
    browseHome  // Allows directory listing
  ]

File Downloads

Serve files for download with appropriate headers:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Writers

let app =
  choose [
    path "/download/file.pdf" >=>
      setHeader "Content-Disposition" "attachment; filename=file.pdf" >=>
      Files.file "./files/file.pdf"
  ]

File Upload Handling

Handle file uploads from forms:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Successful
open Suave.ServerErrors
open System.IO

let app =
  choose [
    POST >=> path "/upload" >=> fun ctx ->
      async {
        try
          let files = ctx.request.files
          if files.Count = 0 then
            return! RequestErrors.BAD_REQUEST "No file provided" ctx
          else
            let file = files.[0]
            let uploadDir = "./uploads"
            Directory.CreateDirectory(uploadDir) |> ignore
            let savePath = Path.Combine(uploadDir, file.fileName)
            // Uploads land on disk at tempFilePath; copy/move from there
            File.Copy(file.tempFilePath, savePath, true)
            return! OK (sprintf "File uploaded: %s" file.fileName) ctx
        with ex ->
          return! INTERNAL_ERROR (sprintf "Upload failed: %s" ex.Message) ctx
      }
  ]

Streaming Large Files

Stream large files without loading entire content to memory:

open Suave
open Suave.Operators
open Suave.Filters
open Suave.Files

// Files.file streams from disk without buffering the whole payload
let app =
  choose [
    path "/large-file" >=> file "./data/large-file.bin"
  ]

Serving Specific File Types

Serve specific file types with correct MIME types:

open Suave
open Suave.Files
open Suave.Operators
open Suave.Filters
open Suave.Writers

let app =
  choose [
    pathRegex @".*\.pdf$" >=>
      setHeader "Content-Type" "application/pdf" >=>
      browseHome

    pathRegex @".*\.txt$" >=>
      setHeader "Content-Type" "text/plain" >=>
      browseHome

    pathRegex @".*\.json$" >=>
      setHeader "Content-Type" "application/json" >=>
      browseHome

    browseHome
  ]

Conditional File Serving

Serve files conditionally based on path:

open Suave
open Suave.Files
open Suave.Operators
open Suave.Filters

let app =
  choose [
    // API routes
    path "/api" >=> RequestErrors.NOT_FOUND "API not found"
    
    // Static files
    GET >=> Files.browseHome
    
    // Everything else
    RequestErrors.NOT_FOUND "Not found"
  ]

File Caching

Enable HTTP caching for static files:

open Suave
open Suave.Files
open Suave.Operators
open Suave.Filters
open Suave.Writers

let cacheFor (seconds: int) =
  setHeader "Cache-Control" (sprintf "public, max-age=%d" seconds)

let app =
  choose [
    pathRegex @".*\.(js|css)$" >=>
      cacheFor 86400 >=>  // 1 day
      browseHome

    pathRegex @".*\.(jpg|png|gif|svg)$" >=>
      cacheFor 604800 >=>  // 7 days
      browseHome

    pathRegex @".*\.html$" >=>
      cacheFor 3600 >=>   // 1 hour
      browseHome

    browseHome
  ]

Serving SPA (Single Page Application)

Serve SPA with fallback to index.html:

open Suave
open Suave.Files
open Suave.Operators
open Suave.Filters

let app =
  choose [
    // API routes first
    path "/api" >=> RequestErrors.NOT_FOUND "API not implemented"

    // Static assets with long cache
    GET >=> pathRegex @"^/(css|js|images)/" >=> Files.browseHome

    // Everything else falls back to index.html for SPA routing
    GET >=> Files.browseFileHome "index.html"
    
    // Default
    Files.browseHome
  ]