mongo-lyon 1.2.4

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 (87) hide show
  1. data/LICENSE.txt +190 -0
  2. data/README.md +344 -0
  3. data/Rakefile +202 -0
  4. data/bin/mongo_console +34 -0
  5. data/docs/1.0_UPGRADE.md +21 -0
  6. data/docs/CREDITS.md +123 -0
  7. data/docs/FAQ.md +116 -0
  8. data/docs/GridFS.md +158 -0
  9. data/docs/HISTORY.md +225 -0
  10. data/docs/REPLICA_SETS.md +72 -0
  11. data/docs/TUTORIAL.md +247 -0
  12. data/docs/WRITE_CONCERN.md +28 -0
  13. data/lib/mongo.rb +77 -0
  14. data/lib/mongo/collection.rb +872 -0
  15. data/lib/mongo/connection.rb +875 -0
  16. data/lib/mongo/cursor.rb +449 -0
  17. data/lib/mongo/db.rb +607 -0
  18. data/lib/mongo/exceptions.rb +68 -0
  19. data/lib/mongo/gridfs/grid.rb +106 -0
  20. data/lib/mongo/gridfs/grid_ext.rb +57 -0
  21. data/lib/mongo/gridfs/grid_file_system.rb +145 -0
  22. data/lib/mongo/gridfs/grid_io.rb +394 -0
  23. data/lib/mongo/gridfs/grid_io_fix.rb +38 -0
  24. data/lib/mongo/repl_set_connection.rb +342 -0
  25. data/lib/mongo/util/conversions.rb +89 -0
  26. data/lib/mongo/util/core_ext.rb +60 -0
  27. data/lib/mongo/util/pool.rb +185 -0
  28. data/lib/mongo/util/server_version.rb +71 -0
  29. data/lib/mongo/util/support.rb +82 -0
  30. data/lib/mongo/util/uri_parser.rb +181 -0
  31. data/lib/mongo/version.rb +3 -0
  32. data/mongo.gemspec +34 -0
  33. data/test/auxillary/1.4_features.rb +166 -0
  34. data/test/auxillary/authentication_test.rb +68 -0
  35. data/test/auxillary/autoreconnect_test.rb +41 -0
  36. data/test/auxillary/repl_set_auth_test.rb +58 -0
  37. data/test/auxillary/slave_connection_test.rb +36 -0
  38. data/test/auxillary/threaded_authentication_test.rb +101 -0
  39. data/test/bson/binary_test.rb +15 -0
  40. data/test/bson/bson_test.rb +614 -0
  41. data/test/bson/byte_buffer_test.rb +190 -0
  42. data/test/bson/hash_with_indifferent_access_test.rb +38 -0
  43. data/test/bson/json_test.rb +17 -0
  44. data/test/bson/object_id_test.rb +154 -0
  45. data/test/bson/ordered_hash_test.rb +197 -0
  46. data/test/collection_test.rb +893 -0
  47. data/test/connection_test.rb +303 -0
  48. data/test/conversions_test.rb +120 -0
  49. data/test/cursor_fail_test.rb +75 -0
  50. data/test/cursor_message_test.rb +43 -0
  51. data/test/cursor_test.rb +457 -0
  52. data/test/db_api_test.rb +715 -0
  53. data/test/db_connection_test.rb +15 -0
  54. data/test/db_test.rb +287 -0
  55. data/test/grid_file_system_test.rb +244 -0
  56. data/test/grid_io_test.rb +120 -0
  57. data/test/grid_test.rb +200 -0
  58. data/test/load/thin/load.rb +24 -0
  59. data/test/load/unicorn/load.rb +23 -0
  60. data/test/replica_sets/connect_test.rb +86 -0
  61. data/test/replica_sets/connection_string_test.rb +32 -0
  62. data/test/replica_sets/count_test.rb +35 -0
  63. data/test/replica_sets/insert_test.rb +53 -0
  64. data/test/replica_sets/pooled_insert_test.rb +55 -0
  65. data/test/replica_sets/query_secondaries.rb +96 -0
  66. data/test/replica_sets/query_test.rb +51 -0
  67. data/test/replica_sets/replication_ack_test.rb +66 -0
  68. data/test/replica_sets/rs_test_helper.rb +27 -0
  69. data/test/safe_test.rb +68 -0
  70. data/test/support/hash_with_indifferent_access.rb +199 -0
  71. data/test/support/keys.rb +45 -0
  72. data/test/support_test.rb +19 -0
  73. data/test/test_helper.rb +83 -0
  74. data/test/threading/threading_with_large_pool_test.rb +90 -0
  75. data/test/threading_test.rb +87 -0
  76. data/test/tools/auth_repl_set_manager.rb +14 -0
  77. data/test/tools/repl_set_manager.rb +266 -0
  78. data/test/unit/collection_test.rb +130 -0
  79. data/test/unit/connection_test.rb +98 -0
  80. data/test/unit/cursor_test.rb +99 -0
  81. data/test/unit/db_test.rb +96 -0
  82. data/test/unit/grid_test.rb +49 -0
  83. data/test/unit/pool_test.rb +9 -0
  84. data/test/unit/repl_set_connection_test.rb +72 -0
  85. data/test/unit/safe_test.rb +125 -0
  86. data/test/uri_test.rb +91 -0
  87. metadata +202 -0
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+
3
+ #
4
+ # --
5
+ # Copyright (C) 2008-2011 10gen Inc.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ # ++
19
+
20
+ module Mongo
21
+ # Generic Mongo Ruby Driver exception class.
22
+ class MongoRubyError < StandardError; end
23
+
24
+ # Raised when MongoDB itself has returned an error.
25
+ class MongoDBError < RuntimeError; end
26
+
27
+ # Raised when configuration options cause connections, queries, etc., to fail.
28
+ class ConfigurationError < MongoRubyError; end
29
+
30
+ # Raised on fatal errors to GridFS.
31
+ class GridError < MongoRubyError; end
32
+
33
+ # Raised on fatal errors to GridFS.
34
+ class GridFileNotFound < GridError; end
35
+
36
+ # Raised on fatal errors to GridFS.
37
+ class GridMD5Failure < GridError; end
38
+
39
+ # Raised when invalid arguments are sent to Mongo Ruby methods.
40
+ class MongoArgumentError < MongoRubyError; end
41
+
42
+ # Raised on failures in connection to the database server.
43
+ class ConnectionError < MongoRubyError; end
44
+
45
+ # Raised on failures in connection to the database server.
46
+ class ReplicaSetConnectionError < ConnectionError; end
47
+
48
+ # Raised on failures in connection to the database server.
49
+ class ConnectionTimeoutError < MongoRubyError; end
50
+
51
+ # Raised when a connection operation fails.
52
+ class ConnectionFailure < MongoDBError; end
53
+
54
+ # Raised when authentication fails.
55
+ class AuthenticationError < MongoDBError; end
56
+
57
+ # Raised when a database operation fails.
58
+ class OperationFailure < MongoDBError; end
59
+
60
+ # Raised when a client attempts to perform an invalid operation.
61
+ class InvalidOperation < MongoDBError; end
62
+
63
+ # Raised when an invalid collection or database name is used (invalid namespace name).
64
+ class InvalidNSName < RuntimeError; end
65
+
66
+ # Raised when the client supplies an invalid value to sort by.
67
+ class InvalidSortValueError < MongoRubyError; end
68
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: UTF-8
2
+
3
+ # --
4
+ # Copyright (C) 2008-2011 10gen Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # ++
18
+
19
+ module Mongo
20
+
21
+ # Implementation of the MongoDB GridFS specification. A file store.
22
+ class Grid
23
+ include GridExt::InstanceMethods
24
+
25
+ DEFAULT_FS_NAME = 'fs'
26
+
27
+ # Initialize a new Grid instance, consisting of a MongoDB database
28
+ # and a filesystem prefix if not using the default.
29
+ #
30
+ # @core gridfs
31
+ #
32
+ # @see GridFileSystem
33
+ def initialize(db, fs_name=DEFAULT_FS_NAME)
34
+ raise MongoArgumentError, "db must be a Mongo::DB." unless db.is_a?(Mongo::DB)
35
+
36
+ @db = db
37
+ @files = @db["#{fs_name}.files"]
38
+ @chunks = @db["#{fs_name}.chunks"]
39
+ @fs_name = fs_name
40
+
41
+ # Ensure indexes only if not connected to slave.
42
+ unless db.connection.slave_ok?
43
+ @chunks.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
44
+ end
45
+ end
46
+
47
+ # Store a file in the file store. This method is designed only for writing new files;
48
+ # if you need to update a given file, first delete it using Grid#delete.
49
+ #
50
+ # Note that arbitary metadata attributes can be saved to the file by passing
51
+ # them in as options.
52
+ #
53
+ # @param [String, #read] data a string or io-like object to store.
54
+ #
55
+ # @option opts [String] :filename (nil) a name for the file.
56
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
57
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
58
+ # the file to be use in lieu of an automatically generated one.
59
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
60
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
61
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
62
+ # @option opts [Integer] (262144) :chunk_size size of file chunks in bytes.
63
+ # @option opts [Boolean] :safe (false) When safe mode is enabled, the chunks sent to the server
64
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
65
+ #
66
+ # @return [Mongo::ObjectId] the file's id.
67
+ def put(data, opts={})
68
+ filename = opts.delete :filename
69
+ opts.merge!(default_grid_io_opts)
70
+ file = GridIO.new(@files, @chunks, filename, 'w', opts=opts)
71
+ file.write(data)
72
+ file.close
73
+ file.files_id
74
+ end
75
+
76
+ # Read a file from the file store.
77
+ #
78
+ # @param [] id the file's unique id.
79
+ #
80
+ # @return [Mongo::GridIO]
81
+ def get(id)
82
+ opts = {:query => {'_id' => id}}.merge!(default_grid_io_opts)
83
+ GridIO.new(@files, @chunks, nil, 'r', opts)
84
+ end
85
+
86
+ # Delete a file from the store.
87
+ #
88
+ # Note that deleting a GridFS file can result in read errors if another process
89
+ # is attempting to read a file while it's being deleted. While the odds for this
90
+ # kind of race condition are small, it's important to be aware of.
91
+ #
92
+ # @param [] id
93
+ #
94
+ # @return [Boolean]
95
+ def delete(id)
96
+ @files.remove({"_id" => id})
97
+ @chunks.remove({"files_id" => id})
98
+ end
99
+
100
+ private
101
+
102
+ def default_grid_io_opts
103
+ {:fs_name => @fs_name}
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: UTF-8
2
+
3
+ # --
4
+ # Copyright (C) 2008-2011 10gen Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # ++
18
+
19
+ module Mongo
20
+ module GridExt
21
+ module InstanceMethods
22
+
23
+ # Check the existence of a file matching the given query selector.
24
+ #
25
+ # Note that this method can be used with both the Grid and GridFileSystem classes. Also
26
+ # keep in mind that if you're going to be performing lots of existence checks, you should
27
+ # keep an instance of Grid or GridFileSystem handy rather than instantiating for each existence
28
+ # check. Alternatively, simply keep a reference to the proper files collection and query that
29
+ # as needed. That's exactly how this methods works.
30
+ #
31
+ # @param [Hash] selector a query selector.
32
+ #
33
+ # @example
34
+ #
35
+ # # Check for the existence of a given filename
36
+ # @grid = GridFileSystem.new(@db)
37
+ # @grid.exist?(:filename => 'foo.txt')
38
+ #
39
+ # # Check for existence filename and content type
40
+ # @grid = GridFileSystem.new(@db)
41
+ # @grid.exist?(:filename => 'foo.txt', :content_type => 'image/jpg')
42
+ #
43
+ # # Check for existence by _id
44
+ # @grid = Grid.new(@db)
45
+ # @grid.exist?(:_id => BSON::ObjectId.from_string('4bddcd24beffd95a7db9b8c8'))
46
+ #
47
+ # # Check for existence by an arbitrary attribute.
48
+ # @grid = Grid.new(@db)
49
+ # @grid.exist?(:tags => {'$in' => ['nature', 'zen', 'photography']})
50
+ #
51
+ # @return [nil, Hash] either nil for the file's metadata as a hash.
52
+ def exist?(selector)
53
+ @files.find_one(selector)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,145 @@
1
+ # encoding: UTF-8
2
+
3
+ # --
4
+ # Copyright (C) 2008-2011 10gen Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # ++
18
+
19
+ module Mongo
20
+
21
+ # A file store built on the GridFS specification featuring
22
+ # an API and behavior similar to that of a traditional file system.
23
+ class GridFileSystem
24
+ include GridExt::InstanceMethods
25
+
26
+ # Initialize a new GridFileSystem instance, consisting of a MongoDB database
27
+ # and a filesystem prefix if not using the default.
28
+ #
29
+ # @param [Mongo::DB] db a MongoDB database.
30
+ # @param [String] fs_name A name for the file system. The default name, based on
31
+ # the specification, is 'fs'.
32
+ def initialize(db, fs_name=Grid::DEFAULT_FS_NAME)
33
+ raise MongoArgumentError, "db must be a Mongo::DB." unless db.is_a?(Mongo::DB)
34
+
35
+ @db = db
36
+ @files = @db["#{fs_name}.files"]
37
+ @chunks = @db["#{fs_name}.chunks"]
38
+ @fs_name = fs_name
39
+
40
+ @default_query_opts = {:sort => [['filename', 1], ['uploadDate', -1]], :limit => 1}
41
+
42
+ # Ensure indexes only if not connected to slave.
43
+ unless db.connection.slave_ok?
44
+ @files.create_index([['filename', 1], ['uploadDate', -1]])
45
+ @chunks.create_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
46
+ end
47
+ end
48
+
49
+ # Open a file for reading or writing. Note that the options for this method only apply
50
+ # when opening in 'w' mode.
51
+ #
52
+ # Note that arbitary metadata attributes can be saved to the file by passing
53
+ # them is as options.
54
+ #
55
+ # @param [String] filename the name of the file.
56
+ # @param [String] mode either 'r' or 'w' for reading from
57
+ # or writing to the file.
58
+ # @param [Hash] opts see GridIO#new
59
+ #
60
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
61
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
62
+ # the file to be use in lieu of an automatically generated one.
63
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
64
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
65
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
66
+ # @option opts [Integer] (262144) :chunk_size size of file chunks in bytes.
67
+ # @option opts [Boolean] :delete_old (false) ensure that old versions of the file are deleted. This option
68
+ # only work in 'w' mode. Certain precautions must be taken when deleting GridFS files. See the notes under
69
+ # GridFileSystem#delete.
70
+ # @option opts [Boolean] :safe (false) When safe mode is enabled, the chunks sent to the server
71
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
72
+ #
73
+ # @example
74
+ #
75
+ # # Store the text "Hello, world!" in the grid file system.
76
+ # @grid = GridFileSystem.new(@db)
77
+ # @grid.open('filename', 'w') do |f|
78
+ # f.write "Hello, world!"
79
+ # end
80
+ #
81
+ # # Output "Hello, world!"
82
+ # @grid = GridFileSystem.new(@db)
83
+ # @grid.open('filename', 'r') do |f|
84
+ # puts f.read
85
+ # end
86
+ #
87
+ # # Write a file on disk to the GridFileSystem
88
+ # @file = File.open('image.jpg')
89
+ # @grid = GridFileSystem.new(@db)
90
+ # @grid.open('image.jpg, 'w') do |f|
91
+ # f.write @file
92
+ # end
93
+ #
94
+ # @return [Mongo::GridIO]
95
+ def open(filename, mode, opts={})
96
+ opts.merge!(default_grid_io_opts(filename))
97
+ del = opts.delete(:delete_old) && mode == 'w'
98
+ file = GridIO.new(@files, @chunks, filename, mode, opts)
99
+ return file unless block_given?
100
+ result = nil
101
+ begin
102
+ result = yield file
103
+ ensure
104
+ id = file.close
105
+ if del
106
+ self.delete do
107
+ @files.find({'filename' => filename, '_id' => {'$ne' => id}}, :fields => ['_id'])
108
+ end
109
+ end
110
+ end
111
+ result
112
+ end
113
+
114
+ # Delete the file with the given filename. Note that this will delete
115
+ # all versions of the file.
116
+ #
117
+ # Be careful with this. Deleting a GridFS file can result in read errors if another process
118
+ # is attempting to read a file while it's being deleted. While the odds for this
119
+ # kind of race condition are small, it's important to be aware of.
120
+ #
121
+ # @param [String] filename
122
+ #
123
+ # @yield [] pass a block that returns an array of documents to be deleted.
124
+ #
125
+ # @return [Boolean]
126
+ def delete(filename=nil)
127
+ if block_given?
128
+ files = yield
129
+ else
130
+ files = @files.find({'filename' => filename}, :fields => ['_id'])
131
+ end
132
+ files.each do |file|
133
+ @files.remove({'_id' => file['_id']})
134
+ @chunks.remove({'files_id' => file['_id']})
135
+ end
136
+ end
137
+ alias_method :unlink, :delete
138
+
139
+ private
140
+
141
+ def default_grid_io_opts(filename=nil)
142
+ {:fs_name => @fs_name, :query => {'filename' => filename}, :query_opts => @default_query_opts}
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,394 @@
1
+ # encoding: UTF-8
2
+
3
+ # --
4
+ # Copyright (C) 2008-2011 10gen Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ # ++
18
+
19
+ require 'digest/md5'
20
+ begin
21
+ require 'mime/types'
22
+ rescue LoadError
23
+ end
24
+
25
+ module Mongo
26
+
27
+ # GridIO objects represent files in the GridFS specification. This class
28
+ # manages the reading and writing of file chunks and metadata.
29
+ class GridIO
30
+ DEFAULT_CHUNK_SIZE = 256 * 1024
31
+ DEFAULT_CONTENT_TYPE = 'binary/octet-stream'
32
+ PROTECTED_ATTRS = [:files_id, :file_length, :client_md5, :server_md5]
33
+
34
+ attr_reader :content_type, :chunk_size, :upload_date, :files_id, :filename,
35
+ :metadata, :server_md5, :client_md5, :file_length, :file_position
36
+
37
+ # Create a new GridIO object. Note that most users will not need to use this class directly;
38
+ # the Grid and GridFileSystem classes will instantiate this class
39
+ #
40
+ # @param [Mongo::Collection] files a collection for storing file metadata.
41
+ # @param [Mongo::Collection] chunks a collection for storing file chunks.
42
+ # @param [String] filename the name of the file to open or write.
43
+ # @param [String] mode 'r' or 'w' or reading or creating a file.
44
+ #
45
+ # @option opts [Hash] :query a query selector used when opening the file in 'r' mode.
46
+ # @option opts [Hash] :query_opts any query options to be used when opening the file in 'r' mode.
47
+ # @option opts [String] :fs_name the file system prefix.
48
+ # @option opts [Integer] (262144) :chunk_size size of file chunks in bytes.
49
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
50
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
51
+ # the file to be use in lieu of an automatically generated one.
52
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
53
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
54
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
55
+ # @option opts [Boolean] :safe (false) When safe mode is enabled, the chunks sent to the server
56
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
57
+ def initialize(files, chunks, filename, mode, opts={})
58
+ @files = files
59
+ @chunks = chunks
60
+ @filename = filename
61
+ @mode = mode
62
+ @query = opts.delete(:query) || {}
63
+ @query_opts = opts.delete(:query_opts) || {}
64
+ @fs_name = opts.delete(:fs_name) || Grid::DEFAULT_FS_NAME
65
+ @safe = opts.delete(:safe) || false
66
+ @local_md5 = Digest::MD5.new if @safe
67
+ @custom_attrs = {}
68
+
69
+ case @mode
70
+ when 'r' then init_read
71
+ when 'w' then init_write(opts)
72
+ else
73
+ raise GridError, "Invalid file mode #{@mode}. Mode should be 'r' or 'w'."
74
+ end
75
+ end
76
+
77
+ def [](key)
78
+ @custom_attrs[key] || instance_variable_get("@#{key.to_s}")
79
+ end
80
+
81
+ def []=(key, value)
82
+ if PROTECTED_ATTRS.include?(key.to_sym)
83
+ warn "Attempting to overwrite protected value."
84
+ return nil
85
+ else
86
+ @custom_attrs[key] = value
87
+ end
88
+ end
89
+
90
+ # Read the data from the file. If a length if specified, will read from the
91
+ # current file position.
92
+ #
93
+ # @param [Integer] length
94
+ #
95
+ # @return [String]
96
+ # the data in the file
97
+ def read(length=nil)
98
+ return '' if @file_length.zero?
99
+ if length == 0
100
+ return ''
101
+ elsif length.nil? && @file_position.zero?
102
+ read_all
103
+ else
104
+ read_length(length)
105
+ end
106
+ end
107
+ alias_method :data, :read
108
+
109
+ # Write the given string (binary) data to the file.
110
+ #
111
+ # @param [String] string
112
+ # the data to write
113
+ #
114
+ # @return [Integer]
115
+ # the number of bytes written.
116
+ def write(io)
117
+ raise GridError, "file not opened for write" unless @mode[0] == ?w
118
+ if io.is_a? String
119
+ if @safe
120
+ @local_md5.update(io)
121
+ end
122
+ write_string(io)
123
+ else
124
+ length = 0
125
+ if @safe
126
+ while(string = io.read(@chunk_size))
127
+ @local_md5.update(string)
128
+ length += write_string(string)
129
+ end
130
+ else
131
+ while(string = io.read(@chunk_size))
132
+ length += write_string(string)
133
+ end
134
+ end
135
+ length
136
+ end
137
+ end
138
+
139
+ # Position the file pointer at the provided location.
140
+ #
141
+ # @param [Integer] pos
142
+ # the number of bytes to advance the file pointer. this can be a negative
143
+ # number.
144
+ # @param [Integer] whence
145
+ # one of IO::SEEK_CUR, IO::SEEK_END, or IO::SEEK_SET
146
+ #
147
+ # @return [Integer] the new file position
148
+ def seek(pos, whence=IO::SEEK_SET)
149
+ raise GridError, "Seek is only allowed in read mode." unless @mode == 'r'
150
+ target_pos = case whence
151
+ when IO::SEEK_CUR
152
+ @file_position + pos
153
+ when IO::SEEK_END
154
+ @file_length + pos
155
+ when IO::SEEK_SET
156
+ pos
157
+ end
158
+
159
+ new_chunk_number = (target_pos / @chunk_size).to_i
160
+ if new_chunk_number != @current_chunk['n']
161
+ save_chunk(@current_chunk) if @mode[0] == ?w
162
+ @current_chunk = get_chunk(new_chunk_number)
163
+ end
164
+ @file_position = target_pos
165
+ @chunk_position = @file_position % @chunk_size
166
+ @file_position
167
+ end
168
+
169
+ # The current position of the file.
170
+ #
171
+ # @return [Integer]
172
+ def tell
173
+ @file_position
174
+ end
175
+
176
+ # Creates or updates the document from the files collection that
177
+ # stores the chunks' metadata. The file becomes available only after
178
+ # this method has been called.
179
+ #
180
+ # This method will be invoked automatically when
181
+ # on GridIO#open is passed a block. Otherwise, it must be called manually.
182
+ #
183
+ # @return [BSON::ObjectId]
184
+ def close
185
+ if @mode[0] == ?w
186
+ if @current_chunk['n'].zero? && @chunk_position.zero?
187
+ warn "Warning: Storing a file with zero length."
188
+ end
189
+ @upload_date = Time.now.utc
190
+ id = @files.insert(to_mongo_object)
191
+ end
192
+ id
193
+ end
194
+
195
+ # Read a chunk of the data from the file and yield it to the given
196
+ # block.
197
+ #
198
+ # Note that this method reads from the current file position.
199
+ #
200
+ # @yield Yields on chunk per iteration as defined by this file's
201
+ # chunk size.
202
+ #
203
+ # @return [Mongo::GridIO] self
204
+ def each
205
+ return read_all unless block_given?
206
+ while chunk = read(chunk_size)
207
+ yield chunk
208
+ end
209
+ self
210
+ end
211
+
212
+ def inspect
213
+ "#<GridIO _id: #{@files_id}>"
214
+ end
215
+
216
+ private
217
+
218
+ def create_chunk(n)
219
+ chunk = BSON::OrderedHash.new
220
+ chunk['_id'] = BSON::ObjectId.new
221
+ chunk['n'] = n
222
+ chunk['files_id'] = @files_id
223
+ chunk['data'] = ''
224
+ @chunk_position = 0
225
+ chunk
226
+ end
227
+
228
+ def save_chunk(chunk)
229
+ @chunks.insert(chunk)
230
+ end
231
+
232
+ def get_chunk(n)
233
+ chunk = @chunks.find({'files_id' => @files_id, 'n' => n}).next_document
234
+ @chunk_position = 0
235
+ chunk
236
+ end
237
+
238
+ def last_chunk_number
239
+ (@file_length / @chunk_size).to_i
240
+ end
241
+
242
+ # Read a file in its entirety.
243
+ def read_all
244
+ buf = ''
245
+ if @current_chunk
246
+ buf << @current_chunk['data'].to_s
247
+ while chunk = get_chunk(@current_chunk['n'] + 1)
248
+ buf << chunk['data'].to_s
249
+ @current_chunk = chunk
250
+ end
251
+ end
252
+ @file_position = @file_length
253
+ buf
254
+ end
255
+
256
+ # Read a file incrementally.
257
+ def read_length(length)
258
+ cache_chunk_data
259
+ remaining = (@file_length - @file_position)
260
+ if length.nil?
261
+ to_read = remaining
262
+ else
263
+ to_read = length > remaining ? remaining : length
264
+ end
265
+ return nil unless remaining > 0
266
+
267
+ buf = ''
268
+ while to_read > 0
269
+ if @chunk_position == @chunk_data_length
270
+ @current_chunk = get_chunk(@current_chunk['n'] + 1)
271
+ cache_chunk_data
272
+ end
273
+ chunk_remainder = @chunk_data_length - @chunk_position
274
+ size = (to_read >= chunk_remainder) ? chunk_remainder : to_read
275
+ buf << @current_chunk_data[@chunk_position, size]
276
+ to_read -= size
277
+ @chunk_position += size
278
+ @file_position += size
279
+ end
280
+ buf
281
+ end
282
+
283
+ def cache_chunk_data
284
+ @current_chunk_data = @current_chunk['data'].to_s
285
+ if @current_chunk_data.respond_to?(:force_encoding)
286
+ @current_chunk_data.force_encoding("binary")
287
+ end
288
+ @chunk_data_length = @current_chunk['data'].length
289
+ end
290
+
291
+ def write_string(string)
292
+ # Since Ruby 1.9.1 doesn't necessarily store one character per byte.
293
+ if string.respond_to?(:force_encoding)
294
+ string.force_encoding("binary")
295
+ end
296
+
297
+ to_write = string.length
298
+ while (to_write > 0) do
299
+ if @current_chunk && @chunk_position == @chunk_size
300
+ next_chunk_number = @current_chunk['n'] + 1
301
+ @current_chunk = create_chunk(next_chunk_number)
302
+ end
303
+ chunk_available = @chunk_size - @chunk_position
304
+ step_size = (to_write > chunk_available) ? chunk_available : to_write
305
+ @current_chunk['data'] = BSON::Binary.new((@current_chunk['data'].to_s << string[-to_write, step_size]).unpack("c*"))
306
+ @chunk_position += step_size
307
+ to_write -= step_size
308
+ save_chunk(@current_chunk)
309
+ end
310
+ string.length - to_write
311
+ end
312
+
313
+ # Initialize the class for reading a file.
314
+ def init_read
315
+ doc = @files.find(@query, @query_opts).next_document
316
+ raise GridFileNotFound, "Could not open file matching #{@query.inspect} #{@query_opts.inspect}" unless doc
317
+
318
+ @files_id = doc['_id']
319
+ @content_type = doc['contentType']
320
+ @chunk_size = doc['chunkSize']
321
+ @upload_date = doc['uploadDate']
322
+ @aliases = doc['aliases']
323
+ @file_length = doc['length']
324
+ @metadata = doc['metadata']
325
+ @md5 = doc['md5']
326
+ @filename = doc['filename']
327
+ @custom_attrs = doc
328
+
329
+ @current_chunk = get_chunk(0)
330
+ @file_position = 0
331
+ end
332
+
333
+ # Initialize the class for writing a file.
334
+ def init_write(opts)
335
+ @files_id = opts.delete(:_id) || BSON::ObjectId.new
336
+ @content_type = opts.delete(:content_type) || (defined? MIME) && get_content_type || DEFAULT_CONTENT_TYPE
337
+ @chunk_size = opts.delete(:chunk_size) || DEFAULT_CHUNK_SIZE
338
+ @metadata = opts.delete(:metadata) if opts[:metadata]
339
+ @aliases = opts.delete(:aliases) if opts[:aliases]
340
+ @file_length = 0
341
+ opts.each {|k, v| self[k] = v}
342
+ check_existing_file if @safe
343
+
344
+ @current_chunk = create_chunk(0)
345
+ @file_position = 0
346
+ end
347
+
348
+ def check_existing_file
349
+ if @files.find_one('_id' => @files_id)
350
+ raise GridError, "Attempting to overwrite with Grid#put. You must delete the file first."
351
+ end
352
+ end
353
+
354
+ def to_mongo_object
355
+ h = BSON::OrderedHash.new
356
+ h['_id'] = @files_id
357
+ h['filename'] = @filename if @filename
358
+ h['contentType'] = @content_type
359
+ h['length'] = @current_chunk ? @current_chunk['n'] * @chunk_size + @chunk_position : 0
360
+ h['chunkSize'] = @chunk_size
361
+ h['uploadDate'] = @upload_date
362
+ h['aliases'] = @aliases if @aliases
363
+ h['metadata'] = @metadata if @metadata
364
+ h['md5'] = get_md5
365
+ h.merge!(@custom_attrs)
366
+ h
367
+ end
368
+
369
+ # Get a server-side md5 and validate against the client if running in safe mode.
370
+ def get_md5
371
+ md5_command = BSON::OrderedHash.new
372
+ md5_command['filemd5'] = @files_id
373
+ md5_command['root'] = @fs_name
374
+ @server_md5 = @files.db.command(md5_command)['md5']
375
+ if @safe
376
+ @client_md5 = @local_md5.hexdigest
377
+ if @local_md5 != @server_md5
378
+ raise GridMD5Failure, "File on server failed MD5 check"
379
+ end
380
+ else
381
+ @server_md5
382
+ end
383
+ end
384
+
385
+ # Determine the content type based on the filename.
386
+ def get_content_type
387
+ if @filename
388
+ if types = MIME::Types.type_for(@filename)
389
+ types.first.simplified unless types.empty?
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end