Skip to content

File Uploads

Handle multipart uploads with consistent patterns per binding.

Upload handler


id: python_upload language: python title: Upload tags: - python


from spikard import Spikard, UploadFile
import os
import uuid
from pathlib import Path

app = Spikard()

# Basic upload handler
@app.post("/upload")
async def upload(file: UploadFile) -> dict:
    content = file.read()
    return {"filename": file.filename, "size": len(content)}

# Complete upload handler with validation and storage
@app.post("/upload/complete")
async def upload_with_validation(file: UploadFile) -> dict:
    # Validate file size (10MB limit)
    MAX_SIZE = 10 * 1024 * 1024
    content = file.read()
    if len(content) > MAX_SIZE:
        raise ValueError(f"File size {len(content)} exceeds {MAX_SIZE} bytes")

    # Validate MIME type
    ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif", "application/pdf"]
    if file.content_type not in ALLOWED_TYPES:
        raise ValueError(f"File type {file.content_type} not allowed")

    # Prevent path traversal - sanitize filename
    safe_filename = os.path.basename(file.filename)
    unique_filename = f"{uuid.uuid4()}_{safe_filename}"

    # Save to local filesystem
    upload_dir = Path("/var/uploads")
    upload_dir.mkdir(parents=True, exist_ok=True)
    file_path = upload_dir / unique_filename

    with open(file_path, "wb") as f:
        f.write(content)

    return {
        "filename": safe_filename,
        "stored_as": unique_filename,
        "size": len(content),
        "content_type": file.content_type,
        "url": f"/files/{unique_filename}"
    }

# Upload to S3/cloud storage
@app.post("/upload/s3")
async def upload_to_s3(file: UploadFile) -> dict:
    import boto3

    s3_client = boto3.client('s3')
    bucket_name = "my-uploads-bucket"

    # Validate and sanitize
    safe_filename = os.path.basename(file.filename)
    s3_key = f"uploads/{uuid.uuid4()}/{safe_filename}"

    # Upload to S3
    s3_client.put_object(
        Bucket=bucket_name,
        Key=s3_key,
        Body=file.read(),
        ContentType=file.content_type
    )

    return {
        "filename": safe_filename,
        "s3_key": s3_key,
        "url": f"https://{bucket_name}.s3.amazonaws.com/{s3_key}"
    }

id: typescript_upload language: typescript title: Upload tags: - typescript


import { Spikard, UploadFile } from "spikard";
import { promises as fs } from "fs";
import path from "path";
import { v4 as uuidv4 } from "uuid";

const app = new Spikard();

// Basic upload handler
app.addRoute(
  { method: "POST", path: "/upload", handler_name: "upload", is_async: true },
  async (req) => {
    const body = req.json<{ file: UploadFile; description?: string }>();
    const size = body.file.size;
    return { filename: body.file.filename, size };
  },
);

// Complete upload handler with validation and storage
app.addRoute(
  { method: "POST", path: "/upload/complete", handler_name: "uploadComplete", is_async: true },
  async (req) => {
    const body = req.json<{ file: UploadFile }>();
    const file = body.file;

    // Validate file size (10MB limit)
    const MAX_SIZE = 10 * 1024 * 1024;
    if (file.size > MAX_SIZE) {
      throw new Error(`File size ${file.size} exceeds ${MAX_SIZE} bytes`);
    }

    // Validate MIME type
    const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/gif", "application/pdf"];
    if (!ALLOWED_TYPES.includes(file.content_type)) {
      throw new Error(`File type ${file.content_type} not allowed`);
    }

    // Prevent path traversal - sanitize filename
    const safeFilename = path.basename(file.filename);
    const uniqueFilename = `${uuidv4()}_${safeFilename}`;

    // Save to local filesystem
    const uploadDir = "/var/uploads";
    await fs.mkdir(uploadDir, { recursive: true });
    const filePath = path.join(uploadDir, uniqueFilename);

    await fs.writeFile(filePath, Buffer.from(file.content));

    return {
      filename: safeFilename,
      stored_as: uniqueFilename,
      size: file.size,
      content_type: file.content_type,
      url: `/files/${uniqueFilename}`,
    };
  },
);

