├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app └── assets │ ├── javascripts │ └── qiniu_direct_uploader.js.coffee │ └── stylesheets │ └── qiniu_direct_uploader.css.scss ├── coffeelint.json ├── lib ├── qiniu_direct_uploader.rb └── qiniu_direct_uploader │ ├── engine.rb │ ├── form_helper.rb │ ├── uploader.rb │ └── version.rb └── qiniu_direct_uploader.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.DS_Store 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGE LOG 2 | 3 | ### v0.0.8 4 | 5 | Fix https://github.com/huobazi/qiniu_direct_uploader/issues/5 6 | 7 | ### v0.0.7 8 | 9 | https://github.com/huobazi/qiniu_direct_uploader/pull/4 10 | 11 | ### v0.0.6 12 | 13 | https://github.com/huobazi/qiniu_direct_uploader/pull/2 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in qiniu_direct_uploader.gemspec 4 | gemspec 5 | 6 | gem 'jquery-fileupload-rails' 7 | gem 'activesupport' 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Marble Wu 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiniu Direct Uploader 2 | 3 | [![Gem Version](https://badge.fury.io/rb/qiniu_direct_uploader@2x.png?0.0.8)](http://badge.fury.io/rb/qiniu_direct_uploader) 4 | 5 | This Gem can direct upload your files to a Qiniu storage bucket. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | gem 'qiniu_direct_uploader' 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install qiniu_direct_uploader 20 | 21 | ## Usage 22 | 23 | see the example project: https://github.com/huobazi/qiniu-direct-upload-example 24 | 25 | ### Views 26 | ```erb 27 | <%= qiniu_uploader_form callback_url: items_path, 28 | bucket: 'spec-test', 29 | id: "photograph-uploader", 30 | save_key: "uploads/items/$(year)/$(mon)/$(day)/$(etag)/$(fname)", 31 | custom_fields: {aaa:1,bbb:2}, 32 | progress_bar_id: 'progress-bar', 33 | drop_paste_zone_id: 'dropzone' do %> 34 | 35 |
36 | <%= file_field_tag :file, multiple: true, accept: "image/gif, image/jpeg" %> You can also drag and drop files here 37 |
38 | 39 | <% end %> 40 | ``` 41 | 42 | see the save_key settings: http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#magicvar 43 | 44 | ### Javascript 45 | ```coffee 46 | $(document).ready -> 47 | photoForm = $("form#photograph-uploader") 48 | if photoForm.length > 0 49 | photoForm.QiniuUploader 50 | # see also https://github.com/blueimp/jQuery-File-Upload/wiki/Options 51 | autoUpload: true 52 | singleFileUploads: false 53 | limitMultiFileUploads: 2 54 | customCallbackData: {"xyz": 100} 55 | onFilesAdd: (file) -> 56 | if file.type != "image/jpeg" 57 | alert('please select image') 58 | return false 59 | else 60 | return true 61 | 62 | photoForm.bind "ajax:success", (e, data) -> 63 | console.log('success') 64 | console.log(data) 65 | 66 | photoForm.bind "ajax:failure", (e, data) -> 67 | console.log('failure') 68 | console.log(data) 69 | ``` 70 | 71 | see also: 72 | 73 | 1. http://docs.qiniu.com/api/v6/put.html#upload-without-callback 74 | 2. http://docs.qiniu.com/api/v6/put.html#upload-api 75 | 3. http://docs.qiniu.com/api/v6/put.html#uploadToken-returnBody 76 | 77 | ## Contributing 78 | 79 | 1. Fork it 80 | 2. Create your feature branch (`git checkout -b my-new-feature`) 81 | 3. Commit your changes (`git commit -am 'Add some feature'`) 82 | 4. Push to the branch (`git push origin my-new-feature`) 83 | 5. Create new Pull Request 84 | 85 | 86 | ## Contributors 87 | 88 | See the [Contributors List](https://github.com/huobazi/qiniu_direct_uploader/graphs/contributors). 89 | 90 | ## CHANGE LOG 91 | 92 | See the [CHANGELOGS.md](https://github.com/huobazi/qiniu_direct_uploader/blob/master/CHANGELOG.md). 93 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/qiniu_direct_uploader.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ 4 | 5 | #= require jquery-fileupload/basic 6 | #= require jquery-fileupload/vendor/tmpl 7 | 8 | $ = jQuery 9 | 10 | $.fn.QiniuUploader = (options) -> 11 | 12 | # support multiple elements 13 | if @length > 1 14 | @each -> 15 | $(this).QiniuUploader options 16 | 17 | return this 18 | 19 | $uploadForm = this 20 | 21 | settings = 22 | customCallbackData: undefined 23 | onFilesAdd: undefined 24 | removeProgressBarWhenCompleted: true 25 | removeProgressBarWhenFailed: false 26 | progressBarId: undefined 27 | buttonId: undefined 28 | dropPasteZoneId: undefined 29 | allowMultipleFiles: true 30 | 31 | $.extend settings, options 32 | 33 | submitButtonId = $uploadForm.data('submit-button-id') 34 | progressBarId = $uploadForm.data('progress-bar-id') 35 | dropPasteZoneId= $uploadForm.data('drop-paste-zone-id') 36 | 37 | submitButton = $('#' + submitButtonId) if submitButtonId 38 | progressBar = $('#' + progressBarId) if progressBarId 39 | dropPasteZone = if dropPasteZoneId then $('#' + dropPasteZoneId) else $(document) 40 | 41 | currentFiles = [] 42 | formsForSubmit = [] 43 | 44 | if submitButton and submitButton.length > 0 45 | submitButton.click -> 46 | form.submit() for form in formsForSubmit 47 | false 48 | 49 | setUploadForm = -> 50 | inner_settings = 51 | dropZone: dropPasteZone 52 | pasteZone: dropPasteZone 53 | add: (e, data) -> 54 | file = data.files[0] 55 | 56 | unless settings.onFilesAdd and not settings.onFilesAdd(file) 57 | currentFiles.push data 58 | if $('#template-upload').length > 0 59 | data.context = $($.trim(tmpl("template-upload", file))) 60 | $(data.context).appendTo(progressBar || $uploadForm) 61 | else if !settings.allowMultipleFiles 62 | data.context = progressBar 63 | if submitButton and submitButton.length > 0 64 | if settings.allowMultipleFiles 65 | formsForSubmit.push data 66 | else 67 | formsForSubmit = [data] 68 | else 69 | data.submit() 70 | 71 | start: (e) -> 72 | $uploadForm.trigger("qiniu_upload_start", [e]) 73 | 74 | progress: (e, data) -> 75 | if data.context 76 | progress = parseInt(data.loaded / data.total * 100, 10) 77 | data.context.find('.bar').css('width', progress + '%') 78 | 79 | done: (e, data) -> 80 | postData = buildCallbackData $uploadForm, data.files[0], data.result 81 | callbackUrl = $uploadForm.data('callback-url') 82 | if callbackUrl 83 | $.ajax 84 | type: $uploadForm.data('callback-method') 85 | url: callbackUrl 86 | data: postData 87 | beforeSend: ( xhr, settings ) -> $uploadForm.trigger( 'ajax:beforeSend', [xhr, settings] ) 88 | complete: ( xhr, status ) -> $uploadForm.trigger( 'ajax:complete', [xhr, status] ) 89 | success: ( data, status, xhr ) -> $uploadForm.trigger( 'ajax:success', [data, status, xhr] ) 90 | error: ( xhr, status, error ) -> $uploadForm.trigger( 'ajax:error', [xhr, status, error] ) 91 | 92 | data.context.remove() if data.context && settings.removeProgressBarWhenCompleted # remove progress bar 93 | $uploadForm.trigger("qiniu_upload_complete", [postData]) 94 | 95 | currentFiles.splice($.inArray(data, currentFiles), 1) # remove that element from the array 96 | $uploadForm.trigger("qiniu_uploads_complete", [postData]) unless currentFiles.length 97 | 98 | fail: (e, data) -> 99 | content = buildCallbackData $uploadForm, data.files[0], data.result 100 | content.errorThrown = data.errorThrown 101 | 102 | data.context.remove() if data.context && settings.removeProgressBarWhenFailed # remove progress bar 103 | $uploadForm.trigger("qiniu_upload_failed", [content]) 104 | 105 | formData: (form) -> 106 | data = form.serializeArray() 107 | 108 | key = $uploadForm.data("key") 109 | 110 | # substitute upload timestamp and uniqueId into key 111 | keyField = $.grep data, (n) -> 112 | n if n.name == "key" 113 | 114 | if keyField.length > 0 115 | keyField[0].value = key 116 | 117 | # IE <= 9 doesn't have XHR2 hence it can't use formData 118 | # replace 'key' field to submit form 119 | unless 'FormData' of window 120 | $uploadForm.find("input[name='key']").val(key) 121 | data 122 | 123 | $uploadForm.fileupload $.extend true, {}, settings, inner_settings 124 | 125 | buildCallbackData = ($uploadForm, file, result) -> 126 | content = {} 127 | content = $.extend content, result if result 128 | content = $.extend content, settings.customCallbackData if settings.customCallbackData 129 | content 130 | 131 | #public methods 132 | @initialize = -> 133 | # Save key for IE9 Fix 134 | $uploadForm.data("key", $uploadForm.find("input[name='key']").val()) 135 | setUploadForm() 136 | this 137 | 138 | @customCallbackData = (newData) -> 139 | settings.customCallbackData = newData 140 | 141 | @initialize() 142 | -------------------------------------------------------------------------------- /app/assets/stylesheets/qiniu_direct_uploader.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the items controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | .upload { 5 | border-top: solid 1px #CCC; 6 | width: 400px; 7 | padding-top: 10px; 8 | margin-top: 10px; 9 | 10 | .progress { 11 | margin-top: 8px; 12 | border: solid 1px #555; 13 | border-radius: 3px; 14 | -moz-border-radius: 3px; 15 | .bar { 16 | height: 10px; 17 | background: #3EC144; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "ignore" 4 | }, 5 | "braces_spacing": { 6 | "level": "ignore", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "ignore", 18 | "spacing": { 19 | "left": 0, 20 | "right": 0 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "level": "ignore", 25 | "value": 10 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "ignore" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "eol_last": { 37 | "level": "ignore" 38 | }, 39 | "indentation": { 40 | "value": 2, 41 | "level": "error" 42 | }, 43 | "line_endings": { 44 | "level": "ignore", 45 | "value": "unix" 46 | }, 47 | "max_line_length": { 48 | "value": 113, 49 | "level": "warn", 50 | "limitComments": true 51 | }, 52 | "missing_fat_arrows": { 53 | "level": "ignore", 54 | "is_strict": false 55 | }, 56 | "newlines_after_classes": { 57 | "value": 3, 58 | "level": "ignore" 59 | }, 60 | "no_backticks": { 61 | "level": "error" 62 | }, 63 | "no_debugger": { 64 | "level": "warn", 65 | "console": false 66 | }, 67 | "no_empty_functions": { 68 | "level": "ignore" 69 | }, 70 | "no_empty_param_list": { 71 | "level": "ignore" 72 | }, 73 | "no_implicit_braces": { 74 | "level": "ignore", 75 | "strict": true 76 | }, 77 | "no_implicit_parens": { 78 | "level": "ignore", 79 | "strict": true 80 | }, 81 | "no_interpolation_in_single_quotes": { 82 | "level": "ignore" 83 | }, 84 | "no_nested_string_interpolation": { 85 | "level": "warn" 86 | }, 87 | "no_plusplus": { 88 | "level": "ignore" 89 | }, 90 | "no_private_function_fat_arrows": { 91 | "level": "warn" 92 | }, 93 | "no_stand_alone_at": { 94 | "level": "ignore" 95 | }, 96 | "no_tabs": { 97 | "level": "error" 98 | }, 99 | "no_this": { 100 | "level": "ignore" 101 | }, 102 | "no_throwing_strings": { 103 | "level": "error" 104 | }, 105 | "no_trailing_semicolons": { 106 | "level": "error" 107 | }, 108 | "no_trailing_whitespace": { 109 | "level": "error", 110 | "allowed_in_comments": false, 111 | "allowed_in_empty_lines": true 112 | }, 113 | "no_unnecessary_double_quotes": { 114 | "level": "ignore" 115 | }, 116 | "no_unnecessary_fat_arrows": { 117 | "level": "warn" 118 | }, 119 | "non_empty_constructor_needs_parens": { 120 | "level": "ignore" 121 | }, 122 | "prefer_english_operator": { 123 | "level": "ignore", 124 | "doubleNotLevel": "ignore" 125 | }, 126 | "space_operators": { 127 | "level": "ignore" 128 | }, 129 | "spacing_after_comma": { 130 | "level": "ignore" 131 | }, 132 | "transform_messes_up_line_numbers": { 133 | "level": "warn" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/qiniu_direct_uploader.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'jquery-fileupload-rails' if defined?(Rails) 4 | 5 | require "qiniu_direct_uploader/version" 6 | require "qiniu_direct_uploader/uploader" 7 | require "qiniu_direct_uploader/form_helper" 8 | 9 | require 'qiniu_direct_uploader/engine' if defined?(Rails) 10 | 11 | ActionView::Base.send(:include, QiniuDirectUploader::FormHelper) if defined?(ActionView::Base) 12 | -------------------------------------------------------------------------------- /lib/qiniu_direct_uploader/engine.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module QiniuDirectUploader 3 | class Engine < ::Rails::Engine 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/qiniu_direct_uploader/form_helper.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module QiniuDirectUploader 3 | module FormHelper 4 | def qiniu_uploader_form(options = {}, &block) 5 | uploader = Uploader.new(options) 6 | form_tag(uploader.action, uploader.form_options) do 7 | all_hidden_fields = {} 8 | all_hidden_fields = all_hidden_fields.merge uploader.fields 9 | 10 | custom_hidden_fields = {} 11 | uploader.custom_fields.each do |key,value| 12 | custom_hidden_fields["x:#{key}"] = value 13 | end 14 | 15 | all_hidden_fields = all_hidden_fields.reverse_merge custom_hidden_fields 16 | #all_hidden_fields = all_hidden_fields.reverse_merge({:ooo=>uploader.return_body}) 17 | 18 | all_hidden_fields.map do |name, value| 19 | hidden_field_tag(name, value) 20 | end.join.html_safe + capture(&block) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/qiniu_direct_uploader/uploader.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module QiniuDirectUploader 3 | class Uploader 4 | def initialize(options) 5 | @options = options.reverse_merge( 6 | expires_in: 360, 7 | ssl: false, 8 | custom_fields: {}, 9 | submit_button_id: nil, 10 | progress_bar_id: nil, 11 | drop_paste_zone_id: nil, 12 | callback_method: "POST" 13 | ) 14 | end 15 | 16 | def form_options 17 | { 18 | id: @options[:id], 19 | class: @options[:class], 20 | method: "post", 21 | authenticity_token: false, 22 | multipart: true, 23 | data: { 24 | callback_url: @options[:callback_url], 25 | callback_method: @options[:callback_method], 26 | submit_button_id: @options[:submit_button_id], 27 | drop_paste_zone_id: @options[:drop_paste_zone_id], 28 | progress_bar_id: @options[:progress_bar_id] 29 | }.reverse_merge(@options[:data] || {}) 30 | } 31 | end 32 | 33 | def fields 34 | the_fields = {} 35 | the_fields[:token] = @options[:token] || token 36 | the_fields[:key] = @options[:key] if @options[:key] 37 | the_fields 38 | end 39 | 40 | def custom_fields 41 | @options[:custom_fields] 42 | end 43 | 44 | def save_key 45 | return @options[:save_key] if @options[:save_key] 46 | "uploads/$(year)/$(mon)/$(day)/$(etag)/$(fname)" 47 | end 48 | 49 | def action 50 | @options[:action] || default_upload_url 51 | end 52 | 53 | def return_body 54 | fields_array = [] 55 | fields_array.push '"etag": $(etag)' 56 | fields_array.push '"fname": $(fname)' 57 | fields_array.push '"fsize": $(fsize)' 58 | fields_array.push '"mimeType": $(mimeType)' 59 | fields_array.push '"imageInfo": $(imageInfo)' 60 | fields_array.push '"exif": $(exif)' 61 | fields_array.push '"endUser": $(endUser)' 62 | fields_array.push '"key": $(key)' 63 | 64 | custom_fields_array = [] 65 | @options[:custom_fields].each do |k,v| 66 | custom_fields_array.push '"' + k.to_s + '": $(x:'+ k.to_s + ')' 67 | end 68 | custom_fields_json = '"custom_fields": {' + custom_fields_array.join(',') + '}' 69 | 70 | fields_array.push custom_fields_json 71 | 72 | '{'+ fields_array.join(',') +'}' 73 | end 74 | 75 | def token 76 | put_policy = Qiniu::Auth::PutPolicy.new( @options[:bucket], @options[:key], @options[:expires_in], nil ) 77 | put_policy.return_body = return_body 78 | put_policy.save_key = save_key 79 | put_policy.end_user = @options[:customer] if @options[:customer] 80 | 81 | Qiniu::Auth.generate_uptoken(put_policy) 82 | end 83 | 84 | private 85 | 86 | def default_upload_url 87 | if @options[:ssl] 88 | "https://up.qbox.me" 89 | else 90 | "http://up.qiniu.com" 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/qiniu_direct_uploader/version.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | module QiniuDirectUploader 3 | VERSION = "0.0.9" 4 | end 5 | -------------------------------------------------------------------------------- /qiniu_direct_uploader.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'qiniu_direct_uploader/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "qiniu_direct_uploader" 8 | s.version = QiniuDirectUploader::VERSION 9 | s.authors = ["Marble Wu"] 10 | s.email = ["huobazi@gmail.com"] 11 | s.description = %q{Direct upload to a Qiniu storage bucket.} 12 | s.summary = %q{Direct upload to a Qiniu storage bucket.} 13 | s.homepage = "https://github.com/huobazi/qiniu_direct_uploader" 14 | s.license = "MIT" 15 | 16 | s.files = `git ls-files`.split($/) 17 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 19 | s.require_paths = ["lib"] 20 | 21 | s.add_dependency 'rails', '>= 3.2' 22 | s.add_dependency 'coffee-rails', '>= 3.2.1' 23 | s.add_dependency 'sass-rails', '>= 3.2.5' 24 | 25 | s.add_dependency 'qiniu', '~> 6.6' 26 | 27 | s.add_dependency 'jquery-fileupload-rails', "~> 0.4.7" 28 | 29 | s.add_development_dependency "bundler", "~> 1.3" 30 | s.add_development_dependency "rake" 31 | end 32 | --------------------------------------------------------------------------------