s3direct 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +4 -0
- data/lib/s3direct.rb +42 -0
- data/lib/s3direct/file.rb +72 -0
- data/lib/s3direct/string_interpolator.rb +31 -0
- data/lib/s3direct/upload_request.rb +82 -0
- data/lib/s3direct/uploadable.rb +14 -0
- data/lib/s3direct/version.rb +3 -0
- data/s3direct.gemspec +30 -0
- data/spec/config_spec.rb +16 -0
- data/spec/file_spec.rb +118 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/string_interpolator_spec.rb +20 -0
- data/spec/upload_request_spec.rb +69 -0
- data/spec/uploadable_spec.rb +35 -0
- data/vendor/assets/javascripts/s3direct.js.coffee +99 -0
- data/vendor/assets/javascripts/s3direct_default_template.jst.ejs +14 -0
- metadata +206 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Brent Dillingham
|
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
|
+
# S3direct
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 's3direct'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install s3direct
|
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 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/s3direct.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
|
3
|
+
require "s3direct/version"
|
4
|
+
require "s3direct/file"
|
5
|
+
require "s3direct/string_interpolator"
|
6
|
+
require "s3direct/upload_request"
|
7
|
+
require "s3direct/uploadable"
|
8
|
+
|
9
|
+
|
10
|
+
module S3Direct
|
11
|
+
def self.configure
|
12
|
+
yield config
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.config
|
16
|
+
@@config ||= Config.new
|
17
|
+
end
|
18
|
+
|
19
|
+
class Config
|
20
|
+
attr_accessor :bucket
|
21
|
+
attr_accessor :bucket_url
|
22
|
+
attr_accessor :access_key
|
23
|
+
attr_accessor :secret_key
|
24
|
+
|
25
|
+
attr_writer :max_upload_size
|
26
|
+
def max_upload_size
|
27
|
+
@max_upload_size ||= 1.gigabyte
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_writer :default_acl
|
31
|
+
def default_acl
|
32
|
+
@default_acl ||= 'public-read'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
class Engine < Rails::Engine
|
38
|
+
end
|
39
|
+
rescue NameError => e
|
40
|
+
puts "Rails is not loaded => #{e.message}"
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module S3Direct
|
2
|
+
class File
|
3
|
+
|
4
|
+
attr_reader :model, :identifier, :pattern
|
5
|
+
|
6
|
+
def self.sanitize_filename(name)
|
7
|
+
unless name.nil?
|
8
|
+
name.strip
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(model, identifier, pattern, opts={})
|
13
|
+
setup_options opts
|
14
|
+
|
15
|
+
@model = model
|
16
|
+
@identifier = identifier
|
17
|
+
@pattern = pattern
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
@model.send "#{identifier}_file"
|
22
|
+
end
|
23
|
+
|
24
|
+
def s3_path
|
25
|
+
StringInterpolator.new(model, pattern).to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def url
|
29
|
+
if exists?
|
30
|
+
::File.join(config.bucket_url, key)
|
31
|
+
else
|
32
|
+
default_url
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def upload_request(filename = name, options = {})
|
37
|
+
if filename.blank?
|
38
|
+
raise "Can't create an upload request without a filename - " +
|
39
|
+
"provide it as an argument or set #{identifier}_file on the model"
|
40
|
+
end
|
41
|
+
UploadRequest.new s3_path, self.class.sanitize_filename(filename), options
|
42
|
+
end
|
43
|
+
|
44
|
+
def key
|
45
|
+
::File.join(s3_path, name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def exists?
|
49
|
+
name.present?
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def config
|
55
|
+
::S3Direct.config
|
56
|
+
end
|
57
|
+
|
58
|
+
def options
|
59
|
+
# set up any defaults here
|
60
|
+
@options ||= {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def setup_options(opts)
|
64
|
+
options.merge! opts
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_url
|
68
|
+
options[:default_url]
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module S3Direct
|
2
|
+
class StringInterpolator
|
3
|
+
DELIM = "/"
|
4
|
+
|
5
|
+
attr_reader :context, :pattern
|
6
|
+
|
7
|
+
def initialize(context, pattern)
|
8
|
+
@pattern = pattern
|
9
|
+
@context = context
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
compile_parts.join(DELIM)
|
14
|
+
end
|
15
|
+
|
16
|
+
def compile_parts
|
17
|
+
pattern.split(DELIM).collect do |part|
|
18
|
+
if part[0] == ':'
|
19
|
+
meth = part[1, part.length - 1]
|
20
|
+
result = context.public_send meth
|
21
|
+
if result.blank?
|
22
|
+
raise ":#{meth} for path '#{pattern}' was blank in #{context.inspect}"
|
23
|
+
end
|
24
|
+
result.to_s.underscore
|
25
|
+
else
|
26
|
+
part
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module S3Direct
|
2
|
+
class UploadRequest
|
3
|
+
attr_reader :path, :filename, :options
|
4
|
+
|
5
|
+
def initialize(path, sanitized_filename, options = {})
|
6
|
+
@path = path
|
7
|
+
@filename = sanitized_filename
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def key
|
12
|
+
::File.join(@path, @filename)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_json
|
16
|
+
data = {
|
17
|
+
url: config.bucket_url,
|
18
|
+
filename: @filename,
|
19
|
+
key: key,
|
20
|
+
policy: s3_upload_policy_document,
|
21
|
+
signature: s3_upload_signature,
|
22
|
+
acl: s3_acl,
|
23
|
+
success_action_status: "200",
|
24
|
+
'AWSAccessKeyId' => config.access_key
|
25
|
+
}
|
26
|
+
|
27
|
+
if attachment_filename
|
28
|
+
data["Content-Disposition"] = %Q{attachment; filename="#{attachment_filename}"}
|
29
|
+
end
|
30
|
+
|
31
|
+
data.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
def attachment_filename
|
35
|
+
options[:attachment_filename].presence
|
36
|
+
end
|
37
|
+
|
38
|
+
def s3_acl
|
39
|
+
options.fetch(:acl, config.default_acl)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# generate the policy document that amazon is expecting.
|
45
|
+
def s3_upload_policy_document
|
46
|
+
policy = {
|
47
|
+
'expiration' => 5.minutes.from_now.utc.xmlschema,
|
48
|
+
'conditions' => [
|
49
|
+
{'bucket' => config.bucket},
|
50
|
+
{'acl' => s3_acl},
|
51
|
+
{'success_action_status' => '200'},
|
52
|
+
{'key' => key},
|
53
|
+
['content-length-range', 0, config.max_upload_size]
|
54
|
+
]
|
55
|
+
}
|
56
|
+
|
57
|
+
if attachment_filename
|
58
|
+
policy['conditions'] << {"Content-Disposition" => %Q{attachment; filename="#{attachment_filename}"}}
|
59
|
+
end
|
60
|
+
|
61
|
+
encode(policy.to_json)
|
62
|
+
end
|
63
|
+
|
64
|
+
# sign our request by Base64 encoding the policy document.
|
65
|
+
def s3_upload_signature
|
66
|
+
signature = OpenSSL::HMAC.digest(
|
67
|
+
OpenSSL::Digest::Digest.new('sha1'),
|
68
|
+
config.secret_key,
|
69
|
+
s3_upload_policy_document
|
70
|
+
)
|
71
|
+
encode(signature)
|
72
|
+
end
|
73
|
+
|
74
|
+
def encode(str)
|
75
|
+
Base64.encode64(str).gsub("\n",'')
|
76
|
+
end
|
77
|
+
|
78
|
+
def config
|
79
|
+
::S3Direct.config
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module S3Direct
|
2
|
+
module Uploadable
|
3
|
+
|
4
|
+
def has_s3_file(attr_name, pattern, options={})
|
5
|
+
define_method attr_name do
|
6
|
+
::S3Direct::File.new(self, attr_name, pattern, options)
|
7
|
+
end
|
8
|
+
define_method "#{attr_name}_file=" do |filename|
|
9
|
+
self["#{attr_name}_file"] = ::S3Direct::File.sanitize_filename(filename)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
data/s3direct.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 's3direct/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "s3direct"
|
8
|
+
spec.version = S3Direct::VERSION
|
9
|
+
spec.authors = ["Brent Dillingham"]
|
10
|
+
spec.email = ["brentdillingham@gmail.com"]
|
11
|
+
spec.summary = %q{Upload directly to S3}
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'activesupport', '>= 3.2.0'
|
22
|
+
spec.add_dependency 'jquery-fileupload-rails', '~> 0.4.1'
|
23
|
+
spec.add_dependency 'coffee-rails'
|
24
|
+
spec.add_dependency 'ejs'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
27
|
+
spec.add_development_dependency "rails", ">= 3.2.1"
|
28
|
+
spec.add_development_dependency "rake"
|
29
|
+
spec.add_development_dependency "rspec"
|
30
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe S3Direct::Config do
|
4
|
+
it "has a default_acl of public-read when not set" do
|
5
|
+
expect(S3Direct.config.default_acl).to eq('public-read')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "allows setting a default_acl" do
|
9
|
+
begin
|
10
|
+
S3Direct.configure {|c| c.default_acl = 'authenticated-read' }
|
11
|
+
expect(S3Direct.config.default_acl).to eq('authenticated-read')
|
12
|
+
ensure
|
13
|
+
S3Direct.configure {|c| c.default_acl = nil }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/file_spec.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe S3Direct::File do
|
4
|
+
|
5
|
+
it "mounts to an activerecord model on initialize" do
|
6
|
+
model = double(:model)
|
7
|
+
file = S3Direct::File.new(model, :my_file, "foo/bar/bat")
|
8
|
+
|
9
|
+
expect(file.model).to eql model
|
10
|
+
expect(file.identifier).to eql :my_file
|
11
|
+
expect(file.pattern).to eql "foo/bar/bat"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "interpolates an s3 path based on the pattern given" do
|
15
|
+
model = double(:model, lesson_id: 1, id: 2)
|
16
|
+
file = S3Direct::File.new(model, :my_file, "lessons/:lesson_id/videos/:id")
|
17
|
+
|
18
|
+
expect(file.s3_path).to eql "lessons/1/videos/2"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "allows a default url to used when a file is not present" do
|
22
|
+
model = double(:model, lesson_id: 1, id: 2, media_file: nil)
|
23
|
+
file = S3Direct::File.new(model, :media, "lessons/:lesson_id/videos/:id", default_url: "http://example.com/default.png")
|
24
|
+
expect(file.url).to eql "http://example.com/default.png"
|
25
|
+
model.stub(media_file: "foo.png")
|
26
|
+
expect(file.url).to_not eql "http://example.com/default.png"
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
describe S3Direct::File, "#exists?" do
|
32
|
+
|
33
|
+
it "is true if the models '{name}_file' is not nil" do
|
34
|
+
model = double :model, media_file: "foobar.png"
|
35
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
36
|
+
expect(file.exists?).to be true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "is false if the models '{name}_file' is nil" do
|
40
|
+
model = double :model, media_file: nil
|
41
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
42
|
+
expect(file.exists?).to be false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe S3Direct::File, "#upload_request" do
|
47
|
+
|
48
|
+
it "returns a s3 direct upload object" do
|
49
|
+
model = double(:model, media_file: 'foo')
|
50
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
51
|
+
|
52
|
+
expect(file.upload_request).to be_an_instance_of(S3Direct::UploadRequest)
|
53
|
+
expect(file.upload_request.key).to eql(file.key)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "accepts the filename as a parameter and sanitizes it" do
|
57
|
+
model = double(:model)
|
58
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
59
|
+
expect(file.upload_request(' foo').filename).to eql 'foo'
|
60
|
+
end
|
61
|
+
|
62
|
+
it "optionally receives options to pass to the UploadRequest" do
|
63
|
+
model = double(:model)
|
64
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
65
|
+
S3Direct::UploadRequest.should_receive(:new).with(anything, "test.txt", {foo: 'bar'})
|
66
|
+
file.upload_request('test.txt', {foo: 'bar'})
|
67
|
+
end
|
68
|
+
|
69
|
+
it "raises an error if the filename is not set or provided" do
|
70
|
+
model = double(:model)
|
71
|
+
file = S3Direct::File.new(model, :media, "foo/bar/bat")
|
72
|
+
expect { file.upload_request }.to raise_error
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
describe S3Direct::File, "#name" do
|
78
|
+
it "returns the file name based on the pattern: '<name>_file'" do
|
79
|
+
model = double(:model)
|
80
|
+
model.stub(:avatar_file) { "my_avatar.png" }
|
81
|
+
file = S3Direct::File.new(model, :avatar, "")
|
82
|
+
|
83
|
+
expect(file.name).to eql "my_avatar.png"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe S3Direct::File, "#url" do
|
88
|
+
context "the file exists" do
|
89
|
+
it "returns the full url of the file" do
|
90
|
+
S3Direct.config.stub(bucket_url: 'http://s3.com/mabucket/')
|
91
|
+
model = double(:model, id: 77, avatar_file: 'my_avatar.png')
|
92
|
+
file = S3Direct::File.new(model, :avatar, "my_model/:id/avatar")
|
93
|
+
|
94
|
+
expect(file.url).to eql "http://s3.com/mabucket/my_model/77/avatar/my_avatar.png"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "the file does not exist" do
|
99
|
+
it "returns nil" do
|
100
|
+
model = double(:model)
|
101
|
+
model.stub(:avatar_file) { nil }
|
102
|
+
file = S3Direct::File.new(model, :avatar, "foo/bar/bat")
|
103
|
+
|
104
|
+
expect(file.url).to be_nil
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe S3Direct::File, "#key" do
|
110
|
+
it "returns the full s3 key for the file" do
|
111
|
+
model = double(:model)
|
112
|
+
model.stub(:id) { 77 }
|
113
|
+
model.stub(:avatar_file) { "my_avatar.png" }
|
114
|
+
file = S3Direct::File.new(model, :avatar, "my_model/:id/avatar/")
|
115
|
+
|
116
|
+
expect(file.key).to eql "my_model/77/avatar/my_avatar.png"
|
117
|
+
end
|
118
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
describe S3Direct::StringInterpolator do
|
5
|
+
|
6
|
+
it "converts :pattern tags using local context methods" do
|
7
|
+
foo = OpenStruct.new(bar: "bar", fizz: "fizzy", buzz: "buzz_buzz")
|
8
|
+
interpolation = S3Direct::StringInterpolator.new(foo, "foo/bar/:bar/fizz/:fizz/buzz/:buzz/:class")
|
9
|
+
expect(interpolation.to_s).to eq "foo/bar/bar/fizz/fizzy/buzz/buzz_buzz/open_struct"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "will not re-interpolate :pattern returned from method calls" do
|
13
|
+
o = OpenStruct.new
|
14
|
+
o.security = ":test"
|
15
|
+
o.test = "boom"
|
16
|
+
interpolation = S3Direct::StringInterpolator.new(o, "/security/:security")
|
17
|
+
expect(interpolation.to_s).to eq '/security/:test'
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe S3Direct::UploadRequest, '#attachment_filename' do
|
4
|
+
it 'is nil if not provided an attachment_filename option' do
|
5
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'buzz.txt')
|
6
|
+
expect(upload_request.attachment_filename).to be_nil
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'is the options[:attachment_filename] if provided' do
|
10
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'random.txt', {
|
11
|
+
attachment_filename: 'expected.txt'
|
12
|
+
})
|
13
|
+
expect(upload_request.attachment_filename).to eq('expected.txt')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe S3Direct::UploadRequest, '#to_json' do
|
18
|
+
before do
|
19
|
+
S3Direct.config.stub(bucket_url: 'http://s3.com/mabucket/')
|
20
|
+
S3Direct.config.stub(secret_key: 'sekret')
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when no attachment_filename option is given" do
|
24
|
+
before do
|
25
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'buzz.txt')
|
26
|
+
@data = JSON[upload_request.to_json]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "does not add a Content-Disposition attachment field" do
|
30
|
+
expect(@data.has_key?("Content-Disposition")).to be_false
|
31
|
+
end
|
32
|
+
|
33
|
+
it "includes no content-disposition in the policy" do
|
34
|
+
policy = JSON[Base64.decode64 @data['policy']]
|
35
|
+
expect(policy['conditions'].include?('Content-Disposition')).to be_false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when an attachment_filename option is given" do
|
40
|
+
before do
|
41
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'buzz.txt', {
|
42
|
+
attachment_filename: 'expected.txt'
|
43
|
+
})
|
44
|
+
@data = JSON[upload_request.to_json]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "adds a Content-Disposition attachment field using the option" do
|
48
|
+
expect(@data["Content-Disposition"]).to eq('attachment; filename="expected.txt"')
|
49
|
+
end
|
50
|
+
|
51
|
+
it "includes the content-disposition in the policy" do
|
52
|
+
policy = JSON[Base64.decode64 @data['policy']]
|
53
|
+
condition = policy['conditions'].detect {|c| c.is_a?(Hash) && c.keys.include?('Content-Disposition') }
|
54
|
+
expect(condition['Content-Disposition']).to eq('attachment; filename="expected.txt"')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe S3Direct::UploadRequest, '#s3_acl' do
|
60
|
+
it "uses the config default_acl by default" do
|
61
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'buzz.txt')
|
62
|
+
expect(upload_request.s3_acl).to eq('public-read')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "uses the acl option if available" do
|
66
|
+
upload_request = S3Direct::UploadRequest.new('/foo/bar', 'buzz.txt', acl: 'authenticated-read')
|
67
|
+
expect(upload_request.s3_acl).to eq('authenticated-read')
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module TestS3Uploadable
|
4
|
+
class Base
|
5
|
+
end
|
6
|
+
|
7
|
+
Base.extend S3Direct::Uploadable
|
8
|
+
|
9
|
+
class Foo < Base
|
10
|
+
has_s3_file :avatar, "foo/:id/avatar", default_url: "foobar.png"
|
11
|
+
|
12
|
+
def avatar_file
|
13
|
+
"avatar.png"
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
42
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
describe S3Direct::Uploadable, "#has_file" do
|
24
|
+
it "defines a method on the object to access the file" do
|
25
|
+
foo = TestS3Uploadable::Foo.new
|
26
|
+
expect(foo.avatar).to be_kind_of(S3Direct::File)
|
27
|
+
expect(foo.avatar.s3_path).to eql "foo/42/avatar"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "allows for default urls" do
|
31
|
+
foo = TestS3Uploadable::Foo.new
|
32
|
+
foo.stub(:avatar_file) { nil }
|
33
|
+
expect(foo.avatar.url).to eql "foobar.png"
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#= require jquery-fileupload/basic
|
2
|
+
#= require s3direct_default_template
|
3
|
+
#= require_self
|
4
|
+
|
5
|
+
class window.S3DirectUploadView
|
6
|
+
template: JST['s3_direct_default_template']
|
7
|
+
|
8
|
+
$: (selector) ->
|
9
|
+
@$el.find(selector)
|
10
|
+
|
11
|
+
uploader: ->
|
12
|
+
@$('.uploader')
|
13
|
+
|
14
|
+
fileInput: ->
|
15
|
+
@$('input[type=file]')
|
16
|
+
|
17
|
+
progressMeter: ->
|
18
|
+
@$('.progress .meter')
|
19
|
+
|
20
|
+
progressBar: ->
|
21
|
+
@$('.progress')
|
22
|
+
|
23
|
+
uploadInfo: ->
|
24
|
+
@$('.upload-info')
|
25
|
+
|
26
|
+
alertBox: ->
|
27
|
+
@$('.alert-box')
|
28
|
+
|
29
|
+
fileInfo: ->
|
30
|
+
@$('.file-info')
|
31
|
+
|
32
|
+
constructor: (options = {}) ->
|
33
|
+
@model = options.model
|
34
|
+
@fileAttribute = options.fileAttribute
|
35
|
+
@requestUrl = options.requestUrl
|
36
|
+
@$el = if options.el then $(options.el) else $('<div>')
|
37
|
+
@el = @$el[0]
|
38
|
+
|
39
|
+
render: ->
|
40
|
+
@$el.html @template(this)
|
41
|
+
@setupFileInput()
|
42
|
+
@setFileInfo @fileName()
|
43
|
+
|
44
|
+
fileName: ->
|
45
|
+
@model.get(@fileAttribute)
|
46
|
+
|
47
|
+
setupFileInput: ->
|
48
|
+
filename = null
|
49
|
+
|
50
|
+
@fileInput()
|
51
|
+
.fileupload
|
52
|
+
paramName : 'file'
|
53
|
+
autoUpload : true
|
54
|
+
dropZone : @$el
|
55
|
+
# When a file is added, make an "upload request" to our server to get the S3 bucket URL,
|
56
|
+
# policy document, signature, etc.
|
57
|
+
add: (e, data) =>
|
58
|
+
$.ajax
|
59
|
+
url: @requestUrl
|
60
|
+
data: {filename: data.files[0].name}
|
61
|
+
success: (uploadRequestObj) =>
|
62
|
+
# The sanitized version of the filename
|
63
|
+
filename = uploadRequestObj.filename
|
64
|
+
@fileInput().fileupload 'option', 'url', uploadRequestObj.url
|
65
|
+
@fileInput().fileupload 'option', 'formData', _.omit(uploadRequestObj, 'url', 'filename')
|
66
|
+
data.submit()
|
67
|
+
# Once the upload to S3 is under way, fade in the progress bar and disable the button.
|
68
|
+
send: =>
|
69
|
+
@uploader().hide()
|
70
|
+
@alertBox().remove()
|
71
|
+
@progressBar().fadeIn()
|
72
|
+
# Show an indeterminate progress bar if browser doesn't support progress (I *think* this is the right attribute to check)
|
73
|
+
@progressMeter().css(width: '100%') unless $.support.xhrFileUpload
|
74
|
+
# Update the progress bar.
|
75
|
+
progress: (e) =>
|
76
|
+
percent = Math.round(e.loaded / e.total * 100)
|
77
|
+
@progressMeter().css(width: percent + '%')
|
78
|
+
# S3 upload was successful; save the LessonMedia object on the server.
|
79
|
+
done: (e, response) =>
|
80
|
+
msg = "<b>Upload succeeded.</b>"
|
81
|
+
@showUploadMessage(msg, true)
|
82
|
+
@model.set @fileAttribute, filename
|
83
|
+
@model.save()
|
84
|
+
# S3 upload failed.
|
85
|
+
error: =>
|
86
|
+
msg = "<b>Something went wrong!</b> Your upload did not complete. Please try again."
|
87
|
+
@showUploadMessage(msg, false)
|
88
|
+
@uploader().show()
|
89
|
+
|
90
|
+
# Displays a message about the success or failure of the upload.
|
91
|
+
showUploadMessage: (msg, wasSuccess) ->
|
92
|
+
@progressBar().fadeOut 300, =>
|
93
|
+
@progressMeter().css(width: 0)
|
94
|
+
@alertBox().remove()
|
95
|
+
alertClass = if wasSuccess then 'success' else 'alert'
|
96
|
+
@uploadInfo().append("<div class='alert-box #{alertClass}'>#{msg}</div>")
|
97
|
+
|
98
|
+
setFileInfo: (name) ->
|
99
|
+
@fileInfo().text name if name
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="s3-direct">
|
2
|
+
<div class="uploader">
|
3
|
+
<span class="button secondary small fileinput-button">
|
4
|
+
<span>Choose File</span>
|
5
|
+
<input type="file" />
|
6
|
+
</span>
|
7
|
+
<span class="file-info">No file chosen</span>
|
8
|
+
</div>
|
9
|
+
<div class="upload-info">
|
10
|
+
<div class="progress" style="display:none">
|
11
|
+
<span class="meter" style="width:0%"></span>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
</div>
|
metadata
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: s3direct
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brent Dillingham
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-03-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: jquery-fileupload-rails
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.4.1
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.4.1
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: coffee-rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: ejs
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: bundler
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '1.3'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '1.3'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rails
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.2.1
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 3.2.1
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rake
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rspec
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
description: Upload directly to S3
|
143
|
+
email:
|
144
|
+
- brentdillingham@gmail.com
|
145
|
+
executables: []
|
146
|
+
extensions: []
|
147
|
+
extra_rdoc_files: []
|
148
|
+
files:
|
149
|
+
- .gitignore
|
150
|
+
- Gemfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- lib/s3direct.rb
|
155
|
+
- lib/s3direct/file.rb
|
156
|
+
- lib/s3direct/string_interpolator.rb
|
157
|
+
- lib/s3direct/upload_request.rb
|
158
|
+
- lib/s3direct/uploadable.rb
|
159
|
+
- lib/s3direct/version.rb
|
160
|
+
- s3direct.gemspec
|
161
|
+
- spec/config_spec.rb
|
162
|
+
- spec/file_spec.rb
|
163
|
+
- spec/spec_helper.rb
|
164
|
+
- spec/string_interpolator_spec.rb
|
165
|
+
- spec/upload_request_spec.rb
|
166
|
+
- spec/uploadable_spec.rb
|
167
|
+
- vendor/assets/javascripts/s3direct.js.coffee
|
168
|
+
- vendor/assets/javascripts/s3direct_default_template.jst.ejs
|
169
|
+
homepage: ''
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
post_install_message:
|
173
|
+
rdoc_options: []
|
174
|
+
require_paths:
|
175
|
+
- lib
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
segments:
|
183
|
+
- 0
|
184
|
+
hash: 1023143937309546202
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
|
+
none: false
|
187
|
+
requirements:
|
188
|
+
- - ! '>='
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
segments:
|
192
|
+
- 0
|
193
|
+
hash: 1023143937309546202
|
194
|
+
requirements: []
|
195
|
+
rubyforge_project:
|
196
|
+
rubygems_version: 1.8.23
|
197
|
+
signing_key:
|
198
|
+
specification_version: 3
|
199
|
+
summary: Upload directly to S3
|
200
|
+
test_files:
|
201
|
+
- spec/config_spec.rb
|
202
|
+
- spec/file_spec.rb
|
203
|
+
- spec/spec_helper.rb
|
204
|
+
- spec/string_interpolator_spec.rb
|
205
|
+
- spec/upload_request_spec.rb
|
206
|
+
- spec/uploadable_spec.rb
|