commit 1325a3632c379e928d4667a17633b0ecc51e1c74 Author: ahoward Date: Mon Jul 23 00:29:15 2012 -0600 first commit diff --git a/.README.md.swp b/.README.md.swp new file mode 100644 index 0000000..8b60850 Binary files /dev/null and b/.README.md.swp differ diff --git a/.Rakefile.swp b/.Rakefile.swp new file mode 100644 index 0000000..e81b224 Binary files /dev/null and b/.Rakefile.swp differ diff --git a/.a.rb.swp b/.a.rb.swp new file mode 100644 index 0000000..99d516c Binary files /dev/null and b/.a.rb.swp differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7841edf --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +NAME +---- + mongoid_grid_fs + +SYNOPSIS +-------- + + ````ruby + + require 'mongoid-grid_fs' + + g = GridFs.put anthing_that_respons_to_read + + GridFS.get id + + GridFS.delete id + + + ```` + +DESCRIPTION +----------- + mongoid_grid_fs is pure mongoid 3 / moped implementation of the mongodb + grid_fs specification + + ref: http://www.mongodb.org/display/DOCS/GridFS+Specification + + it has the following features: + + - implementation is on top of mongoid for portability. moped (the drive) + is barely used + + - simple, REST-like api + + - support for custom namespaces (fs.files vs. image.files) + + - pathnames and io-like objects can be written to the grid + + - auto-unique pathnames are generated (by default) to avoid collisions using #put + + 'path/info/a.rb' -> '$object_id/a.rb' + + - #[] and #[]= methods which allow the grid to be used like a giant file + hash in the sky + + - supprt for data_uris + + ````eruby + + <%= image_tag :src => file.data_url %> + + ```` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..eefe0b0 --- /dev/null +++ b/Rakefile @@ -0,0 +1,446 @@ +This.name = + "GridFs" + +This.synopsis = + "a mongoid 3/moped compatible implementation of the grid_fs specification" + +This.rubyforge_project = 'codeforpeople' +This.author = "Ara T. Howard" +This.email = "ara.t.howard@gmail.com" +This.homepage = "https://github.com/ahoward/#{ This.lib }" + +This.setup! + + + +task :default do + puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort) +end + +task :test do + This.run_tests! +end + +namespace :test do + task(:unit){ This.run_tests!(:unit) } + task(:functional){ This.run_tests!(:functional) } + task(:integration){ This.run_tests!(:integration) } +end + +def This.run_tests!(which = nil) + which ||= '**' + test_dir = File.join(This.dir, "test") + test_glob ||= File.join(test_dir, "#{ which }/**_test.rb") + test_rbs = Dir.glob(test_glob).sort + + div = ('=' * 119) + line = ('-' * 119) + + test_rbs.each_with_index do |test_rb, index| + testno = index + 1 + command = "#{ File.basename(This.ruby) } -I ./lib -I ./test/lib #{ test_rb }" + + puts + This.say(div, :color => :cyan, :bold => true) + This.say("@#{ testno } => ", :bold => true, :method => :print) + This.say(command, :color => :cyan, :bold => true) + This.say(line, :color => :cyan, :bold => true) + + system(command) + + This.say(line, :color => :cyan, :bold => true) + + status = $?.exitstatus + + if status.zero? + This.say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print) + This.say("SUCCESS", :color => :green, :bold => true) + else + This.say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print) + This.say("FAILURE", :color => :red, :bold => true) + end + This.say(line, :color => :cyan, :bold => true) + + exit(status) unless status.zero? + end +end + + +task :gemspec do + ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem'] + ignore_directories = ['pkg', 'db'] + ignore_files = ['test/log', 'test/db.yml', 'a.rb', 'b.rb'] + Dir['db/*'] + %w'db' + + shiteless = + lambda do |list| + list.delete_if do |entry| + next unless test(?e, entry) + extension = File.basename(entry).split(%r/[.]/).last + ignore_extensions.any?{|ext| ext === extension} + end + list.delete_if do |entry| + next unless test(?d, entry) + dirname = File.expand_path(entry) + ignore_directories.any?{|dir| File.expand_path(dir) == dirname} + end + list.delete_if do |entry| + next unless test(?f, entry) + filename = File.expand_path(entry) + ignore_files.any?{|file| File.expand_path(file) == filename} + end + end + + lib = This.lib + object = This.object + version = This.version + files = shiteless[Dir::glob("**/**")] + executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)} + #has_rdoc = true #File.exist?('doc') + test_files = test(?e, "test/#{ lib }.rb") ? "test/#{ lib }.rb" : nil + summary = This.summary || This.synopsis || "#{ lib } kicks the ass" + description = This.description || summary + + if This.extensions.nil? + This.extensions = [] + extensions = This.extensions + %w( Makefile configure extconf.rb ).each do |ext| + extensions << ext if File.exists?(ext) + end + end + extensions = [extensions].flatten.compact + +# TODO + if This.dependencies.nil? + dependencies = [] + else + case This.dependencies + when Hash + dependencies = This.dependencies.values + when Array + dependencies = This.dependencies + end + end + + template = + if test(?e, 'gemspec.erb') + This.template_for{ IO.read('gemspec.erb') } + else + This.template_for { + <<-__ + ## <%= lib %>.gemspec + # + + Gem::Specification::new do |spec| + spec.name = <%= lib.inspect %> + spec.version = <%= version.inspect %> + spec.platform = Gem::Platform::RUBY + spec.summary = <%= lib.inspect %> + spec.description = <%= description.inspect %> + + spec.files =\n<%= files.sort.pretty_inspect %> + spec.executables = <%= executables.inspect %> + + spec.require_path = "lib" + + spec.test_files = <%= test_files.inspect %> + + <% dependencies.each do |lib_version| %> + spec.add_dependency(*<%= Array(lib_version).flatten.inspect %>) + <% end %> + + spec.extensions.push(*<%= extensions.inspect %>) + + spec.rubyforge_project = <%= This.rubyforge_project.inspect %> + spec.author = <%= This.author.inspect %> + spec.email = <%= This.email.inspect %> + spec.homepage = <%= This.homepage.inspect %> + end + __ + } + end + + FileUtils.mkdir_p(This.pkgdir) + gemspec = "#{ lib }.gemspec" + open(gemspec, "w"){|fd| fd.puts(template)} + This.gemspec = gemspec +end + +task :gem => [:clean, :gemspec] do + FileUtils.mkdir_p(This.pkgdir) + before = Dir['*.gem'] + cmd = "gem build #{ This.gemspec }" + `#{ cmd }` + after = Dir['*.gem'] + gem = ((after - before).first || after.first) or abort('no gem!') + FileUtils.mv(gem, This.pkgdir) + This.gem = File.join(This.pkgdir, File.basename(gem)) +end + +task :readme do + samples = '' + prompt = '~ > ' + lib = This.lib + version = This.version + + Dir['sample*/*'].sort.each do |sample| + samples << "\n" << " <========< #{ sample } >========>" << "\n\n" + + cmd = "cat #{ sample }" + samples << This.util.indent(prompt + cmd, 2) << "\n\n" + samples << This.util.indent(`#{ cmd }`, 4) << "\n" + + cmd = "ruby #{ sample }" + samples << This.util.indent(prompt + cmd, 2) << "\n\n" + + cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'" + samples << This.util.indent(`#{ cmd } 2>&1`, 4) << "\n" + end + + template = + if test(?e, 'readme.erb') + This.template_for{ IO.read('readme.erb') } + else + This.template_for { + <<-__ + NAME + #{ lib } + + DESCRIPTION + + INSTALL + gem install #{ lib } + + SAMPLES + #{ samples } + __ + } + end + + open("README", "w"){|fd| fd.puts template} +end + + +task :clean do + Dir[File.join(This.pkgdir, '**/**')].each{|entry| FileUtils.rm_rf(entry)} +end + + +task :release => [:clean, :gemspec, :gem] do + gems = Dir[File.join(This.pkgdir, '*.gem')].flatten + raise "which one? : #{ gems.inspect }" if gems.size > 1 + raise "no gems?" if gems.size < 1 + + cmd = "gem push #{ This.gem }" + puts cmd + puts + system(cmd) + abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero? + + #cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }" + #puts cmd + #puts + #system(cmd) + #abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero? +end + + + + + +BEGIN { +# support for this rakefile +# + $VERBOSE = nil + + require 'erb' + require 'fileutils' + require 'rbconfig' + require 'pp' + +# cache a bunch of stuff about this rakefile/environment +# + + This = + Class.new(Hash) do + + def method_missing(method, *args, &block) + if method.to_s =~ /=/ + key = method.to_s.chomp('=') + value = block ? block : args.shift + self[key] = value + else + key = method.to_s + if block + value = block + self[key] = value + else + value = self[key] + + if value.respond_to?(:call) + self[key] = value.call() + else + value + end + end + end + end + + def inspect + expand! + PP.pp(self, '') + end + + def expand! + keys.each do |key| + value = self[key] + if value.respond_to?(:call) + self[key] = value.call() + end + end + end + + end.new() + + This.file = File.expand_path(__FILE__) + This.dir = File.dirname(This.file) + This.pkgdir = File.join(This.dir, 'pkg') + +# defaults +# + This.lib do + File.basename(Dir.pwd) + end + + def This.setup! + begin + require "./lib/#{ This.lib }" + rescue LoadError + abort("could not load #{ This.lib }") + end + end + + This.name do + This.name = This.lib.capitalize + end + + This.object do + begin + This.object = eval(This.name) + rescue Object + abort("could not determine object from #{ This.name }") + end + end + + This.version do + This.object.send(:version) + end + + This.dependencies do + if This.object.respond_to?(:dependencies) + This.object.dependencies + end + end + + This.ruby do + c = Config::CONFIG + bindir = c["bindir"] || c['BINDIR'] + ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby' + ruby_ext = c['EXEEXT'] || '' + File.join(bindir, (ruby_install_name + ruby_ext)) + end + +# some utils +# + This.util = Module.new do + def indent(s, n = 2) + s = unindent(s) + ws = ' ' * n + s.gsub(%r/^/, ws) + end + + def unindent(s) + indent = nil + s.each_line do |line| + next if line =~ %r/^\s*$/ + indent = line[%r/^\s*/] and break + end + indent ? s.gsub(%r/^#{ indent }/, "") : s + end + + extend self + end + +# template support +# + This.template = Class.new do + def initialize(&block) + @block = block + @template = block.call.to_s + end + + def expand(b=nil) + ERB.new(This.util.unindent(@template)).result((b||@block).binding) + end + + alias_method 'to_s', 'expand' + end + + def This.template_for(*args, &block) + This.template.new(*args, &block) + end + +# colored console output support +# + This.ansi = { + :clear => "\e[0m", + :reset => "\e[0m", + :erase_line => "\e[K", + :erase_char => "\e[P", + :bold => "\e[1m", + :dark => "\e[2m", + :underline => "\e[4m", + :underscore => "\e[4m", + :blink => "\e[5m", + :reverse => "\e[7m", + :concealed => "\e[8m", + :black => "\e[30m", + :red => "\e[31m", + :green => "\e[32m", + :yellow => "\e[33m", + :blue => "\e[34m", + :magenta => "\e[35m", + :cyan => "\e[36m", + :white => "\e[37m", + :on_black => "\e[40m", + :on_red => "\e[41m", + :on_green => "\e[42m", + :on_yellow => "\e[43m", + :on_blue => "\e[44m", + :on_magenta => "\e[45m", + :on_cyan => "\e[46m", + :on_white => "\e[47m" + } + + def This.say(something, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + options[:color] = args.shift.to_s.to_sym unless args.empty? + keys = options.keys + keys.each{|key| options[key.to_s.to_sym] = options.delete(key)} + + color = options[:color] + bold = options.has_key?(:bold) + + parts = [something] + parts.unshift(This.ansi[color]) if color + parts.unshift(This.ansi[:bold]) if bold + parts.push(This.ansi[:clear]) if parts.size > 1 + + method = options[:method] || :puts + + Kernel.send(method, parts.join) + end + +# always run out of the project dir +# + Dir.chdir(This.dir) +} diff --git a/lib/.mongoid-grid_fs.rb.swp b/lib/.mongoid-grid_fs.rb.swp new file mode 100644 index 0000000..24c6bf7 Binary files /dev/null and b/lib/.mongoid-grid_fs.rb.swp differ diff --git a/lib/mongoid-grid_fs.rb b/lib/mongoid-grid_fs.rb new file mode 100644 index 0000000..ddb50a5 --- /dev/null +++ b/lib/mongoid-grid_fs.rb @@ -0,0 +1,419 @@ +require "mongoid" +require "mime/types" +require "digest/md5" +require "cgi" + +class GridFS +## +# + class << GridFS + def version + "1.0.0" + end + + 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 => Moped::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 + nil + end + + begin + block.call(io) + ensure + begin + io.pos = pos + 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 + 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 + +GridFs = GridFS + +GridFS.init! diff --git a/mongoid-grid_fs.gemspec b/mongoid-grid_fs.gemspec new file mode 100644 index 0000000..235c966 --- /dev/null +++ b/mongoid-grid_fs.gemspec @@ -0,0 +1,36 @@ +## mongoid-grid_fs.gemspec +# + +Gem::Specification::new do |spec| + spec.name = "mongoid-grid_fs" + spec.version = "1.0.0" + spec.platform = Gem::Platform::RUBY + spec.summary = "mongoid-grid_fs" + spec.description = "a mongoid 3/moped compatible implementation of the grid_fs specification" + + spec.files = +["README.md", + "Rakefile", + "lib", + "lib/mongoid-grid_fs.rb", + "mongoid-grid_fs.gemspec", + "test", + "test/helper.rb", + "test/mongoid-grid_fs_test.rb", + "test/testing.rb"] + + spec.executables = [] + + spec.require_path = "lib" + + spec.test_files = nil + + + + spec.extensions.push(*[]) + + spec.rubyforge_project = "codeforpeople" + spec.author = "Ara T. Howard" + spec.email = "ara.t.howard@gmail.com" + spec.homepage = "https://github.com/ahoward/mongoid-grid_fs" +end diff --git a/pkg/mongoid-grid_fs-1.0.0.gem b/pkg/mongoid-grid_fs-1.0.0.gem new file mode 100644 index 0000000..1e59dcb Binary files /dev/null and b/pkg/mongoid-grid_fs-1.0.0.gem differ diff --git a/test/.helper.rb.swp b/test/.helper.rb.swp new file mode 100644 index 0000000..181a7a3 Binary files /dev/null and b/test/.helper.rb.swp differ diff --git a/test/.mongoid-grid_fs_test.rb.swp b/test/.mongoid-grid_fs_test.rb.swp new file mode 100644 index 0000000..d1922d9 Binary files /dev/null and b/test/.mongoid-grid_fs_test.rb.swp differ diff --git a/test/.testing.rb.swp b/test/.testing.rb.swp new file mode 100644 index 0000000..871665b Binary files /dev/null and b/test/.testing.rb.swp differ diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..03da677 --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,18 @@ +# -*- encoding : utf-8 -*- +require_relative 'testing' +require_relative '../lib/mongoid-grid_fs.rb' + +Mongoid.configure do |config| + config.connect_to('mongoid-grid_fs_test') +end + +require 'stringio' + +class SIO < StringIO + attr_accessor :filename + + def initialize(filename, *args, &block) + @filename = filename + super(*args, &block) + end +end diff --git a/test/mongoid-grid_fs_test.rb b/test/mongoid-grid_fs_test.rb new file mode 100644 index 0000000..60f63c7 --- /dev/null +++ b/test/mongoid-grid_fs_test.rb @@ -0,0 +1,147 @@ +require_relative 'helper' + +Testing GridFs do +## +# + prepare do + GridFS::File.destroy_all + GridFS::Chunk.destroy_all + end + +## +# + context '#put' do + + test 'default' do + filename = __FILE__ + basename = File.basename(filename) + + g = assert{ GridFS.put(filename) } + + assert{ g.filename =~ %r| #{ object_id_re } / #{ basename } \Z|imox } + assert{ g.content_type == "application/x-ruby" } + assert{ g.data == IO.read(filename) } + end + + test 'with a :filename' do + filename = 'path/info/a.rb' + + g = assert{ GridFS.put(__FILE__, :filename => filename) } + + assert{ g.filename == filename } + end + + end + + +## +# + context '#get' do + + test 'default' do + id = assert{ GridFS::File.last.id } + g = assert{ GridFs.get(id) } + end + + end + +## +# + context '#delete' do + + test 'default' do + id = assert{ GridFS::File.last.id } + g = assert{ GridFs.get(id) } + assert{ GridFs.delete(id) } + assert_raises( Mongoid::Errors::DocumentNotFound){ GridFs.get(id) } + end + + end + +## +# + context '[] and []=' do + + test 'default' do + path = 'a.rb' + data = IO.read(__FILE__) + + sio = SIO.new(path, data) + + g = assert{ GridFs[path] = sio and GridFs[path] } + + assert{ g.data == data } + assert{ g.content_type == "application/x-ruby" } + + before = GridFs::File.count + + assert{ GridFs[path] = SIO.new(path, 'foobar') } + assert{ GridFs[path].data == 'foobar' } + + after = GridFs::File.count + + created = after - before + + assert{ created.zero? } + end + +## +# + context 'data uris' do + + test 'default' do + id = assert{ GridFS::File.last.id } + g = assert{ GridFs.get(id) } + + content_type = g.content_type + base64 = [g.to_s].pack('m').chomp + + data_uri = "data:#{ content_type };base64,".concat(base64) + + assert{ g.data_uri == data_uri } + end + + end + +## +# + context 'namespaces' do + test 'default' do + assert{ GridFs.namespace.prefix == 'fs' } + assert{ GridFs.file_model.collection_name == 'fs.files' } + assert{ GridFs.chunk_model.collection_name == 'fs.chunks' } + end + + test 'new' do + ns = GridFs.namespace_for(:ns) + + assert{ ns.prefix == 'ns' } + + assert{ ns.file_model < Mongoid::Document } + assert{ ns.file_model.collection_name == 'ns.files' } + + assert{ ns.chunk_model < Mongoid::Document } + assert{ ns.chunk_model.collection_name == 'ns.chunks' } + + assert{ ns.file_model.destroy_all } + + count = GridFs::File.count + + assert{ ns.file_model.count == 0} + assert{ ns.put __FILE__ } + assert{ ns.file_model.count == 1} + + assert{ count == GridFs::File.count } + end + end + + end + +## +# + +protected + def object_id_re + %r| \w{#{ BSON::ObjectId.new.to_s.size }} |iomx + end +end diff --git a/test/testing.rb b/test/testing.rb new file mode 100644 index 0000000..945d01e --- /dev/null +++ b/test/testing.rb @@ -0,0 +1,196 @@ +# -*- encoding : utf-8 -*- +require 'test/unit' + +testdir = File.expand_path(File.dirname(__FILE__)) +rootdir = File.dirname(testdir) +libdir = File.join(rootdir, 'lib') + +STDOUT.sync = true + +$:.unshift(testdir) unless $:.include?(testdir) +$:.unshift(libdir) unless $:.include?(libdir) +$:.unshift(rootdir) unless $:.include?(rootdir) + +class Testing + class Slug < ::String + def Slug.for(*args) + string = args.flatten.compact.join('-') + words = string.to_s.scan(%r/\w+/) + words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''} + words.delete_if{|word| word.nil? or word.strip.empty?} + new(words.join('-').downcase) + end + end + + class Context + attr_accessor :name + + def initialize(name, *args) + @name = name + end + + def to_s + Slug.for(name) + end + end +end + +def Testing(*args, &block) + Class.new(::Test::Unit::TestCase) do + + ## class methods + # + class << self + def contexts + @contexts ||= [] + end + + def context(*args, &block) + return contexts.last if(args.empty? and block.nil?) + + context = Testing::Context.new(*args) + contexts.push(context) + + begin + block.call(context) + ensure + contexts.pop + end + end + + def slug_for(*args) + string = [context, args].flatten.compact.join('-') + words = string.to_s.scan(%r/\w+/) + words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''} + words.delete_if{|word| word.nil? or word.strip.empty?} + words.join('-').downcase.sub(/_$/, '') + end + + def name() const_get(:Name) end + + def testno() + '%05d' % (@testno ||= 0) + ensure + @testno += 1 + end + + def testing(*args, &block) + method = ["test", testno, slug_for(*args)].delete_if{|part| part.empty?}.join('_') + define_method(method, &block) + end + + def test(*args, &block) + testing(*args, &block) + end + + def setup(&block) + define_method(:setup, &block) if block + end + + def teardown(&block) + define_method(:teardown, &block) if block + end + + def prepare(&block) + @prepare ||= [] + @prepare.push(block) if block + @prepare + end + + def cleanup(&block) + @cleanup ||= [] + @cleanup.push(block) if block + @cleanup + end + end + + ## configure the subclass! + # + const_set(:Testno, '0') + slug = slug_for(*args).gsub(%r/-/,'_') + name = ['TESTING', '%03d' % const_get(:Testno), slug].delete_if{|part| part.empty?}.join('_') + name = name.upcase! + const_set(:Name, name) + const_set(:Missing, Object.new.freeze) + + ## instance methods + # + alias_method('__assert__', 'assert') + + def assert(*args, &block) + if args.size == 1 and args.first.is_a?(Hash) + options = args.first + expected = getopt(:expected, options){ missing } + actual = getopt(:actual, options){ missing } + if expected == missing and actual == missing + actual, expected, *ignored = options.to_a.flatten + end + expected = expected.call() if expected.respond_to?(:call) + actual = actual.call() if actual.respond_to?(:call) + assert_equal(expected, actual) + end + + if block + label = "assert(#{ args.join(' ') })" + result = nil + assert_nothing_raised{ result = block.call } + __assert__(result, label) + result + else + result = args.shift + label = "assert(#{ args.join(' ') })" + __assert__(result, label) + result + end + end + + def missing + self.class.const_get(:Missing) + end + + def getopt(opt, hash, options = nil, &block) + [opt.to_s, opt.to_s.to_sym].each do |key| + return hash[key] if hash.has_key?(key) + end + default = + if block + block.call + else + options.is_a?(Hash) ? options[:default] : nil + end + return default + end + + def subclass_of exception + class << exception + def ==(other) super or self > other end + end + exception + end + + ## + # + module_eval(&block) + + self.setup() + self.prepare.each{|b| b.call()} + + at_exit{ + self.teardown() + self.cleanup.each{|b| b.call()} + } + + self + end +end + + +if $0 == __FILE__ + + Testing 'Testing' do + testing('foo'){ assert true } + test{ assert true } + p instance_methods.grep(/test/) + end + +end