social-avatar-proxy 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -16,4 +16,5 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  **.DS_Store
19
- spec/internal/log/test.log
19
+ spec/internal/log/test.log
20
+ .tmp
data/.travis.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ services:
5
+ - memcached
data/README.md CHANGED
@@ -54,22 +54,55 @@ image_tag(facebook_avatar_path(12345))
54
54
  You might wish to configure a default image which will be rendered if an avatar could not be found, or if the remote service has an error (e.g. times out or has a redirect loop), this is simple with an initializer:
55
55
 
56
56
  ```ruby
57
- require "social_avatar_proxy/config"
58
-
59
57
  # choose our image
60
- image_path = Rails.root.join("public", "default_avatar.jpg")
58
+ image_path = Rails.root.join(*%W(app assets images default_avatar.jpg))
61
59
  # set the config
62
- SocialAvatarProxy::Config.default_image = image_path
60
+ SocialAvatarProxy.configure do
61
+ default_image(image_path)
62
+ end
63
+ ```
64
+
65
+ HTTP caching can be configured as below:
66
+
67
+ ```ruby
68
+ SocialAvatarProxy.configure do
69
+ http_cache do
70
+ expires 10.minutes
71
+ cache_control({
72
+ max_age: 5.days,
73
+ max_stale: 1.day,
74
+ public: true
75
+ })
76
+ end
77
+ end
78
+ ```
79
+
80
+ Files can be cached in either or both of Memcached and the file system, here is the configuration options:
81
+
82
+ ```ruby
83
+ SocialAvatarProxy.configure do
84
+ file_cache do
85
+ directory "/path/to/folder"
86
+ end
87
+
88
+ memcache do
89
+ enable # only required if you don't set either of the following options
90
+ host "192.168.1.2:11211" # defaults to "localhost:11211"
91
+ namespace "something:" # defaults to "sap/"
92
+ end
93
+ end
63
94
  ```
64
95
 
65
96
  ### Rack
66
97
 
67
98
  The Rack app is available at: SocialAvatarProxy::App.
68
99
 
69
- Both the class and an instance respond to `call` so either can be mounted. The reason for this is that you might wish to set some options first, example:
100
+ Both the class and an instance respond to `call` so either can be mounted.
70
101
 
71
102
  ```ruby
72
- use SocialAvatarProxy::App.new(cache_control: false)
103
+ use SocialAvatarProxy::App
104
+ # or
105
+ use SocialAvatarProxy::App.new
73
106
  ```
74
107
 
75
108
  ## Contributing
@@ -2,33 +2,23 @@ require "social_avatar_proxy/config"
2
2
  require "social_avatar_proxy/facebook_avatar"
3
3
  require "social_avatar_proxy/twitter_avatar"
4
4
  require "social_avatar_proxy/routes"
5
+ require "social_avatar_proxy/response"
5
6
  require "social_avatar_proxy/timeout_error"
6
7
  require "social_avatar_proxy/too_many_redirects_error"
7
8
  require "rack"
8
9
 
9
10
  module SocialAvatarProxy
10
11
  class App
11
- def self.call(env, options = {})
12
- new(options).call(env)
12
+ def self.call(env)
13
+ new.call(env)
13
14
  end
14
15
 
15
- def self.routes(options = {})
16
- new(options).routes
16
+ def self.routes
17
+ new.routes
17
18
  end
18
19
 
19
20
  attr_reader :options, :request
20
21
 
21
- def initialize(options = {})
22
- @options = {
23
- expires: 86400, # 1 day from now
24
- cache_control: {
25
- max_age: 86400, # store for 1 day, after that re-request
26
- max_stale: 86400, # allow cache to be a day stale
27
- public: true # allow proxy caching
28
- }
29
- }.merge(options)
30
- end
31
-
32
22
  def call(env)
33
23
  @request = Rack::Request.new(env)
34
24
  begin
@@ -41,7 +31,7 @@ module SocialAvatarProxy
41
31
  end
42
32
 
43
33
  def path_prefix
44
- (options[:path_prefix] || (request && request.env["SCRIPT_NAME"]) || "").gsub(/\/$/, "")
34
+ ((request && request.env["SCRIPT_NAME"]) || "").gsub(/\/$/, "")
45
35
  end
46
36
 
47
37
  def response
@@ -49,60 +39,11 @@ module SocialAvatarProxy
49
39
  unless request.path =~ /^#{path_prefix}\/(facebook|twitter)\/([\w\-\.]+)(\.(jpe?g|png|gif|bmp))?$/i
50
40
  return not_found
51
41
  end
