halfbyte-mongoid_grid 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ Mongoid::Grid / Rack::Grid
2
+
3
+ Mongoid::Grid is a plugin for mongoid that uses GridFS. Heavily inspired
4
+ by grip (http://github.com/jnunemaker/grip)
5
+
6
+ Rack::Grid is used to serve a GridFS file from rack. Mostly copied
7
+ from http://github.com/skinandbones/rack-gridfs
8
+
9
+ Download the source at
10
+ http://github.com/dusty/mongoid_grid
11
+
12
+
13
+ Installation
14
+
15
+ Put the libraries in your project however you want.
16
+
17
+ You could make a gem to install or use with bundler.
18
+
19
+ # git clone http://github.com/dusty/mongoid_grid
20
+ # cd mongoid_grid
21
+ # gem build mongoid_grid.gemspec
22
+
23
+ Then require the libraries you want to use.
24
+
25
+ require 'mongoid/grid'
26
+ require 'rack/grid'
27
+
28
+
29
+ Usage
30
+
31
+ class Monkey
32
+ include Mongoid::Document
33
+ include Mongoid::Grid
34
+ field :name
35
+ attachment :image
36
+ end
37
+
38
+ m = Monkey.create(:name => 'name')
39
+
40
+ # To add an attachment
41
+ m.image = File.open('/tmp/me.jpg')
42
+ m.save
43
+
44
+ # To remove an attachment
45
+ m.image = nil
46
+ m.save
47
+
48
+ # To get the attachment
49
+ m.image.read
50
+
51
+ # To use Rack::Grid with Sinatra
52
+
53
+ configure do
54
+ use Rack::Grid, :database => 'my_db'
55
+ end
56
+
57
+ <img src="<%= m.image_url %>" alt="<%= m.image_name %>" />
58
+
59
+
@@ -0,0 +1,182 @@
1
+ require 'mime/types'
2
+ require 'mongoid'
3
+ require 'active_support/all'
4
+ module Mongoid
5
+ module Grid
6
+
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
9
+ base.send(:include, InstanceMethods)
10
+ base.class_inheritable_accessor :attachment_types
11
+ base.attachment_types = []
12
+
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ ##
18
+ # Declare an attachment for the object
19
+ #
20
+ # eg: attachment :image
21
+ def attachment(name,prefix='grid')
22
+ ##
23
+ # Callbacks to handle the attachment saving and deleting
24
+ after_save :create_attachments
25
+ after_save :delete_attachments
26
+ after_destroy :destroy_attachments
27
+
28
+ ##
29
+ # Fields for the attachment.
30
+ #
31
+ # Only the _id is really needed, the others are helpful cached
32
+ # so you don't need to hit GridFS
33
+ field "#{name}_id".to_sym, :type => BSON::ObjectID
34
+ field "#{name}_name".to_sym, :type => String
35
+ field "#{name}_size".to_sym, :type => Integer
36
+ field "#{name}_type".to_sym, :type => String
37
+
38
+ ##
39
+ # Add this name to the attachment_types
40
+ attachment_types.push(name).uniq!
41
+
42
+ ##
43
+ # Return the GridFS object.
44
+ # eg: image.filename, image.read
45
+ define_method(name) do
46
+ grid.get(attributes["#{name}_id"]) if attributes["#{name}_id"]
47
+ end
48
+
49
+ ##
50
+ # Returns true if attachment exists.
51
+ # eg: image?
52
+ define_method("#{name}?") do
53
+ !!attributes["#{name}_id"]
54
+ end
55
+
56
+ ##
57
+ # Create a method to set the attachment
58
+ # eg: object.image = File.open('/tmp/somefile.jpg')
59
+ define_method("#{name}=") do |file|
60
+ if file.respond_to?(:read)
61
+ send(:create_attachment, name, file)
62
+ else
63
+ send(:delete_attachment, name, send("#{name}_id"))
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Return the relative URL to the file for use with Rack::Grid
69
+ # eg: /grid/4ba69fde8c8f369a6e000003/somefile.png
70
+ define_method("#{name}_url") do
71
+ _id = send("#{name}_id")
72
+ _name = send("#{name}_name")
73
+ ["/#{prefix}", _id, _name].join('/') if _id && _name
74
+ end
75
+
76
+ end
77
+
78
+ ##
79
+ # Accessor to GridFS
80
+ def grid
81
+ @grid ||= Mongo::Grid.new(Mongoid.database)
82
+ end
83
+
84
+ ##
85
+ # # All the attachments types for this class
86
+ # def attachment_types
87
+ # super + (@attachment_types ||= [])
88
+ # end
89
+
90
+ end
91
+
92
+ module InstanceMethods
93
+
94
+ private
95
+ ##
96
+ # Accessor to GridFS
97
+ def grid
98
+ @grid ||= self.class.grid
99
+ end
100
+
101
+ ##
102
+ # Holds queue of attachments to create
103
+ def create_attachment_queue
104
+ @create_attachment_queue ||= {}
105
+ end
106
+
107
+ ##
108
+ # Holds queue of attachments to delete
109
+ def delete_attachment_queue
110
+ @delete_attachment_queue ||= {}
111
+ end
112
+
113
+ ##
114
+ # Attachments we need to add after save.
115
+ def create_attachment(name,file)
116
+ if file.respond_to?(:read)
117
+ filename = file.respond_to?(:original_filename) ?
118
+ file.original_filename : File.basename(file.path)
119
+ type = MIME::Types.type_for(filename).first
120
+ mime = type ? type.content_type : "application/octet-stream"
121
+ send("#{name}_id=", BSON::ObjectID.new) if attributes["#{name}_id"].nil?
122
+ send("#{name}_name=", filename)
123
+ send("#{name}_size=", File.size(file))
124
+ send("#{name}_type=", mime)
125
+ create_attachment_queue[name] = file
126
+ end
127
+ end
128
+
129
+ ##
130
+ # Save an attachment to GridFS
131
+ def create_grid_attachment(name,file)
132
+ file.rewind if file.respond_to?(:rewind)
133
+ grid.delete(attributes["#{name}_id"])
134
+ grid.put(
135
+ file.read,
136
+ :filename => attributes["#{name}_name"],
137
+ :content_type => attributes["#{name}_type"],
138
+ :_id => attributes["#{name}_id"]
139
+ )
140
+ create_attachment_queue.delete(name)
141
+ end
142
+
143
+ ##
144
+ # Attachments we need to remove after save
145
+ def delete_attachment(name,id)
146
+ delete_attachment_queue[name] = id if id.is_a?(BSON::ObjectID)
147
+ send("#{name}_id=", nil)
148
+ send("#{name}_name=", nil)
149
+ send("#{name}_size=", nil)
150
+ send("#{name}_type=", nil)
151
+ end
152
+
153
+ ##
154
+ # Delete an attachment from GridFS
155
+ def delete_grid_attachment(name,id)
156
+ grid.delete(id) if id.is_a?(BSON::ObjectID)
157
+ delete_attachment_queue.delete(name)
158
+ end
159
+
160
+ ##
161
+ # Create attachments marked for creation
162
+ def create_attachments
163
+ create_attachment_queue.each {|k,v| create_grid_attachment(k,v)}
164
+ end
165
+
166
+ ##
167
+ # Delete attachments marked for deletion
168
+ def delete_attachments
169
+ delete_attachment_queue.each {|k,v| delete_grid_attachment(k,v)}
170
+ end
171
+
172
+ ##
173
+ # Deletes all attachments from document
174
+ def destroy_attachments
175
+ self.class.attachment_types.each do |name|
176
+ delete_grid_attachment(name, send("#{name}_id"))
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,59 @@
1
+ require 'timeout'
2
+ require 'mongo'
3
+
4
+ module Rack
5
+ class Grid
6
+ class ConnectionError < StandardError ; end
7
+
8
+ attr_reader :hostname, :port, :database, :prefix, :db
9
+
10
+ def initialize(app, options = {})
11
+ options = {
12
+ :hostname => 'localhost',
13
+ :prefix => 'grid',
14
+ :port => Mongo::Connection::DEFAULT_PORT,
15
+ }.merge(options)
16
+
17
+ @app = app
18
+ @hostname = options[:hostname]
19
+ @port = options[:port]
20
+ @database = options[:database]
21
+ @prefix = options[:prefix]
22
+ @db = nil
23
+
24
+ connect!
25
+ end
26
+
27
+ ##
28
+ # Strip the _id out of the path. This allows the user to send something
29
+ # like /grid/4ba69fde8c8f369a6e000003/filename.jpg to find the file
30
+ # with an id of 4ba69fde8c8f369a6e000003.
31
+ def call(env)
32
+ request = Rack::Request.new(env)
33
+ if request.path_info =~ /^\/#{prefix}\/(\w+).*$/
34
+ grid_request($1)
35
+ else
36
+ @app.call(env)
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Get file from GridFS or return a 404
42
+ def grid_request(id)
43
+ file = Mongo::Grid.new(db).get(BSON::ObjectID.from_string(id))
44
+ [200, {'Content-Type' => file.content_type}, [file.read]]
45
+ rescue Mongo::GridError, BSON::InvalidObjectID
46
+ [404, {'Content-Type' => 'text/plain'}, ['File not found.']]
47
+ end
48
+
49
+ private
50
+ def connect!
51
+ Timeout::timeout(5) do
52
+ @db = Mongo::Connection.new(hostname).db(database)
53
+ end
54
+ rescue StandardError => e
55
+ raise ConnectionError, "Timeout connecting to GridFS (#{e.to_s})"
56
+ end
57
+
58
+ end
59
+ end
Binary file
Binary file
@@ -0,0 +1 @@
1
+ test1
@@ -0,0 +1 @@
1
+ test2
Binary file
@@ -0,0 +1,45 @@
1
+ require 'tempfile'
2
+ require 'pp'
3
+ require 'shoulda'
4
+ #require 'matchy'
5
+ require 'mocha'
6
+ require 'mongoid'
7
+
8
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/mongoid/grid')
9
+
10
+ Mongoid.configure do |config|
11
+ name = "mongoid_grid_test"
12
+ host = "localhost"
13
+ config.allow_dynamic_fields = false
14
+ config.master = Mongo::Connection.new.db(name)
15
+ end
16
+
17
+ class Test::Unit::TestCase
18
+ def setup
19
+ Mongoid.database.collections.each(&:remove)
20
+ end
21
+
22
+ def assert_difference(expression, difference = 1, message = nil, &block)
23
+ b = block.send(:binding)
24
+ exps = Array.wrap(expression)
25
+ before = exps.map { |e| eval(e, b) }
26
+ yield
27
+ exps.each_with_index do |e, i|
28
+ error = "#{e.inspect} didn't change by #{difference}"
29
+ error = "#{message}.\n#{error}" if message
30
+ assert_equal(before[i] + difference, eval(e, b), error)
31
+ end
32
+ end
33
+
34
+ def assert_no_difference(expression, message = nil, &block)
35
+ assert_difference(expression, 0, message, &block)
36
+ end
37
+
38
+ def assert_grid_difference(difference=1, &block)
39
+ assert_difference("Mongoid.database['fs.files'].find().count", difference, &block)
40
+ end
41
+
42
+ def assert_no_grid_difference(&block)
43
+ assert_grid_difference(0, &block)
44
+ end
45
+ end
@@ -0,0 +1,284 @@
1
+ require 'test_helper'
2
+
3
+ class Asset
4
+ include Mongoid::Document
5
+ include Mongoid::Grid
6
+
7
+ field :title, :type => String
8
+ attachment :image
9
+ attachment :file
10
+ end
11
+
12
+ class BaseModel
13
+ include Mongoid::Document
14
+ include Mongoid::Grid
15
+ attachment :file
16
+ end
17
+
18
+ class Image < BaseModel; attachment :image end
19
+ class Video < BaseModel; attachment :video end
20
+
21
+ module Mongoid::GridTestHelpers
22
+ def all_files
23
+ [@file, @image, @image2, @test1, @test2]
24
+ end
25
+
26
+ def rewind_files
27
+ all_files.each { |file| file.rewind }
28
+ end
29
+
30
+ def open_file(name)
31
+ File.open(File.join(File.dirname(__FILE__), 'fixtures', name), 'r')
32
+ end
33
+
34
+ def grid
35
+ @grid ||= Mongo::Grid.new(Mongoid.database)
36
+ end
37
+
38
+ def key_names
39
+ [:id, :name, :type, :size]
40
+ end
41
+ end
42
+
43
+ class Mongoid::GridTest < Test::Unit::TestCase
44
+ include Mongoid::GridTestHelpers
45
+
46
+ def setup
47
+ super
48
+ @file = open_file('unixref.pdf')
49
+ @image = open_file('mr_t.jpg')
50
+ @image2 = open_file('harmony.png')
51
+ @test1 = open_file('test1.txt')
52
+ @test2 = open_file('test2.txt')
53
+ end
54
+
55
+ def teardown
56
+ all_files.each { |file| file.close }
57
+ end
58
+
59
+ context "Using Grid plugin" do
60
+ should "add each attachment to attachment_types" do
61
+ assert_equal [:image, :file], Asset.attachment_types
62
+ end
63
+
64
+ should "add keys for each attachment" do
65
+ key_names.each do |key|
66
+ assert Asset.fields.keys.include?("image_#{key}")
67
+ assert Asset.fields.keys.include?("file_#{key}")
68
+ end
69
+ end
70
+
71
+ context "with inheritance" do
72
+ should "add attachment to attachment_types" do
73
+ assert_equal [:file], BaseModel.attachment_types
74
+ end
75
+
76
+ should "inherit attachments from superclass, but not share other inherited class attachments" do
77
+ assert_equal [:file, :image], Image.attachment_types
78
+ assert_equal [:file, :video], Video.attachment_types
79
+ end
80
+
81
+ should "add inherit keys from superclass" do
82
+ key_names.each do |key|
83
+ assert BaseModel.fields.keys.include?("file_#{key}")
84
+ assert Image.fields.keys.include?("file_#{key}")
85
+ assert Video.fields.keys.include?("file_#{key}")
86
+ assert Video.fields.keys.include?("video_#{key}")
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ context "Assigning new attachments to document" do
93
+ setup do
94
+ @doc = Asset.create(:image => @image, :file => @file)
95
+ rewind_files
96
+ end
97
+ subject { @doc }
98
+
99
+ should "assign GridFS content_type" do
100
+ assert_equal 'image/jpeg', grid.get(subject.image_id).content_type
101
+ assert_equal 'application/pdf', grid.get(subject.file_id).content_type
102
+ end
103
+
104
+ should "assign joint keys" do
105
+ assert_equal 13661, subject.image_size
106
+ assert_equal 68926, subject.file_size
107
+
108
+ assert_equal "image/jpeg", subject.image_type
109
+ assert_equal "application/pdf", subject.file_type
110
+
111
+ assert_not_nil subject.image_id
112
+ assert_not_nil subject.file_id
113
+
114
+ assert subject.image_id.instance_of?(BSON::ObjectID)
115
+ assert subject.file_id.instance_of?(BSON::ObjectID)
116
+ end
117
+
118
+ should "allow accessing keys through attachment proxy" do
119
+ assert_equal 13661, subject.image_size
120
+ assert_equal 68926, subject.file_size
121
+
122
+ assert_equal "image/jpeg", subject.image_type
123
+ assert_equal "application/pdf", subject.file_type
124
+
125
+ assert_not_nil subject.image_id
126
+ assert_not_nil subject.file_id
127
+
128
+ # assert subject.image.id.instance_of?(BSON::ObjectID),
129
+ # assert subject.file.id.instance_of?(BSON::ObjectID)
130
+ end
131
+
132
+ should "proxy unknown methods to GridIO object" do
133
+ assert_equal subject.image_id, subject.image.files_id
134
+ assert_equal 'image/jpeg', subject.image.content_type
135
+ assert_equal 'mr_t.jpg', subject.image.filename
136
+ assert_equal 13661, subject.image.file_length
137
+ end
138
+
139
+ should "assign file name from path if original file name not available" do
140
+ assert_equal 'mr_t.jpg', subject.image_name
141
+ assert_equal 'unixref.pdf', subject.file_name
142
+ end
143
+
144
+ should "save attachment contents correctly" do
145
+ assert_equal @file.read, subject.file.read
146
+ assert_equal @image.read, subject.image.read
147
+ end
148
+
149
+ should "know that attachment exists" do
150
+ assert subject.image?
151
+ assert subject.file?
152
+ end
153
+
154
+ should "clear assigned attachments so they don't get uploaded twice" do
155
+ Mongo::Grid.any_instance.expects(:put).never
156
+ subject.save
157
+ end
158
+ end
159
+
160
+ context "Updating existing attachment" do
161
+ setup do
162
+ @doc = Asset.create(:file => @test1)
163
+ assert_no_grid_difference do
164
+ @doc.file = @test2
165
+ @doc.save!
166
+ end
167
+ rewind_files
168
+ end
169
+ subject { @doc }
170
+
171
+ should "not change attachment id" do
172
+ assert !subject.file_id_changed?
173
+ end
174
+
175
+ should "update keys" do
176
+ assert_equal 'test2.txt', subject.file_name
177
+ assert_equal "text/plain", subject.file_type
178
+ assert_equal 5, subject.file_size
179
+ end
180
+
181
+ should "update GridFS" do
182
+ grid_obj = grid.get(subject.file_id)
183
+ assert_equal 'test2.txt', grid_obj.filename
184
+ assert_equal 'text/plain', grid_obj.content_type
185
+ assert_equal 5, grid_obj.file_length
186
+ assert_equal @test2.read, grid_obj.read
187
+ end
188
+ end
189
+
190
+ context "Updating document but not attachments" do
191
+ setup do
192
+ @doc = Asset.create(:image => @image)
193
+ @doc.update_attributes(:title => 'Updated')
194
+ @doc.reload
195
+ rewind_files
196
+ end
197
+ subject { @doc }
198
+
199
+ should "not affect attachment" do
200
+ assert_equal @image.read, subject.image.read
201
+ end
202
+
203
+ should "update document attributes" do
204
+ assert_equal('Updated', subject.title)
205
+ end
206
+ end
207
+
208
+ context "Assigning file where file pointer is not at beginning" do
209
+ setup do
210
+ @image.read
211
+ @doc = Asset.create(:image => @image)
212
+ @doc.reload
213
+ rewind_files
214
+ end
215
+ subject { @doc }
216
+
217
+ should "rewind and correctly store contents" do
218
+ assert_equal @image.read, subject.image.read
219
+ end
220
+ end
221
+
222
+ context "Setting attachment to nil" do
223
+ setup do
224
+ @doc = Asset.create(:image => @image)
225
+ rewind_files
226
+ end
227
+ subject { @doc }
228
+
229
+ should "delete attachment after save" do
230
+ assert_no_grid_difference { subject.image = nil }
231
+ assert_grid_difference(-1) { subject.save }
232
+ end
233
+
234
+ should "clear nil attachments after save and not attempt to delete again" do
235
+ Mongo::Grid.any_instance.expects(:delete).once
236
+ subject.image = nil
237
+ subject.save
238
+ Mongo::Grid.any_instance.expects(:delete).never
239
+ subject.save
240
+ end
241
+ end
242
+
243
+ context "Retrieving attachment that does not exist" do
244
+ setup do
245
+ @doc = Asset.create
246
+ rewind_files
247
+ end
248
+ subject { @doc }
249
+
250
+ should "know that the attachment is not present" do
251
+ assert !subject.image?
252
+ end
253
+
254
+ # should "raise Mongo::GridFileNotFound" do
255
+ # assert_raises(Mongo::GridFileNotFound) { subject.image.read }
256
+ # end
257
+ end
258
+
259
+ context "Destroying a document" do
260
+ setup do
261
+ @doc = Asset.create(:image => @image)
262
+ rewind_files
263
+ end
264
+ subject { @doc }
265
+
266
+ should "remove files from grid fs as well" do
267
+ assert_grid_difference(-1) { subject.destroy }
268
+ end
269
+ end
270
+
271
+ context "Assigning file name" do
272
+ should "default to path" do
273
+ assert_equal 'mr_t.jpg', Asset.create(:image => @image).image_name
274
+ end
275
+
276
+ should "use original_filename if available" do
277
+ def @image.original_filename
278
+ 'testing.txt'
279
+ end
280
+ doc = Asset.create(:image => @image)
281
+ assert_equal 'testing.txt', doc.image_name
282
+ end
283
+ end
284
+ end
File without changes
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: halfbyte-mongoid_grid
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 2
9
+ version: 0.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Jan Krutisch
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-24 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: mime-types
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: mongoid
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 1
41
+ - 9
42
+ version: "1.9"
43
+ type: :runtime
44
+ version_requirements: *id002
45
+ description: Plugin for Mongoid to use GridFS and a Rack helper
46
+ email: jan+mongoid-grid@krutisch.de
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README.txt
53
+ files:
54
+ - README.txt
55
+ - lib/mongoid/grid.rb
56
+ - lib/rack/grid.rb
57
+ - test/test_helper.rb
58
+ - test/test_mongoid_grid.rb
59
+ - test/test_rack_grid.rb
60
+ - test/fixtures/harmony.png
61
+ - test/fixtures/mr_t.jpg
62
+ - test/fixtures/test1.txt
63
+ - test/fixtures/test2.txt
64
+ - test/fixtures/unixref.pdf
65
+ has_rdoc: true
66
+ homepage: http://github.com/halfbyte/mongoid_grid
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options: []
71
+
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ requirements: []
89
+
90
+ rubyforge_project: none
91
+ rubygems_version: 1.3.6
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Plugin for Mongoid to use GridFS and a Rack helper
95
+ test_files: []
96
+