httpimagestore 1.8.0 → 1.8.1

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.
data/Gemfile CHANGED
@@ -1,26 +1,27 @@
1
- source "http://rubygems.org"
2
- ruby "1.9.3"
1
+ source 'http://rubygems.org'
2
+ ruby '1.9.3'
3
3
 
4
- gem "unicorn-cuba-base", "~> 1.2.2"
5
- #gem "unicorn-cuba-base", path: "../unicorn-cuba-base"
6
- gem "httpthumbnailer-client", "~> 1.2.0"
7
- #gem "httpthumbnailer-client", path: "../httpthumbnailer-client"
8
- gem "aws-sdk", "~> 1.10"
9
- gem "mime-types", "~> 1.17"
10
- gem "sdl4r", "~> 0.9"
11
- gem "msgpack", "~> 0.5"
4
+ gem 'unicorn-cuba-base', '~> 1.2.2'
5
+ #gem 'unicorn-cuba-base', path: '../unicorn-cuba-base'
6
+ gem 'httpthumbnailer-client', '~> 1.2.0'
7
+ #gem 'httpthumbnailer-client', path: '../httpthumbnailer-client'
8
+ gem 'aws-sdk', '~> 1.10'
9
+ gem 'mime-types', '~> 1.17'
10
+ gem 'sdl4r', '~> 0.9'
11
+ gem 'msgpack', '~> 0.5'
12
+ gem 'addressable', '~> 2.3'
12
13
 
13
14
  # Add dependencies to develop your gem here.
14
15
  # Include everything needed to run rake, tests, features, etc.
15
16
  group :development do
16
- gem "faraday", ">= 0.8"
17
- gem "rspec", "~> 2.13"
18
- gem "cucumber", ">= 0"
19
- gem "jeweler", "~> 1.8.4"
20
- gem "rdoc", "~> 3.9"
21
- gem "daemon", "~> 1"
22
- gem "prawn", "= 0.8.4"
23
- #gem "httpthumbnailer", path: '../httpthumbnailer'
24
- gem "httpthumbnailer", "~> 1.2.0"
17
+ gem 'faraday', '>= 0.8'
18
+ gem 'rspec', '~> 2.13'
19
+ gem 'cucumber', '>= 0'
20
+ gem 'jeweler', '~> 1.8.4'
21
+ gem 'rdoc', '~> 3.9'
22
+ gem 'daemon', '~> 1'
23
+ gem 'prawn', '= 0.8.4'
24
+ #gem 'httpthumbnailer', path: '../httpthumbnailer'
25
+ gem 'httpthumbnailer', '~> 1.2.0'
25
26
  end
26
27
 
@@ -110,6 +110,7 @@ PLATFORMS
110
110
  ruby
111
111
 
112
112
  DEPENDENCIES
113
+ addressable (~> 2.3)
113
114
  aws-sdk (~> 1.10)
114
115
  cucumber
115
116
  daemon (~> 1)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.0
1
+ 1.8.1
@@ -152,3 +152,14 @@ Feature: Image list based thumbnailing and S3 storage
152
152
  Then response status will be 200
153
153
  And response content type will be image/jpeg
154
154
  Then response body will contain JPEG image of size 100x141
155
+
156
+ @compatibility
157
+ Scenario: Input file extension should be based on content detected mime type and not on provided path
158
+ Given test.jpg file content as request body
159
+ When I do PUT request http://localhost:3000/thumbnail/blah/test/image/test.gif
160
+ Then response status will be 400
161
+ And response content type will be text/plain
162
+ And response body will be CRLF ended lines
163
+ """
164
+ no thumbnailing specs were selected, please use at least one of: small, tiny_png, bad
165
+ """
@@ -2,34 +2,102 @@ Feature: Encoded UTF-8 URI support
2
2
  HTTP Image Store should be able to decode UTF-8 characters form URI using URI decode and also support JavaScript encode() format.
3
3
 
4
4
  Background:
5
+ Given S3 settings in AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_S3_TEST_BUCKET environment variables
5
6
  Given httpthumbnailer server is running at http://localhost:3100/health_check
6
7
  Given httpimagestore server is running at http://localhost:3000/health_check with the following configuration
7
8
  """
9
+ s3 key="@AWS_ACCESS_KEY_ID@" secret="@AWS_SECRET_ACCESS_KEY@" ssl=false
8
10
  path "path" "#{path}"
9
11
 
10
- post "encoding" "encoded" {
12
+ post "file" "encoding" "encoded" {
11
13
  store_file "input" root="/tmp" path="path"
12
14
  output_store_uri "input" path="path"
13
15
  }
14
16
 
15
- post "encoding" "decoded" {
17
+ post "file" "encoding" "decoded" {
16
18
  store_file "input" root="/tmp" path="path"
17
19
  output_store_path "input" path="path"
18
20
  }
21
+
22
+ post "s3" "encoding" "encoded" {
23
+ store_s3 "input" bucket="@AWS_S3_TEST_BUCKET@" path="path"
24
+ output_store_uri "input" path="path"
25
+ }
26
+
27
+ post "s3" "encoding" "decoded" {
28
+ store_s3 "input" bucket="@AWS_S3_TEST_BUCKET@" path="path"
29
+ output_store_path "input" path="path"
30
+ }
19
31
  """
