jonbell-mongo 1.3.1.2

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 (88) hide show
  1. data/LICENSE.txt +190 -0
  2. data/README.md +333 -0
  3. data/Rakefile +215 -0
  4. data/bin/mongo_console +21 -0
  5. data/docs/CREDITS.md +123 -0
  6. data/docs/FAQ.md +116 -0
  7. data/docs/GridFS.md +158 -0
  8. data/docs/HISTORY.md +263 -0
  9. data/docs/RELEASES.md +33 -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 +97 -0
  14. data/lib/mongo/collection.rb +895 -0
  15. data/lib/mongo/connection.rb +926 -0
  16. data/lib/mongo/cursor.rb +474 -0
  17. data/lib/mongo/db.rb +617 -0
  18. data/lib/mongo/exceptions.rb +71 -0
  19. data/lib/mongo/gridfs/grid.rb +107 -0
  20. data/lib/mongo/gridfs/grid_ext.rb +57 -0
  21. data/lib/mongo/gridfs/grid_file_system.rb +146 -0
  22. data/lib/mongo/gridfs/grid_io.rb +485 -0
  23. data/lib/mongo/gridfs/grid_io_fix.rb +38 -0
  24. data/lib/mongo/repl_set_connection.rb +356 -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 +177 -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 +185 -0
  31. data/mongo.gemspec +34 -0
  32. data/test/auxillary/1.4_features.rb +166 -0
  33. data/test/auxillary/authentication_test.rb +68 -0
  34. data/test/auxillary/autoreconnect_test.rb +41 -0
  35. data/test/auxillary/fork_test.rb +30 -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 +654 -0
  41. data/test/bson/byte_buffer_test.rb +208 -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 +210 -0
  46. data/test/bson/timestamp_test.rb +24 -0
  47. data/test/collection_test.rb +910 -0
  48. data/test/connection_test.rb +324 -0
  49. data/test/conversions_test.rb +119 -0
  50. data/test/cursor_fail_test.rb +75 -0
  51. data/test/cursor_message_test.rb +43 -0
  52. data/test/cursor_test.rb +483 -0
  53. data/test/db_api_test.rb +738 -0
  54. data/test/db_connection_test.rb +15 -0
  55. data/test/db_test.rb +315 -0
  56. data/test/grid_file_system_test.rb +259 -0
  57. data/test/grid_io_test.rb +209 -0
  58. data/test/grid_test.rb +258 -0
  59. data/test/load/thin/load.rb +24 -0
  60. data/test/load/unicorn/load.rb +23 -0
  61. data/test/replica_sets/connect_test.rb +112 -0
  62. data/test/replica_sets/connection_string_test.rb +32 -0
  63. data/test/replica_sets/count_test.rb +35 -0
  64. data/test/replica_sets/insert_test.rb +53 -0
  65. data/test/replica_sets/pooled_insert_test.rb +55 -0
  66. data/test/replica_sets/query_secondaries.rb +108 -0
  67. data/test/replica_sets/query_test.rb +51 -0
  68. data/test/replica_sets/replication_ack_test.rb +66 -0
  69. data/test/replica_sets/rs_test_helper.rb +27 -0
  70. data/test/safe_test.rb +68 -0
  71. data/test/support/hash_with_indifferent_access.rb +186 -0
  72. data/test/support/keys.rb +45 -0
  73. data/test/support_test.rb +18 -0
  74. data/test/test_helper.rb +102 -0
  75. data/test/threading/threading_with_large_pool_test.rb +90 -0
  76. data/test/threading_test.rb +87 -0
  77. data/test/tools/auth_repl_set_manager.rb +14 -0
  78. data/test/tools/repl_set_manager.rb +266 -0
  79. data/test/unit/collection_test.rb +130 -0
  80. data/test/unit/connection_test.rb +85 -0
  81. data/test/unit/cursor_test.rb +109 -0
  82. data/test/unit/db_test.rb +94 -0
  83. data/test/unit/grid_test.rb +49 -0
  84. data/test/unit/pool_test.rb +9 -0
  85. data/test/unit/repl_set_connection_test.rb +59 -0
  86. data/test/unit/safe_test.rb +125 -0
  87. data/test/uri_test.rb +91 -0
  88. metadata +224 -0
