s3_relay 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +209 -0
- data/Rakefile +31 -0
- data/app/assets/javascripts/s3_relay.coffee +112 -0
- data/app/assets/stylesheets/s3_relay.css +31 -0
- data/app/controllers/s3_relay/uploads_controller.rb +65 -0
- data/app/helpers/s3_relay/uploads_helper.rb +23 -0
- data/app/models/s3_relay/upload.rb +46 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20141009000804_create_s3_relay_uploads.rb +19 -0
- data/lib/s3_relay.rb +4 -0
- data/lib/s3_relay/base.rb +35 -0
- data/lib/s3_relay/engine.rb +18 -0
- data/lib/s3_relay/model.rb +54 -0
- data/lib/s3_relay/private_url.rb +33 -0
- data/lib/s3_relay/s3_relay.rb +4 -0
- data/lib/s3_relay/upload_presigner.rb +60 -0
- data/lib/s3_relay/version.rb +3 -0
- data/test/controllers/s3_relay/uploads_controller_test.rb +138 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/product.rb +6 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +22 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +78 -0
- data/test/dummy/config/environments/test.rb +39 -0
- data/test/dummy/config/initializers/assets.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +7 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20141021002149_create_products.rb +9 -0
- data/test/dummy/db/schema.rb +41 -0
- data/test/dummy/log/development.log +53 -0
- data/test/dummy/log/test.log +14631 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/factories/products.rb +5 -0
- data/test/factories/uploads.rb +15 -0
- data/test/helpers/s3_relay/uploads_helper_test.rb +13 -0
- data/test/lib/s3_relay/model_test.rb +81 -0
- data/test/lib/s3_relay/private_url_test.rb +28 -0
- data/test/lib/s3_relay/upload_presigner_test.rb +38 -0
- data/test/models/s3_relay/upload_test.rb +128 -0
- data/test/support/database_cleaner.rb +14 -0
- data/test/test_helper.rb +28 -0
- metadata +302 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module S3Relay
|
2
|
+
module UploadsHelper
|
3
|
+
|
4
|
+
def s3_relay_field(parent, association, opts={})
|
5
|
+
file_field = file_field_tag(:file, opts.merge(class: "s3r-field"))
|
6
|
+
progress_table = content_tag(:table, "", class: "s3r-upload-list")
|
7
|
+
content = [file_field, progress_table].join
|
8
|
+
parent_type = parent.class.to_s.underscore.downcase
|
9
|
+
|
10
|
+
content_tag(:div, raw(content),
|
11
|
+
{
|
12
|
+
class: "s3r-container",
|
13
|
+
data: {
|
14
|
+
parent_type: parent_type,
|
15
|
+
parent_id: parent.id.to_s,
|
16
|
+
association: association.to_s
|
17
|
+
}
|
18
|
+
}
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Upload < ActiveRecord::Base
|
3
|
+
|
4
|
+
belongs_to :parent, polymorphic: true
|
5
|
+
|
6
|
+
validates :uuid, presence: true, uniqueness: true
|
7
|
+
validates :upload_type, presence: true
|
8
|
+
validates :filename, presence: true
|
9
|
+
validates :content_type, presence: true
|
10
|
+
validates :pending_at, presence: true
|
11
|
+
|
12
|
+
after_initialize :finalize
|
13
|
+
|
14
|
+
def self.pending
|
15
|
+
where(state: "pending")
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.imported
|
19
|
+
where(state: "imported")
|
20
|
+
end
|
21
|
+
|
22
|
+
def pending?
|
23
|
+
state == "pending"
|
24
|
+
end
|
25
|
+
|
26
|
+
def imported?
|
27
|
+
state == "imported"
|
28
|
+
end
|
29
|
+
|
30
|
+
def mark_imported!
|
31
|
+
update_attributes(state: "imported", imported_at: Time.now)
|
32
|
+
end
|
33
|
+
|
34
|
+
def private_url
|
35
|
+
S3Relay::PrivateUrl.new(uuid, filename).generate
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def finalize
|
41
|
+
self.state ||= "pending"
|
42
|
+
self.pending_at ||= Time.now
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateS3RelayUploads < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :s3_relay_uploads do |t|
|
4
|
+
t.binary :uuid, length: 16
|
5
|
+
t.integer :user_id
|
6
|
+
t.string :parent_type
|
7
|
+
t.integer :parent_id
|
8
|
+
t.string :upload_type
|
9
|
+
t.text :filename
|
10
|
+
t.string :content_type
|
11
|
+
t.string :state
|
12
|
+
t.column :data, :json, default: "{}"
|
13
|
+
t.datetime :pending_at
|
14
|
+
t.datetime :imported_at
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/s3_relay.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Base
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def access_key_id
|
7
|
+
ENV["S3_RELAY_ACCESS_KEY_ID"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def secret_access_key
|
11
|
+
ENV["S3_RELAY_SECRET_ACCESS_KEY"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def region
|
15
|
+
ENV["S3_RELAY_REGION"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def bucket
|
19
|
+
ENV["S3_RELAY_BUCKET"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def acl
|
23
|
+
ENV["S3_RELAY_ACL"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def endpoint
|
27
|
+
"https://#{bucket}.s3-#{region}.amazonaws.com"
|
28
|
+
end
|
29
|
+
|
30
|
+
def digest
|
31
|
+
OpenSSL::Digest.new("sha1")
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace S3Relay
|
4
|
+
|
5
|
+
initializer "s3_relay.action_controller" do |app|
|
6
|
+
ActiveSupport.on_load :action_controller do
|
7
|
+
helper S3Relay::UploadsHelper
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require "s3_relay/base"
|
14
|
+
require "s3_relay/model"
|
15
|
+
require "s3_relay/private_url"
|
16
|
+
require "s3_relay/upload_presigner"
|
17
|
+
|
18
|
+
ActiveRecord::Base.extend S3Relay::Model
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module S3Relay
|
2
|
+
module Model
|
3
|
+
|
4
|
+
def s3_relay(attribute, has_many=false)
|
5
|
+
upload_type = attribute.to_s.classify
|
6
|
+
|
7
|
+
if has_many
|
8
|
+
has_many attribute, as: :parent, class_name: "S3Relay::Upload"
|
9
|
+
|
10
|
+
define_method attribute do
|
11
|
+
S3Relay::Upload
|
12
|
+
.where(
|
13
|
+
parent_type: self.class.to_s,
|
14
|
+
parent_id: self.id,
|
15
|
+
upload_type: upload_type
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
virtual_attribute = "new_#{attribute}_uuids"
|
20
|
+
else
|
21
|
+
has_one attribute, as: :parent, class_name: "S3Relay::Upload"
|
22
|
+
|
23
|
+
define_method attribute do
|
24
|
+
S3Relay::Upload
|
25
|
+
.where(
|
26
|
+
parent_type: self.class.to_s,
|
27
|
+
parent_id: self.id,
|
28
|
+
upload_type: upload_type
|
29
|
+
)
|
30
|
+
.order("pending_at DESC").last
|
31
|
+
end
|
32
|
+
|
33
|
+
virtual_attribute = "new_#{attribute}_uuid"
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor virtual_attribute
|
37
|
+
|
38
|
+
association_method = "associate_#{attribute}"
|
39
|
+
|
40
|
+
after_save association_method
|
41
|
+
|
42
|
+
define_method association_method do
|
43
|
+
new_uuids = send(virtual_attribute)
|
44
|
+
return if new_uuids.blank?
|
45
|
+
|
46
|
+
S3Relay::Upload.where(uuid: new_uuids, upload_type: upload_type)
|
47
|
+
.update_all(parent_type: self.class.to_s, parent_id: self.id)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class PrivateUrl < S3Relay::Base
|
3
|
+
|
4
|
+
attr_reader :expires, :path
|
5
|
+
|
6
|
+
def initialize(uuid, file, options={})
|
7
|
+
filename = URI.encode(file).gsub("+", "%2B")
|
8
|
+
@path = [uuid, filename].join("/")
|
9
|
+
@expires = (options[:expires] || 10.minutes.from_now).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
"#{endpoint}/#{path}?#{params}"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def params
|
19
|
+
[
|
20
|
+
"AWSAccessKeyId=#{access_key_id}",
|
21
|
+
"Expires=#{expires}",
|
22
|
+
"Signature=#{signature}"
|
23
|
+
].join("&")
|
24
|
+
end
|
25
|
+
|
26
|
+
def signature
|
27
|
+
string = "GET\n\n\n#{expires}\n/#{bucket}/#{path}"
|
28
|
+
hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string)
|
29
|
+
CGI.escape(Base64.encode64(hmac).strip)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module S3Relay
|
2
|
+
class UploadPresigner < S3Relay::Base
|
3
|
+
|
4
|
+
attr_reader :expires, :uuid
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
@expires = (options[:expires] || 1.minute.from_now).utc.xmlschema
|
8
|
+
@uuid = SecureRandom.uuid
|
9
|
+
end
|
10
|
+
|
11
|
+
def form_data
|
12
|
+
fields.keys.inject({}) { |h,k| h[k.downcase.underscore] = fields[k]; h }
|
13
|
+
.merge(
|
14
|
+
"endpoint" => endpoint,
|
15
|
+
"policy" => encoded_policy,
|
16
|
+
"signature" => signature,
|
17
|
+
"uuid" => uuid
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fields
|
24
|
+
{
|
25
|
+
"AWSAccessKeyID" => access_key_id,
|
26
|
+
"x-amz-server-side-encryption" => "AES256",
|
27
|
+
"key" => "#{uuid}/${filename}",
|
28
|
+
"success_action_status" => "201",
|
29
|
+
"acl" => acl
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def hmac
|
34
|
+
lambda { |data| OpenSSL::HMAC.digest(digest, secret_access_key, data) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def policy_document
|
38
|
+
{
|
39
|
+
"expiration" => expires,
|
40
|
+
"conditions" => [
|
41
|
+
{ "bucket" => bucket },
|
42
|
+
{ "acl" => acl },
|
43
|
+
{ "x-amz-server-side-encryption" => "AES256" },
|
44
|
+
{ "success_action_status" => "201" },
|
45
|
+
["starts-with", "$content-type", ""],
|
46
|
+
["starts-with", "$key", "#{uuid}/"]
|
47
|
+
]
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def encoded_policy
|
52
|
+
Base64.strict_encode64(policy_document.to_json)
|
53
|
+
end
|
54
|
+
|
55
|
+
def signature
|
56
|
+
Base64.strict_encode64(hmac[encoded_policy])
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module S3Relay
|
4
|
+
describe UploadsController do
|
5
|
+
before do
|
6
|
+
S3Relay::Base.any_instance.stubs(:access_key_id)
|
7
|
+
.returns("access-key-id")
|
8
|
+
S3Relay::Base.any_instance.stubs(:secret_access_key)
|
9
|
+
.returns("secret-access-key")
|
10
|
+
S3Relay::Base.any_instance.stubs(:region)
|
11
|
+
.returns("region")
|
12
|
+
S3Relay::Base.any_instance.stubs(:bucket)
|
13
|
+
.returns("bucket")
|
14
|
+
S3Relay::Base.any_instance.stubs(:acl)
|
15
|
+
.returns("acl")
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "GET new" do
|
19
|
+
it do
|
20
|
+
uuid = "123-456-789"
|
21
|
+
time = Time.parse("2014-12-01 12:00am")
|
22
|
+
S3Relay::UploadPresigner.any_instance.stubs(:uuid).returns(uuid)
|
23
|
+
S3Relay::UploadPresigner.any_instance.stubs(:expires).returns(time)
|
24
|
+
|
25
|
+
get :new
|
26
|
+
assert_response 200
|
27
|
+
|
28
|
+
data = JSON.parse(response.body)
|
29
|
+
|
30
|
+
data["awsaccesskeyid"].must_equal "access-key-id"
|
31
|
+
data["x_amz_server_side_encryption"].must_equal "AES256"
|
32
|
+
data["key"].must_equal "#{uuid}/${filename}"
|
33
|
+
data["success_action_status"].must_equal "201"
|
34
|
+
data["acl"].must_equal "acl"
|
35
|
+
data["endpoint"].must_equal "https://bucket.s3-region.amazonaws.com"
|
36
|
+
data["policy"].length.must_equal 324 # TODO: Improve this
|
37
|
+
data["signature"].length.must_equal 28 # TODO: Improve this
|
38
|
+
data["uuid"].must_equal uuid
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "POST create" do
|
43
|
+
|
44
|
+
describe "success" do
|
45
|
+
it do
|
46
|
+
assert_difference "S3Relay::Upload.count", 1 do
|
47
|
+
post :create,
|
48
|
+
association: "photo_uploads",
|
49
|
+
uuid: SecureRandom.uuid,
|
50
|
+
filename: "cat.png",
|
51
|
+
content_type: "image/png"
|
52
|
+
end
|
53
|
+
|
54
|
+
assert_response 201
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "with parent attributes" do
|
58
|
+
describe "matching an object" do
|
59
|
+
before { @product = FactoryGirl.create(:product) }
|
60
|
+
|
61
|
+
it do
|
62
|
+
assert_difference "@product.photo_uploads.count", 1 do
|
63
|
+
post :create,
|
64
|
+
association: "photo_uploads",
|
65
|
+
uuid: SecureRandom.uuid,
|
66
|
+
filename: "cat.png",
|
67
|
+
content_type: "image/png",
|
68
|
+
parent_type: @product.class.to_s,
|
69
|
+
parent_id: @product.id.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
assert_response 201
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "not matching an object" do
|
77
|
+
it do
|
78
|
+
assert_difference "S3Relay::Upload.count" do
|
79
|
+
post :create,
|
80
|
+
association: "photo_uploads",
|
81
|
+
uuid: SecureRandom.uuid,
|
82
|
+
filename: "cat.png",
|
83
|
+
content_type: "image/png",
|
84
|
+
parent_type: "Product",
|
85
|
+
parent_id: "10000000"
|
86
|
+
end
|
87
|
+
|
88
|
+
assert_response 201
|
89
|
+
|
90
|
+
assigns[:upload].parent_type.must_equal nil
|
91
|
+
assigns[:upload].parent_id.must_equal nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "with a current_user" do
|
96
|
+
before do
|
97
|
+
@user = OpenStruct.new(id: 123)
|
98
|
+
@controller.stubs(:current_user).returns(@user)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "associates the upload with the user" do
|
102
|
+
assert_difference "S3Relay::Upload.count", 1 do
|
103
|
+
post :create,
|
104
|
+
association: "photo_uploads",
|
105
|
+
uuid: SecureRandom.uuid,
|
106
|
+
filename: "cat.png",
|
107
|
+
content_type: "image/png"
|
108
|
+
end
|
109
|
+
|
110
|
+
assert_response 201
|
111
|
+
|
112
|
+
assigns[:upload].user_id.must_equal @user.id
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "error" do
|
120
|
+
it do
|
121
|
+
assert_no_difference "S3Relay::Upload.count" do
|
122
|
+
post :create,
|
123
|
+
uuid: SecureRandom.uuid,
|
124
|
+
filename: "cat.png",
|
125
|
+
content_type: "image/png"
|
126
|
+
end
|
127
|
+
|
128
|
+
assert_response 422
|
129
|
+
|
130
|
+
JSON.parse(response.body)["errors"]["upload_type"]
|
131
|
+
.must_include "can't be blank"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|