grid_attachment 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.txt +85 -0
- data/lib/grid_attachment/mongo_mapper.rb +238 -0
- data/lib/grid_attachment/mongo_odm.rb +242 -0
- data/lib/grid_attachment/mongoid.rb +238 -0
- data/lib/grid_attachment/mongomatic.rb +238 -0
- data/test/test_mongo_mapper.rb +0 -0
- data/test/test_mongo_odm.rb +0 -0
- data/test/test_mongoid.rb +0 -0
- data/test/test_mongomatic.rb +0 -0
- metadata +65 -0
data/README.txt
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
Summary
|
2
|
+
|
3
|
+
GridAttachment is a GridFS plugin for MongoDB ORMs.
|
4
|
+
Supports MongoMapper, Mongoid, MongoODM, and Mongomatic.
|
5
|
+
|
6
|
+
Support is built in for rack_grid and rack_grid_thumb to generate URLS and thumbnails:
|
7
|
+
http://github.com/dusty/rack_grid
|
8
|
+
http://github.com/dusty/rack_grid_thumb
|
9
|
+
|
10
|
+
You can pass in a File or a Hash as received by Sinatra on file uploads.
|
11
|
+
|
12
|
+
Installation
|
13
|
+
|
14
|
+
# gem install grid_attachment
|
15
|
+
|
16
|
+
Usage
|
17
|
+
|
18
|
+
require 'grid_attachment/mongo_mapper'
|
19
|
+
class Monkey
|
20
|
+
include MongoMapper::Document
|
21
|
+
plugin GridAttachment::MongoMapper
|
22
|
+
|
23
|
+
attachment :image, :prefix => :grid
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'grid_attachment/mongo_odm'
|
27
|
+
class Monkey
|
28
|
+
include MongoODM::Document
|
29
|
+
include GridAttachment::MongoODM
|
30
|
+
|
31
|
+
attachment :image, :prefix => :grid
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'grid_attachment/mongomatic'
|
35
|
+
class Monkey < Mongomatic::Base
|
36
|
+
include GridAttachment::Mongomatic
|
37
|
+
|
38
|
+
attachment :image, :prefix => :grid
|
39
|
+
end
|
40
|
+
|
41
|
+
require 'grid_attachment/mongoid'
|
42
|
+
class Monkey
|
43
|
+
include Mongoid::Document
|
44
|
+
include GridAttachment::Mongoid
|
45
|
+
|
46
|
+
attachment :image, :prefix => :grid
|
47
|
+
end
|
48
|
+
|
49
|
+
m = Monkey.new(:name => 'name')
|
50
|
+
m.save
|
51
|
+
|
52
|
+
# To add an attachment from the filesystem
|
53
|
+
m.image = File.open('/tmp/me.jpg')
|
54
|
+
m.save
|
55
|
+
|
56
|
+
# To remove an attachment
|
57
|
+
m.image = nil
|
58
|
+
m.save
|
59
|
+
|
60
|
+
# To get the attachment
|
61
|
+
m.image.read
|
62
|
+
|
63
|
+
# To get the URL for rack_grid
|
64
|
+
m.image_url # /grid/4e049e7c69c3b27d53000005/me.jpg
|
65
|
+
|
66
|
+
# To get the thumbail URL for rack_grid_thumb
|
67
|
+
m.image_thumb('50x50') # /grid/4e049e7c69c3b27d53000005/me_50x50.jpg
|
68
|
+
|
69
|
+
# HTML form example
|
70
|
+
<form action = "/monkeys" method="post" enctype="multipart/form-data">
|
71
|
+
<input id="image" name="image" type="file" />
|
72
|
+
</form>
|
73
|
+
|
74
|
+
# Use the image hash provided in params with Sinatra
|
75
|
+
post '/monkeys' do
|
76
|
+
m = Monkey.new
|
77
|
+
m.image = params[:image]
|
78
|
+
m.save
|
79
|
+
# Or just Monkey.new(params).save
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
Inspired By
|
84
|
+
- http://github.com/jnunemaker/joint
|
85
|
+
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module GridAttachment
|
5
|
+
module MongoMapper
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
##
|
11
|
+
# Declare an attachment for the object
|
12
|
+
#
|
13
|
+
# eg: attachment :image
|
14
|
+
def attachment(name,options={})
|
15
|
+
prefix = options[:prefix] ||= :grid
|
16
|
+
|
17
|
+
##
|
18
|
+
# Callbacks to handle the attachment saving and deleting
|
19
|
+
after_save :create_attachments
|
20
|
+
after_save :delete_attachments
|
21
|
+
after_destroy :queue_delete_attachments
|
22
|
+
after_destroy :delete_attachments
|
23
|
+
|
24
|
+
##
|
25
|
+
# Fields for the attachment.
|
26
|
+
#
|
27
|
+
# Only the _id is really needed, the others are helpful cached
|
28
|
+
# so you don't need to hit GridFS
|
29
|
+
key "#{name}_id".to_sym, BSON::ObjectId
|
30
|
+
key "#{name}_name".to_sym, String
|
31
|
+
key "#{name}_size".to_sym, Integer
|
32
|
+
key "#{name}_type".to_sym, String
|
33
|
+
|
34
|
+
##
|
35
|
+
# Add this name to the attachment_types
|
36
|
+
attachment_types.push(name).uniq!
|
37
|
+
|
38
|
+
##
|
39
|
+
# Return the Grid object.
|
40
|
+
# eg: image.filename, image.read
|
41
|
+
define_method(name) do
|
42
|
+
grid.get(read_attribute("#{name}_id")) if read_attribute("#{name}_id")
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Create a method to set the attachment
|
47
|
+
# eg: object.image = File.open('/tmp/somefile.jpg')
|
48
|
+
define_method("#{name}=") do |file|
|
49
|
+
# delete the old file if it exists
|
50
|
+
unless read_attribute("#{name}_id").blank?
|
51
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
52
|
+
end
|
53
|
+
case
|
54
|
+
when file.is_a?(Hash) && file[:tempfile]
|
55
|
+
send(:create_attachment, name, file)
|
56
|
+
when file.respond_to?(:read)
|
57
|
+
send(:create_attachment, name, file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Create a method to set the attachment for binary string.
|
63
|
+
# eg: object.set_image(binary_string, "generated_filename.png")
|
64
|
+
define_method("set_#{name}") do |binary, filename|
|
65
|
+
if !binary.nil?
|
66
|
+
send(:create_attachment_raw, name, binary, filename)
|
67
|
+
else
|
68
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Unset the attachment, queue for removal
|
74
|
+
define_method("unset_#{name}") do
|
75
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Return the relative URL to the attachment for use with Rack::Grid
|
80
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile.png
|
81
|
+
define_method("#{name}_url") do
|
82
|
+
_id = read_attribute("#{name}_id")
|
83
|
+
_name = read_attribute("#{name}_name")
|
84
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Return the relative URL to the thumbnail for use with Rack::GridThumb
|
89
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile_50x.png
|
90
|
+
define_method("#{name}_thumb") do |thumb|
|
91
|
+
_id = read_attribute("#{name}_id")
|
92
|
+
_name = read_attribute("#{name}_name")
|
93
|
+
_ext = File.extname(_name)
|
94
|
+
_base = File.basename(_name,_ext)
|
95
|
+
_name = "#{_base}_#{thumb}#{_ext}"
|
96
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Accessor to Grid
|
102
|
+
def grid
|
103
|
+
@grid ||= Mongo::Grid.new(::MongoMapper.database)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# All the attachments types for this class
|
108
|
+
def attachment_types
|
109
|
+
@attachment_types ||= []
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
module InstanceMethods
|
115
|
+
|
116
|
+
##
|
117
|
+
# Accessor to Grid
|
118
|
+
def grid
|
119
|
+
self.class.grid
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
##
|
124
|
+
# Holds queue of attachments to create
|
125
|
+
def create_attachment_queue
|
126
|
+
@create_attachment_queue ||= {}
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Holds queue of attachments to delete
|
131
|
+
def delete_attachment_queue
|
132
|
+
@delete_attachment_queue ||= {}
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Attachments we need to add after save.
|
137
|
+
def create_attachment(name,file)
|
138
|
+
case
|
139
|
+
when file.is_a?(Hash)
|
140
|
+
filename = file[:filename]
|
141
|
+
size = File.size(file[:tempfile])
|
142
|
+
mime = file[:type]
|
143
|
+
unless mime
|
144
|
+
type = MIME::Types.type_for(filename).first
|
145
|
+
mime = type ? type.content_type : "application/octet-stream"
|
146
|
+
end
|
147
|
+
when file.respond_to?(:read)
|
148
|
+
filename = case
|
149
|
+
when file.respond_to?(:original_filename) && file.original_filename
|
150
|
+
file.original_filename
|
151
|
+
when file.respond_to?(:tempfile)
|
152
|
+
File.basename(file.tempfile.path)
|
153
|
+
else
|
154
|
+
File.basename(file.path)
|
155
|
+
end
|
156
|
+
size = File.size(file.respond_to?(:tempfile) ? file.tempfile : file)
|
157
|
+
type = MIME::Types.type_for(filename).first
|
158
|
+
mime = type ? type.content_type : "application/octet-stream"
|
159
|
+
else
|
160
|
+
return
|
161
|
+
end
|
162
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
163
|
+
write_attribute("#{name}_name", filename)
|
164
|
+
write_attribute("#{name}_size", size)
|
165
|
+
write_attribute("#{name}_type", mime)
|
166
|
+
create_attachment_queue[name] = file
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Attachments we need to add after save.
|
171
|
+
# For binary String data.
|
172
|
+
def create_attachment_raw(name, binary, filename)
|
173
|
+
type = MIME::Types.type_for(filename).first
|
174
|
+
mime = type ? type.content_type : "application/octet-stream"
|
175
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
176
|
+
write_attribute("#{name}_name", filename)
|
177
|
+
write_attribute("#{name}_size", binary.size)
|
178
|
+
write_attribute("#{name}_type", mime)
|
179
|
+
create_attachment_queue[name] = binary
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Save an attachment to Grid
|
184
|
+
def create_grid_attachment(name,file)
|
185
|
+
data = case
|
186
|
+
when file.is_a?(Hash)
|
187
|
+
file[:tempfile].read
|
188
|
+
else
|
189
|
+
file.respond_to?(:read) ? file.read : file
|
190
|
+
end
|
191
|
+
grid.put(
|
192
|
+
data,
|
193
|
+
:filename => read_attribute("#{name}_name"),
|
194
|
+
:content_type => read_attribute("#{name}_type"),
|
195
|
+
:_id => read_attribute("#{name}_id")
|
196
|
+
)
|
197
|
+
create_attachment_queue.delete(name)
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Attachments we need to remove after save
|
202
|
+
def delete_attachment(name,id)
|
203
|
+
delete_attachment_queue[name] = id if id.is_a?(BSON::ObjectId)
|
204
|
+
write_attribute("#{name}_id", nil)
|
205
|
+
write_attribute("#{name}_name", nil)
|
206
|
+
write_attribute("#{name}_size", nil)
|
207
|
+
write_attribute("#{name}_type", nil)
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Delete an attachment from Grid
|
212
|
+
def delete_grid_attachment(name,id)
|
213
|
+
grid.delete(id) if id.is_a?(BSON::ObjectId)
|
214
|
+
delete_attachment_queue.delete(name)
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Create attachments marked for creation
|
219
|
+
def create_attachments
|
220
|
+
create_attachment_queue.each {|k,v| create_grid_attachment(k,v)}
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Delete attachments marked for deletion
|
225
|
+
def delete_attachments
|
226
|
+
delete_attachment_queue.each {|k,v| delete_grid_attachment(k,v)}
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Queues all attachments for deletion
|
231
|
+
def queue_delete_attachments
|
232
|
+
self.class.attachment_types.each do |name|
|
233
|
+
delete_attachment(name, read_attribute("#{name}_id"))
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module GridAttachment
|
5
|
+
module MongoODM
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.send(:include, InstanceMethods)
|
9
|
+
base.send(:extend, ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
##
|
15
|
+
# Declare an attachment for the object
|
16
|
+
#
|
17
|
+
# eg: attachment :image
|
18
|
+
def attachment(name,options={})
|
19
|
+
prefix = options[:prefix] ||= :grid
|
20
|
+
|
21
|
+
##
|
22
|
+
# Callbacks to handle the attachment saving and deleting
|
23
|
+
after_save :create_attachments
|
24
|
+
after_save :delete_attachments
|
25
|
+
after_destroy :queue_delete_attachments
|
26
|
+
after_destroy :delete_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, BSON::ObjectId
|
34
|
+
field "#{name}_name".to_sym, String
|
35
|
+
field "#{name}_size".to_sym, Integer
|
36
|
+
field "#{name}_type".to_sym, String
|
37
|
+
|
38
|
+
##
|
39
|
+
# Add this name to the attachment_types
|
40
|
+
attachment_types.push(name).uniq!
|
41
|
+
|
42
|
+
##
|
43
|
+
# Return the Grid object.
|
44
|
+
# eg: image.filename, image.read
|
45
|
+
define_method(name) do
|
46
|
+
grid.get(read_attribute("#{name}_id")) if read_attribute("#{name}_id")
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Create a method to set the attachment
|
51
|
+
# eg: object.image = File.open('/tmp/somefile.jpg')
|
52
|
+
define_method("#{name}=") do |file|
|
53
|
+
# delete the old file if it exists
|
54
|
+
unless read_attribute("#{name}_id").blank?
|
55
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
56
|
+
end
|
57
|
+
case
|
58
|
+
when file.is_a?(Hash) && file[:tempfile]
|
59
|
+
send(:create_attachment, name, file)
|
60
|
+
when file.respond_to?(:read)
|
61
|
+
send(:create_attachment, name, file)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Create a method to set the attachment for binary string.
|
67
|
+
# eg: object.set_image(binary_string, "generated_filename.png")
|
68
|
+
define_method("set_#{name}") do |binary, filename|
|
69
|
+
if !binary.nil?
|
70
|
+
send(:create_attachment_raw, name, binary, filename)
|
71
|
+
else
|
72
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Unset the attachment, queue for removal
|
78
|
+
define_method("unset_#{name}") do
|
79
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Return the relative URL to the attachment for use with Rack::Grid
|
84
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile.png
|
85
|
+
define_method("#{name}_url") do
|
86
|
+
_id = read_attribute("#{name}_id")
|
87
|
+
_name = read_attribute("#{name}_name")
|
88
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Return the relative URL to the thumbnail for use with Rack::GridThumb
|
93
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile_50x.png
|
94
|
+
define_method("#{name}_thumb") do |thumb|
|
95
|
+
_id = read_attribute("#{name}_id")
|
96
|
+
_name = read_attribute("#{name}_name")
|
97
|
+
_ext = File.extname(_name)
|
98
|
+
_base = File.basename(_name,_ext)
|
99
|
+
_name = "#{_base}_#{thumb}#{_ext}"
|
100
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Accessor to Grid
|
106
|
+
def grid
|
107
|
+
@grid ||= Mongo::Grid.new(::MongoODM.database)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# All the attachments types for this class
|
112
|
+
def attachment_types
|
113
|
+
@attachment_types ||= []
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
module InstanceMethods
|
119
|
+
|
120
|
+
##
|
121
|
+
# Accessor to Grid
|
122
|
+
def grid
|
123
|
+
self.class.grid
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
##
|
128
|
+
# Holds queue of attachments to create
|
129
|
+
def create_attachment_queue
|
130
|
+
@create_attachment_queue ||= {}
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Holds queue of attachments to delete
|
135
|
+
def delete_attachment_queue
|
136
|
+
@delete_attachment_queue ||= {}
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# Attachments we need to add after save.
|
141
|
+
def create_attachment(name,file)
|
142
|
+
case
|
143
|
+
when file.is_a?(Hash)
|
144
|
+
filename = file[:filename]
|
145
|
+
size = File.size(file[:tempfile])
|
146
|
+
mime = file[:type]
|
147
|
+
unless mime
|
148
|
+
type = MIME::Types.type_for(filename).first
|
149
|
+
mime = type ? type.content_type : "application/octet-stream"
|
150
|
+
end
|
151
|
+
when file.respond_to?(:read)
|
152
|
+
filename = case
|
153
|
+
when file.respond_to?(:original_filename) && file.original_filename
|
154
|
+
file.original_filename
|
155
|
+
when file.respond_to?(:tempfile)
|
156
|
+
File.basename(file.tempfile.path)
|
157
|
+
else
|
158
|
+
File.basename(file.path)
|
159
|
+
end
|
160
|
+
size = File.size(file.respond_to?(:tempfile) ? file.tempfile : file)
|
161
|
+
type = MIME::Types.type_for(filename).first
|
162
|
+
mime = type ? type.content_type : "application/octet-stream"
|
163
|
+
else
|
164
|
+
return
|
165
|
+
end
|
166
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
167
|
+
write_attribute("#{name}_name", filename)
|
168
|
+
write_attribute("#{name}_size", size)
|
169
|
+
write_attribute("#{name}_type", mime)
|
170
|
+
create_attachment_queue[name] = file
|
171
|
+
end
|
172
|
+
|
173
|
+
##
|
174
|
+
# Attachments we need to add after save.
|
175
|
+
# For binary String data.
|
176
|
+
def create_attachment_raw(name, binary, filename)
|
177
|
+
type = MIME::Types.type_for(filename).first
|
178
|
+
mime = type ? type.content_type : "application/octet-stream"
|
179
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
180
|
+
write_attribute("#{name}_name", filename)
|
181
|
+
write_attribute("#{name}_size", binary.size)
|
182
|
+
write_attribute("#{name}_type", mime)
|
183
|
+
create_attachment_queue[name] = binary
|
184
|
+
end
|
185
|
+
|
186
|
+
##
|
187
|
+
# Save an attachment to Grid
|
188
|
+
def create_grid_attachment(name,file)
|
189
|
+
data = case
|
190
|
+
when file.is_a?(Hash)
|
191
|
+
file[:tempfile].read
|
192
|
+
else
|
193
|
+
file.respond_to?(:read) ? file.read : file
|
194
|
+
end
|
195
|
+
grid.put(
|
196
|
+
data,
|
197
|
+
:filename => read_attribute("#{name}_name"),
|
198
|
+
:content_type => read_attribute("#{name}_type"),
|
199
|
+
:_id => read_attribute("#{name}_id")
|
200
|
+
)
|
201
|
+
create_attachment_queue.delete(name)
|
202
|
+
end
|
203
|
+
|
204
|
+
##
|
205
|
+
# Attachments we need to remove after save
|
206
|
+
def delete_attachment(name,id)
|
207
|
+
delete_attachment_queue[name] = id if id.is_a?(BSON::ObjectId)
|
208
|
+
write_attribute("#{name}_id", nil)
|
209
|
+
write_attribute("#{name}_name", nil)
|
210
|
+
write_attribute("#{name}_size", nil)
|
211
|
+
write_attribute("#{name}_type", nil)
|
212
|
+
end
|
213
|
+
|
214
|
+
##
|
215
|
+
# Delete an attachment from Grid
|
216
|
+
def delete_grid_attachment(name,id)
|
217
|
+
grid.delete(id) if id.is_a?(BSON::ObjectId)
|
218
|
+
delete_attachment_queue.delete(name)
|
219
|
+
end
|
220
|
+
|
221
|
+
##
|
222
|
+
# Create attachments marked for creation
|
223
|
+
def create_attachments
|
224
|
+
create_attachment_queue.each {|k,v| create_grid_attachment(k,v)}
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Delete attachments marked for deletion
|
229
|
+
def delete_attachments
|
230
|
+
delete_attachment_queue.each {|k,v| delete_grid_attachment(k,v)}
|
231
|
+
end
|
232
|
+
|
233
|
+
##
|
234
|
+
# Queues all attachments for deletion
|
235
|
+
def queue_delete_attachments
|
236
|
+
self.class.attachment_types.each do |name|
|
237
|
+
delete_attachment(name, read_attribute("#{name}_id"))
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module GridAttachment
|
5
|
+
module Mongoid
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
##
|
11
|
+
# Declare an attachment for the object
|
12
|
+
#
|
13
|
+
# eg: attachment :image
|
14
|
+
def attachment(name,options={})
|
15
|
+
prefix = options[:prefix] ||= :grid
|
16
|
+
|
17
|
+
##
|
18
|
+
# Callbacks to handle the attachment saving and deleting
|
19
|
+
after_save :create_attachments
|
20
|
+
after_save :delete_attachments
|
21
|
+
after_destroy :queue_delete_attachments
|
22
|
+
after_destroy :delete_attachments
|
23
|
+
|
24
|
+
##
|
25
|
+
# Fields for the attachment.
|
26
|
+
#
|
27
|
+
# Only the _id is really needed, the others are helpful cached
|
28
|
+
# so you don't need to hit GridFS
|
29
|
+
field "#{name}_id".to_sym, :type => BSON::ObjectId
|
30
|
+
field "#{name}_name".to_sym, :type => String
|
31
|
+
field "#{name}_size".to_sym, :type => Integer
|
32
|
+
field "#{name}_type".to_sym, :type => String
|
33
|
+
|
34
|
+
##
|
35
|
+
# Add this name to the attachment_types
|
36
|
+
attachment_types.push(name).uniq!
|
37
|
+
|
38
|
+
##
|
39
|
+
# Return the Grid object.
|
40
|
+
# eg: image.filename, image.read
|
41
|
+
define_method(name) do
|
42
|
+
grid.get(read_attribute("#{name}_id")) if read_attribute("#{name}_id")
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Create a method to set the attachment
|
47
|
+
# eg: object.image = File.open('/tmp/somefile.jpg')
|
48
|
+
define_method("#{name}=") do |file|
|
49
|
+
# delete the old file if it exists
|
50
|
+
unless read_attribute("#{name}_id").blank?
|
51
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
52
|
+
end
|
53
|
+
case
|
54
|
+
when file.is_a?(Hash) && file[:tempfile]
|
55
|
+
send(:create_attachment, name, file)
|
56
|
+
when file.respond_to?(:read)
|
57
|
+
send(:create_attachment, name, file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Create a method to set the attachment for binary string.
|
63
|
+
# eg: object.set_image(binary_string, "generated_filename.png")
|
64
|
+
define_method("set_#{name}") do |binary, filename|
|
65
|
+
if !binary.nil?
|
66
|
+
send(:create_attachment_raw, name, binary, filename)
|
67
|
+
else
|
68
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Unset the attachment, queue for removal
|
74
|
+
define_method("unset_#{name}") do
|
75
|
+
send(:delete_attachment, name, read_attribute("#{name}_id"))
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Return the relative URL to the attachment for use with Rack::Grid
|
80
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile.png
|
81
|
+
define_method("#{name}_url") do
|
82
|
+
_id = read_attribute("#{name}_id")
|
83
|
+
_name = read_attribute("#{name}_name")
|
84
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Return the relative URL to the thumbnail for use with Rack::GridThumb
|
89
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile_50x.png
|
90
|
+
define_method("#{name}_thumb") do |thumb|
|
91
|
+
_id = read_attribute("#{name}_id")
|
92
|
+
_name = read_attribute("#{name}_name")
|
93
|
+
_ext = File.extname(_name)
|
94
|
+
_base = File.basename(_name,_ext)
|
95
|
+
_name = "#{_base}_#{thumb}#{_ext}"
|
96
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Accessor to Grid
|
102
|
+
def grid
|
103
|
+
@grid ||= Mongo::Grid.new(::Mongoid.database)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# All the attachments types for this class
|
108
|
+
def attachment_types
|
109
|
+
@attachment_types ||= []
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
module InstanceMethods
|
115
|
+
|
116
|
+
##
|
117
|
+
# Accessor to Grid
|
118
|
+
def grid
|
119
|
+
self.class.grid
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
##
|
124
|
+
# Holds queue of attachments to create
|
125
|
+
def create_attachment_queue
|
126
|
+
@create_attachment_queue ||= {}
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Holds queue of attachments to delete
|
131
|
+
def delete_attachment_queue
|
132
|
+
@delete_attachment_queue ||= {}
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Attachments we need to add after save.
|
137
|
+
def create_attachment(name,file)
|
138
|
+
case
|
139
|
+
when file.is_a?(Hash)
|
140
|
+
filename = file[:filename]
|
141
|
+
size = File.size(file[:tempfile])
|
142
|
+
mime = file[:type]
|
143
|
+
unless mime
|
144
|
+
type = MIME::Types.type_for(filename).first
|
145
|
+
mime = type ? type.content_type : "application/octet-stream"
|
146
|
+
end
|
147
|
+
when file.respond_to?(:read)
|
148
|
+
filename = case
|
149
|
+
when file.respond_to?(:original_filename) && file.original_filename
|
150
|
+
file.original_filename
|
151
|
+
when file.respond_to?(:tempfile)
|
152
|
+
File.basename(file.tempfile.path)
|
153
|
+
else
|
154
|
+
File.basename(file.path)
|
155
|
+
end
|
156
|
+
size = File.size(file.respond_to?(:tempfile) ? file.tempfile : file)
|
157
|
+
type = MIME::Types.type_for(filename).first
|
158
|
+
mime = type ? type.content_type : "application/octet-stream"
|
159
|
+
else
|
160
|
+
return
|
161
|
+
end
|
162
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
163
|
+
write_attribute("#{name}_name", filename)
|
164
|
+
write_attribute("#{name}_size", size)
|
165
|
+
write_attribute("#{name}_type", mime)
|
166
|
+
create_attachment_queue[name] = file
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Attachments we need to add after save.
|
171
|
+
# For binary String data.
|
172
|
+
def create_attachment_raw(name, binary, filename)
|
173
|
+
type = MIME::Types.type_for(filename).first
|
174
|
+
mime = type ? type.content_type : "application/octet-stream"
|
175
|
+
write_attribute("#{name}_id", BSON::ObjectId.new)
|
176
|
+
write_attribute("#{name}_name", filename)
|
177
|
+
write_attribute("#{name}_size", binary.size)
|
178
|
+
write_attribute("#{name}_type", mime)
|
179
|
+
create_attachment_queue[name] = binary
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Save an attachment to Grid
|
184
|
+
def create_grid_attachment(name,file)
|
185
|
+
data = case
|
186
|
+
when file.is_a?(Hash)
|
187
|
+
file[:tempfile].read
|
188
|
+
else
|
189
|
+
file.respond_to?(:read) ? file.read : file
|
190
|
+
end
|
191
|
+
grid.put(
|
192
|
+
data,
|
193
|
+
:filename => read_attribute("#{name}_name"),
|
194
|
+
:content_type => read_attribute("#{name}_type"),
|
195
|
+
:_id => read_attribute("#{name}_id")
|
196
|
+
)
|
197
|
+
create_attachment_queue.delete(name)
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Attachments we need to remove after save
|
202
|
+
def delete_attachment(name,id)
|
203
|
+
delete_attachment_queue[name] = id if id.is_a?(BSON::ObjectId)
|
204
|
+
write_attribute("#{name}_id", nil)
|
205
|
+
write_attribute("#{name}_name", nil)
|
206
|
+
write_attribute("#{name}_size", nil)
|
207
|
+
write_attribute("#{name}_type", nil)
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Delete an attachment from Grid
|
212
|
+
def delete_grid_attachment(name,id)
|
213
|
+
grid.delete(id) if id.is_a?(BSON::ObjectId)
|
214
|
+
delete_attachment_queue.delete(name)
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Create attachments marked for creation
|
219
|
+
def create_attachments
|
220
|
+
create_attachment_queue.each {|k,v| create_grid_attachment(k,v)}
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Delete attachments marked for deletion
|
225
|
+
def delete_attachments
|
226
|
+
delete_attachment_queue.each {|k,v| delete_grid_attachment(k,v)}
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Queues all attachments for deletion
|
231
|
+
def queue_delete_attachments
|
232
|
+
self.class.attachment_types.each do |name|
|
233
|
+
delete_attachment(name, read_attribute("#{name}_id"))
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class GridmaticObserver < Mongomatic::Observer
|
5
|
+
def after_insert_or_update(instance, opts)
|
6
|
+
instance.send(:create_attachments)
|
7
|
+
instance.send(:delete_attachments)
|
8
|
+
end
|
9
|
+
def before_remove(instance,opts)
|
10
|
+
instance.send(:queue_delete_attachments)
|
11
|
+
instance.send(:delete_attachments)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module GridAttachment
|
16
|
+
module Mongomatic
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
base.send(:include, Mongomatic::Observable)
|
20
|
+
base.send(:observer, :GridmaticObserver)
|
21
|
+
base.send(:include, InstanceMethods)
|
22
|
+
base.send(:extend, ClassMethods)
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
|
27
|
+
##
|
28
|
+
# Declare an attachment for the object
|
29
|
+
#
|
30
|
+
# eg: attachment :image
|
31
|
+
def attachment(name,options={})
|
32
|
+
prefix = options[:prefix] ||= :grid
|
33
|
+
|
34
|
+
##
|
35
|
+
# Add this name to the attachment_types
|
36
|
+
attachment_types.push(name).uniq!
|
37
|
+
|
38
|
+
##
|
39
|
+
# Return the Grid object.
|
40
|
+
# eg: image.filename, image.read
|
41
|
+
define_method(name) do
|
42
|
+
grid.get(self["#{name}_id"]) if self["#{name}_id"]
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Create a method to set the attachment
|
47
|
+
# eg: object.image = File.open('/tmp/somefile.jpg')
|
48
|
+
define_method("#{name}=") do |file|
|
49
|
+
# delete the old file if it exists
|
50
|
+
unless self["#{name}_id"].blank?
|
51
|
+
send(:delete_attachment, name, self["#{name}_id"])
|
52
|
+
end
|
53
|
+
case
|
54
|
+
when file.is_a?(Hash) && file[:tempfile]
|
55
|
+
send(:create_attachment, name, file)
|
56
|
+
when file.respond_to?(:read)
|
57
|
+
send(:create_attachment, name, file)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Create a method to set the attachment for binary string.
|
63
|
+
# eg: object.set_image(binary_string, "generated_filename.png")
|
64
|
+
define_method("set_#{name}") do |binary, filename|
|
65
|
+
if !binary.nil?
|
66
|
+
send(:create_attachment_raw, name, binary, filename)
|
67
|
+
else
|
68
|
+
send(:delete_attachment, name, self["#{name}_id"])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Unset the attachment, queue for removal
|
74
|
+
define_method("unset_#{name}") do
|
75
|
+
send(:delete_attachment, name, self["#{name}_id"])
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Return the relative URL to the attachment for use with Rack::Grid
|
80
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile.png
|
81
|
+
define_method("#{name}_url") do
|
82
|
+
_id = self["#{name}_id"]
|
83
|
+
_name = self["#{name}_name"]
|
84
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Return the relative URL to the thumbnail for use with Rack::GridThumb
|
89
|
+
# eg: /grid/4ba69fde8c8f369a6e000003/somefile_50x.png
|
90
|
+
define_method("#{name}_thumb") do |thumb|
|
91
|
+
_id = self["#{name}_id"]
|
92
|
+
_name = self["#{name}_name"]
|
93
|
+
_ext = File.extname(_name)
|
94
|
+
_base = File.basename(_name,_ext)
|
95
|
+
_name = "#{_base}_#{thumb}#{_ext}"
|
96
|
+
URI.escape(["/#{prefix}", _id, _name].join('/')) if _id && _name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Accessor to Grid
|
102
|
+
def grid
|
103
|
+
@grid ||= Mongo::Grid.new(::Mongomatic.db)
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# All the attachments types for this class
|
108
|
+
def attachment_types
|
109
|
+
@attachment_types ||= []
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
module InstanceMethods
|
115
|
+
|
116
|
+
##
|
117
|
+
# Accessor to Grid
|
118
|
+
def grid
|
119
|
+
self.class.grid
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
##
|
124
|
+
# Holds queue of attachments to create
|
125
|
+
def create_attachment_queue
|
126
|
+
@create_attachment_queue ||= {}
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Holds queue of attachments to delete
|
131
|
+
def delete_attachment_queue
|
132
|
+
@delete_attachment_queue ||= {}
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Attachments we need to add after save.
|
137
|
+
def create_attachment(name,file)
|
138
|
+
case
|
139
|
+
when file.is_a?(Hash)
|
140
|
+
filename = file[:filename]
|
141
|
+
size = File.size(file[:tempfile])
|
142
|
+
mime = file[:type]
|
143
|
+
unless mime
|
144
|
+
type = MIME::Types.type_for(filename).first
|
145
|
+
mime = type ? type.content_type : "application/octet-stream"
|
146
|
+
end
|
147
|
+
when file.respond_to?(:read)
|
148
|
+
filename = case
|
149
|
+
when file.respond_to?(:original_filename) && file.original_filename
|
150
|
+
file.original_filename
|
151
|
+
when file.respond_to?(:tempfile)
|
152
|
+
File.basename(file.tempfile.path)
|
153
|
+
else
|
154
|
+
File.basename(file.path)
|
155
|
+
end
|
156
|
+
size = File.size(file.respond_to?(:tempfile) ? file.tempfile : file)
|
157
|
+
type = MIME::Types.type_for(filename).first
|
158
|
+
mime = type ? type.content_type : "application/octet-stream"
|
159
|
+
else
|
160
|
+
return
|
161
|
+
end
|
162
|
+
self["#{name}_id"] = BSON::ObjectId.new
|
163
|
+
self["#{name}_name"] = filename
|
164
|
+
self["#{name}_size"] = size
|
165
|
+
self["#{name}_type"] = mime
|
166
|
+
create_attachment_queue[name] = file
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Attachments we need to add after save.
|
171
|
+
# For binary String data.
|
172
|
+
def create_attachment_raw(name, binary, filename)
|
173
|
+
type = MIME::Types.type_for(filename).first
|
174
|
+
mime = type ? type.content_type : "application/octet-stream"
|
175
|
+
self["#{name}_id"] = BSON::ObjectId.new
|
176
|
+
self["#{name}_name"] = filename
|
177
|
+
self["#{name}_size"] = binary.size
|
178
|
+
self["#{name}_type"] = mime
|
179
|
+
create_attachment_queue[name] = binary
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# Save an attachment to Grid
|
184
|
+
def create_grid_attachment(name,file)
|
185
|
+
data = case
|
186
|
+
when file.is_a?(Hash)
|
187
|
+
file[:tempfile].read
|
188
|
+
else
|
189
|
+
file.respond_to?(:read) ? file.read : file
|
190
|
+
end
|
191
|
+
grid.put(
|
192
|
+
data,
|
193
|
+
:filename => self["#{name}_name"],
|
194
|
+
:content_type => self["#{name}_type"],
|
195
|
+
:_id => self["#{name}_id"]
|
196
|
+
)
|
197
|
+
create_attachment_queue.delete(name)
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Attachments we need to remove after save
|
202
|
+
def delete_attachment(name,id)
|
203
|
+
delete_attachment_queue[name] = id if id.is_a?(BSON::ObjectId)
|
204
|
+
self["#{name}_id"] = nil
|
205
|
+
self["#{name}_name"] = nil
|
206
|
+
self["#{name}_size"] = nil
|
207
|
+
self["#{name}_type"] = nil
|
208
|
+
end
|
209
|
+
|
210
|
+
##
|
211
|
+
# Delete an attachment from Grid
|
212
|
+
def delete_grid_attachment(name,id)
|
213
|
+
grid.delete(id) if id.is_a?(BSON::ObjectId)
|
214
|
+
delete_attachment_queue.delete(name)
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# Create attachments marked for creation
|
219
|
+
def create_attachments
|
220
|
+
create_attachment_queue.each {|k,v| create_grid_attachment(k,v)}
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Delete attachments marked for deletion
|
225
|
+
def delete_attachments
|
226
|
+
delete_attachment_queue.each {|k,v| delete_grid_attachment(k,v)}
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Queues all attachments for deletion
|
231
|
+
def queue_delete_attachments
|
232
|
+
self.class.attachment_types.each do |name|
|
233
|
+
delete_attachment(name, self["#{name}_id"])
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grid_attachment
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dusty Doris
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-08-15 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mime-types
|
16
|
+
requirement: &70132069007560 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70132069007560
|
25
|
+
description: Plugin for various Mongo ODMs to attach files via GridFS
|
26
|
+
email: github@dusty.name
|
27
|
+
executables: []
|
28
|
+
extensions: []
|
29
|
+
extra_rdoc_files:
|
30
|
+
- README.txt
|
31
|
+
files:
|
32
|
+
- README.txt
|
33
|
+
- lib/grid_attachment/mongo_mapper.rb
|
34
|
+
- lib/grid_attachment/mongo_odm.rb
|
35
|
+
- lib/grid_attachment/mongomatic.rb
|
36
|
+
- lib/grid_attachment/mongoid.rb
|
37
|
+
- test/test_mongo_mapper.rb
|
38
|
+
- test/test_mongo_odm.rb
|
39
|
+
- test/test_mongomatic.rb
|
40
|
+
- test/test_mongoid.rb
|
41
|
+
homepage: http://github.com/dusty/grid_attachment
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project: none
|
61
|
+
rubygems_version: 1.8.7
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Plugin for various Mongo ODMs to attach files via GridFS
|
65
|
+
test_files: []
|