imagery 0.0.6 → 0.2.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.
@@ -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