mongoid-grid_fs 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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