Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Refactoring, tests, documentation

  • Loading branch information...
commit 643251b83cf9f354072fca397ad7a93461911169 1 parent 85d48af
@ches ches authored
Showing with 199 additions and 46 deletions.
  1. +84 −11 README.rdoc
  2. +20 −21 lib/rack/gridfs.rb
  3. +95 −14 test/gridfs_test.rb
View
95 README.rdoc
@@ -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,
@@ -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.
View
41 lib/rack/gridfs.rb
@@ -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 =
@@ -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
View
109 test/gridfs_test.rb
@@ -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
@@ -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')
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.