dynamic_paperclip 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +150 -0
- data/Rakefile +40 -0
- data/app/controllers/dynamic_paperclip/attachment_styles_controller.rb +40 -0
- data/lib/dynamic_paperclip/attachment.rb +64 -0
- data/lib/dynamic_paperclip/config.rb +5 -0
- data/lib/dynamic_paperclip/engine.rb +4 -0
- data/lib/dynamic_paperclip/errors.rb +18 -0
- data/lib/dynamic_paperclip/has_attached_file.rb +47 -0
- data/lib/dynamic_paperclip/paperclip_shim.rb +7 -0
- data/lib/dynamic_paperclip/url_security.rb +11 -0
- data/lib/dynamic_paperclip/version.rb +3 -0
- data/lib/dynamic_paperclip.rb +16 -0
- data/lib/generators/dynamic_paperclip/install_generator.rb +11 -0
- data/test/controllers/attachment_style_controller_test.rb +144 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/models/photo.rb +3 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/dynamic_paperclip.rb +1 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +2 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20131102002336_create_photos.rb +9 -0
- data/test/dummy/db/schema.rb +24 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +24 -0
- data/test/dummy/log/test.log +14470 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/system/photos/images/000/000/001/dynamic_42x42/rails.png +0 -0
- data/test/dummy/public/system/photos/images/000/000/001/original/rails.png +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dynamic_paperclip_test.rb +7 -0
- data/test/fixtures/photos.yml +4 -0
- data/test/fixtures/rails.png +0 -0
- data/test/generators/install_generator_test.rb +20 -0
- data/test/integration/dynamic_attachment_styles_test.rb +26 -0
- data/test/test_helper.rb +21 -0
- data/test/tmp/config/initializers/dynamic_paperclip.rb +1 -0
- data/test/unit/attachment_test.rb +41 -0
- data/test/unit/has_attached_file_test.rb +29 -0
- metadata +216 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Room 118 Solutions, Inc.
|
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,150 @@
|
|
1
|
+
Dynamic Paperclip
|
2
|
+
=================
|
3
|
+
|
4
|
+
Dynamic Paperclip is an extension to the wonderful [Paperclip](http://github.com/thoughtbot/paperclip) gem
|
5
|
+
and a Ruby on Rails engine that allows for the creation of Paperclip attachment styles on-the-fly.
|
6
|
+
|
7
|
+
Instead of defining your attachment styles in your model, Dynamic Paperclip allows you to generate URL's
|
8
|
+
for arbitrary attachment styles (usually in your view), effectively pushing style definition there.
|
9
|
+
When the browser requests that asset, if it has not already been processed, it is then processed on-the-fly
|
10
|
+
and sent back to the browser. If the style has already been processed, it's served just like any other static asset.
|
11
|
+
|
12
|
+
Getting started
|
13
|
+
---------------
|
14
|
+
|
15
|
+
### Requirements
|
16
|
+
|
17
|
+
Dynamic Paperclip requires Paperclip 3.5.0 or above and Rails 3.2.0 or above (although it may work on earlier versions,
|
18
|
+
it's just untested at the moment).
|
19
|
+
|
20
|
+
It also only currently supports Paperclip's File Storage.
|
21
|
+
|
22
|
+
### Application
|
23
|
+
|
24
|
+
Add the gem to your gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'dynamic_paperclip'
|
28
|
+
```
|
29
|
+
|
30
|
+
Run the ``bundle`` command to install it.
|
31
|
+
|
32
|
+
After you install the gem, you need to run the generator:
|
33
|
+
|
34
|
+
```console
|
35
|
+
rails generate dynamic_paperclip:install
|
36
|
+
```
|
37
|
+
|
38
|
+
This will install an initializer that sets up a secret key used in validating that dynamic asset URL's
|
39
|
+
originated from your application.
|
40
|
+
|
41
|
+
Now, you're ready to start using Dynamic Paperclip. Change any attachment definitions that you'd like to make dynamic from:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
has_attached_file :avatar
|
45
|
+
```
|
46
|
+
|
47
|
+
To:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
has_dynamic_attached_file :avatar
|
51
|
+
```
|
52
|
+
|
53
|
+
You can continue defining styles there, too, you don't need to move over entirely to dynamic styles. You can have both!
|
54
|
+
|
55
|
+
Then, whenever you'd like a URL to a dynamic style, simply call ``#dynamic_url`` instead of ``#url`` on the attachment,
|
56
|
+
passing it the style definition that you would normally define in the ``:styles`` hash on ``has_attached_file``:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
@user.avatar.dynamic_url('100x100#')
|
60
|
+
```
|
61
|
+
|
62
|
+
### Server Configuration
|
63
|
+
|
64
|
+
If you're using Rails to serve static assets, then no configuration is required. But if you're not,
|
65
|
+
which you shouldn't be in any production environment, then you just need to make sure that your HTTP server
|
66
|
+
is configured to serve static assets if they exist, but pass the request along to your Rails application
|
67
|
+
if they do not.
|
68
|
+
|
69
|
+
For example, on Nginx, this would be accomplished with something along the lines of:
|
70
|
+
|
71
|
+
```nginx
|
72
|
+
upstream rails {
|
73
|
+
# ...
|
74
|
+
}
|
75
|
+
|
76
|
+
server {
|
77
|
+
# ...
|
78
|
+
|
79
|
+
try_files $uri @rails
|
80
|
+
|
81
|
+
location @rails {
|
82
|
+
# ...
|
83
|
+
|
84
|
+
proxy_pass http://rails;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
```
|
88
|
+
|
89
|
+
This basically says "If the requested URI exists, send that to the browser, if not, pass it along to the Rails app.",
|
90
|
+
and is a pretty standard Nginx setup.
|
91
|
+
|
92
|
+
Why?
|
93
|
+
----
|
94
|
+
|
95
|
+
Because as your Rails application grows, you may discover that you have a large number of attachment styles. This is
|
96
|
+
slowing down your requests, because every time a user attempts to upload a file, Paperclip must process each and every
|
97
|
+
one of those styles right then and there, in the middle of the request.
|
98
|
+
|
99
|
+
Also, when dealing with images, I think it makes more sense to specify the dimensions of a thumbnail in the view
|
100
|
+
that needs it, and not in the model.
|
101
|
+
|
102
|
+
How does this wizardry work?
|
103
|
+
---------------------------
|
104
|
+
|
105
|
+
It's pretty simple, actually. When you define a dynamic attachment on your model, Dynamic Paperclip defines
|
106
|
+
a route in your application that routes the URL you've specified (or Paperclip's default) to the ``DynamicPaperclip::AttachmentStylesController``.
|
107
|
+
|
108
|
+
For security purposes, the class is interpolated and thus hardcoded into the route. So, if your Paperclip attachment definition looks like:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class User
|
112
|
+
has_dynamic_attached_file :avatar, url: '/system/:class/:attachment/:id/:style'
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
Dynamic Paperclip will generate the following route:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
get '/system/users/:attachment/:id/:style', to: 'DynamicPaperclip::AttachmentStyles#generate_user'
|
120
|
+
```
|
121
|
+
|
122
|
+
Now, in your view, you might call something like this:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
@photo.avatar.dynamic_url('50x50')
|
126
|
+
```
|
127
|
+
|
128
|
+
Which will return the following url (assuming a JPG avatar and a User ID of 42):
|
129
|
+
|
130
|
+
```
|
131
|
+
/system/users/avatars/42/dynamic_50x50.jpg?s=secrethash
|
132
|
+
```
|
133
|
+
|
134
|
+
When your visitor's browser requests that URL, if that particular style has already been processed,
|
135
|
+
it'll be served up by your HTTP server, but if it hasn't, Rails will route it to our controller,
|
136
|
+
which validates that "secrethash" to ensure that the dynamic URL was generated by your application,
|
137
|
+
and not some third-party, then simply tells Paperclip to process that style by extracting the definition
|
138
|
+
from the stye name, and then sends it back to your visitor via ``#send_file``.
|
139
|
+
|
140
|
+
On subsequent requests, the attachment will already exist, and your HTTP server will simply return it without
|
141
|
+
ever hitting your Rails application. Sweet!
|
142
|
+
|
143
|
+
Contributing
|
144
|
+
------------
|
145
|
+
|
146
|
+
1. Fork it
|
147
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
148
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
149
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
150
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'DynamicPaperclip'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
Bundler::GemHelper.install_tasks
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
|
32
|
+
Rake::TestTask.new(:test) do |t|
|
33
|
+
t.libs << 'lib'
|
34
|
+
t.libs << 'test'
|
35
|
+
t.pattern = 'test/**/*_test.rb'
|
36
|
+
t.verbose = false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DynamicPaperclip
|
2
|
+
class AttachmentStylesController < ApplicationController
|
3
|
+
def action_missing(name, *args, &block)
|
4
|
+
if name =~ /^generate_(.+)$/
|
5
|
+
send :generate, $1
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def generate(class_name)
|
12
|
+
klass = class_name.camelize.constantize
|
13
|
+
attachment_name = params[:attachment].singularize.to_sym
|
14
|
+
|
15
|
+
# Ensure that we have a valid attachment name and an ID
|
16
|
+
raise Errors::UndefinedAttachment unless klass.attachment_definitions[attachment_name]
|
17
|
+
raise Errors::MissingID unless params[:id] || params[:id_partition]
|
18
|
+
|
19
|
+
id = params[:id] || id_from_partition(params[:id_partition])
|
20
|
+
|
21
|
+
attachment = klass.find(id).send(attachment_name)
|
22
|
+
|
23
|
+
# Only validate style name if it's dynamic,
|
24
|
+
# and only process style if it's dynamic and doesn't exist,
|
25
|
+
# otherwise we may just be fielding a request for
|
26
|
+
# an existing style (i.e. serve_static_assets is true)
|
27
|
+
if params[:style] =~ /^dynamic_/
|
28
|
+
raise Errors::InvalidHash unless DynamicPaperclip::UrlSecurity.valid_hash?(params[:s], params[:style])
|
29
|
+
|
30
|
+
attachment.process_dynamic_style params[:style] unless attachment.exists?(params[:style])
|
31
|
+
end
|
32
|
+
|
33
|
+
send_file attachment.path(params[:style]), :disposition => 'inline', :type => attachment.content_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def id_from_partition(partition)
|
37
|
+
partition.gsub('/', '').to_i
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module DynamicPaperclip
|
2
|
+
class Attachment < Paperclip::Attachment
|
3
|
+
attr_reader :dynamic_styles
|
4
|
+
|
5
|
+
def initialize(name, instance, options = {})
|
6
|
+
super
|
7
|
+
|
8
|
+
@dynamic_styles = {}
|
9
|
+
|
10
|
+
# Add existing dynamic styles
|
11
|
+
if instance.persisted?
|
12
|
+
path_with_wildcard = path('dynamic_*')
|
13
|
+
style_position = path_with_wildcard.index('dynamic_*')
|
14
|
+
|
15
|
+
Dir.glob(path_with_wildcard) do |file|
|
16
|
+
add_dynamic_style! file[style_position..-1].split('/').first
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def styles
|
22
|
+
super.merge dynamic_styles
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_dynamic_style(name)
|
26
|
+
add_dynamic_style! name
|
27
|
+
reprocess! name
|
28
|
+
end
|
29
|
+
|
30
|
+
def dynamic_url(definition)
|
31
|
+
raise DynamicPaperclip::Errors::SecretNotSet, "No secret has been configured. Please run the dynamic_paperclip:install generator." unless DynamicPaperclip.config.secret.present?
|
32
|
+
|
33
|
+
style_name = dynamic_style_name_from_definition(definition)
|
34
|
+
|
35
|
+
url = url(style_name)
|
36
|
+
|
37
|
+
delimiter_char = url.match(/\?.+=/) ? '&' : '?'
|
38
|
+
|
39
|
+
"#{url}#{delimiter_char}s=#{UrlSecurity.generate_hash(style_name)}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Generate style name from style definition,
|
45
|
+
# only supports strings at the moment
|
46
|
+
def dynamic_style_name_from_definition(options)
|
47
|
+
if options.is_a?(String)
|
48
|
+
"dynamic_#{URI.escape(options)}".to_sym
|
49
|
+
else
|
50
|
+
raise 'Only String options are supported with dynamic attachments'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Reverse of #dynamic_style_name_from_definition,
|
55
|
+
# given a dynamic style name, extracts the definition (style options)
|
56
|
+
def style_definition_from_dynamic_style_name(name)
|
57
|
+
URI.unescape name[8..-1]
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_dynamic_style!(name)
|
61
|
+
@dynamic_styles[name.to_sym] = Paperclip::Style.new(name, style_definition_from_dynamic_style_name(name), self)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module DynamicPaperclip
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
module Errors
|
6
|
+
class UndefinedAttachment < DynamicPaperclip::Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class MissingID < DynamicPaperclip::Error
|
10
|
+
end
|
11
|
+
|
12
|
+
class SecretNotSet < DynamicPaperclip::Error
|
13
|
+
end
|
14
|
+
|
15
|
+
class InvalidHash < DynamicPaperclip::Error
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DynamicPaperclip
|
2
|
+
class HasAttachedFile < Paperclip::HasAttachedFile
|
3
|
+
def initialize(klass, name, options)
|
4
|
+
super
|
5
|
+
|
6
|
+
add_route!
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# TODO: Are there any alternatives to literally copying this
|
12
|
+
# method from Paperclip::HasAttachedFile to get Ruby to find
|
13
|
+
# DynamicPaperclip::Attachment instead of Paperclip::Attachment?
|
14
|
+
def define_instance_getter
|
15
|
+
name = @name
|
16
|
+
options = @options
|
17
|
+
|
18
|
+
@klass.send :define_method, @name do |*args|
|
19
|
+
ivar = "@attachment_#{name}"
|
20
|
+
attachment = instance_variable_get(ivar)
|
21
|
+
|
22
|
+
if attachment.nil?
|
23
|
+
attachment = Attachment.new(name, self, options)
|
24
|
+
instance_variable_set(ivar, attachment)
|
25
|
+
end
|
26
|
+
|
27
|
+
if args.length > 0
|
28
|
+
attachment.to_s(args.first)
|
29
|
+
else
|
30
|
+
attachment
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_route!
|
36
|
+
url = (@options[:url] || Attachment.default_options[:url]).gsub(':id_partition', '*id_partition').gsub(':class', @klass.name.underscore.pluralize)
|
37
|
+
action = "generate_#{@klass.name.underscore}"
|
38
|
+
default_attachment = @name.to_s.downcase.pluralize
|
39
|
+
|
40
|
+
Rails.application.routes do
|
41
|
+
get url,
|
42
|
+
:to => "DynamicPaperclip::AttachmentStyles##{action}",
|
43
|
+
:defaults => { :attachment => default_attachment }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module DynamicPaperclip
|
2
|
+
module UrlSecurity
|
3
|
+
def self.generate_hash(style_name)
|
4
|
+
Digest::SHA1.hexdigest "#{DynamicPaperclip.config.secret}#{style_name}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.valid_hash?(hash, style_name)
|
8
|
+
generate_hash(style_name) == hash
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'paperclip'
|
2
|
+
require "dynamic_paperclip/errors"
|
3
|
+
require "dynamic_paperclip/config"
|
4
|
+
require "dynamic_paperclip/engine"
|
5
|
+
require "dynamic_paperclip/attachment"
|
6
|
+
require "dynamic_paperclip/has_attached_file"
|
7
|
+
require "dynamic_paperclip/paperclip_shim"
|
8
|
+
require "dynamic_paperclip/url_security"
|
9
|
+
|
10
|
+
module DynamicPaperclip
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def config
|
14
|
+
@@config ||= Config.new
|
15
|
+
end
|
16
|
+
end
|