staticd 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/staticd +7 -0
- data/lib/rack/auth/hmac.rb +81 -0
- data/lib/rack/request_time.rb +19 -0
- data/lib/staticd.rb +7 -0
- data/lib/staticd/api.rb +356 -0
- data/lib/staticd/app.rb +130 -0
- data/lib/staticd/cache_engine.rb +45 -0
- data/lib/staticd/cli.rb +70 -0
- data/lib/staticd/config.rb +115 -0
- data/lib/staticd/database.rb +41 -0
- data/lib/staticd/datastore.rb +64 -0
- data/lib/staticd/datastores/local.rb +48 -0
- data/lib/staticd/datastores/s3.rb +63 -0
- data/lib/staticd/domain_generator.rb +42 -0
- data/lib/staticd/http_cache.rb +65 -0
- data/lib/staticd/http_server.rb +197 -0
- data/lib/staticd/json_request.rb +15 -0
- data/lib/staticd/json_response.rb +29 -0
- data/lib/staticd/models/base.rb +43 -0
- data/lib/staticd/models/domain_name.rb +17 -0
- data/lib/staticd/models/release.rb +20 -0
- data/lib/staticd/models/resource.rb +19 -0
- data/lib/staticd/models/route.rb +19 -0
- data/lib/staticd/models/site.rb +36 -0
- data/lib/staticd/models/staticd_config.rb +71 -0
- data/lib/staticd/public/jquery-1.11.1.min.js +4 -0
- data/lib/staticd/public/main.css +61 -0
- data/lib/staticd/public/main.js +15 -0
- data/lib/staticd/version.rb +3 -0
- data/lib/staticd/views/main.haml +9 -0
- data/lib/staticd/views/setup.haml +47 -0
- data/lib/staticd/views/welcome.haml +92 -0
- data/lib/staticd_utils/archive.rb +80 -0
- data/lib/staticd_utils/file_size.rb +26 -0
- data/lib/staticd_utils/gli_object.rb +14 -0
- data/lib/staticd_utils/memory_file.rb +36 -0
- data/lib/staticd_utils/sitemap.rb +100 -0
- data/lib/staticd_utils/tar.rb +43 -0
- metadata +300 -0
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            require "digest/sha1"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Staticd
         | 
| 4 | 
            +
              module Datastores
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # Datastore storing files on local directory.
         | 
| 7 | 
            +
                #
         | 
| 8 | 
            +
                # It use the file SHA1 digest as a filename so two identical files are not
         | 
| 9 | 
            +
                # stored twice.
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                # Example:
         | 
| 12 | 
            +
                #   datastore = Staticd::Datastores::Local.new(path: "/tmp/datastore")
         | 
| 13 | 
            +
                #   datastore.put(file_path) unless datastore.exist?(file_path)
         | 
| 14 | 
            +
                #   # => "/tmp/datastore/sha1_digest"
         | 
| 15 | 
            +
                class Local
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize(params)
         | 
| 18 | 
            +
                    @path = params[:path]
         | 
| 19 | 
            +
                    check_store_directory
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def put(file_path)
         | 
| 23 | 
            +
                    datastore_file = stored_file_path(file_path)
         | 
| 24 | 
            +
                    FileUtils.copy_file(file_path, datastore_file) unless exist?(file_path)
         | 
| 25 | 
            +
                    datastore_file
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def exist?(file_path)
         | 
| 29 | 
            +
                    datastore_file = stored_file_path(file_path)
         | 
| 30 | 
            +
                    File.exist?(datastore_file) ? datastore_file : false
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  private
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def check_store_directory
         | 
| 36 | 
            +
                    FileUtils.mkdir_p(@path) unless File.directory?(@path)
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def sha1(file_path)
         | 
| 40 | 
            +
                    Digest::SHA1.hexdigest(File.read(file_path))
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def stored_file_path(file_path)
         | 
| 44 | 
            +
                    "#{@path}/#{sha1(file_path)}"
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            require "digest/sha1"
         | 
| 2 | 
            +
            require "aws-sdk"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Staticd
         | 
