attache_rails 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +144 -0
- data/Rakefile +1 -0
- data/app/assets/javascripts/attache.js +3 -0
- data/app/assets/javascripts/attache/bootstrap3.js +106 -0
- data/app/assets/javascripts/attache/bootstrap3.js.jsx +106 -0
- data/app/assets/javascripts/attache/cors_upload.js +85 -0
- data/app/assets/javascripts/attache/ujs.js +23 -0
- data/lib/attache_rails.rb +43 -0
- data/lib/attache_rails/engine.rb +5 -0
- data/lib/attache_rails/version.rb +3 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d1fa11ba11f4fc5b00b924fd2491a67aa27a94a8
|
4
|
+
data.tar.gz: 9f2648a031e25cfd750d6c8d22f1b3f49ad931bd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bbbea818903c87c0a1bcbbab938df1cd424326cabfeeda4529ab5b14d0e8c5aaa83acac38084974db12cea1b6071c6523ab9bca8c027495a94509ff5541963df
|
7
|
+
data.tar.gz: fc14a4292d0ebebb5f878c484c33bee445ea3b9dc31849ee4b41a17bdc2de8713e5669c24bdfdcd36555743134aa9fdbb1d165f348b5b12a0b10ed9ad252aa91
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 choonkeat
|
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,144 @@
|
|
1
|
+
# attache_rails
|
2
|
+
|
3
|
+
Ruby on Rails integration for [attache server](https://github.com/choonkeat/attache)
|
4
|
+
|
5
|
+
NOTE: You can learn how to use this gem by looking at the [changes made to our example app](https://github.com/choonkeat/attache-railsapp/commit/16cb1274dcce5be01b6c9d42ad60c30c106ad7f9) or follow the step by step instructions in this `README`
|
6
|
+
|
7
|
+
## Dependencies
|
8
|
+
|
9
|
+
[React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Install the attache_rails package from Rubygems:
|
14
|
+
|
15
|
+
``` bash
|
16
|
+
gem install attache_rails
|
17
|
+
```
|
18
|
+
|
19
|
+
Or add this to your `Gemfile`
|
20
|
+
|
21
|
+
``` ruby
|
22
|
+
gem "attache_rails"
|
23
|
+
```
|
24
|
+
|
25
|
+
Add the attache javascript to your `application.js`
|
26
|
+
|
27
|
+
``` javascript
|
28
|
+
//= require attache
|
29
|
+
```
|
30
|
+
|
31
|
+
Or you can include the various scripts yourself
|
32
|
+
|
33
|
+
``` javascript
|
34
|
+
//= require attache/cors_upload
|
35
|
+
//= require attache/bootstrap3
|
36
|
+
//= require attache/ujs
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Database
|
42
|
+
|
43
|
+
To use `attache`, you only need to store the `path`, given to you after you've uploaded a file. So if you have an existing model, you only need to add a string, varchar or text field
|
44
|
+
|
45
|
+
``` bash
|
46
|
+
rails generate migration AddPhotoPathToUsers photo_path:string
|
47
|
+
```
|
48
|
+
|
49
|
+
To assign **multiple** images to **one** model, you'd only need one text field
|
50
|
+
|
51
|
+
``` bash
|
52
|
+
rails generate migration AddPhotoPathToUsers photo_path:text
|
53
|
+
```
|
54
|
+
|
55
|
+
### Model
|
56
|
+
|
57
|
+
In your model, `serialize` the column
|
58
|
+
|
59
|
+
``` ruby
|
60
|
+
class User < ActiveRecord::Base
|
61
|
+
serialize :photo_path, JSON
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### New or Edit form
|
66
|
+
|
67
|
+
In your form, you would add some options to `file_field` using the `attache_options` helper method. For example, a regular file field may look like this:
|
68
|
+
|
69
|
+
``` slim
|
70
|
+
= f.file_field :photo_path
|
71
|
+
```
|
72
|
+
|
73
|
+
Change it to
|
74
|
+
|
75
|
+
``` slim
|
76
|
+
= f.file_field :photo_path, **attache_options('64x64#', f.object.photo_path)
|
77
|
+
```
|
78
|
+
|
79
|
+
Or if you're expecting multiple files uploaded, simply add `multiple: true`
|
80
|
+
|
81
|
+
``` slim
|
82
|
+
= f.file_field :photo_path, multiple: true, **attache_options('64x64#', f.object.photo_path)
|
83
|
+
```
|
84
|
+
|
85
|
+
NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form
|
86
|
+
|
87
|
+
### Strong Parameters
|
88
|
+
|
89
|
+
You'd need to permit the new field in your controller. For example, a strong parameters definition may look like this in your `Users` controller
|
90
|
+
|
91
|
+
``` ruby
|
92
|
+
def user_params
|
93
|
+
params.require(:user).permit(:name)
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
If you're only accepting a single file upload, change it to
|
98
|
+
|
99
|
+
``` ruby
|
100
|
+
def user_params
|
101
|
+
params.require(:user).permit(:name, :photo_path)
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
If you're accepting multiple file uploads via `multiple: true`, change it to
|
106
|
+
|
107
|
+
``` ruby
|
108
|
+
def user_params
|
109
|
+
params.require(:user).permit(:name, photo_path: [])
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
### Show
|
114
|
+
|
115
|
+
Use the `attache_urls` helper to obtain full urls for the values you've captured in your database.
|
116
|
+
|
117
|
+
``` slim
|
118
|
+
- attache_urls(@user.photo_path, '128x128#') do |url|
|
119
|
+
= image_tag(url)
|
120
|
+
```
|
121
|
+
|
122
|
+
Alternatively, you can get the list of urls and manipulate it however you want
|
123
|
+
|
124
|
+
``` slim
|
125
|
+
= image_tag attache_urls(@user.photo_path, '128x128#').sample
|
126
|
+
```
|
127
|
+
|
128
|
+
### Environment configs
|
129
|
+
|
130
|
+
`ATTACHE_UPLOAD_URL` points to the attache server upload url. e.g. `http://localhost:9292/upload`
|
131
|
+
|
132
|
+
`ATTACHE_DOWNLOAD_URL` points to url prefix for downloading the resized images, e.g. `http://cdn.lvh.me:9292/view`
|
133
|
+
|
134
|
+
`ATTACHE_UPLOAD_DURATION` refers to the number of seconds before a signed upload request is considered expired, e.g. `600`
|
135
|
+
|
136
|
+
`ATTACHE_SECRET_KEY` is the shared secret with the `attache` server. e.g. `t0psecr3t`
|
137
|
+
|
138
|
+
* If this variable is not set, then upload requests will not be signed & `ATTACHE_UPLOAD_DURATION` will be ignored
|
139
|
+
* If this variable is set, it must be the same value as `SECRET_KEY` is set on the `attache` server
|
140
|
+
|
141
|
+
|
142
|
+
# License
|
143
|
+
|
144
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,106 @@
|
|
1
|
+
var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
|
2
|
+
|
3
|
+
getInitialState: function() {
|
4
|
+
var files = {};
|
5
|
+
var array = ([].concat(JSON.parse(this.props['data-value'])));
|
6
|
+
$.each(array, function(uid, json) {
|
7
|
+
if (json) files[uid] = { path: json };
|
8
|
+
});
|
9
|
+
return {files: files};
|
10
|
+
},
|
11
|
+
|
12
|
+
onRemove: function(uid, e) {
|
13
|
+
delete this.state.files[uid];
|
14
|
+
this.setState(this.state);
|
15
|
+
e.preventDefault();
|
16
|
+
e.stopPropagation();
|
17
|
+
},
|
18
|
+
|
19
|
+
onChange: function() {
|
20
|
+
var file_element = this.getDOMNode().firstChild;
|
21
|
+
// user cancelled file chooser dialog. ignore
|
22
|
+
if (file_element.files.length == 0) return;
|
23
|
+
this.state.files = {};
|
24
|
+
this.setState(this.state);
|
25
|
+
// upload the file via CORS
|
26
|
+
var that = this;
|
27
|
+
new CORSUpload({
|
28
|
+
file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
|
29
|
+
onError: function(uid, status) { that.setFileValue(uid, { pctString: '90%', pctDesc: status, className: 'progress-bar progress-bar-danger' }); }
|
30
|
+
});
|
31
|
+
// we don't want the file binary to be uploaded in the main form
|
32
|
+
file_element.value = '';
|
33
|
+
},
|
34
|
+
|
35
|
+
setFileValue: function(key, value) {
|
36
|
+
this.state.files[key] = value;
|
37
|
+
this.setState(this.state);
|
38
|
+
},
|
39
|
+
|
40
|
+
render: function() {
|
41
|
+
var that = this;
|
42
|
+
var previews = [];
|
43
|
+
$.each(that.state.files, function(key, result) {
|
44
|
+
var json = JSON.stringify(result);
|
45
|
+
if (result.path) {
|
46
|
+
var parts = result.path.split('/');
|
47
|
+
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
48
|
+
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
49
|
+
}
|
50
|
+
previews.push(
|
51
|
+
React.createElement("div", {className: "thumbnail"},
|
52
|
+
React.createElement("input", {type: "hidden", name: that.props.name, value: result.path, readOnly: "true"}),
|
53
|
+
React.createElement(AttacheFilePreview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
|
54
|
+
)
|
55
|
+
);
|
56
|
+
});
|
57
|
+
return (
|
58
|
+
React.createElement("label", {htmlFor: this.props.id, className: "attache-file-selector"},
|
59
|
+
React.createElement("input", React.__spread({type: "file"}, this.props, {onChange: this.onChange})),
|
60
|
+
previews
|
61
|
+
)
|
62
|
+
);
|
63
|
+
}
|
64
|
+
});
|
65
|
+
|
66
|
+
var AttacheFilePreview = React.createClass({displayName: "AttacheFilePreview",
|
67
|
+
|
68
|
+
getInitialState: function() {
|
69
|
+
return { srcWas: '' };
|
70
|
+
},
|
71
|
+
|
72
|
+
removeProgressBar: function() {
|
73
|
+
this.setState({ srcWas: this.props.src });
|
74
|
+
},
|
75
|
+
|
76
|
+
render: function() {
|
77
|
+
var className = this.props.className || "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
|
78
|
+
var pctString = this.props.pctString || (this.props.src ? 100 : this.props.percentLoaded) + "%";
|
79
|
+
var pctDesc = this.props.pctDesc || (this.props.src ? 'Loading...' : pctString);
|
80
|
+
var img = (this.props.src ? (React.createElement("img", {src: this.props.src, onLoad: this.removeProgressBar})) : '');
|
81
|
+
var pctStyle = { width: pctString, minWidth: '3em' };
|
82
|
+
var cptStyle = { textOverflow: "ellipsis" };
|
83
|
+
var caption = React.createElement("div", {className: "pull-left", style: cptStyle}, this.props.filename || this.props.path && this.props.path.split('/').pop());
|
84
|
+
|
85
|
+
if (this.state.srcWas != this.props.src) {
|
86
|
+
var progress = (
|
87
|
+
React.createElement("div", {className: "progress"},
|
88
|
+
React.createElement("div", {className: className, role: "progressbar", "aria-valuenow": this.props.percentLoaded, "aria-valuemin": "0", "aria-valuemax": "100", style: pctStyle},
|
89
|
+
pctDesc
|
90
|
+
)
|
91
|
+
)
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
return (
|
96
|
+
React.createElement("div", {className: "attache-file-preview"},
|
97
|
+
progress,
|
98
|
+
img,
|
99
|
+
React.createElement("div", {className: "clearfix"},
|
100
|
+
caption,
|
101
|
+
React.createElement("a", {href: "#remove", className: "pull-right", onClick: this.props.onRemove, title: "Click to remove"}, "×")
|
102
|
+
)
|
103
|
+
)
|
104
|
+
);
|
105
|
+
}
|
106
|
+
});
|
@@ -0,0 +1,106 @@
|
|
1
|
+
var AttacheFileInput = React.createClass({
|
2
|
+
|
3
|
+
getInitialState: function() {
|
4
|
+
var files = {};
|
5
|
+
var array = ([].concat(JSON.parse(this.props['data-value'])));
|
6
|
+
$.each(array, function(uid, json) {
|
7
|
+
if (json) files[uid] = { path: json };
|
8
|
+
});
|
9
|
+
return {files: files};
|
10
|
+
},
|
11
|
+
|
12
|
+
onRemove: function(uid, e) {
|
13
|
+
delete this.state.files[uid];
|
14
|
+
this.setState(this.state);
|
15
|
+
e.preventDefault();
|
16
|
+
e.stopPropagation();
|
17
|
+
},
|
18
|
+
|
19
|
+
onChange: function() {
|
20
|
+
var file_element = this.getDOMNode().firstChild;
|
21
|
+
// user cancelled file chooser dialog. ignore
|
22
|
+
if (file_element.files.length == 0) return;
|
23
|
+
this.state.files = {};
|
24
|
+
this.setState(this.state);
|
25
|
+
// upload the file via CORS
|
26
|
+
var that = this;
|
27
|
+
new CORSUpload({
|
28
|
+
file_element: file_element, onComplete: this.setFileValue, onProgress: this.setFileValue,
|
29
|
+
onError: function(uid, status) { that.setFileValue(uid, { pctString: '90%', pctDesc: status, className: 'progress-bar progress-bar-danger' }); }
|
30
|
+
});
|
31
|
+
// we don't want the file binary to be uploaded in the main form
|
32
|
+
file_element.value = '';
|
33
|
+
},
|
34
|
+
|
35
|
+
setFileValue: function(key, value) {
|
36
|
+
this.state.files[key] = value;
|
37
|
+
this.setState(this.state);
|
38
|
+
},
|
39
|
+
|
40
|
+
render: function() {
|
41
|
+
var that = this;
|
42
|
+
var previews = [];
|
43
|
+
$.each(that.state.files, function(key, result) {
|
44
|
+
var json = JSON.stringify(result);
|
45
|
+
if (result.path) {
|
46
|
+
var parts = result.path.split('/');
|
47
|
+
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
48
|
+
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
49
|
+
}
|
50
|
+
previews.push(
|
51
|
+
<div className="thumbnail">
|
52
|
+
<input type="hidden" name={that.props.name} value={result.path} readOnly="true" />
|
53
|
+
<AttacheFilePreview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
|
54
|
+
</div>
|
55
|
+
);
|
56
|
+
});
|
57
|
+
return (
|
58
|
+
<label htmlFor={this.props.id} className="attache-file-selector">
|
59
|
+
<input type="file" {...this.props} onChange={this.onChange}/>
|
60
|
+
{previews}
|
61
|
+
</label>
|
62
|
+
);
|
63
|
+
}
|
64
|
+
});
|
65
|
+
|
66
|
+
var AttacheFilePreview = React.createClass({
|
67
|
+
|
68
|
+
getInitialState: function() {
|
69
|
+
return { srcWas: '' };
|
70
|
+
},
|
71
|
+
|
72
|
+
removeProgressBar: function() {
|
73
|
+
this.setState({ srcWas: this.props.src });
|
74
|
+
},
|
75
|
+
|
76
|
+
render: function() {
|
77
|
+
var className = this.props.className || "progress-bar progress-bar-striped active" + (this.props.src ? " progress-bar-success" : "");
|
78
|
+
var pctString = this.props.pctString || (this.props.src ? 100 : this.props.percentLoaded) + "%";
|
79
|
+
var pctDesc = this.props.pctDesc || (this.props.src ? 'Loading...' : pctString);
|
80
|
+
var img = (this.props.src ? (<img src={this.props.src} onLoad={this.removeProgressBar} />) : '');
|
81
|
+
var pctStyle = { width: pctString, minWidth: '3em' };
|
82
|
+
var cptStyle = { textOverflow: "ellipsis" };
|
83
|
+
var caption = <div className="pull-left" style={cptStyle}>{this.props.filename || this.props.path && this.props.path.split('/').pop()}</div>;
|
84
|
+
|
85
|
+
if (this.state.srcWas != this.props.src) {
|
86
|
+
var progress = (
|
87
|
+
<div className="progress">
|
88
|
+
<div className={className} role="progressbar" aria-valuenow={this.props.percentLoaded} aria-valuemin="0" aria-valuemax="100" style={pctStyle}>
|
89
|
+
{pctDesc}
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
);
|
93
|
+
}
|
94
|
+
|
95
|
+
return (
|
96
|
+
<div className="attache-file-preview">
|
97
|
+
{progress}
|
98
|
+
{img}
|
99
|
+
<div className="clearfix">
|
100
|
+
{caption}
|
101
|
+
<a href="#remove" className="pull-right" onClick={this.props.onRemove} title="Click to remove">×</a>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
);
|
105
|
+
}
|
106
|
+
});
|
@@ -0,0 +1,85 @@
|
|
1
|
+
var CORSUpload = (function() {
|
2
|
+
var counter = 0;
|
3
|
+
|
4
|
+
CORSUpload.prototype.onComplete = function(uid, json) { };
|
5
|
+
CORSUpload.prototype.onProgress = function(uid, json) { };
|
6
|
+
CORSUpload.prototype.onError = function(uid, status) { alert(status); };
|
7
|
+
|
8
|
+
function CORSUpload(options) {
|
9
|
+
if (options == null) options = {};
|
10
|
+
for (option in options) {
|
11
|
+
this[option] = options[option];
|
12
|
+
}
|
13
|
+
this.handleFileSelect(options.file_element);
|
14
|
+
}
|
15
|
+
|
16
|
+
CORSUpload.prototype.handleFileSelect = function(file_element) {
|
17
|
+
var f, files, output, _i, _len, _results, url, $ele;
|
18
|
+
$ele = $(file_element);
|
19
|
+
url = $ele.data('uploadurl');
|
20
|
+
if ($ele.data('hmac')) {
|
21
|
+
url = url +
|
22
|
+
"?hmac=" + encodeURIComponent($ele.data('hmac')) +
|
23
|
+
"&uuid=" + encodeURIComponent($ele.data('uuid')) +
|
24
|
+
"&expiration=" + encodeURIComponent($ele.data('expiration')) +
|
25
|
+
""
|
26
|
+
}
|
27
|
+
|
28
|
+
files = file_element.files;
|
29
|
+
output = [];
|
30
|
+
_results = [];
|
31
|
+
for (_i = 0, _len = files.length; _i < _len; _i++) {
|
32
|
+
f = files[_i];
|
33
|
+
f.uid = counter++;
|
34
|
+
this.onProgress(f.uid, { filename: f.name, percentLoaded: 0, bytesLoaded: 0, bytesTotal: f.size });
|
35
|
+
_results.push(this.performUpload(f, url));
|
36
|
+
}
|
37
|
+
return _results;
|
38
|
+
};
|
39
|
+
|
40
|
+
CORSUpload.prototype.createCORSRequest = function(method, url) {
|
41
|
+
var xhr;
|
42
|
+
xhr = new XMLHttpRequest();
|
43
|
+
if (xhr.withCredentials != null) {
|
44
|
+
xhr.open(method, url, true);
|
45
|
+
} else if (typeof XDomainRequest !== "undefined") {
|
46
|
+
xhr = new XDomainRequest();
|
47
|
+
xhr.open(method, url);
|
48
|
+
} else {
|
49
|
+
xhr = null;
|
50
|
+
}
|
51
|
+
return xhr;
|
52
|
+
};
|
53
|
+
|
54
|
+
CORSUpload.prototype.performUpload = function(file, url) {
|
55
|
+
var this_s3upload, xhr;
|
56
|
+
this_s3upload = this;
|
57
|
+
url = url + (url.indexOf('?') == -1 ? '?' : '&') + 'file=' + encodeURIComponent(file.name);
|
58
|
+
xhr = this.createCORSRequest('PUT', url);
|
59
|
+
if (!xhr) {
|
60
|
+
this.onError(file.uid, 'CORS not supported');
|
61
|
+
} else {
|
62
|
+
xhr.onload = function(e) {
|
63
|
+
if (xhr.status === 200) {
|
64
|
+
this_s3upload.onComplete(file.uid, JSON.parse(e.target.responseText));
|
65
|
+
} else {
|
66
|
+
return this_s3upload.onError(file.uid, xhr.status + ' ' + xhr.statusText);
|
67
|
+
}
|
68
|
+
};
|
69
|
+
xhr.onerror = function() {
|
70
|
+
return this_s3upload.onError(file.uid, 'Unable to reach server');
|
71
|
+
};
|
72
|
+
xhr.upload.onprogress = function(e) {
|
73
|
+
var percentLoaded;
|
74
|
+
if (e.lengthComputable) {
|
75
|
+
percentLoaded = Math.round((e.loaded / e.total) * 100);
|
76
|
+
return this_s3upload.onProgress(file.uid, { filename: file.name, percentLoaded: percentLoaded, bytesLoaded: e.loaded, bytesTotal: e.total });
|
77
|
+
}
|
78
|
+
};
|
79
|
+
}
|
80
|
+
return xhr.send(file);
|
81
|
+
};
|
82
|
+
|
83
|
+
return CORSUpload;
|
84
|
+
|
85
|
+
})();
|
@@ -0,0 +1,23 @@
|
|
1
|
+
(function() {
|
2
|
+
function attacheFileInputs() {
|
3
|
+
var safeWords = { 'class': 'className', 'for': 'htmlFor' };
|
4
|
+
var sel = document.getElementsByClassName('enable-attache');
|
5
|
+
var ele, attrs, name, value;
|
6
|
+
for (var i = sel.length-1; i >= 0; i--) {
|
7
|
+
ele = sel[i];
|
8
|
+
attrs = {};
|
9
|
+
for (var j = 0; j < ele.attributes.length; j++) {
|
10
|
+
name = ele.attributes[j].name;
|
11
|
+
value = ele.attributes[j].value;
|
12
|
+
if (name === 'class') value = value.replace('enable-attache', 'attache-enabled');
|
13
|
+
name = safeWords[name] || name;
|
14
|
+
attrs[name] = value;
|
15
|
+
}
|
16
|
+
var wrap = document.createElement('div');
|
17
|
+
ele.parentNode.replaceChild(wrap, ele); // ele.parentNode.insertBefore(wrap, ele.nextSibling);
|
18
|
+
React.render(React.createElement(AttacheFileInput, React.__spread({}, attrs)), wrap);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
$(document).on('page:change', attacheFileInputs);
|
23
|
+
})();
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "attache_rails/engine"
|
2
|
+
|
3
|
+
module AttacheRails
|
4
|
+
module ViewHelper
|
5
|
+
ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { 'http://localhost:9292/upload' }
|
6
|
+
ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { 'http://localhost:9292/view' }
|
7
|
+
ATTACHE_UPLOAD_DURATION = ENV.fetch('ATTACHE_UPLOAD_DURATION') { '600' }.to_i # 10 minutes
|
8
|
+
|
9
|
+
def attache_urls(json, geometry)
|
10
|
+
array = json.kind_of?(Array) ? json : [*json]
|
11
|
+
array.collect do |path|
|
12
|
+
download_url = ATTACHE_DOWNLOAD_URL
|
13
|
+
prefix, basename = File.split(path)
|
14
|
+
[download_url, prefix, CGI.escape(geometry), basename].join('/').tap do |url|
|
15
|
+
yield url if block_given?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def attache_options(geometry, current_value)
|
21
|
+
auth = if ENV['ATTACHE_SECRET_KEY']
|
22
|
+
uuid = SecureRandom.uuid
|
23
|
+
expiration = (Time.now + ATTACHE_UPLOAD_DURATION).to_i
|
24
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV['ATTACHE_SECRET_KEY'], "#{uuid}#{expiration}")
|
25
|
+
{ uuid: uuid, expiration: expiration, hmac: hmac }
|
26
|
+
else
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
{
|
31
|
+
class: 'enable-attache',
|
32
|
+
data: {
|
33
|
+
geometry: geometry,
|
34
|
+
value: [*current_value],
|
35
|
+
uploadurl: ATTACHE_UPLOAD_URL,
|
36
|
+
downloadurl: ATTACHE_DOWNLOAD_URL,
|
37
|
+
}.merge(auth),
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ActionView::Base.send(:include, AttacheRails::ViewHelper)
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attache_rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- choonkeat
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- choonkeat@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- MIT-LICENSE
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- app/assets/javascripts/attache.js
|
24
|
+
- app/assets/javascripts/attache/bootstrap3.js
|
25
|
+
- app/assets/javascripts/attache/bootstrap3.js.jsx
|
26
|
+
- app/assets/javascripts/attache/cors_upload.js
|
27
|
+
- app/assets/javascripts/attache/ujs.js
|
28
|
+
- lib/attache_rails.rb
|
29
|
+
- lib/attache_rails/engine.rb
|
30
|
+
- lib/attache_rails/version.rb
|
31
|
+
homepage: https://github.com/choonkeat/attache_rails
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.4.5
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Client lib to use attache server
|
55
|
+
test_files: []
|