s3_cors_fileupload 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.document +5 -0
  2. data/.gitignore +51 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +39 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +104 -0
  8. data/Rakefile +58 -0
  9. data/lib/generators/s3_cors_fileupload/install/USAGE +17 -0
  10. data/lib/generators/s3_cors_fileupload/install/install_generator.rb +51 -0
  11. data/lib/generators/s3_cors_fileupload/install/templates/amazon_s3.yml +17 -0
  12. data/lib/generators/s3_cors_fileupload/install/templates/create_source_files.rb +14 -0
  13. data/lib/generators/s3_cors_fileupload/install/templates/s3_uploads.js +94 -0
  14. data/lib/generators/s3_cors_fileupload/install/templates/s3_uploads_controller.rb +90 -0
  15. data/lib/generators/s3_cors_fileupload/install/templates/source_file.rb +53 -0
  16. data/lib/generators/s3_cors_fileupload/install/templates/views/_template_download.html.erb +29 -0
  17. data/lib/generators/s3_cors_fileupload/install/templates/views/_template_upload.html.erb +31 -0
  18. data/lib/generators/s3_cors_fileupload/install/templates/views/_template_uploaded.html.erb +25 -0
  19. data/lib/generators/s3_cors_fileupload/install/templates/views/index.html.erb +43 -0
  20. data/lib/s3_cors_fileupload.rb +2 -0
  21. data/lib/s3_cors_fileupload/rails.rb +8 -0
  22. data/lib/s3_cors_fileupload/rails/config.rb +27 -0
  23. data/lib/s3_cors_fileupload/rails/engine.rb +6 -0
  24. data/lib/s3_cors_fileupload/rails/form_helper.rb +91 -0
  25. data/lib/s3_cors_fileupload/rails/policy_helper.rb +48 -0
  26. data/lib/s3_cors_fileupload/version.rb +5 -0
  27. data/s3_cors_fileupload.gemspec +35 -0
  28. data/spec/s3_cors_fileupload/version_spec.rb +17 -0
  29. data/spec/s3_cors_fileupload_spec.rb +9 -0
  30. data/spec/spec_helper.rb +16 -0
  31. data/vendor/assets/images/loading.gif +0 -0
  32. data/vendor/assets/images/progressbar.gif +0 -0
  33. data/vendor/assets/javascripts/s3_cors_fileupload/index.js +6 -0
  34. data/vendor/assets/javascripts/s3_cors_fileupload/jquery.fileupload-ui.js +732 -0
  35. data/vendor/assets/javascripts/s3_cors_fileupload/jquery.fileupload.js +1106 -0
  36. data/vendor/assets/javascripts/s3_cors_fileupload/jquery.iframe-transport.js +172 -0
  37. data/vendor/assets/javascripts/s3_cors_fileupload/vendor/jquery.ui.widget.js +511 -0
  38. data/vendor/assets/javascripts/s3_cors_fileupload/vendor/load-image.js +122 -0
  39. data/vendor/assets/javascripts/s3_cors_fileupload/vendor/tmpl.js +87 -0
  40. data/vendor/assets/stylesheets/jquery.fileupload-ui.css.erb +85 -0
  41. metadata +205 -0
