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
data/README.md CHANGED
@@ -20,9 +20,9 @@ Or install it yourself as:
20
20
 
21
21
  Generate a config file (default is "config/snapimage\_config.yml"). SnapImage comes with a script to do that.
22
22
 
23
- $ snapimage_generate_config <local root>
23
+ $ snapimage_generate_config <adapter> [options]
24
24
 
25
- The local root argument is a path that tells SnapImage where to store the uploaded files. For other options, use the -h flag.
25
+ For details, use the -h flag.
26
26
 
27
27
  $ snapimage_generate_config -h
28
28
 
@@ -10,10 +10,10 @@ options = {
10
10
  }
11
11
 
12
12
  optparse = OptionParser.new do |opts|
13
- opts.banner = "Usage: snapimage_generate_config local_root [options]"
13
+ opts.banner = "Usage: snapimage_generate_config <adapter> [options]"
14
14
 
15
15
  opts.separator ""
16
- opts.separator "local_root\tPath to the directory where the images will be stored"
16
+ opts.separator "adapter\tStorage adapter to use (local, cloudinary)"
17
17
 
18
18
  opts.separator ""
19
19
  opts.separator "Options:"
@@ -26,6 +26,14 @@ optparse = OptionParser.new do |opts|
26
26
  options[:size] = size.to_i
27
27
  end
28
28
 
29
+ opts.on("-r", "--local_root PATH", "Path to the directory where the images will be stored (only for the local adapter and must be specified)") do |local_root|
30
+ options[:local_root] = local_root
31
+ end
32
+
33
+ opts.on("-u", "--public_url URL", "Public URL to where the images will be accessible (only for the local adapter and must be specified)") do |public_url|
34
+ options[:public_url] = public_url
35
+ end
36
+
29
37
  opts.on("--force", "Overwrites the file if it exists") do
30
38
  options[:force] = true
31
39
  end
@@ -38,11 +46,27 @@ end
38
46
  optparse.parse!
39
47
 
40
48
  unless ARGV.length == 1
49
+ puts "Missing adapter."
50
+ puts
41
51
  puts optparse.help
42
52
  exit
43
53
  end
54
+ adapter = ARGV[0]
44
55
 
45
- local_root = ARGV[0]
56
+ # Check for mandatory options.
57
+ if adapter == "local"
58
+ if !options[:local_root] or !options[:public_url]
59
+ puts "When using the local adapter, local_root and public_url must be specified."
60
+ puts
61
+ puts optparse.help
62
+ exit
63
+ end
64
+ elsif adapter == "cloudinary"
65
+ # Nothing to check for.
66
+ else
67
+ puts optparse.help
68
+ exit
69
+ end
46
70
 
47
71
  if !options[:force] && File.exists?(options[:file])
48
72
  puts "File '#{options[:file]}' already exists. Use --force if you want to overwrite."
@@ -54,10 +78,19 @@ end
54
78
 
55
79
  FileUtils.mkdir_p(File.dirname(options[:file]))
56
80
  File.open(options[:file], "w") do |f|
57
- f.write(<<-EOF
58
- directory: "#{local_root}"
81
+ if adapter == "local"
82
+ f.write(<<-EOF
83
+ adapter: "local"
84
+ directory: "#{options[:local_root]}"
85
+ public_url: "#{options[:public_url]}"
59
86
  max_file_size: #{options[:size]}
60
- EOF
61
- )
87
+ EOF
88
+ )
89
+ elsif adapter == "cloudinary"
90
+ f.write(<<-EOF
91
+ adapter: "cloudinary"
92
+ EOF
93
+ )
94
+ end
62
95
  end
63
96
  puts "Config file generated at '#{options[:file]}'."
@@ -2,24 +2,16 @@ require "rack"
2
2
  require "yaml"
3
3
  require "json"
4
4
  require "fileutils"
5
- require "digest/sha1"
6
- #require "open-uri"
7
- #require "RMagick"
8
5
  require "snapimage/version"
9
6
  require "snapimage/exceptions"
10
7
  require "snapimage/rack/request_file"
11
8
  require "snapimage/rack/request"
12
9
  require "snapimage/rack/response"
13
- #require "snapimage/image/image"
14
- #require "snapimage/image/image_name_utils"
15
- #require "snapimage/storage/storage_server"
16
- #require "snapimage/storage/storage_server.local"
17
- #require "snapimage/storage/storage"
10
+ require "snapimage/adapters/local/config"
11
+ require "snapimage/adapters/local/storage"
12
+ require "snapimage/adapters/cloudinary/config"
13
+ require "snapimage/adapters/cloudinary/storage"
18
14
  require "snapimage/config"