// Upload to S3/cloud storage
app.addRoute(
  { method: "POST", path: "/upload/s3", handler_name: "uploadToS3", is_async: true },
  async (req) => {
    const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
    const body = req.json<{ file: UploadFile }>();
    const file = body.file;

    const s3Client = new S3Client({ region: "us-east-1" });
    const bucketName = "my-uploads-bucket";

    // Validate and sanitize
    const safeFilename = path.basename(file.filename);
    const s3Key = `uploads/${uuidv4()}/${safeFilename}`;

    // Upload to S3
    await s3Client.send(
      new PutObjectCommand({
        Bucket: bucketName,
        Key: s3Key,
        Body: Buffer.from(file.content),
        ContentType: file.content_type,
      }),
    );

    return {
      filename: safeFilename,
      s3_key: s3Key,
      url: `https://${bucketName}.s3.amazonaws.com/${s3Key}`,
    };
  },
);

id: ruby_upload language: ruby title: Upload tags: - ruby


require "spikard"
require "securerandom"
require "fileutils"

app = Spikard::App.new

# Basic upload handler
app.post "/upload" do |_params, _query, body|
  file = body["file"]
  { filename: file[:filename], size: file[:tempfile].size }
end

# Complete upload handler with validation and storage
app.post "/upload/complete" do |_params, _query, body|
  file = body["file"]
  tempfile = file[:tempfile]
  filename = file[:filename]
  content_type = file[:type]

  # Validate file size (10MB limit)
  max_size = 10 * 1024 * 1024
  file_size = tempfile.size
  raise "File size #{file_size} exceeds #{max_size} bytes" if file_size > max_size

  # Validate MIME type
  allowed_types = ["image/jpeg", "image/png", "image/gif", "application/pdf"]
  raise "File type #{content_type} not allowed" unless allowed_types.include?(content_type)

  # Prevent path traversal - sanitize filename
  safe_filename = File.basename(filename)
  unique_filename = "#{SecureRandom.uuid}_#{safe_filename}"

  # Save to local filesystem
  upload_dir = "/var/uploads"
  FileUtils.mkdir_p(upload_dir)
  file_path = File.join(upload_dir, unique_filename)

  File.open(file_path, "wb") do |f|
    f.write(tempfile.read)
  end

  {
    filename: safe_filename,
    stored_as: unique_filename,
    size: file_size,
    content_type: content_type,
    url: "/files/#{unique_filename}"
  }
end

# Upload to S3/cloud storage
app.post "/upload/s3" do |_params, _query, body|
  require "aws-sdk-s3"

  file = body["file"]
  tempfile = file[:tempfile]
  filename = file[:filename]
  content_type = file[:type]

  s3_client = Aws::S3::Client.new(region: "us-east-1")
  bucket_name = "my-uploads-bucket"

  # Validate and sanitize
  safe_filename = File.basename(filename)
  s3_key = "uploads/#{SecureRandom.uuid}/#{safe_filename}"

  # Upload to S3
  s3_client.put_object(
    bucket: bucket_name,
    key: s3_key,
    body: tempfile.read,
    content_type: content_type
  )

  {
    filename: safe_filename,
    s3_key: s3_key,
    url: "https://#{bucket_name}.s3.amazonaws.com/#{s3_key}"
  }
end

id: php_upload language: php title: Upload tags: - php


<?php

declare(strict_types=1);

use Spikard\App;
use Spikard\Attributes\Post;
use Spikard\Config\ServerConfig;
use Spikard\Http\Request;
use Spikard\Http\Response;

