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.
Files changed (61) hide show
  1. data/.autotest +3 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +29 -0
  7. data/Rakefile +2 -0
  8. data/bin/snapimage_generate_config +63 -0
  9. data/bin/snapimage_server +55 -0
  10. data/lib/snapimage.rb +24 -0
  11. data/lib/snapimage/config.rb +51 -0
  12. data/lib/snapimage/exceptions.rb +25 -0
  13. data/lib/snapimage/image/image.rb +96 -0
  14. data/lib/snapimage/image/image_name_utils.rb +131 -0
  15. data/lib/snapimage/middleware.rb +27 -0
  16. data/lib/snapimage/rack/request.rb +19 -0
  17. data/lib/snapimage/rack/request_file.rb +26 -0
  18. data/lib/snapimage/rack/response.rb +51 -0
  19. data/lib/snapimage/server.rb +50 -0
  20. data/lib/snapimage/server_actions/server_actions.authorize.rb +69 -0
  21. data/lib/snapimage/server_actions/server_actions.delete_resource_images.rb +23 -0
  22. data/lib/snapimage/server_actions/server_actions.generate_image.rb +167 -0
  23. data/lib/snapimage/server_actions/server_actions.list_resource_images.rb +23 -0
  24. data/lib/snapimage/server_actions/server_actions.sync_resource.rb +78 -0
  25. data/lib/snapimage/storage/storage.rb +120 -0
  26. data/lib/snapimage/storage/storage_server.local.rb +120 -0
  27. data/lib/snapimage/storage/storage_server.rb +110 -0
  28. data/lib/snapimage/version.rb +3 -0
  29. data/snapimage.gemspec +27 -0
  30. data/spec/acceptance/delete_resource_images_spec.rb +166 -0
  31. data/spec/acceptance/list_resource_images_spec.rb +158 -0
  32. data/spec/acceptance/modify_spec.rb +165 -0
  33. data/spec/acceptance/sync_spec.rb +260 -0
  34. data/spec/acceptance/upload_spec.rb +235 -0
  35. data/spec/snapimage/config_spec.rb +56 -0
  36. data/spec/snapimage/image/image_name_utils_spec.rb +127 -0
  37. data/spec/snapimage/image/image_spec.rb +71 -0
  38. data/spec/snapimage/middleware_spec.rb +27 -0
  39. data/spec/snapimage/rack/request_file_spec.rb +15 -0
  40. data/spec/snapimage/rack/request_spec.rb +52 -0
  41. data/spec/snapimage/rack/response_spec.rb +33 -0
  42. data/spec/snapimage/server_actions/server_actions.authorize_spec.rb +67 -0
  43. data/spec/snapimage/server_actions/server_actions.generate_image_spec.rb +146 -0
  44. data/spec/snapimage/server_actions/server_actions.sync_resource_spec.rb +91 -0
  45. data/spec/snapimage/server_spec.rb +55 -0
  46. data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1-0x0x1x1-1x1-1.gif +0 -0
  47. data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1-0x0x1x1-300x200-0.jpg +0 -0
  48. data/spec/snapimage/storage/assets/local/resource_1/12345678-1x1.png +0 -0
  49. data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1-0x0x1x1-1x1-1.gif +0 -0
  50. data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1-0x0x1x1-300x200-0.jpg +0 -0
  51. data/spec/snapimage/storage/assets/local/resource_2/12345678-1x1.png +0 -0
  52. data/spec/snapimage/storage/storage_server.local_spec.rb +150 -0
  53. data/spec/snapimage/storage/storage_server_spec.rb +97 -0
  54. data/spec/snapimage/storage/storage_spec.rb +49 -0
  55. data/spec/spec_helper.rb +18 -0
  56. data/spec/support/assets/config.json +8 -0
  57. data/spec/support/assets/config.yml +9 -0
  58. data/spec/support/assets/stub-1x1.png +0 -0
  59. data/spec/support/assets/stub-2048x100.png +0 -0
  60. data/spec/support/assets/stub-300x200.png +0 -0
  61. metadata +272 -0