52
- # load the avatar
53
- avatar = load_avatar($1, $2)
54
- # if it exists
55
- if avatar.exist?
56
- # render the avatar to the response
57
- Rack::Response.new do |response|
58
- # set the response data
59
- response.write(avatar.body)
60
- # set the response headers
61
- response = set_avatar_headers(response, avatar)
62
- response = set_caching_headers(response)
63
- # return the response
64
- response
65
- end
66
- # if the avatar doesn't exist
67
- else
68
- not_found
69
- end
70
- end
71
-
72
- def not_found
73
- Rack::Response.new([], 404) do |response|
74
- # if we have a custom default image
75
- if Config.default_image
76
- render_default_image(response)
77
- # without a default image
78
- else
79
- response.write "Not Found"
80
- end
81
- end
82
- end
83
-
84
- def timeout
85
- Rack::Response.new([], 504) do |response|
86
- # if we have a custom default image
87
- if Config.default_image
88
- render_default_image(response)
89
- # without a default image
90
- else
91
- response.write "Remote server timeout"
92
- end
93
- end
94
- end
95
-
96
- def bad_gateway
97
- Rack::Response.new([], 502) do |response|
98
- # if we have a custom default image
99
- if Config.default_image
100
- render_default_image(response)
101
- # without a default image
102
- else
103
- response.write "Bad response from remote server"
104
- end
105
- end
42
+ # create our response
43
+ SocialAvatarProxy::Response.build({
44
+ service: $1,
45
+ identifier: $2
46
+ })
106
47
  end
107
48
 
108
49
  def routes
@@ -110,51 +51,16 @@ module SocialAvatarProxy
110
51
  end
111
52
 
112
53
  private
113
- def render_default_image(response)
114
- # render the image
115
- response.write(Config.default_image_data)
116
- # set the content type
117
- response["Content-Type"] = Config.default_image_content_type
118
- # set expiry
119
- response = set_caching_headers(response)
120
- # return the response
121
- response
122
- end
123
-
124
- def set_avatar_headers(response, avatar)
125
- # set the last modified header
126
- response["Last-Modified"] = avatar.last_modified
127
- # set the content type header
128
- response["Content-Type"] = avatar.content_type
129
- # return the response
130
- response
54
+ def not_found
55
+ Rack::Response.new("Not Found", 404)
131
56
  end
132
57
 
133
- def set_caching_headers(response)
134
- # if we want to expire in a set time, calculate the header
135
- if options[:expires]
136
- response["Expires"] = (Time.now + options[:expires]).httpdate
137
- end
138
- # if we want to set cache control settings
139
- if cc = options[:cache_control]
140
- directives = []
141
- directives << "no-cache" if cc[:no_cache]
142
- directives << "max-stale=#{cc[:max_stale]}" if cc[:max_stale]
143
- directives << "max-age=#{cc[:max_age]}" if cc[:max_age]
144
- directives << (cc[:public] ? "public" : "private")
145
- response["Cache-Control"] = directives.join(", ")
146
- end
147
- # return the response
148
- response
58
+ def timeout
59
+ Rack::Response.new("Gateway Timeout", 504)
149
60
  end
150
61
 
151
- def load_avatar(service, id)
152
- # titleize the service name
153
- service = service.gsub(/[\_\-]/, " ").gsub(/\b([a-z])/) do |match|
154
- match.upcase
155
- end
156
- # pass it onto the
157
- SocialAvatarProxy.const_get("#{service}Avatar").new(id)
62
+ def bad_gateway
63
+ Rack::Response.new("Bad Gateway: Too many redirects", 502)
158
64
  end
159
65
  end
160
66
  end
@@ -1,4 +1,5 @@
1
1
  require "social_avatar_proxy/remote_file_resolver"
2
+ require "social_avatar_proxy/avatar_file"
2
3
 
3
4
  module SocialAvatarProxy
4
5
  class Avatar
@@ -39,16 +40,26 @@ module SocialAvatarProxy
39
40
  end
40
41
  end
41
42
 
42
- # returns whether the avatar has changed
43
- def modified_since?(timestamp)
44
- last_modified > timestamp
45
- end
46
-
47
43
  # in the base Avatar class, we'll use the identifier as the URL
48
44
  def remote_url
49
45
  identifier
50
46
  end
51
47
 
48
+ # return a temporary file object
49
+ def file
50
+ AvatarFile.new(File.basename(remote_url)).tap do |file|
51
+ begin
52
+ file.write(body)
53
+ file.content_type(content_type)
54
+ file.mtime(last_modified)
55
+ file.rewind
56
+ rescue
57
+ file.close
58
+ raise
59
+ end
60
+ end if exist?
61
+ end
62
+
52
63
  private
53
64
  # request the remote file
54
65
  def response
