mongo 1.10.0-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/LICENSE +190 -0
  5. data/README.md +149 -0
  6. data/Rakefile +31 -0
  7. data/VERSION +1 -0
  8. data/bin/mongo_console +43 -0
  9. data/ext/jsasl/target/jsasl.jar +0 -0
  10. data/lib/mongo.rb +90 -0
  11. data/lib/mongo/bulk_write_collection_view.rb +380 -0
  12. data/lib/mongo/collection.rb +1164 -0
  13. data/lib/mongo/collection_writer.rb +364 -0
  14. data/lib/mongo/connection.rb +19 -0
  15. data/lib/mongo/connection/node.rb +239 -0
  16. data/lib/mongo/connection/pool.rb +347 -0
  17. data/lib/mongo/connection/pool_manager.rb +325 -0
  18. data/lib/mongo/connection/sharding_pool_manager.rb +67 -0
  19. data/lib/mongo/connection/socket.rb +18 -0
  20. data/lib/mongo/connection/socket/socket_util.rb +37 -0
  21. data/lib/mongo/connection/socket/ssl_socket.rb +95 -0
  22. data/lib/mongo/connection/socket/tcp_socket.rb +86 -0
  23. data/lib/mongo/connection/socket/unix_socket.rb +39 -0
  24. data/lib/mongo/cursor.rb +719 -0
  25. data/lib/mongo/db.rb +735 -0
  26. data/lib/mongo/exception.rb +88 -0
  27. data/lib/mongo/functional.rb +21 -0
  28. data/lib/mongo/functional/authentication.rb +318 -0
  29. data/lib/mongo/functional/logging.rb +85 -0
  30. data/lib/mongo/functional/read_preference.rb +174 -0
  31. data/lib/mongo/functional/sasl_java.rb +48 -0
  32. data/lib/mongo/functional/uri_parser.rb +374 -0
  33. data/lib/mongo/functional/write_concern.rb +66 -0
  34. data/lib/mongo/gridfs.rb +18 -0
  35. data/lib/mongo/gridfs/grid.rb +112 -0
  36. data/lib/mongo/gridfs/grid_ext.rb +53 -0
  37. data/lib/mongo/gridfs/grid_file_system.rb +163 -0
  38. data/lib/mongo/gridfs/grid_io.rb +484 -0
  39. data/lib/mongo/legacy.rb +140 -0
  40. data/lib/mongo/mongo_client.rb +702 -0
  41. data/lib/mongo/mongo_replica_set_client.rb +523 -0
  42. data/lib/mongo/mongo_sharded_client.rb +159 -0
  43. data/lib/mongo/networking.rb +370 -0
  44. data/lib/mongo/utils.rb +19 -0
  45. data/lib/mongo/utils/conversions.rb +110 -0
  46. data/lib/mongo/utils/core_ext.rb +70 -0
  47. data/lib/mongo/utils/server_version.rb +69 -0
  48. data/lib/mongo/utils/support.rb +80 -0
  49. data/lib/mongo/utils/thread_local_variable_manager.rb +25 -0
  50. data/mongo.gemspec +36 -0
  51. data/test/functional/authentication_test.rb +35 -0
  52. data/test/functional/bulk_api_stress_test.rb +133 -0
  53. data/test/functional/bulk_write_collection_view_test.rb +1129 -0
  54. data/test/functional/client_test.rb +565 -0
  55. data/test/functional/collection_test.rb +2073 -0
  56. data/test/functional/collection_writer_test.rb +83 -0
  57. data/test/functional/conversions_test.rb +163 -0
  58. data/test/functional/cursor_fail_test.rb +63 -0
  59. data/test/functional/cursor_message_test.rb +57 -0
  60. data/test/functional/cursor_test.rb +625 -0
  61. data/test/functional/db_api_test.rb +819 -0
  62. data/test/functional/db_connection_test.rb +27 -0
  63. data/test/functional/db_test.rb +344 -0
  64. data/test/functional/grid_file_system_test.rb +285 -0
  65. data/test/functional/grid_io_test.rb +252 -0
  66. data/test/functional/grid_test.rb +273 -0
  67. data/test/functional/pool_test.rb +62 -0
  68. data/test/functional/safe_test.rb +98 -0
  69. data/test/functional/ssl_test.rb +29 -0
  70. data/test/functional/support_test.rb +62 -0
  71. data/test/functional/timeout_test.rb +58 -0
  72. data/test/functional/uri_test.rb +330 -0
  73. data/test/functional/write_concern_test.rb +118 -0
  74. data/test/helpers/general.rb +50 -0
  75. data/test/helpers/test_unit.rb +317 -0
  76. data/test/replica_set/authentication_test.rb +35 -0
  77. data/test/replica_set/basic_test.rb +174 -0
  78. data/test/replica_set/client_test.rb +341 -0
  79. data/test/replica_set/complex_connect_test.rb +77 -0
  80. data/test/replica_set/connection_test.rb +138 -0
  81. data/test/replica_set/count_test.rb +64 -0
  82. data/test/replica_set/cursor_test.rb +212 -0
  83. data/test/replica_set/insert_test.rb +140 -0
  84. data/test/replica_set/max_values_test.rb +145 -0
  85. data/test/replica_set/pinning_test.rb +55 -0
  86. data/test/replica_set/query_test.rb +73 -0
  87. data/test/replica_set/read_preference_test.rb +214 -0
  88. data/test/replica_set/refresh_test.rb +175 -0
  89. data/test/replica_set/replication_ack_test.rb +94 -0
  90. data/test/replica_set/ssl_test.rb +32 -0
  91. data/test/sharded_cluster/basic_test.rb +197 -0
  92. data/test/shared/authentication/basic_auth_shared.rb +286 -0
  93. data/test/shared/authentication/bulk_api_auth_shared.rb +259 -0
  94. data/test/shared/authentication/gssapi_shared.rb +164 -0
  95. data/test/shared/authentication/sasl_plain_shared.rb +96 -0
  96. data/test/shared/ssl_shared.rb +235 -0
  97. data/test/test_helper.rb +56 -0
  98. data/test/threading/basic_test.rb +120 -0
  99. data/test/tools/mongo_config.rb +608 -0
  100. data/test/tools/mongo_config_test.rb +160 -0
  101. data/test/unit/client_test.rb +347 -0
  102. data/test/unit/collection_test.rb +166 -0
  103. data/test/unit/connection_test.rb +325 -0
  104. data/test/unit/cursor_test.rb +299 -0
  105. data/test/unit/db_test.rb +136 -0
  106. data/test/unit/grid_test.rb +76 -0
  107. data/test/unit/mongo_sharded_client_test.rb +48 -0
  108. data/test/unit/node_test.rb +93 -0
  109. data/test/unit/pool_manager_test.rb +142 -0
  110. data/test/unit/read_pref_test.rb +115 -0
  111. data/test/unit/read_test.rb +159 -0
  112. data/test/unit/safe_test.rb +158 -0
  113. data/test/unit/sharding_pool_manager_test.rb +84 -0
  114. data/test/unit/write_concern_test.rb +175 -0
  115. metadata +260 -0
  116. metadata.gz.sig +0 -0
