attache_rails 0.0.6 → 0.1.0
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.
- checksums.yaml +4 -4
- data/README.md +21 -39
- data/app/assets/javascripts/attache.js +1 -1
- data/app/assets/javascripts/attache/bootstrap3.js +26 -0
- data/app/assets/javascripts/attache/bootstrap3.js.jsx +26 -0
- data/app/assets/javascripts/attache/cors_upload.js +3 -2
- data/app/assets/javascripts/attache/file_input.js +45 -13
- data/app/assets/javascripts/attache/file_input.js.jsx +44 -12
- data/lib/attache_rails.rb +7 -40
- data/lib/attache_rails/model.rb +91 -0
- data/lib/attache_rails/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c7546fc76fade2bf70f9075fe87be7b71fffa6f
|
4
|
+
data.tar.gz: ee7f0526cbebe7292f685cb86ed501721d46f7b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76d9c2a0aeec9e6de2980cd752bcf4e7fc83720426e073d60e85073e5d6a97e2ef4211733a0e415fd9af9280c49da0e7af52a8678bc72ce27548b6b442983290
|
7
|
+
data.tar.gz: 1b955d139c9fda1b3828e6f91a7ace1d8b213a92085cce02af4e0936c16b273558ee040082815dc93c391c1bc03530a5eed3a94ec9346d5b28e77999c488bf7d
|
data/README.md
CHANGED
@@ -2,8 +2,6 @@
|
|
2
2
|
|
3
3
|
Ruby on Rails integration for [attache server](https://github.com/choonkeat/attache)
|
4
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/compare/fc47c17...master) or follow the step by step instructions in this `README`
|
6
|
-
|
7
5
|
## Dependencies
|
8
6
|
|
9
7
|
[React](https://github.com/reactjs/react-rails), jQuery, Bootstrap 3
|
@@ -40,47 +38,38 @@ If you want to customize the file upload look and feel, define your own react `<
|
|
40
38
|
|
41
39
|
### Database
|
42
40
|
|
43
|
-
To use `attache`, you only need to store the
|
41
|
+
To use `attache`, you only need to store the JSON attributes given to you after you've uploaded a file. So if you have an existing model, you only need to add a text column
|
44
42
|
|
45
43
|
``` bash
|
46
|
-
rails generate migration AddPhotoPathToUsers
|
44
|
+
rails generate migration AddPhotoPathToUsers photo:text
|
47
45
|
```
|
48
46
|
|
49
|
-
To assign **multiple** images to **one** model,
|
47
|
+
To assign **multiple** images to **one** model, the same column can be used, although pluralized column name reads better
|
50
48
|
|
51
49
|
``` bash
|
52
|
-
rails generate migration AddPhotoPathToUsers
|
50
|
+
rails generate migration AddPhotoPathToUsers photos:text
|
53
51
|
```
|
54
52
|
|
55
53
|
### Model
|
56
54
|
|
57
|
-
In your model, `
|
55
|
+
In your model, define whether it `has_one_attache` or `has_many_attaches`
|
58
56
|
|
59
57
|
``` ruby
|
60
58
|
class User < ActiveRecord::Base
|
61
|
-
|
59
|
+
has_many_attaches :photos
|
62
60
|
end
|
63
61
|
```
|
64
62
|
|
65
63
|
### New or Edit form
|
66
64
|
|
67
|
-
In your form, you would add some options to `file_field` using the `attache_options` helper method
|
65
|
+
In your form, you would add some options to `file_field` using the `attache_options` helper method:
|
68
66
|
|
69
67
|
``` slim
|
70
|
-
= f.file_field :
|
68
|
+
= f.file_field :photos, f.object.avatar_options('64x64#')
|
71
69
|
```
|
72
70
|
|
73
|
-
|
74
|
-
|
75
|
-
``` slim
|
76
|
-
= f.file_field :photo_path, **attache_options('64x64#', f.object.photo_path)
|
77
|
-
```
|
71
|
+
If you were using `has_many_attaches` the file input will automatically allow multiple files, otherwise the file input will only accept 1 file.
|
78
72
|
|
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
73
|
|
85
74
|
NOTE: `64x64#` is just an example, you should define a suitable [geometry](http://www.imagemagick.org/Usage/resize/) for your form
|
86
75
|
|
@@ -98,45 +87,38 @@ If you're only accepting a single file upload, change it to
|
|
98
87
|
|
99
88
|
``` ruby
|
100
89
|
def user_params
|
101
|
-
params.require(:user).permit(:name, :
|
90
|
+
params.require(:user).permit(:name, :photo, attaches_discarded: [])
|
102
91
|
end
|
103
92
|
```
|
104
93
|
|
105
|
-
If you're accepting multiple file uploads
|
94
|
+
If you're accepting multiple file uploads, change it to
|
106
95
|
|
107
96
|
``` ruby
|
108
97
|
def user_params
|
109
|
-
params.require(:user).permit(:name,
|
98
|
+
params.require(:user).permit(:name, photos: [], attaches_discarded: [])
|
110
99
|
end
|
111
100
|
```
|
112
101
|
|
102
|
+
NOTE: You do not have to manage `params[:attaches_discarded]` yourself. It is automatically managed for you between the frontend javascript and the ActiveRecord integration: files that are discarded will be removed from the attache server when you update or destroy your model.
|
103
|
+
|
113
104
|
### Show
|
114
105
|
|
115
|
-
Use the `
|
106
|
+
Use the `*_url` or `*_urls` methods (depending on whether you are accepting multiple files) to obtain full urls.
|
116
107
|
|
117
108
|
``` slim
|
118
|
-
|
119
|
-
= image_tag(url)
|
109
|
+
= image_tag @user.photo_url('100x100#')
|
120
110
|
```
|
121
111
|
|
122
|
-
|
123
|
-
|
124
|
-
``` ruby
|
125
|
-
class User < ActiveRecord::Base
|
126
|
-
include AttacheRails::Helper
|
127
|
-
serialize :photo_path, JSON
|
112
|
+
or
|
128
113
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
end
|
114
|
+
``` slim
|
115
|
+
- @user.photos_urls('200x200#').each do |url|
|
116
|
+
= image_tag url
|
133
117
|
```
|
134
118
|
|
135
119
|
### Environment configs
|
136
120
|
|
137
|
-
`
|
138
|
-
|
139
|
-
`ATTACHE_DOWNLOAD_URL` points to url prefix for downloading the resized images, e.g. `http://cdn.lvh.me:9292/view`
|
121
|
+
`ATTACHE_URL` points to the attache server. e.g. `http://localhost:9292`
|
140
122
|
|
141
123
|
`ATTACHE_UPLOAD_DURATION` refers to the number of seconds before a signed upload request is considered expired, e.g. `600`
|
142
124
|
|
@@ -46,3 +46,29 @@ if (typeof AttacheFilePreview === 'undefined') {
|
|
46
46
|
});
|
47
47
|
|
48
48
|
}
|
49
|
+
|
50
|
+
if (typeof AttachePlaceholder === 'undefined') {
|
51
|
+
|
52
|
+
var AttachePlaceholder = React.createClass({displayName: "AttachePlaceholder",
|
53
|
+
render: function() {
|
54
|
+
return (
|
55
|
+
React.createElement("div", {className: "attache-file-preview"},
|
56
|
+
React.createElement("img", {src: this.props.src})
|
57
|
+
)
|
58
|
+
);
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
}
|
63
|
+
|
64
|
+
if (typeof AttacheHeader === 'undefined') {
|
65
|
+
|
66
|
+
var AttacheHeader = React.createClass({displayName: "AttacheHeader",
|
67
|
+
render: function() {
|
68
|
+
return (
|
69
|
+
React.createElement("noscript", null)
|
70
|
+
);
|
71
|
+
}
|
72
|
+
});
|
73
|
+
|
74
|
+
}
|
@@ -46,3 +46,29 @@ if (typeof AttacheFilePreview === 'undefined') {
|
|
46
46
|
});
|
47
47
|
|
48
48
|
}
|
49
|
+
|
50
|
+
if (typeof AttachePlaceholder === 'undefined') {
|
51
|
+
|
52
|
+
var AttachePlaceholder = React.createClass({
|
53
|
+
render: function() {
|
54
|
+
return (
|
55
|
+
<div className="attache-file-preview">
|
56
|
+
<img src={this.props.src} />
|
57
|
+
</div>
|
58
|
+
);
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
}
|
63
|
+
|
64
|
+
if (typeof AttacheHeader === 'undefined') {
|
65
|
+
|
66
|
+
var AttacheHeader = React.createClass({
|
67
|
+
render: function() {
|
68
|
+
return (
|
69
|
+
<noscript />
|
70
|
+
);
|
71
|
+
}
|
72
|
+
});
|
73
|
+
|
74
|
+
}
|
@@ -14,7 +14,7 @@ var AttacheCORSUpload = (function() {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
AttacheCORSUpload.prototype.handleFileSelect = function(file_element) {
|
17
|
-
var f, files, output, _i, _len, _results, url, $ele;
|
17
|
+
var f, files, output, _i, _len, _results, url, $ele, prefix;
|
18
18
|
$ele = $(file_element);
|
19
19
|
url = $ele.data('uploadurl');
|
20
20
|
if ($ele.data('hmac')) {
|
@@ -25,12 +25,13 @@ var AttacheCORSUpload = (function() {
|
|
25
25
|
""
|
26
26
|
}
|
27
27
|
|
28
|
+
prefix = Date.now() + "_";
|
28
29
|
files = file_element.files;
|
29
30
|
output = [];
|
30
31
|
_results = [];
|
31
32
|
for (_i = 0, _len = files.length; _i < _len; _i++) {
|
32
33
|
f = files[_i];
|
33
|
-
f.uid = counter
|
34
|
+
f.uid = prefix + (counter++);
|
34
35
|
this.onProgress(f.uid, { filename: f.name, percentLoaded: 0, bytesLoaded: 0, bytesTotal: f.size });
|
35
36
|
_results.push(this.performUpload(f, url));
|
36
37
|
}
|
@@ -2,25 +2,31 @@ var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
|
|
2
2
|
|
3
3
|
getInitialState: function() {
|
4
4
|
var files = {};
|
5
|
-
|
6
|
-
|
7
|
-
if (json) files[uid] = { path: json };
|
5
|
+
if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
|
6
|
+
if (json) files[uid] = json;
|
8
7
|
});
|
9
|
-
return {files: files};
|
8
|
+
return {files: files, attaches_discarded: []};
|
10
9
|
},
|
11
10
|
|
12
11
|
onRemove: function(uid, e) {
|
13
|
-
delete this.state.files[uid];
|
14
|
-
this.setState(this.state);
|
15
12
|
e.preventDefault();
|
16
13
|
e.stopPropagation();
|
14
|
+
|
15
|
+
var fieldname = this.getDOMNode().firstChild.name; // when 'user[avatar]'
|
16
|
+
var newfield = fieldname.replace(/\w+\](\[\]|)$/, 'attaches_discarded][]'); // become 'user[attaches_discarded][]'
|
17
|
+
|
18
|
+
this.state.attaches_discarded.push({ fieldname: newfield, path: this.state.files[uid].path });
|
19
|
+
delete this.state.files[uid];
|
20
|
+
|
21
|
+
this.setState(this.state);
|
17
22
|
},
|
18
23
|
|
19
24
|
onChange: function() {
|
20
25
|
var file_element = this.getDOMNode().firstChild;
|
21
26
|
// user cancelled file chooser dialog. ignore
|
22
27
|
if (file_element.files.length == 0) return;
|
23
|
-
this.state.files = {};
|
28
|
+
if (! this.props.multiple) this.state.files = {};
|
29
|
+
|
24
30
|
this.setState(this.state);
|
25
31
|
// upload the file via CORS
|
26
32
|
var that = this;
|
@@ -45,26 +51,52 @@ var AttacheFileInput = React.createClass({displayName: "AttacheFileInput",
|
|
45
51
|
|
46
52
|
render: function() {
|
47
53
|
var that = this;
|
54
|
+
|
55
|
+
var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
|
56
|
+
var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
|
57
|
+
var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
|
58
|
+
|
48
59
|
var previews = [];
|
49
60
|
$.each(that.state.files, function(key, result) {
|
50
|
-
var json = JSON.stringify(result);
|
51
61
|
if (result.path) {
|
52
62
|
var parts = result.path.split('/');
|
53
63
|
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
54
64
|
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
55
65
|
result.filename = result.src.split('/').pop().split(/[#?]/).shift();
|
66
|
+
result.multiple = that.props.multiple;
|
56
67
|
}
|
68
|
+
var json = JSON.stringify(result);
|
57
69
|
previews.push(
|
58
70
|
React.createElement("div", {className: "attache-file-input"},
|
59
|
-
React.createElement("input", {type: "hidden", name: that.props.name, value:
|
60
|
-
React.createElement(
|
71
|
+
React.createElement("input", {type: "hidden", name: that.props.name, value: json, readOnly: "true"}),
|
72
|
+
React.createElement(Preview, React.__spread({}, result, {key: key, onRemove: that.onRemove.bind(that, key)}))
|
61
73
|
)
|
62
74
|
);
|
63
75
|
});
|
76
|
+
|
77
|
+
var placeholders = [];
|
78
|
+
if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
|
79
|
+
placeholders.push(
|
80
|
+
React.createElement(Placeholder, React.__spread({}, that.props, {src: src}))
|
81
|
+
);
|
82
|
+
});
|
83
|
+
|
84
|
+
var discards = [];
|
85
|
+
$.each(that.state.attaches_discarded, function(index, discard) {
|
86
|
+
discards.push(
|
87
|
+
React.createElement("input", {type: "hidden", name: discard.fieldname, value: discard.path})
|
88
|
+
);
|
89
|
+
});
|
90
|
+
|
91
|
+
var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
|
64
92
|
return (
|
65
|
-
React.createElement("label", {htmlFor:
|
66
|
-
React.createElement("input", React.__spread({type: "file"},
|
67
|
-
|
93
|
+
React.createElement("label", {htmlFor: that.props.id, className: className},
|
94
|
+
React.createElement("input", React.__spread({type: "file"}, that.props, {onChange: this.onChange})),
|
95
|
+
React.createElement("input", {type: "hidden", name: that.props.name, value: ""}),
|
96
|
+
React.createElement(Header, React.__spread({}, that.props)),
|
97
|
+
previews,
|
98
|
+
placeholders,
|
99
|
+
discards
|
68
100
|
)
|
69
101
|
);
|
70
102
|
}
|
@@ -2,25 +2,31 @@ var AttacheFileInput = React.createClass({
|
|
2
2
|
|
3
3
|
getInitialState: function() {
|
4
4
|
var files = {};
|
5
|
-
|
6
|
-
|
7
|
-
if (json) files[uid] = { path: json };
|
5
|
+
if (this.props['data-value']) $.each(JSON.parse(this.props['data-value']), function(uid, json) {
|
6
|
+
if (json) files[uid] = json;
|
8
7
|
});
|
9
|
-
return {files: files};
|
8
|
+
return {files: files, attaches_discarded: []};
|
10
9
|
},
|
11
10
|
|
12
11
|
onRemove: function(uid, e) {
|
13
|
-
delete this.state.files[uid];
|
14
|
-
this.setState(this.state);
|
15
12
|
e.preventDefault();
|
16
13
|
e.stopPropagation();
|
14
|
+
|
15
|
+
var fieldname = this.getDOMNode().firstChild.name; // when 'user[avatar]'
|
16
|
+
var newfield = fieldname.replace(/\w+\](\[\]|)$/, 'attaches_discarded][]'); // become 'user[attaches_discarded][]'
|
17
|
+
|
18
|
+
this.state.attaches_discarded.push({ fieldname: newfield, path: this.state.files[uid].path });
|
19
|
+
delete this.state.files[uid];
|
20
|
+
|
21
|
+
this.setState(this.state);
|
17
22
|
},
|
18
23
|
|
19
24
|
onChange: function() {
|
20
25
|
var file_element = this.getDOMNode().firstChild;
|
21
26
|
// user cancelled file chooser dialog. ignore
|
22
27
|
if (file_element.files.length == 0) return;
|
23
|
-
this.state.files = {};
|
28
|
+
if (! this.props.multiple) this.state.files = {};
|
29
|
+
|
24
30
|
this.setState(this.state);
|
25
31
|
// upload the file via CORS
|
26
32
|
var that = this;
|
@@ -45,26 +51,52 @@ var AttacheFileInput = React.createClass({
|
|
45
51
|
|
46
52
|
render: function() {
|
47
53
|
var that = this;
|
54
|
+
|
55
|
+
var Header = eval(this.props['data-header-component'] || 'AttacheHeader');
|
56
|
+
var Preview = eval(this.props['data-preview-component'] || 'AttacheFilePreview');
|
57
|
+
var Placeholder = eval(this.props['data-placeholder-component'] || 'AttachePlaceholder');
|
58
|
+
|
48
59
|
var previews = [];
|
49
60
|
$.each(that.state.files, function(key, result) {
|
50
|
-
var json = JSON.stringify(result);
|
51
61
|
if (result.path) {
|
52
62
|
var parts = result.path.split('/');
|
53
63
|
parts.splice(parts.length-1, 0, encodeURIComponent(that.props['data-geometry'] || '128x128#'));
|
54
64
|
result.src = that.props['data-downloadurl'] + '/' + parts.join('/');
|
55
65
|
result.filename = result.src.split('/').pop().split(/[#?]/).shift();
|
66
|
+
result.multiple = that.props.multiple;
|
56
67
|
}
|
68
|
+
var json = JSON.stringify(result);
|
57
69
|
previews.push(
|
58
70
|
<div className="attache-file-input">
|
59
|
-
<input type="hidden" name={that.props.name} value={
|
60
|
-
<
|
71
|
+
<input type="hidden" name={that.props.name} value={json} readOnly="true" />
|
72
|
+
<Preview {...result} key={key} onRemove={that.onRemove.bind(that, key)}/>
|
61
73
|
</div>
|
62
74
|
);
|
63
75
|
});
|
76
|
+
|
77
|
+
var placeholders = [];
|
78
|
+
if (previews.length == 0 && that.props['data-placeholder']) $.each(JSON.parse(that.props['data-placeholder']), function(uid, src) {
|
79
|
+
placeholders.push(
|
80
|
+
<Placeholder {...that.props} src={src} />
|
81
|
+
);
|
82
|
+
});
|
83
|
+
|
84
|
+
var discards = [];
|
85
|
+
$.each(that.state.attaches_discarded, function(index, discard) {
|
86
|
+
discards.push(
|
87
|
+
<input type="hidden" name={discard.fieldname} value={discard.path} />
|
88
|
+
);
|
89
|
+
});
|
90
|
+
|
91
|
+
var className = ["attache-file-selector", "attache-placeholders-count-" + placeholders.length, "attache-previews-count-" + previews.length, this.props['data-classname']].join(' ').trim();
|
64
92
|
return (
|
65
|
-
<label htmlFor={
|
66
|
-
<input type="file" {...
|
93
|
+
<label htmlFor={that.props.id} className={className}>
|
94
|
+
<input type="file" {...that.props} onChange={this.onChange}/>
|
95
|
+
<input type="hidden" name={that.props.name} value="" />
|
96
|
+
<Header {...that.props} />
|
67
97
|
{previews}
|
98
|
+
{placeholders}
|
99
|
+
{discards}
|
68
100
|
</label>
|
69
101
|
);
|
70
102
|
}
|
data/lib/attache_rails.rb
CHANGED
@@ -1,43 +1,10 @@
|
|
1
|
-
require "attache_rails/engine"
|
2
|
-
|
3
1
|
module AttacheRails
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
2
|
+
ATTACHE_URL = ENV.fetch('ATTACHE_URL') { "http://localhost:9292" }
|
3
|
+
ATTACHE_UPLOAD_URL = ENV.fetch('ATTACHE_UPLOAD_URL') { "#{ATTACHE_URL}/upload" }
|
4
|
+
ATTACHE_DOWNLOAD_URL = ENV.fetch('ATTACHE_DOWNLOAD_URL') { "#{ATTACHE_URL}/view" }
|
5
|
+
ATTACHE_DELETE_URL = ENV.fetch('ATTACHE_DELETE_URL') { "#{ATTACHE_URL}/delete" }
|
6
|
+
ATTACHE_UPLOAD_DURATION = ENV.fetch('ATTACHE_UPLOAD_DURATION') { "600" }.to_i # signed upload form expires in 10 minutes
|
41
7
|
end
|
42
8
|
|
43
|
-
|
9
|
+
require "attache_rails/engine"
|
10
|
+
require "attache_rails/model"
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module AttacheRails
|
5
|
+
module Utils
|
6
|
+
class << self
|
7
|
+
def attache_url_for(json_string, geometry)
|
8
|
+
JSON.parse(json_string).tap do |attrs|
|
9
|
+
prefix, basename = File.split(attrs['path'])
|
10
|
+
attrs['url'] = [ATTACHE_DOWNLOAD_URL, prefix, CGI.escape(geometry), basename].join('/')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def attache_auth_options
|
15
|
+
if ENV['ATTACHE_SECRET_KEY']
|
16
|
+
uuid = SecureRandom.uuid
|
17
|
+
expiration = (Time.now + ATTACHE_UPLOAD_DURATION).to_i
|
18
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), ENV['ATTACHE_SECRET_KEY'], "#{uuid}#{expiration}")
|
19
|
+
{ uuid: uuid, expiration: expiration, hmac: hmac }
|
20
|
+
else
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def attache_options(geometry, current_value, options)
|
26
|
+
{
|
27
|
+
multiple: options[:multiple],
|
28
|
+
class: 'enable-attache',
|
29
|
+
data: {
|
30
|
+
geometry: geometry,
|
31
|
+
value: [*current_value],
|
32
|
+
placeholder: [*options[:placeholder]],
|
33
|
+
uploadurl: ATTACHE_UPLOAD_URL,
|
34
|
+
downloadurl: ATTACHE_DOWNLOAD_URL,
|
35
|
+
}.merge(options[:data_attrs] || {}).merge(attache_auth_options),
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
module Model
|
41
|
+
def self.included(base)
|
42
|
+
base.extend ClassMethods
|
43
|
+
base.class_eval do
|
44
|
+
attr_accessor :attaches_discarded
|
45
|
+
after_commit :attaches_discard!, if: :attaches_discarded
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def attaches_discard!(files = attaches_discarded)
|
50
|
+
if files.present?
|
51
|
+
logger.info "DELETE #{files.inspect}"
|
52
|
+
Net::HTTP.post_form(
|
53
|
+
URI.parse(ATTACHE_DELETE_URL),
|
54
|
+
Utils.attache_auth_options.merge(paths: files.join("\n"))
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module ClassMethods
|
60
|
+
def has_one_attache(name)
|
61
|
+
serialize name, JSON
|
62
|
+
define_method "#{name}_options", -> (geometry, options = {}) { Utils.attache_options(geometry, [self.send("#{name}_attributes", geometry)], multiple: false, **options) }
|
63
|
+
define_method "#{name}_url", -> (geometry) { self.send("#{name}_attributes", geometry).try(:[], 'url') }
|
64
|
+
define_method "#{name}_attributes", -> (geometry) { str = self.send(name); Utils.attache_url_for(str, geometry) if str; }
|
65
|
+
define_method "#{name}_discard", -> do
|
66
|
+
self.attaches_discarded ||= []
|
67
|
+
self.attaches_discarded.push(self.send("#{name}_attributes", 'original')['path'])
|
68
|
+
end
|
69
|
+
after_destroy "#{name}_discard"
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_many_attaches(name)
|
73
|
+
serialize name, JSON
|
74
|
+
define_method "#{name}_options", -> (geometry, options = {}) { Utils.attache_options(geometry, self.send("#{name}_attributes", geometry), multiple: true, **options) }
|
75
|
+
define_method "#{name}_urls", -> (geometry) { self.send("#{name}_attributes", geometry).collect {|attrs| attrs['url'] } }
|
76
|
+
define_method "#{name}_attributes", -> (geometry) {
|
77
|
+
(self.send(name) || []).inject([]) do |sum, str|
|
78
|
+
sum + (str.blank? ? [] : [Utils.attache_url_for(str, geometry)])
|
79
|
+
end
|
80
|
+
}
|
81
|
+
define_method "#{name}_discard", -> do
|
82
|
+
self.attaches_discarded ||= []
|
83
|
+
self.send("#{name}_attributes", 'original').each {|attrs| self.attaches_discarded.push(attrs['path']) }
|
84
|
+
end
|
85
|
+
after_destroy "#{name}_discard"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
ActiveRecord::Base.send(:include, AttacheRails::Model)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attache_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- choonkeat
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -29,6 +29,7 @@ files:
|
|
29
29
|
- app/assets/javascripts/attache/ujs.js
|
30
30
|
- lib/attache_rails.rb
|
31
31
|
- lib/attache_rails/engine.rb
|
32
|
+
- lib/attache_rails/model.rb
|
32
33
|
- lib/attache_rails/version.rb
|
33
34
|
homepage: https://github.com/choonkeat/attache_rails
|
34
35
|
licenses:
|
@@ -50,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
51
|
version: '0'
|
51
52
|
requirements: []
|
52
53
|
rubyforge_project:
|
53
|
-
rubygems_version: 2.4.
|
54
|
+
rubygems_version: 2.4.6
|
54
55
|
signing_key:
|
55
56
|
specification_version: 4
|
56
57
|
summary: Client lib to use attache server
|