@@ -0,0 +1,27 @@
1
+ require "tempfile"
2
+ require "fileutils"
3
+ require "delegate"
4
+
5
+ module SocialAvatarProxy
6
+ class AvatarFile < SimpleDelegator
7
+ attr_reader :file
8
+
9
+ def initialize(path)
10
+ if File.exist?(path)
11
+ @file = File.new(path)
12
+ mtime(file.mtime)
13
+ else
14
+ @file = Tempfile.new(path.gsub("/", "-"))
15
+ end
16
+ super(file)
17
+ end
18
+
19
+ def mtime(value = nil)
20
+ value ? @mtime = value.to_i : Time.at(@mtime)
21
+ end
22
+
23
+ def content_type(value = nil)
24
+ value ? @content_type = value : @content_type
25
+ end
26
+ end
27
+ end
@@ -1,30 +1,9 @@
1
+ require "social_avatar_proxy/configuration"
2
+
1
3
  module SocialAvatarProxy
2
- class Config
3
- class << self
4
- attr_accessor :default_image
5
-
6
- def default_image_file
7
- default_image && File.new(default_image)
8
- end
9
-
10
- def default_image_data
11
- default_image_file.read
12
- end
13
-
14
- def default_image_content_type
15
- case File.extname(default_image).downcase
16
- when /^\.jpe?g$/
17
- "image/jpeg"
18
- when /^\.png$/
19
- "image/png"
20
- when /^\.gif$/
21
- "image/gif"
22
- when /^\.svg$/
23
- "image/svg+xml"
24
- else
25
- "application/octet-stream"
26
- end
27
- end
28
- end
4
+ Config = Configuration.new
5
+
6
+ def self.configure(&block)
7
+ Config.configure(&block)
29
8
  end
30
9
  end
