module Merb
# Module that is mixed in to all implemented controllers.
module ControllerMixin
# Enqueu a block to run in a background thread outside of the request
# response dispatch
#
# ==== Parameters
# takes a block to run later
#
# ==== Example
# run_later do
# SomeBackgroundTask.run
# end
#
def run_later(&blk)
Merb::Dispatcher.work_queue << blk
end
# Renders the block given as a parameter using chunked encoding.
#
# ==== Parameters
# &blk::
# A block that, when called, will use send_chunks to send chunks of data
# down to the server. The chunking will terminate once the block returns.
#
# ==== Examples
# def stream
# prefix = '
'
# suffix = "
\r\n"
# render_chunked do
# IO.popen("cat /tmp/test.log") do |io|
# done = false
# until done
# sleep 0.3
# line = io.gets.chomp
#
# if line == 'EOF'
# done = true
# else
# send_chunk(prefix + line + suffix)
# end
# end
# end
# end
# end
def render_chunked(&blk)
must_support_streaming!
headers['Transfer-Encoding'] = 'chunked'
Proc.new { |response|
@response = response
response.send_status_no_connection_close('')
response.send_header
blk.call
response.write("0\r\n\r\n")
}
end
# Writes a chunk from +render_chunked+ to the response that is sent back to
# the client. This should only be called within a +render_chunked+ block.
#
# ==== Parameters
# data:: a chunk of data to return.
def send_chunk(data)
@response.write('%x' % data.size + "\r\n")
@response.write(data + "\r\n")
end
# ==== Parameters
# &blk::
# A proc that should get called outside the mutex, and which will return
# the value to render.
#
# ==== Returns
# Proc::
# A block that Mongrel can call later, allowing Merb to release the
# thread lock and render another request.
def render_deferred(&blk)
must_support_streaming!
Proc.new {|response|
result = blk.call
response.send_status(result.length)
response.send_header
response.write(result)
}
end
# Renders the passed in string, then calls the block outside the mutex and
# after the string has been returned to the client.
#
# ==== Parameters
# str:: A +String+ to return to the client.
# &blk:: A block that should get called once the string has been returned.
#
# ==== Returns
# Proc::
# A block that Mongrel can call after returning the string to the user.
def render_then_call(str, &blk)
must_support_streaming!
Proc.new {|response|
response.send_status(str.length)
response.send_header
response.write(str)
blk.call
}
end
# ==== Parameters
# url::
# URL to redirect to. It can be either a relative or fully-qualified URL.
# opts:: An options hash (see below)
#
# ==== Options (opts)
# :message::
# Messages to pass in url query string as value for "_message"
# :permanent::
# When true, return status 301 Moved Permanently
#
# ==== Returns
# String:: Explanation of redirect.
#
# ==== Examples
# redirect("/posts/34")
# redirect("/posts/34", :message => { :notice => 'Post updated successfully!' })
# redirect("http://www.merbivore.com/")
# redirect("http://www.merbivore.com/", :permanent => true)
def redirect(url, opts = {})
default_redirect_options = { :message => nil, :permanent => false }
opts = default_redirect_options.merge(opts)
if opts[:message]
notice = Merb::Request.escape([Marshal.dump(opts[:message])].pack("m"))
url = url =~ /\?/ ? "#{url}&_message=#{notice}" : "#{url}?_message=#{notice}"
end
self.status = opts[:permanent] ? 301 : 302
Merb.logger.info("Redirecting to: #{url} (#{self.status})")
headers['Location'] = url
"You are being redirected."
end
def message
@_message = defined?(@_message) ? @_message : request.message
end
# Sends a file over HTTP. When given a path to a file, it will set the
# right headers so that the static file is served directly.
#
# ==== Parameters
# file:: Path to file to send to the client.
# opts:: Options for sending the file (see below).
#
# ==== Options (opts)
# :disposition::
# The disposition of the file send. Defaults to "attachment".
# :filename::
# The name to use for the file. Defaults to the filename of file.
# :type:: The content type.
#
# ==== Returns
# IO:: An I/O stream for the file.
def send_file(file, opts={})
opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts))
disposition = opts[:disposition].dup || 'attachment'
disposition << %(; filename="#{opts[:filename] ? opts[:filename] : File.basename(file)}")
headers.update(
'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
File.open(file, 'rb')
end
# Send binary data over HTTP to the user as a file download. May set content type,
# apparent file name, and specify whether to show data inline or download as an attachment.
#
# ==== Parameters
# data:: Path to file to send to the client.
# opts:: Options for sending the data (see below).
#
# ==== Options (opts)
# :disposition::
# The disposition of the file send. Defaults to "attachment".
# :filename::
# The name to use for the file. Defaults to the filename of file.
# :type:: The content type.
def send_data(data, opts={})
opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts))
disposition = opts[:disposition].dup || 'attachment'
disposition << %(; filename="#{opts[:filename]}") if opts[:filename]
headers.update(
'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
data
end
# Streams a file over HTTP.
#
# ==== Parameters
# opts:: Options for the file streaming (see below).
# &stream::
# A block that, when called, will return an object that responds to
# +get_lines+ for streaming.
#
# ==== Options
# :disposition::
# The disposition of the file send. Defaults to "attachment".
# :type:: The content type.
# :content_length:: The length of the content to send.
# :filename:: The name to use for the streamed file.
#
# ==== Examples
# stream_file({ :filename => file_name, :type => content_type,
# :content_length => content_length }) do |response|
# AWS::S3::S3Object.stream(user.folder_name + "-" + user_file.unique_id, bucket_name) do |chunk|
# response.write chunk
# end
# end
def stream_file(opts={}, &stream)
must_support_streaming!
opts.update(Merb::Const::DEFAULT_SEND_FILE_OPTIONS.merge(opts))
disposition = opts[:disposition].dup || 'attachment'
disposition << %(; filename="#{opts[:filename]}")
headers.update(
'Content-Type' => opts[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary',
# Rack specification requires header values to respond to :each
'CONTENT-LENGTH' => opts[:content_length].to_s
)
Proc.new{|response|
response.send_status(opts[:content_length])
response.send_header
stream.call(response)
}
end
# Uses the nginx specific +X-Accel-Redirect+ header to send a file directly
# from nginx. For more information, see the nginx wiki:
# http://wiki.codemongers.com/NginxXSendfile
#
# ==== Parameters
# file:: Path to file to send to the client.
def nginx_send_file(file)
headers['X-Accel-Redirect'] = file
return ' '
end
# Sets a cookie to be included in the response.
#
# If you need to set a cookie, then use the +cookies+ hash.
#
# ==== Parameters
# name<~to_s>:: A name for the cookie.
# value<~to_s>:: A value for the cookie.
# expires<~gmtime:~strftime, Hash>:: An expiration time for the cookie, or a hash of cookie options.
# ---
# @public
def set_cookie(name, value, expires)
options = expires.is_a?(Hash) ? expires : {:expires => expires}
cookies.set_cookie(name, value, options)
end
# Marks a cookie as deleted and gives it an expires stamp in the past. This
# method is used primarily internally in Merb.
#
# Use the +cookies+ hash to manipulate cookies instead.
#
# ==== Parameters
# name<~to_s>:: A name for the cookie to delete.
def delete_cookie(name)
set_cookie(name, nil, Merb::Const::COOKIE_EXPIRED_TIME)
end
# Escapes the string representation of +obj+ and escapes it for use in XML.
#
# ==== Parameter
# obj<~to_s>:: The object to escape for use in XML.
#
# ==== Returns
# String:: The escaped object.
def escape_xml(obj)
Erubis::XmlHelper.escape_xml(obj.to_s)
end
alias h escape_xml
alias html_escape escape_xml
private
# Checks whether streaming is supported by the current Rack adapter.
#
# ==== Raises
# NotImplemented:: The Rack adapter doens't support streaming.
def must_support_streaming!
unless request.env['rack.streaming']
raise(Merb::ControllerExceptions::NotImplemented, "Current Rack adapter does not support streaming")
end
end
end
end