lib/roda/plugins/public.rb
# frozen-string-literal: true require 'uri' # class Roda module RodaPlugins # The public plugin adds a +r.public+ routing method to serve static files # from a directory. # # The public plugin recognizes the application's :root option, and defaults to # using the +public+ subfolder of the application's +:root+ option. If the application's # :root option is not set, it defaults to the the +public+ folder in the working # directory. Additionally, if a relative path is provided as the :root # option to the plugin, it will be considered relative to the application's # +:root+ option. # # Examples: # # opts[:root] = '/path/to/app' # plugin :public # plugin :public, :root=>'static' module Public NULL_BYTE = "\0".freeze SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact) PARSER = RUBY_VERSION >= '1.9' ? URI::DEFAULT_PARSER : URI # Use options given to setup a Rack::File instance for serving files. Options: # :default_mime :: The default mime type to use if the mime type is not recognized. # :gzip :: Whether to serve already gzipped files with a .gz extension for clients # supporting gzipped transfer encoding. # :headers :: A hash of headers to use for statically served files # :root :: Use this option for the root of the public directory (default: "public") def self.configure(app, opts={}) root = File.expand_path(opts[:root]||"public", app.opts[:root]) app.opts[:public_server] = ::Rack::File.new(root, opts[:headers]||{}, opts[:default_mime] || 'text/plain') app.opts[:public_gzip] = opts[:gzip] end module RequestMethods # Serve files from the public directory if the file exists and this is a GET request. def public if is_get? path = PARSER.unescape(remaining_path) return if path.include?(NULL_BYTE) roda_opts = roda_class.opts server = roda_opts[:public_server] path = ::File.join(server.root, *public_path_segments(path)) if roda_opts[:public_gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/ gzip_path = path + '.gz' if public_file_readable?(gzip_path) res = public_serve(server, gzip_path) headers = res[1] if mime_type = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') headers['Content-Type'] = mime_type end headers['Content-Encoding'] = 'gzip' halt res end end if public_file_readable?(path) halt public_serve(server, path) end end end private # Return an array of segments for the given path, handling .. # and . components def public_path_segments(path) segments = [] path.split(SPLIT).each do |seg| next if seg.empty? || seg == '.' seg == '..' ? segments.pop : segments << seg end segments end # Return whether the given path is a readable regular file. def public_file_readable?(path) ::File.file?(path) && ::File.readable?(path) rescue SystemCallError false end if ::Rack.release > '2' # :nocov: def public_serve(server, path) server.serving(self, path) end # :nocov: else # Serve the given path using the given Rack::File server. def public_serve(server, path) server = server.dup server.path = path server.serving(env) end end end end register_plugin(:public, Public) end end