blinkbox-common_mapping 0.1.4

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.
@@ -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: []