imagery 0.0.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +36 -63
- data/imagery.gemspec +8 -71
- data/lib/imagery.rb +176 -18
- data/lib/imagery/faking.rb +2 -2
- data/lib/imagery/s3.rb +68 -128
- data/lib/imagery/test.rb +5 -5
- data/test/faking.rb +71 -0
- data/test/helper.rb +28 -9
- data/test/imagery.rb +172 -0
- data/test/s3.rb +109 -0
- metadata +22 -91
- data/.document +0 -5
- data/.gitignore +0 -24
- data/Rakefile +0 -56
- data/VERSION +0 -1
- data/lib/imagery/missing.rb +0 -30
- data/lib/imagery/model.rb +0 -217
- data/test/fixtures/lake.jpg +0 -0
- data/test/test_imagery.rb +0 -172
- data/test/test_missing.rb +0 -30
- data/test/test_with_s3.rb +0 -167
data/lib/imagery/faking.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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
|
data/lib/imagery/s3.rb
CHANGED
@@ -1,165 +1,106 @@
|
|
1
|
-
require
|
1
|
+
require "aws/s3"
|
2
2
|
|
3
|
-
|
3
|
+
class Imagery
|
4
4
|
module S3
|
5
|
-
def self.included(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
14
|
+
module Config
|
13
15
|
def s3_bucket(bucket = nil)
|
14
|
-
@s3_bucket = bucket
|
15
|
-
@s3_bucket
|
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
|
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
|
41
|
-
@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
|
-
|
31
|
+
# Convenience attribute which returns all size keys including `:original`.
|
32
|
+
attr :keys
|
48
33
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
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
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
49
|
+
"#{self.class.s3_host}/#{self.class.s3_bucket}#{super}"
|
91
50
|
end
|
92
51
|
end
|
93
|
-
|
94
|
-
#
|
95
|
-
# the
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
110
|
-
# all
|
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
|
-
|
114
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/imagery/test.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
|
1
|
+
class Imagery
|
2
2
|
module Test
|
3
3
|
def self.included(base)
|
4
|
-
Imagery
|
5
|
-
Imagery
|
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
|
11
|
+
Imagery.real { yield true }
|
12
12
|
else
|
13
|
-
Imagery
|
13
|
+
Imagery.faked { yield false }
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
data/test/faking.rb
ADDED
@@ -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
|
data/test/helper.rb
CHANGED
@@ -1,12 +1,31 @@
|
|
1
|
-
|
2
|
-
require 'test/unit'
|
3
|
-
require 'contest'
|
4
|
-
require 'mocha'
|
1
|
+
$:.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
require 'imagery'
|
3
|
+
require "cutest"
|
4
|
+
require "imagery"
|
9
5
|
|
10
|
-
|
11
|
-
|
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
|
data/test/imagery.rb
ADDED
@@ -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
|