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 +246 -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: 03db4a98d4b40da254386e6012a30766888820fe
4
+ data.tar.gz: 22ec535710720c582adb8f29aa39944e69188b1c
5
+ SHA512:
6
+ metadata.gz: 98316961b6b787789c59ee478b88aee0736085f9fb72683f4836ddc39197b0ffae619147d50e6f35d12268b911528a9dbb1028ec5131ef0778f1a0825a05d953
7
+ data.tar.gz: 523a172320987ae58673c5dce39a87ead7207434065fef824665c4b5e956978d30ef0bcc571f2d0046587253f6d0f15a64f0f8cd2f44da6208e4597c7714acfc
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,246 @@
1
+ # Mongoid Direct Upload to Amazon S3
2
+
3
+ [Original Gem for ActiveRecord](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html)
4
+
5
+ Enables direct file uploads to Amazon S3 and provides a flexible pattern for
6
+ your Rails app to asynchronously ingest the files.
7
+
8
+ ## Overview
9
+
10
+ This Rails engine allows you to quickly implement direct uploads to Amazon S3
11
+ from your Rails 3.1+ / 4.x application. It does not depend on any specific file
12
+ upload libraries, UI frameworks or AWS gems, like other solutions tend to.
13
+
14
+ It works by utilizing Amazon S3's
15
+ [Cross-Origin Resource Sharing](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html)
16
+ to permit browser-based uploads directly to S3 with presigned URLs generated by
17
+ this gem with your application's API credentials. As each file is uploaded,
18
+ the gem persists detail about the uploaded file in your application's database.
19
+ This table should be thought of much like a queue - think
20
+ [DelayedJob](https://github.com/collectiveidea/delayed_job) for your
21
+ uploaded-but-not-yet-ingested file uploads.
22
+
23
+ How (and if) you choose to import each uploaded file into your processing
24
+ library of choice is completely up to you. The gem tracks the state of each
25
+ upload so that you may used the provided `.pending` scope and `mark_imported!`
26
+ method to fetch, process (via your background processor of choice), then
27
+ mark-off each upload record whose file has been successfully ingested by your
28
+ app.
29
+
30
+ ## Features
31
+
32
+ * Files can be uploaded before or after your parent object has been saved.
33
+ * File upload fields can be placed inside or outside of your parent object's
34
+ form.
35
+ * File uploads display completion progress.
36
+ * Boilerplate styling can be used or easily replaced.
37
+ * 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
38
+ write to disk. S3 then decrypts and streams the files as they are downloaded.
39
+ * Models can have multiple types of uploads and specify for each if only one
40
+ file is permitted or if multiple are.
41
+ * Multiple files can upload concurrently to S3.
42
+
43
+ ## Technology & Requirements
44
+
45
+ Uploads are made possible by use of the `FormData` object, defined in
46
+ [XMLHttpRequest Level 2](http://dev.w3.org/2006/webapi/XMLHttpRequest-2/).
47
+ Many people are broadly referring to this as being provided by HTML5, but
48
+ technically it's part of the aforementioned spec that browsers have been
49
+ adhering to for a couple of major versions now. Even IE, wuh?
50
+
51
+ The latest versions of all of the following are ideal, but here are the gem's
52
+ minimum requirements:
53
+
54
+ * For rails 5.1+, use version 0.6.x+ and ruby 2.3+
55
+ * For rails 3.1 to 5.0, use version 0.5.x and ruby 1.9.3+
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 a 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 "s3_relay"` 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
+ ```
95
+ ENV["S3_RELAY_ACCESS_KEY_ID"]="abc123"
96
+ ENV["S3_RELAY_SECRET_ACCESS_KEY"]="xzy456"
97
+ ENV["S3_RELAY_REGION"]="us-west-2"
98
+ ENV["S3_RELAY_BUCKET"]="some-s3-bucket"
99
+ ENV["S3_RELAY_ACL"]="public-read"
100
+ ```
101
+
102
+ ## Use
103
+
104
+ ### Add upload definitions to your model
105
+
106
+ ```ruby
107
+ class Product < ActiveRecord::Base
108
+ s3_relay :icon
109
+ s3_relay :photo_uploads, has_many: true
110
+ end
111
+ ```
112
+
113
+ ### Restricting uploads to authenticated users
114
+
115
+ If your app's file uploads need to be restricted to logged in users, simply
116
+ override the following method in your application controller to call any
117
+ authentication method you're currently using.
118
+
119
+ ```ruby
120
+ def authenticate_for_s3_relay
121
+ authenticate_user! # Devise example
122
+ end
123
+ ```
124
+
125
+ ### Add virtual attributes to your controller's Strong Parameters config
126
+
127
+ ```ruby
128
+ product_params = params.require(:product)
129
+ .permit(:name, :new_icon_uuids: [], new_photo_uploads_uuids: [])
130
+
131
+ @product = Product.new(product_params)
132
+ ```
133
+
134
+ ### Add file upload fields to your views
135
+
136
+ ```erb
137
+ <%= s3_relay_field @product, :icon %>
138
+ <%= s3_relay_field @product, :photo_uploads, multiple: true %>
139
+ ```
140
+
141
+ * By default the content-disposition on the files stored in the uploads bucket
142
+ will be set to inline. You can optionally set it to attachment by passing that
143
+ option like so:
144
+
145
+ ```erb
146
+ <%= s3_relay_field @artist, :mp3_uploads, multiple: true, disposition: "attachment" %>
147
+ ```
148
+
149
+ ### View file
150
+
151
+ ```erb
152
+ <%= image_tag @product.icon.public_url %>
153
+ ```
154
+
155
+ ### Importing files
156
+
157
+ #### Processing uploads asynchronously
158
+
159
+ Use your background job processor of choice to process uploads pending
160
+ ingestion (and image processing) by your app.
161
+
162
+ Say you're using [Resque](https://github.com/resque/resque) and [CarrierWave](https://github.com/carrierwaveuploader/carrierwave), you could define a job class:
163
+
164
+ ```ruby
165
+ class ProductPhoto::Import
166
+ @queue = :photo_import
167
+
168
+ def self.perform(product_id, upload_id)
169
+ @product = Product.find(product_id)
170
+ @upload = S3Relay::Upload.find(upload_id)
171
+
172
+ @product.photos.create!(remote_file_url: @upload.private_url)
173
+ @upload.mark_imported!
174
+ end
175
+ end
176
+ ```
177
+
178
+ #### Triggering upload imports for existing parent objects
179
+
180
+ If you would like to immediately enqueue a job to begin importing an upload
181
+ into its final desination, simply define a method on your parent object
182
+ called `import_upload` and that method will be called after an `S3Relay::Upload`
183
+ is created.
184
+
185
+ #### Triggering upload imports for new parent objects
186
+
187
+ If you would like to immediately enqueue a job to begin importing all of the
188
+ uploads for a new parent object following its creation, you might want to setup
189
+ a callback to enqueue those imports.
190
+
191
+ #### Examples
192
+
193
+ ```ruby
194
+ class Product
195
+
196
+ # Called by s3_relay when an associated S3Relay::Upload object is created
197
+ def import_upload(upload_id)
198
+ Resque.enqueue(ProductPhoto::Import, id, upload_id)
199
+ end
200
+
201
+ after_commit :import_uploads, on: :create
202
+
203
+ # Called via after_commit to enqueue imports of S3Relay::Upload objects
204
+ def import_uploads
205
+ photo_uploads.pending.each do |upload|
206
+ Resque.enqueue(ProductPhoto::Import, id, upload.id)
207
+ end
208
+ end
209
+
210
+ end
211
+ ```
212
+
213
+ ### Restricting the objects uploads can be associated with
214
+
215
+ Remember the time when that guy found a way to a submit Github form in such a
216
+ way that it linked a new SSH key he provided to DHH's user record? No bueno.
217
+ Don't let your users attach files to objects they don't have access to.
218
+
219
+ You can prevent this by defining a method in ApplicationController that
220
+ filters out the parent object params passed during upload creation if your logic
221
+ finds that the user doesn't have access to the parent object in question. Ex:
222
+
223
+ ```ruby
224
+ def order_file_uploads_params(parent)
225
+ if parent.user == current_user
226
+ # Yep, that's your order, you can add files to it
227
+ { parent: parent }
228
+ else
229
+ # Nope, you're trying to add a file to someone else's order, or whatever
230
+ { }
231
+ end
232
+ end
233
+ ```
234
+
235
+ ## Contributing
236
+
237
+ 1. [Fork it](https://github.com/kjohnston/s3_relay/fork)
238
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
239
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
240
+ 4. Push to the branch (`git push origin my-new-feature`)
241
+ 5. [Create a Pull Request](https://github.com/kjohnston/s3_relay/pull/new)
242
+
243
+ ## License
244
+
245
+ * Freely distributable and licensed under the [MIT license](http://kjohnston.mit-license.org/license.html).
246
+ * 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
+ }