| 5 | 
            +
              module Datastores
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                # Datastore storing files on Amazon S3.
         | 
| 8 | 
            +
                #
         | 
| 9 | 
            +
                # It use the file SHA1 digest as a filename so two identical files are not
         | 
| 10 | 
            +
                # stored twice.
         | 
| 11 | 
            +
                # Each files can be accessed afterwards using a public HTTP(S) address.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # Example:
         | 
| 14 | 
            +
                #   datastore = Staticd::Datastores::S3.new(
         | 
| 15 | 
            +
                #     host: bucket_name,
         | 
| 16 | 
            +
                #     username: access_key_id,
         | 
| 17 | 
            +
                #     password: secret_access_key
         | 
| 18 | 
            +
                #   )
         | 
| 19 | 
            +
                #   datastore.put(file_path) unless datastore.exist?(file_path)
         | 
| 20 | 
            +
                #   # => http://bucket_name.hostname/sha1_digest
         | 
| 21 | 
            +
                class S3
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def initialize(params)
         | 
| 24 | 
            +
                    @bucket_name = params[:host]
         | 
| 25 | 
            +
                    @access_key = ENV["AWS_ACCESS_KEY_ID"]
         | 
| 26 | 
            +
                    @secret_key = ENV["AWS_SECRET_ACCESS_KEY"]
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def put(file_path)
         | 
| 30 | 
            +
                    s3_object = object(file_path)
         | 
| 31 | 
            +
                    s3_object.write(file: file_path)
         | 
| 32 | 
            +
                    s3_object.acl = :public_read
         | 
| 33 | 
            +
                    s3_object.public_url(secure: false).to_s
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def exist?(file_path)
         | 
| 37 | 
            +
                    s3_object = object(file_path)
         | 
| 38 | 
            +
                    s3_object.exists? ? s3_object.public_url(secure: false) : false
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def s3
         | 
| 44 | 
            +
                    @s3 ||= AWS::S3.new(
         | 
| 45 | 
            +
                      access_key_id: @access_key,
         | 
| 46 | 
            +
                      secret_access_key: @secret_key
         | 
| 47 | 
            +
                    )
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def bucket
         | 
| 51 | 
            +
                    @bucket ||= s3.buckets[@bucket_name]
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def object(file_path)
         | 
| 55 | 
            +
                    bucket.objects[sha1(file_path)]
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def sha1(file_path)
         | 
| 59 | 
            +
                    Digest::SHA1.hexdigest(File.read(file_path))
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            module Staticd
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Domain name generator.
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              # This class can be used to generate random words of various length.
         | 
| 6 | 
            +
              # Options:
         | 
| 7 | 
            +
              # * length: the length of the random word
         | 
| 8 | 
            +
              # * suffix: a suffix to append to the random world (default is none)
         | 
| 9 | 
            +
              #
         | 
| 10 | 
            +
              # Example:
         | 
| 11 | 
            +
              #   DomainGenerator.new(length: 2, suffix: ".domain.tld")
         | 
| 12 | 
            +
              #   # => rb.domain.tld
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              # A block can be used to validate the generated domain. It must return true
         | 
| 15 | 
            +
              # to validate the domain, otherwise a new one is proposed. This feature can
         | 
| 16 | 
            +
              # be used to validate the domain against certain rules.
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              # Example:
         | 
| 19 | 
            +
              #   DomainGenerator.new(suffix: ".domain.tld") do |generated_domain|
         | 
| 20 | 
            +
              #     ["admin", "www"].include?(generated_domain)
         | 
| 21 | 
            +
              #   end
         | 
| 22 | 
            +
              class DomainGenerator
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def self.new(options={})
         | 
| 25 | 
            +
                  if block_given?
         | 
| 26 | 
            +
                    until domain = generate(options)
         | 
| 27 | 
            +
                      yield domain
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                    domain
         | 
| 30 | 
            +
                  else
         | 
| 31 | 
            +
                    generate(options)
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def self.generate(options={})
         | 
| 36 | 
            +
                  length = options[:length] || 6
         | 
