dynamic_paperclip 0.0.4 → 1.0.0a.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.
- data/README.md +33 -25
- data/lib/dynamic_paperclip/attachment.rb +5 -1
- data/lib/dynamic_paperclip/attachment_registry.rb +4 -0
- data/lib/dynamic_paperclip/attachment_style_generator.rb +73 -0
- data/lib/dynamic_paperclip/errors.rb +0 -9
- data/lib/dynamic_paperclip/has_attached_file.rb +3 -19
- data/lib/dynamic_paperclip/railtie.rb +9 -0
- data/lib/dynamic_paperclip/version.rb +1 -1
- data/lib/dynamic_paperclip.rb +6 -1
- data/lib/generators/dynamic_paperclip/install_generator.rb +1 -3
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +5561 -0
- data/test/generators/install_generator_test.rb +1 -3
- data/test/tmp/config/initializers/dynamic_paperclip.rb +1 -1
- data/test/unit/attachment_style_generator_test.rb +95 -0
- data/test/unit/attachment_test.rb +2 -2
- data/test/unit/has_attached_file_test.rb +4 -22
- metadata +68 -16
- data/app/controllers/dynamic_paperclip/attachment_styles_controller.rb +0 -43
- data/lib/dynamic_paperclip/engine.rb +0 -4
- data/test/controllers/attachment_style_controller_test.rb +0 -129
data/README.md
CHANGED
@@ -2,7 +2,7 @@ Dynamic Paperclip
|
|
2
2
|
=================
|
3
3
|
|
4
4
|
Dynamic Paperclip is an extension to the wonderful [Paperclip](http://github.com/thoughtbot/paperclip) gem
|
5
|
-
and
|
5
|
+
and Rack middleware that allows for the creation of Paperclip attachment styles on-the-fly.
|
6
6
|
|
7
7
|
Instead of defining your attachment styles in your model, Dynamic Paperclip allows you to generate URL's
|
8
8
|
for arbitrary attachment styles (usually in your view), effectively pushing style definition there.
|
@@ -14,10 +14,7 @@ Getting started
|
|
14
14
|
|
15
15
|
### Requirements
|
16
16
|
|
17
|
-
Dynamic Paperclip requires Paperclip 3.5.0 or above and
|
18
|
-
it's just untested at the moment).
|
19
|
-
|
20
|
-
It also only currently supports Paperclip's File Storage.
|
17
|
+
Dynamic Paperclip requires Paperclip 3.5.0 or above and only currently supports Paperclip's File Storage.
|
21
18
|
|
22
19
|
### Application
|
23
20
|
|
@@ -29,7 +26,7 @@ gem 'dynamic_paperclip'
|
|
29
26
|
|
30
27
|
Run the ``bundle`` command to install it.
|
31
28
|
|
32
|
-
After you install the gem, you need to run the generator:
|
29
|
+
After you install the gem, you need to run the generator if you're using Rails:
|
33
30
|
|
34
31
|
```console
|
35
32
|
rails generate dynamic_paperclip:install
|
@@ -38,6 +35,12 @@ rails generate dynamic_paperclip:install
|
|
38
35
|
This will install an initializer that sets up a secret key used in validating that dynamic asset URL's
|
39
36
|
originated from your application.
|
40
37
|
|
38
|
+
If you are not using Rails, you need to set ``DynamicPaperclip.config.secret`` to some random string during
|
39
|
+
your application's boot process, check out [install_generator.rb](lib/generators/dynamic_paperclip/install_generator.rb) to see how the Rails generator does it.
|
40
|
+
|
41
|
+
You'll also need to configure your application to use the ``DynamicPaperclip::AttachmentStyleGenerator`` Rack middleware
|
42
|
+
if you are not using Rails. This is configured automatically for Rails applications.
|
43
|
+
|
41
44
|
Now, you're ready to start using Dynamic Paperclip. Change any attachment definitions that you'd like to make dynamic from:
|
42
45
|
|
43
46
|
```ruby
|
@@ -65,9 +68,9 @@ passing it the style definition that you would normally define in the ``:styles`
|
|
65
68
|
|
66
69
|
### Server Configuration
|
67
70
|
|
68
|
-
If you're using
|
71
|
+
If you're using your application to serve static assets, then no configuration is required. But if you're not,
|
69
72
|
which you shouldn't be in any production environment, then you just need to make sure that your HTTP server
|
70
|
-
is configured to serve static assets if they exist, but pass the request along to your
|
73
|
+
is configured to serve static assets if they exist, but pass the request along to your application
|
71
74
|
if they do not.
|
72
75
|
|
73
76
|
For example, on Nginx, this would be accomplished with something along the lines of:
|
@@ -96,7 +99,7 @@ and is a pretty standard Nginx setup.
|
|
96
99
|
Why?
|
97
100
|
----
|
98
101
|
|
99
|
-
Because as your
|
102
|
+
Because as your application grows, you may discover that you have a large number of attachment styles. This is
|
100
103
|
slowing down your requests, because every time a user attempts to upload a file, Paperclip must process each and every
|
101
104
|
one of those styles right then and there, in the middle of the request.
|
102
105
|
|
@@ -106,10 +109,11 @@ that needs it, and not in the model.
|
|
106
109
|
How does this wizardry work?
|
107
110
|
---------------------------
|
108
111
|
|
109
|
-
It's pretty simple, actually.
|
110
|
-
|
112
|
+
It's pretty simple, actually. Dynamic Paperclip includes a piece of Rack middleware that intercepts requests
|
113
|
+
for URLs to any dynamic attachments, generates the requested style if it doesn't exist, and sends it
|
114
|
+
back to the browser.
|
111
115
|
|
112
|
-
For
|
116
|
+
For example, in your model, you may define a dynamic attachment like this:
|
113
117
|
|
114
118
|
```ruby
|
115
119
|
class User
|
@@ -117,13 +121,7 @@ class User
|
|
117
121
|
end
|
118
122
|
```
|
119
123
|
|
120
|
-
|
121
|
-
|
122
|
-
```ruby
|
123
|
-
get '/system/users/:attachment/:id/dynamic_:definition', to: 'DynamicPaperclip::AttachmentStyles#generate_user'
|
124
|
-
```
|
125
|
-
|
126
|
-
Now, in your view, you might call something like this:
|
124
|
+
Then, in your view, you might call something like this:
|
127
125
|
|
128
126
|
```ruby
|
129
127
|
@user.avatar.dynamic_url('50x50')
|
@@ -135,14 +133,17 @@ Which will return the following url (assuming a JPG avatar and a User ID of 42):
|
|
135
133
|
/system/users/avatars/42/dynamic_50x50.jpg?s=secrethash
|
136
134
|
```
|
137
135
|
|
138
|
-
When
|
139
|
-
|
140
|
-
|
141
|
-
and
|
142
|
-
from the stye name, and then sends it back to your visitor via ``#send_file``.
|
136
|
+
When the first request comes in for that URL, Dynamic Paperclip's Rack middleware will intercept the request,
|
137
|
+
validate that "secrethash" to ensure that the dynamic URL was generated by your application and not some third-party,
|
138
|
+
then simply tell Paperclip to process that style by extracting the definition from the stye name,
|
139
|
+
and then finally send the processed file back to your visitor's browser.
|
143
140
|
|
144
141
|
On subsequent requests, the attachment will already exist, and your HTTP server will simply return it without
|
145
|
-
ever hitting your
|
142
|
+
ever hitting your application. Sweet!
|
143
|
+
|
144
|
+
If your HTTP server is not configured to serve static assets, on subsequent requests, the middleware will simply
|
145
|
+
intercept the request and return the asset to the browser. It will not reprocess it. This is still far less efficient
|
146
|
+
than having your HTTP server serve the asset itself.
|
146
147
|
|
147
148
|
Contributing
|
148
149
|
------------
|
@@ -152,3 +153,10 @@ Contributing
|
|
152
153
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
153
154
|
4. Push to the branch (`git push origin my-new-feature`)
|
154
155
|
5. Create new Pull Request
|
156
|
+
|
157
|
+
Known Issues
|
158
|
+
------------
|
159
|
+
- Dynamic attachments aren't registered in time when class caching is disabled (Rails development, etc.).
|
160
|
+
Since we register the attachment when it's defined, a request for a dynamic attachment in an environment
|
161
|
+
where the class isn't preloaded will pass through the middleware, since the attachment won't be registered
|
162
|
+
yet, and never generate.
|
@@ -13,7 +13,11 @@ module DynamicPaperclip
|
|
13
13
|
style_position = path_with_wildcard.index('dynamic_*')
|
14
14
|
|
15
15
|
Dir.glob(path_with_wildcard) do |file|
|
16
|
-
|
16
|
+
style_name = file[style_position..-1].split('/').first
|
17
|
+
|
18
|
+
# In the event that the style name is used as the filename,
|
19
|
+
# we want to remove the extension for our style name
|
20
|
+
add_dynamic_style! File.basename(style_name, File.extname(style_name))
|
17
21
|
end
|
18
22
|
end
|
19
23
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
require 'action_controller/metal/data_streaming'
|
3
|
+
|
4
|
+
module DynamicPaperclip
|
5
|
+
class AttachmentStyleGenerator
|
6
|
+
# Rack middleware that catches requests for dynamic attachment styles
|
7
|
+
# that have not yet been generated and generates them.
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
request = Rack::Request.new(env)
|
15
|
+
|
16
|
+
DynamicPaperclip::AttachmentRegistry.each_definition do |klass, name, options|
|
17
|
+
if match = regexp_for_attachment_url(klass, (options[:url] || Attachment.default_options[:url])).match(request.path)
|
18
|
+
id = id_from_partition(match[:id])
|
19
|
+
attachment = klass.find(id).send(name)
|
20
|
+
|
21
|
+
# The definition will be escaped twice in the URL, so we need to unescape it once.
|
22
|
+
# We should always reference dynamic style names after escaping once - that's how they reside on the FS.
|
23
|
+
style_name = StyleNaming.dynamic_style_name_from_definition(URI.unescape(match[:definition]), false)
|
24
|
+
|
25
|
+
# Validate URL hash against requested style name
|
26
|
+
if DynamicPaperclip::UrlSecurity.valid_hash?(request.params['s'], style_name)
|
27
|
+
|
28
|
+
# Only process style if it doesn't exist,
|
29
|
+
# otherwise we may just be fielding a request for
|
30
|
+
# an existing style
|
31
|
+
attachment.process_dynamic_style style_name unless attachment.exists?(style_name)
|
32
|
+
|
33
|
+
return [
|
34
|
+
200,
|
35
|
+
{
|
36
|
+
'Content-Type' => attachment.content_type,
|
37
|
+
'Content-Transfer-Encoding' => 'binary',
|
38
|
+
'Content-Disposition' => "inline; filename=#{File.basename(attachment.path(style_name))}"
|
39
|
+
},
|
40
|
+
ActionController::DataStreaming::FileBody.new(attachment.path(style_name))
|
41
|
+
]
|
42
|
+
else
|
43
|
+
# Invalid hash, just 403
|
44
|
+
|
45
|
+
return [403, {}, []]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
@app.call env
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def regexp_for_attachment_url(klass, url)
|
56
|
+
Regexp.new '^' + url.
|
57
|
+
gsub('.' , '\.').
|
58
|
+
gsub(':id_partition', '(?<id>.*)').
|
59
|
+
gsub(':class' , klass.name.underscore.pluralize).
|
60
|
+
gsub(':style' , "dynamic_#{url_named_capture_group('definition')}").
|
61
|
+
gsub(/\:(\w+)/ , url_named_capture_group('\1')) + '$'
|
62
|
+
end
|
63
|
+
|
64
|
+
def url_named_capture_group(name)
|
65
|
+
"(?<#{name}>[^\/]*)"
|
66
|
+
end
|
67
|
+
|
68
|
+
def id_from_partition(partition)
|
69
|
+
partition.gsub('/', '').to_i
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -3,16 +3,7 @@ module DynamicPaperclip
|
|
3
3
|
end
|
4
4
|
|
5
5
|
module Errors
|
6
|
-
class UndefinedAttachment < DynamicPaperclip::Error
|
7
|
-
end
|
8
|
-
|
9
|
-
class MissingID < DynamicPaperclip::Error
|
10
|
-
end
|
11
|
-
|
12
6
|
class SecretNotSet < DynamicPaperclip::Error
|
13
7
|
end
|
14
|
-
|
15
|
-
class InvalidHash < DynamicPaperclip::Error
|
16
|
-
end
|
17
8
|
end
|
18
9
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module DynamicPaperclip
|
2
2
|
class HasAttachedFile < Paperclip::HasAttachedFile
|
3
|
-
def
|
4
|
-
|
3
|
+
def register_new_attachment
|
4
|
+
DynamicPaperclip::AttachmentRegistry.register(@klass, @name, @options)
|
5
5
|
|
6
|
-
|
6
|
+
super
|
7
7
|
end
|
8
8
|
|
9
9
|
private
|
@@ -31,21 +31,5 @@ module DynamicPaperclip
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
34
|
-
|
35
|
-
def add_route!
|
36
|
-
url = (@options[:url] || Attachment.default_options[:url]).
|
37
|
-
gsub(':id_partition', '*id_partition').
|
38
|
-
gsub(':class' , @klass.name.underscore.pluralize).
|
39
|
-
gsub(':style' , "dynamic_:definition")
|
40
|
-
|
41
|
-
action = "generate_#{@klass.name.underscore}"
|
42
|
-
default_attachment = @name.to_s.downcase.pluralize
|
43
|
-
|
44
|
-
Rails.application.routes do
|
45
|
-
get url,
|
46
|
-
:to => "DynamicPaperclip::AttachmentStyles##{action}",
|
47
|
-
:defaults => { :attachment => default_attachment }
|
48
|
-
end
|
49
|
-
end
|
50
34
|
end
|
51
35
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'dynamic_paperclip/attachment_style_generator'
|
2
|
+
|
3
|
+
module DynamicPaperclip
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer 'dynamic_paperclip.insert_middleware' do |app|
|
6
|
+
app.config.middleware.use "DynamicPaperclip::AttachmentStyleGenerator"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
data/lib/dynamic_paperclip.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
require 'paperclip'
|
2
2
|
require "dynamic_paperclip/errors"
|
3
3
|
require "dynamic_paperclip/config"
|
4
|
-
require "dynamic_paperclip/engine"
|
5
4
|
require "dynamic_paperclip/attachment"
|
6
5
|
require "dynamic_paperclip/has_attached_file"
|
7
6
|
require "dynamic_paperclip/paperclip_shim"
|
8
7
|
require "dynamic_paperclip/url_security"
|
9
8
|
require "dynamic_paperclip/style_naming"
|
9
|
+
require "dynamic_paperclip/attachment_registry"
|
10
|
+
|
11
|
+
require 'dynamic_paperclip/railtie' if defined?(Rails)
|
12
|
+
|
13
|
+
require 'active_support'
|
14
|
+
require 'active_support/core_ext/string/inflections'
|
10
15
|
|
11
16
|
module DynamicPaperclip
|
12
17
|
extend self
|
@@ -2,9 +2,7 @@ module DynamicPaperclip
|
|
2
2
|
class InstallGenerator < Rails::Generators::Base
|
3
3
|
def create_initializer_file
|
4
4
|
create_file 'config/initializers/dynamic_paperclip.rb' do
|
5
|
-
|
6
|
-
DynamicPaperclip.config.secret = '#{SecureRandom.urlsafe_base64(50)}'
|
7
|
-
init
|
5
|
+
"DynamicPaperclip.config.secret = '#{SecureRandom.urlsafe_base64(50)}'"
|
8
6
|
end
|
9
7
|
end
|
10
8
|
end
|
data/test/dummy/db/test.sqlite3
CHANGED
Binary file
|