Implementing the Client Pattern in Ruby for File Hosting Services

Reading Time: 4 minutes

Integrating multiple file hosting services can become cumbersome as each platform has different APIs, authentication methods, and file-handling processes. Without a clear structure, this can lead to a messy codebase, slowing down development and making it harder to switch providers. That’s where the Client Pattern comes in. It offers a scalable, maintainable way to handle multiple service integrations without complicating your application’s core logic.

In this blog post, I’ll guide you through implementing the Client pattern for integrating multiple file hosting services, such as Gofile and DDownload. You’ll learn how to define a common base client and extend it for each provider, streamlining your integration process and ensuring consistent, maintainable code.

But before diving into that we must answer the following questions you might be wondering about: What is the Client pattern? Why use it?

What is the Client Pattern?

The Client Pattern focuses on creating a distinct module or class that acts as a bridge between your application and a particular service or library. It simplifies the process by centralizing key operations such as sending HTTP requests, managing authentication, and interpreting API responses, allowing the rest of the application to interact with the service through a clean, unified interface.

Think of the Client Pattern as a translator between your app and external services. It standardizes communication, so regardless of the file hosting provider you use, your app speaks the same language when it comes to uploads and downloads.

Why Use the Client Pattern?

The Client Pattern allows us to:

  • Abstract common behavior into a base client.
  • Easily switch between different file hosting providers.
  • Handle the specific details of each provider without cluttering the main logic.
  • As your application scales, integrating more file hosting providers becomes as simple as extending the base client, rather than reinventing the wheel for each new service.
  • Keep the code clean and maintainable, allowing for easy integration of new services in the future.

Let’s start with the Base client.

At the heart of this design is a base client class that defines common behavior, such as HTTP client configuration and abstract methods for uploading and retrieving files. Each file hosting provider will extend this base class and implement its own version of these methods.

require 'net/http'

module FileHosting
  module Base
    class Client
      def upload(file_path)
        raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
      end

      def retrieve(file_handle)
        raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
      end

      def http_client(uri)
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = uri.scheme == 'https'
        http.verify_mode = if @ssl_verify
                             OpenSSL::SSL::VERIFY_PEER
                           else
                             OpenSSL::SSL::VERIFY_NONE
                           end
        http
      end
    end
  end
end
Fundamental Aspects of the Base Client
  • Abstract Methods: Both the upload and retrieve methods raise NotImplementedError. This ensures that subclasses (specific file hosting providers) must implement these methods.
  • HTTP Client: The http_client method handles HTTP connections with SSL support. It allows optional SSL verification based on the @ssl_verify variable.

Gofile Client Implementation

Now that we have a solid foundation with the Base Client, let’s implement the Gofile client, a popular file hosting provider.

module FileHosting
  module Providers
    module Gofile
      class Client < FileHosting::Base::Client
        def initialize(api_key: ENV.fetch('GOFILE_API_KEY'))
          @api_key = api_key
          @base_url = 'https://api.gofile.io'
        end

        def upload(file_path)
          uri = URI("#{select_server}/contents/uploadfile") # Select server and construct URI
          request = Net::HTTP::Post.new(uri)   # Create POST request
          authorize(request) # Set authorization header with API key
          request.set_form([
                             ['file', File.open(file_path)] # Add file to request as multipart/form-data
                           ], 'multipart/form-data')

          response = http_client(uri).request(request) # Send request and get response
          JSON.parse(response.body)  # Parse and return JSON response
        end

        def retrieve(file_handle)
          uri = URI("#{@base_url}/contents/#{file_handle}")
          request = Net::HTTP::Get.new(uri)
          authorize(request)
          response = http_client(uri).request(request)
          JSON.parse(response.body)
        end

        def select_server
          uri = URI("#{@base_url}/servers?key=#{@api_key}")
          request = Net::HTTP::Get.new(uri)
          server_response = http_client(uri).request(request)
          authorize(request)
          server_data = JSON.parse(server_response.body)
          raise 'Failed to get upload server' unless server_data['status'] == 'ok'

          server_name = server_data['data']['servers'].sample['name']
          @base_url.gsub('api', server_name)
        end

        def authorize(request)
          request['Authorization'] = "Bearer #{@api_key}"
        end
      end
    end
  end
end
Fundamental Aspects of the Gofile Client
  • API Key: The Gofile::Client is initialized with an API key for authentication.
  • Upload File: The upload method selects the appropriate server and uploads the file using a multipart form.
  • Retrieve File Info: The retrieve method retrieves information about a specific file from Gofile.
  • Server Selection: The select_server method dynamically selects the best server for file uploads.
  • Authorization: The authorize method sets the appropriate authorization headers for requests.

