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