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.
- data/README.md +2 -2
- data/bin/snapimage_generate_config +40 -7
- data/lib/snapimage.rb +5 -13
- data/lib/snapimage/adapters/cloudinary/config.rb +13 -0
- data/lib/snapimage/adapters/cloudinary/storage.rb +22 -0
- data/lib/snapimage/adapters/local/config.rb +14 -0
- data/lib/snapimage/adapters/local/storage.rb +48 -0
- data/lib/snapimage/config.rb +3 -1
- data/lib/snapimage/exceptions.rb +0 -6
- data/lib/snapimage/middleware.rb +2 -1
- data/lib/snapimage/rack/request.rb +1 -1
- data/lib/snapimage/rack/response.rb +2 -10
- data/lib/snapimage/server.rb +12 -17
- data/lib/snapimage/storage.rb +10 -0
- data/lib/snapimage/version.rb +1 -1
- data/snapimage.gemspec +0 -1
- data/spec/acceptance/upload_spec.rb +45 -3
- data/spec/snapimage/adapters/local/storage_spec.rb +44 -0
- data/spec/snapimage/middleware_spec.rb +2 -0
- data/spec/snapimage/rack/request_spec.rb +3 -9
- data/spec/snapimage/server_spec.rb +27 -11
- data/spec/support/assets/config.json +2 -0
- data/spec/support/assets/config.yml +2 -0
- metadata +9 -34
- data/lib/snapimage/image/image.rb +0 -103
- data/lib/snapimage/image/image_name_utils.rb +0 -131
- data/lib/snapimage/server_actions/server_actions.authorize.rb +0 -69
- data/lib/snapimage/server_actions/server_actions.delete_resource_images.rb +0 -23
- data/lib/snapimage/server_actions/server_actions.generate_image.rb +0 -169
- data/lib/snapimage/server_actions/server_actions.list_resource_images.rb +0 -23
- data/lib/snapimage/server_actions/server_actions.sync_resource.rb +0 -78
- data/lib/snapimage/storage/storage.rb +0 -120
- data/lib/snapimage/storage/storage_server.local.rb +0 -120
- 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 <
|
23
|
+
$ snapimage_generate_config <adapter> [options]
|
24
24
|
|
25
|
-
|
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
|
13
|
+
opts.banner = "Usage: snapimage_generate_config <adapter> [options]"
|
14
14
|
|
15
15
|
opts.separator ""
|
16
|
-
opts.separator "
|
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
|
-
|
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
|
-
|
58
|
-
|
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]}'."
|
data/lib/snapimage.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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,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
|
data/lib/snapimage/config.rb
CHANGED
@@ -9,7 +9,9 @@ module SnapImage
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def validate_config
|
12
|
-
raise SnapImage::InvalidConfig, 'Missing "
|
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
|
data/lib/snapimage/exceptions.rb
CHANGED
@@ -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
|
data/lib/snapimage/middleware.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
38
|
+
write(@template.gsub(/{{json}}/, @json.to_json))
|
47
39
|
self["Content-Type"] = @content_type
|
48
40
|
super
|
49
41
|
end
|
data/lib/snapimage/server.rb
CHANGED
@@ -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 = /^[
|
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
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
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
|
data/lib/snapimage/version.rb
CHANGED
data/snapimage.gemspec
CHANGED
@@ -23,7 +23,12 @@ describe "Upload" do
|
|
23
23
|
SnapImage::Middleware.new(
|
24
24
|
app,
|
25
25
|
path: "/snapimage_api",
|
26
|
-
config: {
|
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/
|
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/
|
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"
|