final class UploadController
{
    #[Post('/upload')]
    public function upload(Request $request): Response
    {
        $file = $request->files['file'] ?? null;

        if ($file === null) {
            return Response::json(['error' => 'No file uploaded'], 400);
        }

        $filename = $file['filename'] ?? 'unknown';
        $size = $file['size'] ?? 0;
        $contentType = $file['content_type'] ?? 'application/octet-stream';

        return Response::json([
            'filename' => $filename,
            'size' => $size,
            'content_type' => $contentType,
            'received' => true
        ]);
    }
}

$app = (new App(new ServerConfig(port: 8000)))
    ->registerController(new UploadController());

id: rust_upload language: rust title: Upload tags: - rust


use spikard::prelude::*;
use spikard::UploadFile;
use std::path::Path;
use tokio::fs;
use uuid::Uuid;

// Basic upload handler
app.route(post("/upload"), |ctx: Context| async move {
    let upload: UploadFile = ctx.json()?;
    Ok(Json(json!({ "filename": upload.filename, "size": upload.size })))
})?;

// Complete upload handler with validation and storage
app.route(post("/upload/complete"), |ctx: Context| async move {
    let upload: UploadFile = ctx.json()?;

    // Validate file size (10MB limit)
    const MAX_SIZE: usize = 10 * 1024 * 1024;
    if upload.content.len() > MAX_SIZE {
        return Err(format!("File size {} exceeds {} bytes", upload.content.len(), MAX_SIZE).into());
    }

    // Validate MIME type
    let allowed_types = vec!["image/jpeg", "image/png", "image/gif", "application/pdf"];
    if !allowed_types.contains(&upload.content_type.as_str()) {
        return Err(format!("File type {} not allowed", upload.content_type).into());
    }

    // Prevent path traversal - sanitize filename
    let safe_filename = Path::new(&upload.filename)
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or("Invalid filename")?;
    let unique_filename = format!("{}_{}", Uuid::new_v4(), safe_filename);

    // Save to local filesystem
    let upload_dir = Path::new("/var/uploads");
    fs::create_dir_all(upload_dir).await?;
    let file_path = upload_dir.join(&unique_filename);

    fs::write(&file_path, &upload.content).await?;

    Ok(Json(json!({
        "filename": safe_filename,
        "stored_as": unique_filename,
        "size": upload.content.len(),
        "content_type": upload.content_type,
        "url": format!("/files/{}", unique_filename)
    })))
})?;

// Upload to S3/cloud storage
app.route(post("/upload/s3"), |ctx: Context| async move {
    use aws_sdk_s3::Client;

    let upload: UploadFile = ctx.json()?;
    let config = aws_config::load_from_env().await;
    let s3_client = Client::new(&config);
    let bucket_name = "my-uploads-bucket";

    // Validate and sanitize
    let safe_filename = Path::new(&upload.filename)
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or("Invalid filename")?;
    let s3_key = format!("uploads/{}/{}", Uuid::new_v4(), safe_filename);

    // Upload to S3
    s3_client
        .put_object()
        .bucket(bucket_name)
        .key(&s3_key)
        .body(upload.content.into())
        .content_type(&upload.content_type)
        .send()
        .await?;

    Ok(Json(json!({
        "filename": safe_filename,
        "s3_key": s3_key,
        "url": format!("https://{}.s3.amazonaws.com/{}", bucket_name, s3_key)
    })))
})?;

Validation

Always validate uploaded files to prevent security issues and resource exhaustion:

File size limits: Prevent denial-of-service attacks by enforcing maximum file sizes before processing.

MIME type validation: Check the content_type field to ensure only expected file types are accepted. Never trust file extensions alone.

Filename sanitization: Use path.basename() or equivalent to prevent directory traversal attacks. Malicious filenames like ../../etc/passwd could overwrite system files.

Content validation: For images, consider using libraries to validate that the file content matches the declared MIME type.

Storage strategies

Local filesystem

Store files on the server's filesystem for simple deployments:

  • Generate unique filenames using UUIDs to prevent collisions
  • Create organized directory structures (e.g., by date or user ID)
  • Set appropriate file permissions
  • Consider disk space monitoring