@@ -0,0 +1,57 @@
1
+ module SocialAvatarProxy
2
+ class Configuration
3
+ class Cache
4
+ def read(options = {})
5
+ false
6
+ end
7
+
8
+ def write(file, options = {})
9
+ file
10
+ end
11
+
12
+ def fetch(options = {}, &block)
13
+ # if the cache is enabled attempt read
14
+ file = attempt_read(options)
15
+ # if read fails yield the block
16
+ # if the cache is enabled attempt write
17
+ # return the block result
18
+ unless file
19
+ file = block.yield
20
+ attempt_write(file, options) if file
21
+ end
22
+ # return the file
23
+ file
24
+ end
25
+
26
+ def configure(&block)
27
+ enable
28
+ instance_eval(&block)
29
+ end
30
+
31
+ def disabled?
32
+ !enabled?
33
+ end
34
+
35
+ def enabled?
36
+ !!@enabled
37
+ end
38
+
39
+ private
40
+ def disable
41
+ @enabled = false
42
+ end
43
+
44
+ def enable
45
+ @enabled = true
46
+ end
47
+
48
+ def attempt_read(options = {})
49
+ enabled? && read(options)
50
+ end
51
+
52
+ def attempt_write(file, options = {})
53
+ enabled? && write(file, options)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,15 @@
1
+ module SocialAvatarProxy
2
+ class Configuration
3
+ class Caches < Array
4
+ def fetch(*args, &block)
5
+ if cache = delete_at(0)
6
+ cache.fetch(*args) do
7
+ fetch(*args, &block)
8
+ end
9
+ else
10
+ block.yield
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ require "social_avatar_proxy/avatar_file"
2
+ require "social_avatar_proxy/configuration/cache"
3
+ require "digest/md5"
4
+ require "fileutils"
5
+
6
+ module SocialAvatarProxy
7
+ class Configuration
8
+ class FileCache < Cache
9
+ def read(options = {})
10
+ path = generate_key(options)
11
+ # ensure the file exists and it's readable
12
+ unless File.exist?(path) && File.readable?(path)
13
+ return false
14
+ end
15
+ # return the file
16
+ AvatarFile.new(path).tap do |file|
17
+ file.mtime(File.mtime(path))
18
+ end
19
+ end
20
+
21
+ def write(file, options = {})
22
+ # generate the file location
23
+ path = generate_key(options)
24
+ # create the folder
25
+ FileUtils.mkdir_p File.dirname(path)
26
+ # write the file
27
+ File.open(path, "wb") do |f|
28
+ f.write(file.read)
29
+ end
30
+ end
31
+
32
+ private
33
+ def generate_key(options = {})
34
+ # generate a hash for the filename
35
+ hash = "#{options[:service]}-#{options[:identifier]}"
36
+ name = "sap-#{Digest::MD5.hexdigest(hash)}"
37
+ File.join(@directory, name)
38
+ end
39
+
40
+ # set the directory
41
+ def directory(value)
42
+ @directory = value
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,55 @@
1
+ module SocialAvatarProxy
2
+ class Configuration
3
+ class HttpCache
4
+ def configure(&block)
5
+ @enabled = true
6
+ instance_eval(&block)
7
+ end
8
+
9
+ def apply_caching_headers(response)
10
+ # if we want to expire in a set time, calculate the header
11
+ if expires
12
+ response["Expires"] = (Time.now + expires.to_i).httpdate
13
+ end
14
+ # if we want to set cache control settings
15
+ if cc = cache_control
16
+ directives = []
17
+ directives << "no-cache" if cc[:no_cache]
18
+ directives << "max-stale=#{cc[:max_stale]}" if cc[:max_stale]
19
+ directives << "max-age=#{cc[:max_age]}" if cc[:max_age]
20
+ directives << (cc[:public] ? "public" : "private")
21
+ response["Cache-Control"] = directives.join(", ")
22
+ end
23
+ # return the response
24
+ response
25
+ end
26
+
27
+ def disabled?
28
+ !enabled?
29
+ end
30
+
31
+ def enabled?
32
+ !!@enabled
33
+ end
34
+
35
+ private
36
+ def cache_control(value = nil)
37
+ if value
38
+ @cache_control = value
39
+ end
40
+ @cache_control
41
+ end
42
+
43
+ def expires(value = nil)
44
+ if value
45
+ @expires = value
46
+ end
47
+ @expires
48
+ end
49
+
50
+ def disable
51
+ @enabled = false
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,97 @@
1
+ require "social_avatar_proxy/avatar_file"
2
+ require "social_avatar_proxy/configuration/cache"
3
+ require "dalli"
4
+ require "digest/md5"
5
+
6
+ module SocialAvatarProxy
7
+ class Configuration
8
+ class Memcache < Cache
9
+ def read(options = {})
10
+ # generate the key
11
+ file_key = generate_key(options)
12
+ # if the key exists, serve the data
13
+ if data = connection.get(file_key)
14
+ content_type = connection.get("#{file_key}-content-type") || "image/jpeg"
15
+ mtime = connection.get("#{file_key}-mtime") || Time.now
16
+ # serve the avatar file
17
+ AvatarFile.new(file_key).tap do |file|
18
+ begin
19
+ file.write(data)
20
+ file.content_type(content_type)
21
+ file.mtime(mtime)
22
+ file.rewind
23
+ rescue
24
+ file.close
25
+ file.unlink
26
+ raise
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def write(file, options = {})
33
+ # generate the file location
34
+ file_key = generate_key(options)
35
+ # write the file
36
+ connection.set(file_key, file.read)
37
+ # if the file has a content type
38
+ if file.respond_to?(:content_type) && file.content_type
39
+ # write the content type
40
+ connection.set("#{file_key}-content-type", file.content_type)
41
+ end
42
+ # if the file has a modified time
43
+ if file.respond_to?(:mtime) && file.mtime
44
+ # write the content type
45
+ connection.set("#{file_key}-mtime", file.mtime.to_i)
46
+ end
47
+ end
48
+
49
+ def initialize
50
+ key do |options|
51
+ pre_hash = "#{options[:service]}-#{options[:identifier]}"
52
+ "#{@namespace || "sap/"}#{Digest::MD5.hexdigest(pre_hash)}"
53
+ end
54
+ end
55
+
56
+ private
57
+ # set which Memcached server to use
58
+ # default: "localhost:11211"
59
+ def host(value)
60
+ @host = value
61
+ end
62
+
63
+ # set the key namespace
64
+ # default: "sap/"
65
+ def namespace(value)
66
+ @namespace = value
67
+ end
68
+
69
+ # set the key generation block
70
+ # should accept a hash of options
71
+ def key(&block)
72
+ @key = block
73
+ end
74
+
75
+ # generate a key given the arguments
76
+ def generate_key(*args)
77
+ @key.yield(*args)
78
+ end
79
+
80
+ # return a connection to Memcache
81
+ # defaults to a Dalli::Client object
82
+ def connection(value = nil)
83
+ if value
84
+ unless value.respond_to?(:get) && value.respond_to?(:set)
85
+ raise ArgumentError, %Q(
86
+ #{value.inspect}
87
+ must respond to #set and #get
88
+ when using as a Memcache connection
89
+ )
90
+ end
91
+ @connection = value
92
+ end
93
+ @connection ||= Dalli::Client.new(@host || "localhost:11211")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,112 @@
1
+ require "social_avatar_proxy/configuration/file_cache"
2
+ require "social_avatar_proxy/configuration/http_cache"
3
+ require "social_avatar_proxy/configuration/memcache"
4
+ require "social_avatar_proxy/configuration/caches"
5
+ require "social_avatar_proxy/avatar_file"
6
+
7
+ module SocialAvatarProxy
8
+ class Configuration
9
+ def initialize
10
+ @memcache = Memcache.new
11
+ @file_cache = FileCache.new
12
+ @http_cache = HttpCache.new
13
+ end
14
+
15
+ # updates the configuration
16
+ def configure(&block)
17
+ instance_eval(&block)
18
+ end
19
+
20
+ # configures or retrieves the file cache instance
21
+ def file_cache(&block)
22
+ if block_given?
23
+ @file_cache.configure(&block)
24
+ else
25
+ @file_cache
26
+ end
27
+ end
28
+
29
+ # configures or retrieves the http cache instance
30
+ def http_cache(&block)
31
+ if block_given?
32
+ @http_cache.configure(&block)
33
+ else
34
+ @http_cache
35
+ end
36
+ end
37
+
38
+ # configures or retrieves the memcache cache instance
39
+ def memcache(&block)
40
+ if block_given?
41
+ @memcache.configure(&block)
42
+ else
43
+ @memcache
44
+ end
45
+ end
46
+
47
+ # serves the file via a header
48
+ def x_send_file
49
+ serve_via(:x_send_file)
50
+ end
51
+
52
+ # serves the file via an x_accel_redirect
53
+ def x_accel_redirect
54
+ serve_via(:x_accel_redirect)
55
+ end
56
+
57
+ # returns how the app should serve the file
58
+ # defaults to streaming the file data
59
+ def delivery_method
60
+ @serve_via ||= :stream
61
+ end
62
+
63
+ # returns a Caches object containing the configured caches
64
+ def caches
65
+ Caches.new.tap do |set|
66
+ set.push(@memcache) if @memcache.enabled?
67
+ set.push(@file_cache) if @file_cache.enabled?
68
+ end
69
+ end
70
+
71
+ def default_image(value = nil)
72
+ if value
73
+ unless File.exist?(value) && File.readable?(value)
74
+ raise ArgumentError, "#{value} does not exist, or is unreadable"
75
+ end
76
+ @default_image = value
77
+ end
78
+ @default_image && AvatarFile.new(@default_image).tap do |file|
79
+ file.content_type(default_image_content_type)
80
+ end
81
+ end
82
+
83
+ def default_image_content_type(value = nil)
84
+ @default_image_content_type = value if value
85
+ [
86
+ @default_image_content_type,
87
+ auto_detect_default_image_content_type,
88
+ "application/octet-stream"
89
+ ].compact.first
90
+ end
91
+
92
+ private
93
+ def serve_via(value)
94
+ @serve_via = value
95
+ end
96
+
97
+ def auto_detect_default_image_content_type
98
+ case File.extname(@default_image)
99
+ when ".png"
100
+ "image/png"
101
+ when ".gif"
102
+ "image/gif"
103
+ when ".svg"
104
+ "application/svg+xml"
105
+ when /\.jpe?g/
106
+ "image/jpeg"
107
+ else
108
+ "application/octet-stream"
109
+ end if @default_image
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,90 @@
1
+ require "social_avatar_proxy/config"
2
+ require "rack"
3
+
4
+ module SocialAvatarProxy
5
+ class Response < Rack::Response
6
+ def self.build(options = {})
7
+ # build the response
8
+ response = ResponseBuilder.new(options).response
9
+ # apply the HTTP caching to the response
10
+ SocialAvatarProxy::Config.http_cache.apply_caching_headers(response)
11
+ end
12
+ end
13
+
14
+ class ResponseBuilder
15
+ attr_reader :service, :identifier
16
+
17
+ def initialize(options)
18
+ @service = options.fetch(:service)
19
+ @identifier = options.fetch(:identifier)
20
+ end
21
+
22
+ def response
23
+ return @response if defined?(@response)
24
+ if file
25
+ @response = Response.new do |response|
26
+ write_file(response)
27
+ set_file_headers(response)
28
+ end
29
+ else
30
+ @response = Rack::Response.new("Not Found", 404)
31
+ end
32
+ end
33
+
34
+ def set_file_headers(response)
35
+ # set the modified time
36
+ if file.respond_to?(:mtime) && file.mtime
37
+ response["Last-Modified"] = file.mtime.httpdate
38
+ end
39
+ # set the content type
40
+ if file.respond_to?(:content_type) && file.content_type
41
+ response["Content-Type"] = file.content_type
42
+ end
43
+ end
44
+
45
+ def write_file(response)
46
+ # configure the file delivery
47
+ case Config.delivery_method
48
+ when :x_accel_redirect
49
+ # set the NGINX header
50
+ response["X-Accel-Redirect"] = file.path
51
+ when :x_send_file
52
+ # set the Apache header
53
+ response["X-Sendfile"] = file.path
54
+ else
55
+ begin
56
+ # read the file contents
57
+ data = file.read
58
+ # write it to the response
59
+ response.write(data)
60
+ ensure
61
+ # close the file
62
+ file.close
63
+ end
64
+ end
65
+ end
66
+
67
+ def file
68
+ @file ||= SocialAvatarProxy::Config.caches.fetch({
69
+ service: service,
70
+ identifier: identifier
71
+ }) do
72
+ remote_avatar_file || default_image
73
+ end
74
+ end
75
+
76
+ private
77
+ def default_image
78
+ SocialAvatarProxy::Config.default_image
79
+ end
80
+
81
+ def remote_avatar_file
82
+ # titleize the service name
83
+ service_name = service.gsub(/[\_\-]/, " ").gsub(/\b([a-z])/) do |match|
84
+ match.upcase
85
+ end
86
+ # pass it onto the service, and retrieve the file
87
+ SocialAvatarProxy.const_get("#{service_name}Avatar").new(identifier).file
88
+ end
89
+ end
90
+ end
@@ -1,3 +1,3 @@
1
1
  module SocialAvatarProxy
