mongo-find_replace 0.18.3

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.
Files changed (68) hide show
  1. data/LICENSE.txt +202 -0
  2. data/README.rdoc +358 -0
  3. data/Rakefile +133 -0
  4. data/bin/bson_benchmark.rb +59 -0
  5. data/bin/fail_if_no_c.rb +11 -0
  6. data/examples/admin.rb +42 -0
  7. data/examples/capped.rb +22 -0
  8. data/examples/cursor.rb +48 -0
  9. data/examples/gridfs.rb +88 -0
  10. data/examples/index_test.rb +126 -0
  11. data/examples/info.rb +31 -0
  12. data/examples/queries.rb +70 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/strict.rb +35 -0
  15. data/examples/types.rb +36 -0
  16. data/lib/mongo.rb +61 -0
  17. data/lib/mongo/admin.rb +95 -0
  18. data/lib/mongo/collection.rb +664 -0
  19. data/lib/mongo/connection.rb +555 -0
  20. data/lib/mongo/cursor.rb +393 -0
  21. data/lib/mongo/db.rb +527 -0
  22. data/lib/mongo/exceptions.rb +60 -0
  23. data/lib/mongo/gridfs.rb +22 -0
  24. data/lib/mongo/gridfs/chunk.rb +90 -0
  25. data/lib/mongo/gridfs/grid_store.rb +555 -0
  26. data/lib/mongo/types/binary.rb +48 -0
  27. data/lib/mongo/types/code.rb +36 -0
  28. data/lib/mongo/types/dbref.rb +38 -0
  29. data/lib/mongo/types/min_max_keys.rb +58 -0
  30. data/lib/mongo/types/objectid.rb +219 -0
  31. data/lib/mongo/types/regexp_of_holding.rb +45 -0
  32. data/lib/mongo/util/bson_c.rb +18 -0
  33. data/lib/mongo/util/bson_ruby.rb +595 -0
  34. data/lib/mongo/util/byte_buffer.rb +222 -0
  35. data/lib/mongo/util/conversions.rb +97 -0
  36. data/lib/mongo/util/ordered_hash.rb +135 -0
  37. data/lib/mongo/util/server_version.rb +69 -0
  38. data/lib/mongo/util/support.rb +26 -0
  39. data/lib/mongo/util/xml_to_ruby.rb +112 -0
  40. data/mongo-ruby-driver.gemspec +28 -0
  41. data/test/replica/count_test.rb +34 -0
  42. data/test/replica/insert_test.rb +50 -0
  43. data/test/replica/pooled_insert_test.rb +54 -0
  44. data/test/replica/query_test.rb +39 -0
  45. data/test/test_admin.rb +67 -0
  46. data/test/test_bson.rb +397 -0
  47. data/test/test_byte_buffer.rb +81 -0
  48. data/test/test_chunk.rb +82 -0
  49. data/test/test_collection.rb +534 -0
  50. data/test/test_connection.rb +160 -0
  51. data/test/test_conversions.rb +120 -0
  52. data/test/test_cursor.rb +386 -0
  53. data/test/test_db.rb +254 -0
  54. data/test/test_db_api.rb +783 -0
  55. data/test/test_db_connection.rb +16 -0
  56. data/test/test_grid_store.rb +306 -0
  57. data/test/test_helper.rb +42 -0
  58. data/test/test_objectid.rb +156 -0
  59. data/test/test_ordered_hash.rb +168 -0
  60. data/test/test_round_trip.rb +114 -0
  61. data/test/test_slave_connection.rb +36 -0
  62. data/test/test_threading.rb +87 -0
  63. data/test/threading/test_threading_large_pool.rb +90 -0
  64. data/test/unit/collection_test.rb +52 -0
  65. data/test/unit/connection_test.rb +59 -0
  66. data/test/unit/cursor_test.rb +94 -0
  67. data/test/unit/db_test.rb +97 -0
  68. metadata +123 -0
