moped-gridfs 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9a366fb5dfef7a6f12ee74c4e5e4d1bb39d810a0
4
+ data.tar.gz: b20b96931842cfb6a35ee986a1fa7e327d9882f2
5
+ SHA512:
6
+ metadata.gz: 7921ced6e4e20baae692c7a7255e1f4dc1ab9afa8cf743660fe4d4bc1968ebf4184e191c878026e8f0c88bfe40a5c80ceee07165b3e40a191631ca667c021318
7
+ data.tar.gz: afd679c13f190f0e4a40ce2a533cdf5563e58b4580e20a24f2ede733cc59025dd70fafad3cbf1f359eaa8ffdb9bae92f57c405d601f40700a6eea8a81d018828
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ moped-gridfs
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.0
5
+ - jruby
6
+ gemfile:
7
+ - Gemfile
8
+ - gemfiles/moped15.gemfile
9
+ services:
10
+ - mongodb
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 topac
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # moped-gridfs
2
+
3
+ Add [GridFS support](http://docs.mongodb.org/manual/core/gridfs) to the [Moped driver](https://github.com/mongoid/moped).
4
+
5
+ Moped is a fast MongoDB driver for Ruby,
6
+ but it does not implement the GridFS specifications
7
+ (while [mongo-ruby-driver](https://github.com/mongodb/mongo-ruby-driver) does).
8
+
9
+ ## Bucket
10
+
11
+ GridFS places the collections in a common bucket by prefixing each with the bucket name.
12
+ By default, GridFS uses two collections with names prefixed by "fs" bucket: _fs.files_ and _fs.chunks_.
13
+
14
+ You can choose a different bucket name than "fs", and create multiple buckets in a single database.
15
+ Access the default bucket (named "fs") this way:
16
+
17
+ ```ruby
18
+ require 'moped'
19
+ require 'moped/gridfs'
20
+
21
+ session = Moped::Session.new(["127.0.0.1:27017"])
22
+ session.use("test")
23
+ bucket = session.bucket #<Moped::GridFS::Bucket:7ffbdbd4e160 name=fs>
24
+ ```
25
+ or
26
+ ```ruby
27
+ bucket = Moped::GridFS::Bucket.new(session) #<Moped::GridFS::Bucket:7fc06db72c00 name=fs>
28
+ ```
29
+
30
+ A list of all the buckets can be retrieved with `Session#buckets`.
31
+ For example, you can access the _photos_ bucket with `session.buckets['photos']`.
32
+
33
+ To open a file call `Bucket#open`, with the filename (or the _id) and the open mode.
34
+ A more generic selector can be also given instead of the filename.
35
+
36
+ ## File
37
+
38
+ The GridFS::File class exposes an API similar to the ruby File class.
39
+
40
+ ```ruby
41
+ file = bucket.open("myfile", "w+") #<Moped::GridFS::File:7f88599a58b0 bucket=fs _id=539c532ddb13a973ed000001 mode=w+ filename=myfile length=0>
42
+ file.write("foobar") # 6
43
+ file.seek(3) # 3
44
+ file.read # "bar"
45
+ ```
46
+
47
+ All the [open modes](http://www.ruby-doc.org/core-2.1.2/IO.html#method-c-new-label-IO+Open+Mode) are supported:
48
+ r, r+, w, w+, a and a+.
49
+
50
+ GridFS::File attributes are: _id, length, chunk_size, filename, content_type, md5, aliases, metadata and uploadDate.
51
+ Some of them may be changed if the file is opened in write/append mode.
52
+
53
+ ```ruby
54
+ file.content_type # "application/octet-stream"
55
+ file.md5 # "3858f62230ac3c915f300c664312c63f"
56
+ file.filename = "test"
57
+ file.filename # "test"
58
+ ```
59
+
60
+ ## Thread safe?
61
+ It depends on what you're doing.
62
+ You may face race conditions if many threads are writing on the same file a buffer that have to be splitted onto multiple chunks. This is due to how the GridFS specs have been designed: [read this](https://jira.mongodb.org/browse/NODE-157).
63
+
64
+ ## Performance
65
+
66
+ Are pretty much the same of mongo-ruby-driver (run the script perf/compare.rb).
67
+
68
+
69
+ ## Installation
70
+
71
+ Add this line to your application's Gemfile:
72
+
73
+ gem 'moped-gridfs'
74
+
75
+ And then execute:
76
+
77
+ $ bundle
78
+
79
+ Or install it yourself as:
80
+
81
+ $ gem install moped-gridfs
82
+
83
+
84
+ ## Contributing
85
+
86
+ 1. Fork it
87
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
88
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
89
+ 4. Push to the branch (`git push origin my-new-feature`)
90
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+ gemspec path: '..'
3
+
4
+ gem "moped", "~> 1.5.0"
@@ -0,0 +1,2 @@
1
+ require "moped/gridfs/version"
2
+ require "moped/gridfs/extensions/session"
@@ -0,0 +1,48 @@
1
+ module Moped
2
+ module GridFS
3
+ module AccessModes
4
+
5
+ attr_reader :mode
6
+
7
+ ACCESS_MODES = %w[r r+ w w+ a a+]
8
+
9
+ def readable?
10
+ mode =~ /r|\+/
11
+ end
12
+
13
+ def writable?
14
+ mode =~ /w|\+|a/
15
+ end
16
+
17
+ def append?
18
+ mode =~ /a/
19
+ end
20
+
21
+ def read_only?
22
+ mode == 'r'
23
+ end
24
+
25
+ def write_only?
26
+ mode == 'w' or mode == 'a'
27
+ end
28
+
29
+ def read_write?
30
+ mode =~ /\+/
31
+ end
32
+
33
+ private
34
+
35
+ def append_only?
36
+ mode == 'a'
37
+ end
38
+
39
+ def need_file?
40
+ mode == 'r' or mode == 'r+'
41
+ end
42
+
43
+ def truncate?
44
+ mode == 'w' or mode == 'w+'
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,66 @@
1
+ require "moped/gridfs/files"
2
+ require "moped/gridfs/file"
3
+ require "moped/gridfs/inspectable"
4
+ require "moped/gridfs/bucketable"
5
+
6
+ module Moped
7
+ module GridFS
8
+ class Bucket
9
+ include Bucketable
10
+ include Inspectable
11
+
12
+ attr_reader :name, :session
13
+
14
+ DEFAULT_NAME = 'fs'
15
+
16
+ def initialize(session, name = DEFAULT_NAME)
17
+ @name = name.to_s.strip
18
+ @session = session
19
+
20
+ raise ArgumentError.new("Bucket name cannot be empty") if @name.empty?
21
+ end
22
+
23
+ def open(selector, mode)
24
+ ensure_indexes
25
+ file = File.new(self, mode, selector)
26
+ block_given? ? yield(file) : file
27
+ end
28
+
29
+ def ensure_indexes
30
+ @indexes_ensured ||= begin
31
+ chunks_collection.indexes.create(files_id: 1, n: 1)
32
+ # Optional index on filename
33
+ files_collection.indexes.create({filename: 1}, {background: true})
34
+ true
35
+ end
36
+ end
37
+
38
+ def files
39
+ Files.new(self)
40
+ end
41
+
42
+ def md5(file_id)
43
+ session.command(filemd5: file_id, root: name)['md5']
44
+ end
45
+
46
+ def delete(selector)
47
+ document = files_collection.find(parse_selector(selector)).first
48
+ return unless document
49
+ chunks_collection.find(files_id: document['_id']).remove_all
50
+ files_collection.find(_id: document['_id']).remove_all
51
+ true
52
+ end
53
+
54
+ alias :remove :delete
55
+
56
+ def drop
57
+ [files_collection, chunks_collection].map(&:drop)
58
+ @indexes_ensured = false
59
+ end
60
+
61
+ def inspect
62
+ build_inspect_string(name: name)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ module Moped
2
+ module GridFS
3
+ module Bucketable
4
+ def files_collection
5
+ if self.respond_to?(:session)
6
+ session[:"#{name}.files"]
7
+ else
8
+ bucket.files_collection
9
+ end
10
+ end
11
+
12
+ def chunks_collection
13
+ if self.respond_to?(:session)
14
+ session[:"#{name}.chunks"]
15
+ else
16
+ bucket.chunks_collection
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def parse_selector(selector)
23
+ if selector.kind_of?(String)
24
+ {filename: selector}
25
+ elsif selector.kind_of?(BSON::ObjectId)
26
+ {_id: selector}
27
+ else
28
+ selector
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ require "moped/gridfs/bucket"
2
+
3
+ module Moped
4
+ module GridFS
5
+ class Buckets
6
+ include Enumerable
7
+
8
+ attr_reader :session
9
+
10
+ def initialize(session)
11
+ @session = session
12
+ end
13
+
14
+ def names
15
+ collections.map { |collection| collection.name.gsub('.files', '') }
16
+ end
17
+
18
+ def count
19
+ collections.size
20
+ end
21
+
22
+ def [](name)
23
+ Bucket.new(session, name)
24
+ end
25
+
26
+ def each(&block)
27
+ names.each { |name| yield(self[name]) }
28
+ end
29
+
30
+ private
31
+
32
+ def collections
33
+ session.collections.select { |collection| collection.name =~ /.+\.files\z/ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ require "moped"
2
+ require "moped/gridfs/bucket"
3
+ require "moped/gridfs/buckets"
4
+
5
+ module Moped
6
+ module GridFS
7
+ module Extensions
8
+ module Session
9
+ def bucket
10
+ Bucket.new(self)
11
+ end
12
+
13
+ def buckets
14
+ Buckets.new(self)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Moped::Session.__send__(:include, Moped::GridFS::Extensions::Session)
@@ -0,0 +1,233 @@
1
+ require "moped/gridfs/inspectable"
2
+ require "moped/gridfs/bucketable"
3
+ require "moped/gridfs/access_modes"
4
+
5
+ module Moped
6
+ module GridFS
7
+ class File
8
+ include Bucketable
9
+ include Inspectable
10
+ include AccessModes
11
+
12
+ attr_reader :mode, :attributes, :bucket, :pos
13
+
14
+ def initialize(bucket, mode, selector)
15
+ selector = parse_selector(selector)
16
+ @mode = mode
17
+ @bucket = bucket
18
+ @cached_chunk = nil
19
+ @pos = 0
20
+
21
+ raise ArgumentError.new("Invalid access mode #{mode}") unless ACCESS_MODES.include?(mode)
22
+
23
+ document = files_collection.find(selector).first
24
+
25
+ raise "No such file" if need_file? and !document
26
+
27
+ if document and truncate?
28
+ chunks_collection.find(files_id: document['_id']).remove_all
29
+ files_collection.find(_id: document['_id']).remove_all
30
+
31
+ @attributes = normalize_attributes(selector)
32
+ else
33
+ @attributes = normalize_attributes(document || selector)
34
+ @attributes.freeze if read_only?
35
+ end
36
+
37
+ define_dynamic_accessors
38
+
39
+ file_query.upsert(attributes) if writable?
40
+
41
+ @pos = length if append_only?
42
+ end
43
+
44
+ alias :tell :pos
45
+
46
+ def pos=(value)
47
+ check_negative_value(value)
48
+
49
+ @pos = (append_only? and value < length) ? length : value
50
+ end
51
+
52
+ alias :seek :pos=
53
+
54
+ def rewind
55
+ self.pos = 0
56
+ end
57
+
58
+ def eof?
59
+ raise "Not opened for reading" if write_only?
60
+
61
+ pos >= length
62
+ end
63
+
64
+ EMPTINESS = ''.force_encoding('BINARY')
65
+
66
+ def read(size = length)
67
+ raise "Not opened for reading" if write_only?
68
+
69
+ check_negative_value(size)
70
+
71
+ chunk_number = pos / chunk_size
72
+ chunk_offset = pos % chunk_size
73
+
74
+ data = EMPTINESS
75
+
76
+ loop do
77
+ break if data.size >= size
78
+ break unless read_chunk(chunk_number)
79
+ buffer = @cached_chunk[:data][chunk_offset..-1]
80
+ data.empty? ? (data = buffer) : (data << buffer)
81
+ chunk_number += 1
82
+ chunk_offset = 0
83
+ end
84
+
85
+ data = data[0..size - 1]
86
+ @pos += data.size
87
+ data
88
+ end
89
+
90
+ def write(data)
91
+ raise "Not opened for writing" if read_only?
92
+
93
+ data.force_encoding('BINARY') if data.respond_to?(:force_encoding)
94
+
95
+ @pos = length if @pos > length
96
+ @pos = length if append?
97
+
98
+ chunk_number = pos / chunk_size
99
+ chunk_offset = pos % chunk_size
100
+ written = data.size
101
+ new_length = 0
102
+
103
+ loop do
104
+ if buffer = read_chunk(chunk_number)
105
+ data = (chunk_offset.zero? ? EMPTINESS : buffer[0..chunk_offset - 1]) + data + (buffer[chunk_offset + data.size..-1] || EMPTINESS)
106
+ end
107
+
108
+ to_write = data[0..chunk_size - 1] || EMPTINESS
109
+
110
+ break if to_write.empty?
111
+
112
+ new_length = chunk_number * chunk_size + write_chunk(chunk_number, to_write)
113
+
114
+ data = data[chunk_size..-1] || EMPTINESS
115
+
116
+ break if data.empty?
117
+
118
+ chunk_number += 1
119
+ chunk_offset = 0
120
+ end
121
+
122
+ # Update internal position
123
+ @pos += written
124
+
125
+ # Calculate new md5 (if needed)
126
+ md5 = bucket.md5(@attributes[:_id]) if written > 0
127
+
128
+ # Update if something changed
129
+ updates = {}
130
+ updates[:md5] = md5 if md5
131
+ updates[:length] = new_length if new_length > length
132
+ change_attributes(updates) if updates.any?
133
+
134
+ written
135
+ end
136
+
137
+ DEFAULT_CHUNK_SIZE = 255 * 1024
138
+
139
+ def default_chunk_size
140
+ DEFAULT_CHUNK_SIZE
141
+ end
142
+
143
+ def inspect
144
+ build_inspect_string(bucket: bucket.name, _id: _id, mode: mode, filename: filename, length: length)
145
+ end
146
+
147
+ private
148
+
149
+ def normalize_attributes(provided)
150
+ provided.keys.each do |key|
151
+ provided[key.to_sym] = provided.delete(key)
152
+ end
153
+
154
+ attrs = {}
155
+ attrs[:_id] = provided[:_id] ? BSON::ObjectId.from_string(provided[:_id]) : BSON::ObjectId.new
156
+ attrs[:length] = (provided[:length] || 0).to_i
157
+ attrs[:chunkSize] = (provided[:chunkSize] || provided[:chunk_size] || default_chunk_size).to_i
158
+ attrs[:filename] = provided[:filename] || attrs[:_id].to_s
159
+ attrs[:contentType] = provided[:content_type] || provided[:contentType] || 'application/octet-stream'
160
+ attrs[:md5] = provided[:md5]
161
+ attrs[:aliases] = provided[:aliases] || []
162
+ attrs[:metadata] = provided[:metadata] || {}
163
+ attrs[:uploadDate] = provided[:upload_date] || provided[:uploadDate] || Time.now.utc
164
+ attrs
165
+ end
166
+
167
+ PROTECTED_ATTRIBUTES = [:_id, :length, :chunkSize, :md5]
168
+
169
+ def define_dynamic_accessors
170
+ attributes.keys.each do |attrname|
171
+ method_name = underscorize(attrname)
172
+
173
+ __send__(:define_singleton_method, method_name) { attributes[attrname] }
174
+
175
+ if writable? and !PROTECTED_ATTRIBUTES.include?(attrname)
176
+ __send__(:define_singleton_method, :"#{method_name}=") do |value|
177
+ change_attributes(:"#{attrname}" => value)
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ def underscorize(name)
184
+ name.to_s.gsub(/([A-Z])/, '_\1').downcase.to_sym
185
+ end
186
+
187
+ def check_negative_value(value)
188
+ raise ArgumentError.new("negative value #{value} given") if value < 0
189
+ end
190
+
191
+ def change_attributes(hash)
192
+ file_query.update('$set' => hash)
193
+ @attributes.merge!(hash)
194
+ end
195
+
196
+ def file_query
197
+ files_collection.find(_id: attributes[:_id])
198
+ end
199
+
200
+ def chunk_query(n)
201
+ chunks_collection.find(files_id: attributes[:_id], n: n)
202
+ end
203
+
204
+ def chunk(n)
205
+ chunk_query(n).first
206
+ end
207
+
208
+ def write_chunk(n, data)
209
+ chunk_query(n).upsert('$set' => {data: binarize(data)})
210
+ @cached_chunk = {n: n, data: data}
211
+ data.size
212
+ end
213
+
214
+ def read_chunk(n)
215
+ return @cached_chunk[:data] if @cached_chunk and @cached_chunk[:n] == n
216
+ readed = chunk(n)
217
+ return unless readed
218
+ @cached_chunk = {n: readed['n'], data: readed['data'].data}
219
+ @cached_chunk[:data]
220
+ end
221
+
222
+ if Moped::VERSION < '2.0.0'
223
+ def binarize(data)
224
+ BSON::Binary.new(:generic, data)
225
+ end
226
+ else
227
+ def binarize(data)
228
+ BSON::Binary.new(data, :generic)
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end