mongoid-direct-s3-upload 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +250 -0
  4. data/Rakefile +31 -0
  5. data/app/assets/javascripts/s3_relay.coffee +118 -0
  6. data/app/assets/stylesheets/s3_relay.css +31 -0
  7. data/app/controllers/s3_relay/uploads_controller.rb +71 -0
  8. data/app/helpers/s3_relay/uploads_helper.rb +24 -0
  9. data/app/models/s3_relay/upload.rb +73 -0
  10. data/config/routes.rb +5 -0
  11. data/lib/s3_relay.rb +4 -0
  12. data/lib/s3_relay/base.rb +35 -0
  13. data/lib/s3_relay/engine.rb +16 -0
  14. data/lib/s3_relay/model.rb +51 -0
  15. data/lib/s3_relay/private_url.rb +37 -0
  16. data/lib/s3_relay/s3_relay.rb +4 -0
  17. data/lib/s3_relay/upload_presigner.rb +61 -0
  18. data/lib/s3_relay/version.rb +3 -0
  19. data/test/controllers/s3_relay/uploads_controller_test.rb +144 -0
  20. data/test/dummy/README.md +24 -0
  21. data/test/dummy/Rakefile +6 -0
  22. data/test/dummy/app/assets/config/manifest.js +3 -0
  23. data/test/dummy/app/assets/javascripts/application.js +15 -0
  24. data/test/dummy/app/assets/javascripts/cable.js +13 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  26. data/test/dummy/app/channels/application_cable/channel.rb +4 -0
  27. data/test/dummy/app/channels/application_cable/connection.rb +4 -0
  28. data/test/dummy/app/controllers/application_controller.rb +3 -0
  29. data/test/dummy/app/helpers/application_helper.rb +2 -0
  30. data/test/dummy/app/jobs/application_job.rb +2 -0
  31. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  32. data/test/dummy/app/models/application_record.rb +3 -0
  33. data/test/dummy/app/models/product.rb +6 -0
  34. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  35. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  36. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  37. data/test/dummy/bin/bundle +3 -0
  38. data/test/dummy/bin/rails +9 -0
  39. data/test/dummy/bin/rake +9 -0
  40. data/test/dummy/bin/setup +38 -0
  41. data/test/dummy/bin/spring +17 -0
  42. data/test/dummy/bin/update +29 -0
  43. data/test/dummy/bin/yarn +11 -0
  44. data/test/dummy/config.ru +5 -0
  45. data/test/dummy/config/application.rb +19 -0
  46. data/test/dummy/config/boot.rb +3 -0
  47. data/test/dummy/config/cable.yml +10 -0
  48. data/test/dummy/config/database.yml +22 -0
  49. data/test/dummy/config/environment.rb +5 -0
  50. data/test/dummy/config/environments/development.rb +54 -0
  51. data/test/dummy/config/environments/production.rb +91 -0
  52. data/test/dummy/config/environments/test.rb +42 -0
  53. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  54. data/test/dummy/config/initializers/assets.rb +14 -0
  55. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  57. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  58. data/test/dummy/config/initializers/inflections.rb +16 -0
  59. data/test/dummy/config/initializers/mime_types.rb +4 -0
  60. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  61. data/test/dummy/config/locales/en.yml +33 -0
  62. data/test/dummy/config/puma.rb +56 -0
  63. data/test/dummy/config/routes.rb +7 -0
  64. data/test/dummy/config/secrets.yml +32 -0
  65. data/test/dummy/config/spring.rb +6 -0
  66. data/test/dummy/db/migrate/20141021002149_create_products.rb +9 -0
  67. data/test/dummy/db/schema.rb +41 -0
  68. data/test/dummy/db/seeds.rb +7 -0
  69. data/test/dummy/package.json +5 -0
  70. data/test/dummy/public/404.html +67 -0
  71. data/test/dummy/public/422.html +67 -0
  72. data/test/dummy/public/500.html +66 -0
  73. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  74. data/test/dummy/public/apple-touch-icon.png +0 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/dummy/public/robots.txt +1 -0
  77. data/test/dummy/test/application_system_test_case.rb +5 -0
  78. data/test/dummy/test/test_helper.rb +9 -0
  79. data/test/factories/products.rb +5 -0
  80. data/test/factories/uploads.rb +23 -0
  81. data/test/helpers/s3_relay/uploads_helper_test.rb +29 -0
  82. data/test/lib/s3_relay/model_test.rb +66 -0
  83. data/test/lib/s3_relay/private_url_test.rb +28 -0
  84. data/test/lib/s3_relay/upload_presigner_test.rb +38 -0
  85. data/test/models/s3_relay/upload_test.rb +142 -0
  86. data/test/support/database_cleaner.rb +14 -0
  87. data/test/test_helper.rb +29 -0
  88. metadata +356 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2566dba40ae75b7fc32541abd4a5732ae16c4fed
