imagery 0.0.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- module Imagery
1
+ class Imagery
2
2
  module Faking
3
3
  def self.included(base)
4
4
  base.extend ClassMethods
@@ -73,7 +73,7 @@ module Imagery
73
73
 
74
74
  # Implement the stubbed version of save and skips actual operation
75
75
  # if Imagery::Model.mode == :fake
76
- def save(io)
76
+ def save(io, key = nil)
77
77
  return true if self.class.mode == :fake
78
78
 
79
79
  super
@@ -1,165 +1,106 @@
1
- require 'aws/s3'
1
+ require "aws/s3"
2
2
 
3
- module Imagery
3
+ class Imagery
4
4
  module S3
5
- def self.included(base)
6
- base.extend Configs
7
- class << base
8
- attr_writer :s3_bucket, :s3_distribution_domain, :s3_host
9
- end
5
+ def self.included(imagery)
6
+ imagery.extend Config
7
+
8
+ # Set the default host for amazon S3. You can also set this
9
+ # to https://s3.amazon.com if you want to force secure connections
10
+ # on a global scale.
11
+ imagery.s3_host "http://s3.amazonaws.com"
10
12
  end
11
13
 
12
- module Configs
14
+ module Config
13
15
  def s3_bucket(bucket = nil)
14
- @s3_bucket = bucket if bucket
15
- @s3_bucket || raise(UndefinedBucket, BUCKET_ERROR_MSG)
16
+ @s3_bucket = bucket if bucket
17
+ @s3_bucket
16
18
  end
17
19
 
18
- BUCKET_ERROR_MSG = (<<-MSG).gsub(/^ {6}/, '')
19
-
20
- You need to define a bucket name. Example:
21
-
22
- class Imagery::Model
23
- include Imagery::S3
24
-
25
- s3_bucket 'my-bucket-name'
26
- end
27
- MSG
28
-
29
20
  def s3_distribution_domain(domain = nil)
30
- @s3_distribution_domain = domain if domain
21
+ @s3_distribution_domain = domain if domain
31
22
  @s3_distribution_domain
32
23
  end
33
-
34
- # Allows you to customize the S3 host. Usually happens when you use
35
- # amazon S3 EU.
36
- #
37
- # @param [String] host the custom host you want to use instead.
38
- # @return [String] the s3 host currently set.
24
+
39
25
  def s3_host(host = nil)
40
- @s3_host = host if host
41
- @s3_host || S3_HOST
26
+ @s3_host = host if host
27
+ @s3_host
42
28
  end
43
29
  end
44
-
45
- UndefinedBucket = Class.new(StandardError)
46
30
 
47
- S3_HOST = "http://s3.amazonaws.com"
31
+ # Convenience attribute which returns all size keys including `:original`.
32
+ attr :keys
48
33
 
49
- # Returns a url publicly accessible. If a distribution domain is set,
50
- # then the url will be based on that.
51
- #
52
- # @example
53
- #
54
- # class Imagery::Model
55
- # include Imagery::S3
56
- #
57
- # s3_bucket 'bucket-name'
58
- # end
59
- #
60
- # Photo = Class.new(Struct.new(:id))
61
- # i = Imagery.new(Photo.new(1001))
62
- #
63
- # i.url == 'http://s3.amazonaws.com/bucket-name/photo/1001/original.png'
64
- # # => true
65
- #
66
- # Imagery::Model.s3_distribution_domain = 'assets.site.com'
67
- # i.url == 'http://assets.site.com/photo/1001/original.png'
68
- # # => true
69
- #
70
- # # You may also subclass this of course since it's just a ruby object
71
- # # and configure them differently as needed.
72
- #
73
- # class CloudFront < Imagery::Model
74
- # include Imagery::S3
75
- # s3_bucket 'cloudfront'
76
- # s3_distribution_domain 'assets.site.com'
77
- # end
78
- #
79
- # class RegularS3 < Imagery::Model
80
- # include Imagery::S3
81
- # s3_bucket 'cloudfront'
82
- # end
34
+ def initialize(*args)
35
+ super
36
+
37
+ @keys = [@original] + sizes.keys
38
+ end
39
+
40
+ # If you specify a distribution domain (i.e. a cloudfront domain,
41
+ # or even an S3 domain with a prefix), that distribution domain is
42
+ # used.
83
43
  #
84
- # @param [Symbol] size the preferred size you want for the url.
85
- # @return [String] the size specific url.
86
- def url(size = self.default_size)
87
- if domain = self.class.s3_distribution_domain
88
- [domain, namespace, key, filename(size)].join('/')
44
+ # Otherwise the default canonical S3 url is used.
45
+ def url(file = @original)
46
+ if self.class.s3_distribution_domain
47
+ "#{self.class.s3_distribution_domain}#{super}"
89
48
  else
