diff --git a/README.rdoc b/README.rdoc index 1195065..eee3679 100644 --- a/README.rdoc +++ b/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 :id or :path + (example below). Default is :id. -== 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 Rack::GridFS in a Rails application, add it as middleware in +application.rb or config/environments/*with something like this: + + config.middleware.insert_after Rack::Runtime, Rack::GridFS, + :prefix => 'uploads', :database => "my_app_#{Rails.env}" + +Run rake middleware 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 :lookup => :path 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 /uploads/user/avatar/4d250d04a8f41c0a31000006/original_filename.jpg +being generated for the view helpers, and Carrierwave will store +user/avatar/4d250d04a8f41c0a31000006/original_filename.jpg as the ++filename+ in GridFS. Thus, you can configure Rack::GridFS 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. diff --git a/lib/rack/gridfs.rb b/lib/rack/gridfs.rb index 17a5a76..79535d6 100644 --- a/lib/rack/gridfs.rb +++ b/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 diff --git a/test/gridfs_test.rb b/test/gridfs_test.rb index 952dcf7..8f55f53 100644 --- a/test/gridfs_test.rb +++ b/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