Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/rsence/rsence
Browse files Browse the repository at this point in the history
  • Loading branch information
Petteri Salmi committed Jun 27, 2013
2 parents 58d1055 + 3e2764e commit 4970788
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 33 deletions.
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
3.0.0.10.pre
3.0.0.11.pre
52 changes: 20 additions & 32 deletions lib/rsence/http/request.rb
@@ -1,6 +1,6 @@

module RSence

# Simple Request class, slightly more involved
# than the Rack::Request it's extending.
class Request < Rack::Request
Expand All @@ -20,38 +20,26 @@ def initialize(env)
def unparsed_uri
return @header['request-uri']
end
@@env_transl = {
'SERVER_NAME' => 'server-name',
'PATH_INFO' => 'path-info',
'SERVER_PROTOCOL' => 'server-protocol',
'REQUEST_PATH' => 'request-path',
'SERVER_SOFTWARE' => 'server-software',
'REMOTE_ADDR' => 'remote-addr',
'REQUEST_URI' => 'request-uri',
'SERVER_PORT' => 'server-port',
'QUERY_STRING' => 'query-string',
'REQUEST_METHOD' => 'request-method'
}
def env2header
[ ['SERVER_NAME', 'server-name'],
['HTTP_USER_AGENT', 'user-agent'],
['HTTP_ACCEPT_ENCODING', 'accept-encoding'],
['PATH_INFO', 'path-info'],
['HTTP_HOST', 'host'],
['HTTP_ACCEPT_LANGUAGE', 'accept-language'],
['SERVER_PROTOCOL', 'server-protocol'],
['REQUEST_PATH', 'request-path'],
['HTTP_KEEP_ALIVE', 'keep-alive'],
['SERVER_SOFTWARE', 'server-software'],
['REMOTE_ADDR', 'remote-addr'],
['HTTP_REFERER', 'referer'],
['HTTP_VERSION', 'version'],
['HTTP_ACCEPT_CHARSET', 'accept-charset'],
['REQUEST_URI', 'request-uri'],
['SERVER_PORT', 'server-port'],
['QUERY_STRING', 'query-string'],
['HTTP_ACCEPT', 'accept'],
['REQUEST_METHOD', 'request-method'],
['HTTP_CONNECTION', 'connection'],
['HTTP_SOAPACTION', 'soapaction'],
['HTTP_FORWARDED_HOST', 'forwarded-host']
].each do |env_key,header_key|
if @env.has_key?(env_key)
@header[header_key] = @env[env_key]
end
if env_key.start_with?( 'HTTP_' )
x_env_key = "HTTP_X#{env_key[4..-1]}"
if @env.has_key?( x_env_key )
@header["x-#{header_key}"] = @env[ x_env_key ]
end
@env.each do |env_key,env_val|
if @@env_transl.has_key?(env_key)
http_key = @@env_transl[env_key]
@header[http_key] = @env[env_key]
elsif env_key.start_with?( 'HTTP_' )
http_key = env_key[4..-1].gsub(/_([A-Z0-9]+)/) {|m|'-'+$1.downcase}[1..-1]
@header[http_key] = @env[env_key]
end
end
end
Expand Down
180 changes: 180 additions & 0 deletions plugins/fileserve/fileserve.rb
@@ -0,0 +1,180 @@

# The FileServe is accessible as +@plugins.fileserve+ and just +fileserve+ from other plugins.
#
# = FileServe automatically server files from your project files path, if it exists.
# You can add paths with the #serve method, which takes the mount path and filesystem path as arguments.
# If you want to unmount a mountpoint, just define path as false.
class FileServe < Servlet

# This plugin prefers the mimemagic gem for identifying files.
begin
require 'mimemagic'
@@has_magic = true
rescue LoadError
warn "Please install the 'mimemagic' gem"
@@has_magic = false
end

# Setup by serving '/files' from the project 'files' directory.
def open
super
@dirs = {}
@opts = {}
@default_opts = {
:content_type => 'binary/octet-stream',
:chunk_max => 256*1024 # 256kB
}
default_path = File.expand_path( 'files', RSence.env_path )
mount( '/files', default_path )
end

# Returns which mount point the uri belongs to. Return nil, if none.
def mountpt_of( uri )
@dirs.each_key do |mountpt|
return mountpt if uri.start_with? mountpt
end
nil
end