2
- VERSION = "1.2.0"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -4,6 +4,7 @@ require "social_avatar_proxy/app"
4
4
  require "social_avatar_proxy/avatar"
5
5
  require "social_avatar_proxy/facebook_avatar"
6
6
  require "social_avatar_proxy/twitter_avatar"
7
+ require "social_avatar_proxy/config"
7
8
 
8
9
  if defined?(Rails) && defined?(Rails::Engine)
9
10
  require "social_avatar_proxy/engine"
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  s.add_dependency "rack"
23
+ s.add_dependency "dalli"
23
24
 
24
25
  s.add_development_dependency "combustion"
25
26
  s.add_development_dependency "rake"
@@ -6,9 +6,7 @@ describe SocialAvatarProxy::App do
6
6
  Rack::MockRequest.new(app)
7
7
  end
8
8
 
9
- let(:app) { SocialAvatarProxy::App.new(options) }
10
-
11
- let(:options) { Hash.new }
9
+ let(:app) { SocialAvatarProxy::App.new }
12
10
 
13
11
  let(:successful_response) do
14
12
  response = Net::HTTPSuccess.new("1.1", 200, "Success")
@@ -23,7 +21,13 @@ describe SocialAvatarProxy::App do
23
21
 
24
22
  let(:response) { nil }