| 37 | 
            +
                  suffix = options[:suffix] || ""
         | 
| 38 | 
            +
                  random = ("a".."z").to_a.shuffle[0, length].join
         | 
| 39 | 
            +
                  random + suffix
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            require "staticd/database"
         | 
| 2 | 
            +
            require "staticd/cache_engine"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Staticd
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              # Rack middleware to manage remote HTTP resources caching.
         | 
| 7 | 
            +
              #
         | 
| 8 | 
            +
              # For each request, it will find the last release of the site associed with
         | 
| 9 | 
            +
              # the requested domain name and cache the corresponding resource if not
         | 
| 10 | 
            +
              # already cached.
         | 
| 11 | 
            +
              class HTTPCache
         | 
| 12 | 
            +
                include Staticd::Models
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def initialize(http_root, app)
         | 
| 15 | 
            +
                  @app = app
         | 
| 16 | 
            +
                  @http_root = http_root
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  raise "No HTTP root folder provided" unless @http_root
         | 
| 19 | 
            +
                  raise "No rack app provided" unless @app
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def call(env)
         | 
| 23 | 
            +
                  dup._call(env)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def _call(env)
         | 
| 27 | 
            +
                  @env = env
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  # Change the Request Path to '/index.html' if root path is asked.
         | 
| 30 | 
            +
                  @env["PATH_INFO"] = "/index.html" if @env["PATH_INFO"] == '/'
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  # Get the release from the request Host header.
         | 
| 33 | 
            +
                  release = Release.last(
         | 
| 34 | 
            +
                    Release.site.domain_names.name => Rack::Request.new(@env).host
         | 
| 35 | 
            +
                  )
         | 
| 36 | 
            +
                  return next_middleware unless release
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  # Change the script name to include the site name and release version.
         | 
| 39 | 
            +
                  @env["SCRIPT_NAME"] = "/#{release.site_name}/#{release}"
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  req = Rack::Request.new(@env)
         | 
| 42 | 
            +
                  cache_engine = CacheEngine.new(@http_root)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  # Do nothing else if the resource is already cached.
         | 
| 45 | 
            +
                  return next_middleware if cache_engine.cached?(req.path)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  # Get the resource to cache.
         | 
| 48 | 
            +
                  resource = Resource.first(
         | 
| 49 | 
            +
                    Resource.routes.release_id => release.id,
         | 
| 50 | 
            +
                    Resource.routes.path => req.path_info
         | 
| 51 | 
            +
                  )
         | 
| 52 | 
            +
                  return next_middleware unless resource
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  # Cache the resource.
         | 
| 55 | 
            +
                  cache_engine.cache(req.path, resource.url)
         | 
| 56 | 
            +
                  next_middleware
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                private
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def next_middleware
         | 
| 62 | 
            +
                  @app.call(@env)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,197 @@ | |
| 1 | 
            +
            require "sendfile"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Staticd
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # Simple HTTP server Rack app.
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # If the resource is readable from the root folder at the path given by the
         | 
| 8 | 
            +
              # request, the resource is sent to the client. Otherwise a 404 Not Found HTTP
         | 
| 9 | 
            +
              # error is sent.
         | 
| 10 | 
            +
              class HTTPServer
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # Mime types served by the webserver (from NGiNX mime.types file).
         | 
