Skip to content

Ruby on Rails

Use PutPut as a lightweight alternative to Active Storage. No gem needed — just plain HTTP calls from a controller.

1. Environment variable

# .env or Rails credentials
PUTPUT_TOKEN=pp_guest_...

2. Service class

# app/services/putput_service.rb
class PutputService
  API = "https://putput.io/api/v1"
  TOKEN = ENV.fetch("PUTPUT_TOKEN")

  def presign(filename:, content_type:, size_bytes:)
    uri = URI("#{API}/upload/presign")
    req = Net::HTTP::Post.new(uri)
    req["Authorization"] = "Bearer #{TOKEN}"
    req["Content-Type"] = "application/json"
    req.body = { filename:, content_type:, size_bytes: }.to_json

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
    JSON.parse(res.body)
  end

  def confirm(upload_id:)
    uri = URI("#{API}/upload/confirm")
    req = Net::HTTP::Post.new(uri)
    req["Authorization"] = "Bearer #{TOKEN}"
    req["Content-Type"] = "application/json"
    req.body = { upload_id: }.to_json

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
    JSON.parse(res.body)
  end
end

3. Controller

# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
  def presign
    service = PutputService.new
    result = service.presign(
      filename: params[:filename],
      content_type: params[:content_type],
      size_bytes: params[:size_bytes].to_i
    )
    render json: result
  end

  def confirm
    service = PutputService.new
    result = service.confirm(upload_id: params[:upload_id])
    render json: result
  end
end

# config/routes.rb
Rails.application.routes.draw do
  post "uploads/presign", to: "uploads#presign"
  post "uploads/confirm", to: "uploads#confirm"
end

4. JavaScript upload (Stimulus or plain JS)

// app/javascript/upload.js
async function uploadFile(file) {
  // 1. Presign via Rails backend
  const presign = await fetch("/uploads/presign", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
    },
    body: JSON.stringify({
      filename: file.name,
      content_type: file.type,
      size_bytes: file.size,
    }),
  }).then((r) => r.json());

  // 2. Upload directly to R2
  await fetch(presign.presigned_url, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });

  // 3. Confirm
  const result = await fetch("/uploads/confirm", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
    },
    body: JSON.stringify({ upload_id: presign.upload_id }),
  }).then((r) => r.json());

  return result.file.public_url;
}
v0.4.77