jonbell-mongo 1.3.1.2

Sign up to get free protection for your applications and to get access to all the features.
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