| 13 | 
            +
                EXT_MIME_TYPE = {
         | 
| 14 | 
            +
                  ".html"    => "text/html",
         | 
| 15 | 
            +
                  ".htm"     => "text/html",
         | 
| 16 | 
            +
                  ".shtml"   => "text/html",
         | 
| 17 | 
            +
                  ".css"     => "text/css",
         | 
| 18 | 
            +
                  ".xml"     => "text/xml",
         | 
| 19 | 
            +
                  ".rss"     => "text/xml",
         | 
| 20 | 
            +
                  ".gif"     => "image/gif",
         | 
| 21 | 
            +
                  ".jpeg"    => "image/jpeg",
         | 
| 22 | 
            +
                  ".jpg"     => "image/jpeg",
         | 
| 23 | 
            +
                  ".js"      => "application/x-javascript",
         | 
| 24 | 
            +
                  ".txt"     => "text/plain",
         | 
| 25 | 
            +
                  ".htc"     => "text/x-component",
         | 
| 26 | 
            +
                  ".mml"     => "text/mathml",
         | 
| 27 | 
            +
                  ".png"     => "image/png",
         | 
| 28 | 
            +
                  ".ico"     => "image/x-icon",
         | 
| 29 | 
            +
                  ".jng"     => "image/x-jng",
         | 
| 30 | 
            +
                  ".wbmp"    => "image/vnd.wap.wbmp",
         | 
| 31 | 
            +
                  ".jar"     => "application/java-archive",
         | 
| 32 | 
            +
                  ".war"     => "application/java-archive",
         | 
| 33 | 
            +
                  ".ear"     => "application/java-archive",
         | 
| 34 | 
            +
                  ".hqx"     => "application/mac-binhex40",
         | 
| 35 | 
            +
                  ".pdf"     => "application/pdf",
         | 
| 36 | 
            +
                  ".cco"     => "application/x-cocoa",
         | 
| 37 | 
            +
                  ".jardiff" => "application/x-java-archive-diff",
         | 
| 38 | 
            +
                  ".jnlp"    => "application/x-java-jnlp-file",
         | 
| 39 | 
            +
                  ".run"     => "application/x-makeself",
         | 
| 40 | 
            +
                  ".pm"      => "application/x-perl",
         | 
| 41 | 
            +
                  ".pl"      => "application/x-perl",
         | 
| 42 | 
            +
                  ".prc"     => "application/x-pilot",
         | 
| 43 | 
            +
                  ".pdb"     => "application/x-pilot",
         | 
| 44 | 
            +
                  ".rar"     => "application/x-rar-compressed",
         | 
| 45 | 
            +
                  ".rpm"     => "application/x-redhat-package-manager",
         | 
| 46 | 
            +
                  ".sea"     => "application/x-sea",
         | 
| 47 | 
            +
                  ".swf"     => "application/x-shockwave-flash",
         | 
| 48 | 
            +
                  ".sit"     => "application/x-stuffit",
         | 
| 49 | 
            +
                  ".tcl"     => "application/x-tcl",
         | 
| 50 | 
            +
                  ".tk"      => "application/x-tcl",
         | 
| 51 | 
            +
                  ".der"     => "application/x-x509-ca-cert",
         | 
| 52 | 
            +
                  ".pem"     => "application/x-x509-ca-cert",
         | 
| 53 | 
            +
                  ".crt"     => "application/x-x509-ca-cert",
         | 
| 54 | 
            +
                  ".xpi"     => "application/x-xpinstall",
         | 
| 55 | 
            +
                  ".zip"     => "application/zip",
         | 
| 56 | 
            +
                  ".deb"     => "application/octet-stream",
         | 
| 57 | 
            +
                  ".bin"     => "application/octet-stream",
         | 
| 58 | 
            +
                  ".exe"     => "application/octet-stream",
         | 
| 59 | 
            +
                  ".dll"     => "application/octet-stream",
         | 
| 60 | 
            +
                  ".dmg"     => "application/octet-stream",
         | 
| 61 | 
            +
                  ".eot"     => "application/octet-stream",
         | 
| 62 | 
            +
                  ".iso"     => "application/octet-stream",
         | 
| 63 | 
            +
                  ".img"     => "application/octet-stream",
         | 
| 64 | 
            +
                  ".msi"     => "application/octet-stream",
         | 
| 65 | 
            +
                  ".msp"     => "application/octet-stream",
         | 
| 66 | 
            +
                  ".msm"     => "application/octet-stream",
         | 
| 67 | 
            +
                  ".mp3"     => "audio/mpeg",
         | 
| 68 | 
            +
                  ".ra"      => "audio/x-realaudio",
         | 
| 69 | 
            +
                  ".mpeg"    => "video/mpeg",
         | 
| 70 | 
            +
                  ".mpg"     => "video/mpeg",
         | 
| 71 | 
            +
                  ".mov"     => "video/quicktime",
         | 
| 72 | 
            +
                  ".flv"     => "video/x-flv",
         | 
| 73 | 
            +
                  ".avi"     => "video/x-msvideo",
         | 
| 74 | 
            +
                  ".wmv"     => "video/x-ms-wmv",
         | 
| 75 | 
            +
                  ".asx"     => "video/x-ms-asf",
         | 
| 76 | 
            +
                  ".asf"     => "video/x-ms-asf",
         | 
| 77 | 
            +
                  ".mng"     => "video/x-mng"
         | 
| 78 | 
            +
                }
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                # Mime type used when no type has been identified.
         | 
