s3_relay 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +209 -0
  4. data/Rakefile +31 -0
  5. data/app/assets/javascripts/s3_relay.coffee +112 -0
  6. data/app/assets/stylesheets/s3_relay.css +31 -0
  7. data/app/controllers/s3_relay/uploads_controller.rb +65 -0
  8. data/app/helpers/s3_relay/uploads_helper.rb +23 -0
  9. data/app/models/s3_relay/upload.rb +46 -0
  10. data/config/routes.rb +5 -0
  11. data/db/migrate/20141009000804_create_s3_relay_uploads.rb +19 -0
  12. data/lib/s3_relay.rb +4 -0
  13. data/lib/s3_relay/base.rb +35 -0
  14. data/lib/s3_relay/engine.rb +18 -0
  15. data/lib/s3_relay/model.rb +54 -0
  16. data/lib/s3_relay/private_url.rb +33 -0
  17. data/lib/s3_relay/s3_relay.rb +4 -0
  18. data/lib/s3_relay/upload_presigner.rb +60 -0
  19. data/lib/s3_relay/version.rb +3 -0
  20. data/test/controllers/s3_relay/uploads_controller_test.rb +138 -0
  21. data/test/dummy/README.rdoc +28 -0
  22. data/test/dummy/Rakefile +6 -0
  23. data/test/dummy/app/assets/javascripts/application.js +13 -0
  24. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  25. data/test/dummy/app/controllers/application_controller.rb +5 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/models/product.rb +6 -0
  28. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  29. data/test/dummy/bin/bundle +3 -0
  30. data/test/dummy/bin/rails +4 -0
  31. data/test/dummy/bin/rake +4 -0
  32. data/test/dummy/config.ru +4 -0
  33. data/test/dummy/config/application.rb +23 -0
  34. data/test/dummy/config/boot.rb +5 -0
  35. data/test/dummy/config/database.yml +22 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +37 -0
  38. data/test/dummy/config/environments/production.rb +78 -0
  39. data/test/dummy/config/environments/test.rb +39 -0
  40. data/test/dummy/config/initializers/assets.rb +8 -0
  41. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  42. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  43. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  44. data/test/dummy/config/initializers/inflections.rb +16 -0
  45. data/test/dummy/config/initializers/mime_types.rb +4 -0
  46. data/test/dummy/config/initializers/session_store.rb +3 -0
  47. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/test/dummy/config/locales/en.yml +23 -0
  49. data/test/dummy/config/routes.rb +7 -0
  50. data/test/dummy/config/secrets.yml +22 -0
  51. data/test/dummy/db/migrate/20141021002149_create_products.rb +9 -0
  52. data/test/dummy/db/schema.rb +41 -0
  53. data/test/dummy/log/development.log +53 -0
  54. data/test/dummy/log/test.log +14631 -0
  55. data/test/dummy/public/404.html +67 -0
  56. data/test/dummy/public/422.html +67 -0
  57. data/test/dummy/public/500.html +66 -0
  58. data/test/dummy/public/favicon.ico +0 -0
  59. data/test/factories/products.rb +5 -0
  60. data/test/factories/uploads.rb +15 -0
  61. data/test/helpers/s3_relay/uploads_helper_test.rb +13 -0
  62. data/test/lib/s3_relay/model_test.rb +81 -0
  63. data/test/lib/s3_relay/private_url_test.rb +28 -0
  64. data/test/lib/s3_relay/upload_presigner_test.rb +38 -0
  65. data/test/models/s3_relay/upload_test.rb +128 -0
  66. data/test/support/database_cleaner.rb +14 -0
  67. data/test/test_helper.rb +28 -0
  68. 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
@@ -0,0 +1,5 @@
1
+ S3Relay::Engine.routes.draw do
2
+
3
+ resources :uploads, only: [:new, :create]
4
+
5
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ require "s3_relay/engine"
2
+
3
+ module S3Relay
4
+ end
@@ -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,4 @@
1
+ require "s3_relay/engine"
2
+
3
+ module S3Relay
4
+ 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,3 @@
1
+ module S3Relay
2
+ VERSION = "0.0.2"
3
+ 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