snapimage 0.1.1 → 0.2.0

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