snapimage 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/README.md +2 -2
  2. data/bin/snapimage_generate_config +40 -7
  3. data/lib/snapimage.rb +5 -13
  4. data/lib/snapimage/adapters/cloudinary/config.rb +13 -0
  5. data/lib/snapimage/adapters/cloudinary/storage.rb +22 -0
  6. data/lib/snapimage/adapters/local/config.rb +14 -0
  7. data/lib/snapimage/adapters/local/storage.rb +48 -0
  8. data/lib/snapimage/config.rb +3 -1
  9. data/lib/snapimage/exceptions.rb +0 -6
  10. data/lib/snapimage/middleware.rb +2 -1
  11. data/lib/snapimage/rack/request.rb +1 -1
  12. data/lib/snapimage/rack/response.rb +2 -10
  13. data/lib/snapimage/server.rb +12 -17
  14. data/lib/snapimage/storage.rb +10 -0
  15. data/lib/snapimage/version.rb +1 -1
  16. data/snapimage.gemspec +0 -1
  17. data/spec/acceptance/upload_spec.rb +45 -3
  18. data/spec/snapimage/adapters/local/storage_spec.rb +44 -0
  19. data/spec/snapimage/middleware_spec.rb +2 -0
  20. data/spec/snapimage/rack/request_spec.rb +3 -9
  21. data/spec/snapimage/server_spec.rb +27 -11
  22. data/spec/support/assets/config.json +2 -0
  23. data/spec/support/assets/config.yml +2 -0
  24. metadata +9 -34
  25. data/lib/snapimage/image/image.rb +0 -103
  26. data/lib/snapimage/image/image_name_utils.rb +0 -131
  27. data/lib/snapimage/server_actions/server_actions.authorize.rb +0 -69
  28. data/lib/snapimage/server_actions/server_actions.delete_resource_images.rb +0 -23
  29. data/lib/snapimage/server_actions/server_actions.generate_image.rb +0 -169
  30. data/lib/snapimage/server_actions/server_actions.list_resource_images.rb +0 -23
  31. data/lib/snapimage/server_actions/server_actions.sync_resource.rb +0 -78
  32. data/lib/snapimage/storage/storage.rb +0 -120
  33. data/lib/snapimage/storage/storage_server.local.rb +0 -120
  34. 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