DDownload Client Implementation

Here’s how the DDownload client can be implemented by extending the base client:

module FileHosting
  module Providers
    module DDownload
      class Client < FileHosting::Base::Client
        def initialize(api_key: ENV.fetch('DDOWNLOAD_API_KEY'))
          @api_key = api_key
          @base_url = 'https://api-v2.ddownload.com/api'
        end

        def upload(file_path)
          server_data = select_server
          uri = URI(server_data['result'])
          request = Net::HTTP::Post.new(uri)
          request.set_form([
                             ['key', @api_key],
                             ['file', File.open(file_path)],
                             ['sess_id', server_data['sess_id']]
                           ], 'multipart/form-data')

          response = http_client(uri).request(request)

          JSON.parse(response.body)
        end

        def retrieve(file_handle)
          uri = URI("#{@base_url}/file/info?key=#{@api_key}&file_code=#{file_handle}")
          response = http_client(uri).get(uri)
          JSON.parse(response.body)
        end

        def select_server
          uri = URI("#{@base_url}/upload/server?key=#{@api_key}")
          server_response = Net::HTTP.get_response(uri)
          server_data = JSON.parse(server_response.body)
          raise "Failed to get upload server: #{server_data['msg']}" unless server_data['status'] == 200

          server_data
        end
      end
    end
  end
end
Fundamental Aspects of the DDownload Client:
  • Server Selection: Just like Gofile, the DDownload client selects an upload server dynamically before uploading the file.
  • Authorization: It uses an API key for authorization, passed as a form parameter in the upload requests.
  • Upload File: Files are uploaded via multipart forms with an additional session ID (sess_id).
  • Retrieve File Info: File details are retrieved using the retrieve method by providing the file handle.

Usage

file = Rails.root.join('app/assets/images/kitty.png')
gofile = FileHosting::Providers::Gofile::Client.new

gofile.upload(file) => 
{"data"=>
  {"createTime"=>1727895101,
   "downloadPage"=>"https://gofile.io/d/WqeeUZ",
   "id"=>"508ab745-ac98-43e3-bf16-b0e316668ff7",
   "md5"=>"e0e128f00d44667b4605cf0dbe8a475d",
   "mimetype"=>"image/jpeg",
   "modTime"=>1727895101,
   "name"=>"kitty.jpg",
   "parentFolder"=>"4d8d0a92-dc11-4311-8dc0-33f9bf9614ca",
   "parentFolderCode"=>"WqeeUZ",
   "servers"=>["store3"],
   "size"=>243563,
   "type"=>"file"},
 "status"=>"ok"}
file = Rails.root.join('app/assets/images/kitty.png')
ddownload = FileHosting::Providers::DDownload::Client.new

ddownload.upload(file) => 
[{"file_code"=>"pt5h7u1h0tv9", "file_status"=>"OK"}]

Adding New Providers with Flexibility

Let’s consider a scenario where you want to give users the option to choose their preferred file hosting provider. The following service allows you to dynamically select the correct provider based on the user’s input or configuration.

module FileHosting
  class Service
    def self.create(provider)
      case  provider
      when :gofile
        FileHosting::Providers::Gofile::Client.new
      when :ddownload
        FileHosting::Providers::DDownload::Client.new
      else
        raise "Unsupported provider: #{provider}"
      end
    end
  end
end

# usage
service = FileHosting::Service.create(:gofile)
service2 = FileHosting::Service.create(:ddownload)

Conclusion

By using the Client Pattern, we’ve built a flexible, maintainable system that allows us to integrate with multiple file hosting services easily. The Base Client provides common functionality, while each provider implements its own file upload and retrieval logic. This approach makes it easy to add new providers or update existing ones without disrupting the rest of the system.

This architecture is particularly useful when dealing with third-party APIs in various contexts.
As your application grows and requires integration with more file hosting providers,
this pattern will save time and reduce complexity.

Note: Another improvement you could add is the ability to return formatted responses for each provider. Instead of returning a hash object, you can normalize the response to a consistent format, providing a more user-friendly interface.

Hire an Expert

Integrating multiple file hosting services or third-party APIs can be challenging, but we’re here to make it simple! Our expert team specializes in creating scalable, maintainable solutions using design patterns like the Client Pattern to ensure clean, flexible, and efficient code. Whether you’re looking to refactor existing integrations or build a robust system from the ground up, our experienced developers are ready to help you deliver reliable, high-performance results.

Hire an expert from MagmaLabs today and experience the benefits of seamless, maintainable integrations!

0 Shares:
You May Also Like