Cloud storage (S3, GCS, Azure Blob)

For production deployments, use object storage:

  • Direct uploads reduce server load
  • Built-in redundancy and scalability
  • CDN integration for faster downloads
  • Lifecycle policies for automatic cleanup

Database storage

Store small files (< 1MB) directly in databases:

  • Simplifies backup and replication
  • Keeps data and metadata together
  • Not recommended for large files due to performance impact

Testing file uploads

Test your upload handlers with different scenarios:

import pytest
from spikard.testing import TestClient

def test_upload_success(client: TestClient):
    response = client.post(
        "/upload/complete",
        files={"file": ("test.jpg", b"fake image data", "image/jpeg")}
    )
    assert response.status_code == 200
    assert response.json()["filename"] == "test.jpg"

def test_upload_exceeds_size_limit(client: TestClient):
    large_file = b"x" * (11 * 1024 * 1024)  # 11MB
    response = client.post(
        "/upload/complete",
        files={"file": ("large.jpg", large_file, "image/jpeg")}
    )
    assert response.status_code == 400

def test_upload_invalid_type(client: TestClient):
    response = client.post(
        "/upload/complete",
        files={"file": ("malware.exe", b"fake data", "application/x-msdownload")}
    )
    assert response.status_code == 400

def test_upload_path_traversal(client: TestClient):
    response = client.post(
        "/upload/complete",
        files={"file": ("../../etc/passwd", b"fake data", "image/jpeg")}
    )
    # Should sanitize to just "passwd"
    assert "../../" not in response.json()["stored_as"]
import { TestClient } from "spikard/testing";

test("upload success", async () => {
  const client = new TestClient(app);
  const response = await client.post("/upload/complete", {
    file: {
      filename: "test.jpg",
      content: Buffer.from("fake image data"),
      content_type: "image/jpeg",
    },
  });
  expect(response.status).toBe(200);
  expect(response.json().filename).toBe("test.jpg");
});

test("upload exceeds size limit", async () => {
  const client = new TestClient(app);
  const largeFile = Buffer.alloc(11 * 1024 * 1024); // 11MB
  const response = await client.post("/upload/complete", {
    file: {
      filename: "large.jpg",
      content: largeFile,
      content_type: "image/jpeg",
    },
  });
  expect(response.status).toBe(400);
});
require "rack/test"

RSpec.describe "File uploads" do
  include Rack::Test::Methods

  def app
    @app
  end

  it "uploads file successfully" do
    post "/upload/complete", {
      file: Rack::Test::UploadedFile.new(
        StringIO.new("fake image data"),
        "image/jpeg",
        original_filename: "test.jpg"
      )
    }
    expect(last_response.status).to eq(200)
    expect(JSON.parse(last_response.body)["filename"]).to eq("test.jpg")
  end

  it "rejects files exceeding size limit" do
    large_file = StringIO.new("x" * (11 * 1024 * 1024))
    post "/upload/complete", {
      file: Rack::Test::UploadedFile.new(
        large_file,
        "image/jpeg",
        original_filename: "large.jpg"
      )
    }
    expect(last_response.status).to eq(400)
  end
end

Security considerations

  • Virus scanning: For user-generated content, integrate antivirus scanning before storage
  • Content-Type spoofing: Validate file signatures (magic bytes) in addition to MIME types
  • Resource limits: Set timeouts for upload processing to prevent slowloris attacks
  • Access control: Verify user permissions before allowing uploads
  • Temporary file cleanup: Ensure temporary files are deleted after processing or on errors

Tips

  • Enforce size/type limits via middleware or schema where supported.
  • For large uploads, stream chunks instead of reading all bytes into memory.
  • Return metadata (filename, size, type) and store bytes in durable storage.
  • Use presigned URLs for direct client-to-S3 uploads to bypass your server entirely.
  • Implement rate limiting to prevent abuse of upload endpoints.

Edit this page on GitHub