19
- #require "snapimage/server_actions/server_actions.authorize"
20
- #require "snapimage/server_actions/server_actions.generate_image"
21
- #require "snapimage/server_actions/server_actions.sync_resource"
22
- #require "snapimage/server_actions/server_actions.delete_resource_images"
23
- #require "snapimage/server_actions/server_actions.list_resource_images"
15
+ require "snapimage/storage"
24
16
  require "snapimage/server"
25
17
  require "snapimage/middleware"
@@ -0,0 +1,13 @@
1
+ module SnapImage
2
+ module Cloudinary
3
+ class Config
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ def validate_config
9
+ # Nothing to validate.
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ module SnapImage
2
+ module Cloudinary
3
+ class Storage
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ # Stores the file in the given directory and returns the publicly
9
+ # accessible URL.
10
+ # Options:
11
+ # * directory - directory to store the file in
12
+ def store(filename, file, options = {})
13
+ ext = File.extname(filename)
14
+ basename = File.basename(filename, ext)
15
+ public_id = "#{basename}_#{rand(9999)}"
16
+ public_id = File.join(options[:directory], public_id) if options[:directory]
17
+ response = ::Cloudinary::Uploader.upload(file, public_id: public_id)
18
+ ::Cloudinary::Utils.cloudinary_url(public_id + ext)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module SnapImage
2
+ module Local
3
+ class Config
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ def validate_config
9
+ raise SnapImage::InvalidConfig, 'Missing "directory"' unless @config["directory"]
10
+ raise SnapImage::InvalidConfig, 'Missing "public_url"' unless @config["public_url"]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ module SnapImage
2
+ module Local
3
+ class Storage
4
+ def initialize(config)
5
+ @config = config
6
+ @directory = config["directory"]
7
+ @public_url = config["public_url"]
8
+ end
9
+
10
+ # Stores the file in the given directory and returns the publicly
11
+ # accessible URL.
12
+ # Options:
13
+ # * directory - directory to store the file in
14
+ def store(filename, file, options = {})
15
+ file_directory = File.join(@directory, options[:directory])
16
+ file_path = get_file_path(file_directory, filename)
17
+ # Make sure the directory exists.
18
+ FileUtils.mkdir_p(file_directory)
19
+ # Save the file.
20
+ File.open(file_path, "wb") { |f| f.write(file.read) } unless File.exists?(file_path)
21
+ # Return the public URL.
22
+ File.join(@public_url, options[:directory], File.basename(file_path))
23
+ end
24
+
25
+ private
26
+
27
+ # Gets the file path from the given directory and filename.
28
+ # If the file path already exists, appends a (1) to the basename.
29
+ # If there are already multiple files, appends the next (num) in the
30
+ # sequence.
31
+ def get_file_path(directory, filename)
32
+ file_path = File.join(directory, filename)
33
+ if File.exists?(file_path)
34
+ ext = File.extname(filename)
35
+ basename = File.basename(filename, ext)
36
+ files = Dir.glob(File.join(directory, "#{basename}([0-9]*)#{ext}"))
37
+ if files.size == 0
38
+ num = 1
39
+ else
40
+ num = files.map { |f| f.match(/\((\d+)\)/)[1].to_i }.sort.last + 1
41
+ end
42
+ file_path = "#{File.join(directory, basename)}(#{num})#{ext}"
43
+ end
44
+ file_path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -9,7 +9,9 @@ module SnapImage
9
9
  end
10
10
 
11
11
  def validate_config
12
- raise SnapImage::InvalidConfig, 'Missing "directory"' unless @config["directory"]
12
+ raise SnapImage::InvalidConfig, 'Missing "adapter"' unless @config["adapter"]
13
+ raise SnapImage::InvalidConfig, "Unknown adapter type. Expecting local or cloudinary: #{@config["adapter"]}" unless @config["adapter"]
14
+ SnapImage.const_get(@config["adapter"].capitalize).const_get("Config").new(@config).validate_config
13
15
  end
14
16
 
15
17
  def set_config_defaults
@@ -4,19 +4,13 @@ module SnapImage
4
4
  class UnknownConfigType < StandardError; end
5
5
  class MissingConfig < StandardError; end
6
6
 
7
- # Authorization.
8
- #class AuthorizationRequired < StandardError; end
9
- #class AuthorizationFailed < StandardError; end
10
-
11
7
  # Request.
12
8
  class BadRequest < StandardError; end
13
- #class ActionNotImplemented < StandardError; end
14
9
  class InvalidFilename < StandardError; end
15
10
  class InvalidDirectory < StandardError; end
16
11
  class FileTooLarge < StandardError; end
17
12
 
18
13
  # Files.
19
14
  class UnknownFileType < StandardError; end
