snapimage 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|