@@ -0,0 +1,66 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Mongo
16
+ module WriteConcern
17
+ VALID_KEYS = [:w, :j, :fsync, :wtimeout]
18
+ DEFAULT_WRITE_CONCERN = {:w => 1}
19
+
20
+ attr_reader :legacy_write_concern
21
+
22
+ @@safe_warn = nil
23
+ def write_concern_from_legacy(opts)
24
+ # Warn if 'safe' parameter is being used,
25
+ if opts.key?(:safe) && !@@safe_warn && !ENV['TEST_MODE']
26
+ warn "[DEPRECATED] The 'safe' write concern option has been deprecated in favor of 'w'."
27
+ @@safe_warn = true
28
+ end
29
+
30
+ # nil: set :w => 0
31
+ # false: set :w => 0
32
+ # true: set :w => 1
33
+ # hash: set :w => 0 and merge with opts
34
+
35
+ unless opts.has_key?(:w)
36
+ opts[:w] = 0 # legacy default, unacknowledged
37
+ safe = opts.delete(:safe)
38
+ if(safe && safe.is_a?(Hash))
39
+ opts.merge!(safe)
40
+ elsif(safe == true)
41
+ opts[:w] = 1
42
+ end
43
+ end
44
+ end
45
+
46
+ # todo: throw exception for conflicting write concern options
47
+ def get_write_concern(opts, parent=nil)
48
+ write_concern_from_legacy(opts) if opts.key?(:safe) || legacy_write_concern
49
+ write_concern = DEFAULT_WRITE_CONCERN.dup
50
+ write_concern.merge!(parent.write_concern) if parent
51
+ write_concern.merge!(opts.reject {|k,v| !VALID_KEYS.include?(k)})
52
+ write_concern[:w] = write_concern[:w].to_s if write_concern[:w].is_a?(Symbol)
53
+ write_concern
54
+ end
55
+
56
+ def self.gle?(write_concern)
57
+ (write_concern[:w].is_a? Symbol) ||
58
+ (write_concern[:w].is_a? String) ||
59
+ write_concern[:w] > 0 ||
60
+ write_concern[:j] ||
61
+ write_concern[:fsync] ||
62
+ write_concern[:wtimeout]
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'mongo/gridfs/grid_ext'
16
+ require 'mongo/gridfs/grid'
17
+ require 'mongo/gridfs/grid_file_system'
18
+ require 'mongo/gridfs/grid_io'
@@ -0,0 +1,112 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Mongo
16
+
17
+ # Implementation of the MongoDB GridFS specification. A file store.
18
+ class Grid
19
+ include GridExt::InstanceMethods
20
+
21
+ DEFAULT_FS_NAME = 'fs'
22
+
23
+ # Initialize a new Grid instance, consisting of a MongoDB database
24
+ # and a filesystem prefix if not using the default.
25
+ #
26
+ # @see GridFileSystem
27
+ def initialize(db, fs_name=DEFAULT_FS_NAME)
28
+ raise MongoArgumentError, "db must be a Mongo::DB." unless db.is_a?(Mongo::DB)
29
+
30
+ @db = db
31
+ @files = @db["#{fs_name}.files"]
32
+ @chunks = @db["#{fs_name}.chunks"]
33
+ @fs_name = fs_name
34
+
35
+ # This will create indexes only if we're connected to a primary node.
36
+ begin
37
+ @chunks.ensure_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
38
+ rescue Mongo::ConnectionFailure
39
+ end
40
+ end
41
+
42
+ # Store a file in the file store. This method is designed only for writing new files;
43
+ # if you need to update a given file, first delete it using Grid#delete.
44
+ #
45
+ # Note that arbitrary metadata attributes can be saved to the file by passing
46
+ # them in as options.
47
+ #
48
+ # @param [String, #read] data a string or io-like object to store.
49
+ #
50
+ # @option opts [String] :filename (nil) a name for the file.
51
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
52
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
53
+ # the file to be use in lieu of an automatically generated one.
54
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
55
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
56
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
57
+ # @option opts [Integer] (261120) :chunk_size size of file chunks in bytes.
58
+ # @option opts [String, Integer, Symbol] :w (1) Set write concern
59
+ #
60
+ # Notes on write concern:
61
+ # When :w > 0, the chunks sent to the server are validated using an md5 hash.
62
+ # If validation fails, an exception will be raised.
63
+ #
64
+ # @return [BSON::ObjectId] the file's id.
65
+ def put(data, opts={})
66
+ begin
67
+ # Ensure there is an index on files_id and n, as state may have changed since instantiation of self.
68
+ # Recall that index definitions are cached with ensure_index so this statement won't unneccesarily repeat index creation.
69
+ @chunks.ensure_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
70
+ opts = opts.dup
71
+ filename = opts.delete(:filename)
72
+ opts.merge!(default_grid_io_opts)
73
+ file = GridIO.new(@files, @chunks, filename, 'w', opts)
74
+ file.write(data)
75
+ file.close
76
+ file.files_id
77
+ rescue Mongo::ConnectionFailure => e
78
+ raise e, "Failed to create necessary index and write data."
79
+ end
80
+ end
81
+
82
+ # Read a file from the file store.
83
+ #
84
+ # @param id the file's unique id.
85
+ #
86
+ # @return [Mongo::GridIO]
87
+ def get(id)
88
+ opts = {:query => {'_id' => id}}.merge!(default_grid_io_opts)
89
+ GridIO.new(@files, @chunks, nil, 'r', opts)
90
+ end
91
+
92
+ # Delete a file from the store.
93
+ #
94
+ # Note that deleting a GridFS file can result in read errors if another process
95
+ # is attempting to read a file while it's being deleted. While the odds for this
96
+ # kind of race condition are small, it's important to be aware of.
97
+ #
98
+ # @param id
99
+ #
100
+ # @return [Boolean]
101
+ def delete(id)
102
+ @files.remove({"_id" => id})
103
+ @chunks.remove({"files_id" => id})
104
+ end
105
+
106
+ private
107
+
108
+ def default_grid_io_opts
109
+ {:fs_name => @fs_name}
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,53 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Mongo
16
+ module GridExt
17
+ module InstanceMethods
18
+
19
+ # Check the existence of a file matching the given query selector.
20
+ #
21
+ # Note that this method can be used with both the Grid and GridFileSystem classes. Also
22
+ # keep in mind that if you're going to be performing lots of existence checks, you should
23
+ # keep an instance of Grid or GridFileSystem handy rather than instantiating for each existence
24
+ # check. Alternatively, simply keep a reference to the proper files collection and query that
25
+ # as needed. That's exactly how this methods works.
26
+ #
27
+ # @param [Hash] selector a query selector.
28
+ #
29
+ # @example
30
+ #
31
+ # # Check for the existence of a given filename
32
+ # @grid = Mongo::GridFileSystem.new(@db)
33
+ # @grid.exist?(:filename => 'foo.txt')
34
+ #
35
+ # # Check for existence filename and content type
36
+ # @grid = Mongo::GridFileSystem.new(@db)
37
+ # @grid.exist?(:filename => 'foo.txt', :content_type => 'image/jpg')
38
+ #
39
+ # # Check for existence by _id
40
+ # @grid = Mongo::Grid.new(@db)
41
+ # @grid.exist?(:_id => BSON::ObjectId.from_string('4bddcd24beffd95a7db9b8c8'))
42
+ #
43
+ # # Check for existence by an arbitrary attribute.
44
+ # @grid = Mongo::Grid.new(@db)
45
+ # @grid.exist?(:tags => {'$in' => ['nature', 'zen', 'photography']})
46
+ #
47
+ # @return [nil, Hash] either nil for the file's metadata as a hash.
48
+ def exist?(selector)
49
+ @files.find_one(selector)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,163 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Mongo
16
+
17
+ # A file store built on the GridFS specification featuring
18
+ # an API and behavior similar to that of a traditional file system.
19
+ class GridFileSystem
20
+ include GridExt::InstanceMethods
21
+
22
+ # Initialize a new GridFileSystem instance, consisting of a MongoDB database
23
+ # and a filesystem prefix if not using the default.
24
+ #
25
+ # @param [Mongo::DB] db a MongoDB database.
26
+ # @param [String] fs_name A name for the file system. The default name, based on
27
+ # the specification, is 'fs'.
28
+ def initialize(db, fs_name=Grid::DEFAULT_FS_NAME)
29
+ raise MongoArgumentError, "db must be a Mongo::DB." unless db.is_a?(Mongo::DB)
30
+
31
+ @db = db
32
+ @files = @db["#{fs_name}.files"]
33
+ @chunks = @db["#{fs_name}.chunks"]
34
+ @fs_name = fs_name
35
+
36
+ @default_query_opts = {:sort => [['filename', 1], ['uploadDate', -1]], :limit => 1}
37
+
38
+ # This will create indexes only if we're connected to a primary node.
39
+ begin
40
+ @files.ensure_index([['filename', 1], ['uploadDate', -1]])
41
+ @chunks.ensure_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
42
+ rescue Mongo::ConnectionFailure
43
+ end
44
+ end
45
+
46
+ # Open a file for reading or writing. Note that the options for this method only apply
47
+ # when opening in 'w' mode.
48
+ #
49
+ # Note that arbitrary metadata attributes can be saved to the file by passing
50
+ # them is as options.
51
+ #
52
+ # @param [String] filename the name of the file.
53
+ # @param [String] mode either 'r' or 'w' for reading from
54
+ # or writing to the file.
55
+ # @param [Hash] opts see GridIO#new
56
+ #
57
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
58
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
59
+ # the file to be use in lieu of an automatically generated one.
60
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
61
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
62
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
63
+ # @option opts [Integer] (261120) :chunk_size size of file chunks in bytes.
64
+ # @option opts [Boolean] :delete_old (false) ensure that old versions of the file are deleted. This option
65
+ # only work in 'w' mode. Certain precautions must be taken when deleting GridFS files. See the notes under
66
+ # GridFileSystem#delete.
67
+ # @option opts [String, Integer, Symbol] :w (1) Set write concern
68
+ #
69
+ # Notes on write concern:
70
+ # When :w > 0, the chunks sent to the server
71
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
72
+ # @option opts [Integer] :versions (false) deletes all versions which exceed the number specified to
73
+ # retain ordered by uploadDate. This option only works in 'w' mode. Certain precautions must be taken when
74
+ # deleting GridFS files. See the notes under GridFileSystem#delete.
75
+ #
76
+ # @example
77
+ #
78
+ # # Store the text "Hello, world!" in the grid file system.
79
+ # @grid = Mongo::GridFileSystem.new(@db)
80
+ # @grid.open('filename', 'w') do |f|
81
+ # f.write "Hello, world!"
82
+ # end
83
+ #
84
+ # # Output "Hello, world!"
85
+ # @grid = Mongo::GridFileSystem.new(@db)
86
+ # @grid.open('filename', 'r') do |f|
87
+ # puts f.read
88
+ # end
89
+ #
90
+ # # Write a file on disk to the GridFileSystem
91
+ # @file = File.open('image.jpg')
92
+ # @grid = Mongo::GridFileSystem.new(@db)
93
+ # @grid.open('image.jpg, 'w') do |f|
94
+ # f.write @file
95
+ # end
96
+ #
97
+ # @return [Mongo::GridIO]
98
+ def open(filename, mode, opts={})
99
+ opts = opts.dup
100
+ opts.merge!(default_grid_io_opts(filename))
101
+ if mode == 'w'
102
+ begin
103
+ # Ensure there are the appropriate indexes, as state may have changed since instantiation of self.
104
+ # Recall that index definitions are cached with ensure_index so this statement won't unneccesarily repeat index creation.
105
+ @files.ensure_index([['filename', 1], ['uploadDate', -1]])
106
+ @chunks.ensure_index([['files_id', Mongo::ASCENDING], ['n', Mongo::ASCENDING]], :unique => true)
107
+ versions = opts.delete(:versions)
108
+ if opts.delete(:delete_old) || (versions && versions < 1)
109
+ versions = 1
110
+ end
111
+ rescue Mongo::ConnectionFailure => e
112
+ raise e, "Failed to create necessary indexes and write data."
113
+ return
114
+ end
115
+ end
116
+ file = GridIO.new(@files, @chunks, filename, mode, opts)
117
+ return file unless block_given?
118
+ result = nil
119
+ begin
120
+ result = yield file
121
+ ensure
122
+ id = file.close
123
+ if versions
124
+ self.delete do
125
+ @files.find({'filename' => filename, '_id' => {'$ne' => id}}, :fields => ['_id'], :sort => ['uploadDate', -1], :skip => (versions - 1))
126
+ end
127
+ end
128
+ end
129
+ result
130
+ end
131
+
132
+ # Delete the file with the given filename. Note that this will delete
133
+ # all versions of the file.
134
+ #
135
+ # Be careful with this. Deleting a GridFS file can result in read errors if another process
136
+ # is attempting to read a file while it's being deleted. While the odds for this
137
+ # kind of race condition are small, it's important to be aware of.
138
+ #
139
+ # @param [String] filename
140
+ #
141
+ # @yield [] pass a block that returns an array of documents to be deleted.
142
+ #
143
+ # @return [Boolean]
144
+ def delete(filename=nil)
145
+ if block_given?
146
+ files = yield
147
+ else
148
+ files = @files.find({'filename' => filename}, :fields => ['_id'])
149
+ end
150
+ files.each do |file|
151
+ @files.remove({'_id' => file['_id']})
152
+ @chunks.remove({'files_id' => file['_id']})
153
+ end
154
+ end
155
+ alias_method :unlink, :delete
156
+
157
+ private
158
+
159
+ def default_grid_io_opts(filename=nil)
160
+ {:fs_name => @fs_name, :query => {'filename' => filename}, :query_opts => @default_query_opts}
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,484 @@
1
+ # Copyright (C) 2009-2013 MongoDB, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'digest/md5'
16
+
17
+ module Mongo
18
+
19
+ # GridIO objects represent files in the GridFS specification. This class
20
+ # manages the reading and writing of file chunks and metadata.
21
+ class GridIO
22
+ include Mongo::WriteConcern
23
+
24
+ DEFAULT_CHUNK_SIZE = 255 * 1024
25
+ DEFAULT_CONTENT_TYPE = 'binary/octet-stream'
26
+ PROTECTED_ATTRS = [:files_id, :file_length, :client_md5, :server_md5]
27
+
28
+ attr_reader :content_type, :chunk_size, :upload_date, :files_id, :filename,
29
+ :metadata, :server_md5, :client_md5, :file_length, :file_position
30
+
31
+ # Create a new GridIO object. Note that most users will not need to use this class directly;
32
+ # the Grid and GridFileSystem classes will instantiate this class
33
+ #
34
+ # @param [Mongo::Collection] files a collection for storing file metadata.
35
+ # @param [Mongo::Collection] chunks a collection for storing file chunks.
36
+ # @param [String] filename the name of the file to open or write.
37
+ # @param [String] mode 'r' or 'w' or reading or creating a file.
38
+ #
39
+ # @option opts [Hash] :query a query selector used when opening the file in 'r' mode.
40
+ # @option opts [Hash] :query_opts any query options to be used when opening the file in 'r' mode.
41
+ # @option opts [String] :fs_name the file system prefix.
42
+ # @option opts [Integer] (261120) :chunk_size size of file chunks in bytes.
43
+ # @option opts [Hash] :metadata ({}) any additional data to store with the file.
44
+ # @option opts [ObjectId] :_id (ObjectId) a unique id for
45
+ # the file to be use in lieu of an automatically generated one.
46
+ # @option opts [String] :content_type ('binary/octet-stream') If no content type is specified,
47
+ # the content type will may be inferred from the filename extension if the mime-types gem can be
48
+ # loaded. Otherwise, the content type 'binary/octet-stream' will be used.
49
+ # @option opts [String, Integer, Symbol] :w (1) Set the write concern
50
+ #
51
+ # Notes on write concern:
52
+ # When :w > 0, the chunks sent to the server
53
+ # will be validated using an md5 hash. If validation fails, an exception will be raised.
54
+ def initialize(files, chunks, filename, mode, opts={})
55
+ @files = files
56
+ @chunks = chunks
57
+ @filename = filename
58
+ @mode = mode
59
+ opts = opts.dup
60
+ @query = opts.delete(:query) || {}
61
+ @query_opts = opts.delete(:query_opts) || {}
62
+ @fs_name = opts.delete(:fs_name) || Grid::DEFAULT_FS_NAME
63
+ @write_concern = get_write_concern(opts)
64
+ @local_md5 = Digest::MD5.new if Mongo::WriteConcern.gle?(@write_concern)
65
+ @custom_attrs = {}
66
+
67
+ case @mode
68
+ when 'r' then init_read
69
+ when 'w' then init_write(opts)
70
+ else
71
+ raise GridError, "Invalid file mode #{@mode}. Mode should be 'r' or 'w'."
72
+ end
73
+ end
74
+
75
+ def [](key)
76
+ @custom_attrs[key] || instance_variable_get("@#{key.to_s}")
77
+ end
78
+
79
+ def []=(key, value)
80
+ if PROTECTED_ATTRS.include?(key.to_sym)
81
+ warn "Attempting to overwrite protected value."
82
+ return nil
83
+ else
84
+ @custom_attrs[key] = value
85
+ end
86
+ end
87
+
88
+ # Read the data from the file. If a length if specified, will read from the
89
+ # current file position.
90
+ #
91
+ # @param [Integer] length
92
+ #
93
+ # @return [String]
94
+ # the data in the file
95
+ def read(length=nil)
96
+ return '' if @file_length.zero?
97
+ if length == 0
98
+ return ''
99
+ elsif length.nil? && @file_position.zero?
100
+ read_all
101
+ else
102
+ read_length(length)
103
+ end
104
+ end
105
+ alias_method :data, :read
106
+
107
+ # Write the given string (binary) data to the file.
108
+ #
109
+ # @param [String] io the data to write.
110
+ #
111
+ # @return [Integer] the number of bytes written.
112
+ def write(io)
113
+ raise GridError, "file not opened for write" unless @mode[0] == ?w
114
+ if io.is_a? String
115
+ if Mongo::WriteConcern.gle?(@write_concern)
116
+ @local_md5.update(io)
117
+ end
118
+ write_string(io)
119
+ else
120
+ length = 0
121
+ if Mongo::WriteConcern.gle?(@write_concern)
122
+ while(string = io.read(@chunk_size))
123
+ @local_md5.update(string)
124
+ length += write_string(string)
125
+ end
126
+ else
127
+ while(string = io.read(@chunk_size))
128
+ length += write_string(string)
129
+ end
130
+ end
131
+ length
132
+ end
133
+ end
134
+
135
+ # Position the file pointer at the provided location.
136
+ #
137
+ # @param [Integer] pos
138
+ # the number of bytes to advance the file pointer. this can be a negative
139
+ # number.
140
+ # @param [Integer] whence
141
+ # one of IO::SEEK_CUR, IO::SEEK_END, or IO::SEEK_SET
142
+ #
143
+ # @return [Integer] the new file position
144
+ def seek(pos, whence=IO::SEEK_SET)
145
+ raise GridError, "Seek is only allowed in read mode." unless @mode == 'r'
146
+ target_pos = case whence
147
+ when IO::SEEK_CUR
148
+ @file_position + pos
149
+ when IO::SEEK_END
150
+ @file_length + pos
151
+ when IO::SEEK_SET
152
+ pos
153
+ end
154
+
155
+ new_chunk_number = (target_pos / @chunk_size).to_i
156
+ if new_chunk_number != @current_chunk['n']
157
+ save_chunk(@current_chunk) if @mode[0] == ?w
158
+ @current_chunk = get_chunk(new_chunk_number)
159
+ end
160
+ @file_position = target_pos
161
+ @chunk_position = @file_position % @chunk_size
162
+ @file_position
163
+ end
164
+
165
+ # The current position of the file.
166
+ #
167
+ # @return [Integer]
168
+ def tell
169
+ @file_position
170
+ end
171
+ alias :pos :tell
172
+
173
+ # Rewind the file. This is equivalent to seeking to the zeroth position.
174
+ #
175
+ # @return [Integer] the position of the file after rewinding (always zero).
176
+ def rewind
177
+ raise GridError, "file not opened for read" unless @mode[0] == ?r
178
+ seek(0)
179
+ end
180
+
181
+ # Return a boolean indicating whether the position pointer is
182
+ # at the end of the file.
183
+ #
184
+ # @return [Boolean]
185
+ def eof
186
+ raise GridError, "file not opened for read #{@mode}" unless @mode[0] == ?r
187
+ @file_position >= @file_length
188
+ end
189
+ alias :eof? :eof
190
+
191
+ # Return the next line from a GridFS file. This probably
192
+ # makes sense only if you're storing plain text. This method
193
+ # has a somewhat tricky API, which it inherits from Ruby's
194
+ # StringIO#gets.
195
+ #
196
+ # @param [String, Integer] separator or length. If a separator,
197
+ # read up to the separator. If a length, read the +length+ number
198
+ # of bytes. If nil, read the entire file.
199
+ # @param [Integer] length If a separator is provided, then
200
+ # read until either finding the separator or
201
+ # passing over the +length+ number of bytes.
202
+ #
203
+ # @return [String]
204
+ def gets(separator="\n", length=nil)
205
+ if separator.nil?
206
+ read_all
207
+ elsif separator.is_a?(Integer)
208
+ read_length(separator)
209
+ elsif separator.length > 1
210
+ read_to_string(separator, length)
211
+ else
212
+ read_to_character(separator, length)
213
+ end
214
+ end
215
+
216
+ # Return the next byte from the GridFS file.
217
+ #
218
+ # @return [String]
219
+ def getc
220
+ read_length(1)
221
+ end
222
+
223
+ # Creates or updates the document from the files collection that
224
+ # stores the chunks' metadata. The file becomes available only after
225
+ # this method has been called.
226
+ #
227
+ # This method will be invoked automatically when
228
+ # on GridIO#open is passed a block. Otherwise, it must be called manually.
229
+ #
230
+ # @return [BSON::ObjectId]
231
+ def close
232
+ if @mode[0] == ?w
233
+ if @current_chunk['n'].zero? && @chunk_position.zero?
234
+ warn "Warning: Storing a file with zero length."
235
+ end
236
+ @upload_date = Time.now.utc
237
+ id = @files.insert(to_mongo_object)
238
+ end
239
+ id
240
+ end
241
+
242
+ # Read a chunk of the data from the file and yield it to the given
243
+ # block.
244
+ #
245
+ # Note that this method reads from the current file position.
246
+ #
247
+ # @yield Yields on chunk per iteration as defined by this file's
248
+ # chunk size.
249
+ #
250
+ # @return [Mongo::GridIO] self
251
+ def each
252
+ return read_all unless block_given?
253
+ while chunk = read(chunk_size)
254
+ yield chunk
255
+ break if chunk.empty?
256
+ end
257
+ self
258
+ end
259
+
260
+ def inspect
261
+ "#<GridIO _id: #{@files_id}>"
262
+ end
263
+
264
+ private
265
+
266
+ def create_chunk(n)
267
+ chunk = BSON::OrderedHash.new
268
+ chunk['_id'] = BSON::ObjectId.new
269
+ chunk['n'] = n
270
+ chunk['files_id'] = @files_id
271
+ chunk['data'] = ''
272
+ @chunk_position = 0
273
+ chunk
274
+ end
275
+
276
+ def save_chunk(chunk)
277
+ @chunks.save(chunk)
278
+ end
279
+
280
+ def get_chunk(n)
281
+ chunk = @chunks.find({'files_id' => @files_id, 'n' => n}).next_document
282
+ @chunk_position = 0
283
+ chunk
284
+ end
285
+
286
+ # Read a file in its entirety.
287
+ def read_all
288
+ buf = ''
289
+ if @current_chunk
290
+ buf << @current_chunk['data'].to_s
291
+ while buf.size < @file_length
292
+ @current_chunk = get_chunk(@current_chunk['n'] + 1)
293
+ break if @current_chunk.nil?
294
+ buf << @current_chunk['data'].to_s
295
+ end
296
+ @file_position = @file_length
297
+ end
298
+ buf
299
+ end
300
+
301
+ # Read a file incrementally.
302
+ def read_length(length)
303
+ cache_chunk_data
304
+ remaining = (@file_length - @file_position)
305
+ if length.nil?
306
+ to_read = remaining
307
+ else
308
+ to_read = length > remaining ? remaining : length
309
+ end
310
+ return nil unless remaining > 0
311
+
312
+ buf = ''
313
+ while to_read > 0
314
+ if @chunk_position == @chunk_data_length
315
+ @current_chunk = get_chunk(@current_chunk['n'] + 1)
316
+ cache_chunk_data
317
+ end
318
+ chunk_remainder = @chunk_data_length - @chunk_position
319
+ size = (to_read >= chunk_remainder) ? chunk_remainder : to_read
320
+ buf << @current_chunk_data[@chunk_position, size]
321
+ to_read -= size
322
+ @chunk_position += size
323
+ @file_position += size
324
+ end
325
+ buf
326
+ end
327
+
328
+ def read_to_character(character="\n", length=nil)
329
+ result = ''
330
+ len = 0
331
+ while char = getc
332
+ result << char
333
+ len += 1
334
+ break if char == character || (length ? len >= length : false)
335
+ end
336
+ result.length > 0 ? result : nil
337
+ end
338
+
339
+ def read_to_string(string="\n", length=nil)
340
+ result = ''
341
+ len = 0
342
+ match_idx = 0
343
+ match_num = string.length - 1
344
+ to_match = string[match_idx].chr
345
+ if length
346
+ matcher = lambda {|idx, num| idx < num && len < length }
347
+ else
348
+ matcher = lambda {|idx, num| idx < num}
349
+ end
350
+ while matcher.call(match_idx, match_num) && char = getc
351
+ result << char
352
+ len += 1
353
+ if char == to_match
354
+ while match_idx < match_num do
355
+ match_idx += 1
356
+ to_match = string[match_idx].chr
357
+ char = getc
358
+ result << char
359
+ if char != to_match
360
+ match_idx = 0
361
+ to_match = string[match_idx].chr
362
+ break
363
+ end
364
+ end
365
+ end
366
+ end
367
+ result.length > 0 ? result : nil
368
+ end
369
+
370
+ def cache_chunk_data
371
+ @current_chunk_data = @current_chunk['data'].to_s
372
+ if @current_chunk_data.respond_to?(:force_encoding)
373
+ @current_chunk_data.force_encoding("binary")
374
+ end
375
+ @chunk_data_length = @current_chunk['data'].length
376
+ end
377
+
378
+ def write_string(string)
379
+ # Since Ruby 1.9.1 doesn't necessarily store one character per byte.
380
+ if string.respond_to?(:force_encoding)
381
+ string.force_encoding("binary")
382
+ end
383
+
384
+ to_write = string.length
385
+ while (to_write > 0) do
386
+ if @current_chunk && @chunk_position == @chunk_size
387
+ next_chunk_number = @current_chunk['n'] + 1
388
+ @current_chunk = create_chunk(next_chunk_number)
389
+ end
390
+ chunk_available = @chunk_size - @chunk_position
391
+ step_size = (to_write > chunk_available) ? chunk_available : to_write
392
+ @current_chunk['data'] = BSON::Binary.new((@current_chunk['data'].to_s << string[-to_write, step_size]).unpack("c*"))
393
+ @chunk_position += step_size
394
+ to_write -= step_size
395
+ save_chunk(@current_chunk)
396
+ end
397
+ string.length - to_write
398
+ end
399
+
400
+ # Initialize the class for reading a file.
401
+ def init_read
402
+ doc = @files.find(@query, @query_opts).next_document
403
+ raise GridFileNotFound, "Could not open file matching #{@query.inspect} #{@query_opts.inspect}" unless doc
404
+
405
+ @files_id = doc['_id']
406
+ @content_type = doc['contentType']
407
+ @chunk_size = doc['chunkSize']
408
+ @upload_date = doc['uploadDate']
409
+ @aliases = doc['aliases']
410
+ @file_length = doc['length']
411
+ @metadata = doc['metadata']
412
+ @md5 = doc['md5']
413
+ @filename = doc['filename']
414
+ @custom_attrs = doc
415
+
416
+ @current_chunk = get_chunk(0)
417
+ @file_position = 0
418
+ end
419
+
420
+ # Initialize the class for writing a file.
421
+ def init_write(opts)
422
+ opts = opts.dup
423
+ @files_id = opts.delete(:_id) || BSON::ObjectId.new
424
+ @content_type = opts.delete(:content_type) || (defined? MIME) && get_content_type || DEFAULT_CONTENT_TYPE
425
+ @chunk_size = opts.delete(:chunk_size) || DEFAULT_CHUNK_SIZE
426
+ @metadata = opts.delete(:metadata)
427
+ @aliases = opts.delete(:aliases)
428
+ @file_length = 0
429
+ opts.each {|k, v| self[k] = v}
430
+ check_existing_file if Mongo::WriteConcern.gle?(@write_concern)
431
+
432
+ @current_chunk = create_chunk(0)
433
+ @file_position = 0
434
+ end
435
+
436
+ def check_existing_file
437
+ if @files.find_one('_id' => @files_id)
438
+ raise GridError, "Attempting to overwrite with Grid#put. You must delete the file first."
439
+ end
440
+ end
441
+
442
+ def to_mongo_object
443
+ h = BSON::OrderedHash.new
444
+ h['_id'] = @files_id
445
+ h['filename'] = @filename if @filename
446
+ h['contentType'] = @content_type
447
+ h['length'] = @current_chunk ? @current_chunk['n'] * @chunk_size + @chunk_position : 0
448
+ h['chunkSize'] = @chunk_size
449
+ h['uploadDate'] = @upload_date
450
+ h['aliases'] = @aliases if @aliases
451
+ h['metadata'] = @metadata if @metadata
452
+ h['md5'] = get_md5
453
+ h.merge!(@custom_attrs)
454
+ h
455
+ end
456
+
457
+ # Get a server-side md5 and validate against the client if running with acknowledged writes
458
+ def get_md5
459
+ md5_command = BSON::OrderedHash.new
460
+ md5_command['filemd5'] = @files_id
461
+ md5_command['root'] = @fs_name
462
+ @server_md5 = @files.db.command(md5_command)['md5']
463
+ if Mongo::WriteConcern.gle?(@write_concern)
464
+ @client_md5 = @local_md5.hexdigest
465
+ if @local_md5 == @server_md5
466
+ @server_md5
467
+ else
468
+ raise GridMD5Failure, "File on server failed MD5 check"
469
+ end
470
+ else
471
+ @server_md5
472
+ end
473
+ end
474
+
475
+ # Determine the content type based on the filename.
476
+ def get_content_type
477
+ if @filename
478
+ if types = MIME::Types.type_for(@filename)
479
+ types.first.simplified unless types.empty?
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end