gnuside-mongoid-grid_fs 1.9.1

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