httpimagestore 1.8.0 → 1.8.1

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