require 'spec_helper' require 'rcs-common/gridfs' module RCS::Common::GridFS describe 'compatibility' do begin # Note: use the modified version of mongo-ruby-driver (with append support) # path = File.expand_path('~/.rvm/gems/ruby-2.0.0-p451@rcs-project/bundler/gems/mongo-ruby-driver-b1d59bfe1700/lib') # $LOAD_PATH << path if File.exist?(path) require 'mongo' rescue LoadError $mongo_gem_missing = true end break if $mongo_gem_missing let(:bucket) { Bucket.new } let(:db_name) { bucket.session.options[:database] } let(:mongo_client_db) { Mongo::MongoClient.new[db_name] } let(:grid) { Mongo::Grid.new(mongo_client_db)} let(:grid_filesystem) { Mongo::GridFileSystem.new(mongo_client_db) } let(:chunk_size) { Bucket::DEFAULT_CHUNK_SIZE } it 'is compatibile with mongo grid#put' do data = 'foo bar' id = grid.put(data) expect(bucket.get(id.to_s).read).to eq(data) data = ('bar'*chunk_size)+'foo' id = grid.put(data) expect(bucket.get(id.to_s).read).to eq(data) end it 'is compatibile with mongo grid#get' do data = 'foo bar' id = bucket.put(data) id = ::BSON::ObjectId.from_string(id.to_s) expect(grid.get(id).read).to eq(data) data = ('bar'*chunk_size)+'foo' id = bucket.put(data) id = ::BSON::ObjectId.from_string(id.to_s) expect(grid.get(id).read).to eq(data) end it 'is compatibile with mongo file#read' do data = ('bar'*chunk_size*5)+'foo' id = grid.put(data) bucket_file = bucket.get(id.to_s) grid_file = grid.get(id) loop do bytes = rand(0..1000) buff1 = grid_file.read(bytes) buff2 = bucket_file.read(bytes) expect(buff1).to eq(buff2) break if buff1 == '' or buff2 == '' end end it 'is compatibile with mongo-ruby-driver append' do data = "foo"*chunk_size id = bucket.put('a') id2 = ::BSON::ObjectId.new fs = grid_filesystem.open(id2, 'w') { |f| f.write('a') } index, seek = 0, (chunk_size / 2 - 10) loop { cnt = data[index..index+seek-1] break if cnt.nil? or cnt.size == 0 index += seek bucket.append(id, cnt) grid_filesystem.open(id2, 'a') { |f| f.write(cnt) } expect(bucket.content(id)).to eq('a'+data[0..index - 1]) expect(grid_filesystem.open(id2, 'r') { |f| f.read }).to eq('a'+data[0..index - 1]) } end end # Keep it at least >= 3 in these tests $chunk_size = 5 describe File do before do Bucket.__send__(:remove_const, :DEFAULT_CHUNK_SIZE) Bucket.const_set(:DEFAULT_CHUNK_SIZE, $chunk_size) end let(:bucket) { Bucket.new } let(:content) { ('a'*$chunk_size)+'b' } let(:file) { bucket.get(bucket.put(content)) } describe '#eof?' do it 'is false when there are bytes to read' do expect(file.eof?).to be_falsey end context 'when file#read has been called' do before { file.read } it 'is true' do expect(file.eof?).to be_truthy end end context 'when the file has been read sequentially' do let(:content) { ('a'*$chunk_size)+('b'*$chunk_size)+'c' } let(:file) { bucket.get(bucket.put(content)) } it 'is false until the are no more bytes to read' do file.read(1) expect(file.eof?).to be_falsey file.read($chunk_size**2) expect(file.eof?).to be_truthy file.rewind file.read(1) expect(file.eof?).to be_falsey file.read($chunk_size*2) expect(file.eof?).to be_truthy end end end describe '#read' do it 'reads the entire file without parameters' do expect(file.read).to eq(content) end context 'when the content is binary' do let(:content_size) { $chunk_size*20+$chunk_size/2 } let(:content) do content = "" content_size.times { content << rand(0..255).chr } content end let(:file) { bucket.get(bucket.put(content)) } def print_bytes(str) str2 = [] str.bytes.each { |b| str2 << b.to_i } str2.join(", ") end it 'reads the file sequentially (binary)' do readed = "" loop { break if file.eof?; readed << file.read($chunk_size/2) } expect(readed).to eq(content) end end it 'reads the file sequentially' do expect(file.read($chunk_size)).to eq('a'*$chunk_size) expect(file.read).to eq('b') file.rewind expect(file.read($chunk_size)).to eq('a'*$chunk_size) expect(file.read($chunk_size)).to eq('b') file.rewind expect(file.read($chunk_size-1)).to eq(('a'*($chunk_size-1))) expect(file.read).to eq('ab') expect(file.read).to be_empty file.rewind $chunk_size.times { expect(file.read(1)).to eq('a') } expect(file.read(1)).to eq('b') expect(file.read(1)).to be_empty file.rewind expect(file.read($chunk_size**2)).to eq(content) end end end describe Bucket do before do described_class.__send__(:remove_const, :DEFAULT_CHUNK_SIZE) described_class.const_set(:DEFAULT_CHUNK_SIZE, $chunk_size) end it "deal with different encodings" do bucket = Bucket.new non_utf8_string = "- Men\xFC -" expect(non_utf8_string.valid_encoding?).to be_falsey id = bucket.put non_utf8_string file = bucket.get(id) expect(file.read.encoding.to_s).to eq('ASCII-8BIT') utf8_string = "ciao" id = bucket.put utf8_string file = bucket.get(id) expect(file.read.encoding.to_s).to eq('UTF-8') end describe '#append' do let(:bucket) { Bucket.new } it 'appends the given data to the file' do content = 'a'*$chunk_size file_id = bucket.put(content) content << 'b' bucket.append(file_id, 'b') expect(bucket.content(file_id)).to eq(content) content << 'c'*$chunk_size bucket.append(file_id, 'c'*$chunk_size) expect(bucket.content(file_id)).to eq(content) content << 'd'*($chunk_size-1) bucket.append(file_id, 'd'*($chunk_size-1)) expect(bucket.content(file_id)).to eq(content) end it 'updates the file length' do content = 'a'*$chunk_size file_id = bucket.put(content[1]) expect(bucket.get(file_id).length).to eq(1) bucket.append(file_id, content[1..-1]) expect(bucket.get(file_id).length).to eq(content.bytesize) content = 'a'*$chunk_size * 2 file_id = bucket.put(content[0..$chunk_size-1]) expect(bucket.get(file_id).length).to eq($chunk_size) bucket.append(file_id, content[$chunk_size..-1]) expect(bucket.get(file_id).length).to eq($chunk_size * 2) end it 'updates the md5 (by default)' do content = 'a'*$chunk_size file_id = bucket.put(content[1]) expect(bucket.get(file_id).md5).to eq(Digest::MD5.hexdigest('a')) bucket.append(file_id, content[1..-1]) expect(bucket.get(file_id).md5).to eq(Digest::MD5.hexdigest(content)) end context 'when the file is missing' do it 'raises an error' do fake_id = ::BSON::ObjectId.new expect { bucket.append(fake_id, 'foo') }.to raise_error(/not found/) end context 'when option :create is passed' do it 'creates the file' do fake_id = ::BSON::ObjectId.new expect { bucket.append(fake_id, 'foo', create: true) }.not_to raise_error fake_id = ::BSON::ObjectId.new file_id, length = bucket.append(fake_id, 'foo', create: {filename: 'bar'}) file = bucket.get(file_id) expect(file.filename).to eq('bar') expect(file.content).to eq('foo') end end end context "when {md5: false} is given as options" do it 'sets the md5 attribute to nil' do content = 'a'*$chunk_size file_id = bucket.put(content[1]) expect(bucket.get(file_id).md5).to eq(Digest::MD5.hexdigest('a')) bucket.append(file_id, content[1..-1], md5: false) expect(bucket.get(file_id).md5).to be_nil end end end describe '#delete' do let(:bucket) { Bucket.new } let(:content) { ('a'*$chunk_size)+'b' } it 'returns nil when the file exists' do id = bucket.put(content) expect(bucket.delete(id)).to be_nil end it 'returns nil when the file is missing' do id = ::BSON::ObjectId.new expect(bucket.delete(id)).to be_nil end it 'removes the file and its chunks' do id = bucket.put(content) expect(id).not_to be_nil bucket.delete(id) expect(bucket.get(id)).to be_nil expect(bucket.files_collection.find(_id: id).count).to be_zero expect(bucket.chunks_collection.find(files_id: id).count).to be_zero end it 'does not deletes other files' do id = bucket.put(content) id2 = bucket.put(content+'c') bucket.delete(id) expect(bucket.get(id)).to be_nil expect(bucket.get(id2).read).to eq(content+'c') end end describe '#drop' do it 'removes the collections' do bucket = Bucket.new(nil, lazy: false) bucket.drop expect(bucket.session.collections).to be_empty end context 'when the collections are missing' do let(:bucket) do bucket = Bucket.new end context '#get' do it 'returns nil (without errors)' do expect(bucket.get(::BSON::ObjectId.new)).to be_nil end end context '#put' do it 'creates the collections and the indexes' do bucket.put 'foo bar' expect(bucket.session['fs.chunks'].indexes.count).to eq(2) expect(bucket.session['fs.files'].indexes.count).to eq(2) end end end end describe '#initialize' do it 'creates the requied collections with the default name' do session = Bucket.new(nil, lazy: false).session names = session.collections.map(&:name).sort expect(names).to eq(['fs.chunks', 'fs.files']) end it 'creates the requied collections with the given prefix' do session = Bucket.new('foo.bar', lazy: false).session names = session.collections.map(&:name).sort expect(names).to eq(['foo.bar.chunks', 'foo.bar.files']) end it 'creates the indexes' do session = Bucket.new(nil, lazy: false).session db_name = session.options[:database] chunks_indexes = session['fs.chunks'].indexes files_indexes = session['fs.files'].indexes expect(chunks_indexes.count).to eq(2) expect(chunks_indexes.to_a[1]).to eq("v"=>1, "key"=>{"files_id"=>1, "n"=>1}, "unique"=>true, "ns"=>"#{db_name}.fs.chunks", "name"=>"files_id_1_n_1") expect(files_indexes.count).to eq(2) expect(files_indexes.to_a[1]).to eq("v"=>1, "key"=>{"filename"=>1}, "ns"=>"#{db_name}.fs.files", "background"=>true, "name"=>"filename_1") end context 'when nil is passed as name' do it 'creates the requied collections with the default name' do session = Bucket.new(nil, lazy: false).session names = session.collections.map(&:name).sort expect(names).to eq(['fs.chunks', 'fs.files']) end end context 'when a blank string is passed as name' do it 'creates the requied collections with the default name' do session = Bucket.new(' ', lazy: false).session names = session.collections.map(&:name).sort expect(names).to eq(['fs.chunks', 'fs.files']) end end context 'when lazy' do it 'does not create the collections' do session = Bucket.new('fs', lazy: true).session expect(session.collections).to be_empty end end end describe '#md5' do let(:bucket) { Bucket.new } let(:content) { "foo bar" } let(:md5) { Digest::MD5.hexdigest(content) } it 'gets the md5 of a stored file' do id = bucket.put(content) expect(bucket.md5(id)).to eq(md5) end it 'does not read the md5 attribute on the file document' do id = bucket.put(content) bucket.files_collection.find(_id: id).update('$set' => {md5: 'foo'}) expect(bucket.md5(id)).to eq(md5) expect(bucket.get(id).md5).to eq('foo') end end describe '#get' do let(:bucket) { Bucket.new } let(:content) { "foo bar" } it 'retuns a object with a read method' do id = bucket.put(content) expect(bucket.get(id)).to respond_to(:read) end it 'returns nil when nothing is found' do id = ::BSON::ObjectId.new expect(bucket.get(id)).to be_nil end it 'works if the given id is a string' do id = bucket.put(content) expect(bucket.get(id.to_s)).not_to be_nil end it 'works if the given id is a (Moped::)BSON::ObjectId' do id = bucket.put(content) id = BSON ? ::BSON::ObjectId.from_string(id.to_s) : ::BSON::ObjectId.from_string(id.to_s) expect(bucket.get(id)).not_to be_nil end it 'does not works with filenames (raise an error)' do id = bucket.put(content, filename: 'foo.bar') expect{ bucket.get('foo.bar') }.to raise_error(BSON::ObjectId::Invalid) end end describe '#put' do let(:bucket) { Bucket.new } let(:now) { Time.now.utc } context 'the file size is 7 bytes (< DEFAULT_CHUNK_SIZE)' do let(:content) { "foo bar" } it 'returns nil when nil (or empty string) is passed as content' do expect(bucket.put(nil)).to be_nil expect(bucket.files_collection.find.count).to be_zero expect(bucket.chunks_collection.find.count).to be_zero end it 'stores a file with the given (valid) attributes' do id = bucket.put(content, filename: 'prova.txt', upload_date: now, metadata: {a: 1}) file = bucket.get(id) expect(file.read).to eq(content) expect(file.filename).to eq('prova.txt') expect(file.upload_date.to_i).to eq(now.to_i) expect(file.metadata).to eq({'a' => 1}) end it 'raise an error with invalid attributes' do id = bucket.put(content, foo: 'bar', content_type: 'text') file = bucket.get(id) expect{ file.foo }.to raise_error(NoMethodError) expect(file.content_type).to eq('text') end it 'forces the aliases attribute to be an array' do id = bucket.put(content, aliases: 'bar') file = bucket.get(id) expect(file.aliases).to eq(['bar']) id = bucket.put(content, aliases: ['bar', 'foo']) file = bucket.get(id) expect(file.aliases).to eq(['bar', 'foo']) end it 'forces the metadata attribute to be an hash when its empty' do id = bucket.put(content, metadata: '') file = bucket.get(id) expect(file.metadata).to eq({}) id = bucket.put(content, metadata: nil) file = bucket.get(id) expect(file.metadata).to eq({}) end it 'does not overwrite calculated attributes' do id = bucket.put(content, md5: 'foo', chunk_size: 'foo', chunkSize: 'foo', length: 'foo') file = bucket.get(id) expect(file.md5).to eq(Digest::MD5.hexdigest(content)) expect(file.length).to eq(content.bytesize) expect(file.chunk_size).to eq($chunk_size) expect(file.chunkSize).to eq($chunk_size) end it 'uses default values for some attributes' do id = bucket.put(content, filename: 'prova.txt') file = bucket.get(id) expect(file.content_type).to eq('application/octet-stream') expect(file.aliases).to eq([]) expect(file.metadata).to eq({}) end end context 'the file size is DEFAULT_CHUNK_SIZE bytes long' do let(:content) { 'a'*$chunk_size } it 'uses only a chunk' do id = bucket.put(content) expect(bucket.chunks_collection.find(files_id: id).count).to eq(1) end it 'the chunk number is zero' do id = bucket.put(content) expect(bucket.chunks_collection.find(files_id: id).first['n']).to eq(0) end it 'stores the files correctly' do id = bucket.put(content) file = bucket.get(id) expect(file.read).to eq(content) expect(file.md5).to eq(bucket.md5(id)) expect(file.length).to eq($chunk_size) end end context 'the file size is DEFAULT_CHUNK_SIZE + 1 bytes long' do let(:content) { ('a'*$chunk_size)+'b' } it 'uses 2 chunks' do id = bucket.put(content) expect(bucket.chunks_collection.find(files_id: id).count).to eq(2) end it 'numbers the chunks correctly' do id = bucket.put(content) numbers = bucket.chunks_collection.find(files_id: id).map { |doc| doc['n'] } expect(numbers).to eq([0, 1]) end it 'stores the files correctly' do id = bucket.put(content) file = bucket.get(id) expect(file.read).to eq(content) expect(file.md5).to eq(bucket.md5(id)) expect(file.length).to eq($chunk_size + 1) end end context 'the file size is DEFAULT_CHUNK_SIZE * 2 bytes long' do let(:content) { 'a'*$chunk_size*2 } it 'uses 2 chunks' do id = bucket.put(content) expect(bucket.chunks_collection.find(files_id: id).count).to eq(2) end it 'numbers the chunks correctly' do id = bucket.put(content) numbers = bucket.chunks_collection.find(files_id: id).map { |doc| doc['n'] } expect(numbers).to eq([0, 1]) end it 'stores the files correctly' do id = bucket.put(content) file = bucket.get(id) expect(file.read).to eq(content) expect(file.md5).to eq(bucket.md5(id)) expect(file.length).to eq($chunk_size*2) end end end end end .