snapimage 0.0.1
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/.autotest +3 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/bin/snapimage_generate_config +63 -0
- data/bin/snapimage_server +55 -0
- data/lib/snapimage.rb +24 -0
- data/lib/snapimage/config.rb +51 -0
- data/lib/snapimage/exceptions.rb +25 -0
- data/lib/snapimage/image/image.rb +96 -0
- data/lib/snapimage/image/image_name_utils.rb +131 -0
- data/lib/snapimage/middleware.rb +27 -0
- data/lib/snapimage/rack/request.rb +19 -0
- data/lib/snapimage/rack/request_file.rb +26 -0
- data/lib/snapimage/rack/response.rb +51 -0
- data/lib/snapimage/server.rb +50 -0
- data/lib/snapimage/server_actions/server_actions.authorize.rb +69 -0
- data/lib/snapimage/server_actions/server_actions.delete_resource_images.rb +23 -0
- data/lib/snapimage/server_actions/server_actions.generate_image.rb +167 -0
- data/lib/snapimage/server_actions/server_actions.list_resource_images.rb +23 -0
- data/lib/snapimage/server_actions/server_actions.sync_resource.rb +78 -0
- data/lib/snapimage/storage/storage.rb +120 -0
- data/lib/snapimage/storage/storage_server.local.rb +120 -0
- data/lib/snapimage/storage/storage_server.rb +110 -0
- data/lib/snapimage/version.rb +3 -0
- data/snapimage.gemspec +27 -0
- data/spec/acceptance/delete_resource_images_spec.rb +166 -0
- data/spec/acceptance/list_resource_images_spec.rb +158 -0
- data/spec/acceptance/modify_spec.rb +165 -0
- data/spec/acceptance/sync_spec.rb +260 -0
- data/spec/acceptance/upload_spec.rb +235 -0
- data/spec/snapimage/config_spec.rb +56 -0
- data/spec/snapimage/image/image_name_utils_spec.rb +127 -0
- data/spec/snapimage/image/image_spec.rb +71 -0
- data/spec/snapimage/middleware_spec.rb +27 -0
- data/spec/snapimage/rack/request_file_spec.rb +15 -0
- data/spec/snapimage/rack/request_spec.rb +52 -0
- data/spec/snapimage/rack/response_spec.rb +33 -0
- data/spec/snapimage/server_actions/server_actions.authorize_spec.rb +67 -0
- data/spec/snapimage/server_actions/server_actions.generate_image_spec.rb +146 -0
- data/spec/snapimage/server_actions/server_actions.sync_resource_spec.rb +91 -0
- data/spec/snapimage/server_spec.rb +55 -0
- data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1-0x0x1x1-1x1-1.gif +0 -0
- data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1-0x0x1x1-300x200-0.jpg +0 -0
- data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1.png +0 -0
- data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1-0x0x1x1-1x1-1.gif +0 -0
- data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1-0x0x1x1-300x200-0.jpg +0 -0
- data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1.png +0 -0
- data/spec/snapimage/storage/storage_server.local_spec.rb +150 -0
- data/spec/snapimage/storage/storage_server_spec.rb +97 -0
- data/spec/snapimage/storage/storage_spec.rb +49 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/assets/config.json +8 -0
- data/spec/support/assets/config.yml +9 -0
- data/spec/support/assets/stub-1x1.png +0 -0
- data/spec/support/assets/stub-2048x100.png +0 -0
- data/spec/support/assets/stub-300x200.png +0 -0
- metadata +272 -0
@@ -0,0 +1,78 @@
|
|
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
|
@@ -0,0 +1,120 @@
|
|
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
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module SnapImage
|
2
|
+
module StorageServer
|
3
|
+
class Local < SnapImage::StorageServer::Base
|
4
|
+
def validate_config
|
5
|
+
super
|
6
|
+
raise InvalidStorageConfig, 'Missing "local_root"' unless @config["local_root"]
|
7
|
+
end
|
8
|
+
|
9
|
+
def store_file(file, type, resource_id)
|
10
|
+
image = SnapImage::Image.from_blob(file.read)
|
11
|
+
name = SnapImage::ImageNameUtils.generate_image_name(image.width, image.height, type)
|
12
|
+
store(image, name, resource_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def store_url(url, type, resource_id)
|
16
|
+
image = SnapImage::Image.from_blob(URI.parse(url).read)
|
17
|
+
name = SnapImage::ImageNameUtils.generate_image_name(image.width, image.height, type)
|
18
|
+
store(image, name, resource_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def store_image(image, name, resource_id)
|
22
|
+
store(image, name, resource_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(url)
|
26
|
+
path = public_url_to_local_path(url)
|
27
|
+
raise SnapImage::FileDoesNotExist, "Missing file: #{path}" unless File.exists?(path)
|
28
|
+
SnapImage::Image.from_path(path, url)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_resource_urls(resource_id, timestamp = nil)
|
32
|
+
urls = []
|
33
|
+
get_resource_filenames(resource_id).each do |filename|
|
34
|
+
urls.push(local_path_to_public_url(filename)) if file_modified_before_timestamp?(filename, timestamp)
|
35
|
+
end
|
36
|
+
urls
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete(url)
|
40
|
+
path = public_url_to_local_path(url)
|
41
|
+
raise SnapImage::FileDoesNotExist, "Missing file: #{path}" unless File.exists?(path)
|
42
|
+
File.delete(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_resource_images(resource_id)
|
46
|
+
deleted_urls = get_resource_urls(resource_id)
|
47
|
+
FileUtils.rm_rf(File.join(root, resource_id)) if deleted_urls.size > 0
|
48
|
+
deleted_urls
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Stores the image and returns a SnapImage::Image object.
|
54
|
+
# If the image does not fit within the server max width/height, the image
|
55
|
+
# is resized and the modified image is returned.
|
56
|
+
# Arguments:
|
57
|
+
# * image:: SnapImage::Image to store
|
58
|
+
# * name:: Suggested name to use
|
59
|
+
# * resource_id:: Resource identifier
|
60
|
+
def store(image, name, resource_id)
|
61
|
+
result = resize_to_fit(image, name)
|
62
|
+
image = result[:image]
|
63
|
+
name = result[:name]
|
64
|
+
|
65
|
+
# Generate the filename and public url.
|
66
|
+
local_path = File.join(resource_id, name)
|
67
|
+
image.public_url = "#{File.join(@config["public_url"], local_path)}"
|
68
|
+
|
69
|
+
# Store the file.
|
70
|
+
path = File.join(root, local_path)
|
71
|
+
# Ensure the directory exists accounting for the resource_id.
|
72
|
+
dir = File.dirname(path)
|
73
|
+
FileUtils.mkdir_p(dir)
|
74
|
+
# Write the file to the storage.
|
75
|
+
File.open(path, "wb") { |f| f.write(image.blob) }
|
76
|
+
|
77
|
+
# Return the image.
|
78
|
+
image
|
79
|
+
end
|
80
|
+
|
81
|
+
def root
|
82
|
+
unless @root_exists
|
83
|
+
FileUtils.mkdir_p(@config["local_root"]) unless File.directory?(@config["local_root"])
|
84
|
+
@root_exists = true
|
85
|
+
end
|
86
|
+
@config["local_root"]
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_local_path_parts(path)
|
90
|
+
match = path.match(/#{root}\/(.+)\/([^\/]+\.(png|jpg|gif))/)
|
91
|
+
return match && {
|
92
|
+
resource_id: match[1],
|
93
|
+
filename: match[2]
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
def local_path_to_public_url(path)
|
98
|
+
parts = get_local_path_parts(path)
|
99
|
+
"#{File.join(@config["public_url"], parts[:resource_id], parts[:filename])}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def public_url_to_local_path(url)
|
103
|
+
parts = get_url_parts(url)
|
104
|
+
path = File.join(root, parts[:path])
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_resource_filenames(resource_id)
|
108
|
+
Dir.glob(File.join(root, resource_id, "/**/*.{png,jpg,gif}"))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns true if no timestamp is given or the file was modified before
|
112
|
+
# the timestamp.
|
113
|
+
def file_modified_before_timestamp?(filename, timestamp = nil)
|
114
|
+
# File.mtime returns a Time object. Convert it to a DateTime because
|
115
|
+
# timestamp is a DateTime.
|
116
|
+
!timestamp || File.mtime(filename).to_datetime < timestamp
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module SnapImage
|
2
|
+
module StorageServer
|
3
|
+
class Base
|
4
|
+
# Config:
|
5
|
+
# * name:: Name of the storage.
|
6
|
+
# * public_url:: URL to acces the storage.
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
validate_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
@config["name"]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a regular expression for matching urls handled by this storage
|
17
|
+
# server.
|
18
|
+
def url_regexp
|
19
|
+
@url_regexp ||= /#{@config["public_url"]}\/.+?\.(png|gif|jpg)/
|
20
|
+
end
|
21
|
+
|
22
|
+
def local?(url)
|
23
|
+
!!url.match(url_regexp)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Stores the file in the storage and returns a SnapImage::Image object.
|
27
|
+
# Arguments:
|
28
|
+
# * file:: File object representing the file to store.
|
29
|
+
# * type:: File type
|
30
|
+
# * resource_id:: Resource identifier.
|
31
|
+
def store_file(file, type, resource_id)
|
32
|
+
raise "#store_file needs to be overridden."
|
33
|
+
end
|
34
|
+
|
35
|
+
# Downloads the file and adds it to the storage and returns a
|
36
|
+
# SnapImage::Image object.
|
37
|
+
# Arguments:
|
38
|
+
# * url:: Url to get the image
|
39
|
+
# * type:: File type
|
40
|
+
# * resource_id:: Resource identifier.
|
41
|
+
def store_url(url, type, resource_id)
|
42
|
+
raise "#store_url needs to be overridden."
|
43
|
+
end
|
44
|
+
|
45
|
+
# Adds the image to the storage. Overwrites existing file.
|
46
|
+
# Arguments:
|
47
|
+
# * image:: SnapImage::Image object
|
48
|
+
# * name:: Name of the image
|
49
|
+
# * resource_id:: Resource identifier
|
50
|
+
def store_image(image, name, resource_id)
|
51
|
+
raise "#store_image needs to be overridden."
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the SnapImage:Image object from the url.
|
55
|
+
def get(url)
|
56
|
+
raise "#get needs to be overriden."
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns all the image urls for the given resource in the storage.
|
60
|
+
# Arguments:
|
61
|
+
# * resource_id:: Filter by resource identifier
|
62
|
+
# * timestamp:: Only images that were updated before the DateTime
|
63
|
+
def get_resource_urls(resource_id, timestamp = nil)
|
64
|
+
raise "#get_all_urls needs to be overriden."
|
65
|
+
end
|
66
|
+
|
67
|
+
# Deletes the given url.
|
68
|
+
def delete(url)
|
69
|
+
raise "#delete needs to be overridden."
|
70
|
+
end
|
71
|
+
|
72
|
+
# Deletes the given resource images.
|
73
|
+
def delete_resource_images(resource_id)
|
74
|
+
raise "#delete_resource needs to be overridden."
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Validates the config. Subclasses should add to this.
|
80
|
+
def validate_config
|
81
|
+
raise InvalidStorageConfig, 'Missing "name"' unless @config["name"]
|
82
|
+
raise InvalidStorageConfig, 'Missing "public_url"' unless @config["public_url"]
|
83
|
+
raise InvalidStorageConfig, 'Missing "max_width"' unless @config["max_width"]
|
84
|
+
raise InvalidStorageConfig, 'Missing "max_height"' unless @config["max_height"]
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_url_parts(url)
|
88
|
+
match = url.match(/^(([a-z]+):|)(#{@config["public_url"]})\/(.+)$/)
|
89
|
+
return match && {
|
90
|
+
protocol: match[2],
|
91
|
+
public_url: match[3],
|
92
|
+
path: match[4]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
# Resizes the image if it doesn't fit on the server. Updates the name if
|
97
|
+
# needed.
|
98
|
+
# Returns { image: resized_image, name: resized_name }.
|
99
|
+
def resize_to_fit(image, name)
|
100
|
+
# Resize the image if it's larger than the max width/height.
|
101
|
+
if image.width > @config["max_width"] || image.height > @config["max_height"]
|
102
|
+
resized_image = image.resize([image.width, @config["max_width"]].min, [image.height, @config["max_height"]].min)
|
103
|
+
# Generate a new name.
|
104
|
+
resized_name = SnapImage::ImageNameUtils.get_resized_image_name(name, resized_image.width, resized_image.height)
|
105
|
+
end
|
106
|
+
{ image: resized_image || image, name: resized_name || name }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/snapimage.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/snapimage/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Wesley Wong"]
|
6
|
+
gem.email = ["wesley@snapeditor.com"]
|
7
|
+
gem.description = %q{Rack Middleware for handling the SnapImage API}
|
8
|
+
gem.summary = %q{SnapImage API Rack Middleware}
|
9
|
+
gem.homepage = "http://SnapEditor.com"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "snapimage"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = SnapImage::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency("rack")
|
19
|
+
gem.add_dependency("rmagick")
|
20
|
+
gem.add_dependency("sinatra")
|
21
|
+
gem.add_dependency("thin")
|
22
|
+
gem.add_dependency("rake")
|
23
|
+
|
24
|
+
gem.add_development_dependency("rspec")
|
25
|
+
gem.add_development_dependency("autotest")
|
26
|
+
gem.add_development_dependency("rack-test")
|
27
|
+
end
|