s3_cors_fileupload 0.1.1

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