90
- [self.class.s3_host, self.class.s3_bucket, namespace, key, filename(size)].join('/')
49
+ "#{self.class.s3_host}/#{self.class.s3_bucket}#{super}"
91
50
  end
92
51
  end
93
-
94
- # In addition to saving the files and resizing them locally, uploads all
95
- # the different sizes to amazon S3.
96
- def save(io)
97
- if super
98
- s3_object_keys.each do |key, size|
99
- Gateway.store(key,
100
- File.open(file(size)),
101
- self.class.s3_bucket,
102
- :access => :public_read,
103
- :content_type => "image/png"
104
- )
105
- end
106
- end
52
+
53
+ # Returns the complete S3 key used for this object. The S3 key
54
+ # is simply composed of the prefix and filename, e.g.
55
+ #
56
+ # - photos/1001/original.jpg
57
+ # - photos/1001/small.jpg
58
+ # - photos/1001/tiny.jpg
59
+ #
60
+ def s3_key(file)
61
+ "#{prefix}/#{key}/#{ext(file)}"
107
62
  end
108
-
109
- # Deletes all the files related to this Imagery instance and also
110
- # all the S3 keys.
63
+
64
+ # Deletes all keys defined for this object, which includes `:original`
65
+ # and all keys in `sizes`.
111
66
  def delete
112
67
  super
113
- s3_object_keys.each do |key, size|
114
- Gateway.delete key, self.class.s3_bucket
68
+
69
+ keys.each do |file|
70
+ Gateway.delete(s3_key(file), self.class.s3_bucket)
115
71
  end
116
72
  end
73
+
74
+ # Save the object as we normall would, but also upload all resulting
75
+ # files to S3. We set the proper content type and Cache-Control setting
76
+ # optimized for a cloudfront setup.
77
+ def save(io, key = nil)
78
+ super
117
79
 
118
- protected
119
- def s3_object_keys
120
- sizes.keys.map do |size|
121
- [[namespace, key, filename(size)].join('/'), size]
80
+ keys.each do |file|
81
+ Gateway.store(s3_key(file),
82
+ File.open(root(ext(file))),
83
+ self.class.s3_bucket,
84
+ :access => :public_read,
85
+ :content_type => "image/jpeg",
86
+ "Cache-Control" => "max-age=315360000"
87
+ )
122
88
  end
123
89
  end
124
-
90
+
91
+ # Provides a convenience wrapper around AWS::S3::S3Object and
92
+ # serves as an auto-connect module.
125
93
  module Gateway
126
- # A wrapper for AWS::S3::S3Object.store. Basically auto-connects
127
- # using the environment vars.
128
- #
129
- # @example
130
- #
131
- # AWS::S3::Base.connected?
132
- # # => false
133
- #
134
- # Imagery::S3::Gateway.store(
135
- # 'avatar', File.open('avatar.jpg'), 'bucket'
136
- # )
137
- # AWS::S3::Base.connected?
138
- # # => true
139
- #
140
- def store(*args)
94
+ def self.store(*args)
141
95
  execute(:store, *args)
142
96
  end
143
- module_function :store
144
97
 
145
- # A wrapper for AWS::S3::S3Object.delete. Basically auto-connects
146
- # using the environment vars.
147
- #
148
- # @example
149
- #
150
- # AWS::S3::Base.connected?
151
- # # => false
152
- #
153
- # Imagery::S3::Gateway.delete('avatar', 'bucket')
154
- # AWS::S3::Base.connected?
155
- # # => true
156
- def delete(*args)
98
+ def self.delete(*args)
157
99
  execute(:delete, *args)
158
100
  end
159
- module_function :delete
160
101
 
161
102
  private
162
- def execute(command, *args)
103
+ def self.execute(command, *args)
163
104
  begin
164
105
  AWS::S3::S3Object.__send__(command, *args)
165
106
  rescue AWS::S3::NoConnectionEstablished
@@ -170,7 +111,6 @@ module Imagery
170
111
  retry
171
112
  end
172
113
  end
173
- module_function :execute
174
114
  end
175
115
  end
176
116
  end
@@ -1,16 +1,16 @@
1
- module Imagery
1
+ class Imagery
2
2
  module Test
3
3
  def self.included(base)
4
- Imagery::Model.send :include, Imagery::Faking
5
- Imagery::Model.mode = :fake
4
+ Imagery.send :include, Imagery::Faking
5
+ Imagery.mode = :fake
6
6
  end
7
7
 
8
8
  protected
9
9
  def imagery
10
10
  if ENV["REAL_IMAGERY"]
11
- Imagery::Model.real { yield true }
11
+ Imagery.real { yield true }
12
12
  else