20
32
 
21
- Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding
33
+ Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding for file based storage
22
34
  Given test.png file content as request body
23
- When I do POST request http://localhost:3000/encoding/encoded/triple%20kro%25u0301l.png
35
+ When I do POST request http://localhost:3000/file/encoding/encoded/triple%20kro%25u0301l.png
36
+ And response content type will be text/uri-list
24
37
  And response body will be CRLF ended lines
25
38
  """
26
- /triple%20kro%CC%81l.png
39
+ /triple%20kr%C3%B3l.png
27
40
  """
28
41
 
29
- Scenario: URL encoded variable decoding
42
+ Scenario: JavaScript encode() + URL encoded variable decoding and URL encoding for S3 based storage
30
43
  Given test.png file content as request body
31
- When I do POST request http://localhost:3000/encoding/decoded/triple%20kr%C3%B3l.png
44
+ When I do POST request http://localhost:3000/s3/encoding/encoded/triple%20kro%25u0301l.png
45
+ And response content type will be text/uri-list
46
+ And response body will be CRLF ended lines
47
+ """
48
+ /triple%20kr%C3%B3l.png
49
+ """
50
+
51
+ Scenario: URL encoded variable decoding and URL encoding with ? for file based storage
52
+ Given test.png file content as request body
53
+ When I do POST request http://localhost:3000/file/encoding/encoded/hello%3Fworld.png
54
+ And response content type will be text/uri-list
55
+ And response body will be CRLF ended lines
56
+ """
57
+ /hello%3Fworld.png
58
+ """
59
+
60
+ Scenario: URL encoded variable decoding and URL encoding with ? for S3 based storage
61
+ Given test.png file content as request body
62
+ When I do POST request http://localhost:3000/s3/encoding/encoded/hello%3Fworld.png
63
+ And response content type will be text/uri-list
64
+ And response body will be CRLF ended lines
65
+ """
66
+ /hello%3Fworld.png
67
+ """
68
+
69
+ Scenario: URL encoded variable decoding for file based storage
70
+ Given test.png file content as request body
71
+ When I do POST request http://localhost:3000/file/encoding/decoded/triple%20kr%C3%B3l.png
72
+ And response content type will be text/plain
32
73
  And response body will be CRLF ended lines
33
74
  """
34
75
  triple król.png
35
76
  """
77
+
78
+ Scenario: URL encoded variable decoding for S3 based storage
79
+ Given test.png file content as request body
80
+ When I do POST request http://localhost:3000/s3/encoding/decoded/triple%20kr%C3%B3l.png
81
+ And response content type will be text/plain
82
+ And response body will be CRLF ended lines
83
+ """
84
+ triple król.png
85
+ """
86
+
87
+ Scenario: URL path normalization with ? for file based storage
88
+ Given test.png file content as request body
89
+ When I do POST request http://localhost:3000/file/encoding/decoded/hello%3Fworld.png
90
+ And response content type will be text/plain
91
+ And response body will be CRLF ended lines
92
+ """
93
+ hello?world.png
94
+ """
95
+
96
+ Scenario: URL path normalization with ? S3 file based storage
97
+ Given test.png file content as request body
98
+ When I do POST request http://localhost:3000/s3/encoding/decoded/hello%3Fworld.png
99
+ And response content type will be text/plain
100
+ And response body will be CRLF ended lines
101
+ """
102
+ hello?world.png
103
+ """
@@ -52,7 +52,7 @@ Feature: Rewrite of the output path and URL
52
52
 
53
53
  post "rewrite" "demo" {
54
54
  store_file "input" root="/tmp" path="input_digest"
55
- output_store_url "input" scheme="ftp" host="example.com" port="21" path="demo"
55
+ output_store_url "input" scheme="ftp" host="example.com" port="421" path="demo"
56
56
  }
57
57
  """
58
58
 
@@ -109,7 +109,7 @@ Feature: Rewrite of the output path and URL
109
109
  When I do POST request http://localhost:3000/rewrite/url/port?number=41
110
110
  And response body will be CRLF ended lines
111
111
  """
112
- file::41/b0fe25319ba5909a
112
+ file://localhost:41/b0fe25319ba5909a
113
113
  """
114
114
 
115
115
  Scenario: Store URL rewriting demo
@@ -117,6 +117,6 @@ Feature: Rewrite of the output path and URL
117
117
  When I do POST request http://localhost:3000/rewrite/demo
118
118
  And response body will be CRLF ended lines
119
119
  """