20
- #class FileDoesNotExist < StandardError; end
21
15
 
22
16
  end
@@ -12,12 +12,13 @@ module SnapImage
12
12
  @app = app
13
13
  @path = options[:path] || "/snapimage_api"
14
14
  @config = SnapImage::Config.new(options[:config] || "config/snapimage_config.yml")
15
+ @storage = SnapImage::Storage.new(@config)
15
16
  end
16
17
 
17
18
  def call(env)
18
19
  request = SnapImage::Request.new(env)
19
20
  if request.path_info == @path
20
- SnapImage::Server.new(request, @config).call
21
+ SnapImage::Server.new(request, @config, @storage).call
21
22
  else
22
23
  @app.call(env)
23
24
  end
@@ -1,7 +1,7 @@
1
1
  module SnapImage
2
2
  class Request < Rack::Request
3
3
  def bad_request?
4
- !(self.post? && self.POST["file"] && self.POST["directory"])
4
+ !(self.post? && self.POST["file"])
5
5
  end
6
6
 
7
7
  # Returns a SnapImage::RequestFile which encapsulates the file that Rack
@@ -6,7 +6,7 @@ module SnapImage
6
6
  @content_type = options[:content_type] || "text/json"
7
7
  @template = options[:template] || "{{json}}"
8
8
  @json = {}
9
- super
9
+ super()
10
10
  end
11
11
 
12
12
  def set_success(info = {})
@@ -18,14 +18,6 @@ module SnapImage
18
18
  @json = { status_code: 400, message: "Bad Request" }
19
19
  end
20
20
 
21
- #def set_authorization_required
22
- #@json = { status_code: 401, message: "Authorization Required" }
23
- #end
24
-
25
- #def set_authorization_failed
26
- #@json = { status_code: 402, message: "Authorization Failed" }
27
- #end
28
-
29
21
  def set_invalid_filename
30
22
  @json = { status_code: 403, message: "Invalid Filename" }
31
23
  end
@@ -43,7 +35,7 @@ module SnapImage
43
35
  end
44
36
 
45
37
  def finish
46
- self.body = [@template.gsub(/{{json}}/, @json.to_json)]
38
+ write(@template.gsub(/{{json}}/, @json.to_json))
47
39
  self["Content-Type"] = @content_type
48
40
  super
49
41
  end
@@ -1,44 +1,39 @@
1
1
  module SnapImage
2
2
  class Server
3
3
  DIRECTORY_REGEXP = /^[a-z0-9_-]+(\/[a-z0-9_-]+)*$/
4
- FILENAME_REGEXP = /^[a-z0-9_-]+[.](gif|jpg|jpeg|png)$/
4
+ FILENAME_REGEXP = /^[^\/]+[.](gif|jpg|jpeg|png)$/
5
5
 
6
6
  # Arguments:
7
7
  # * request:: Rack::Request
8
- def initialize(request, config)
8
+ def initialize(request, config, storage)
9
9
  @request = request
10
10
  @config = config
11
+ @storage = storage
11
12
  end
12
13
 
13
14
  # Handles the request and returns a Rack::Response.
14
15
  def call
15
- @response = SnapImage::Response.new
16
+ # If the request is not an XHR, the response type should be text/html or
17
+ # text/plain. This affects browsers like IE8/9 when performing uploads
18
+ # through an iframe transport. If we return using text/json, the browser
19
+ # attempts to download the file instead.
20
+ @response = SnapImage::Response.new(content_type: @request.xhr? ? "text/json" : "text/html")
16
21
  begin
17
22
  raise SnapImage::BadRequest if @request.bad_request?
18
23
  raise SnapImage::InvalidFilename unless @request.file.filename =~ SnapImage::Server::FILENAME_REGEXP
19
- raise SnapImage::InvalidDirectory unless @request["directory"] =~ SnapImage::Server::DIRECTORY_REGEXP
24
+ directory = @request["directory"] || "uncategorized"
25
+ raise SnapImage::InvalidDirectory unless directory =~ SnapImage::Server::DIRECTORY_REGEXP
20
26
  raise SnapImage::FileTooLarge if @request.file.tempfile.size > @config["max_file_size"]
21
- directory = File.join(@config["directory"], @request["directory"])
22
- file_path = File.join(directory, @request.file.filename)
23
- # Make sure the directory exists.
24
- FileUtils.mkdir_p(directory)
25
- # Save the file.
26
- File.open(file_path, "wb") { |f| f.write(@request.file.tempfile.read) } unless File.exists?(file_path)
27
- @response.set_success
27
+ url = @storage.store(@request.file.filename, @request.file.tempfile, directory: directory)
28
+ @response.set_success(url: url)
28
29
  rescue SnapImage::BadRequest
