blinkbox-common_mapping 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NGZlZjU1MzQyOTQ4YjY0YWEwNDg4NDEwODcxZTI3YjNhNGQxOTE3ZA==
5
+ data.tar.gz: !binary |-
6
+ NjgxYWFjMTQ2ZGZjNjkxMjZkYzEwMDY2YWI3OGJkYjk4YWQ0M2U4Mw==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NGJhNjE2YmIyMmM1ZDg5ODQ5NTVjMjllNTAxZmI2ODhmM2Y5ZjExNjAwMGY1
10
+ NTk2OWFiYjhlNGEzYWQ4NWJhNjIxMzlkYWZmMmIxMWM4ZmVhYzFhMjE5NzU5
11
+ MTUxMGEzYzUzOGM4OWYzNmIxMTM4NmVkZmU4MTM1NjZkMWIwZDc=
12
+ data.tar.gz: !binary |-
13
+ NmExZWZjZjRhOWI4Yjg0ZjE1Mzk1NzE5ODkyZWY2NWExNzQ1MTI2NjE5ZDBl
14
+ YTNiNTg0NDdlMGVkYzExOTEwMmY4NTgwY2JhZmRhMTM3ODI2N2MxY2RiZjA2
15
+ ODZjYmUyNmQ0YTY5YWFjMDA4MGYyMDc4OTNiNmRlYzMyNGJmY2I=
@@ -0,0 +1,73 @@
1
+ # Change log
2
+
3
+ ## Open Source release (2015-01-28 14:11:21)
4
+
5
+ Today we have decided to publish parts of our codebase on Github under the [MIT licence](LICENCE). The licence is very permissive, but we will always appreciate hearing if and where our work is used!
6
+
7
+ ## 0.1.4 ([#8](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/8) 2015-01-14 15:06:59)
8
+
9
+ Deal with Storage Service omissions
10
+
11
+ ### Bug fix
12
+
13
+ - Deal with the situation where the storage service doesn't have status information for the given token. (This *always* occurs with the fake storage service)
14
+
15
+ ## 0.1.3 ([#7](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/7) 2015-01-07 17:13:29)
16
+
17
+ Return
18
+
19
+ ### Bugfix
20
+
21
+ - The `open` method should return the value the block yields when a block is given.
22
+
23
+ ## 0.1.2 ([#6](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/6) 2014-11-25 17:48:31)
24
+
25
+ Pluralise default folder
26
+
27
+ ### Improvements
28
+
29
+ - The default schema file is `schemas` not `schema`
30
+ - Upgraded to needing the version of common_messaging from [#8](/Platform/common_messaging.rb/pull/8)
31
+
32
+ ## 0.1.1 ([#5](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/5) 2014-11-20 09:25:18)
33
+
34
+ Valid User Agent
35
+
36
+ ### Bug fix
37
+
38
+ - Previous User-Agent was invalid.
39
+
40
+ ## 0.1.0 ([#4](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/4) 2014-11-18 16:05:09)
41
+
42
+ Legit
43
+
44
+ ### New features
45
+
46
+ - Actually works!
47
+ - Can deal with HTTP and FILE urls passed in the mapping file from the storage service
48
+ - Opens files and downloads HTTP resources into temporary files, passing an IO object back for interaction with.
49
+
50
+ ## 0.0.3 ([#3](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/3) 2014-10-27 18:11:33)
51
+
52
+ Unescape URI components
53
+
54
+ ### Improvement
55
+
56
+ - Allows URI encoded files to come through (allows spaces!)
57
+
58
+ ## 0.0.2 ([#2](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/2) 2014-10-08 13:06:55)
59
+
60
+ Deal with triple or single slash
61
+
62
+ ### Bugfix
63
+
64
+ - URLs with a single or triple slash were failing to process (eg. `file:/path/to/stuff`)
65
+
66
+ ## 0.0.1 ([#1](https://git.mobcastdev.com/Platform/common_mapping.rb/pull/1) 2014-10-06 14:35:20)
67
+
68
+ Correct the gemspec
69
+
70
+ ### Improvement
71
+
72
+ - First release, mock only (while the Quartermaster is being spec'd out)
73
+
@@ -0,0 +1,16 @@
1
+ # Blinkbox::CommonMapping
2
+
3
+ Deals with blinkbox Books virtual URIs and acts like a local `File` object.
4
+
5
+ ```ruby
6
+ mapper = Mappings.new(
7
+ "http://quatermaster.blinkbox.local",
8
+ service_name: "Labs/example_code"
9
+ )
10
+
11
+ mapper.open("bbbmap::testfile:/some/path/component.epub") do |io|
12
+ p io
13
+ # This is a Tempfile object, interact with it as you wish!
14
+ end
15
+ # Temporary file has been deleted
16
+ ```
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.4
@@ -0,0 +1,260 @@
1
+ require "uri"
2
+ require "json"
3
+ require "time"
4
+ require "socket"
5
+ require "net/http"
6
+ require "tempfile"
7
+ require "blinkbox/common_messaging"
8
+
9
+ module Blinkbox
10
+ class CommonMapping
11
+ VERSION = begin
12
+ File.read(File.join(__dir__, "../../VERSION")).strip
13
+ rescue Errno::ENOENT
14
+ "0.0.0-unknown"
15
+ end
16
+
17
+ # Set a logger to send all log messages to
18
+ #
19
+ # @param [:debug,:info,:warn,:error,:fatal] logger A logger instance.
20
+ def self.logger=(logger)
21
+ @@logger = logger
22
+ end
23
+
24
+ # NullLogger by default
25
+ @@logger = Class.new {
26
+ def debug(*); end
27
+ def info(*); end
28
+ def warn(*); end
29
+ def error(*); end
30
+ def fatal(*); end
31
+ }.new
32
+
33
+ # Initializing a mapper will retrieve the mapping file from the specified storage service and set up an exclusive queue
34
+ # to receive updates which might occur while this instance is in use.
35
+ #
36
+ # @param [String] storage_service_url The Base URL for the storage service.
37
+ # @param [String] :service_name The name of your service. Defines the name of the mapping updates queue.
38
+ # @param [String, nil] :schema_root If not nil, the location (relative to the current directory) of the schema root (mapping/update/v1.schema.json will be used to validate messages).
39
+ # @param [Integer] :mapping_timeout The length of time before a new mapping file is requested from the storage service.
40
+ def initialize(storage_service_url, service_name: raise(ArgumentError, "A service name is required"), schema_root: "schemas", mapping_timeout: 7 * 24 * 3600)
41
+ @ss = URI.parse(storage_service_url)
42
+ @service_name = service_name
43
+ uid = [Socket.gethostname, Process.pid].join("$")
44
+ queue_name = "#{service_name.tr('/', '.')}.mapping_updates.#{uid}"
45
+
46
+ @queue = CommonMessaging::Queue.new(
47
+ queue_name,
48
+ exchange: "Mapping",
49
+ bindings: [{ "content-type" => "application/vnd.blinkbox.books.mapping.update.v1+json" }],
50
+ prefetch: 1,
51
+ exclusive: true,
52
+ temporary: true,
53
+ dlx: nil
54
+ )
55
+
56
+ @timeout = mapping_timeout
57
+
58
+ opts = { block: false }
59
+ if !schema_root.nil?
60
+ CommonMessaging.init_from_schema_at(File.join(schema_root, "mapping"), schema_root)
61
+ opts[:accept] = [CommonMessaging::MappingUpdateV1]
62
+ end
63
+
64
+ # We're about to request the latest mapping file, so we don't need any of the ones on the queue.
65
+ @queue.purge!
66
+ @queue.subscribe(opts) do |metadata, update|
67
+ next :reject unless metadata[:timestamp].is_a?(Time)
68
+ update_mapping!(metadata[:timestamp], update)
69
+ :ack
70
+ end
71
+
72
+ @@logger.debug "Queue #{queue_name} created, bound and subscribed to"
73
+ retrieve_mapping!
74
+ @@logger.info "Mapping initialized"
75
+ end
76
+
77
+ # Opens a given token and returns an IO object for the associated asset or - if a block
78
+ # is passed - yields with the IO object as the only argument.
79
+ #
80
+ # @param [String] token The token referring to the asset to be opened
81
+ # @raise InvalidTokenError if the given token string isn't a valid token
82
+ # @return An IO object referencing the object or (if a block was given) the value returned from the block
83
+ def open(token)
84
+ raise InvalidTokenError unless valid_token?(token)
85
+ @@logger.debug "Opening #{token}"
86
+ locations = map(token)
87
+ @@logger.debug "Locations for #{token} are: #{locations.inspect}"
88
+ # TODO: We currently assume the first is the best. Later iterations of this library may be more intelligent
89
+ while locations.size > 0
90
+ provider, uri = locations.shift
91
+ @@logger.debug "Trying #{uri} from #{provider}"
92
+ begin
93
+ io = open_uri(URI.parse(uri))
94
+ return io if !block_given?
95
+ returns = yield(io)
96
+ io.close
97
+ return returns
98
+ rescue
99
+ # There was a problem with this provider file, register it and move on to another
100
+ status = retrieve_status(token, provider_failure: provider)
101
+ if status.nil?
102
+ @@logger.warn "Storage service has no status for #{token}."
103
+ break
104
+ end
105
+ available_providers = status['providers'].map { |this_provider, this_status|
106
+ this_status['available'] ? this_provider : nil
107
+ }.compact
108
+ locations.delete_if { |p, u|
109
+ !(status['providers'][p] && status['providers'][p]['available'])
110
+ }
111
+ end
112
+ end
113
+ raise MissingAssetError, "The asset for #{token} could not be downloaded from anywhere."
114
+ end
115
+
116
+ # Gets information about a specific token.
117
+ #
118
+ # @param [String] token The token for the asset to get the status of
119
+ def status(token)
120
+ # Duplicate method so the external API can't register a provider failure
121
+ retrieve_status(token)
122
+ end
123
+
124
+ # Collects the mappings from the storage service specified when initialised.
125
+ #
126
+ # @return [Boolean] Returns true if the mapping file was updated, false if the mapping already stored was the same or more recent.
127
+ # @raise StorageServiceUnavailableError if the response from the server isn't 200, isn't JSON or (if the schema are available) isn't a valid mapping document.
128
+ def retrieve_mapping!
129
+ response = ss_get("/mappings")
130
+ raise StorageServiceUnavailableError, "Storage service gave #{response.code} response code, cannot update the mapping details." unless response.code == "200"
131
+ mapping = JSON.parse(response.body)
132
+ # This will raise a JSON::Schema::ValidationError if the mapping file isn't valid
133
+ CommonMessaging::MappingUpdateV1.new(mapping) if CommonMessaging.const_defined?('MappingUpdateV1')
134
+ timestamp = response['Date'].nil? ? Time.now : Time.parse(response['Date'])
135
+ update_mapping!(timestamp, mapping)
136
+ rescue JSON::ParserError, JSON::Schema::ValidationError
137
+ raise StorageServiceUnavailableError, "The response from the storage service wasn't a valid mapping update."
138
+ end
139
+
140
+ def inspect
141
+ "<Token Mapper: #{@ss.host}>"
142
+ end
143
+
144
+ private
145
+
146
+ # Stores a retrieved mapping file along with the timestamp
147
+ #
148
+ # @param [Time] timestamp The timestamp at which the given mapping was accurate
149
+ # @param [Hash, CommonMessaging::MappingUpdateV1] mapping The mapping file which needs to be stored
150
+ # @return [Boolean] Returns true if the mapping file was updated, false if the mapping already stored was the same or more recent.
151
+ def update_mapping!(timestamp, mapping)
152
+ return false if (!@mapping.nil? && timestamp < @mapping[:timestamp])
153
+ return false if (!@mapping.nil? && @mapping[:data] == mapping)
154
+ @mapping = {
155
+ data: mapping,
156
+ timestamp: timestamp
157
+ }
158
+ true
159
+ end
160
+
161
+ # Gets the status of a specific token, optionally recording that a provider has failed for a specific
162
+ # asset if specified.
163
+ #
164
+ # @return [nil] if the token does not exist
165
+ # @return [Hash] details of the asset
166
+ def retrieve_status(token, provider_failure: nil)
167
+ raise InvalidTokenError unless valid_token?(token)
168
+ res = ss_get("/resources/#{token}")
169
+ return nil if res.code == "404"
170
+ # TODO: Deal with other response codes
171
+ raise StorageServiceUnavailableError, "Storage service responded with #{res.code}" unless res.code == "200"
172
+ JSON.parse(res.body)
173
+ end
174
+
175
+ # TODO: Deal with unlinking tempfiles
176
+ def open_uri(uri)
177
+ case uri.scheme
178
+ when "file"
179
+ path = URI.decode(uri.path)
180
+ @@logger.debug "Attempting to open #{path}"
181
+ raise MissingAssetError unless File.exist?(path)
182
+ io = File.open(path)
183
+ when "http", "https"
184
+ io = Tempfile.new("common_mapping_file")
185
+ Net::HTTP.start(uri.host, uri.port) do |http|
186
+ http.request_get(uri.path) do |resp|
187
+ raise MissingAssetError, "Received a #{resp.code} while trying to retrieve #{uri}" if resp.code != "200"
188
+ resp.read_body do |segment|
189
+ io.write(segment)
190
+ end
191
+ end
192
+ end
193
+ io.rewind
194
+ else
195
+ raise NotImplementedError
196
+ end
197
+ io
198
+ end
199
+
200
+ def ss_get(path)
201
+ @http ||= Net::HTTP.new(@ss.host, @ss.port)
202
+ request = Net::HTTP::Get.new(path)
203
+ request.initialize_http_header({"User-Agent" => "common_mapping.rb/#{VERSION}"})
204
+ @http.request(request)
205
+ rescue Timeout::Error
206
+ raise StorageServiceUnavailableError, "A request to the storage service timed out"
207
+ end
208
+
209
+ # @param [String] token The token that wants to be checked
210
+ # @return [Boolean] Whether the token is valid or not
211
+ def valid_token?(token)
212
+ uri = URI(token)
213
+ uri.scheme == "bbbmap"
214
+ rescue URI::InvalidURIError
215
+ false
216
+ end
217
+
218
+ # Uses the mapping file retrieved to convert a token into URLs. The first item in the hash
219
+ # will be the first provider listed in the first matched label group etc.
220
+ #
221
+ # Will retrieve the mapping afresh if the mapping data has expired (is older than the
222
+ # timeout value used to initialise this object)
223
+ #
224
+ # @param [URI] token The token (as a URI object) to look up.
225
+ # @raise UnknownLabelError if no mappings exist for the token given.
226
+ # @raise InvalidTokenError if the given token string isn't a valid token
227
+ # @return [Hash] A hash of provider names (key) to URLs (value) for this asset.
228
+ def map(token)
229
+ raise InvalidTokenError unless valid_token?(token)
230
+ retrieve_mapping! if (Time.now.to_i - @mapping[:timestamp].to_i > @timeout)
231
+ @@logger.debug "Using mapping: #{@mapping[:data]}"
232
+ matched_providers = {}
233
+ @mapping[:data].each do |label_map|
234
+ re = Regexp.new(label_map['extractor'])
235
+ if token.match(re)
236
+ @@logger.debug "Using #{label_map['label']}"
237
+ capture_groups = Hash[
238
+ Regexp.last_match.names.zip(Regexp.last_match.captures)
239
+ ].inject({}){ |memo, (k, v)|
240
+ memo[k.to_sym] = v
241
+ memo
242
+ }
243
+ label_map['providers'].each_pair do |name, url_template|
244
+ # If there are multiple matching label_maps then providers from the first will take priority over later ones.
245
+ begin
246
+ matched_providers[name] ||= (url_template % capture_groups)
247
+ rescue
248
+ end
249
+ end
250
+ end
251
+ end
252
+ matched_providers
253
+ end
254
+ end
255
+
256
+ class MissingAssetError < RuntimeError; end
257
+ class UnknownLabelError < RuntimeError; end
258
+ class InvalidTokenError < URI::InvalidURIError; end
259
+ class StorageServiceUnavailableError < RuntimeError; end
260
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blinkbox-common_mapping
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - JP Hastings-Spital
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: blinkbox-common_messaging
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.5.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.5'
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.5.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec-mocks
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: webmock
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: simplecov
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ description: Deal with blinkbox Books virtual URLs
104
+ email:
105
+ - jphastings@blinkbox.com
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files:
109
+ - CHANGELOG.md
110
+ - README.md
111
+ files:
112
+ - CHANGELOG.md
113
+ - README.md
114
+ - VERSION
115
+ - lib/blinkbox/common_mapping.rb
116
+ homepage: ''
117
+ licenses: []
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.4.5
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Deal with blinkbox Books virtual URLs
139
+ test_files: []