moped-gridfs 1.0.0

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