dynamic_paperclip 0.0.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.
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