s3ff 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +107 -0
- data/app/assets/javascripts/s3ff.js +76 -0
- data/lib/s3ff/engine.rb +4 -0
- data/lib/s3ff/railtie.rb +6 -0
- data/lib/s3ff/version.rb +3 -0
- data/lib/s3ff/view_helper.rb +46 -0
- data/lib/s3ff.rb +32 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: aa68843504870dde78c2146cbd742ec26a9c5826
|
4
|
+
data.tar.gz: a90a1ceb009cc4488aef06886bdac71806667a62
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dca77539e239ef3ca2c666861f75a9adac27945d461615f6bb7132bffe77aa8faf5a0ceb7c60e565edb127b9a41fced54580e97d65dc13231194a6ee6741070b
|
7
|
+
data.tar.gz: 60e7ed6509d3a9ef284c754df7380cf7b4b3f6955435790bff7c78b5fbd1cb2558dfc551d3823cef0507208cc163005aaf55af3d7e2dbc32cdfb1330563f8e1e
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# S3FF
|
2
|
+
|
3
|
+
Using `s3_file_field` with `paperclip`
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
### 1. Add javascript
|
8
|
+
|
9
|
+
in your application.js
|
10
|
+
|
11
|
+
```
|
12
|
+
//= require s3ff
|
13
|
+
```
|
14
|
+
|
15
|
+
### 2. Change `file_field` input to use `s3_file_field`
|
16
|
+
|
17
|
+
```
|
18
|
+
= form_for :user do |f|
|
19
|
+
= f.s3_file_field :avatar
|
20
|
+
```
|
21
|
+
|
22
|
+
or if you're using `simple_form`
|
23
|
+
|
24
|
+
```
|
25
|
+
= simple_form_for :user do |f|
|
26
|
+
= f.input :avatar do
|
27
|
+
= f.s3_file_field :avatar, :class => "form-control"
|
28
|
+
```
|
29
|
+
|
30
|
+
### 3. Add footer
|
31
|
+
|
32
|
+
```
|
33
|
+
= include_s3ff_templates
|
34
|
+
```
|
35
|
+
|
36
|
+
NOTE: Feel free to modify & render the templates manually, but keep the `s3ff_` prefixed CSS classes for our javascript to work properly.
|
37
|
+
|
38
|
+
## What will happen
|
39
|
+
|
40
|
+
To illustate, if you have a file field like this
|
41
|
+
|
42
|
+
```
|
43
|
+
<input type="file" name="user[avatar]">
|
44
|
+
```
|
45
|
+
|
46
|
+
When `s3ff` kicks in, it would upgrade the field to a `s3_file_field`. When your user chooses a file, it will be uploaded, with a progress indicator, directly into your S3 bucket (see `s3_file_field` gem for configuration). Your `form` will be disabled during the upload and re-enabled once upload completes. After this process, 4 new hidden form fields will be attached to your form:
|
47
|
+
|
48
|
+
```
|
49
|
+
<input type="file" name="user[avatar_direct_url]" value="https://....">
|
50
|
+
<input type="file" name="user[avatar_file_name]" value="face.png">
|
51
|
+
<input type="file" name="user[avatar_file_size]" value="162534">
|
52
|
+
<input type="file" name="user[avatar_content_type]" value="image/png">
|
53
|
+
```
|
54
|
+
|
55
|
+
## Code changes to your app
|
56
|
+
|
57
|
+
`s3ff` designed to minimize moving parts and code changes to your Rails app - all it does is give you 4 form fields in return for every direct s3 file upload that happened in your user's browser.
|
58
|
+
|
59
|
+
How you deal with these form fields are entirely up to you. Here's a simple way:
|
60
|
+
|
61
|
+
#### 1. Edit strong parameters
|
62
|
+
|
63
|
+
If your controller was specifying
|
64
|
+
|
65
|
+
```
|
66
|
+
params.require(:user).permit(:avatar)
|
67
|
+
```
|
68
|
+
|
69
|
+
It would need to be changed to accept the new form fields
|
70
|
+
|
71
|
+
```
|
72
|
+
params.require(:user).permit(:avatar,
|
73
|
+
:avatar_direct_url,
|
74
|
+
:avatar_file_name,
|
75
|
+
:avatar_file_size,
|
76
|
+
:avatar_content_type
|
77
|
+
)
|
78
|
+
```
|
79
|
+
|
80
|
+
#### 2. Upgrade model to also accept direct url
|
81
|
+
|
82
|
+
If your model was originally
|
83
|
+
|
84
|
+
```
|
85
|
+
class User < ActiveRecord::Base
|
86
|
+
has_attached_file :avatar
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Download the file from S3 when given `avatar_direct_url`. This leave all your existing Paperclip code and logic unchanged.
|
91
|
+
|
92
|
+
```
|
93
|
+
class User < ActiveRecord::Base
|
94
|
+
has_attached_file :avatar
|
95
|
+
|
96
|
+
attr_accessor :avatar_direct_url
|
97
|
+
def avatar_direct_url=(value)
|
98
|
+
self.avatar = open(value) if value.present?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
### License
|
106
|
+
|
107
|
+
This repository is MIT-licensed.
|
@@ -0,0 +1,76 @@
|
|
1
|
+
//= require s3_file_field
|
2
|
+
//= require jsrender.min
|
3
|
+
|
4
|
+
$(function() {
|
5
|
+
|
6
|
+
// adapted from http://www.html5rocks.com/en/tutorials/cors/
|
7
|
+
function iCanHasCORSRequest() {
|
8
|
+
var xhr = new XMLHttpRequest();
|
9
|
+
return (("withCredentials" in xhr) || (typeof XDomainRequest != "undefined"));
|
10
|
+
}
|
11
|
+
if (! iCanHasCORSRequest()) return;
|
12
|
+
|
13
|
+
var ready = function() {
|
14
|
+
var s3ff_init = function () {
|
15
|
+
if ($(this).hasClass('s3ff_enabled')) return;
|
16
|
+
|
17
|
+
var that = $(this).addClass('s3ff_enabled');
|
18
|
+
var multi = that.attr('multiple');
|
19
|
+
var label = that.wrap($.templates($('#s3ff_label').html()).render()).parents('.s3ff_label').attr('for', that.attr('id'));
|
20
|
+
var wrap = that.wrap('<div></div>').parent();
|
21
|
+
var section = function(file, selector) {
|
22
|
+
var upload = label.find("#upload-" + file.unique_id);
|
23
|
+
return (selector ? upload.find(selector) : upload);
|
24
|
+
};
|
25
|
+
|
26
|
+
var s3ff_handlers = {
|
27
|
+
change: function(e, data) {
|
28
|
+
if (! multi) label.children('.s3ff_section').remove();
|
29
|
+
},
|
30
|
+
always: function(e, data) {
|
31
|
+
wrap.parents("form").find("[type='submit']").each(function() {
|
32
|
+
this.uploading = this.uploading || 0;
|
33
|
+
$(this).attr({'disabled': (--this.uploading > 0)});
|
34
|
+
});
|
35
|
+
},
|
36
|
+
add: function(e, data) {
|
37
|
+
wrap.parents("form").find("[type='submit']").each(function() {
|
38
|
+
this.uploading = this.uploading || 0;
|
39
|
+
$(this).attr({'disabled': (++this.uploading > 0)});
|
40
|
+
});
|
41
|
+
data.submit();
|
42
|
+
},
|
43
|
+
send: function(e, data) {
|
44
|
+
label.append($.templates($('#s3ff_upload').html()).render(data.files[0]));
|
45
|
+
},
|
46
|
+
done: function(e, data) {
|
47
|
+
var upload = section(data.files[0]);
|
48
|
+
var field_prefix = that.attr('name').replace(/\]$/, '');
|
49
|
+
upload.append($.templates($('#s3ff_done').html()).render(data, { field_prefix: field_prefix }));
|
50
|
+
upload.find('.s3ff_progress').hide();
|
51
|
+
},
|
52
|
+
fail: function(e, data) {
|
53
|
+
section(data.files[0], '.s3ff_progress').html($.templates($('#s3ff_fail').html()).render(data));
|
54
|
+
},
|
55
|
+
progress: function(e, data) {
|
56
|
+
var pct = (parseInt(data.loaded / data.total * 100, 10)) + "%";
|
57
|
+
section(data.files[0], '.s3ff_progress .s3ff_bar').css({width: pct}).text(pct);
|
58
|
+
}
|
59
|
+
};
|
60
|
+
|
61
|
+
var data = that.data('s3ff');
|
62
|
+
if (data) {
|
63
|
+
s3ff_handlers.send.apply(this, [null, data]);
|
64
|
+
s3ff_handlers.done.apply(this, [null, data]);
|
65
|
+
}
|
66
|
+
|
67
|
+
that.S3FileField(s3ff_handlers);
|
68
|
+
};
|
69
|
+
|
70
|
+
var selector = "[data-acl][data-aws-access-key-id]:not(.s3ff_enabled)";
|
71
|
+
$(selector).each(s3ff_init);
|
72
|
+
$(document).on("mouseenter mousedown touchstart", selector, s3ff_init);
|
73
|
+
};
|
74
|
+
|
75
|
+
$(document).on('page:change', ready);
|
76
|
+
});
|
data/lib/s3ff/engine.rb
ADDED
data/lib/s3ff/railtie.rb
ADDED
data/lib/s3ff/version.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module S3FF
|
2
|
+
module ViewHelper
|
3
|
+
def include_s3ff_templates(map = { _direct_url: 'result.url', _file_name: 'result.filename', _file_size: 'result.filesize', _content_type: 'result.filetype' })
|
4
|
+
<<-EOM
|
5
|
+
<div style="display:none;">
|
6
|
+
<script id="s3ff_label" type="text/x-tmpl">
|
7
|
+
<label class="s3ff_label" style="display:block;"></label>
|
8
|
+
</script>
|
9
|
+
<script id="s3ff_upload" type="text/x-tmpl" data-jsv-tmpl="_0">
|
10
|
+
<div class="s3ff_section" id="upload-{{:unique_id}}" style="margin-top:1em;">
|
11
|
+
<div class="progress s3ff_progress">
|
12
|
+
<div class="progress-bar progress-bar-striped active s3ff_bar" style="width: 0%"></div>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
</script>
|
16
|
+
<script id="s3ff_fail" type="text/x-tmpl">
|
17
|
+
<div class="progress-bar progress-bar-danger" style="width: 80%">
|
18
|
+
<span class="fa fa-exclamation-triangle"></span>
|
19
|
+
{{if failReason}}
|
20
|
+
{{:failReason}}
|
21
|
+
{{else}}
|
22
|
+
Upload failed!
|
23
|
+
{{/if}}
|
24
|
+
</div>
|
25
|
+
</script>
|
26
|
+
<script id="s3ff_done" type="text/x-tmpl">
|
27
|
+
<button class="close" data-dismiss="alert" style="margin-right:4px;" type="button">
|
28
|
+
<span aria-hidden="true">×</span><span class="sr-only">Close</span>
|
29
|
+
</button>
|
30
|
+
<span class="form-control">
|
31
|
+
<span class="fa fa-file-o"></span>
|
32
|
+
{{:result.filename}}
|
33
|
+
{{if result.filesize}}
|
34
|
+
({{:result.filesize}} bytes)
|
35
|
+
{{/if}}
|
36
|
+
</span>
|
37
|
+
<div style="display:none;">
|
38
|
+
#{map.collect {|k,v| "<input name=\"{{>~field_prefix}}#{k}]\" type=\"hidden\" value=\"{{:#{v}}}\" />" }.join}
|
39
|
+
</div>
|
40
|
+
</script>
|
41
|
+
</div>
|
42
|
+
EOM
|
43
|
+
.html_safe
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/s3ff.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 's3_file_field'
|
2
|
+
require 's3ff/view_helper'
|
3
|
+
require 's3ff/railtie'
|
4
|
+
require 's3ff/engine'
|
5
|
+
|
6
|
+
ActionView::Base.send(:include, S3FF::ViewHelper)
|
7
|
+
|
8
|
+
S3FileField::FormBuilder.class_eval do
|
9
|
+
def s3_file_field_with_s3ff(method, options = {})
|
10
|
+
changes = @object.try(:changes) || {} # { attr => [old_value, new_value], ... }
|
11
|
+
if new_direct_url = changes["#{method}_direct_url"].try(:last)
|
12
|
+
# if *_direct_url_changed? it means we're re-rendering
|
13
|
+
# :. we should prepopulate the s3ff fields to avoid re-uploading
|
14
|
+
options[:data] ||= {}
|
15
|
+
options[:data].merge!({
|
16
|
+
s3ff: {
|
17
|
+
files: [{
|
18
|
+
unique_id: "#{@object_name.parameterize}#{SecureRandom.hex}",
|
19
|
+
}],
|
20
|
+
result: {
|
21
|
+
filename: changes["#{method}_file_name"].try(:last) || File.basename(new_direct_url),
|
22
|
+
filesize: changes["#{method}_file_size"].try(:last),
|
23
|
+
filetype: changes["#{method}_content_type"].try(:last),
|
24
|
+
url: new_direct_url,
|
25
|
+
}
|
26
|
+
}
|
27
|
+
})
|
28
|
+
end
|
29
|
+
s3_file_field_without_s3ff(method, options)
|
30
|
+
end
|
31
|
+
alias_method_chain :s3_file_field, :s3ff
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: s3ff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chew Choon Keat
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: s3_file_field
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.3.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.3.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Direct S3 upload using CORS with s3_file_field + paperclip
|
70
|
+
email:
|
71
|
+
- choonkeat@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- README.md
|
77
|
+
- app/assets/javascripts/s3ff.js
|
78
|
+
- lib/s3ff.rb
|
79
|
+
- lib/s3ff/engine.rb
|
80
|
+
- lib/s3ff/railtie.rb
|
81
|
+
- lib/s3ff/version.rb
|
82
|
+
- lib/s3ff/view_helper.rb
|
83
|
+
homepage: https://github.com/choonkeat/s3ff
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.2.2
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Direct S3 upload using CORS with s3_file_field + paperclip
|
107
|
+
test_files: []
|