29
30
  @response.set_bad_request
30
- #rescue SnapImage::AuthorizationRequired
31
- #@response.set_authorization_required
32
- #rescue SnapImage::AuthorizationFailed
33
- #@response.set_authorization_failed
34
31
  rescue SnapImage::InvalidFilename
35
32
  @response.set_invalid_filename
36
33
  rescue SnapImage::InvalidDirectory
37
34
  @response.set_invalid_directory
38
35
  rescue SnapImage::FileTooLarge
39
36
  @response.set_file_too_large
40
- rescue
41
- @response.set_internal_server_error
42
37
  end
43
38
  @response.finish
44
39
  end
@@ -0,0 +1,10 @@
1
+ module SnapImage
2
+ class Storage
3
+ extend Forwardable
4
+ def_delegators :@storage, :store
5
+
6
+ def initialize(config)
7
+ @storage = SnapImage.const_get(config["adapter"].capitalize).const_get("Storage").new(config)
8
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module SnapImage
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -16,7 +16,6 @@ Gem::Specification.new do |gem|
16
16
  gem.version = SnapImage::VERSION
17
17
 
18
18
  gem.add_dependency("rack")
19
- gem.add_dependency("rmagick")
20
19
  gem.add_dependency("sinatra")
21
20
  gem.add_dependency("thin")
22
21
  gem.add_dependency("rake")
@@ -23,7 +23,12 @@ describe "Upload" do
23
23
  SnapImage::Middleware.new(
24
24
  app,
25
25
  path: "/snapimage_api",
26
- config: { "directory" => File.join(RSpec.root, "storage"), "max_file_size" => 600 }
26
+ config: {
27
+ "adapter" => "local",
28
+ "directory" => File.join(RSpec.root, "storage"),
29
+ "public_url" => "http://snapimage.com/public",
30
+ "max_file_size" => 600
31
+ }
27
32
  )
28
33
  end
29
34
 
@@ -34,10 +39,11 @@ describe "Upload" do
34
39
 
35
40
  it "is successful" do
36
41
  last_response.should be_successful
37
- last_response["Content-Type"].should eq "text/json"
42
+ last_response["Content-Type"].should eq "text/html"
38
43
  json = JSON.parse(last_response.body)
39
44
  json["status_code"].should eq 200
40
45
  json["message"].should eq "Success"
46
+ json["url"].should eq "http://snapimage.com/public/abc/123/stub-300x200.png"
41
47
  end
42
48
 
43
49
  it "stores the image" do
@@ -46,6 +52,42 @@ describe "Upload" do
46
52
  end
47
53
  end
48
54
 
55
+ context "upload duplicate files" do
56
+ before do
57
+ @times = 12
58
+ end
59
+
60
+ it "is successful" do
61
+ @times.times do |i|
62
+ post "/snapimage_api", "file" => Rack::Test::UploadedFile.new(@image_path, "image/png"), "directory" => @directory
63
+ last_response.should be_successful
64
+ last_response["Content-Type"].should eq "text/html"
65
+ json = JSON.parse(last_response.body)
66
+ json["status_code"].should eq 200
67
+ json["message"].should eq "Success"
68
+ if i == 0
69
+ json["url"].should eq "http://snapimage.com/public/abc/123/stub-300x200.png"
70
+ else
71
+ json["url"].should eq "http://snapimage.com/public/abc/123/stub-300x200(#{i}).png"
72
+ end
73
+ end
74
+ end
75
+
76
+ it "stores the images" do
77
+ ext = File.extname(@image_path)
78
+ basename = File.basename(@image_path, ext)
79
+ @times.times do |i|
80
+ post "/snapimage_api", "file" => Rack::Test::UploadedFile.new(@image_path, "image/png"), "directory" => @directory
81
+ if i == 0
82
+ path = File.join(@local_root, @directory, "#{basename}#{ext}")
83
+ else
84
+ path = File.join(@local_root, @directory, "#{basename}(#{i})#{ext}")
85
+ end
86
+ File.exist?(path).should be_true
87
+ end
88
+ end
89
+ end
90
+
49
91
  context "upload too large" do
50
92
  before do
51
93
  post "/snapimage_api", "file" => Rack::Test::UploadedFile.new(@large_image_path, "image/png"), "directory" => @directory
@@ -53,7 +95,7 @@ describe "Upload" do
53
95
 
54
96
  it "fails" do
55
97
  last_response.should be_successful
56
- last_response["Content-Type"].should eq "text/json"
98
+ last_response["Content-Type"].should eq "text/html"
57
99
  json = JSON.parse(last_response.body)
58
100
  json["status_code"].should eq 405
59
101
  json["message"].should eq "File Too Large"