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.
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