13
- Imagery::Model.faked { yield false }
13
+ Imagery.faked { yield false }
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,71 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+ require "stringio"
3
+
4
+ # Imagery::Test hooks
5
+ scope do
6
+ setup do
7
+ Object.send :include, Imagery::Test
8
+ end
9
+
10
+ test "auto-includes Imagery::Faking upon inclusion" do
11
+ assert Imagery.ancestors.include?(Imagery::Faking)
12
+ end
13
+
14
+ test "auto-sets mode to :fake" do
15
+ assert_equal :fake, Imagery.mode
16
+ end
17
+ end
18
+
19
+
20
+ class Imagery
21
+ include Faking
22
+ end
23
+
24
+ test "skips saving when faked" do
25
+ Imagery.mode = :fake
26
+
27
+ i = Imagery.new(:avatar, "1001")
28
+ i.save(StringIO.new)
29
+
30
+ assert ! File.exist?(Imagery.root("avatar", "1001"))
31
+ end
32
+
33
+ test "skips deleting when faked" do
34
+ Imagery.mode = :fake
35
+
36
+ FileUtils.mkdir_p(Imagery.root("avatar", "1001"))
37
+
38
+ i = Imagery.new(:avatar, "1001")
39
+ i.delete
40
+
41
+ assert File.exist?(Imagery.root("avatar", "1001"))
42
+ end
43
+
44
+ # Imagery::Test
45
+ scope do
46
+ extend Imagery::Test
47
+
48
+ test "yields true when REAL_IMAGERY is set" do
49
+ ENV["REAL_IMAGERY"] = "true"
50
+
51
+ enabled = nil
52
+
53
+ imagery do |e|
54
+ enabled = e
55
+ end
56
+
57
+ assert enabled
58
+ end
59
+
60
+ test "yields false when REAL_IMAGERY is not set" do
61
+ ENV["REAL_IMAGERY"] = nil
62
+
63
+ enabled = nil
64
+
65
+ imagery do |e|
66
+ enabled = e
67
+ end
68
+
69
+ assert_equal false, enabled
70
+ end
71
+ end
@@ -1,12 +1,31 @@
1
- require 'rubygems'
2
- require 'test/unit'
3
- require 'contest'
4
- require 'mocha'
1
+ $:.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
5
2
 
6
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
- $LOAD_PATH.unshift(File.dirname(__FILE__))
8
- require 'imagery'
3
+ require "cutest"
4
+ require "imagery"
9
5
 
10
- class Test::Unit::TestCase
11
- FIXTURES = File.dirname(__FILE__) + '/fixtures'
6
+ def fixture(filename)
7
+ File.expand_path("fixtures/#{filename}", File.dirname(__FILE__))
8
+ end
9
+
10
+ def resolution(file)
11
+ `gm identify #{file}`[/(\d+x\d+)/, 1]
12
+ end
13
+
14
+ prepare do
15
+ FileUtils.rm_rf(File.expand_path("../public", File.dirname(__FILE__)))
16
+ Imagery::S3::Gateway.commands.clear
17
+ end
18
+
19
+ class Imagery
20
+ module S3
21
+ module Gateway
22
+ def self.execute(command, *args)
23
+ commands << [command, *args.select { |a| a.respond_to?(:to_str) }]
24
+ end
25
+
26
+ def self.commands
27
+ @commands ||= []
28
+ end
29
+ end
30
+ end
12
31
  end
