s3_relay 0.0.2
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.
- 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
|