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;
}