mongoid-grid_fs 2.0.0 → 2.1.0

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.
@@ -0,0 +1,591 @@
1
+ require "mongoid"
2
+ require "mime/types"
3
+
4
+ ##
5
+ #
6
+ module Mongoid
7
+ class GridFs
8
+ class << GridFs
9
+ attr_accessor :namespace
10
+ attr_accessor :file_model
11
+ attr_accessor :chunk_model
12
+
13
+ def init!
14
+ GridFs.build_namespace_for(:Fs)
15
+
16
+ GridFs.namespace = Fs
17
+ GridFs.file_model = Fs.file_model
18
+ GridFs.chunk_model = Fs.chunk_model
19
+
20
+ const_set(:File, Fs.file_model)
21
+ const_set(:Chunk, Fs.chunk_model)
22
+
23
+ to_delegate = %w(
24
+ put
25
+ get
26
+ delete
27
+ find
28
+ []
29
+ []=
30
+ clear
31
+ )
32
+
33
+ to_delegate.each do |method|
34
+ class_eval <<-__
35
+ def self.#{ method }(*args, &block)
36
+ ::Mongoid::GridFs::Fs::#{ method }(*args, &block)
37
+ end
38
+ __
39
+ end
40
+ end
41
+ end
42
+
43
+ ##
44
+ #
45
+ def GridFs.namespace_for(prefix)
46
+ prefix = prefix.to_s.downcase
47
+ const = "::GridFs::#{ prefix.to_s.camelize }"
48
+ namespace = const.split(/::/).last
49
+ const_defined?(namespace) ? const_get(namespace) : build_namespace_for(namespace)
50
+ end
51
+
52
+ ##
53
+ #
54
+ def GridFs.build_namespace_for(prefix)
55
+ prefix = prefix.to_s.downcase
56
+ const = prefix.camelize
57
+
58
+ namespace =
59
+ Module.new do
60
+ module_eval(&NamespaceMixin)
61
+ self
62
+ end
63
+
64
+ const_set(const, namespace)
65
+
66
+ file_model = build_file_model_for(namespace)
67
+ chunk_model = build_chunk_model_for(namespace)
68
+
69
+ file_model.namespace = namespace
70
+ chunk_model.namespace = namespace
71
+
72
+ file_model.chunk_model = chunk_model
73
+ chunk_model.file_model = file_model
74
+
75
+ namespace.prefix = prefix
76
+ namespace.file_model = file_model
77
+ namespace.chunk_model = chunk_model
78
+
79
+ namespace.send(:const_set, :File, file_model)
80
+ namespace.send(:const_set, :Chunk, chunk_model)
81
+
82
+ #at_exit{ file_model.create_indexes rescue nil }
83
+ #at_exit{ chunk_model.create_indexes rescue nil }
84
+
85
+ const_get(const)
86
+ end
87
+
88
+ NamespaceMixin = proc do
89
+ class << self
90
+ attr_accessor :prefix
91
+ attr_accessor :file_model
92
+ attr_accessor :chunk_model
93
+
94
+ def to_s
95
+ prefix
96
+ end
97
+
98
+ def namespace
99
+ prefix
100
+ end
101
+
102
+ def put(readable, attributes = {})
103
+ chunks = []
104
+ file = file_model.new
105
+ attributes.to_options!
106
+
107
+ if attributes.has_key?(:id)
108
+ file.id = attributes.delete(:id)
109
+ end
110
+
111
+ if attributes.has_key?(:_id)
112
+ file.id = attributes.delete(:_id)
113
+ end
114
+
115
+ if attributes.has_key?(:content_type)
116
+ attributes[:contentType] = attributes.delete(:content_type)
117
+ end
118
+
119
+ if attributes.has_key?(:upload_date)
120
+ attributes[:uploadDate] = attributes.delete(:upload_date)
121
+ end
122
+
123
+ if attributes.has_key?(:meta_data)
124
+ attributes[:metadata] = attributes.delete(:meta_data)
125
+ end
126
+
127
+ if attributes.has_key?(:aliases)
128
+ attributes[:aliases] = Array(attributes.delete(:aliases)).flatten.compact.map{|a| "#{ a }"}
129
+ end
130
+
131
+ md5 = Digest::MD5.new
132
+ length = 0
133
+ chunkSize = file.chunkSize
134
+ n = 0
135
+
136
+ GridFs.reading(readable) do |io|
137
+ unless attributes.has_key?(:filename)
138
+ attributes[:filename] =
139
+ [file.id.to_s, GridFs.extract_basename(io)].join('/').squeeze('/')
140
+ end
141
+
142
+ unless attributes.has_key?(:contentType)
143
+ attributes[:contentType] =
144
+ GridFs.extract_content_type(attributes[:filename]) || file.contentType
145
+ end
146
+
147
+ GridFs.chunking(io, chunkSize) do |buf|
148
+ md5 << buf
149
+ length += buf.size
150
+ chunk = file.chunks.build
151
+ chunk.data = binary_for(buf)
152
+ chunk.n = n
153
+ n += 1
154
+ chunk.save!
155
+ chunks.push(chunk)
156
+ end
157
+ end
158
+
159
+ attributes[:length] ||= length
160
+ attributes[:uploadDate] ||= Time.now.utc
161
+ attributes[:md5] ||= md5.hexdigest
162
+
163
+ file.update_attributes(attributes)
164
+
165
+ file.save!
166
+ file
167
+ rescue
168
+ chunks.each{|chunk| chunk.destroy rescue nil}
169
+ raise
170
+ end
171
+
172
+ def binary_for(*buf)
173
+ if defined?(Moped::BSON)
174
+ Moped::BSON::Binary.new(:generic, buf.join)
175
+ else
176
+ BSON::Binary.new(buf.join, :generic)
177
+ end
178
+ end
179
+
180
+ def get(id)
181
+ file_model.find(id)
182
+ end
183
+
184
+ def delete(id)
185
+ file_model.find(id).destroy
186
+ rescue
187
+ nil
188
+ end
189
+
190
+ def where(conditions = {})
191
+ case conditions
192
+ when String
193
+ file_model.where(:filename => conditions)
194
+ else
195
+ file_model.where(conditions)
196
+ end
197
+ end
198
+
199
+ def find(*args)
200
+ where(*args).first
201
+ end
202
+
203
+ def [](filename)
204
+ file_model.
205
+ where(:filename => filename.to_s).
206
+ order_by(:uploadDate => :desc).
207
+ limit(1).
208
+ first
209
+ end
210
+
211
+ def []=(filename, readable)
212
+ put(readable, :filename => filename.to_s)
213
+ end
214
+
215
+ def clear
216
+ file_model.destroy_all
217
+ end
218
+
219
+ # TODO - opening with a mode = 'w' should return a GridIO::IOProxy
220
+ # implementing a StringIO-like interface
221
+ #
222
+ def open(filename, mode = 'r', &block)
223
+ raise NotImplementedError
224
+ end
225
+ end
226
+ end
227
+
228
+ ##
229
+ #
230
+ class Defaults < ::Hash
231
+ def method_missing(method, *args, &block)
232
+ case method.to_s
233
+ when /(.*)=/
234
+ key = $1
235
+ val = args.first
236
+ update(key => val)
237
+ else
238
+ key = method.to_s
239
+ super unless has_key?(key)
240
+ fetch(key)
241
+ end
242
+ end
243
+ end
244
+
245
+ ##
246
+ #
247
+ def GridFs.build_file_model_for(namespace)
248
+ prefix = namespace.name.split(/::/).last.downcase
249
+ file_model_name = "#{ namespace.name }::File"
250
+ chunk_model_name = "#{ namespace.name }::Chunk"
251
+
252
+ Class.new do
253
+ include Mongoid::Document
254
+ include Mongoid::Attributes::Dynamic if Mongoid::VERSION.to_i >= 4
255
+
256
+ singleton_class = class << self; self; end
257
+
258
+ singleton_class.instance_eval do
259
+ define_method(:name){ file_model_name }
260
+ attr_accessor :namespace
261
+ attr_accessor :chunk_model
262
+ attr_accessor :defaults
263
+ end
264
+
265
+ self.store_in :collection => "#{ prefix }.files"
266
+
267
+ self.defaults = Defaults.new
268
+
269
+ self.defaults.chunkSize = 4 * (mb = 2**20)
270
+ self.defaults.contentType = 'application/octet-stream'
271
+
272
+ field(:length, :type => Integer, :default => 0)
273
+ field(:chunkSize, :type => Integer, :default => defaults.chunkSize)
274
+ field(:uploadDate, :type => Time, :default => Time.now.utc)
275
+ field(:md5, :type => String, :default => Digest::MD5.hexdigest(''))
276
+
277
+ field(:filename, :type => String)
278
+ field(:contentType, :type => String, :default => defaults.contentType)
279
+ field(:aliases, :type => Array)
280
+ field(:metadata) rescue nil
281
+
282
+ required = %w( length chunkSize uploadDate md5 )
283
+
284
+ required.each do |f|
285
+ validates_presence_of(f)
286
+ end
287
+
288
+ index({:filename => 1})
289
+ index({:aliases => 1})
290
+ index({:uploadDate => 1})
291
+ index({:md5 => 1})
292
+
293
+ has_many(:chunks, :class_name => chunk_model_name, :inverse_of => :files, :dependent => :destroy, :order => [:n, :asc])
294
+
295
+ def path
296
+ filename
297
+ end
298
+
299
+ def basename
300
+ ::File.basename(filename) if filename
301
+ end
302
+
303
+ def attachment_filename(*paths)
304
+ return basename if basename
305
+
306
+ if paths.empty?
307
+ paths.push('attachment')
308
+ paths.push(id.to_s)
309
+ paths.push(updateDate.iso8601)
310
+ end
311
+
312
+ path = paths.join('--')
313
+ base = ::File.basename(path).split('.', 2).first
314
+ ext = GridFs.extract_extension(contentType)
315
+
316
+ "#{ base }.#{ ext }"
317
+ end
318
+
319
+ def prefix
320
+ self.class.namespace.prefix
321
+ end
322
+
323
+ def each(&block)
324
+ fetched, limit = 0, 7
325
+
326
+ while fetched < chunks.size
327
+ chunks.where(:n.lt => fetched+limit, :n.gte => fetched).
328
+ order_by([:n, :asc]).each do |chunk|
329
+ block.call(chunk.to_s)
330
+ end
331
+
332
+ fetched += limit
333
+ end
334
+ end
335
+
336
+ def slice(*args)
337
+ case args.first
338
+ when Range
339
+ range = args.first
340
+ first_chunk = (range.min / chunkSize).floor
341
+ last_chunk = (range.max / chunkSize).floor
342
+ offset = range.min % chunkSize
343
+ length = range.max - range.min + 1
344
+ when Fixnum
345
+ start = args.first
346
+ start = self.length + start if start < 0
347
+ length = args.size == 2 ? args.last : 1
348
+ first_chunk = (start / chunkSize).floor
349
+ last_chunk = ((start + length) / chunkSize).floor
350
+ offset = start % chunkSize
351
+ end
352
+
353
+ data = ''
354
+
355
+ chunks.where(:n => (first_chunk..last_chunk)).order_by(n: 'asc').each do |chunk|
356
+ data << chunk
357
+ end
358
+
359
+ data[offset, length]
360
+ end
361
+
362
+ def data
363
+ data = ''
364
+ each{|chunk| data << chunk}
365
+ data
366
+ end
367
+
368
+ def base64
369
+ Array(to_s).pack('m')
370
+ end
371
+
372
+ def data_uri(options = {})
373
+ data = base64.chomp
374
+ "data:#{ content_type };base64,".concat(data)
375
+ end
376
+
377
+ def bytes(&block)
378
+ if block
379
+ each{|data| block.call(data)}
380
+ length
381
+ else
382
+ bytes = []
383
+ each{|data| bytes.push(*data)}
384
+ bytes
385
+ end
386
+ end
387
+
388
+ def close
389
+ self
390
+ end
391
+
392
+ def content_type
393
+ contentType
394
+ end
395
+
396
+ def update_date
397
+ updateDate
398
+ end
399
+
400
+ def created_at
401
+ updateDate
402
+ end
403
+
404
+ def namespace
405
+ self.class.namespace
406
+ end
407
+ end
408
+ end
409
+
410
+ ##
411
+ #
412
+ def GridFs.build_chunk_model_for(namespace)
413
+ prefix = namespace.name.split(/::/).last.downcase
414
+ file_model_name = "#{ namespace.name }::File"
415
+ chunk_model_name = "#{ namespace.name }::Chunk"
416
+
417
+ Class.new do
418
+ include Mongoid::Document
419
+
420
+ singleton_class = class << self; self; end
421
+
422
+ singleton_class.instance_eval do
423
+ define_method(:name){ chunk_model_name }
424
+ attr_accessor :file_model
425
+ attr_accessor :namespace
426
+ end
427
+
428
+ self.store_in :collection => "#{ prefix }.chunks"
429
+
430
+ field(:n, :type => Integer, :default => 0)
431
+ field(:data, :type => (defined?(Moped::BSON) ? Moped::BSON::Binary : BSON::Binary))
432
+
433
+ belongs_to(:file, :foreign_key => :files_id, :class_name => file_model_name)
434
+
435
+ index({:files_id => 1, :n => -1}, :unique => true)
436
+
437
+ def namespace
438
+ self.class.namespace
439
+ end
440
+
441
+ def to_s
442
+ data.data
443
+ end
444
+
445
+ alias_method 'to_str', 'to_s'
446
+ end
447
+ end
448
+
449
+ ##
450
+ #
451
+ def GridFs.reading(arg, &block)
452
+ if arg.respond_to?(:read)
453
+ rewind(arg) do |io|
454
+ block.call(io)
455
+ end
456
+ else
457
+ open(arg.to_s) do |io|
458
+ block.call(io)
459
+ end
460
+ end
461
+ end
462
+
463
+ def GridFs.chunking(io, chunk_size, &block)
464
+ if io.method(:read).arity == 0
465
+ data = io.read
466
+ i = 0
467
+ loop do
468
+ offset = i * chunk_size
469
+ length = i + chunk_size < data.size ? chunk_size : data.size - offset
470
+
471
+ break if offset >= data.size
472
+
473
+ buf = data[offset, length]
474
+ block.call(buf)
475
+ i += 1
476
+ end
477
+ else
478
+ while((buf = io.read(chunk_size)))
479
+ block.call(buf)
480
+ end
481
+ end
482
+ end
483
+
484
+ def GridFs.rewind(io, &block)
485
+ begin
486
+ pos = io.pos
487
+ io.flush
488
+ io.rewind
489
+ rescue
490
+ nil
491
+ end
492
+
493
+ begin
494
+ block.call(io)
495
+ ensure
496
+ begin
497
+ io.pos = pos
498
+ rescue
499
+ nil
500
+ end
501
+ end
502
+ end
503
+
504
+ def GridFs.extract_basename(object)
505
+ filename = nil
506
+
507
+ [:original_path, :original_filename, :path, :filename, :pathname].each do |msg|
508
+ if object.respond_to?(msg)
509
+ filename = object.send(msg)
510
+ break
511
+ end
512
+ end
513
+
514
+ filename ? cleanname(filename) : nil
515
+ end
516
+
517
+ MIME_TYPES = {
518
+ 'md' => 'text/x-markdown; charset=UTF-8'
519
+ }
520
+
521
+ def GridFs.mime_types
522
+ MIME_TYPES
523
+ end
524
+
525
+ def GridFs.extract_content_type(filename, options = {})
526
+ options.to_options!
527
+
528
+ basename = ::File.basename(filename.to_s)
529
+ parts = basename.split('.')
530
+ parts.shift
531
+ ext = parts.pop
532
+
533
+ default =
534
+ case
535
+ when options[:default]==false
536
+ nil
537
+ when options[:default]==true
538
+ "application/octet-stream"
539
+ else
540
+ (options[:default] || "application/octet-stream").to_s
541
+ end
542
+
543
+ content_type = mime_types[ext] || MIME::Types.type_for(::File.basename(filename.to_s)).first
544
+
545
+ if content_type
546
+ content_type.to_s
547
+ else
548
+ default
549
+ end
550
+ end
551
+
552
+ def GridFs.extract_extension(content_type)
553
+ list = MIME::Types[content_type.to_s]
554
+ type = list.first
555
+ if type
556
+ type.extensions.first
557
+ end
558
+ end
559
+
560
+ def GridFs.cleanname(pathname)
561
+ basename = ::File.basename(pathname.to_s)
562
+ CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_')
563
+ end
564
+ end
565
+
566
+ GridFS = GridFs
567
+ GridFs.init!
568
+ end
569
+
570
+ ##
571
+ #
572
+ if defined?(Rails)
573
+ class Mongoid::GridFs::Engine < Rails::Engine
574
+ paths['app/models'] = File.dirname(File.expand_path("../", __FILE__))
575
+ end
576
+
577
+ module Mongoid::GridFsHelper
578
+ def grid_fs_render(grid_fs_file, options = {})
579
+ options.to_options!
580
+
581
+ if options[:inline] == false or options[:attachment] == true
582
+ headers['Content-Disposition'] = "attachment; filename=#{ grid_fs_file.attachment_filename }"
583
+ end
584
+
585
+ self.content_type = grid_fs_file.content_type
586
+ self.response_body = grid_fs_file
587
+ end
588
+ end
589
+
590
+ Mongoid::GridFs::Helper = Mongoid::GridFsHelper
591
+ end