120
- ftp://example.com:21/image/b0fe25319ba5909aa97fded546847a96d7fdf26e18715b0cfccfcbee52dce57e.jpg
120
+ ftp://example.com:421/image/b0fe25319ba5909aa97fded546847a96d7fdf26e18715b0cfccfcbee52dce57e.jpg
121
121
  """
122
122
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "httpimagestore"
8
- s.version = "1.8.0"
8
+ s.version = "1.8.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jakub Pastuszek"]
12
- s.date = "2014-08-22"
12
+ s.date = "2014-08-28"
13
13
  s.description = "Thumbnails images using httpthumbnailer and stored data on HTTP server (S3)"
14
14
  s.email = "jpastuszek@gmail.com"
15
15
  s.executables = ["httpimagestore"]
@@ -98,6 +98,7 @@ Gem::Specification.new do |s|
98
98
  s.add_runtime_dependency(%q<mime-types>, ["~> 1.17"])
99
99
  s.add_runtime_dependency(%q<sdl4r>, ["~> 0.9"])
100
100
  s.add_runtime_dependency(%q<msgpack>, ["~> 0.5"])
101
+ s.add_runtime_dependency(%q<addressable>, ["~> 2.3"])
101
102
  s.add_development_dependency(%q<faraday>, [">= 0.8"])
102
103
  s.add_development_dependency(%q<rspec>, ["~> 2.13"])
103
104
  s.add_development_dependency(%q<cucumber>, [">= 0"])
@@ -113,6 +114,7 @@ Gem::Specification.new do |s|
113
114
  s.add_dependency(%q<mime-types>, ["~> 1.17"])
114
115
  s.add_dependency(%q<sdl4r>, ["~> 0.9"])
115
116
  s.add_dependency(%q<msgpack>, ["~> 0.5"])
117
+ s.add_dependency(%q<addressable>, ["~> 2.3"])
116
118
  s.add_dependency(%q<faraday>, [">= 0.8"])
117
119
  s.add_dependency(%q<rspec>, ["~> 2.13"])
118
120
  s.add_dependency(%q<cucumber>, [">= 0"])
@@ -129,6 +131,7 @@ Gem::Specification.new do |s|
129
131
  s.add_dependency(%q<mime-types>, ["~> 1.17"])
130
132
  s.add_dependency(%q<sdl4r>, ["~> 0.9"])
131
133
  s.add_dependency(%q<msgpack>, ["~> 0.5"])
134
+ s.add_dependency(%q<addressable>, ["~> 2.3"])
132
135
  s.add_dependency(%q<faraday>, [">= 0.8"])
133
136
  s.add_dependency(%q<rspec>, ["~> 2.13"])
134
137
  s.add_dependency(%q<cucumber>, [">= 0"])
@@ -2,7 +2,7 @@ require 'httpimagestore/configuration/path'
2
2
  require 'httpimagestore/configuration/handler'
3
3
  require 'httpimagestore/configuration/source_failover'
4
4
  require 'pathname'
5
- require 'uri'
5
+ require 'addressable/uri'
6
6
 
7
7
  module Configuration
8
8
  class FileStorageOutsideOfRootDirError < ConfigurationError
@@ -42,22 +42,27 @@ module Configuration
42
42
  end
43
43
 
44
44
  def initialize(global, image_name, matcher, root_dir, path_spec)
45
- super global, image_name, matcher
45
+ super global, image_name, matcher, path_spec
46
46
  @root_dir = Pathname.new(root_dir).cleanpath
47
- @path_spec = path_spec
48
47
  end
49
48
 
50
49
  def storage_path(rendered_path)
51
50
  path = Pathname.new(rendered_path)
52
51
 
53
52
  storage_path = (@root_dir + path).cleanpath
54
- storage_path.to_s =~ /^#{@root_dir.to_s}/ or raise FileStorageOutsideOfRootDirError.new(@image_name, path)
53
+ storage_path.to_s =~ /^#{@root_dir.to_s}/ or raise FileStorageOutsideOfRootDirError.new(image_name, path)
55
54
 
56
55
  storage_path
57
56
  end
58
57
 
58
+ def file_url(rendered_path)
59
+ uri = rendered_path.to_uri
60
+ uri.scheme = 'file'
61
+ uri
62
+ end
63
+
59
64
  def to_s
60
- "FileSource[image_name: '#{@image_name}' root_dir: '#{@root_dir}' path_spec: '#{@path_spec}']"
65
+ "FileSource[image_name: '#{image_name}' root_dir: '#{@root_dir}' path_spec: '#{path_spec}']"
61
66
  end
62
67
  end
63
68
 
@@ -86,7 +91,7 @@ module Configuration
86
91
  FileSourceStoreBase.stats.incr_total_file_source_bytes(data.bytesize)
87
92
 
88
93
  image = Image.new(data)
89
- image.source_url = URI::Generic.new('file', nil, nil, nil, nil, "/#{URI.encode(rendered_path.to_s)}", nil, nil, nil, URI::DEFAULT_PARSER, true)
94
+ image.source_url = file_url(rendered_path)
90
95
  image
91
96
  rescue Errno::ENOENT
92
97
  raise NoSuchFileError.new(image_name, rendered_path)
@@ -112,7 +117,7 @@ module Configuration
112
117
  get_named_image_for_storage(request_state) do |image_name, image, rendered_path|
113
118
  storage_path = storage_path(rendered_path)
114
119
 
115
- image.store_url = URI::Generic.new('file', nil, nil, nil, nil, "/#{URI.encode(rendered_path.to_s)}", nil, nil, nil, URI::DEFAULT_PARSER, true)
120
+ image.store_url = file_url(rendered_path)
116
121
 
117
122
  log.info "storing '#{image_name}' in file '#{storage_path}' (#{image.data.length} bytes)"
118
123
  storage_path.open('wb'){|io| io.write image.data}
@@ -85,7 +85,8 @@ module Configuration
85
85
  attr_reader :memory_limit
86
86
  attr_reader :headers
87
87
 
88
- def with_locals(locals)
88
+ def with_locals(*locals)
89
+ locals = locals.reduce{|a, b| a.merge(b)}
89
90
  log.debug "using additional local variables: #{locals}"
90
91
  self.dup.merge!(locals)
91
92
  end
@@ -98,10 +99,6 @@ module Configuration
98
99
  @output_callback or fail 'no output callback'
99
100
  end
100
101
 
101
- def render_template(template)
102
- RubyStringTemplate.new(template).render(self)
103
- end
104
-
105
102
  def fetch_base_variable(name, base_name)
106
103
  fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
107
104
  end
@@ -195,51 +192,86 @@ module Configuration
195
192
  end
196
193
  end
197
194
 
198
- module ConditionalInclusion
199
- def inclusion_matcher(matcher)
200
- (@matchers ||= []) << matcher if matcher
201
- end
195
+ class HandlerStatement
196
+ module ImageName
197
+ attr_reader :image_name
202
198
 
203
- def included?(request_state)
204
- return true unless @matchers
205
- @matchers.any? do |matcher|
206
- matcher.included?(request_state)
199
+ def initialize(global, *args)
200
+ @image_name = args.pop
201
+
202
+ super(global, *args)
203
+
204
+ config_local :imagename, @image_name # deprecated
205
+ config_local :image_name, @image_name
207
206
  end
208
207
  end
209
208
 
210
- def excluded?(request_state)
211
- not included? request_state
209
+ module PathSpec
210
+ attr_reader :path_spec
211
+
212
+ def initialize(global, *args)
213
+ @path_spec = args.pop
214
+ super(global, *args)
215
+ end
216
+
217
+ def path_template
218
+ @global.paths[@path_spec]
219
+ end
212
220
  end
213
- end
214
221
 
215
- class SourceStoreBase
216
- include ConditionalInclusion
222
+ module ConditionalInclusion
223
+ def initialize(global, *args)
224
+ @matchers = []
225
+ matcher = args.pop
226
+ @matchers << matcher if matcher
227
+ super(global, *args)
228
+ end
217
229
 
218
- def initialize(global, image_name, matcher)
219
- @global = global
220
- @image_name = image_name
221
- @locals = {}
230
+ def inclusion_matcher(matcher)
231
+ @matchers << matcher
232
+ end
233
+
234
+ def included?(request_state)
235
+ return true if @matchers.empty?
236
+ @matchers.any? do |matcher|
237
+ matcher.included?(request_state)
238
+ end
239
+ end
222
240
 
223
- inclusion_matcher matcher
224
- local :imagename, @image_name # deprecated
225
- local :image_name, @image_name
241
+ def excluded?(request_state)
242
+ not included? request_state
243
+ end
226
244
  end
227
245
 
228
- private
246
+ def initialize(global, *args)
247
+ @global = global
248
+ @config_locals = {}
249
+ @module_args = args
250
+ end
229
251
 
230
- attr_accessor :image_name
252
+ attr_reader :config_locals
253
+ def config_local(name, value)
254
+ @config_locals[name] = value
255
+ end
231
256
 
232
- def local(name, value)
233
- @locals[name] = value
257
+ def path_template(path_spec)
258
+ @global.paths[path_spec]
234
259
  end
260
+ end
235
261
 
236
- def rendered_path(request_state)
237
- path = @global.paths[@path_spec]
238
- Pathname.new(path.render(request_state.with_locals(@locals))).cleanpath.to_s
262
+ class SourceStoreBase < HandlerStatement
263
+ include ImageName
264
+ include PathSpec
265
+ include ConditionalInclusion
266
+
267
+ def initialize(global, image_name, matcher, path_spec)
268
+ super(global, image_name, path_spec, matcher)
239
269
  end
240
270
 
271
+ private
272
+
241
273
  def put_sourced_named_image(request_state)
242
- rendered_path = rendered_path(request_state)
274
+ rendered_path = path_template.render(request_state.with_locals(config_locals))
243
275
 
244
276
  image = yield @image_name, rendered_path
245
277
 
@@ -249,7 +281,7 @@ module Configuration
249
281
 
250
282
  def get_named_image_for_storage(request_state)
251
283
  image = request_state.images[@image_name]
252
- rendered_path = rendered_path(request_state)
284
+ rendered_path = path_template.render(request_state.with_locals(config_locals))
253
285
  image.store_path = rendered_path
254
286
 
255
287
  yield @image_name, image, rendered_path
@@ -3,7 +3,7 @@ require 'httpimagestore/ruby_string_template'
3
3
  require 'httpimagestore/configuration/handler'
4
4
 
5
5
  module Configuration
6
- class Identify
6
+ class Identify < HandlerStatement
7
7
  include ClassLogging
8
8
 
9
9
  extend Stats
@@ -12,8 +12,6 @@ module Configuration
12
12
  :total_identify_requests_bytes
13
13
  )
14
14
 
15
- include ConditionalInclusion
16
-
17
15
  def self.match(node)
18
16
  node.name == 'identify'
19
17
  end
@@ -27,10 +25,11 @@ module Configuration
27
25
  configuration.processors << self.new(configuration.global, image_name, matcher)
28
26
  end
29
27
 
28
+ include ImageName
29
+ include ConditionalInclusion
30
+
30
31
  def initialize(global, image_name, matcher = nil)
31
- @global = global
32
- @image_name = image_name
33
- inclusion_matcher matcher if matcher
32
+ super(global, image_name, matcher)
34
33
  end
35
34
 
36
35
  def realize(request_state)
@@ -1,6 +1,6 @@
1
1
  require 'httpimagestore/configuration/handler'
2
2
  require 'httpimagestore/ruby_string_template'
3
- require 'uri'
3
+ require 'addressable/uri'
4
4
  require 'base64'
5
5
 
6
6
  module Configuration
@@ -64,60 +64,47 @@ module Configuration
64
64
  Handler::register_node_parser OutputText
65
65
 
66
66
  class OutputMultiBase
67
- class OutputSpec
67
+ class OutputSpec < HandlerStatement
68
+ include ImageName
69
+ include PathSpec
68
70
  include ConditionalInclusion
69
- attr_reader :image_name
70
- attr_reader :path_spec
71
71
 
72
72
  def initialize(global, image_name, scheme, host, port, path_spec, matcher)
73
- @global = global
74
- @image_name = image_name
75
- @scheme = scheme
76
- @host = host
77
- @port = port
78
- @path_spec = path_spec
79
- inclusion_matcher matcher
73
+ super(global, image_name, path_spec, matcher)
74
+ @scheme = scheme && scheme.to_template
75
+ @host = host && host.to_template
76
+ @port = port && port.to_template
80
77
  end
81
78
 
82
79
  def store_path(request_state)
83
80
  store_path = request_state.images[@image_name].store_path or raise StorePathNotSetForImage.new(@image_name)
84
81
  return store_path unless @path_spec
85
82
 
86
- locals = {
87
- image_name: @image_name,
88
- path: store_path
89
- }
90
-
91
- rendered_path(request_state.with_locals(locals))
83
+ path_template.render(request_state.with_locals(config_locals, path: store_path))
92
84
  end
93
85
 
94
86
  def store_url(request_state)
95
87
  url = request_state.images[@image_name].store_url or raise StoreURLNotSetForImage.new(@image_name)
96
88
  url = url.dup
89
+ store_path = request_state.images[@image_name].store_path or raise StorePathNotSetForImage.new(@image_name)
97
90
 
98
- locals = {
99
- image_name: @image_name,
100
- path: URI.utf_decode(url.path),
91
+ request_locals = {
92
+ path: store_path,
101
93
  url: url.to_s
102
94
  }
103
- locals[:scheme] = url.scheme if url.scheme
104
- locals[:host] = url.host if url.host
105
- locals[:port] = url.port if url.port
95
+ request_locals[:scheme] = url.scheme if url.scheme
96
+ request_locals[:host] = url.host if url.host
97
+ request_locals[:port] = url.port if url.port
106
98
 
107
- request_state = request_state.with_locals(locals)
99
+ request_state = request_state.with_locals(config_locals, request_locals)
108
100
 
109
101
  # optional rewrites
110
- url.scheme = request_state.render_template(@scheme) if @scheme
111
- url.host = request_state.render_template(@host) if @host
112
- url.port = request_state.render_template(@port).to_i if @port
113
- url.path = URI.encode(rendered_path(request_state)).tap{|path| path.replace('/' + path) if path[0] != '/'} if @path_spec
114
-
115
- url
116
- end
102
+ url.scheme = @scheme.render(request_state) if @scheme
103
+ url.host = @host.render(request_state) if @host
104
+ (url.host ||= 'localhost'; url.port = @port.render(request_state).to_i) if @port
105
+ url.path = path_template.render(request_state).to_uri if @path_spec
117
106
 
118
- private
119
- def rendered_path(request_state)
120
- Pathname.new(@global.paths[@path_spec].render(request_state)).cleanpath.to_s
107
+ url.normalize
121
108
  end
122
109
  end
123
110
 
@@ -1,5 +1,6 @@
1
1
  require 'httpimagestore/ruby_string_template'
2
2
  require 'digest/sha2'
3
+ require 'addressable/uri'
3
4
 
4
5
  module Configuration
5
6
  class PathNotDefinedError < ConfigurationError
@@ -21,6 +22,14 @@ module Configuration
21
22
  end
22
23
 
23
24
  class Path < RubyStringTemplate
25
+ class RenderedPath < String
26
+ def to_uri
27
+ uri_path = self.gsub(/^\/?([a-zA-Z])[\|:][\\\/]/){"/#{$1.downcase}:/"} # fix windows backslash
28
+ uri_path = Addressable::URI::SLASH + uri_path if uri_path[0] != Addressable::URI::SLASH # make absolute
29
+ Addressable::URI.new(path: uri_path).normalize
30
+ end
31
+ end
32
+
24
33
  def self.match(node)
25
34
  node.name == 'path'
26
35
  end
@@ -50,6 +59,10 @@ module Configuration
50
59
  end or raise NoValueForPathTemplatePlaceholerError.new(path_name, template, name)
51
60
  end
52
61
  end
62
+
63
+ def render(locals = {})
64
+ RenderedPath.new(super)
65
+ end
53
66
  end
54
67
  Global.register_node_parser Path
55
68
  end
@@ -1,6 +1,7 @@
1
1
  require 'aws-sdk'
2
2
  require 'digest/sha2'
3
3
  require 'msgpack'
4
+ require 'addressable/uri'
4
5
  require 'httpimagestore/aws_sdk_regions_hack'
5
6
  require 'httpimagestore/configuration/path'
6
7
  require 'httpimagestore/configuration/handler'
@@ -177,7 +178,7 @@ module Configuration
177
178
  end
178
179
 
179
180
  def private_url
180
- s3_object.url_for(:read, expires: 60 * 60 * 24 * 365 * 20) # expire in 20 years
181
+ s3_object.url_for(:read, expires: 60 * 60 * 24 * 365 * 20)
181
182
  end
182
183
 
183
184
  def public_url
@@ -237,19 +238,19 @@ module Configuration
237
238
  end
238
239
 
239
240
  def private_url
240
- url = @cache_file.header['private_url'] and return URI(url)
241
+ url = @cache_file.header['private_url'] and return Addressable::URI.parse(url)
241
242
  dirty! :private_url
242
243
  url = super
243
244
  @cache_file.header['private_url'] = url.to_s
244
- url
245
+ Addressable::URI.parse(url)
245
246
  end
246
247
 
247
248
  def public_url
248
- url = @cache_file.header['public_url'] and return URI(url)
249
+ url = @cache_file.header['public_url'] and return Addressable::URI.parse(url)
249
250
  dirty! :public_url
250
251
  url = super
251
252
  @cache_file.header['public_url'] = url.to_s
252
- url
253
+ Addressable::URI.parse(url)
253
254
  end
254
255
 
255
256
  def content_type
@@ -314,9 +315,8 @@ module Configuration
314
315
  end
315
316
 
316
317
  def initialize(global, image_name, matcher, bucket, path_spec, public_access, cache_control, prefix, cache_root)
317
- super global, image_name, matcher
318
+ super global, image_name, matcher, path_spec
318
319
  @bucket = bucket
319
- @path_spec = path_spec
320
320
  @public_access = public_access
321
321
  @cache_control = cache_control
322
322
  @prefix = prefix
@@ -333,7 +333,7 @@ module Configuration
333
333
  log.warn "not using S3 object cache for image '#{image_name}'", error
334
334
  end
335
335
 
336
- local :bucket, @bucket
336
+ config_local :bucket, @bucket
337
337
  end
338
338
 
339
339
  def client
@@ -406,7 +406,7 @@ module Configuration
406
406
  end
407
407
 
408
408
  def to_s
409
- "S3Source[image_name: '#{@image_name}' bucket: '#{@bucket}' prefix: '#{@prefix}' path_spec: '#{@path_spec}']"
409
+ "S3Source[image_name: '#{image_name}' bucket: '#{@bucket}' prefix: '#{@prefix}' path_spec: '#{path_spec}']"
410
410
  end
411
411
  end
412
412
  Handler::register_node_parser S3Source
@@ -31,7 +31,13 @@ module Configuration
31
31
  end
32
32
  end
33
33
 
34
- class Thumbnail
34
+ class NoSpecSelectedError < RuntimeError
35
+ def initialize(specs)
36
+ super "no thumbnailing specs were selected, please use at least one of: #{specs.join(', ')}"
37
+ end
38
+ end
39
+
40
+ class Thumbnail < HandlerStatement
35
41
  include ClassLogging
36
42
 
37
43
  extend Stats
@@ -55,34 +61,27 @@ module Configuration
55
61
  attr_reader :remote_error
56
62
  end
57
63
 
58
- class ThumbnailSpec
59
- class Spec < RubyStringTemplate
60
- def initialize(image_name, sepc_name, template)
61
- super(template) do |locals, name|
62
- locals[name] or raise NoValueForSpecTemplatePlaceholerError.new(image_name, sepc_name, name, template)
63
- end
64
- end
65
- end
66
-
64
+ class ThumbnailSpec < HandlerStatement
65
+ include ImageName
67
66
  include ConditionalInclusion
68
67
 
69
68
  def initialize(image_name, method, width, height, format, options = {}, matcher = nil)
70
- @image_name = image_name
71
- @method = Spec.new(image_name, 'method', method)
72
- @width = Spec.new(image_name, 'width', width)
73
- @height = Spec.new(image_name, 'height', height)
74
- @format = Spec.new(image_name, 'format', format)
75
- @options = options.inject({}){|h, v| h[v.first] = Spec.new(image_name, v.first, v.last); h}
76
- inclusion_matcher matcher if matcher
69
+ super(nil, image_name, matcher)
70
+ @method = method.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'method', key, method)}
71
+ @width = width.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'width', key, width)}
72
+ @height = height.to_s.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'height', key, height)}
73
+ @format = format.to_template.with_missing_resolver{|locals, key| raise NoValueForSpecTemplatePlaceholerError.new(image_name, 'format', key, format)}
74
+
75
+ @options = options.merge(options) do |option, old, template|
76
+ template.to_s.to_template.with_missing_resolver{|locals, field| raise NoValueForSpecTemplatePlaceholerError.new(image_name, option, field, template)}
77
+ end
77
78
  end
78
79
 
79
- attr_reader :image_name
80
-
81
80
  def render(locals = {})
82
81
  options = @options.inject({}){|h, v| h[v.first] = v.last.render(locals); h}
83
82
  nested_options = options['options'] ? Hash[options.delete('options').to_s.split(',').map{|pair| pair.split(':', 2)}] : {}
84
83
  {
85
- @image_name =>
84
+ image_name =>
86
85
  [
87
86
  @method.render(locals),
88
87
  @width.render(locals),
@@ -94,8 +93,6 @@ module Configuration
94
93
  end
95
94
  end
96
95
 
97
- include ConditionalInclusion
98
-
99
96
  def self.match(node)
100
97
  node.name == 'thumbnail'
101
98
  end
@@ -142,12 +139,13 @@ module Configuration
142
139
  )
143
140
  end
144
141
 
142
+ include ConditionalInclusion
143
+
145
144
  def initialize(global, source_image_name, specs, use_multipart_api, matcher)
146
- @global = global
145
+ super(global, matcher)
147
146
  @source_image_name = source_image_name
148
147
  @specs = specs
149
148
  @use_multipart_api = use_multipart_api
150
- inclusion_matcher matcher
151
149
  end
152
150
 
153
151
  def realize(request_state)
@@ -159,6 +157,9 @@ module Configuration
159
157
  end.each do |spec|
160
158
  rendered_specs.merge! spec.render(request_state)
161
159
  end
160
+
161
+ rendered_specs.empty? and raise NoSpecSelectedError.new(@specs.map(&:image_name))
162
+
162
163
  source_image = request_state.images[@source_image_name]
163
164
 
164
165
  thumbnails = {}
@@ -16,7 +16,10 @@ class ErrorReporter < Controller
16
16
  write_error status, error
17
17
  end
18
18
 
19
- on error Configuration::ZeroBodyLengthError do |error|
19
+ on error(
20
+ Configuration::ZeroBodyLengthError,
21
+ Configuration::NoSpecSelectedError
22
+ ) do |error|
20
23
  write_error 400, error
21
24
  end
22
25
 
@@ -1,4 +1,4 @@
1
- class RubyStringTemplate
1
+ class RubyStringTemplate < String
2
2
  class NoValueForTemplatePlaceholderError < ArgumentError
3
3
  def initialize(name, template)
4
4
  super "no value for '\#{#{name}}' in template '#{template}'"
@@ -6,18 +6,32 @@ class RubyStringTemplate
6
6
  end
7
7
 
8
8
  def initialize(template, &resolver)
9
- @template = template.to_s
10
- @resolver = resolver ? resolver : ->(locals, name){locals[name]}
9
+ super(template.to_s)
10
+ @resolvers = []
11
+ @resolvers << resolver if resolver
12
+ @resolvers << ->(locals, name){locals[name]}
13
+ end
14
+
15
+ def initialize_copy(source)
16
+ super
17
+ # copy resolvers array
18
+ resolvers =
19
+ source.instance_eval do
20
+ resolvers = @resolvers
21
+ end
22
+ @resolvers = resolvers.dup
11
23
  end
12
24
 
13
25
  def render(locals = {})
14
- template = @template.dup
26
+ template = self.to_s
15
27
  values = {}
16
28
 
29
+ # use gsub with block instead!
17
30
  template.scan(/#\{[^\}]+\}/um).uniq.each do |placeholder|
18
31
  name = placeholder.match(/#\{([^\}]*)\}/u).captures.first.to_sym
19
- value = @resolver.call(locals, name)
20
- value or fail NoValueForTemplatePlaceholderError.new(name, @template)
32
+
33
+ value = nil
34
+ @resolvers.find{|resolver| value = resolver.call(locals, name)} or fail NoValueForTemplatePlaceholderError.new(name, self)
21
35
  values[placeholder] = value.to_s
22
36
  end
23
37
 
@@ -27,5 +41,28 @@ class RubyStringTemplate
27
41
 
28
42
  template
29
43
  end
44
+
45
+ def to_template
46
+ self
47
+ end
48
+
49
+ #def inspect
50
+ #super + "[#{@resolvers}]"
51
+ #end
52
+
53
+ def add_missing_resolver(&resolver)
54
+ @resolvers << resolver
55
+ end
56
+
57
+ def with_missing_resolver(&resolver)
58
+ new_template = self.dup
59
+ new_template.add_missing_resolver(&resolver)
60
+ new_template
61
+ end
30
62
  end
31
63
 
64
+ class String
65
+ def to_template
66
+ RubyStringTemplate.new(self)
67
+ end
68
+ end
@@ -52,7 +52,7 @@ describe Configuration do
52
52
  it 'should have source path and url' do
53
53
  subject.handlers[0].sources[0].realize(state)
54
54
 
55
- state.images['original'].source_path.should == "test.in"
55
+ state.images['original'].source_path.to_s.should == "test.in"
56
56
  state.images['original'].source_url.to_s.should == "file:/test.in"
57
57
  end
58
58
 
@@ -75,7 +75,7 @@ describe Configuration do
75
75
 
76
76
  it 'should provide image name to be used as #{image_name}' do
77
77
  subject.handlers[0].sources[0].realize(state)
78
- state.images['test-image-name'].source_path.should == 'test-image-name.jpg'
78
+ state.images['test-image-name'].source_path.to_s.should == 'test-image-name.jpg'
79
79
  state.images['test-image-name'].data.should == 'hello world'
80
80
  end
81
81
  end
@@ -543,7 +543,7 @@ describe Configuration do
543
543
  env.res.data.should == "file://localhost/abc/test.out\r\n"
544
544
  end
545
545
 
546
- it 'should allow rewriting port component' do
546
+ it 'should allow rewriting port component (defaults host to localhost)' do
547
547
  subject = Configuration.read(<<-'EOF')
548
548
  path "out" "abc/test.out"
549
549
 
@@ -560,13 +560,13 @@ describe Configuration do
560
560
 
561
561
  env.instance_eval &state.output_callback
562
562
  env.res['Content-Type'].should == 'text/uri-list'
563
- env.res.data.should == "file::21/abc/test.out\r\n"
563
+ env.res.data.should == "file://localhost:21/abc/test.out\r\n"
564
564
  end
565
565
 
566
566
  it 'should allow using variables for all supported rewrites' do
567
567
  state = Configuration::RequestState.new('abc',
568
568
  remote: 'example.com',
569
- remote_port: 21,
569
+ remote_port: 421,
570
570
  proto: 'ftp'
571
571
  )
572
572
  subject = Configuration.read(<<-'EOF')
@@ -586,7 +586,7 @@ describe Configuration do
586
586
 
587
587
  env.instance_eval &state.output_callback
588
588
  env.res['Content-Type'].should == 'text/uri-list'
589
- env.res.data.should == "ftp://example.com:21/hello/abc/world/test-xyz.out\r\n"
589
+ env.res.data.should == "ftp://example.com:421/hello/abc/world/test-xyz.out\r\n"
590
590
  end
591
591
  end
592
592
 
@@ -218,7 +218,7 @@ else
218
218
  it 'should keep private source URL' do
219
219
  subject.handlers[0].sources[0].realize(state)
220
220
 
221
- state.images['original'].source_url.should be_a URI::HTTPS
221
+ state.images['original'].source_url.should be_a Addressable::URI
222
222
  state.images['original'].source_url.to_s.should == 'https://s3-eu-west-1.amazonaws.com/test/ghost.jpg?' + ENV['AWS_ACCESS_KEY_ID']
223
223
  end
224
224
 
@@ -232,7 +232,7 @@ else
232
232
  EOF
233
233
  subject.handlers[0].sources[0].realize(state)
234
234
 
235
- state.images['original'].source_url.should be_a URI::HTTPS
235
+ state.images['original'].source_url.should be_a Addressable::URI
236
236
  state.images['original'].source_url.to_s.should == 'https://s3-eu-west-1.amazonaws.com/test/ghost.jpg'
237
237
  end
238
238
 
@@ -1,8 +1,8 @@
1
1
  require_relative 'spec_helper'
2
2
  require 'httpimagestore/configuration'
3
- require 'httpimagestore/configuration/output'
4
3
  MemoryLimit.logger = Configuration::Scope.logger = RootLogger.new('/dev/null')
5
4
 
5
+ require 'httpimagestore/configuration/output'
6
6
  require 'httpimagestore/configuration/thumbnailer'
7
7
 
8
8
  describe Configuration do
@@ -42,6 +42,20 @@ describe RubyStringTemplate do
42
42
  subject.render(hello: 'hello', test: 123)
43
43
  }.to raise_error RubyStringTemplate::NoValueForTemplatePlaceholderError, %q{no value for '#{world}' in template '>#{hello}-#{world}#{test}<'}
44
44
  end
45
+
46
+ it 'should allow using custom missing value resolver' do
47
+ template = subject
48
+ expect {
49
+ template.with_missing_resolver do |locals, key|
50
+ fail "missing key: #{key}"
51
+ end
52
+ .render(hello: 'hello', test: 123)
53
+ }.to raise_error RuntimeError, "missing key: world"
54
+
55
+ expect {
56
+ template.render(hello: 'hello', test: 123)
57
+ }.to raise_error RubyStringTemplate::NoValueForTemplatePlaceholderError, %q{no value for '#{world}' in template '>#{hello}-#{world}#{test}<'}
58
+ end
45
59
  end
46
60
  end
47
61
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpimagestore
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.8.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-08-22 00:00:00.000000000 Z
12
+ date: 2014-08-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: unicorn-cuba-base
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: '0.5'
110
+ - !ruby/object:Gem::Dependency
111
+ name: addressable
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '2.3'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: '2.3'
110
126
  - !ruby/object:Gem::Dependency
111
127
  name: faraday
112
128
  requirement: !ruby/object:Gem::Requirement
@@ -323,7 +339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
323
339
  version: '0'
324
340
  segments:
325
341
  - 0
326
- hash: -4420249284992726613
342
+ hash: 3314645469105412308
327
343
  required_rubygems_version: !ruby/object:Gem::Requirement
328
344
  none: false
329
345
  requirements: