Works with any backend npm install Zero dependencies
← All Backend Guides

Ruby on Rails

Handle MultipleUpload file uploads with Ruby on Rails using Active Storage or direct file handling. This guide covers both standard multipart uploads and chunked uploads.

Routes

Add upload routes in config/routes.rb.

# config/routes.rb
Rails.application.routes.draw do
  post "/api/upload", to: "uploads#create"
  post "/api/upload-chunk", to: "uploads#chunk"
end

CORS Configuration

Add the rack-cors gem and configure it in config/initializers/cors.rb.

# Gemfile
gem "rack-cors"

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "*"
    resource "/api/*",
      headers: %w[
        Content-Type X-Upload-Id X-Chunk-Index
        X-Chunk-Count X-File-Name X-File-Size
      ],
      methods: [:post, :options]
  end
end

Multipart Form Upload

The controller receives the file through Rails params and saves it with a unique GUID.

# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
  skip_before_action :verify_authenticity_token

  UPLOAD_DIR = Rails.root.join("storage", "uploads")
  CHUNK_DIR  = Rails.root.join("storage", "uploads", "chunks")

  def create
    file = params[:file]

    unless file.present?
      return render json: { success: false, error: "No file uploaded" }, status: :bad_request
    end

    file_guid = SecureRandom.uuid
    ext = File.extname(file.original_filename)
    dest_path = UPLOAD_DIR.join("#{file_guid}#{ext}")

    FileUtils.mkdir_p(UPLOAD_DIR)
    File.open(dest_path, "wb") do |f|
      f.write(file.read)
    end

    render json: {
      success: true,
      fileGuid: file_guid,
      fileName: file.original_filename,
      fileSize: file.size
    }
  end
end

Chunked Upload

Each chunk arrives as a raw body with custom headers. Store chunks in a temporary directory and assemble the final file once all chunks have arrived.

# Add to UploadsController
def chunk
  upload_id   = request.headers["X-Upload-Id"]
  chunk_index = request.headers["X-Chunk-Index"].to_i
  chunk_count = request.headers["X-Chunk-Count"].to_i
  file_name   = request.headers["X-File-Name"] || "unknown"
  file_size   = request.headers["X-File-Size"].to_i

  if upload_id.blank? || chunk_count.zero?
    return render json: { success: false, error: "Missing headers" }, status: :bad_request
  end

  session_dir = CHUNK_DIR.join(upload_id)
  FileUtils.mkdir_p(session_dir)

  # Save this chunk from raw body
  chunk_path = session_dir.join("chunk_#{chunk_index}")
  File.open(chunk_path, "wb") do |f|
    f.write(request.body.read)
  end

  # Count received chunks
  received = Dir.glob(session_dir.join("chunk_*")).count

  if received == chunk_count
    # Assemble final file
    file_guid = SecureRandom.uuid
    ext = File.extname(file_name)
    final_path = UPLOAD_DIR.join("#{file_guid}#{ext}")

    FileUtils.mkdir_p(UPLOAD_DIR)
    File.open(final_path, "wb") do |output|
      chunk_count.times do |i|
        cp = session_dir.join("chunk_#{i}")
        output.write(File.binread(cp))
      end
    end

    # Clean up chunks
    FileUtils.rm_rf(session_dir)

    return render json: {
      success: true,
      fileGuid: file_guid,
      fileName: file_name,
      fileSize: file_size
    }
  end

  render json: { success: true, chunksReceived: received }
end

Expected JSON Response

Both endpoints must return this JSON structure on successful upload completion.

{
  "success": true,
  "fileGuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "fileName": "photo.jpg",
  "fileSize": 204800
}

Client-Side JavaScript

Initialize MultipleUpload on the client, pointing to your Rails API endpoints.

<link rel="stylesheet" href="multipleupload.css" />
<div id="uploader"></div>
<script src="multipleupload.js"></script>
<script>
  var uploader = new MultipleUpload("#uploader", {
    uploadUrl: "/api/upload",
    chunkUploadUrl: "/api/upload-chunk",
    chunkSize: 2 * 1024 * 1024, // 2 MB chunks
    onFileUploaded: function (file, response) {
      console.log("Uploaded:", response.fileName, response.fileGuid);
    }
  });
</script>