25
23
 
24
+ let(:image_path) do
25
+ File.join(File.dirname(__FILE__), "../fixtures/image.jpg")
26
+ end
27
+
26
28
  before(:each) do
29
+ SocialAvatarProxy.send(:remove_const, :Config)
30
+ SocialAvatarProxy.const_set(:Config, SocialAvatarProxy::Configuration.new)
27
31
  klass = SocialAvatarProxy::Avatar
28
32
  klass.any_instance.stub(:response).and_return(response)
29
33
  end
@@ -41,9 +45,8 @@ describe SocialAvatarProxy::App do
41
45
  subject { SocialAvatarProxy::App }
42
46
 
43
47
  it "should return a object providing the path helpers" do
44
- options = { path_prefix: "/test" }
45
- expect(subject.routes(options).facebook_avatar_path("ryandtownsend")).to match(/^\/.*$/)
46
- expect(subject.routes(options).twitter_avatar_path(2202971)).to match(/^\/.*$/)
48
+ expect(subject.routes.facebook_avatar_path("ryandtownsend")).to match(/^\/.*$/)
49
+ expect(subject.routes.twitter_avatar_path(2202971)).to match(/^\/.*$/)
47
50
  end
48
51
  end
49
52
 
@@ -62,17 +65,6 @@ describe SocialAvatarProxy::App do
62
65
  it "should return a 404 response" do
63
66
  expect(subject.get(path).status).to eq(404)
64
67
  end
65
-
66
- context "with a custom image" do
67
- before(:each) do
68
- image_path = File.join(File.dirname(__FILE__), "../fixtures/image.jpg")
69
- SocialAvatarProxy::Config.default_image = image_path
70
- end
71
-
72
- it "should render the image" do
73
- expect(subject.get(path)["Content-Type"]).to eq("image/jpeg")
74
- end
75
- end
76
68
  end
77
69
 
78
70
  context "given a valid path" do
@@ -104,17 +96,76 @@ describe SocialAvatarProxy::App do
104
96
  expect(subject.get(path).status).to eq(200)
105
97
  end
106
98
 
107
- it "should return a Cache-Control header by default" do
108
- expect(subject.get(path).headers["Cache-Control"]).to eq("max-stale=86400, max-age=86400, public")
99
+ context "with memcache in place" do
100
+ before(:each) do
101
+ SocialAvatarProxy.configure do
102
+ memcache do
103
+ enable
104
+ end
105
+ end
106
+ end
107
+
108
+ it "should attempt to get the image from the cache" do
109
+ expect(subject.get(path).status).to eq(200)
110
+ end
111
+ end
112
+
113
+ context "with file cache in place" do
114
+ before(:each) do
115
+ SocialAvatarProxy.configure do
116
+ file_cache do
117
+ directory File.join(File.dirname(__FILE__), "../../.tmp")
118
+ end
119
+ end
120
+ end
121
+
122
+ it "should attempt to get the image from the cache" do
123
+ expect(subject.get(path).status).to eq(200)
124
+ end
125
+ end
126
+
127
+ context "with memcache and the file cache in place" do
128
+ before(:each) do
129
+ SocialAvatarProxy.configure do
130
+ memcache do
131
+ enable
132
+ end
133
+ file_cache do
134
+ directory File.join(File.dirname(__FILE__), "../../.tmp")
135
+ end
136
+ end
137
+ end
138
+
139
+ it "should attempt to get the image from the cache" do
140
+ expect(subject.get(path).status).to eq(200)
141
+ end
109
142
  end