@@ -0,0 +1,60 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ module Mongo
18
+ # Generic Mongo Ruby Driver exception class.
19
+ class MongoRubyError < StandardError; end
20
+
21
+ # Raised when MongoDB itself has returned an error.
22
+ class MongoDBError < RuntimeError; end
23
+
24
+ # Raised when configuration options cause connections, queries, etc., to fail.
25
+ class ConfigurationError < MongoRubyError; end
26
+
27
+ # Raised when invalid arguments are sent to Mongo Ruby methods.
28
+ class MongoArgumentError < MongoRubyError; end
29
+
30
+ # Raised when given a string is not valid utf-8 (Ruby 1.8 only).
31
+ class InvalidStringEncoding < MongoRubyError; end
32
+
33
+ # Raised when attempting to initialize an invalid ObjectID.
34
+ class InvalidObjectID < MongoRubyError; end
35
+
36
+ # Raised on failures in connection to the database server.
37
+ class ConnectionError < MongoRubyError; end
38
+
39
+ # Raised on failures in connection to the database server.
40
+ class ConnectionTimeoutError < MongoRubyError; end
41
+
42
+ # Raised when trying to insert a document that exceeds the 4MB limit or
43
+ # when the document contains objects that can't be serialized as BSON.
44
+ class InvalidDocument < MongoDBError; end
45
+
46
+ # Raised when a database operation fails.
47
+ class OperationFailure < MongoDBError; end
48
+
49
+ # Raised when a connection operation fails.
50
+ class ConnectionFailure < MongoDBError; end
51
+
52
+ # Raised when a client attempts to perform an invalid operation.
53
+ class InvalidOperation < MongoDBError; end
54
+
55
+ # Raised when an invalid name is used.
56
+ class InvalidName < RuntimeError; end
57
+
58
+ # Raised when the client supplies an invalid value to sort by.
59
+ class InvalidSortValueError < MongoRubyError; end
60
+ end
@@ -0,0 +1,22 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+ require 'mongo/gridfs/grid_store'
17
+
18
+ # GridFS is a specification for storing large binary objects in MongoDB.
19
+ # See the documentation for GridFS::GridStore
20
+ # @see GridFS::GridStore
21
+ module GridFS
22
+ end
@@ -0,0 +1,90 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require 'mongo/types/objectid'
18
+ require 'mongo/util/byte_buffer'
19
+ require 'mongo/util/ordered_hash'
20
+
21
+ module GridFS
22
+
23
+ # A chunk stores a portion of GridStore data.
24
+ class Chunk
25
+
26
+ DEFAULT_CHUNK_SIZE = 1024 * 256
27
+
28
+ attr_reader :object_id, :chunk_number
29
+ attr_accessor :data
30
+
31
+ def initialize(file, mongo_object={})
32
+ @file = file
33
+ @object_id = mongo_object['_id'] || Mongo::ObjectID.new
34
+ @chunk_number = mongo_object['n'] || 0
35
+
36
+ @data = ByteBuffer.new
37
+ case mongo_object['data']
38
+ when String
39
+ mongo_object['data'].each_byte { |b| @data.put(b) }
40
+ when ByteBuffer
41
+ @data.put_array(mongo_object['data'].to_a)
42
+ when Array
43
+ @data.put_array(mongo_object['data'])
44
+ when nil
45
+ else
46
+ raise "illegal chunk format; data is #{mongo_object['data'] ? (' ' + mongo_object['data'].class.name) : 'nil'}"
47
+ end
48
+ @data.rewind
49
+ end
50
+
51
+ def pos; @data.position; end
52
+ def pos=(pos); @data.position = pos; end
53
+ def eof?; !@data.more?; end
54
+
55
+ def size; @data.size; end
56
+ alias_method :length, :size
57
+
58
+ def truncate
59
+ if @data.position < @data.length
60
+ curr_data = @data
61
+ @data = ByteBuffer.new
62
+ @data.put_array(curr_data.to_a[0...curr_data.position])
63
+ end
64
+ end
65
+
66
+ def getc
67
+ @data.more? ? @data.get : nil
68
+ end
69
+
70
+ def putc(byte)
71
+ @data.put(byte)
72
+ end
73
+
74
+ def save
75
+ coll = @file.chunk_collection
76
+ coll.remove({'_id' => @object_id})
77
+ coll.insert(to_mongo_object)
78
+ end
79
+
80
+ def to_mongo_object
81
+ h = OrderedHash.new
82
+ h['_id'] = @object_id
83
+ h['files_id'] = @file.files_id
84
+ h['n'] = @chunk_number
85
+ h['data'] = data
86
+ h
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,555 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ require 'mongo/types/objectid'
18
+ require 'mongo/util/ordered_hash'
19
+ require 'mongo/gridfs/chunk'
20
+
21
+ module GridFS
22
+
23
+ # GridStore is an IO-like class that provides input and output for
24
+ # streams of data to MongoDB.
25
+ #
26
+ # @example
27
+ #
28
+ # include GridFS
29
+ #
30
+ # #Store the text "Hello, world!" in the grid store.
31
+ # GridStore.open(database, 'filename', 'w') do |f|
32
+ # f.puts "Hello, world!"
33
+ # end
34
+ #
35
+ # # Output "Hello, world!"
36
+ # GridStore.open(database, 'filename', 'r') do |f|
37
+ # puts f.read
38
+ # end
39
+ #
40
+ # # Add text to the grid store.
41
+ # GridStore.open(database, 'filename', 'w+') do |f|
42
+ # f.puts "But wait, there's more!"
43
+ # end
44
+ #
45
+ # # Retrieve everything, outputting "Hello, world!\nBut wait, there's more!\n"
46
+ # GridStore.open(database, 'filename', 'r') do |f|
47
+ # puts f.read
48
+ # end
49
+ class GridStore
50
+
51
+ DEFAULT_ROOT_COLLECTION = 'fs'
52
+ DEFAULT_CONTENT_TYPE = 'text/plain'
53
+
54
+ include Enumerable
55
+
56
+ attr_accessor :filename
57
+
58
+ # Array of strings; may be +nil+
59
+ attr_accessor :aliases
60
+
61
+ # Default is DEFAULT_CONTENT_TYPE
62
+ attr_accessor :content_type
63
+
64
+ # Size of file in bytes
65
+ attr_reader :length
66
+
67
+ attr_accessor :metadata
68
+
69
+ attr_reader :files_id
70
+
71
+ # Time that the file was first saved.
72
+ attr_reader :upload_date
73
+
74
+ attr_reader :chunk_size
75
+
76
+ attr_accessor :lineno
77
+
78
+ attr_reader :md5
79
+
80
+ # Determine whether a given file exists in the GridStore.
81
+ #
82
+ # @param [Mongo::DB] a MongoDB database.
83
+ # @param [String] name the filename.
84
+ # @param [String] root_collection the name of the gridfs root collection.
85
+ #
86
+ # @return [Boolean]
87
+ def self.exist?(db, name, root_collection=DEFAULT_ROOT_COLLECTION)
88
+ db.collection("#{root_collection}.files").find({'filename' => name}).next_document != nil
89
+ end
90
+
91
+ # Open a GridFS file for reading, writing, or appending. Note that
92
+ # this method must be used with a block.
93
+ #
94
+ # @param [Mongo::DB] a MongoDB database.
95
+ # @param [String] name the filename.
96
+ # @param [String] mode one of 'r', 'w', or 'w+' for reading, writing,
97
+ # and appending, respectively.
98
+ # @param [Hash] options any of the options available on
99
+ # GridStore initialization.
100
+ #
101
+ # @see GridStore#initialize.
102
+ # @see The various GridStore class methods, e.g., GridStore.open, GridStore.read etc.
103
+ def self.open(db, name, mode, options={})
104
+ gs = self.new(db, name, mode, options)
105
+ result = nil
106
+ begin
107
+ result = yield gs if block_given?
108
+ ensure
109
+ gs.close
110
+ end
111
+ result
112
+ end
113
+
114
+ # Read a file stored in GridFS.
115
+ #
116
+ # @param [Mongo::DB] db a MongoDB database.
117
+ # @param [String] name the name of the file.
118
+ # @param [Integer] length the number of bytes to read.
119
+ # @param [Integer] offset the number of bytes beyond the
120
+ # beginning of the file to start reading.
121
+ #
122
+ # @return [String] the file data
123
+ def self.read(db, name, length=nil, offset=nil)
124
+ GridStore.open(db, name, 'r') do |gs|
125
+ gs.seek(offset) if offset
126
+ gs.read(length)
127
+ end
128
+ end
129
+
130
+ # List the contents of all GridFS files stored in the given db and
131
+ # root collection.
132
+ #
133
+ # @param [Mongo::DB] db a MongoDB database.
134
+ # @param [String] root_collection the name of the root collection.
135
+ #
136
+ # @return [Array]
137
+ def self.list(db, root_collection=DEFAULT_ROOT_COLLECTION)
138
+ db.collection("#{root_collection}.files").find().map do |f|
139
+ f['filename']
140
+ end
141
+ end
142
+
143
+ # Get each line of data from the specified file
144
+ # as an array of strings.
145
+ #
146
+ # @param [Mongo::DB] db a MongoDB database.
147
+ # @param [String] name the filename.
148
+ # @param [String, Reg] separator
149
+ #
150
+ # @return [Array]
151
+ def self.readlines(db, name, separator=$/)
152
+ GridStore.open(db, name, 'r') do |gs|
153
+ gs.readlines(separator)
154
+ end
155
+ end
156
+
157
+ # Remove one for more files from the given db.
158
+ #
159
+ # @param [Mongo::Database] db a MongoDB database.
160
+ # @param [Array<String>] names the filenames to remove
161
+ #
162
+ # @return [True]
163
+ def self.unlink(db, *names)
164
+ names.each do |name|
165
+ gs = GridStore.new(db, name)
166
+ gs.send(:delete_chunks)
167
+ gs.collection.remove('_id' => gs.files_id)
168
+ end
169
+ end
170
+ class << self
171
+ alias_method :delete, :unlink
172
+ end
173
+
174
+ # Rename a file in this collection. Note that this method uses
175
+ # Collection#update, which means that you will not be notified
176
+ #
177
+ # @param [Mongo::DB] a MongoDB database.
178
+ # @param [String] src the name of the source file.
179
+ # @param [String] dest the name of the destination file.
180
+ # @param [String] root_collection the name of the default root collection.
181
+ def self.mv(db, src, dest, root_collection=DEFAULT_ROOT_COLLECTION)
182
+ db.collection("#{root_collection}.files").update({ :filename => src }, { '$set' => { :filename => dest } })
183
+ end
184
+
185
+ # Initialize a GridStore instance for reading, writing, or modifying a given file.
186
+ # Note that it's often easier to work with the various GridStore class methods (open, read, etc.).
187
+ #
188
+ # @param [Mongo::DB] db a MongoDB database.
189
+ # @param [String] name a filename.
190
+ # @param [String] mode either 'r', 'w', or 'w+' for reading, writing, or appending, respectively.
191
+ #
192
+ # @option options [String] :root DEFAULT_ROOT_COLLECTION ('r', 'w', 'w+') the name of the root collection to use.
193
+ #
194
+ # @option options [String] :metadata ({}) (w, w+) A hash containing any data you want persisted as
195
+ # this file's metadata.
196
+ #
197
+ # @option options [Integer] :chunk_size (Chunk::DEFAULT_CHUNK_SIZE) (w) Sets chunk size for files opened for writing.
198
+ # See also GridStore#chunk_size=.
199
+ #
200
+ # @option options [String] :content_type ('text/plain') Set the content type stored as the
201
+ # file's metadata. See also GridStore#content_type=.
202
+ def initialize(db, name, mode='r', options={})
203
+ @db, @filename, @mode = db, name, mode
204
+ @root = options[:root] || DEFAULT_ROOT_COLLECTION
205
+
206
+ doc = collection.find({'filename' => @filename}).next_document
207
+ if doc
208
+ @files_id = doc['_id']
209
+ @content_type = doc['contentType']
210
+ @chunk_size = doc['chunkSize']
211
+ @upload_date = doc['uploadDate']
212
+ @aliases = doc['aliases']
213
+ @length = doc['length']
214
+ @metadata = doc['metadata']
215
+ @md5 = doc['md5']
216
+ else
217
+ @files_id = Mongo::ObjectID.new
218
+ @content_type = DEFAULT_CONTENT_TYPE
219
+ @chunk_size = Chunk::DEFAULT_CHUNK_SIZE
220
+ @length = 0
221
+ end
222
+
223
+ case mode
224
+ when 'r'
225
+ @curr_chunk = nth_chunk(0)
226
+ @position = 0
227
+ when 'w'
228
+ chunk_collection.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]])
229
+ delete_chunks
230
+ @curr_chunk = Chunk.new(self, 'n' => 0)
231
+ @content_type = options[:content_type] if options[:content_type]
232
+ @chunk_size = options[:chunk_size] if options[:chunk_size]
233
+ @metadata = options[:metadata] if options[:metadata]
234
+ @position = 0
235
+ when 'w+'
236
+ chunk_collection.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]])
237
+ @curr_chunk = nth_chunk(last_chunk_number) || Chunk.new(self, 'n' => 0) # might be empty
238
+ @curr_chunk.pos = @curr_chunk.data.length if @curr_chunk
239
+ @metadata = options[:metadata] if options[:metadata]
240
+ @position = @length
241
+ else
242
+ raise "error: illegal mode #{mode}"
243
+ end
244
+
245
+ @lineno = 0
246
+ @pushback_byte = nil
247
+ end
248
+
249
+ # Get the files collection referenced by this GridStore instance.
250
+ #
251
+ # @return [Mongo::Collection]
252
+ def collection
253
+ @db.collection("#{@root}.files")
254
+ end
255
+
256
+ # Get the chunk collection referenced by this GridStore.
257
+ #
258
+ # @return [Mongo::Collection]
259
+ def chunk_collection
260
+ @db.collection("#{@root}.chunks")
261
+ end
262
+
263
+ # Change the chunk size. This is permitted only when the file is opened for write
264
+ # and no data has yet been written.
265
+ #
266
+ # @param [Integer] size the new chunk size, in bytes.
267
+ #
268
+ # @return [Integer] the new chunk size.
269
+ def chunk_size=(size)
270
+ unless @mode[0] == ?w && @position == 0 && @upload_date == nil
271
+ raise "error: can only change chunk size if open for write and no data written."
272
+ end
273
+ @chunk_size = size
274
+ end
275
+
276
+ # ================ reading ================
277
+
278
+ def getc
279
+ if @pushback_byte
280
+ byte = @pushback_byte
281
+ @pushback_byte = nil
282
+ @position += 1
283
+ byte
284
+ elsif eof?
285
+ nil
286
+ else
287
+ if @curr_chunk.eof?
288
+ @curr_chunk = nth_chunk(@curr_chunk.chunk_number + 1)
289
+ end
290
+ @position += 1
291
+ @curr_chunk.getc
292
+ end
293
+ end
294
+
295
+ def gets(separator=$/)
296
+ str = ''
297
+ byte = self.getc
298
+ return nil if byte == nil # EOF
299
+ while byte != nil
300
+ s = byte.chr
301
+ str << s
302
+ break if s == separator
303
+ byte = self.getc
304
+ end
305
+ @lineno += 1
306
+ str
307
+ end
308
+
309
+ def read(len=nil, buf=nil)
310
+ if len
311
+ read_partial(len, buf)
312
+ else
313
+ read_all(buf)
314
+ end
315
+ end
316
+
317
+ def readchar
318
+ byte = self.getc
319
+ raise EOFError.new if byte == nil
320
+ byte
321
+ end
322
+
323
+ def readline(separator=$/)
324
+ line = gets
325
+ raise EOFError.new if line == nil
326
+ line
327
+ end
328
+
329
+ def readlines(separator=$/)
330
+ read.split(separator).collect { |line| "#{line}#{separator}" }
331
+ end
332
+
333
+ def each
334
+ line = gets
335
+ while line
336
+ yield line
337
+ line = gets
338
+ end
339
+ end
340
+ alias_method :each_line, :each
341
+
342
+ def each_byte
343
+ byte = self.getc
344
+ while byte
345
+ yield byte
346
+ byte = self.getc
347
+ end
348
+ end
349
+
350
+ def ungetc(byte)
351
+ @pushback_byte = byte
352
+ @position -= 1
353
+ end
354
+
355
+ # ================ writing ================
356
+
357
+ def putc(byte)
358
+ if @curr_chunk.pos == @chunk_size
359
+ prev_chunk_number = @curr_chunk.chunk_number
360
+ @curr_chunk.save
361
+ @curr_chunk = Chunk.new(self, 'n' => prev_chunk_number + 1)
362
+ end
363
+ @position += 1
364
+ @curr_chunk.putc(byte)
365
+ end
366
+
367
+ def print(*objs)
368
+ objs = [$_] if objs == nil || objs.empty?
369
+ objs.each { |obj|
370
+ str = obj.to_s
371
+ str.each_byte { |byte| self.putc(byte) }
372
+ }
373
+ nil
374
+ end
375
+
376
+ def puts(*objs)
377
+ if objs == nil || objs.empty?
378
+ self.putc(10)
379
+ else
380
+ print(*objs.collect{ |obj|
381
+ str = obj.to_s
382
+ str << "\n" unless str =~ /\n$/
383
+ str
384
+ })
385
+ end
386
+ nil
387
+ end
388
+
389
+ def <<(obj)
390
+ write(obj.to_s)
391
+ end
392
+
393
+ def write(string)
394
+ raise "#@filename not opened for write" unless @mode[0] == ?w
395
+ # Since Ruby 1.9.1 doesn't necessarily store one character per byte.
396
+ if string.respond_to?(:force_encoding)
397
+ string.force_encoding("binary")
398
+ end
399
+ to_write = string.length
400
+ while (to_write > 0) do
401
+ if @curr_chunk && @curr_chunk.data.position == @chunk_size
402
+ prev_chunk_number = @curr_chunk.chunk_number
403
+ @curr_chunk = GridFS::Chunk.new(self, 'n' => prev_chunk_number + 1)
404
+ end
405
+ chunk_available = @chunk_size - @curr_chunk.data.position
406
+ step_size = (to_write > chunk_available) ? chunk_available : to_write
407
+ @curr_chunk.data.put_array(ByteBuffer.new(string[-to_write,step_size]).to_a)
408
+ to_write -= step_size
409
+ @curr_chunk.save
410
+ end
411
+ string.length - to_write
412
+ end
413
+
414
+ # A no-op.
415
+ def flush
416
+ end
417
+
418
+ # ================ status ================
419
+
420
+ def eof
421
+ raise IOError.new("stream not open for reading") unless @mode[0] == ?r
422
+ @position >= @length
423
+ end
424
+ alias_method :eof?, :eof
425
+
426
+ # ================ positioning ================
427
+
428
+ def rewind
429
+ if @curr_chunk.chunk_number != 0
430
+ if @mode[0] == ?w
431
+ delete_chunks
432
+ @curr_chunk = Chunk.new(self, 'n' => 0)
433
+ else
434
+ @curr_chunk == nth_chunk(0)
435
+ end
436
+ end
437
+ @curr_chunk.pos = 0
438
+ @lineno = 0
439
+ @position = 0
440
+ end
441
+
442
+ def seek(pos, whence=IO::SEEK_SET)
443
+ target_pos = case whence
444
+ when IO::SEEK_CUR
445
+ @position + pos
446
+ when IO::SEEK_END
447
+ @length + pos
448
+ when IO::SEEK_SET
449
+ pos
450
+ end
451
+
452
+ new_chunk_number = (target_pos / @chunk_size).to_i
453
+ if new_chunk_number != @curr_chunk.chunk_number
454
+ @curr_chunk.save if @mode[0] == ?w
455
+ @curr_chunk = nth_chunk(new_chunk_number)
456
+ end
457
+ @position = target_pos
458
+ @curr_chunk.pos = @position % @chunk_size
459
+ 0
460
+ end
461
+
462
+ def tell
463
+ @position
464
+ end
465
+
466
+ #---
467
+ # ================ closing ================
468
+ #+++
469
+
470
+ def close
471
+ if @mode[0] == ?w
472
+ if @curr_chunk
473
+ @curr_chunk.truncate
474
+ @curr_chunk.save if @curr_chunk.pos > 0
475
+ end
476
+ files = collection
477
+ if @upload_date
478
+ files.remove('_id' => @files_id)
479
+ else
480
+ @upload_date = Time.now
481
+ end
482
+ files.insert(to_mongo_object)
483
+ end
484
+ @db = nil
485
+ end
486
+
487
+ def closed?
488
+ @db == nil
489
+ end
490
+
491
+ #---
492
+ # ================ protected ================
493
+ #+++
494
+
495
+ protected
496
+
497
+ def to_mongo_object
498
+ h = OrderedHash.new
499
+ h['_id'] = @files_id
500
+ h['filename'] = @filename
501
+ h['contentType'] = @content_type
502
+ h['length'] = @curr_chunk ? @curr_chunk.chunk_number * @chunk_size + @curr_chunk.pos : 0
503
+ h['chunkSize'] = @chunk_size
504
+ h['uploadDate'] = @upload_date
505
+ h['aliases'] = @aliases
506
+ h['metadata'] = @metadata
507
+ md5_command = OrderedHash.new
508
+ md5_command['filemd5'] = @files_id
509
+ md5_command['root'] = @root
510
+ h['md5'] = @db.command(md5_command)['md5']
511
+ h
512
+ end
513
+
514
+ def read_partial(len, buf=nil)
515
+ buf ||= ''
516
+ byte = self.getc
517
+ while byte != nil && (len == nil || len > 0)
518
+ buf << byte.chr
519
+ len -= 1 if len
520
+ byte = self.getc if (len == nil || len > 0)
521
+ end
522
+ buf
523
+ end
524
+
525
+ def read_all(buf=nil)
526
+ buf ||= ''
527
+ while true do
528
+ if (@curr_chunk.pos > 0)
529
+ data = @curr_chunk.data.to_s
530
+ buf += data[@position, data.length]
531
+ else
532
+ buf += @curr_chunk.data.to_s
533
+ end
534
+ break if @curr_chunk.chunk_number == last_chunk_number
535
+ @curr_chunk = nth_chunk(@curr_chunk.chunk_number + 1)
536
+ end
537
+ buf
538
+ end
539
+
540
+ def delete_chunks
541
+ chunk_collection.remove({'files_id' => @files_id}) if @files_id
542
+ @curr_chunk = nil
543
+ end
544
+
545
+ def nth_chunk(n)
546
+ mongo_chunk = chunk_collection.find({'files_id' => @files_id, 'n' => n}).next_document
547
+ Chunk.new(self, mongo_chunk || {})
548
+ end
549
+
550
+ def last_chunk_number
551
+ (@length / @chunk_size).to_i
552
+ end
553
+
554
+ end
555
+ end