From b024d8d7075f0c6e89e399be85f188f239990aca Mon Sep 17 00:00:00 2001 From: ahoward Date: Mon, 23 Jul 2012 18:23:48 -0600 Subject: [PATCH] factor out deps. formatting code. --- lib/mongoid-grid_fs.rb | 827 ++++++++++++++++++---------------- mongoid-grid_fs.gemspec | 6 +- pkg/mongoid-grid_fs-1.0.0.gem | Bin 12288 -> 0 bytes test/mongoid-grid_fs_test.rb | 4 +- 4 files changed, 445 insertions(+), 392 deletions(-) delete mode 100644 pkg/mongoid-grid_fs-1.0.0.gem diff --git a/lib/mongoid-grid_fs.rb b/lib/mongoid-grid_fs.rb index 8c0939e..a2683d9 100644 --- a/lib/mongoid-grid_fs.rb +++ b/lib/mongoid-grid_fs.rb @@ -1,419 +1,466 @@ -require "mongoid" -require "mime/types" -require "digest/md5" -require "cgi" - -class GridFS ## # - class << GridFS - def version - "1.0.0" - end + class GridFS + const_set :Version, '1.1.0' - attr_accessor :namespace - attr_accessor :file_model - attr_accessor :chunk_model - - def init! - GridFS.build_namespace_for(:Fs) - - GridFS.namespace = Fs - GridFS.file_model = Fs.file_model - GridFS.chunk_model = Fs.chunk_model - - const_set(:File, Fs.file_model) - const_set(:Chunk, Fs.chunk_model) - - to_delegate = %w( - put - get - delete - find - [] - []= - ) - - to_delegate.each do |method| - class_eval <<-__ - def GridFS.#{ method }(*args, &block) - ::GridFS::Fs::#{ method }(*args, &block) - end - __ - end - end - end - -## -# - def GridFS.namespace_for(prefix) - prefix = prefix.to_s.downcase - const = "::GridFS::#{ prefix.to_s.camelize }" - namespace = const.split(/::/).last - const_defined?(namespace) ? const_get(namespace) : build_namespace_for(namespace) - end - -## -# - def GridFS.build_namespace_for(prefix) - prefix = prefix.to_s.downcase - const = prefix.camelize - - namespace = - Module.new do - module_eval(&NamespaceMixin) - self + class << GridFS + def version + const_get :Version end - const_set(const, namespace) - - file_model = build_file_model_for(namespace) - chunk_model = build_chunk_model_for(namespace) - - file_model.namespace = namespace - chunk_model.namespace = namespace - - file_model.chunk_model = chunk_model - chunk_model.file_model = file_model - - namespace.prefix = prefix - namespace.file_model = file_model - namespace.chunk_model = chunk_model - - namespace.send(:const_set, :File, file_model) - namespace.send(:const_set, :Chunk, chunk_model) - - #at_exit{ file_model.create_indexes rescue nil } - #at_exit{ chunk_model.create_indexes rescue nil } - - const_get(const) - end - - NamespaceMixin = proc do - class << self - attr_accessor :prefix - attr_accessor :file_model - attr_accessor :chunk_model - - def to_s - prefix + def dependencies + { + 'mongoid' => [ 'mongoid' , ' >= 3.0.1' ] , + 'mime/types' => [ 'mime-types' , ' >= 1.19' ] , + } end - def namespace - prefix - end - - def put(readable, attributes = {}) - chunks = [] - file = file_model.new - attributes.to_options! - - if attributes.has_key?(:id) - file.id = attributes.delete(:id) - end - - if attributes.has_key?(:_id) - file.id = attributes.delete(:_id) - end - - if attributes.has_key?(:content_type) - attributes[:contentType] = attributes.delete(:content_type) - end - - if attributes.has_key?(:upload_date) - attributes[:uploadDate] = attributes.delete(:upload_date) - end - - md5 = Digest::MD5.new - length = 0 - chunkSize = file.chunkSize - n = 0 - - GridFS.reading(readable) do |io| - - filename = - attributes[:filename] ||= - [file.id.to_s, GridFS.extract_basename(io)].join('/').squeeze('/') - - content_type = - attributes[:contentType] ||= - GridFS.extract_content_type(filename) || file.contentType - - while((buf = io.read(chunkSize))) - md5 << buf - length += buf.size - chunk = file.chunks.build - chunk.data = binary_for(buf) - chunk.n = n - n += 1 - chunk.save! - chunks.push(chunk) - end - - end - - attributes[:length] ||= length - attributes[:uploadDate] ||= Time.now.utc - attributes[:md5] ||= md5.hexdigest - - file.update_attributes(attributes) - - file.save! - file + def libdir(*args, &block) + @libdir ||= File.expand_path(__FILE__).sub(/\.rb$/,'') + args.empty? ? @libdir : File.join(@libdir, *args) ensure - chunks.each{|chunk| chunk.destroy rescue nil} if $! - end - - if defined?(Moped) - def binary_for(*buf) - Moped::BSON::Binary.new(:generic, buf.join) - end - else - def binary_for(buf) - BSON::Binary.new(buf.bytes.to_a) - end - end - - def get(id) - file_model.find(id) - end - - def delete(id) - file_model.find(id).destroy - rescue - nil - end - - def where(conditions = {}) - case conditions - when String - file_model.where(:filename => conditions) - else - file_model.where(conditions) - end - end - - def find(*args) - where(*args).first - end - - def [](filename) - file_model.where(:filename => filename.to_s).first - end - - def []=(filename, readable) - file = self[filename] - file.destroy if file - put(readable, :filename => filename.to_s) - end - - # TODO - opening with a mode = 'w' should return a GridIO::IOProxy - # implementing a StringIO-like interface - # - def open(filename, mode = 'r', &block) - raise NotImplementedError - end - end - end - -## -# - def GridFS.build_file_model_for(namespace) - prefix = namespace.name.split(/::/).last.downcase - file_model_name = "#{ namespace.name }::File" - chunk_model_name = "#{ namespace.name }::Chunk" - - Class.new do - include Mongoid::Document - - singleton_class = class << self; self; end - - singleton_class.instance_eval do - define_method(:name){ file_model_name } - attr_accessor :chunk_model - attr_accessor :namespace - end - - self.default_collection_name = "#{ prefix }.files" - - field(:filename, :type => String) - field(:contentType, :type => String, :default => 'application/octet-stream') - - field(:length, :type => Integer, :default => 0) - field(:chunkSize, :type => Integer, :default => (256 * (2 ** 20))) - field(:uploadDate, :type => Date, :default => Time.now.utc) - field(:md5, :type => String, :default => Digest::MD5.hexdigest('')) - - %w( filename contentType length chunkSize uploadDate md5 ).each do |f| - validates_presence_of(f) - end - validates_uniqueness_of(:filename) - - has_many(:chunks, :class_name => chunk_model_name, :inverse_of => :files, :dependent => :destroy, :order => [:n, :asc]) - - index({:filename => 1}, :unique => true) - - def path - filename - end - - def basename - ::File.basename(filename) - end - - def prefix - self.class.namespace.prefix - end - - def each(&block) - chunks.all.order_by([:n, :asc]).each do |chunk| - block.call(chunk.to_s) - end - end - - def data - data = '' - each{|chunk| data << chunk} - data - end - - def base64 - Array(to_s).pack('m') - end - - def data_uri(options = {}) - data = base64.chomp - "data:#{ content_type };base64,".concat(data) - end - - def bytes(&block) if block - each{|data| block.call(data)} - length - else - bytes = [] - each{|data| bytes.push(*data)} - bytes + begin + $LOAD_PATH.unshift(@libdir) + block.call() + ensure + $LOAD_PATH.shift() + end end end - def close - self - end - - def content_type - contentType - end - - def update_date - updateDate - end - - def created_at - updateDate - end - - def namespace - self.class.namespace + def load(*libs) + libs = libs.join(' ').scan(/[^\s+]+/) + libdir{ libs.each{|lib| Kernel.load(lib) } } end end - end -## -# - def GridFS.build_chunk_model_for(namespace) - prefix = namespace.name.split(/::/).last.downcase - file_model_name = "#{ namespace.name }::File" - chunk_model_name = "#{ namespace.name }::Chunk" - - Class.new do - include Mongoid::Document - - singleton_class = class << self; self; end - - singleton_class.instance_eval do - define_method(:name){ chunk_model_name } - attr_accessor :file_model - attr_accessor :namespace - end - - self.default_collection_name = "#{ prefix }.chunks" - - field(:n, :type => Integer, :default => 0) - field(:data, :type => (defined?(Moped) ? Moped::BSON::Binary : BSON::Binary)) - - belongs_to(:file, :foreign_key => :files_id, :class_name => file_model_name) - - index({:files_id => 1, :n => -1}, :unique => true) - - def namespace - self.class.namespace - end - - def to_s - data.data - end - - alias_method 'to_str', 'to_s' - end - end - -## -# - def GridFS.reading(arg, &block) - if arg.respond_to?(:read) - rewind(arg) do |io| - block.call(io) - end - else - open(arg.to_s) do |io| - block.call(io) - end - end - end - - def GridFS.rewind(io, &block) begin - pos = io.pos - io.flush - io.rewind - rescue + require 'rubygems' + rescue LoadError nil end - begin - block.call(io) - ensure + if defined?(gem) + dependencies.each do |lib, dependency| + gem(*dependency) + require(lib) + end + end + + require "digest/md5" + require "cgi" + end + +## +# + class GridFS + class << GridFS + attr_accessor :namespace + attr_accessor :file_model + attr_accessor :chunk_model + + def init! + GridFS.build_namespace_for(:Fs) + + GridFS.namespace = Fs + GridFS.file_model = Fs.file_model + GridFS.chunk_model = Fs.chunk_model + + const_set(:File, Fs.file_model) + const_set(:Chunk, Fs.chunk_model) + + to_delegate = %w( + put + get + delete + find + [] + []= + ) + + to_delegate.each do |method| + class_eval <<-__ + def GridFS.#{ method }(*args, &block) + ::GridFS::Fs::#{ method }(*args, &block) + end + __ + end + end + end + + ## + # + def GridFS.namespace_for(prefix) + prefix = prefix.to_s.downcase + const = "::GridFS::#{ prefix.to_s.camelize }" + namespace = const.split(/::/).last + const_defined?(namespace) ? const_get(namespace) : build_namespace_for(namespace) + end + + ## + # + def GridFS.build_namespace_for(prefix) + prefix = prefix.to_s.downcase + const = prefix.camelize + + namespace = + Module.new do + module_eval(&NamespaceMixin) + self + end + + const_set(const, namespace) + + file_model = build_file_model_for(namespace) + chunk_model = build_chunk_model_for(namespace) + + file_model.namespace = namespace + chunk_model.namespace = namespace + + file_model.chunk_model = chunk_model + chunk_model.file_model = file_model + + namespace.prefix = prefix + namespace.file_model = file_model + namespace.chunk_model = chunk_model + + namespace.send(:const_set, :File, file_model) + namespace.send(:const_set, :Chunk, chunk_model) + + #at_exit{ file_model.create_indexes rescue nil } + #at_exit{ chunk_model.create_indexes rescue nil } + + const_get(const) + end + + NamespaceMixin = proc do + class << self + attr_accessor :prefix + attr_accessor :file_model + attr_accessor :chunk_model + + def to_s + prefix + end + + def namespace + prefix + end + + def put(readable, attributes = {}) + chunks = [] + file = file_model.new + attributes.to_options! + + if attributes.has_key?(:id) + file.id = attributes.delete(:id) + end + + if attributes.has_key?(:_id) + file.id = attributes.delete(:_id) + end + + if attributes.has_key?(:content_type) + attributes[:contentType] = attributes.delete(:content_type) + end + + if attributes.has_key?(:upload_date) + attributes[:uploadDate] = attributes.delete(:upload_date) + end + + md5 = Digest::MD5.new + length = 0 + chunkSize = file.chunkSize + n = 0 + + GridFS.reading(readable) do |io| + + filename = + attributes[:filename] ||= + [file.id.to_s, GridFS.extract_basename(io)].join('/').squeeze('/') + + content_type = + attributes[:contentType] ||= + GridFS.extract_content_type(filename) || file.contentType + + while((buf = io.read(chunkSize))) + md5 << buf + length += buf.size + chunk = file.chunks.build + chunk.data = binary_for(buf) + chunk.n = n + n += 1 + chunk.save! + chunks.push(chunk) + end + + end + + attributes[:length] ||= length + attributes[:uploadDate] ||= Time.now.utc + attributes[:md5] ||= md5.hexdigest + + file.update_attributes(attributes) + + file.save! + file + ensure + chunks.each{|chunk| chunk.destroy rescue nil} if $! + end + + if defined?(Moped) + def binary_for(*buf) + Moped::BSON::Binary.new(:generic, buf.join) + end + else + def binary_for(buf) + BSON::Binary.new(buf.bytes.to_a) + end + end + + def get(id) + file_model.find(id) + end + + def delete(id) + file_model.find(id).destroy + rescue + nil + end + + def where(conditions = {}) + case conditions + when String + file_model.where(:filename => conditions) + else + file_model.where(conditions) + end + end + + def find(*args) + where(*args).first + end + + def [](filename) + file_model.where(:filename => filename.to_s).first + end + + def []=(filename, readable) + file = self[filename] + file.destroy if file + put(readable, :filename => filename.to_s) + end + + # TODO - opening with a mode = 'w' should return a GridIO::IOProxy + # implementing a StringIO-like interface + # + def open(filename, mode = 'r', &block) + raise NotImplementedError + end + end + end + + ## + # + def GridFS.build_file_model_for(namespace) + prefix = namespace.name.split(/::/).last.downcase + file_model_name = "#{ namespace.name }::File" + chunk_model_name = "#{ namespace.name }::Chunk" + + Class.new do + include Mongoid::Document + + singleton_class = class << self; self; end + + singleton_class.instance_eval do + define_method(:name){ file_model_name } + attr_accessor :chunk_model + attr_accessor :namespace + end + + self.default_collection_name = "#{ prefix }.files" + + field(:filename, :type => String) + field(:contentType, :type => String, :default => 'application/octet-stream') + + field(:length, :type => Integer, :default => 0) + field(:chunkSize, :type => Integer, :default => (256 * (2 ** 20))) + field(:uploadDate, :type => Date, :default => Time.now.utc) + field(:md5, :type => String, :default => Digest::MD5.hexdigest('')) + + %w( filename contentType length chunkSize uploadDate md5 ).each do |f| + validates_presence_of(f) + end + validates_uniqueness_of(:filename) + + has_many(:chunks, :class_name => chunk_model_name, :inverse_of => :files, :dependent => :destroy, :order => [:n, :asc]) + + index({:filename => 1}, :unique => true) + + def path + filename + end + + def basename + ::File.basename(filename) + end + + def prefix + self.class.namespace.prefix + end + + def each(&block) + chunks.all.order_by([:n, :asc]).each do |chunk| + block.call(chunk.to_s) + end + end + + def data + data = '' + each{|chunk| data << chunk} + data + end + + def base64 + Array(to_s).pack('m') + end + + def data_uri(options = {}) + data = base64.chomp + "data:#{ content_type };base64,".concat(data) + end + + def bytes(&block) + if block + each{|data| block.call(data)} + length + else + bytes = [] + each{|data| bytes.push(*data)} + bytes + end + end + + def close + self + end + + def content_type + contentType + end + + def update_date + updateDate + end + + def created_at + updateDate + end + + def namespace + self.class.namespace + end + end + end + + ## + # + def GridFS.build_chunk_model_for(namespace) + prefix = namespace.name.split(/::/).last.downcase + file_model_name = "#{ namespace.name }::File" + chunk_model_name = "#{ namespace.name }::Chunk" + + Class.new do + include Mongoid::Document + + singleton_class = class << self; self; end + + singleton_class.instance_eval do + define_method(:name){ chunk_model_name } + attr_accessor :file_model + attr_accessor :namespace + end + + self.default_collection_name = "#{ prefix }.chunks" + + field(:n, :type => Integer, :default => 0) + field(:data, :type => (defined?(Moped) ? Moped::BSON::Binary : BSON::Binary)) + + belongs_to(:file, :foreign_key => :files_id, :class_name => file_model_name) + + index({:files_id => 1, :n => -1}, :unique => true) + + def namespace + self.class.namespace + end + + def to_s + data.data + end + + alias_method 'to_str', 'to_s' + end + end + + ## + # + def GridFS.reading(arg, &block) + if arg.respond_to?(:read) + rewind(arg) do |io| + block.call(io) + end + else + open(arg.to_s) do |io| + block.call(io) + end + end + end + + def GridFS.rewind(io, &block) begin - io.pos = pos + pos = io.pos + io.flush + io.rewind rescue nil end - end - end - def GridFS.extract_basename(object) - filename = nil - [:original_path, :original_filename, :path, :filename, :pathname].each do |msg| - if object.respond_to?(msg) - filename = object.send(msg) - break + begin + block.call(io) + ensure + begin + io.pos = pos + rescue + nil + end end end - filename ? cleanname(filename) : nil + + def GridFS.extract_basename(object) + filename = nil + [:original_path, :original_filename, :path, :filename, :pathname].each do |msg| + if object.respond_to?(msg) + filename = object.send(msg) + break + end + end + filename ? cleanname(filename) : nil + end + + def GridFS.extract_content_type(filename) + content_type = MIME::Types.type_for(::File.basename(filename.to_s)).first + content_type.to_s if content_type + end + + def GridFS.cleanname(pathname) + basename = ::File.basename(pathname.to_s) + CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_') + end end - def GridFS.extract_content_type(filename) - content_type = MIME::Types.type_for(::File.basename(filename.to_s)).first - content_type.to_s if content_type - end +## +# + GridFs = GridFS - def GridFS.cleanname(pathname) - basename = ::File.basename(pathname.to_s) - CGI.unescape(basename).gsub(%r/[^0-9a-zA-Z_@)(~.-]/, '_').gsub(%r/_+/,'_') - end -end - -GridFs = GridFS - -GridFS.init! + GridFS.init! diff --git a/mongoid-grid_fs.gemspec b/mongoid-grid_fs.gemspec index 235c966..a71a5ae 100644 --- a/mongoid-grid_fs.gemspec +++ b/mongoid-grid_fs.gemspec @@ -3,7 +3,7 @@ Gem::Specification::new do |spec| spec.name = "mongoid-grid_fs" - spec.version = "1.0.0" + spec.version = "1.1.0" spec.platform = Gem::Platform::RUBY spec.summary = "mongoid-grid_fs" spec.description = "a mongoid 3/moped compatible implementation of the grid_fs specification" @@ -26,6 +26,10 @@ Gem::Specification::new do |spec| spec.test_files = nil + spec.add_dependency(*["mongoid", " >= 3.0.1"]) + + spec.add_dependency(*["mime-types", " >= 1.19"]) + spec.extensions.push(*[]) diff --git a/pkg/mongoid-grid_fs-1.0.0.gem b/pkg/mongoid-grid_fs-1.0.0.gem deleted file mode 100644 index 1e59dcbdf284d712f84fc45131ed4dd6f274d483..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 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