@@ -0,0 +1,90 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'digest/sha1'
4
+
5
+ class S3UploadsController < ApplicationController
6
+
7
+ helper_method :s3_upload_policy_document, :s3_upload_signature
8
+
9
+ # GET /source_files
10
+ # GET /source_files.json
11
+ def index
12
+ @source_files = SourceFile.all
13
+
14
+ respond_to do |format|
15
+ format.html # index.html.erb
16
+ format.json { render json: @source_files.map{|sf| sf.to_jq_upload } }
17
+ end
18
+ end
19
+
20
+ # POST /source_files
21
+ # POST /source_files.json
22
+ def create
23
+ @source_file = SourceFile.new(params[:source_file])
24
+ respond_to do |format|
25
+ if @source_file.save
26
+ format.html {
27
+ render :json => @source_file.to_jq_upload,
28
+ :content_type => 'text/html',
29
+ :layout => false
30
+ }
31
+ format.json { render json: @source_file.to_jq_upload, status: :created }
32
+ else
33
+ format.html { render action: "new" }
34
+ format.json { render json: @source_file.errors, status: :unprocessable_entity }
35
+ end
36
+ end
37
+ end
38
+
39
+ # DELETE /source_files/1
40
+ # DELETE /source_files/1.json
41
+ def destroy
42
+ @source_file = SourceFile.find(params[:id])
43
+ @source_file.destroy
44
+
45
+ respond_to do |format|
46
+ format.html { redirect_to source_files_url }
47
+ format.json { head :no_content }
48
+ format.xml { head :no_content }
49
+ end
50
+ end
51
+
52
+ # used for s3_uploader
53
+ def generate_key
54
+ uid = SecureRandom.uuid.gsub(/-/,'')
55
+
56
+ render json: {
57
+ key: "uploads/#{uid}/#{params[:filename]}",
58
+ success_action_redirect: "/"
59
+ }
60
+ end
61
+
62
+ # ---- Helpers ----
63
+ # generate the policy document that amazon is expecting.
64
+ def s3_upload_policy_document
65
+ Base64.encode64(
66
+ {
67
+ expiration: 1.hour.from_now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
68
+ conditions: [
69
+ { bucket: S3CorsFileupload::Config.bucket },
70
+ { acl: 'public-read' },
71
+ { success_action_status: '201' },
72
+ ["starts-with", "$key", ""],
73
+ ["starts-with", "$Content-Type", ""]
74
+ ]
75
+ }.to_json
76
+ ).gsub(/\n|\r/, '')
77
+ end
78
+
79
+ # sign our request by Base64 encoding the policy document.
80
+ def s3_upload_signature
81
+ Base64.encode64(
82
+ OpenSSL::HMAC.digest(
83
+ OpenSSL::Digest::Digest.new('sha1'),
84
+ S3CorsFileupload::Config.secret_access_key,
85
+ s3_upload_policy_document
86
+ )
87
+ ).gsub(/\n/, '')
88
+ end
89
+
90
+ end
@@ -0,0 +1,53 @@
1
+ require 'aws/s3'
2
+
3
+ class SourceFile < ActiveRecord::Base
4
+ attr_accessible :url, :bucket, :key
5
+
6
+ validates_presence_of :file_name, :file_content_type, :file_size, :key, :bucket
7
+
8
+ before_validation(:on => :create) do
9
+ self.file_name = key.split('/').last if key
10
+ # for some reason, the response from AWS seems to escape the slashes in the keys, this line will unescape the slash
11
+ self.url = url.gsub(/%2F/, '/') if url
12
+ self.file_size ||= s3_object.try(:size)
13
+ self.file_content_type ||= s3_object.try(:content_type)
14
+ end
15
+ # make all attributes readonly after creating the record (not sure we need this?)
16
+ after_create { readonly! }
17
+ # cleanup; destroy corresponding file on S3
18
+ after_destroy { s3_object.try(:delete) }
19
+
20
+ def to_jq_upload
21
+ {
22
+ 'id' => id,
23
+ 'name' => file_name,
24
+ 'size' => file_size,
25
+ 'url' => url,
26
+ 'image' => self.is_image?,
27
+ 'delete_url' => Rails.application.routes.url_helpers.source_file_path(self)
28
+ }
29
+ end
30
+
31
+ def is_image?
32
+ !!file_content_type.try(:match, /image/)
33
+ end
34
+
35
+ #---- start S3 related methods -----
36
+ def s3_object
37
+ @s3_object ||= AWS::S3::S3Object.find(key, bucket) if self.class.open_aws && key
38
+ rescue
39
+ nil
40
+ end
41
+
42
+ def self.open_aws
43
+ unless AWS::S3::Base.connected?
44
+ AWS::S3::Base.establish_connection!(
45
+ :access_key_id => S3CorsFileupload::Config.access_key_id,
46
+ :secret_access_key => S3CorsFileupload::Config.secret_access_key
47
+ )
48
+ end
49
+ return AWS::S3::Base.connected?
50
+ end
51
+ #---- end S3 related methods -----
52
+
53
+ end
@@ -0,0 +1,29 @@
1
+ <!-- The template to display files available for download -->
2
+ <script id="template-download" type="text/x-tmpl">
3
+ {% for (var i=0, file; file=o.files[i]; i++) { %}
4
+ <tr class="template-download fade">
5
+ {% if (file.error) { %}
6
+ <td></td>
7
+ <td class="name"><span>{%=file.name%}</span></td>
8
+ <td class="size"><span>{%=o.formatFileSize(file.size)%}</span></td>
9
+ <td class="error" colspan="2"><span class="label label-important">Error</span> {%=file.error%}</td>
10
+ {% } else { %}
11
+ <td class="preview">{% if (file.thumbnail_url) { %}
12
+ <a href="{%=file.url%}" title="{%=file.name%}" rel="gallery" download="{%=file.name%}"><img src="{%=file.thumbnail_url%}"></a>
13
+ {% } %}</td>
14
+ <td class="name">
15
+ <a href="{%=file.url%}" title="{%=file.name%}" rel="{%=file.thumbnail_url&&'gallery'%}" download="{%=file.name%}">{%=file.name%}</a>
16
+ </td>
17
+ <td class="size"><span>{%=o.formatFileSize(file.size)%}</span></td>
18
+ <td colspan="2"></td>
19
+ {% } %}
20
+ <td class="delete">
21
+ <button class="btn btn-danger" data-type="{%=file.delete_type%}" data-url="{%=file.delete_url%}">
22
+ <i class="icon-trash icon-white"></i>
23
+ <span>Delete</span>
24
+ </button>
25
+ <input type="checkbox" name="delete" value="1">
26
+ </td>
27
+ </tr>
28
+ {% } %}
29
+ </script>
@@ -0,0 +1,31 @@
1
+ <!-- The template to display files available for upload -->
2
+ <script id="template-upload" type="text/x-tmpl">
3
+ {% for (var i=0, file; file=o.files[i]; i++) { %}
4
+ <tr class="template-upload fade">
5
+ <td class="preview"><span class="fade"></span></td>
6
+ <td class="name"><span>{%=file.name%}</span></td>
7
+ <td class="size"><span>{%=o.formatFileSize(file.size)%}</span></td>
8
+ {% if (file.error) { %}
9
+ <td class="error" colspan="2"><span class="label label-important">Error</span> {%=file.error%}</td>
10
+ {% } else if (o.files.valid && !i) { %}
11
+ <td>
12
+ <div class="progress progress-success progress-striped active"><div class="bar" style="width:0%;"></div></div>
13
+ </td>
14
+ <td class="start">{% if (!o.options.autoUpload) { %}
15
+ <button class="btn btn-primary">
16
+ <i class="icon-upload icon-white"></i>
17
+ <span>Start</span>
18
+ </button>
19
+ {% } %}</td>
20
+ {% } else { %}
21
+ <td colspan="2"></td>
22
+ {% } %}
23
+ <td class="cancel">{% if (!i) { %}
24
+ <button class="btn btn-warning">
25
+ <i class="icon-ban-circle icon-white"></i>
26
+ <span>Cancel</span>
27
+ </button>
28
+ {% } %}</td>
29
+ </tr>
30
+ {% } %}
31
+ </script>
@@ -0,0 +1,25 @@
1
+ <!-- The template to display files that have been uploaded to S3 already -->
2
+ <!-- This essentially is a stand-in for the template_download partial for the s3_cors_fileupload gem's purposes -->
3
+ <script id="template-uploaded" type="text/x-tmpl">
4
+ <tr class="template-uploaded" id="source_file_{%=o.id%}">
5
+ <td class="preview">
6
+ {% if (o.image == true) { %}
7
+ <a href="{%=o.url%}" title="{%=o.name%}" rel="gallery" download="{%=o.name%}">
8
+ <image src="{%=o.url%}", style='width:80px; height:56px;'></image>
9
+ </a>
10
+ {% } %}
11
+ </td>
12
+ <td class="name">
13
+ <a href="{%=o.url%}" title="{%=o.name%}" rel="{%=o.thumbnail_url&&'gallery'%}" download="{%=o.name%}">{%=o.name%}</a>
14
+ </td>
15
+ <td class="size"><span>{%=formatFileSize(o.size)%}</span></td>
16
+ <td colspan="2"></td>
17
+ <td class="delete">
18
+ <button class="btn btn-danger" data-type="DELETE" data-url="{%=o.delete_url%}">
19
+ <i class="icon-trash icon-white"></i>
20
+ <span>Delete</span>
21
+ </button>
22
+ <input type="checkbox" name="delete" value="1">
23
+ </td>
24
+ </tr>
25
+ </script>
@@ -0,0 +1,43 @@
1
+ <%= stylesheet_link_tag '//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.1/css/bootstrap-combined.min.css' %>
2
+
3
+ <div style="padding: 10px;">
4
+ <h2>Upload file</h2>
5
+ <%= s3_cors_fileupload_form :id => 'fileupload' %>
6
+
7
+ <div class="javascript-templates">
8
+ <%= render 'template_upload' %>
9
+ <%= render 'template_uploaded' %>
10
+ <%# render 'template_download' %>
11
+ </div>
12
+ </div>
13
+
14
+ <script>
15
+ //<![CDATA[
16
+ $(function () {
17
+ // Initialize the jQuery File Upload widget:
18
+ $('#fileupload').fileupload({
19
+ dataType: 'xml',
20
+ sequentialUploads: true,
21
+ downloadTemplateId: null,
22
+ downloadTemplate: null
23
+ });
24
+
25
+ // Load existing files:
26
+ $.getJSON('/source_files', function (files) {
27
+ $.each(files, function(index, value) {
28
+ $('#upload_files tbody').append(tmpl('template-uploaded', value));
29
+ });
30
+ });
31
+ });
32
+
33
+ // used by the jQuery File Upload
34
+ var fileUploadErrors = {
35
+ maxFileSize: 'File is too big',
36
+ minFileSize: 'File is too small',
37
+ acceptFileTypes: 'Filetype not allowed',
38
+ maxNumberOfFiles: 'Max number of files exceeded',
39
+ uploadedBytes: 'Uploaded bytes exceed file size',
40
+ emptyResult: 'Empty file upload result'
41
+ };
42
+ //]]>
43
+ </script>
@@ -0,0 +1,2 @@
1
+ require 's3_cors_fileupload/version'
2
+ require 's3_cors_fileupload/rails'
@@ -0,0 +1,8 @@
1
+ if defined?(::Rails)
2
+ require 's3_cors_fileupload/rails/config'
3
+ require 's3_cors_fileupload/rails/engine' if ::Rails.version >= '3.1'
4
+ require 's3_cors_fileupload/rails/policy_helper'
5
+ require 's3_cors_fileupload/rails/form_helper'
6
+
7
+ ActionView::Base.send(:include, S3CorsFileupload::FormHelper) if defined?(ActionView::Base)
8
+ end
@@ -0,0 +1,27 @@
1
+ module S3CorsFileupload
2
+ module Config
3
+ require 'yaml'
4
+
5
+ # this allows us to lazily instantiate the configuration by reading it in when it needs to be accessed
6
+ class << self
7
+ # if a method is called on the class, attempt to look it up in the config array
8
+ def method_missing(meth, *args, &block)
9
+ if args.empty? && block.nil?
10
+ config[meth.to_s]
11
+ else
12
+ super
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def config
19
+ @config ||= YAML.load_file(File.join(::Rails.root, 'config', 'amazon_s3.yml'))[::Rails.env]
20
+ rescue
21
+ warn('WARNING: s3_cors_fileupload gem was unable to locate a configuration file in config/amazon_s3.yml and may not ' +
22
+ 'be able to function properly. Please run `rails generate s3_cors_upload:install` before proceeding.')
23
+ {}
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module S3CorsFileupload
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,91 @@
1
+ module S3CorsFileupload
2
+ module FormHelper
3
+ # Options:
4
+ # :access_key_id The AWS Access Key ID of the owner of the bucket.
5
+ # Defaults to the `Config.access_key_id` (read from the yaml config file).
6
+ #
7
+ # :acl One of S3's Canned Access Control Lists, must be one of:
8
+ # 'private', 'public-read', 'public-read-write', 'authenticated-read'.
9
+ # Defaults to `'public-read'`.
10
+ #
11
+ # :max_file_size The max file size (in bytes) that you wish to allow to be uploaded.
12
+ # Defaults to `Config.max_file_size` or, if no value is set on the yaml file `524288000` (500 MB)
13
+ #
14
+ # :bucket The name of the bucket on S3 you wish for the files to be uploaded to.
15
+ # Defaults to `Config.bucket` (read from the yaml config file).
16
+ #
17
+ # Any other key creates standard HTML options for the form tag.
18
+ def s3_cors_fileupload_form(options = {}, &block)
19
+ policy_helper = PolicyHelper.new(options)
20
+ # initialize the hidden form fields
21
+ hidden_form_fields = {
22
+ :key => '',
23
+ 'Content-Type' => '',
24
+ 'AWSAccessKeyId' => options[:access_key_id] || Config.access_key_id,
25
+ :acl => policy_helper.options[:acl],
26
+ :policy => policy_helper.policy_document,
27
+ :signature => policy_helper.upload_signature,
28
+ :success_action_status => '201'
29
+ }
30
+ # assume that all of the non-documented keys are
31
+ _html_options = options.reject { |key, val| [:access_key_id, :acl, :max_file_size, :bucket].include?(key) }
32
+ # return the form html
33
+ construct_form_html(hidden_form_fields, policy_helper.options[:bucket], _html_options, &block)
34
+ end
35
+
36
+ private
37
+
38
+ def build_form_options(options = {})
39
+ { :id => 'fileupload' }.merge(options).merge(:multipart => true, :authenticity_token => false)
40
+ end
41
+
42
+ # hidden fields argument should be a hash of key value pairs (values may be blank if desired)
43
+ def construct_form_html(hidden_fields, bucket, html_options = {}, &block)
44
+ # now build the html for the form
45
+ form_text = form_tag("https://#{bucket}.s3.amazonaws.com", build_form_options(html_options)) do
46
+ hidden_fields.map do |name, value|
47
+ hidden_field_tag(name, value)
48
+ end.join.html_safe + "
49
+ <!-- The fileupload-buttonbar contains buttons to add/delete files and start/cancel the upload -->
50
+ <div class='row fileupload-buttonbar'>
51
+ <div class='span7'>
52
+ <button class='btn btn-success fileinput-button'>
53
+ <i class='icon-plus icon-white'></i>
54
+ <span>Add files...</span>
55
+ ".html_safe +
56
+ file_field_tag(:file, :multiple => true) + "
57
+ </button>
58
+ <button class='btn btn-primary start' type='submit'>
59
+ <i class='icon-upload icon-white'></i>
60
+ <span>Start upload</span>
61
+ </button>
62
+ <button class='btn btn-warning cancel' type='reset'>
63
+ <i class='icon-ban-circle icon-white'></i>
64
+ <span>Cancel upload</span>
65
+ </button>
66
+ <button class='btn btn-danger delete' type='button'>
67
+ <i class='icon-trash icon-white'></i>
68
+ <span>Delete</span>
69
+ </button>
70
+ <input class='toggle' type='checkbox'></input>
71
+ </div>
72
+ <div class='span5'>
73
+ <!-- The global progress bar -->
74
+ <div class='progress progress-success progress-striped active fade'>
75
+ <div class='bar' style='width: 0%'></div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ <!-- The loading indicator is shown during image processing -->
80
+ <div class='fileupload-loading'></div>
81
+ <br>
82
+ <!-- The table listing the files available for upload/download -->
83
+ <table class='table table-striped' id='upload_files'>
84
+ <tbody class='files' data-target='#modal-gallery' data-toggle='modal-gallery'></tbody>
85
+ </table>".html_safe
86
+ end
87
+ form_text += capture(&block) if block
88
+ form_text
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,48 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'digest/sha1'
4
+ require 'json'
5
+
6
+ module S3CorsFileupload
7
+ class PolicyHelper
8
+ attr_reader :options
9
+
10
+ def initialize(_options = {})
11
+ # default max_file_size to 500 MB if nothing is received
12
+ @options = {
13
+ :acl => 'public-read',
14
+ :max_file_size => Config.max_file_size || 524288000,
15
+ :bucket => Config.bucket
16
+ }.merge(_options).merge(:secret_access_key => Config.secret_access_key)
17
+ end
18
+
19
+ # generate the policy document that amazon is expecting.
20
+ def policy_document
21
+ Base64.encode64(
22
+ {
23
+ expiration: 1.hour.from_now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
24
+ conditions: [
25
+ { bucket: options[:bucket] },
26
+ { acl: options[:acl] },
27
+ { success_action_status: '201' },
28
+ ["content-length-range", 0, options[:max_file_size]],
29
+ ["starts-with", "$utf8", ""],
30
+ ["starts-with", "$key", ""],
31
+ ["starts-with", "$Content-Type", ""]
32
+ ]
33
+ }.to_json
34
+ ).gsub(/\n|\r/, '')
35
+ end
36
+
37
+ # sign our request by Base64 encoding the policy document.
38
+ def upload_signature
39
+ Base64.encode64(
40
+ OpenSSL::HMAC.digest(
41
+ OpenSSL::Digest::Digest.new('sha1'),
42
+ options[:secret_access_key],
43
+ self.policy_document
44
+ )
45
+ ).gsub(/\n/, '')
46
+ end
47
+ end
48
+ end