Skip to Content
Course content

212: Direct Uploads to S3

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when developers first start integrating cloud storage: they assume the file upload flow is a linear pipeline. They think the process goes Browser → Ruby Server → S3. In their mind, the Rails controller receives the file as a parameter, holds it in memory (or a temp file), and then the server pushes it over to Amazon.

The Myth: The Server as the Middleman

If you're treating your Ruby server as a proxy for your files, you're creating a massive bottleneck. Imagine a user uploading a 100MB high-resolution PDF portfolio for a job application. Your Ruby process has to swallow that 100MB, hold onto it, and then spend several seconds streaming it to S3. While that's happening, that worker thread is blocked. If ten people do this at once, your application effectively freezes.

I've seen this crash production servers more times than I care to admit. You'll see your RAM spike and your request queue blow up, all because you're treating your application server like a file transfer service. It's just not what Ruby is for.

The Reality: Handing Out the Golden Ticket

The correct way to handle this is through Direct Uploads. Instead of the server handling the file, the server handles the permission. The flow actually looks like this: Browser → Ruby Server (Request Permission) → Browser → S3.

The secret sauce is called a Presigned URL. Your Ruby app uses the AWS SDK to generate a temporary, cryptographically signed URL that says, "Whoever has this link is allowed to upload exactly one file called 'portfolio.pdf' to this specific S3 bucket for the next 15 minutes."

The browser then takes that URL and PUTs the file directly to Amazon's infrastructure. Your server never even touches the bytes of the file; it just coordinates the handshake.

Generating the Presigned URL in Ruby

Using the aws-sdk-s3 gem, the implementation is surprisingly lean. You don't need a complex state machine; you just need a controller action that returns a JSON response with the URL.


def presign
  s3 = Aws::S3::Resource.new(region: 'us-east-1')
  obj = s3.bucket('my-app-uploads').object("uploads/#{SecureRandom.uuid}")

  # This creates a URL that is valid for 15 minutes
  url = obj.presigned_url(:put, expires_in: 900)

  render json: { url: url, key: obj.key }
end

Notice I'm using SecureRandom.uuid for the key. Never trust the filename the user provides—they might upload image.jpg, and then someone else uploads image.jpg, overwriting the first one. Always rename the file to something unique on your end.

Completing the Circuit on the Frontend

Once your Ruby app sends that JSON response back, the browser takes over. You'll use a standard fetch request to send the file. It's important to remember that this is a PUT request, not a POST.


async function uploadFile(file) {
  // 1. Ask our Ruby server for the "golden ticket"
  const response = await fetch('/uploads/presign');
  const { url, key } = await response.json();

  // 2. Upload the file directly to S3
  const upload = await fetch(url, {
    method: 'PUT',
    body: file,
    headers: { 'Content-Type': file.type }
  });

  if (upload.ok) {
    console.log('Uploaded successfully! Now save the key to the DB.');
  }
}

At this point, the file is in S3, but your database doesn't know about it yet. Your final step is to send a second request back to your Ruby server—this time just sending the key (the path to the file)—to associate that upload with a user or a record in your database.




📋 Practical Task

Build a Direct-to-S3 User Avatar Uploader

Your goal is to implement the "permission handshake" for a user profile picture upload. You will not be using a third-party library like Active Storage for this; you must implement the raw logic using the aws-sdk-s3 gem to demonstrate your understanding of the flow.

  • The Backend: Create a AvatarsController#presign action. It should generate a presigned PUT URL for a file located at avatars/#{current_user.id}.jpg. Return the URL as JSON.
  • The Frontend: Write a JavaScript function handleAvatarUpload(file) that:
    1. Fetches the presigned URL from your new controller action.
    2. Uses fetch with the PUT method to upload the file directly to the provided URL.
    3. Sends a final PATCH request to /profile containing the S3 key to update the user's record.

Verification: To verify your work, check your S3 bucket to ensure the file exists under the avatars/ prefix and confirm that your User model in the database now stores the correct S3 key string.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.