Supabase offers a robust cloud storage solution through its bucket system. Although the official Ruby SDK for Supabase is under active development and does not yet support storage operations, you can interact with Supabase Storage using Ruby's built-in HTTP libraries. In this guide, we illustrate how to export files to Supabase storage buckets via the REST API.

Prerequisites

Before beginning, ensure you have:

  • Ruby 2.7 or higher installed
  • A Supabase account and project with storage enabled
  • Basic familiarity with Ruby programming
  • Your Supabase project URL and API key

Setting up your environment

Create a new Ruby project and set up the necessary dependencies. Although some libraries like net/http and json come with Ruby, we include them for clarity, and add mime-types for determining file content types:

mkdir supabase-storage-demo
cd supabase-storage-demo
bundle init

Add the following gems to your Gemfile:

source 'https://rubygems.org'

# Net/http and JSON are part of Ruby's standard library
gem 'mime-types'

Install the dependencies:

bundle install

Configuring a REST API client in Ruby

Since direct storage operations via a Ruby SDK are not available, we'll build a simple client using Ruby's Net::HTTP to interact with the Supabase Storage REST API.

require 'net/http'
require 'json'
require 'mime/types'

class SupabaseStorage
  def initialize(project_url, api_key)
    @project_url = project_url.chomp('/')
    @api_key = api_key
    @storage_url = "#{@project_url}/storage/v1"
  end

  private

  def headers
    {
      'Authorization' => "Bearer #{@api_key}",
      'apikey' => @api_key
    }
  end

  def make_request(uri, request)
    Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end
  end

  public

  # Upload a file to a specific bucket and destination path
  def upload_file(bucket_name, file_path, destination_path)
    uri = URI("#{@storage_url}/object/#{bucket_name}/#{destination_path}")
    request = Net::HTTP::Post.new(uri)
    headers.each { |key, value| request[key] = value }

    # Open the file in binary mode and set up the request stream
    File.open(file_path, 'rb') do |file|
      request.body_stream = file
      request['Content-Type'] = MIME::Types.type_for(file_path).first.to_s
      request['Content-Length'] = File.size(file_path).to_s

      response = make_request(uri, request)
      JSON.parse(response.body)
    end
  rescue StandardError => e
    { 'error' => e.message }
  end

  # List files in a bucket with an optional prefix path
  def list_files(bucket_name, path = '')
    uri = URI("#{@storage_url}/object/list/#{bucket_name}")
    uri.query = URI.encode_www_form(prefix: path)

    request = Net::HTTP::Get.new(uri)
    headers.each { |key, value| request[key] = value }

    response = make_request(uri, request)
    JSON.parse(response.body)
  rescue StandardError => e
    { 'error' => e.message }
  end

  # Delete a file from a bucket; returns true if deletion is successful
  def delete_file(bucket_name, file_path)
    uri = URI("#{@storage_url}/object/#{bucket_name}/#{file_path}")

    request = Net::HTTP::Delete.new(uri)
    headers.each { |key, value| request[key] = value }

    response = make_request(uri, request)
    response.code == '200' || response.code == '204'
  rescue StandardError => e
    false
  end
end

Usage examples

Below is an example of how to use the SupabaseStorage class to upload, list, and delete files from a Supabase bucket.

# Initialize the client with your environment variables
storage = SupabaseStorage.new(
  ENV['SUPABASE_URL'],
  ENV['SUPABASE_ANON_KEY']
)

# Upload a file
result = storage.upload_file(
  'my-bucket',
  'path/to/local/image.jpg',
  'uploads/image.jpg'
)

if result['error']
  puts "Upload failed: #{result['error']}"
else
  puts 'File uploaded successfully'
end

# List files in a bucket
files = storage.list_files('my-bucket', 'uploads/')
if files.is_a?(Array)
  files.each do |file|
    puts "File: #{file['name']}, Size: #{file.dig('metadata', 'size')}"
  end
else
  puts "Error listing files: #{files['error']}"
end

# Delete a file
if storage.delete_file('my-bucket', 'uploads/image.jpg')
  puts 'File deleted successfully'
else
  puts 'Failed to delete file'
end

Error handling and file validation

It is important to validate files before uploading to avoid errors and ensure security. The following validator checks for file existence, size limits, and allowed file types.

class FileValidator
  MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
  ALLOWED_TYPES = %w[.jpg .jpeg .png .pdf .doc .docx]

  def self.validate!(file_path)
    raise 'File not found' unless File.exist?(file_path)

    size = File.size(file_path)
    raise 'File exceeds maximum size' if size > MAX_FILE_SIZE

    extension = File.extname(file_path).downcase
    raise 'Invalid file type' unless ALLOWED_TYPES.include?(extension)

    true
  end
end

# Example usage:
begin
  FileValidator.validate!('path/to/file.jpg')
  storage.upload_file('my-bucket', 'path/to/file.jpg', 'uploads/file.jpg')
rescue StandardError => e
  puts "Validation failed: #{e.message}"
end

Best practices

Generate unique filenames

Creating unique filenames helps prevent collisions in storage. For example:

require 'securerandom'

def generate_unique_filename(original_filename)
  extension = File.extname(original_filename)
  basename = File.basename(original_filename, extension)
  timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
  "#{basename}-#{timestamp}-#{SecureRandom.hex(4)}#{extension}"
end

Organize files by date

Storing files in date-based directories can simplify management:

def generate_storage_path(filename)
  date = Time.now
  "uploads/#{date.year}/#{date.month}/#{filename}"
end

Handling large files

Supabase Storage's REST API does not currently support native chunked uploads. For large file uploads, you might need to implement custom logic by splitting your file into smaller chunks and uploading them sequentially or using an intermediary server solution.

def upload_large_file(bucket_name, file_path, destination_path, chunk_size = 5_242_880)
  File.open(file_path, 'rb') do |file|
    while (chunk = file.read(chunk_size))
      # Implement your custom chunked upload logic here.
      # This may involve storing chunks temporarily and then finalizing the upload.
      puts "Uploading chunk of size #{chunk.size} bytes..."
      # For demonstration, we simply note the chunk size.
    end
  end
end

Common use cases

  • User avatar uploads
  • Document storage systems
  • Media file management
  • Backup solutions
  • Content delivery systems

Security considerations

  • Validate files before uploading to prevent issues.
  • Store your Supabase project URL and API key in environment variables.
  • Implement proper CORS policies if exposing your API.
  • Set appropriate bucket permissions.
  • Use secure URLs for sensitive content.

Conclusion

While the official Ruby SDK for Supabase remains under active development, using Ruby's Net::HTTP and the Supabase REST API provides a viable method for managing file storage. With proper validation, error handling, and security measures, you can efficiently export files to Supabase storage buckets in your Ruby applications.

For additional media processing needs, consider exploring Transloadit's features to further enhance your workflow.