@@ -0,0 +1,172 @@
1
+ require File.expand_path("helper", File.dirname(__FILE__))
2
+
3
+ test "defining with a prefix" do
4
+ i = Imagery.new(:avatar)
5
+
6
+ assert_equal "avatar", i.prefix
7
+ end
8
+
9
+ test "defining with a key" do
10
+ i = Imagery.new(:avatar, "1001")
11
+
12
+ assert_equal "avatar", i.prefix
13
+ assert_equal "1001", i.key
14
+ end
15
+
16
+ test "defining with sizes" do
17
+ i = Imagery.new(:avatar, "1001", small: ["100x100"])
18
+
19
+ assert_equal "avatar", i.prefix
20
+ assert_equal "1001", i.key
21
+ assert_equal({ small: ["100x100"] }, i.sizes)
22
+ end
23
+
24
+ test "root defined to be Dir.pwd/public" do
25
+ assert_equal File.join(Dir.pwd, "public"), Imagery.root
26
+ end
27
+
28
+ test "root accepts arguments" do
29
+ assert_equal File.join(Dir.pwd, "public", "tmp"), Imagery.root("tmp")
30
+ end
31
+
32
+ test "allows override of the default Dir.pwd" do
33
+ begin
34
+ tmp = File.expand_path("tmp", File.dirname(__FILE__))
35
+
36
+ Imagery.root = tmp
37
+ assert_equal tmp, Imagery.root
38
+
39
+ ensure
40
+ Imagery.root = File.join(Dir.pwd, "public")
41
+ end
42
+ end
43
+
44
+ test "url when missing key" do
45
+ i = Imagery.new(:avatar)
46
+
47
+ assert_equal "/missing/avatar/original.jpg", i.url
48
+ end
49
+
50
+ test "url with a key" do
51
+ i = Imagery.new(:avatar, "1001")
52
+ assert_equal "/avatar/1001/original.jpg", i.url
53
+ end
54
+
55
+ test "url with a key and a file" do
56
+ i = Imagery.new(:avatar, "1001")
57
+ assert_equal "/avatar/1001/small.jpg", i.url(:small)
58
+ end
59
+
60
+ # basic persistence
61
+ scope do
62
+ setup do
63
+ imagery = Imagery.new(:avatar, "1001")
64
+ io = File.open(fixture("r8.jpg"), "rb")
65
+
66
+ [imagery, io]
67
+ end
68
+
69
+ test "saving without any sizes defined" do |im, io|
70
+ assert im.save(io)
71
+
72
+ assert File.exist?(im.root("original.jpg"))
73
+ assert_equal "1024x768", resolution(im.root("original.jpg"))
74
+ end
75
+
76
+ test "saving and specifying the key" do |im, io|
77
+ assert im.save(io, "GUID")
78
+ assert File.exist?(Imagery.root("avatar/GUID/original.jpg"))
79
+ end
80
+
81
+ test "saving with an already existing image" do |im, io|
82
+ im.save(io)
83
+
84
+ assert im.save(io, "GUID")
85
+ assert File.exist?(Imagery.root("avatar/GUID/original.jpg"))
86
+ assert ! File.exist?(Imagery.root("avatar/1001/original.jpg"))
87
+ end
88
+ end
89
+
90
+ # basic resizing
91
+ scope do
92
+ setup do
93
+ imagery = Imagery.new(:avatar, 1, small: ["100x100"], tiny: ["30x30"])
94
+ io = File.open(fixture("r8.jpg"), "rb")
95
+
96
+ [imagery, io]
97
+ end
98
+
99
+ test "saves the different sizes" do |im, io|
100
+ assert im.save(io)
101
+
102
+ assert File.exist?(im.root("original.jpg"))
103
+ assert File.exist?(im.root("small.jpg"))
104
+ assert File.exist?(im.root("tiny.jpg"))
105
+
106
+ assert_equal "1024x768", resolution(im.root("original.jpg"))
107
+
108
+ # Since there was no extent or geometry specified, this will
109
+ # be resized by fitting the image proportionally within 100x100.
110
+ assert_equal "100x75", resolution(im.root("small.jpg"))
111
+
112
+ # Like small.jpg, it will be resized to fit within 30x30
113
+ assert_equal "30x23", resolution(im.root("tiny.jpg"))
114
+ end
115
+ end
116
+
117
+ # resizing with extent
118
+ scope do
119
+ setup do
120
+ imagery = Imagery.new(:avatar, 1, small: ["100x100^", "100x100"])
121
+ io = File.open(fixture("r8.jpg"), "rb")
122
+
123
+ [imagery, io]
124
+ end
125
+
126
+ test "saves an image maximized within the extent" do |im, io|
127
+ im.save(io)
128
+
129
+ assert_equal "100x100", resolution(im.root("small.jpg"))
130
+ end
131
+ end
132
+
133
+ # saving a corrupted / unrecognized file
134
+ scope do
135
+ setup do
136
+ imagery = Imagery.new(:avatar, 1, small: ["100x100^", "100x100"])
137
+ io = File.open(fixture("r8.jpg"), "rb")
138
+
139
+ [imagery, io]
140
+ end
141
+
142
+ test "creating a new image" do |im, io|
143
+ ex = nil
144
+
145
+ begin
146
+ im.save(File.open(fixture("broken.jpg"), "rb"))
147
+ rescue Imagery::InvalidImage => e
148
+ ex = e
149
+ end
150
+
151
+ assert ex
152
+
153
+ o, s = im.root("original.jpg"), im.root("small.jpg")
154
+
155
+ assert ! File.exist?(o)
156
+ assert ! File.exist?(s)
157
+ end
158
+
159
+ test "trying to save over an existing image" do |im, io|
160
+ im.save(io)
161
+
162
+ o, s = im.root("original.jpg"), im.root("small.jpg")
163
+
164
+ begin
165
+ im.save(File.open(fixture("broken.jpg"), "rb"), 2)
166
+ rescue Imagery::InvalidImage
167
+ end
168
+
169
+ assert File.exist?(o)
170
+ assert File.exist?(s)
171
+ end
172
+ end