# Returns the option of uri, or default if none defined
def uri_opt( uri, option )
return nil unless @default_opts.include? option
mountpt = mountpt_of( uri )
return nil unless mountpt
return @opts[mountpt][option] if @opts[mountpt].has_key? option
@default_opts[option]
end

# Returns the mount state of mountpt
def mounted?( mountpt ); @dirs.has_key?( mountpt ); end

# Mounts and unmounts filesystem paths on mountpt.
# Options may include the following:
# :content_type Default content_type, if mimemagic is not installed
# :chunk_max Max size of chunk in bytes. For streaming purposes, set it fairly small.
def mount( mountpt, path=false, options={} )
if not path
@dirs.delete path
@opts.delete path
true
elsif File.exist? path and File.directory? path
@dirs[mountpt] = path
@opts[mountpt] = options
true
else
warn "FileServe#serve: no such path: #{path}"
end
false
end

# Shortcut for unmounting
def unmount( mountpt ); mount( mountpt, false ); end

# Converts uri to filesystem path from mounted dirs. Returns nil, if no match.
def uri2path( uri )
mountpt = mountpt_of( uri )
return nil unless mountpt
file_path = valid_file?( mountpt, uri, true )
return file_path if file_path
nil
end

# Returns false, unless the uri at mountpt exists, is a file and is readable.
# If return_path is true, returns the path instead of true.
def valid_file?( mountpt, uri, return_path=false )
mount_path = @dirs[mountpt]
file_path = File.expand_path( uri[(mountpt.length+1)..-1], mount_path )
return false unless file_path.start_with? mount_path
return false unless File.exist? file_path
return false unless File.readable? file_path
return false unless File.file? file_path
return true unless return_path
file_path
end

# Checks if a file exists, based on uri.
def has_file?( uri )
mountpt = mountpt_of( uri )
return false unless mountpt
valid_file?(mountpt,uri)
end

# @private Checks if a uri and method combination matches.
def match( uri, method )
return has_file? uri if method == :get
false
end

# @private Returns a 404 error
def res404( uri, res )
res.status = 404
res['Content-Type'] = 'text/plain'
body = "# 404\r\nNo such file: #{uri}"
res['Content-Length'] = body.bytesize
res.body = body
end

# @private Handles file serving, supports ranges
def serve_file( uri, res, range=nil )
file_path = uri2path( uri )
fstat = File.stat( file_path )
fh = File.open(file_path,'rb:binary')
if @@has_magic
res['Content-Type'] = MimeMagic.by_magic( fh )
else
res['Content-Type'] = uri_opt( uri, :content_type )
end
( file_len, file_mtime ) = [ fstat.size, httime(fstat.mtime) ]
res['Last-Modified'] = file_mtime
if range
( range_min, range_max ) = range
range_max = file_len if range_max.nil?
range_len = range_max - range_min
chunk_max = uri_opt( uri, :chunk_max )
if range_len > chunk_max
range_len = chunk_max
range_max = range_min+range_len
end
if file_len > range_len
res.status = 206
res['Content-Length'] = range_len
res['Accept-Ranges'] = 'bytes'
res['Content-Range'] = "bytes #{range_min}-#{range_max}/#{file_len}"
fh.seek(range_min) unless range_min == 0
file_data = fh.read(range_len)
else
range = false
end
end
unless range
res.status = 200
res['Content-Length'] = file_len
fh.seek(0)
file_data = fh.read
end
fh.close
res.body = file_data
end

# @private Responder of get requests
def get( req, res, ses )
uri = req.path
head = req.header
if head['range']
req_range = head['range']
( range_unit, range_rest ) = req_range.split('=')
( range_start, range_end ) = range_rest.split('-')
range_start = range_start.to_i
range_end = range_end.to_i unless range_end.nil?
range = [range_start,range_end]
if range_unit != 'bytes'
warn "Unknown range unit: #{range_unit.inspect}"
range = nil
end
else
range = nil
end
return res404(uri,res) unless has_file?( uri )
serve_file(uri,res,range)
end
end
6 changes: 6 additions & 0 deletions plugins/fileserve/info.yaml
@@ -0,0 +1,6 @@
title: FileServe
category: :system
depends_on: :main
description: |
FileServe serves files; by default the 'files' directory
of your project becomes mounted at '/files'.

0 comments on commit 4970788

Please sign in to comment.