grid_attachment 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|