110
143
 
111
- it "should return a Expires header by default" do
112
- expect(subject.get(path).headers["Expires"]).to eq((Time.now + 86400).httpdate)
144
+ context "with Cache-Control set with Max-Age and Max-Stale as 1 day" do
145
+ before(:each) do
146
+ SocialAvatarProxy.configure do
147
+ http_cache do
148
+ cache_control({
149
+ max_stale: 86400,
150
+ max_age: 86400,
151
+ public: true
152
+ })
153
+ end
154
+ end
155
+ end
156
+
157
+ it "should return a Cache-Control header" do
158
+ expect(subject.get(path).headers["Cache-Control"]).to eq("max-stale=86400, max-age=86400, public")
159
+ end
113
160
  end
114
161
 
115
162
  context "given Cache-Control is turned off" do
116
- let(:options) do
117
- { cache_control: false }
163
+ before(:each) do
164
+ SocialAvatarProxy.configure do
165
+ http_cache do
166
+ disable
167
+ end
168
+ end
118
169
  end
119
170
 
120
171
  it "should not return a Cache-Control header" do
@@ -123,8 +174,12 @@ describe SocialAvatarProxy::App do
123
174
  end
124
175
 
125
176
  context "given Cache-Control is set to private" do
126
- let(:options) do
127
- { cache_control: { public: false } }
177
+ before(:each) do
178
+ SocialAvatarProxy.configure do
179
+ http_cache do
180
+ cache_control({ public: false })
181
+ end
182
+ end
128
183
  end
129
184
 
130
185
  it "should return a Cache-Control header marked for private use" do
@@ -133,8 +188,12 @@ describe SocialAvatarProxy::App do
133
188
  end
134
189
 
135
190
  context "given Expires is turned off" do
136
- let(:options) do
137
- { expires: false }
191
+ before(:each) do
192
+ SocialAvatarProxy.configure do
193
+ http_cache do
194
+ cache_control({ expires: false })
195
+ end
196
+ end
138
197
  end
139
198
 
140
199
  it "should not return a Cache-Control header" do
@@ -143,8 +202,12 @@ describe SocialAvatarProxy::App do
143
202
  end
144
203
 
145
204
  context "given Expires is set to two days" do
146
- let(:options) do
147
- { expires: (86400 * 2) }
205
+ before(:each) do
206
+ SocialAvatarProxy.configure do
207
+ http_cache do
208
+ expires 86400 * 2
209
+ end
210
+ end
148
211
  end
149
212
 
150
213
  it "should not return a Cache-Control header" do
@@ -155,11 +218,24 @@ describe SocialAvatarProxy::App do
155
218
 
156
219
  context "that fails to find an avatar" do
157
220
  let(:response) { not_found_response }
221
+ let(:path) { "/facebook/someRandomUserThatDoesntExist" }
158
222
 
159
223
  it "should return a 404 response" do
160
- path = "/facebook/someRandomUserThatDoesntExist"
161
224
  expect(subject.get(path).status).to eq(404)
162
225
  end
226
+
227
+ context "with a custom image" do
228
+ before(:each) do
229
+ ip = image_path
230
+ SocialAvatarProxy.configure do
231
+ default_image(ip)
232
+ end
233
+ end
234
+
235
+ it "should render the image" do
236
+ expect(subject.get(path)["Content-Type"]).to eq("image/jpeg")
237
+ end
238
+ end
163
239
  end
164
240
  end
165
241
  end
@@ -72,20 +72,6 @@ describe SocialAvatarProxy::Avatar do
72
72
  expect(subject.last_modified.to_i).to eq(timestamp.to_i)
73
73
  end
74
74
  end
75
-
76
- describe "#modified_since?" do
77
- context "given a date older than the timestamp" do
78
- it "should return true" do
79
- expect(subject.modified_since?(timestamp - 86400)).to be_true
80
- end
81
- end
82
-
83
- context "given a date newer than the timestamp" do
84
- it "should return true" do
85
- expect(subject.modified_since?(Time.now)).to be_false
86
- end
87
- end
88
- end
89
75
  end
90
76
 
91
77
  context "with no last-modified header" do
@@ -3,8 +3,13 @@ require "spec_helper"
3
3
  describe SocialAvatarProxy::Config do
4
4
  subject { SocialAvatarProxy::Config }
5
5
 
6
- after(:each) do
7
- subject.default_image = nil
6
+ around(:each) do
7
+ SocialAvatarProxy.send(:remove_const, :Config)
8
+ SocialAvatarProxy.const_set(:Config, SocialAvatarProxy::Configuration.new)
9
+ end
10
+
11
+ let(:image_path) do
12
+ File.join(File.dirname(__FILE__), "../fixtures/image.jpg")
8
13
  end
