snapimage 0.1.1 → 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.
- data/README.md +2 -2
- data/bin/snapimage_generate_config +40 -7
- data/lib/snapimage.rb +5 -13
- data/lib/snapimage/adapters/cloudinary/config.rb +13 -0
- data/lib/snapimage/adapters/cloudinary/storage.rb +22 -0
- data/lib/snapimage/adapters/local/config.rb +14 -0
- data/lib/snapimage/adapters/local/storage.rb +48 -0
- data/lib/snapimage/config.rb +3 -1
- data/lib/snapimage/exceptions.rb +0 -6
- data/lib/snapimage/middleware.rb +2 -1
- data/lib/snapimage/rack/request.rb +1 -1
- data/lib/snapimage/rack/response.rb +2 -10
- data/lib/snapimage/server.rb +12 -17
- data/lib/snapimage/storage.rb +10 -0
- data/lib/snapimage/version.rb +1 -1
- data/snapimage.gemspec +0 -1
- data/spec/acceptance/upload_spec.rb +45 -3
- data/spec/snapimage/adapters/local/storage_spec.rb +44 -0
- data/spec/snapimage/middleware_spec.rb +2 -0
- data/spec/snapimage/rack/request_spec.rb +3 -9
- data/spec/snapimage/server_spec.rb +27 -11
- data/spec/support/assets/config.json +2 -0
- data/spec/support/assets/config.yml +2 -0
- metadata +9 -34
- data/lib/snapimage/image/image.rb +0 -103
- data/lib/snapimage/image/image_name_utils.rb +0 -131
- data/lib/snapimage/server_actions/server_actions.authorize.rb +0 -69
- data/lib/snapimage/server_actions/server_actions.delete_resource_images.rb +0 -23
- data/lib/snapimage/server_actions/server_actions.generate_image.rb +0 -169
- data/lib/snapimage/server_actions/server_actions.list_resource_images.rb +0 -23
- data/lib/snapimage/server_actions/server_actions.sync_resource.rb +0 -78
- data/lib/snapimage/storage/storage.rb +0 -120
- data/lib/snapimage/storage/storage_server.local.rb +0 -120
- data/lib/snapimage/storage/storage_server.rb +0 -110
@@ -1,69 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
module ServerActions
|
3
|
-
module Authorize
|
4
|
-
def get_token(role)
|
5
|
-
@request.json["#{role}_security_token"]
|
6
|
-
end
|
7
|
-
|
8
|
-
# Arguments:
|
9
|
-
# * role:: Can be either :client or :server
|
10
|
-
def token_available?(role)
|
11
|
-
!!get_token(role)
|
12
|
-
end
|
13
|
-
|
14
|
-
# A string is generated using
|
15
|
-
# * role:: "client" or "server"
|
16
|
-
# * date:: Date in the format "YYYY-MM-DD"
|
17
|
-
# * salt:: A shared salt
|
18
|
-
# * resource_id:: The resource's identifier
|
19
|
-
# and concatenated as "role:date:salt:resource_id".
|
20
|
-
#
|
21
|
-
# A SHA1 digest is generated off of the string to create a token.
|
22
|
-
#
|
23
|
-
# 3 tokens are generated: [yesterday, today, tomorrow].
|
24
|
-
def generate_tokens(role)
|
25
|
-
salt = @config["security_salt"]
|
26
|
-
now = Time.now
|
27
|
-
yesterday = (now - 24*60*60).strftime("%Y-%m-%d")
|
28
|
-
today = now.strftime("%Y-%m-%d")
|
29
|
-
tomorrow = (now + 24*60*60).strftime("%Y-%m-%d")
|
30
|
-
resource_id = @request.json["resource_identifier"]
|
31
|
-
[
|
32
|
-
Digest::SHA1.hexdigest("#{role}:#{yesterday}:#{salt}:#{resource_id}"),
|
33
|
-
Digest::SHA1.hexdigest("#{role}:#{today}:#{salt}:#{resource_id}"),
|
34
|
-
Digest::SHA1.hexdigest("#{role}:#{tomorrow}:#{salt}:#{resource_id}")
|
35
|
-
]
|
36
|
-
end
|
37
|
-
|
38
|
-
# If "security_salt" is set in the config, authorization is performed.
|
39
|
-
#
|
40
|
-
# A string is generated using
|
41
|
-
# * role:: "client" or "server"
|
42
|
-
# * date:: Date in the format "YYYY-MM-DD"
|
43
|
-
# * salt:: A shared salt
|
44
|
-
# * resource_id:: The resource's identifier
|
45
|
-
# and concatenated as "role:date:salt:resource_id".
|
46
|
-
#
|
47
|
-
# A SHA1 digest is generated off of the string to create a token. A token
|
48
|
-
# for yesterday, today, and tomorrow are generated and used to compare
|
49
|
-
# with the security token.
|
50
|
-
#
|
51
|
-
# When authorization fails, an error is raised. Authorization can fail in
|
52
|
-
# 2 ways.
|
53
|
-
# * AuthorizationRequired:: The role's security token is missing
|
54
|
-
# * AuthorizationFailed:: The security token did not match the generated token
|
55
|
-
#
|
56
|
-
# If authorization is successful, true is returned.
|
57
|
-
#
|
58
|
-
# Arguments:
|
59
|
-
# * role:: Can be either :client or :server
|
60
|
-
def authorize(role)
|
61
|
-
if @config["security_salt"]
|
62
|
-
raise SnapImage::AuthorizationRequired unless token_available?(role)
|
63
|
-
raise SnapImage::AuthorizationFailed unless generate_tokens(role).include?(get_token(role))
|
64
|
-
end
|
65
|
-
return true
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
module ServerActions
|
3
|
-
class DeleteResourceImages
|
4
|
-
include SnapImage::ServerActions::Authorize
|
5
|
-
|
6
|
-
def initialize(config, request, response)
|
7
|
-
@config = config
|
8
|
-
@storage = config.storage
|
9
|
-
@request = request
|
10
|
-
@response = response
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
authorize(:server)
|
15
|
-
@response.set_success(
|
16
|
-
message: "Delete Resource Images Successful",
|
17
|
-
deleted_image_urls: @storage.delete_resource_images(@request.json["resource_identifier"])
|
18
|
-
)
|
19
|
-
@response
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,169 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
module ServerActions
|
3
|
-
class GenerateImage
|
4
|
-
include SnapImage::ServerActions::Authorize
|
5
|
-
|
6
|
-
def initialize(config, request, response)
|
7
|
-
@config = config
|
8
|
-
@storage = config.storage
|
9
|
-
@request = request
|
10
|
-
@response = response
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
authorize(:client)
|
15
|
-
if request_valid?
|
16
|
-
image = get_image_for_modification
|
17
|
-
parts = SnapImage::ImageNameUtils.get_image_name_parts(image.public_url)
|
18
|
-
result = modify_image(image)
|
19
|
-
modified_image = result[:image]
|
20
|
-
modified_image_name = result[:name]
|
21
|
-
stored_image = @storage.add_image(modified_image, modified_image_name, @request.json["resource_identifier"])
|
22
|
-
@response.set_success(
|
23
|
-
message: "Get Modified Image Successful",
|
24
|
-
image_url: stored_image.public_url,
|
25
|
-
image_width: stored_image.width,
|
26
|
-
image_height: stored_image.height
|
27
|
-
)
|
28
|
-
# Release memory.
|
29
|
-
GC.start
|
30
|
-
else
|
31
|
-
@response.set_bad_request
|
32
|
-
end
|
33
|
-
@response
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
# Returns true if the request is valid. False otherwise.
|
39
|
-
def request_valid?
|
40
|
-
source_image_defined?
|
41
|
-
end
|
42
|
-
|
43
|
-
# Returns true if either "file" or JSON "url" is defined.
|
44
|
-
def source_image_defined?
|
45
|
-
!!(@request.file || @request.json["url"])
|
46
|
-
end
|
47
|
-
|
48
|
-
# Returns true if the image is being uploaded. False otherwise.
|
49
|
-
def upload?
|
50
|
-
@request.file || !@storage.local?(@request.json["url"])
|
51
|
-
end
|
52
|
-
|
53
|
-
# Returns true if cropping is required. False otherwise.
|
54
|
-
def crop?
|
55
|
-
@request.json["crop_x"] || @request.json["crop_y"] || @request.json["crop_width"] || @request.json["crop_height"]
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns true if all the cropping values are defined. False otherwise.
|
59
|
-
def crop_valid?
|
60
|
-
@request.json["crop_x"] && @request.json["crop_y"] && @request.json["crop_width"] && @request.json["crop_height"]
|
61
|
-
end
|
62
|
-
|
63
|
-
# Returns true if resizing is required. False otherwise.
|
64
|
-
def resize?
|
65
|
-
@request.json["width"] || @request.json["height"]
|
66
|
-
end
|
67
|
-
|
68
|
-
# Returns true if the image is too large and needs to be resized to fit.
|
69
|
-
# False otherwise.
|
70
|
-
def resize_to_fit?(image)
|
71
|
-
image.width > get_max_width || image.height > get_max_height
|
72
|
-
end
|
73
|
-
|
74
|
-
# Returns true if sharpening is required. False otherwise
|
75
|
-
def sharpen?
|
76
|
-
@request.json["sharpen"]
|
77
|
-
end
|
78
|
-
|
79
|
-
# Gets the max width. Takes the lesser of the JSON "max_width" or the
|
80
|
-
# server max width.
|
81
|
-
def get_max_width
|
82
|
-
server_max_width = @config["max_width"]
|
83
|
-
[(@request.json["max_width"] && @request.json["max_width"].to_i || server_max_width), server_max_width].min
|
84
|
-
end
|
85
|
-
|
86
|
-
# Gets the max height. Takes the lesser of the JSON "max_height" or the
|
87
|
-
# server max height.
|
88
|
-
def get_max_height
|
89
|
-
server_max_height = @config["max_height"]
|
90
|
-
[(@request.json["max_height"] && @request.json["max_height"].to_i || server_max_height), server_max_height].min
|
91
|
-
end
|
92
|
-
|
93
|
-
def get_image_for_modification
|
94
|
-
if upload?
|
95
|
-
# Add the image to the storage.
|
96
|
-
if @request.file
|
97
|
-
image = @storage.add_upload(@request.file, @request.json["resource_identifier"])
|
98
|
-
else
|
99
|
-
image = @storage.add_url(@request.json["url"], @request.json["resource_identifier"])
|
100
|
-
end
|
101
|
-
else
|
102
|
-
# Get the base image.
|
103
|
-
raise SnapImage::InvalidImageIdentifier unless SnapImage::ImageNameUtils.valid?(@request.json["url"])
|
104
|
-
image = @storage.get(SnapImage::ImageNameUtils.get_base_image_path(@request.json["url"]))
|
105
|
-
end
|
106
|
-
image
|
107
|
-
end
|
108
|
-
|
109
|
-
# Arguments:
|
110
|
-
# * image:: SnapImage::Image object that represents the base image
|
111
|
-
def modify_image(image)
|
112
|
-
parts = SnapImage::ImageNameUtils.get_image_name_parts(image.public_url)
|
113
|
-
|
114
|
-
# Crop.
|
115
|
-
cropped = crop?
|
116
|
-
crop = nil
|
117
|
-
if cropped
|
118
|
-
raise SnapImage::BadRequest, "Missing crop values." unless crop_valid?
|
119
|
-
crop = {
|
120
|
-
x: @request.json["crop_x"],
|
121
|
-
y: @request.json["crop_y"],
|
122
|
-
width: @request.json["crop_width"],
|
123
|
-
height: @request.json["crop_height"]
|
124
|
-
}
|
125
|
-
image.crop(crop[:x], crop[:y], crop[:width], crop[:height])
|
126
|
-
end
|
127
|
-
|
128
|
-
# Resize.
|
129
|
-
resized = resize?
|
130
|
-
if resized
|
131
|
-
width = @request.json["width"] && @request.json["width"].to_i
|
132
|
-
height = @request.json["height"] && @request.json["height"].to_i
|
133
|
-
if width && height
|
134
|
-
# When both width and height are specified, resize without
|
135
|
-
# maintaining the aspect ratio.
|
136
|
-
image.resize(width, height, false)
|
137
|
-
else
|
138
|
-
# When only one of width/height is specified, set the other to the
|
139
|
-
# max and maintain the aspect ratio.
|
140
|
-
image.resize(width || @config["max_width"], height || @config["max_height"])
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# Resize to fit.
|
145
|
-
resized_to_fit = resize_to_fit?(image)
|
146
|
-
image.resize(get_max_width, get_max_height) if resized_to_fit
|
147
|
-
|
148
|
-
# Sharpen.
|
149
|
-
sharpened = sharpen?
|
150
|
-
image.sharpen if sharpened
|
151
|
-
|
152
|
-
# Get the dimensions at the end.
|
153
|
-
if cropped || resized || resized_to_fit || sharpened
|
154
|
-
modifications = {
|
155
|
-
crop: crop || { x: 0, y: 0, width: parts[:original_dimensions][0], height: parts[:original_dimensions][1] },
|
156
|
-
width: image.width,
|
157
|
-
height: image.height,
|
158
|
-
sharpen: sharpened
|
159
|
-
}
|
160
|
-
name = SnapImage::ImageNameUtils.generate_image_name(parts[:original_dimensions][0], parts[:original_dimensions][1], parts[:extname], {basename: parts[:basename]}.merge(modifications))
|
161
|
-
else
|
162
|
-
name = parts[:filename]
|
163
|
-
end
|
164
|
-
|
165
|
-
{ image: image, name: name }
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
module ServerActions
|
3
|
-
class ListResourceImages
|
4
|
-
include SnapImage::ServerActions::Authorize
|
5
|
-
|
6
|
-
def initialize(config, request, response)
|
7
|
-
@config = config
|
8
|
-
@storage = config.storage
|
9
|
-
@request = request
|
10
|
-
@response = response
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
authorize(:server)
|
15
|
-
@response.set_success(
|
16
|
-
message: "List Resource Images Successful",
|
17
|
-
image_urls: @storage.get_resource_urls(@request.json["resource_identifier"])
|
18
|
-
)
|
19
|
-
@response
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
module ServerActions
|
3
|
-
class SyncResource
|
4
|
-
include SnapImage::ServerActions::Authorize
|
5
|
-
|
6
|
-
def initialize(config, request, response)
|
7
|
-
@config = config
|
8
|
-
@storage = config.storage
|
9
|
-
@request = request
|
10
|
-
@response = response
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
authorize(:server)
|
15
|
-
if request_valid?
|
16
|
-
@response.set_success(
|
17
|
-
message: "Image Sync Successful",
|
18
|
-
deleted_image_urls: sync
|
19
|
-
)
|
20
|
-
else
|
21
|
-
@response.set_bad_request
|
22
|
-
end
|
23
|
-
@response
|
24
|
-
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
# Returns true if the request is valid. False otherwise.
|
29
|
-
def request_valid?
|
30
|
-
!!(content_valid? && @request.json["sync_date_time"])
|
31
|
-
end
|
32
|
-
|
33
|
-
# Returns true if "content" in the request is valid. False otherwise.
|
34
|
-
def content_valid?
|
35
|
-
content = @request.json["content"]
|
36
|
-
!!(content && content.is_a?(Hash) && !content.empty?)
|
37
|
-
end
|
38
|
-
|
39
|
-
# Concatenates all the content and returns it.
|
40
|
-
def get_content
|
41
|
-
@request.json["content"].values.inject("") { |result, element| result + element }
|
42
|
-
end
|
43
|
-
|
44
|
-
def urls_to_keep
|
45
|
-
content = get_content
|
46
|
-
keep = {}
|
47
|
-
@storage.url_regexps.each do |regexp|
|
48
|
-
# We use #scan instead of #match because #match returns only the
|
49
|
-
# first match. #scan will return all matches that don't overlap.
|
50
|
-
# However, #scan does not behave like #match. If the regexp contains
|
51
|
-
# groupings, #scan returns only the matched groups. Otherwise, it
|
52
|
-
# returns the entire match.
|
53
|
-
# To normalize, we take the given regexp and always wrap it in a
|
54
|
-
# group to ensure that the regexp always has a group and we always
|
55
|
-
# get back the entire match.
|
56
|
-
content.scan(Regexp.new("(#{regexp.source})", regexp.options)).each do |match|
|
57
|
-
keep[match[0]] = true
|
58
|
-
# If the image is modified, make sure to keep the base image too.
|
59
|
-
if !SnapImage::ImageNameUtils.base_image?(match[0])
|
60
|
-
keep[SnapImage::ImageNameUtils.get_base_image_path(match[0])] = true
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
keep.keys
|
65
|
-
end
|
66
|
-
|
67
|
-
def sync
|
68
|
-
urls_to_delete = @storage.get_resource_urls(
|
69
|
-
@request.json["resource_identifier"],
|
70
|
-
# DateTime only deals with years and days. We need to convert to a
|
71
|
-
# Time first to handle seconds, then convert back to a DateTime.
|
72
|
-
(DateTime.parse(@request.json["sync_date_time"]).to_time - 3).to_datetime
|
73
|
-
) - urls_to_keep
|
74
|
-
urls_to_delete.each { |url| @storage.delete(url) }
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,120 +0,0 @@
|
|
1
|
-
module SnapImage
|
2
|
-
class Storage
|
3
|
-
TYPES = %w{LOCAL}
|
4
|
-
FILE_TYPES = %w{png jpg gif}
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(server_configs, primary_server_name, max_width, max_height)
|
8
|
-
@server_configs = server_configs
|
9
|
-
# TODO: Remove this once we figure out how to handle multiple servers.
|
10
|
-
raise SnapImage::InvalidConfig, "Only one storage server can be specified at the moment" unless @server_configs.size == 1
|
11
|
-
@primary_server_name = primary_server_name
|
12
|
-
@max_width = max_width
|
13
|
-
@max_height = max_height
|
14
|
-
end
|
15
|
-
|
16
|
-
# Returns an array of all the url regexps that are handled by the storage.
|
17
|
-
def url_regexps
|
18
|
-
@url_regexps ||= servers.values.map { |s| s.url_regexp }
|
19
|
-
end
|
20
|
-
|
21
|
-
# Returns true if the url is local to the storage. False otherwise.
|
22
|
-
def local?(url)
|
23
|
-
!!get_server_by_url(url)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Adds the image to the storage and returns the public url to the file.
|
27
|
-
# Arguments:
|
28
|
-
# * file:: SnapImage::RequestFile
|
29
|
-
# * resource_id:: Resource identifier
|
30
|
-
def add_upload(file, resource_id)
|
31
|
-
raise UnknownFileType, "Unknown file type for upload: #{file.type}" unless FILE_TYPES.include?(file.type)
|
32
|
-
primary_server.store_file(file.file, file.type, resource_id)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Adds the image to the storage from the url.
|
36
|
-
# If the image is from a storage server, nothing happens.
|
37
|
-
# If the image is from another place, the image is downloaded and added to
|
38
|
-
# the primary storage server.
|
39
|
-
# The image is returned.
|
40
|
-
# Arguments:
|
41
|
-
# * url:: A full url to the image
|
42
|
-
# * resource_id:: Resource identifier
|
43
|
-
def add_url(url, resource_id)
|
44
|
-
# If the url is local, then the image should already be in the storage.
|
45
|
-
return get(url) if get_server_by_url(url)
|
46
|
-
type = ImageNameUtils.get_image_type(url)
|
47
|
-
raise UnknownFileType, "Unknown file type for upload: #{type}" unless FILE_TYPES.include?(type)
|
48
|
-
primary_server.store_url(url, type, resource_id)
|
49
|
-
end
|
50
|
-
|
51
|
-
# Adds the image to the storage.
|
52
|
-
# Arguments:
|
53
|
-
# * image:: SnapImage::Image object
|
54
|
-
# * name:: Name of the image
|
55
|
-
# * resource_id:: Resource identifier
|
56
|
-
def add_image(image, name, resource_id)
|
57
|
-
primary_server.store_image(image, name, resource_id)
|
58
|
-
end
|
59
|
-
|
60
|
-
# Returns a SnapImage::Image using the url.
|
61
|
-
def get(url)
|
62
|
-
get_server_by_url(url).get(url)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Returns all the image urls for the given resource in the storage.
|
66
|
-
# Arguments:
|
67
|
-
# * resource_id:: Filter by resource identifier
|
68
|
-
# * timestamp:: Only images that were updated before the DateTime
|
69
|
-
def get_resource_urls(resource_id, timestamp = nil)
|
70
|
-
servers.values.inject([]) { |urls, server| urls + server.get_resource_urls(resource_id, timestamp) }
|
71
|
-
end
|
72
|
-
|
73
|
-
# Deletes the given url from the storage.
|
74
|
-
# Arguments:
|
75
|
-
# * url:: URL to delete
|
76
|
-
def delete(url)
|
77
|
-
get_server_by_url(url).delete(url)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Deletes all the image related to the resource.
|
81
|
-
# Arugments:
|
82
|
-
# * resource_id:: Resource identifier
|
83
|
-
def delete_resource_images(resource_id)
|
84
|
-
deleted_urls = []
|
85
|
-
servers.each do |name, server|
|
86
|
-
deleted_urls += server.delete_resource_images(resource_id)
|
87
|
-
end
|
88
|
-
deleted_urls
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def servers
|
94
|
-
return @servers if @servers
|
95
|
-
@servers = {}
|
96
|
-
@server_configs.each do |config|
|
97
|
-
type = config["type"]
|
98
|
-
@servers[config["name"]] = get_server_class(type).new(config.merge("max_width" => @max_width, "max_height" => @max_height))
|
99
|
-
end
|
100
|
-
@servers
|
101
|
-
end
|
102
|
-
|
103
|
-
def primary_server
|
104
|
-
servers[@primary_server_name]
|
105
|
-
end
|
106
|
-
|
107
|
-
def get_server_class(type)
|
108
|
-
raise SnapImage::InvalidStorageConfig, "Storage server type not supported: #{type}" unless TYPES.include?(type)
|
109
|
-
klassname = type.downcase.split("_").map { |t| t.capitalize }.join("")
|
110
|
-
SnapImage::StorageServer.const_get(klassname)
|
111
|
-
end
|
112
|
-
|
113
|
-
def get_server_by_url(url)
|
114
|
-
servers.each do |name, server|
|
115
|
-
return server if server.local?(url)
|
116
|
-
end
|
117
|
-
return nil
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|