@@ -0,0 +1,71 @@
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 socket read operation times out.
61
+ class OperationTimeout < ::Timeout::Error; end
62
+
63
+ # Raised when a client attempts to perform an invalid operation.
64
+ class InvalidOperation < MongoDBError; end
65
+
66
+ # Raised when an invalid collection or database name is used (invalid namespace name).
67
+ class InvalidNSName < RuntimeError; end
68
+
69
+ # Raised when the client supplies an invalid value to sort by.
70
+ class InvalidSortValueError < MongoRubyError; end
71
+ end
@@ -0,0 +1,107 @@
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
+ opts = opts.dup
69
+ filename = opts[:filename]
70
+ opts.merge!(default_grid_io_opts)
71
+ file = GridIO.new(@files, @chunks, filename, 'w', opts=opts)
72
+ file.write(data)
73
+ file.close
74
+ file.files_id
75
+ end
76
+
77
+ # Read a file from the file store.
78
+ #
79
+ # @param [] id the file's unique id.
80
+ #
81
+ # @return [Mongo::GridIO]
82
+ def get(id)
83
+ opts = {:query => {'_id' => id}}.merge!(default_grid_io_opts)
84
+ GridIO.new(@files, @chunks, nil, 'r', opts)
85
+ end
86
+
87
+ # Delete a file from the store.
88
+ #
89
+ # Note that deleting a GridFS file can result in read errors if another process
90
+ # is attempting to read a file while it's being deleted. While the odds for this
91
+ # kind of race condition are small, it's important to be aware of.
92
+ #
93
+ # @param [] id
94
+ #
95
+ # @return [Boolean]
96
+ def delete(id)
97
+ @files.remove({"_id" => id})
98
+ @chunks.remove({"files_id" => id})
99
+ end
100
+
101
+ private
102
+
103
+ def default_grid_io_opts
104
+ {:fs_name => @fs_name}
105
+ end
106
+ end
107
+ 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,146 @@
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 = opts.dup
97
+ opts.merge!(default_grid_io_opts(filename))
98
+ del = opts.delete(:delete_old) && mode == 'w'
99
+ file = GridIO.new(@files, @chunks, filename, mode, opts)
100
+ return file unless block_given?
101
+ result = nil
102
+ begin
103
+ result = yield file
104
+ ensure
105
+ id = file.close
106
+ if del
107
+ self.delete do
108
+ @files.find({'filename' => filename, '_id' => {'$ne' => id}}, :fields => ['_id'])
109
+ end
110
+ end
111
+ end
112
+ result
113
+ end
114
+
115
+ # Delete the file with the given filename. Note that this will delete
116
+ # all versions of the file.
117
+ #
118
+ # Be careful with this. Deleting a GridFS file can result in read errors if another process
119
+ # is attempting to read a file while it's being deleted. While the odds for this
120
+ # kind of race condition are small, it's important to be aware of.
121
+ #
122
+ # @param [String] filename
123
+ #
124
+ # @yield [] pass a block that returns an array of documents to be deleted.
125
+ #
126
+ # @return [Boolean]
127
+ def delete(filename=nil)
128
+ if block_given?
129
+ files = yield
130
+ else
131
+ files = @files.find({'filename' => filename}, :fields => ['_id'])
132
+ end
133
+ files.each do |file|
134
+ @files.remove({'_id' => file['_id']})
135
+ @chunks.remove({'files_id' => file['_id']})
136
+ end
137
+ end
138
+ alias_method :unlink, :delete
139
+
140
+ private
141
+
142
+ def default_grid_io_opts(filename=nil)
143
+ {:fs_name => @fs_name, :query => {'filename' => filename}, :query_opts => @default_query_opts}
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,485 @@
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
+
21
+ module Mongo
22
+
23
+ # GridIO objects represent files in the GridFS specification. This class
24
+ # manages the reading and writing of file chunks and metadata.
25
+ class GridIO
26
+ DEFAULT_CHUNK_SIZE = 256 * 1024
27
+ DEFAULT_CONTENT_TYPE = 'binary/octet-stream'
28
+ PROTECTED_ATTRS = [:files_id, :file_length, :client_md5, :server_md5]
29
+
30
+ attr_reader :content_type, :chunk_size, :upload_date, :files_id, :filename,
31
+ :metadata, :server_md5, :client_md5, :file_length, :file_position
32
+
33
+ # Create a new GridIO object. Note that most users will not need to use this class directly;
34
+ # the Grid and GridFileSystem classes will instantiate this class
35
+ #
36
+ # @param [Mongo::Collection] files a collection for storing file metadata.
37
+ # @param [Mongo::Collection] chunks a collection for storing file chunks.
38
+ # @param [String] filename the name of the file to open or write.
39
+ # @param [String] mode 'r' or 'w' or reading or creating a file.
40
+ #
41
+ # @option opts [Hash] :query a query selector used when opening the file in 'r' mode.
42
+ # @option opts [Hash] :query_opts any query options to be used when opening the file in 'r' mode.
43
+ # @option opts [String] :fs_name the file system prefix.
44
+ # @option opts [Integer] (262144) :chunk_size size of file chunks in bytes.
45
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
46
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
47
+ # the file to be use in lieu of an automatically generated one.
48
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
49
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
50
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
51
+ # @option opts [Boolean] :safe (false) When safe mode is enabled, the chunks sent to the server
52
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
53
+ def initialize(files, chunks, filename, mode, opts={})
54
+ @files = files
55
+ @chunks = chunks
56
+ @filename = filename
57
+ @mode = mode
58
+ opts = opts.dup
59
+ @query = opts.delete(:query) || {}
60
+ @query_opts = opts.delete(:query_opts) || {}
61
+ @fs_name = opts.delete(:fs_name) || Grid::DEFAULT_FS_NAME
62
+ @safe = opts.delete(:safe) || false
63
+ @local_md5 = Digest::MD5.new if @safe
64
+ @custom_attrs = {}
65
+
66
+ case @mode
67
+ when 'r' then init_read
68
+ when 'w' then init_write(opts)
69
+ else
70
+ raise GridError, "Invalid file mode #{@mode}. Mode should be 'r' or 'w'."
71
+ end
72
+ end
73
+
74
+ def [](key)
75
+ @custom_attrs[key] || instance_variable_get("@#{key.to_s}")
76
+ end
77
+
78
+ def []=(key, value)
79
+ if PROTECTED_ATTRS.include?(key.to_sym)
80
+ warn "Attempting to overwrite protected value."
81
+ return nil
82
+ else
83
+ @custom_attrs[key] = value
84
+ end
85
+ end
86
+
87
+ # Read the data from the file. If a length if specified, will read from the
88
+ # current file position.
89
+ #
90
+ # @param [Integer] length
91
+ #
92
+ # @return [String]
93
+ # the data in the file
94
+ def read(length=nil)
95
+ return '' if @file_length.zero?
96
+ if length == 0
97
+ return ''
98
+ elsif length.nil? && @file_position.zero?
99
+ read_all
100
+ else
101
+ read_length(length)
102
+ end
103
+ end
104
+ alias_method :data, :read
105
+
106
+ # Write the given string (binary) data to the file.
107
+ #
108
+ # @param [String] string
109
+ # the data to write
110
+ #
111
+ # @return [Integer]
112
+ # the number of bytes written.
113
+ def write(io)
114
+ raise GridError, "file not opened for write" unless @mode[0] == ?w
115
+ if io.is_a? String
116
+ if @safe
117
+ @local_md5.update(io)
118
+ end
119
+ write_string(io)
120
+ else
121
+ length = 0
122
+ if @safe
123
+ while(string = io.read(@chunk_size))
124
+ @local_md5.update(string)
125
+ length += write_string(string)
126
+ end
127
+ else
128
+ while(string = io.read(@chunk_size))
129
+ length += write_string(string)
130
+ end
131
+ end
132
+ length
133
+ end
134
+ end
135
+
136
+ # Position the file pointer at the provided location.
137
+ #
138
+ # @param [Integer] pos
139
+ # the number of bytes to advance the file pointer. this can be a negative
140
+ # number.
141
+ # @param [Integer] whence
142
+ # one of IO::SEEK_CUR, IO::SEEK_END, or IO::SEEK_SET
143
+ #
144
+ # @return [Integer] the new file position
145
+ def seek(pos, whence=IO::SEEK_SET)
146
+ raise GridError, "Seek is only allowed in read mode." unless @mode == 'r'
147
+ target_pos = case whence
148
+ when IO::SEEK_CUR
149
+ @file_position + pos
150
+ when IO::SEEK_END
151
+ @file_length + pos
152
+ when IO::SEEK_SET
153
+ pos
154
+ end
155
+
156
+ new_chunk_number = (target_pos / @chunk_size).to_i
157
+ if new_chunk_number != @current_chunk['n']
158
+ save_chunk(@current_chunk) if @mode[0] == ?w
159
+ @current_chunk = get_chunk(new_chunk_number)
160
+ end
161
+ @file_position = target_pos
162
+ @chunk_position = @file_position % @chunk_size
163
+ @file_position
164
+ end
165
+
166
+ # The current position of the file.
167
+ #
168
+ # @return [Integer]
169
+ def tell
170
+ @file_position
171
+ end
172
+ alias :pos :tell
173
+
174
+ # Rewind the file. This is equivalent to seeking to the zeroth position.
175
+ #
176
+ # @return [Integer] the position of the file after rewinding (always zero).
177
+ def rewind
178
+ raise GridError, "file not opened for read" unless @mode[0] == ?r
179
+ seek(0)
180
+ end
181
+
182
+ # Return a boolean indicating whether the position pointer is
183
+ # at the end of the file.
184
+ #
185
+ # @return [Boolean]
186
+ def eof
187
+ raise GridError, "file not opened for read #{@mode}" unless @mode[0] == ?r
188
+ @file_position >= @file_length
189
+ end
190
+ alias :eof? :eof
191
+
192
+ # Return the next line from a GridFS file. This probably
193
+ # makes sense only if you're storing plain text. This method
194
+ # has a somewhat tricky API, which it inherits from Ruby's
195
+ # StringIO#gets.
196
+ #
197
+ # @param [String, Integer] separator or length. If a separator,
198
+ # read up to the separator. If a length, read the +length+ number
199
+ # of bytes. If nil, read the entire file.
200
+ # @param [Integer] length If a separator is provided, then
201
+ # read until either finding the separator or
202
+ # passing over the +length+ number of bytes.
203
+ #
204
+ # @return [String]
205
+ def gets(separator="\n", length=nil)
206
+ if separator.nil?
207
+ read_all
208
+ elsif separator.is_a?(Integer)
209
+ read_length(separator)
210
+ elsif separator.length > 1
211
+ read_to_string(separator, length)
212
+ else
213
+ read_to_character(separator, length)
214
+ end
215
+ end
216
+
217
+ # Return the next byte from the GridFS file.
218
+ #
219
+ # @return [String]
220
+ def getc
221
+ read_length(1)
222
+ end
223
+
224
+ # Creates or updates the document from the files collection that
225
+ # stores the chunks' metadata. The file becomes available only after
226
+ # this method has been called.
227
+ #
228
+ # This method will be invoked automatically when
229
+ # on GridIO#open is passed a block. Otherwise, it must be called manually.
230
+ #
231
+ # @return [BSON::ObjectId]
232
+ def close
233
+ if @mode[0] == ?w
234
+ if @current_chunk['n'].zero? && @chunk_position.zero?
235
+ warn "Warning: Storing a file with zero length."
236
+ end
237
+ @upload_date = Time.now.utc
238
+ id = @files.insert(to_mongo_object)
239
+ end
240
+ id
241
+ end
242
+
243
+ # Read a chunk of the data from the file and yield it to the given
244
+ # block.
245
+ #
246
+ # Note that this method reads from the current file position.
247
+ #
248
+ # @yield Yields on chunk per iteration as defined by this file's
249
+ # chunk size.
250
+ #
251
+ # @return [Mongo::GridIO] self
252
+ def each
253
+ return read_all unless block_given?
254
+ while chunk = read(chunk_size)
255
+ yield chunk
256
+ break if chunk.empty?
257
+ end
258
+ self
259
+ end
260
+
261
+ def inspect
262
+ "#<GridIO _id: #{@files_id}>"
263
+ end
264
+
265
+ private
266
+
267
+ def create_chunk(n)
268
+ chunk = BSON::OrderedHash.new
269
+ chunk['_id'] = BSON::ObjectId.new
270
+ chunk['n'] = n
271
+ chunk['files_id'] = @files_id
272
+ chunk['data'] = ''
273
+ @chunk_position = 0
274
+ chunk
275
+ end
276
+
277
+ def save_chunk(chunk)
278
+ @chunks.insert(chunk)
279
+ end
280
+
281
+ def get_chunk(n)
282
+ chunk = @chunks.find({'files_id' => @files_id, 'n' => n}).next_document
283
+ @chunk_position = 0
284
+ chunk
285
+ end
286
+
287
+ # Read a file in its entirety.
288
+ def read_all
289
+ buf = ''
290
+ if @current_chunk
291
+ buf << @current_chunk['data'].to_s
292
+ while buf.size < @file_length
293
+ @current_chunk = get_chunk(@current_chunk['n'] + 1)
294
+ break if @current_chunk.nil?
295
+ buf << @current_chunk['data'].to_s
296
+ end
297
+ @file_position = @file_length
298
+ end
299
+ buf
300
+ end
301
+
302
+ # Read a file incrementally.
303
+ def read_length(length)
304
+ cache_chunk_data
305
+ remaining = (@file_length - @file_position)
306
+ if length.nil?
307
+ to_read = remaining
308
+ else
309
+ to_read = length > remaining ? remaining : length
310
+ end
311
+ return nil unless remaining > 0
312
+
313
+ buf = ''
314
+ while to_read > 0
315
+ if @chunk_position == @chunk_data_length
316
+ @current_chunk = get_chunk(@current_chunk['n'] + 1)
317
+ cache_chunk_data
318
+ end
319
+ chunk_remainder = @chunk_data_length - @chunk_position
320
+ size = (to_read >= chunk_remainder) ? chunk_remainder : to_read
321
+ buf << @current_chunk_data[@chunk_position, size]
322
+ to_read -= size
323
+ @chunk_position += size
324
+ @file_position += size
325
+ end
326
+ buf
327
+ end
328
+
329
+ def read_to_character(character="\n", length=nil)
330
+ result = ''
331
+ len = 0
332
+ while char = getc
333
+ result << char
334
+ len += 1
335
+ break if char == character || (length ? len >= length : false)
336
+ end
337
+ result.length > 0 ? result : nil
338
+ end
339
+
340
+ def read_to_string(string="\n", length=nil)
341
+ result = ''
342
+ len = 0
343
+ match_idx = 0
344
+ match_num = string.length - 1
345
+ to_match = string[match_idx].chr
346
+ if length
347
+ matcher = lambda {|idx, num| idx < num && len < length }
348
+ else
349
+ matcher = lambda {|idx, num| idx < num}
350
+ end
351
+ while matcher.call(match_idx, match_num) && char = getc
352
+ result << char
353
+ len += 1
354
+ if char == to_match
355
+ while match_idx < match_num do
356
+ match_idx += 1
357
+ to_match = string[match_idx].chr
358
+ char = getc
359
+ result << char
360
+ if char != to_match
361
+ match_idx = 0
362
+ to_match = string[match_idx].chr
363
+ break
364
+ end
365
+ end
366
+ end
367
+ end
368
+ result.length > 0 ? result : nil
369
+ end
370
+
371
+ def cache_chunk_data
372
+ @current_chunk_data = @current_chunk['data'].to_s
373
+ if @current_chunk_data.respond_to?(:force_encoding)
374
+ @current_chunk_data.force_encoding("binary")
375
+ end
376
+ @chunk_data_length = @current_chunk['data'].length
377
+ end
378
+
379
+ def write_string(string)
380
+ # Since Ruby 1.9.1 doesn't necessarily store one character per byte.
381
+ if string.respond_to?(:force_encoding)
382
+ string.force_encoding("binary")
383
+ end
384
+
385
+ to_write = string.length
386
+ while (to_write > 0) do
387
+ if @current_chunk && @chunk_position == @chunk_size
388
+ next_chunk_number = @current_chunk['n'] + 1
389
+ @current_chunk = create_chunk(next_chunk_number)
390
+ end
391
+ chunk_available = @chunk_size - @chunk_position
392
+ step_size = (to_write > chunk_available) ? chunk_available : to_write
393
+ @current_chunk['data'] = BSON::Binary.new((@current_chunk['data'].to_s << string[-to_write, step_size]).unpack("c*"))
394
+ @chunk_position += step_size
395
+ to_write -= step_size
396
+ save_chunk(@current_chunk)
397
+ end
398
+ string.length - to_write
399
+ end
400
+
401
+ # Initialize the class for reading a file.
402
+ def init_read
403
+ doc = @files.find(@query, @query_opts).next_document
404
+ raise GridFileNotFound, "Could not open file matching #{@query.inspect} #{@query_opts.inspect}" unless doc
405
+
406
+ @files_id = doc['_id']
407
+ @content_type = doc['contentType']
408
+ @chunk_size = doc['chunkSize']
409
+ @upload_date = doc['uploadDate']
410
+ @aliases = doc['aliases']
411
+ @file_length = doc['length']
412
+ @metadata = doc['metadata']
413
+ @md5 = doc['md5']
414
+ @filename = doc['filename']
415
+ @custom_attrs = doc
416
+
417
+ @current_chunk = get_chunk(0)
418
+ @file_position = 0
419
+ end
420
+
421
+ # Initialize the class for writing a file.
422
+ def init_write(opts)
423
+ opts = opts.dup
424
+ @files_id = opts.delete(:_id) || BSON::ObjectId.new
425
+ @content_type = opts.delete(:content_type) || (defined? MIME) && get_content_type || DEFAULT_CONTENT_TYPE
426
+ @chunk_size = opts.delete(:chunk_size) || DEFAULT_CHUNK_SIZE
427
+ @metadata = opts.delete(:metadata)
428
+ @aliases = opts.delete(:aliases)
429
+ @file_length = 0
430
+ opts.each {|k, v| self[k] = v}
431
+ check_existing_file if @safe
432
+
433
+ @current_chunk = create_chunk(0)
434
+ @file_position = 0
435
+ end
436
+
437
+ def check_existing_file
438
+ if @files.find_one('_id' => @files_id)
439
+ raise GridError, "Attempting to overwrite with Grid#put. You must delete the file first."
440
+ end
441
+ end
442
+
443
+ def to_mongo_object
444
+ h = BSON::OrderedHash.new
445
+ h['_id'] = @files_id
446
+ h['filename'] = @filename if @filename
447
+ h['contentType'] = @content_type
448
+ h['length'] = @current_chunk ? @current_chunk['n'] * @chunk_size + @chunk_position : 0
449
+ h['chunkSize'] = @chunk_size
450
+ h['uploadDate'] = @upload_date
451
+ h['aliases'] = @aliases if @aliases
452
+ h['metadata'] = @metadata if @metadata
453
+ h['md5'] = get_md5
454
+ h.merge!(@custom_attrs)
455
+ h
456
+ end
457
+
458
+ # Get a server-side md5 and validate against the client if running in safe mode.
459
+ def get_md5
460
+ md5_command = BSON::OrderedHash.new
461
+ md5_command['filemd5'] = @files_id
462
+ md5_command['root'] = @fs_name
463
+ @server_md5 = @files.db.command(md5_command)['md5']
464
+ if @safe
465
+ @client_md5 = @local_md5.hexdigest
466
+ if @local_md5 == @server_md5
467
+ @server_md5
468
+ else
469
+ raise GridMD5Failure, "File on server failed MD5 check"
470
+ end
471
+ else
472
+ @server_md5
473
+ end
474
+ end
475
+
476
+ # Determine the content type based on the filename.
477
+ def get_content_type
478
+ if @filename
479
+ if types = MIME::Types.type_for(@filename)
480
+ types.first.simplified unless types.empty?
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end