data/.autotest ADDED
@@ -0,0 +1,3 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ %w{.git doc log tmp vendor}.each { |ex| at.add_exception(ex) }
3
+ end
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rbenv-version
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ /vendor/bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snapimage.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Wesley Wong
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Snapimage
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'snapimage'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install snapimage
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+
5
+ options = {
6
+ file: "snapimage_config.yml",
7
+ force: false
8
+ }
9
+
10
+ optparse = OptionParser.new do |opts|
11
+ opts.banner = "Usage: snapimage_generate_config local_root public_url [options]"
12
+
13
+ opts.separator ""
14
+ opts.separator "local_root\tPath to the directory where the images will be stored"
15
+ opts.separator "public_url\tURL at which the images will accessible from"
16
+
17
+ opts.separator ""
18
+ opts.separator "Options:"
19
+
20
+ opts.on("-f", "--file FILE", "File to generate (Default: snapimage_config.yml)") do |file|
21
+ options[:file] = file
22
+ end
23
+
24
+ opts.on("--force", "Overwrites the file if it exists") do
25
+ options[:force] = true
26
+ end
27
+
28
+ opts.on("-h", "--help", "Display the help screen") do
29
+ puts opts
30
+ exit
31
+ end
32
+ end
33
+ optparse.parse!
34
+
35
+ unless ARGV.length == 2
36
+ puts optparse.help
37
+ exit
38
+ end
39
+
40
+ local_root = ARGV[0]
41
+ public_url = ARGV[1]
42
+
43
+ if !options[:force] && File.exists?(options[:file])
44
+ puts "File '#{options[:file]}' already exists. Use --force if you want to overwrite."
45
+ puts "Config file not generated."
46
+ puts
47
+ puts optparse.help
48
+ exit
49
+ end
50
+
51
+ File.open(options[:file], "w") do |f|
52
+ f.write(<<-EOF
53
+ primary_storage_server: "local"
54
+ storage_servers:
55
+ -
56
+ name: "local"
57
+ type: "LOCAL"
58
+ local_root: "#{local_root}"
59
+ public_url: "#{public_url}"
60
+ EOF
61
+ )
62
+ end
63
+ puts "Config file generated at '#{options[:file]}'."
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+
5
+ options = {
6
+ port: 54321
7
+ }
8
+
9
+ optparse = OptionParser.new do |opts|
10
+ opts.banner = "Usage: snapimage_server config [options]"
11
+
12
+ opts.on("-p", "--port", "Set the port (Default: 54321") do |port|
13
+ options[:port] = port
14
+ end
15
+
16
+ opts.on("--path PATH", "Set the URL path of the SnapImage API Server (Default: /snapimage_api)") do |path|
17
+ options[:path] = path
18
+ end
19
+
20
+ opts.on("-h", "--help", "Display the help screen") do
21
+ puts opts
22
+ exit
23
+ end
24
+ end
25
+ optparse.parse!
26
+
27
+ unless ARGV.length == 1
28
+ puts optparse.help
29
+ exit
30
+ end
31
+ options[:config] = ARGV[0]
32
+
33
+ require "sinatra"
34
+ require "snapimage"
35
+
36
+ # Make sure the built-in web server runs.
37
+ set :run, true
38
+
39
+ # Set up Sinatra.
40
+ set :environment, :production
41
+ set :port, options[:port]
42
+
43
+ use SnapImage::Middleware, options
44
+
45
+ get "/crossdomain.xml" do
46
+ <<-XML
47
+ <?xml version="1.0"?>
48
+ <!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
49
+ <cross-domain-policy>
50
+
51
+ <allow-access-from domain="*" to-ports="#{options[:port]}" />
52
+
53
+ </cross-domain-policy>
54
+ XML
55
+ end
data/lib/snapimage.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "rack"
2
+ require "json"
3
+ require "fileutils"
4
+ require "digest/sha1"
5
+ require "open-uri"
6
+ require "RMagick"
7
+ require "snapimage/version"
8
+ require "snapimage/exceptions"
9
+ require "snapimage/rack/request_file"
10
+ require "snapimage/rack/request"
11
+ require "snapimage/rack/response"
12
+ require "snapimage/image/image"
13
+ require "snapimage/image/image_name_utils"
14
+ require "snapimage/storage/storage_server"
15
+ require "snapimage/storage/storage_server.local"
16
+ require "snapimage/storage/storage"
17
+ require "snapimage/config"
18
+ require "snapimage/server_actions/server_actions.authorize"
19
+ require "snapimage/server_actions/server_actions.generate_image"
20
+ require "snapimage/server_actions/server_actions.sync_resource"
21
+ require "snapimage/server_actions/server_actions.delete_resource_images"
22
+ require "snapimage/server_actions/server_actions.list_resource_images"
23
+ require "snapimage/server"
24
+ require "snapimage/middleware"
@@ -0,0 +1,51 @@
1
+ module SnapImage
2
+ class Config
3
+ # Arguments:
4
+ # * config:: Filename of the YAML or JSOn file to load or a config Hash.
5
+ #
6
+ # NOTE: All keys are strings, not symbols.
7
+ def initialize(config)
8
+ @raw_config = config
9
+ end
10
+
11
+ def validate_config
12
+ raise SnapImage::InvalidConfig, 'Missing "primary_storage_server"' unless @config["primary_storage_server"]
13
+ raise SnapImage::InvalidConfig, 'Missing "storage_servers"' unless @config["storage_servers"]
14
+ raise SnapImage::InvalidConfig, '"storage_servers" must be an array' unless @config["storage_servers"].is_a? Array
15
+ end
16
+
17
+ def set_config_defaults
18
+ @config["max_width"] ||= 1024
19
+ @config["max_height"] ||= 2048
20
+ end
21
+
22
+ def get_config
23
+ return @config if @config
24
+ @config = @raw_config
25
+ if @raw_config.is_a? String
26
+ ext = File.extname(@raw_config)
27
+ case ext
28
+ when ".yml", ".yaml"
29
+ @config = YAML::load(File.open(@raw_config))
30
+ when ".json"
31
+ @config = JSON::parse(File.read(@raw_config))
32
+ else
33
+ raise SnapImage::UnknownFileType, "Unknown filetype. Expecting .yaml, .yml, or .json: #{@raw_config}"
34
+ end
35
+ end
36
+
37
+ raise SnapImage::UnknownConfigType, "Unknown config type. Expecting a filename or hash: #{@config}" unless @config.is_a? Hash
38
+ validate_config
39
+ set_config_defaults
40
+ @config
41
+ end
42
+
43
+ def [](key)
44
+ get_config[key]
45
+ end
46
+
47
+ def storage
48
+ @storage ||= SnapImage::Storage.new(get_config["storage_servers"], get_config["primary_storage_server"], get_config["max_width"], get_config["max_height"])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ module SnapImage
2
+ # Configuration.
3
+ class InvalidConfig < StandardError; end
4
+ class InvalidStorageConfig < StandardError; end
5
+ class UnknownConfigType < StandardError; end
6
+ class MissingConfig < StandardError; end
7
+
8
+ # Authorization.
9
+ class AuthorizationRequired < StandardError; end
10
+ class AuthorizationFailed < StandardError; end
11
+
12
+ # Request.
13
+ class BadRequest < StandardError; end
14
+ class ActionNotImplemented < StandardError; end
15
+
16
+ # Files.
17
+ class UnknownFileType < StandardError; end
18
+ class FileDoesNotExist < StandardError; end
19
+
20
+ # Images.
21
+ class InvalidImageIdentifier < StandardError; end
22
+
23
+ # Resources.
24
+ class InvalidResourceIdentifier < StandardError; end
25
+ end
@@ -0,0 +1,96 @@
1
+ module SnapImage
2
+ class Image
3
+ def self.from_path(path, public_url)
4
+ image = SnapImage::Image.new(public_url)
5
+ image.set_image_from_path(path)
6
+ image
7
+ end
8
+
9
+ def self.from_blob(blob)
10
+ image = SnapImage::Image.new
11
+ image.set_image_from_blob(blob)
12
+ image
13
+ end
14
+
15
+ def self.from_image(img)
16
+ image = SnapImage::Image.new
17
+ image.set_image_from_image(img)
18
+ image
19
+ end
20
+
21
+ attr_accessor :public_url
22
+
23
+ # Arguments:
24
+ # * public_url:: Public URL associated with the image
25
+ def initialize(public_url = nil)
26
+ @public_url = public_url
27
+ end
28
+
29
+ # Arguments:
30
+ # * path:: Local path or URL
31
+ def set_image_from_path(path)
32
+ @image = Magick::ImageList.new(path)
33
+ end
34
+
35
+ # Arguments:
36
+ # * blob:: Image blob
37
+ def set_image_from_blob(blob)
38
+ @image = Magick::ImageList.new
39
+ @image.from_blob(blob)
40
+ end
41
+
42
+ # Arguments:
43
+ # * image:: RMagick Image
44
+ def set_image_from_image(image)
45
+ @image = image
46
+ end
47
+
48
+ def width
49
+ @image.columns
50
+ end
51
+
52
+ def height
53
+ @image.rows
54
+ end
55
+
56
+ def blob
57
+ @image.to_blob
58
+ end
59
+
60
+ # Crops the image with the given parameters and returns a SnapImage::Image
61
+ # object.
62
+ #
63
+ # Arguments:
64
+ # * x:: x coordinate of the top left corner
65
+ # * y:: y coordinate of the top left corner
66
+ # * width:: width of the crop rectangle
67
+ # * height:: height of the crop rectangle
68
+ def crop(x, y, width, height)
69
+ SnapImage::Image.from_image(@image.crop(x, y, width, height))
70
+ end
71
+
72
+ # Generates a new resized image and returns it as a SnapImage::Image object.
73
+ #
74
+ # Arguments:
75
+ # * width:: Width to resize to
76
+ # * height:: Height to resize to (optional)
77
+ # * maintain_aspect_ratio:: If true, the image will be resized to fit within the width/height specified while maintaining the aspect ratio. If false, the image is allowed to be stretched.
78
+ def resize(width, height = nil, maintain_aspect_ratio = true)
79
+ raise "Height must be specified when not maintaining aspect ratio." if !maintain_aspect_ratio && !height
80
+ # If no height is given, make sure it does not interfere with the
81
+ # resizing.
82
+ height ||= [width, @image.rows].max
83
+ if maintain_aspect_ratio
84
+ SnapImage::Image.from_image(@image.resize_to_fit(width, height))
85
+ else
86
+ SnapImage::Image.from_image(@image.resize(width, height))
87
+ end
88
+ end
89
+
90
+ # Generates a new sharpened image and returns it as a SnapImage::Image
91
+ # object.
92
+ def sharpen
93
+ SnapImage::Image.from_image(@image.sharpen)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,131 @@
1
+ module SnapImage
2
+ class ImageNameUtils
3
+ # Base Image Format:
4
+ # {basename}-{original dimensions}.{extname}
5
+ #
6
+ # Modified Image Format:
7
+ # {basename}-{original dimensions}-{crop}-{dimensions}-{sharpen}.{extname}
8
+ #
9
+ # Notes
10
+ # * crop:: {x}x{y}x{width}x{height}
11
+ # * dimensions:: {width}x{height}
12
+ # * sharpen:: 0 or 1
13
+ IMAGE_PATH_REGEXP = /^(.*\/|)(([a-z0-9]{8})-(\d+)x(\d+)(-(\d+)x(\d+)x(\d+)x(\d+)-(\d+)x(\d+)-([01])|).(png|jpg|gif))$/
14
+
15
+ class << self
16
+ # Returns true if the path is for a base image. False otherwise.
17
+ #
18
+ # The path points to a base image if the dimensions are the same as the
19
+ # original dimensions, there is no cropping, and there is no sharpening.
20
+ def base_image?(path)
21
+ get_image_name_parts(path)[:is_base]
22
+ end
23
+
24
+ # Returns the extension name of the path. Normalizes jpeg to jpg.
25
+ def get_image_type(path)
26
+ type = File.extname(path).sub(/^\./, "")
27
+ type = "jpg" if type == "jpeg"
28
+ type
29
+ end
30
+
31
+ # Returns true if the name is valid. False otherwise.
32
+ def valid?(path)
33
+ get_image_name_parts(path)
34
+ return true
35
+ rescue
36
+ return false
37
+ end
38
+
39
+ # Parses the following format:
40
+ # {basename}-{original dimensions}-{crop}-{dimensions}-{sharpen}.{extname}
41
+ # Notes about the format:
42
+ # * crop:: {x}x{y}x{width}x{height}
43
+ # * dimensions:: {width}x{height}
44
+ # * sharpen:: 0 or 1
45
+ #
46
+ # Returns the information in a hash.
47
+ def get_image_name_parts(path)
48
+ matches = path.match(SnapImage::ImageNameUtils::IMAGE_PATH_REGEXP)
49
+ raise SnapImage::InvalidImageIdentifier, "The image identifier is invalid: #{path}" unless matches
50
+ parts = {
51
+ is_base: matches[6].size == 0,
52
+ full: matches[0],
53
+ path: matches[1].sub(/\/$/, ""),
54
+ filename: matches[2],
55
+ basename: matches[3],
56
+ original_dimensions: [matches[4].to_i, matches[5].to_i],
57
+ extname: matches[14]
58
+ }
59
+ unless parts[:is_base]
60
+ parts.merge!({
61
+ crop: {
62
+ x: matches[7].to_i,
63
+ y: matches[8].to_i,
64
+ width: matches[9].to_i,
65
+ height: matches[10].to_i
66
+ },
67
+ dimensions: [matches[11].to_i, matches[12].to_i],
68
+ sharpen: !!matches[13]
69
+ })
70
+ end
71
+ parts
72
+ end
73
+
74
+ # Returns the base image name from the path.
75
+ def get_base_image_path(path)
76
+ parts = get_image_name_parts(path)
77
+ "#{parts[:path]}/#{parts[:basename]}-#{parts[:original_dimensions][0]}x#{parts[:original_dimensions][1]}.#{parts[:extname]}"
78
+ end
79
+
80
+ # Returns the name with the new width and height.
81
+ def get_resized_image_name(name, width, height)
82
+ parts = SnapImage::ImageNameUtils.get_image_name_parts(name)
83
+ if parts[:is_base]
84
+ resized_name = SnapImage::ImageNameUtils.generate_image_name(width, height, parts[:extname], basename: parts[:basename])
85
+ else
86
+ options = {
87
+ basename: parts[:basename],
88
+ crop: parts[:crop],
89
+ width: width,
90
+ height: height,
91
+ sharpen: parts[:sharpend]
92
+ }
93
+ resized_name = SnapImage::ImageNameUtils.generate_image_name(parts[:original_dimensions][0], parts[:original_dimensions][1], parts[:extname], options)
94
+ end
95
+ resized_name
96
+ end
97
+
98
+ # Generates a random alphanumeric string of 8 characters.
99
+ def generate_basename
100
+ (0...8).map { rand(36).to_s(36) }.join
101
+ end
102
+
103
+ # When no options besides :basename are given, generates a base image
104
+ # name in the format:
105
+ # {basename}-{original dimensions}.{extname}
106
+ #
107
+ # Otherwise, generates a modified image name in the format:
108
+ # {basename}-{original dimensions}-{crop}-{dimensions}-{sharpen}.{extname}
109
+ #
110
+ # Notes about the format:
111
+ # * crop:: {x}x{y}x{width}x{height}
112
+ # * dimensions:: {width}x{height}
113
+ # * sharpen:: 0 or 1
114
+ #
115
+ # Options:
116
+ # * basename:: Defaults to a randomly generated basename of 8 characters
117
+ # * crop:: Crop values {x: <int>, y: <int>, width: <int>, height: <int>}
118
+ # * width:: New width
119
+ # * height:: New height
120
+ # * sharpen:: True or false
121
+ def generate_image_name(original_width, original_height, extname, options = {})
122
+ basename = options.delete(:basename) || self.generate_basename
123
+ if options.empty?
124
+ "#{basename}-#{original_width}x#{original_height}.#{extname}"
125
+ else
126
+ "#{basename}-#{original_width}x#{original_height}-#{options[:crop][:x]}x#{options[:crop][:y]}x#{options[:crop][:width]}x#{options[:crop][:height]}-#{options[:width]}x#{options[:height]}-#{options[:sharpen] ? 1 : 0}.#{extname}"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end