| 81 | 
            +
                DEFAULT_MIME_TYPE = "application/octet-stream"
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def initialize(http_root, access_logger=nil)
         | 
| 84 | 
            +
                  @http_root = http_root
         | 
| 85 | 
            +
                  unless (@access_logger = access_logger)
         | 
| 86 | 
            +
                    @access_logger = Logger.new(STDOUT)
         | 
| 87 | 
            +
                    @access_logger.formatter = proc { |_, _, _, msg| "#{msg}\n"}
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  raise "No HTTP root folder provided" unless @http_root
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def call(env)
         | 
| 94 | 
            +
                  dup._call(env)
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                def _call(env)
         | 
| 98 | 
            +
                  @env = env
         | 
| 99 | 
            +
                  req = Rack::Request.new(@env)
         | 
| 100 | 
            +
                  file_path = @http_root + req.path
         | 
| 101 | 
            +
                  res = File.readable?(file_path) ? serve(file_path) : send_404
         | 
| 102 | 
            +
                  log(req, res)
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                private
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                # Log a request using the "Extended Log File Format".
         | 
| 108 | 
            +
                # See: http://www.w3.org/TR/WD-logfile.html
         | 
| 109 | 
            +
                #
         | 
| 110 | 
            +
                # Use the request.time key setup by the Rack::RequestTime middleware.
         | 
| 111 | 
            +
                #
         | 
| 112 | 
            +
                # Version: 1.0
         | 
| 113 | 
            +
                # Fields: time cs-dns cs-ip date cs-method cs-uri sc-status sc-byte sc-time-taken
         | 
| 114 | 
            +
                def log(req, res)
         | 
| 115 | 
            +
                  request_stop_time = Time.now
         | 
| 116 | 
            +
                  request_start_time =
         | 
| 117 | 
            +
                    req.env.key?("request.time") ? req.env["request.time"] : nil
         | 
| 118 | 
            +
                  request_completed_time =
         | 
| 119 | 
            +
                    if request_start_time
         | 
| 120 | 
            +
                      (request_stop_time - request_start_time).round(4)
         | 
| 121 | 
            +
                    else
         | 
| 122 | 
            +
                      "-"
         | 
| 123 | 
            +
                    end
         | 
| 124 | 
            +
                  content_length =
         | 
| 125 | 
            +
                    res[1].key?("Content-Length") ? " #{res[1]["Content-Length"]}" : "-"
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  log_string = "#{request_stop_time.strftime("%Y-%m-%d %H:%M:%S")}"
         | 
| 128 | 
            +
                  log_string << " #{req.host}"
         | 
| 129 | 
            +
                  log_string << " #{req.env["REMOTE_ADDR"]}"
         | 
| 130 | 
            +
                  log_string << " #{req.env["REQUEST_METHOD"]} #{req.path_info}"
         | 
| 131 | 
            +
                  log_string << " #{res[0]}"
         | 
| 132 | 
            +
                  log_string << content_length
         | 
| 133 | 
            +
                  log_string << " #{request_completed_time}"
         | 
| 134 | 
            +
                  @access_logger.info(log_string)
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                  res
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                # Serve a file.
         | 
| 140 | 
            +
                #
         | 
| 141 | 
            +
                # This method will return a Rack compatible response ready to be served to
         | 
| 142 | 
            +
                # the client. It will use the appropriate method (loading file into memory
         | 
