Skip to content

Commit

Permalink
Refactoring, tests, documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ches committed Jan 6, 2011
1 parent 85d48af commit 643251b
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 46 deletions.
95 changes: 84 additions & 11 deletions README.rdoc
Expand Up @@ -2,8 +2,8 @@

Rack:GridFS is a Rack middleware for creating HTTP endpoints for files
stored in MongoDB's GridFS. You can configure a prefix string which
will be used to match the path of a request and supply an id for looking
up the file in the GridFS store.
will be used to match the path of a request, and further look up GridFS
files based on either their +ObjectId+ or +filename+ field.

For example,

Expand All @@ -24,28 +24,101 @@ to the GridFS API prior to v1.0, you may have luck with the git-tagged version
== Usage

require 'rack/gridfs'
use Rack::GridFS, :hostname => 'localhost', :port => 27017, :database => 'test', :prefix => 'gridfs'
use Rack::GridFS, :prefix => 'gridfs', :hostname => 'localhost', :port => 27017, :database => 'test'

You must specify MongoDB database details:
- hostname: the hostname/IP where the MongoDB server is running. Default 'localhost'.
- port: the port of the MongoDB server. Default 27017.
- database: the MongoDB database to connect to.
- prefix: a string used to match against incoming paths and route to through the middleware. Default 'gridfs'.
Options:
- +prefix+: a string used to match against incoming paths and route to through
the middleware. Default 'gridfs'.
- +lookup+: whether to look up a file based on <tt>:id</tt> or <tt>:path</tt>
(example below). Default is <tt>:id</tt>.

== Sinatra Example
You must also specify MongoDB database details:
- +hostname+: the hostname/IP where the MongoDB server is running. Default 'localhost'.
- +port+: the port of the MongoDB server. Default 27017.
- +database+: the name of the MongoDB database to connect to.
- +username+ and +password+: if you need to authenticate to MongoDB.

# TODO: THIS COULD USE A LOT MORE EXPLANATION!
=== Simple Sinatra Example

require 'rubygems'
require 'sinatra'

require 'rack/gridfs'
use Rack::GridFS, :hostname => 'localhost', :port => 27017, :database => 'test', :prefix => 'gridfs'
use Rack::GridFS, :database => 'test', :prefix => 'gridfs'

get /.*/ do
"The URL did not match a file in GridFS."
end

=== Usage with Rails

