Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
212: Direct Uploads to S3
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#presignaction. It should generate a presigned PUT URL for a file located atavatars/#{current_user.id}.jpg. Return the URL as JSON. - The Frontend: Write a JavaScript function
handleAvatarUpload(file)that:- Fetches the presigned URL from your new controller action.
- Uses
fetchwith thePUTmethod to upload the file directly to the provided URL. - Sends a final
PATCHrequest to/profilecontaining 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.
There are no comments for now.