9
14
 
10
15
  describe "::default_image" do
@@ -13,45 +18,43 @@ describe SocialAvatarProxy::Config do
13
18
  end
14
19
 
15
20
  it "should store a path" do
16
- subject.default_image = "/test.jpg"
17
- expect(subject.default_image).to eq "/test.jpg"
21
+ subject.configure do
22
+ default_image(image_path)
23
+ end
24
+ expect(subject.default_image.path).to eq(image_path)
18
25
  end
19
26
  end
20
27
 
21
28
  describe "::default_image_content_type" do
22
- context "with a JPEG" do
23
- it "should return image/jpeg" do
24
- subject.default_image = "/test.JPG"
25
- expect(subject.default_image_content_type).to eq "image/jpeg"
26
- end
29
+ it "should default to nil" do
30
+ expect(subject.default_image_content_type).to be_nil
27
31
  end
28
32
 
29
- context "with a PNG" do
30
- it "should return image/png" do
31
- subject.default_image = "/test.png"
32
- expect(subject.default_image_content_type).to eq "image/png"
33
+ context "with an overridden content type" do
34
+ it "should return the value set" do
35
+ subject.configure do
36
+ default_image(image_path)
37
+ default_image_content_type("something/banana")
38
+ end
39
+ expect(subject.default_image_content_type).to eq("something/banana")
33
40
  end
34
41
  end
35
-
36
- context "with a GIF" do
37
- it "should return image/gif" do
38
- subject.default_image = "/test.gif"
39
- expect(subject.default_image_content_type).to eq "image/gif"
42
+
43
+ context "with a default_image set" do
44
+ before(:each) do
45
+ subject.configure do
46
+ default_image(image_path)
47
+ end
40
48
  end
41
- end
42
49
 
43
- context "with a SVG" do
44
- it "should return image/svg+xml" do
45
- subject.default_image = "/test.svg"
46
- expect(subject.default_image_content_type).to eq "image/svg+xml"
50
+ it "should return the calculated content type" do
51
+ expect(subject.default_image_content_type).to eq("image/jpeg")
47
52
  end
48
- end
49
53
 
50
- context "with an unknown type" do
51
- it "should return application/octet-stream" do
52
- subject.default_image = "/test"
53
- expect(subject.default_image_content_type).to eq "application/octet-stream"
54
+ it "should set the content type on the image" do
55
+ expect(subject.default_image.content_type).to eq("image/jpeg")
54
56
  end
55
57
  end
56
58
  end
59
+
57
60
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: social-avatar-proxy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
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: 2012-10-09 00:00:00.000000000 Z
12
+ date: 2012-11-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -27,6 +27,22 @@ dependencies:
27
27
  - - ! '>='
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: dalli
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: combustion
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -126,11 +142,19 @@ files:
126
142
  - lib/social_avatar_proxy.rb
127
143
  - lib/social_avatar_proxy/app.rb
128
144
  - lib/social_avatar_proxy/avatar.rb
145
+ - lib/social_avatar_proxy/avatar_file.rb
129
146
  - lib/social_avatar_proxy/config.rb
147
+ - lib/social_avatar_proxy/configuration.rb
148
+ - lib/social_avatar_proxy/configuration/cache.rb
149
+ - lib/social_avatar_proxy/configuration/caches.rb
150
+ - lib/social_avatar_proxy/configuration/file_cache.rb
151
+ - lib/social_avatar_proxy/configuration/http_cache.rb
152
+ - lib/social_avatar_proxy/configuration/memcache.rb
130
153
  - lib/social_avatar_proxy/engine.rb
131
154
  - lib/social_avatar_proxy/facebook_avatar.rb
132
155
  - lib/social_avatar_proxy/path_helpers.rb
133
156
  - lib/social_avatar_proxy/remote_file_resolver.rb
157
+ - lib/social_avatar_proxy/response.rb
134
158
  - lib/social_avatar_proxy/routes.rb
135
159
  - lib/social_avatar_proxy/timeout_error.rb
136
160
  - lib/social_avatar_proxy/too_many_redirects_error.rb
@@ -163,15 +187,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
187
  - - ! '>='
164
188
  - !ruby/object:Gem::Version
165
189
  version: '0'
190
+ segments:
191
+ - 0
192
+ hash: -284777560749383164
166
193
  required_rubygems_version: !ruby/object:Gem::Requirement
167
194
  none: false
168
195
  requirements:
169
196
  - - ! '>='
170
197
  - !ruby/object:Gem::Version
171
198
  version: '0'
199
+ segments:
200
+ - 0
201
+ hash: -284777560749383164
172
202
  requirements: []
173
203
  rubyforge_project:
174
- rubygems_version: 1.8.23
204
+ rubygems_version: 1.8.24
175
205
  signing_key:
176
206
  specification_version: 3
177
207
  summary: This gem acts as a proxy for avatars on Twitter & Facebook.