To use <tt>Rack::GridFS</tt> in a Rails application, add it as middleware in
<tt>application.rb</tt> or <tt>config/environments/*</tt>with something like this:

config.middleware.insert_after Rack::Runtime, Rack::GridFS,
:prefix => 'uploads', :database => "my_app_#{Rails.env}"

Run <tt>rake middleware</tt> to decide for yourself where to best place it in
the middleware stack for your app using {the Rails convenience methods}[http://guides.rubyonrails.org/rails_on_rack.html#configuring-middleware-stack],
taking into consideration that it can probably be near the top since it simply
returns a "static" file or a 404.

=== Path (filename) Lookup

The <tt>:lookup => :path</tt> option causes files to be looked up from the GridFS
store based on their +filename+ field (which can be a full file path) rather than
+ObjectId+ (requests still need to match the +prefix+ you've set). This allows
you to find files based on essentially arbitrary URLs such as:

GET '/prefix/media/images/jane_avatar.jpg'

How filenames are set is specific to your application. We'll look at an example
with Carrierwave below.

*NOTE*: The Mongo Ruby driver will try to create an index on the +filename+
field for you automatically, but if you are using filename lookup you'll want to
double-check that it is created appropriately (on slaves only if you have a
master-slave architecture, etc.).

=== Carrierwave Example

Path lookup works well for usage with Carrierwave[https://github.com/jnicklas/carrierwave].
As a minimal example with Mongoid:

# config/initializers/carrierwave.rb
CarrierWave.configure do |config|
config.storage = :grid_fs
config.grid_fs_connection = Mongoid.database
config.grid_fs_access_url = "/uploads"
end

# app/uploaders/avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
# (Virtual) path where uploaded files will be stored, appended to the
# gridfs_access_url by methods used with view helpers
def store_dir
"#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end

# app/models/user.rb
class User
include Mongoid::Document
mount_uploader :avatar, AvatarUploader
end

# app/views/user/show.html.erb
<%= image_tag(@user.avatar.url) if @user.avatar? %>

This will result in URL paths like <tt>/uploads/user/avatar/4d250d04a8f41c0a31000006/original_filename.jpg</tt>
being generated for the view helpers, and Carrierwave will store
<tt>user/avatar/4d250d04a8f41c0a31000006/original_filename.jpg</tt> as the
+filename+ in GridFS. Thus, you can configure <tt>Rack::GridFS</tt> to serve
these files as such:

config.middleware.insert_after Rack::Runtime, Rack::GridFS,
:prefix => 'uploads', :lookup => :path, :database => "my_app_#{Rails.env}"

== Copyright

Copyright (c) 2010 Blake Carlson. See LICENSE for details.
41 changes: 20 additions & 21 deletions lib/rack/gridfs.rb
Expand Up @@ -2,25 +2,22 @@
require 'mongo'

module Rack

class GridFSConnectionError < StandardError ; end

class GridFS
VERSION = "0.2.0"

attr_reader :hostname, :port, :database, :prefix, :db

def initialize(app, options = {})
options = {
:hostname => 'localhost',
:prefix => 'gridfs',
:port => Mongo::Connection::DEFAULT_PORT,
:accessor => 'id'
:prefix => 'gridfs',
:lookup => :id
}.merge(options)

@app = app
@prefix = options[:prefix]
@accessor = options[:accessor]
@lookup = options[:lookup]
@db = nil

@hostname, @port, @database, @username, @password =
Expand All @@ -31,34 +28,36 @@ def initialize(app, options = {})

def call(env)
request = Rack::Request.new(env)
if request.path_info =~ /^\/#{prefix}\/(.+)$/
if request.path_info =~ /^\/#{@prefix}\/(.+)$/
gridfs_request($1)
else
@app.call(env)
end
end

def gridfs_request(path)
if @accessor == 'id'
file = Mongo::Grid.new(db).get(BSON::ObjectId.from_string(path))
elsif @accessor == 'path'
file = Mongo::GridFileSystem.new(db).open(path, "r")
end
[200, {'Content-Type' => file.content_type}, file]
rescue Mongo::GridFileNotFound, BSON::InvalidObjectId
[404, {'Content-Type' => 'text/plain'}, ['File not found.']]
end

private
def connect!
Timeout::timeout(5) do
@db = Mongo::Connection.new(hostname, port).db(database)
@db = Mongo::Connection.new(@hostname, @port).db(@database)
@db.authenticate(@username, @password) if @username
end
rescue Exception => e
raise Rack::GridFSConnectionError, "Unable to connect to the MongoDB server (#{e.to_s})"
end

end
def gridfs_request(identifier)
file = find_file(identifier)
[200, {'Content-Type' => file.content_type}, file]
rescue Mongo::GridFileNotFound, BSON::InvalidObjectId
[404, {'Content-Type' => 'text/plain'}, ['File not found.']]
end

def find_file(identifier)
case @lookup.to_sym
when :id then Mongo::Grid.new(@db).get(BSON::ObjectId.from_string(identifier))
when :path then Mongo::GridFileSystem.new(@db).open(identifier, "r")
end
end

end
end # GridFS class
end # Rack module
109 changes: 95 additions & 14 deletions test/gridfs_test.rb
Expand Up @@ -15,66 +15,103 @@ def db
@db ||= Mongo::Connection.new(test_database_options[:hostname], test_database_options[:port]).db(test_database_options[:database])
end

def app
gridfs_opts = test_database_options
def setup_app(opts={})
gridfs_opts = test_database_options.merge(opts)
Rack::Builder.new do
use Rack::GridFS, gridfs_opts
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
end
end

def load_artifact(filename, content_type)
def load_artifact(filename, content_type, path=nil)
contents = File.read(File.join(File.dirname(__FILE__), 'artifacts', filename))
Mongo::Grid.new(db).put(contents, :filename => filename, :content_type => content_type)
if path
grid = Mongo::GridFileSystem.new(db)
file = [path, filename].join('/')
grid.open(file, 'w') { |f| f.write contents }
grid.open(file, 'r')
else
Mongo::Grid.new(db).put(contents, :filename => filename, :content_type => content_type)
end
end

context "Rack::GridFS" do
setup do
def app; setup_app end
end

context "on initialization" do

setup do
stub_mongodb_connection
@options = { :hostname => 'myhostname.mydomain', :port => 8765, :database => 'mydatabase', :prefix => 'myprefix' }
@options = {
:hostname => 'myhostname.mydomain',
:port => 8765,
:database => 'mydatabase',
:prefix => 'myprefix',
:username => 'bob',
:password => 'so-s3cur3'
}
end

should "have a hostname option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal @options[:hostname], mware.hostname
assert_equal @options[:hostname], mware.instance_variable_get(:@hostname)
end

should "have a default hostname" do
mware = Rack::GridFS.new(nil, @options.except(:hostname))
assert_equal 'localhost', mware.hostname
assert_equal 'localhost', mware.instance_variable_get(:@hostname)
end

should "have a port option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal @options[:port], mware.port
assert_equal @options[:port], mware.instance_variable_get(:@port)
end

should "have a default port" do
mware = Rack::GridFS.new(nil, @options.except(:port))
assert_equal Mongo::Connection::DEFAULT_PORT, mware.port
assert_equal Mongo::Connection::DEFAULT_PORT, mware.instance_variable_get(:@port)
end

should "have a database option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal @options[:database], mware.database
assert_equal @options[:database], mware.instance_variable_get(:@database)
end

should "not have a default database" do
mware = Rack::GridFS.new(nil, @options.except(:database))
assert_nil mware.database
assert_nil mware.instance_variable_get(:@database)
end

should "have a prefix option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal mware.prefix, @options[:prefix]
assert_equal mware.instance_variable_get(:@prefix), @options[:prefix]
end

should "have a default prefix" do
mware = Rack::GridFS.new(nil, @options.except(:prefix))
assert_equal mware.prefix, 'gridfs'
assert_equal mware.instance_variable_get(:@prefix), 'gridfs'
end

should "have a username option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal @options[:username], mware.instance_variable_get(:@username)
end

should "have a password option" do
mware = Rack::GridFS.new(nil, @options)
assert_equal @options[:password], mware.instance_variable_get(:@password)
end

should "not have a default username" do
mware = Rack::GridFS.new(nil, @options.except(:username))
assert_nil mware.instance_variable_get(:@username)
end

should "not have a default password" do
mware = Rack::GridFS.new(nil, @options.except(:password))
assert_nil mware.instance_variable_get(:@password)
end

should "connect to the MongoDB server" do
Expand All @@ -92,7 +129,7 @@ def load_artifact(filename, content_type)
end
end

context "with files in GridFS" do
context "for lookup by ObjectId" do
setup do
@text_id = load_artifact('test.txt', 'text/plain')
@html_id = load_artifact('test.html', 'text/html')
Expand Down Expand Up @@ -135,6 +172,50 @@ def load_artifact(filename, content_type)
end
end

context "for lookup by filename" do
setup do
def app; setup_app(:lookup => :path) end
@text_file = load_artifact('test.txt', nil, path='text')
@html_file = load_artifact('test.html', nil, path='html')
end

teardown do
db.collection('fs.files').remove
end

should "return TXT files stored in GridFS" do
get "/gridfs/#{@text_file.filename}"
assert_equal "Lorem ipsum dolor sit amet.", last_response.body
end

should "return the proper content type for TXT files" do
get "/gridfs/#{@text_file.filename}"
assert_equal 'text/plain', last_response.content_type
end

should "return HTML files stored in GridFS" do
get "/gridfs/#{@html_file.filename}"
assert_match /html.*?body.*Test/m, last_response.body
end

should "return the proper content type for HTML files" do
get "/gridfs/#{@html_file.filename}"
assert_equal 'text/html', last_response.content_type
end

should "return a not found for a unknown path" do
get '/gridfs/unknown'
assert last_response.not_found?
end

should "work for small images" do
image_id = load_artifact('3wolfmoon.jpg', nil, 'images')
get "/gridfs/#{image_id.filename}"
assert last_response.ok?
assert_equal 'image/jpeg', last_response.content_type
end
end

end

end
Expand Down

0 comments on commit 643251b

Please sign in to comment.