dynamic_paperclip 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +150 -0
  3. data/Rakefile +40 -0
  4. data/app/controllers/dynamic_paperclip/attachment_styles_controller.rb +40 -0
  5. data/lib/dynamic_paperclip/attachment.rb +64 -0
  6. data/lib/dynamic_paperclip/config.rb +5 -0
  7. data/lib/dynamic_paperclip/engine.rb +4 -0
  8. data/lib/dynamic_paperclip/errors.rb +18 -0
  9. data/lib/dynamic_paperclip/has_attached_file.rb +47 -0
  10. data/lib/dynamic_paperclip/paperclip_shim.rb +7 -0
  11. data/lib/dynamic_paperclip/url_security.rb +11 -0
  12. data/lib/dynamic_paperclip/version.rb +3 -0
  13. data/lib/dynamic_paperclip.rb +16 -0
  14. data/lib/generators/dynamic_paperclip/install_generator.rb +11 -0
  15. data/test/controllers/attachment_style_controller_test.rb +144 -0
  16. data/test/dummy/Rakefile +7 -0
  17. data/test/dummy/app/controllers/application_controller.rb +3 -0
  18. data/test/dummy/app/models/photo.rb +3 -0
  19. data/test/dummy/config/application.rb +59 -0
  20. data/test/dummy/config/boot.rb +10 -0
  21. data/test/dummy/config/database.yml +25 -0
  22. data/test/dummy/config/environment.rb +5 -0
  23. data/test/dummy/config/environments/test.rb +37 -0
  24. data/test/dummy/config/initializers/dynamic_paperclip.rb +1 -0
  25. data/test/dummy/config/initializers/secret_token.rb +7 -0
  26. data/test/dummy/config/initializers/session_store.rb +8 -0
  27. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  28. data/test/dummy/config/locales/en.yml +5 -0
  29. data/test/dummy/config/routes.rb +2 -0
  30. data/test/dummy/config.ru +4 -0
  31. data/test/dummy/db/development.sqlite3 +0 -0
  32. data/test/dummy/db/migrate/20131102002336_create_photos.rb +9 -0
  33. data/test/dummy/db/schema.rb +24 -0
  34. data/test/dummy/db/test.sqlite3 +0 -0
  35. data/test/dummy/log/development.log +24 -0
  36. data/test/dummy/log/test.log +14470 -0
  37. data/test/dummy/public/404.html +26 -0
  38. data/test/dummy/public/422.html +26 -0
  39. data/test/dummy/public/500.html +25 -0
  40. data/test/dummy/public/favicon.ico +0 -0
  41. data/test/dummy/public/system/photos/images/000/000/001/dynamic_42x42/rails.png +0 -0
  42. data/test/dummy/public/system/photos/images/000/000/001/original/rails.png +0 -0
  43. data/test/dummy/script/rails +6 -0
  44. data/test/dynamic_paperclip_test.rb +7 -0
  45. data/test/fixtures/photos.yml +4 -0
  46. data/test/fixtures/rails.png +0 -0
  47. data/test/generators/install_generator_test.rb +20 -0
  48. data/test/integration/dynamic_attachment_styles_test.rb +26 -0
  49. data/test/test_helper.rb +21 -0
  50. data/test/tmp/config/initializers/dynamic_paperclip.rb +1 -0
  51. data/test/unit/attachment_test.rb +41 -0
  52. data/test/unit/has_attached_file_test.rb +29 -0
  53. 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,5 @@
1
+ module DynamicPaperclip
2
+ class Config
3
+ attr_accessor :secret
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module DynamicPaperclip
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ 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,7 @@
1
+ module Paperclip
2
+ module ClassMethods
3
+ def has_dynamic_attached_file(name, options = {})
4
+ DynamicPaperclip::HasAttachedFile.define_on(self, name, options)
5
+ end
6
+ end
7
+ 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,3 @@
1
+ module DynamicPaperclip
2
+ VERSION = "0.0.1"
3
+ 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