sudara/alonetone

View on GitHub
bin/fastly

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby

# Starts an HTTP server on port 6060 that implements just enough of the
# Fastly Image Optimition APIs so it can be used with Alonetone. When you
# are using Alonetone over HTTP you can set fastly_base_url in the
# configuration to http://localhost:6060.

require 'yaml'
require 'rack'
require 'webrick'
require 'pathname'
require 'open-uri'
require 'digest/sha1'

require 'aws-sdk-s3'
require 'ruby-vips'

require_relative '../lib/configurable'

module Image
  # Transforms a source image to an optimized JPEG and optionally downscales to
  # a maximum width when the width is specified. It takes a few short-cuts
  # because all Alonetone images are cropped to a square.
  #
  # For example:
  #
  #   Image::Transformation.new(image_data, width: 800).jpeg
  class Transformation
    attr_reader :image
    attr_reader :transformations

    def initialize(original, transformations)
      @transformations = transformations
      @image = Vips::Image.new_from_buffer(original, "")
    end

    def width
      transformations['width']&.to_i
    end

    def quality
      transformations.fetch('quality', 60).to_i
    end

    def jpeg
      transform.jpegsave_buffer(
        # The following are JPEG writer options: JPEG quality 65%, optimize
        # JPEG to reduce file size, leave EXIF data out of the file.
        Q: quality, optimize_coding: true, strip: true
      )
    end

    private

    def transform_options
      { crop: :centre }
    end

    def transform
      # Vips automatically applies EXIF rotation to images when the Image is
      # initialized so we don't have to explicitly specify this operation.
      if width
        image.thumbnail_image(width, transform_options)
      else
        image
      end
    end
  end
end

class Fastly
  # WEBrick servlet that serves the images.
  class Servlet < WEBrick::HTTPServlet::AbstractServlet
    LSTRIP = %r{\A/}

    def initialize(server, bucket, path_prefix)
      super server
      @bucket = bucket
      @path_prefix = path_prefix
    end

    def service(http_request, http_response)
      key = http_request.path.split(@path_prefix).last.gsub(LSTRIP, '')
      object = @bucket.object(key)
      params = Rack::Utils.parse_query(http_request.query_string)
      etag = '"' + Digest::SHA1.hexdigest(http_request.request_uri.to_s) + '"'
      http_response.status = 200
      http_response['ETag'] = etag
      http_response['Content-Type'] = 'image/jpeg'
      http_response['Expires'] = (Time.now + 3600).httpdate
      http_response['Cache-Control'] = 'max-age=3600'
      http_response.body = Image::Transformation.new(
        object.get.body.read, params
      ).jpeg
    rescue => e
      http_response.status = 500
      http_response.body = e.message + "\n" + http_request.path
    end
  end

  def run
    start_server
  end

  private

  def port
    ENV.fetch('PORT', 6060)
  end

  def server
    @server ||= build_server
  end

  def build_server
    server = WEBrick::HTTPServer.new(Port: port)
    server.mount "/", Fastly::Servlet, bucket, path_prefix
    server
  end

  def start_server
    trap "INT" do
      server.shutdown
    end
    server.start
  end

  def environment
    'development'
  end

  def root
    Pathname.new(File.expand_path('../..', __FILE__))
  end

  def config_filename
    root.join('config/alonetone.yml')
  end

  def config_hash
    YAML.load_file(config_filename)
  end

  def config
    ::Configurable.new(environment, config_hash[environment])
  end

  def client
    Aws::S3::Client.new(
      region: config.amazon_s3_region,
      access_key_id: config.amazon_access_key_id,
      secret_access_key: config.amazon_secret_access_key
    )
  end

  def bucket
    Aws::S3::Bucket.new(config.amazon_s3_bucket_name, client: client)
  end

  def path_prefix
    URI.parse(config.fastly_base_url).path
  end
end

Fastly.new.run