social-avatar-proxy 1.2.0 → 2.0.0

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/.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.