| 143 | 
            +
                # vs serving file using the sendfile system call) based on availability.
         | 
| 144 | 
            +
                def serve(file_path)
         | 
| 145 | 
            +
                  @env['rack.hijack?'] ? sendfile(file_path) : send(file_path)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                # Send a file loading it in memory.
         | 
| 149 | 
            +
                #
         | 
| 150 | 
            +
                # Method used in the first implementation.
         | 
| 151 | 
            +
                # Keep it for compatibility purpose when the Rack hijack API is not
         | 
| 152 | 
            +
                # supported.
         | 
| 153 | 
            +
                def send(file_path)
         | 
| 154 | 
            +
                  response = Rack::Response.new
         | 
| 155 | 
            +
                  response["Content-Type"] = mime(file_path)
         | 
| 156 | 
            +
                  File.foreach(file_path) { |chunk| response.write(chunk) }
         | 
| 157 | 
            +
                  response.finish
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                # Use sendfile system call to send file without loading it into memory.
         | 
| 161 | 
            +
                #
         | 
| 162 | 
            +
                # It use the sendfile gem and the rack hijacking api.
         | 
| 163 | 
            +
                # See: https://github.com/codeslinger/sendfile
         | 
| 164 | 
            +
                # See: http://blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
         | 
| 165 | 
            +
                def sendfile(file_path)
         | 
| 166 | 
            +
             | 
| 167 | 
            +
                  response_header = {
         | 
| 168 | 
            +
                    "Content-Type" => mime(file_path),
         | 
| 169 | 
            +
                    "Content-Length" => size(file_path),
         | 
| 170 | 
            +
                    "Connection" => "close"
         | 
| 171 | 
            +
                  }
         | 
| 172 | 
            +
                  response_header["rack.hijack"] = lambda do |io|
         | 
| 173 | 
            +
                    begin
         | 
| 174 | 
            +
                      File.open(file_path) { |file| io.sendfile(file) }
         | 
| 175 | 
            +
                      io.flush
         | 
| 176 | 
            +
                    ensure
         | 
| 177 | 
            +
                      io.close
         | 
| 178 | 
            +
                    end
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
                  [200, response_header]
         | 
| 181 | 
            +
                end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                def send_404
         | 
| 184 | 
            +
                  res = Rack::Response.new(["Not Found"], 404, {})
         | 
| 185 | 
            +
                  res.finish
         | 
| 186 | 
            +
                end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                def mime(file_path)
         | 
| 189 | 
            +
                  ext = File.extname(file_path).downcase
         | 
| 190 | 
            +
                  EXT_MIME_TYPE.key?(ext) ? EXT_MIME_TYPE[ext] : DEFAULT_MIME_TYPE
         | 
| 191 | 
            +
                end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                def size(file_path)
         | 
| 194 | 
            +
                  File.size(file_path).to_s
         | 
| 195 | 
            +
                end
         | 
| 196 | 
            +
              end
         | 
| 197 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            require "json"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Staticd
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              # Simple HTTP response constructor for JSON content.
         | 
| 6 | 
            +
              #
         | 
| 7 | 
            +
              # Example:
         | 
| 8 | 
            +
              #   response = JSONResponse.send(:success, {foo: :bar})
         | 
| 9 | 
            +
              class JSONResponse
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.send(type, content=nil)
         | 
| 12 | 
            +
                  case type
         | 
| 13 | 
            +
                  when :success
         | 
| 14 | 
            +
                    @status = 200
         | 
| 15 | 
            +
                    @body = content
         | 
| 16 | 
            +
                  when :success_no_content
         | 
| 17 | 
            +
                    @status = 204
         | 
| 18 | 
            +
                  when :error
         | 
| 19 | 
            +
                    @status = 403
         | 
| 20 | 
            +
                    @body = {error: content}
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    @status = 500
         | 
| 23 | 
            +
                    @body = {error: "Something went wrong on our side."}
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                  json_body = @body ? JSON.generate(@body) : nil
         | 
| 26 | 
            +
                  [@status, json_body]
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         |