4
+ data.tar.gz: 3ff5182fbec54c955c3d0f5e7380cb0932711027
5
+ SHA512:
6
+ metadata.gz: d1f57ed9c72a90eda341ffb8815bab7556631839494789e07a374b704a44bcfe584292ed49f6f23dc9b35a1775b05e946bf12209f11ad9429fb895757eec18df
7
+ data.tar.gz: dbc65a1713bb8c91180676bbaec241269dcfb202ba9c5b8774e03527809722fe825b511f554514a53a4cc734c83acf27882c89e3b5428e0bf735f0f982b56f75
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Kenny Johnston
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # Mongoid Direct Upload to Amazon S3
2
+
3
+ Or `s3_relay_mongoid`
4
+
5
+ [Original s3_relay Gem for ActiveRecord](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html)
6
+
7
+ Enables direct file uploads to Amazon S3 and provides a flexible pattern for
8
+ your Rails app to asynchronously ingest the files.
9
+
10
+ ## Overview
11
+
12
+ This Rails engine allows you to quickly implement direct uploads to Amazon S3
13
+ from your Rails 3.1+ / 4.x / 5.x application. It does not depend on any specific file
14
+ upload libraries, UI frameworks or AWS gems, like other solutions tend to.
15
+
16
+ It works by utilizing Amazon S3's
17
+ [Cross-Origin Resource Sharing](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html)
18
+ to permit browser-based uploads directly to S3 with presigned URLs generated by
19
+ this gem with your application's API credentials. As each file is uploaded,
20
+ the gem persists detail about the uploaded file in your application's database.
21
+ This table should be thought of much like a queue - think
22
+ [DelayedJob](https://github.com/collectiveidea/delayed_job) for your
23
+ uploaded-but-not-yet-ingested file uploads.
24
+
25
+ How (and if) you choose to import each uploaded file into your processing
26
+ library of choice is completely up to you. The gem tracks the state of each
27
+ upload so that you may used the provided `.pending` scope and `mark_imported!`
28
+ method to fetch, process (via your background processor of choice), then
29
+ mark-off each upload record whose file has been successfully ingested by your
30
+ app.
31
+
32
+ ## Features
33
+
34
+ * Files can be uploaded before or after your parent object has been saved.
35
+ * File upload fields can be placed inside or outside of your parent object's
36
+ form.
37
+ * File uploads display completion progress.
38
+ * Boilerplate styling can be used or easily replaced.
39
+ * All uploads are marked for [Server-Side Encryption by AWS](http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingEncryption.html) so that they are encrypted upon
40
+ write to disk. S3 then decrypts and streams the files as they are downloaded.
41
+ * Models can have multiple types of uploads and specify for each if only one
42
+ file is permitted or if multiple are.
43
+ * Multiple files can upload concurrently to S3.
44
+
45
+ ## Technology & Requirements
46
+
47
+ Uploads are made possible by use of the `FormData` object, defined in
48
+ [XMLHttpRequest Level 2](http://dev.w3.org/2006/webapi/XMLHttpRequest-2/).
49
+ Many people are broadly referring to this as being provided by HTML5, but
50
+ technically it's part of the aforementioned spec that browsers have been
51
+ adhering to for a couple of major versions now. Even IE, wuh?
52
+
53
+ The latest versions of all of the following are ideal, but here are the gem's
54
+ minimum requirements:
55
+
56
+ * Modern versions of Chrome, Safari, FireFox or IE 10+
57
+ * Note: Progress bars are currently disabled in IE
58
+ * Note: IE <= 9 users will be instructed to upgrade their browser upon
59
+ selecting a file
60
+
61
+ ## Demo
62
+
63
+ See the ActiveRecord demo application using `s3_relay` [here](https://github.com/kjohnston/s3_relay-demo).
64
+
65
+ ## Configuring CORS
66
+
67
+ Edit your S3 bucket's CORS Configuration to resemble the following:
68
+
69
+ ```
70
+ <CORSConfiguration>
71
+ <CORSRule>
72
+ <AllowedOrigin>*</AllowedOrigin>
73
+ <AllowedMethod>POST</AllowedMethod>
74
+ <AllowedHeader>Content-Type</AllowedHeader>
75
+ <AllowedHeader>origin</AllowedHeader>
76
+ </CORSRule>
77
+ </CORSConfiguration>
78
+ ```
79
+
80
+ Note: The example above is a starting point for development. Obviously, you
81
+ don't want to permit requests from any domain to upload to your S3 bucket.
82
+ Please see the [AWS Documentation](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html)
83
+ to learn how to lock it down further.
84
+
85
+ ## Installation
86
+
87
+ * Add `gem "mongoid-direct-s3-upload"` to your Gemfile and run `bundle`.
88
+ * Add migrations to your app with `rake s3_relay:install:migrations db:migrate`.
89
+ * Add `mount S3Relay::Engine => "/s3_relay"` to the top of your routes file.
90
+ * Add `require s3_relay` to your JavaScript manifest.
91
+ * [Optional] Add `require s3_relay` to your Style Sheet manifest.
92
+ * Add the following environment variables to your app:
93
+
94
+ `./config/initializers/mongoid-direct-s3-upload.rb`
95
+
96
+ ```
97
+ ENV["S3_RELAY_ACCESS_KEY_ID"]="abc123"
98
+ ENV["S3_RELAY_SECRET_ACCESS_KEY"]="xzy456"
99
+ ENV["S3_RELAY_REGION"]="us-west-2"
100
+ ENV["S3_RELAY_BUCKET"]="some-s3-bucket"
101
+ ENV["S3_RELAY_ACL"]="public-read"
102
+ ```
103
+
104
+ ## Use
105
+
106
+ ### Add upload definitions to your model
107
+
108
+ ```ruby
109
+ class Product
110
+ include Mongoid::Document
111
+ extend S3Relay::Model
112
+ s3_relay :icon
113
+ s3_relay :photo_uploads, has_many: true
114
+ end
115
+ ```
116
+
117
+ ### Restricting uploads to authenticated users
118
+
119
+ If your app's file uploads need to be restricted to logged in users, simply
120
+ override the following method in your application controller to call any
121
+ authentication method you're currently using.
122
+
123
+ ```ruby
124
+ def authenticate_for_s3_relay
125
+ authenticate_user! # Devise example
126
+ end
127
+ ```
128
+
129
+ ### Add virtual attributes to your controller's Strong Parameters config
130
+
131
+ ```ruby
132
+ product_params = params.require(:product)
133
+ .permit(:name, :new_icon_uuids: [], new_photo_uploads_uuids: [])
134
+
135
+ @product = Product.new(product_params)
136
+ ```
137
+
138
+ ### Add file upload fields to your views
139
+
140
+ ```erb
141
+ <%= s3_relay_field @product, :icon %>
142
+ <%= s3_relay_field @product, :photo_uploads, multiple: true %>
143
+ ```
144
+
145
+ * By default the content-disposition on the files stored in the uploads bucket
146
+ will be set to inline. You can optionally set it to attachment by passing that
147
+ option like so:
148
+
149
+ ```erb
150
+ <%= s3_relay_field @artist, :mp3_uploads, multiple: true, disposition: "attachment" %>
151
+ ```
152
+
153
+ ### View file on Amazon S3
154
+
155
+ ```erb
156
+ <%= image_tag @product.icon.public_url %>
157
+ ```
158
+
159
+ ### Importing files back to the server for processing
160
+
161
+ #### Processing uploads asynchronously
162
+
163
+ Use your background job processor of choice to process uploads pending
164
+ ingestion (and image processing) by your app.
165
+
166
+ Say you're using [Resque](https://github.com/resque/resque) and [CarrierWave](https://github.com/carrierwaveuploader/carrierwave), you could define a job class:
167
+
168
+ ```ruby
169
+ class ProductPhoto::Import
170
+ @queue = :photo_import
171
+
172
+ def self.perform(product_id, upload_id)
173
+ @product = Product.find(product_id)
174
+ @upload = S3Relay::Upload.find(upload_id)
175
+
176
+ @product.photos.create!(remote_file_url: @upload.private_url)
177
+ @upload.mark_imported!
178
+ end
179
+ end
180
+ ```
181
+
182
+ #### Triggering upload imports for existing parent objects
183
+
184
+ If you would like to immediately enqueue a job to begin importing an upload
185
+ into its final desination, simply define a method on your parent object
186
+ called `import_upload` and that method will be called after an `S3Relay::Upload`
187
+ is created.
188
+
189
+ #### Triggering upload imports for new parent objects
190
+
191
+ If you would like to immediately enqueue a job to begin importing all of the
192
+ uploads for a new parent object following its creation, you might want to setup
193
+ a callback to enqueue those imports.
194
+
195
+ #### Examples
196
+
197
+ ```ruby
198
+ class Product
199
+
200
+ # Called by s3_relay when an associated S3Relay::Upload object is created
201
+ def import_upload(upload_id)
202
+ Resque.enqueue(ProductPhoto::Import, id, upload_id)
203
+ end
204
+
205
+ after_commit :import_uploads, on: :create
206
+
207
+ # Called via after_commit to enqueue imports of S3Relay::Upload objects
208
+ def import_uploads
209
+ photo_uploads.pending.each do |upload|
210
+ Resque.enqueue(ProductPhoto::Import, id, upload.id)
211
+ end
212
+ end
213
+
214
+ end
215
+ ```
216
+
217
+ ### Restricting the objects uploads can be associated with
218
+
219
+ Remember the time when that guy found a way to a submit Github form in such a
220
+ way that it linked a new SSH key he provided to DHH's user record? No bueno.
221
+ Don't let your users attach files to objects they don't have access to.
222
+
223
+ You can prevent this by defining a method in ApplicationController that
224
+ filters out the parent object params passed during upload creation if your logic
225
+ finds that the user doesn't have access to the parent object in question. Ex:
226
+
227
+ ```ruby
228
+ def order_file_uploads_params(parent)
229
+ if parent.user == current_user
230
+ # Yep, that's your order, you can add files to it
231
+ { parent: parent }
232
+ else
233
+ # Nope, you're trying to add a file to someone else's order, or whatever
234
+ { }
235
+ end
236
+ end
237
+ ```
238
+
239
+ ## Contributing
240
+
241
+ 1. [Fork it](https://github.com/kjohnston/s3_relay/fork)
242
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
243
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
244
+ 4. Push to the branch (`git push origin my-new-feature`)
245
+ 5. [Create a Pull Request](https://github.com/kjohnston/s3_relay/pull/new)
246
+
247
+ ## License
248
+
249
+ * Freely distributable and licensed under the [MIT license](http://kjohnston.mit-license.org/license.html).
250
+ * Copyright (c) 2014 Kenny Johnston
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "S3Relay"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("README.rdoc")
14
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
+ load "rails/tasks/engine.rake"
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require "rake/testtask"
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << "lib"
26
+ t.libs << "test"
27
+ t.pattern = "test/**/*_test.rb"
28
+ t.verbose = false
29
+ end
30
+
31
+ task default: :test
@@ -0,0 +1,118 @@
1
+ displayFailedUpload = (progressColumn=null) ->
2
+ if progressColumn
3
+ progressColumn.text("File could not be uploaded")
4
+ else
5
+ alert("File could not be uploaded")
6
+
7
+ publishEvent = (target, name, detail) ->
8
+ $(target).trigger( name, detail )
9
+
10
+ saveUrl = (container, uuid, filename, contentType, publicUrl, progressColumn, fileColumn) ->
11
+ privateUrl = null
12
+
13
+ $.ajax
14
+ type: "POST"
15
+ url: "/s3_relay/uploads"
16
+ data:
17
+ parent_type: container.data("parentType")
18
+ parent_id: container.data("parentId")
19
+ association: container.data("association")
20
+ uuid: uuid
21
+ filename: filename
22
+ content_type: contentType
23
+ public_url: publicUrl
24
+ success: (data, status, xhr) ->
25
+ privateUrl = data.private_url
26
+ if privateUrl == null
27
+ displayFailedUpload(progressColumn)
28
+ else
29
+ fileColumn.html("<a href='#{privateUrl}' target='_blank'>#{filename}</a>")
30
+
31
+ virtualAttr = "#{container.data('parentType')}[new_#{container.data('association')}_uuids]"
32
+ hiddenField = "<input type='hidden' name='#{virtualAttr}[]' value='#{uuid}' />"
33
+ container.append(hiddenField)
34
+ publishEvent(container, "upload:success", [ uuid, filename, privateUrl ])
35
+ error: (xhr) ->
36
+ console.log xhr.responseText
37
+
38
+ return privateUrl
39
+
40
+ uploadFiles = (container) ->
41
+ fileInput = $("input.s3r-field", container)
42
+ files = fileInput.get(0).files
43
+ uploadFile(container, file) for file in files
44
+ fileInput.val("")
45
+
46
+ uploadFile = (container, file) ->
47
+ fileName = file.name
48
+
49
+ # Assign unique value to each request so Safari doesn't consolidate them
50
+ @s3r_upload_index ||= 0
51
+ @s3r_upload_index += 1
52
+
53
+ $.ajax
54
+ type: "GET"
55
+ url: "/s3_relay/uploads/new?s3r_upload_index=#{s3r_upload_index}"
56
+ success: (data, status, xhr) ->
57
+ formData = new FormData()
58
+ xhr = new XMLHttpRequest()
59
+ endpoint = data.endpoint
60
+ disposition = container.data("disposition")
61
+
62
+ formData.append("AWSAccessKeyID", data.awsaccesskeyid)
63
+ formData.append("x-amz-server-side-encryption", data.x_amz_server_side_encryption)
64
+ formData.append("key", data.key)
65
+ formData.append("success_action_status", data.success_action_status)
66
+ formData.append("acl", data.acl)
67
+ formData.append("policy", data.policy)
68
+ formData.append("signature", data.signature)
69
+ formData.append("content-type", file.type)
70
+ formData.append("content-disposition", "#{disposition}; filename=\"#{fileName}\"")
71
+ formData.append("file", file)
72
+
73
+ uuid = data.uuid
74
+
75
+ uploadList = $(".s3r-upload-list", container)
76
+ uploadList.prepend("<tr id='#{uuid}'><td><div class='s3r-file-url'>#{fileName}</div></td><td class='s3r-progress'><div class='s3r-bar' style='display: none;'><div class='s3r-meter'></div></div></td></tr>")
77
+ fileColumn = $(".s3r-upload-list ##{uuid} .s3r-file-url", container)
78
+ progressColumn = $(".s3r-upload-list ##{uuid} .s3r-progress", container)
79
+ progressBar = $(".s3r-bar", progressColumn)
80
+ progressMeter = $(".s3r-meter", progressColumn)
81
+
82
+ xhr.upload.addEventListener "progress", (ev) ->
83
+ if ev.loaded
84
+ percentage = ((ev.loaded / ev.total) * 100.0).toFixed(0)
85
+ progressBar.show()
86
+ progressMeter.css "width", "#{percentage}%"
87
+ else
88
+ progressColumn.text("Uploading...") # IE can't get position
89
+
90
+ xhr.onreadystatechange = (ev) ->
91
+ if xhr.readyState is 4
92
+ progressColumn.text("") # IE can't get position
93
+ progressBar.remove()
94
+
95
+ if xhr.status == 201
96
+ contentType = file.type
97
+ publicUrl = $("Location", xhr.responseXML).text()
98
+ saveUrl(container, uuid, fileName, contentType, publicUrl, progressColumn, fileColumn)
99
+ else
100
+ displayFailedUpload(progressColumn)
101
+ console.log $("Message", xhr.responseXML).text()
102
+
103
+ xhr.open "POST", endpoint, true
104
+ xhr.send formData
105
+ error: (xhr) ->
106
+ displayFailedUpload()
107
+ console.log xhr.responseText
108
+
109
+ jQuery ->
110
+
111
+ $(document).on "change", ".s3r-field", ->
112
+ $this = $(this)
113
+
114
+ if !!window.FormData
115
+ uploadFiles($this.parent())
116
+ else
117
+ $this.hide()
118
+ $this.parent().append("<p>Your browser can't handle file uploads, please switch to <a href='http://google.com/chrome'>Google Chrome</a>.</p>")
@@ -0,0 +1,31 @@
1
+ .s3r-upload-list {
2
+ border: 1px solid #ccc;
3
+ border-collapse: collapse;
4
+ margin: 15px 0;
5
+ }
6
+
7
+ .s3r-upload-list td {
8
+ border-bottom: 1px solid #ccc;
9
+ padding: .5em;
10
+ width: 180px;
11
+ }
12
+
13
+ .s3r-file-url {
14
+ overflow: hidden;
15
+ text-overflow: ellipsis;
16
+ white-space: nowrap;
17
+ width: 180px;
18
+ }
19
+
20
+ .s3r-bar {
21
+ background-color: #eee;
22
+ border-radius: 3px;
23
+ height: 15px;
24
+ width: 180px;
25
+ }
26
+
27
+ .s3r-meter {
28
+ background-color: #43ac6a;
29
+ border-radius: 3px;
30
+ height: 15px;
31
+ }