├── .gitignore
├── .rspec
├── .travis.yml
├── Gemfile
├── History.md
├── LICENSE
├── README.md
├── Rakefile
├── dev
├── grid.jpg
├── irbrc.rb
├── rails_template.rb
├── test.ru
└── test_rails
├── dragonfly.gemspec
├── lib
├── dragonfly.rb
├── dragonfly
│ ├── app.rb
│ ├── configurable.rb
│ ├── content.rb
│ ├── cookie_monster.rb
│ ├── core_ext
│ │ ├── array.rb
│ │ ├── hash.rb
│ │ └── object.rb
│ ├── file_data_store.rb
│ ├── has_filename.rb
│ ├── hash_with_css_style_keys.rb
│ ├── image_magick
│ │ ├── analysers
│ │ │ └── image_properties.rb
│ │ ├── commands.rb
│ │ ├── generators
│ │ │ ├── plain.rb
│ │ │ ├── plasma.rb
│ │ │ └── text.rb
│ │ ├── plugin.rb
│ │ └── processors
│ │ │ ├── encode.rb
│ │ │ └── thumb.rb
│ ├── job.rb
│ ├── job
│ │ ├── fetch.rb
│ │ ├── fetch_file.rb
│ │ ├── fetch_url.rb
│ │ ├── generate.rb
│ │ ├── process.rb
│ │ └── step.rb
│ ├── job_endpoint.rb
│ ├── memory_data_store.rb
│ ├── middleware.rb
│ ├── model.rb
│ ├── model
│ │ ├── attachment.rb
│ │ ├── attachment_class_methods.rb
│ │ ├── class_methods.rb
│ │ ├── instance_methods.rb
│ │ └── validations.rb
│ ├── param_validators.rb
│ ├── rails
│ │ └── images.rb
│ ├── railtie.rb
│ ├── register.rb
│ ├── response.rb
│ ├── routed_endpoint.rb
│ ├── serializer.rb
│ ├── server.rb
│ ├── shell.rb
│ ├── spec
│ │ └── data_store_examples.rb
│ ├── temp_object.rb
│ ├── url_attributes.rb
│ ├── url_mapper.rb
│ ├── utils.rb
│ ├── version.rb
│ └── whitelist.rb
└── rails
│ └── generators
│ └── dragonfly
│ ├── USAGE
│ ├── dragonfly_generator.rb
│ └── templates
│ └── initializer.rb.erb
├── samples
├── DSC02119.JPG
├── a.jp2
├── beach.jpg
├── beach.png
├── egg.png
├── gif.gif
├── mevs' white pixel.png
├── round.gif
├── sample.docx
└── taj.jpg
├── spec
├── dragonfly
│ ├── app_spec.rb
│ ├── configurable_spec.rb
│ ├── content_spec.rb
│ ├── cookie_monster_spec.rb
│ ├── core_ext
│ │ ├── array_spec.rb
│ │ └── hash_spec.rb
│ ├── file_data_store_spec.rb
│ ├── has_filename_spec.rb
│ ├── hash_with_css_style_keys_spec.rb
│ ├── image_magick
│ │ ├── analysers
│ │ │ └── image_properties_spec.rb
│ │ ├── commands_spec.rb
│ │ ├── generators
│ │ │ ├── plain_spec.rb
│ │ │ ├── plasma_spec.rb
│ │ │ └── text_spec.rb
│ │ ├── plugin_spec.rb
│ │ └── processors
│ │ │ ├── encode_spec.rb
│ │ │ └── thumb_spec.rb
│ ├── job
│ │ ├── fetch_file_spec.rb
│ │ ├── fetch_spec.rb
│ │ ├── fetch_url_spec.rb
│ │ ├── generate_spec.rb
│ │ └── process_spec.rb
│ ├── job_endpoint_spec.rb
│ ├── job_spec.rb
│ ├── memory_data_store_spec.rb
│ ├── middleware_spec.rb
│ ├── model
│ │ ├── active_record_spec.rb
│ │ ├── model_spec.rb
│ │ └── validations_spec.rb
│ ├── param_validators_spec.rb
│ ├── register_spec.rb
│ ├── routed_endpoint_spec.rb
│ ├── serializer_spec.rb
│ ├── server_spec.rb
│ ├── shell_spec.rb
│ ├── temp_object_spec.rb
│ ├── url_attributes_spec.rb
│ ├── url_mapper_spec.rb
│ ├── utils_spec.rb
│ └── whitelist_spec.rb
├── dragonfly_spec.rb
├── fixtures
│ └── deprecated_stored_content
│ │ ├── eggs.bonus
│ │ └── eggs.bonus.meta
├── functional
│ ├── cleanup_spec.rb
│ ├── configuration_spec.rb
│ ├── model_urls_spec.rb
│ ├── remote_on_the_fly_spec.rb
│ ├── shell_commands_spec.rb
│ ├── to_response_spec.rb
│ └── urls_spec.rb
├── spec_helper.rb
└── support
│ ├── argument_matchers.rb
│ ├── image_matchers.rb
│ ├── model_helpers.rb
│ ├── rack_helpers.rb
│ └── simple_matchers.rb
└── tmp
└── .gitignore
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | pkg
3 | .yardoc
4 | doc
5 | .bundle
6 | Gemfile.lock
7 | .s3_spec.yml
8 | ***.rbc
9 | dragonfly.log
10 | _site
11 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - "2.2"
4 | - "2.3"
5 | - "2.4"
6 | - "2.5"
7 | - "2.6"
8 | - "2.7"
9 | - "3.0"
10 | - "jruby"
11 |
12 | env:
13 | global:
14 | - SKIP_FLAKY_TESTS: true
15 |
16 | before_install:
17 | - sudo apt-get update
18 | - sudo apt-get install -y ghostscript
19 |
20 | script: bundle exec rspec spec --backtrace
21 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
4 |
5 | group :development do
6 | gem 'pry'
7 | gem 'rake'
8 | end
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2013 Mark Evans
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.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Dragonfly
2 | ===========
3 |
4 | [](https://travis-ci.org/markevans/dragonfly)
5 |
6 | Hello!!
7 | Dragonfly is a highly customizable ruby gem for handling images and other attachments and is already in use on thousands of websites.
8 |
9 | If you want to generate image thumbnails in Rails ...
10 | ```ruby
11 | class User < ActiveRecord::Base # model
12 | dragonfly_accessor :photo
13 | end
14 | ```
15 | ```erb
16 | <%= image_tag @user.photo.thumb('300x200#').url if @user.photo_stored? # view %>
17 | ```
18 |
19 | ... or generate text images on-demand in Sinatra ...
20 | ```ruby
21 | get "/:text" do |text|
22 | Dragonfly.app.generate(:text, text, "font-size" => 32).to_response(env)
23 | end
24 | ```
25 |
26 | ... or just generally manage attachments in your web app ...
27 | ```ruby
28 | wav = Dragonfly.app.fetch_url("http://free.music/lard.wav") # GET from t'interwebs
29 | mp3 = wav.to_mp3 # to_mp3 is a custom processor
30 | uid = mp3.store # store in the configured datastore, e.g. S3
31 |
32 | url = Dragonfly.app.remote_url_for(uid) # ===> http://s3.amazon.com/my-stuff/lard.mp3
33 | ```
34 |
35 | ... then Dragonfly is for you! See [the documentation](http://markevans.github.io/dragonfly) to get started!
36 |
37 | Documentation
38 | =============
39 | THE MAIN DOCUMENTATION IS HERE!!!
40 |
41 | RDoc documentation is here
42 |
43 | Installation
44 | ============
45 |
46 | gem install dragonfly
47 |
48 | or in your Gemfile
49 | ```ruby
50 | gem 'dragonfly', '~> 1.4.0'
51 | ```
52 |
53 | Require with
54 | ```ruby
55 | require 'dragonfly'
56 | ```
57 | Articles
58 | ========
59 | See [the Articles wiki](http://github.com/markevans/dragonfly/wiki/Articles) for articles and tutorials.
60 |
61 | Please feel free to contribute!!
62 |
63 | Examples
64 | ========
65 | See [the Wiki](http://github.com/markevans/dragonfly/wiki) and see the pages list for examples.
66 |
67 | Please feel free to contribute!!
68 |
69 | Plugins / add-ons
70 | =================
71 | See [the Add-ons wiki](http://github.com/markevans/dragonfly/wiki/Dragonfly-add-ons).
72 |
73 | Please feel free to contribute!!
74 |
75 | Security notice!
76 | =================
77 | If you have set `verify_urls` to `false` (which is **not** recommended) then you should upgrade to version `1.4.x` for a security fix ([CVE-2021-33564](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33564)).
78 |
79 | Issues
80 | ======
81 | Please use the github issue tracker if you have any issues.
82 |
83 | Known Issues
84 | ============
85 | There are known issues when using with json gem version 1.5.2 which can potentially cause an "incorrect sha" error for files with non-ascii characters in the name. Please see https://github.com/markevans/dragonfly/issues/387 for more information.
86 |
87 | Suggestions/Questions
88 | =====================
89 | Google group dragonfly-users
90 |
91 | Ruby Versions
92 | =============
93 | See [Travis-CI](https://travis-ci.org/markevans/dragonfly) for tested versions.
94 |
95 | Upgrading from v0.9 to v1.0
96 | ===========================
97 | Dragonfly has changed somewhat since version 0.9.
98 | See [the Upgrading wiki](http://github.com/markevans/dragonfly/wiki/Upgrading-from-0.9-to-1.0) for notes on changes, and feel free to add anything you come across while upgrading!
99 |
100 | Changes are listed in [History.md](https://github.com/markevans/dragonfly/blob/master/History.md)
101 |
102 | If for whatever reason you can't upgrade, then
103 | the docs for version 0.9.x are here.
104 |
105 | Credits
106 | =======
107 | [Mark Evans](http://github.com/markevans) (author) with awesome contributions from
108 | these guys
109 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
--------------------------------------------------------------------------------
/dev/grid.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/dev/grid.jpg
--------------------------------------------------------------------------------
/dev/irbrc.rb:
--------------------------------------------------------------------------------
1 | require "rubygems"
2 | require "bundler/setup"
3 | $:.unshift(File.expand_path('../../lib', __FILE__))
4 | require 'dragonfly'
5 | require 'pry'
6 |
7 | APP = Dragonfly.app.configure do
8 | plugin :imagemagick
9 | datastore :memory
10 | end
11 |
12 | class Model
13 | extend Dragonfly::Model
14 | attr_accessor :image_uid, :image_name, :image_width, :small_image_uid
15 | dragonfly_accessor :image
16 | dragonfly_accessor :small_image
17 | end
18 |
19 | def reload
20 | self.class.send(:remove_const, :APP)
21 | Dragonfly.constants.each do |const|
22 | Dragonfly.send(:remove_const, const)
23 | end
24 | $LOADED_FEATURES.grep(/dragonfly/).each do |path|
25 | load path
26 | end
27 | nil
28 | end
29 | alias reload! reload
30 |
31 | puts "Loaded stuff from dragonfly irbrc"
32 | puts "\nAvailable sample images:\n"
33 | puts Dir['samples/*']
34 | puts "\nAvailable constants:\n"
35 | puts "APP"
36 | puts "\nModel:\n"
37 | puts "dragonfly_accessor :image with image_name, image_width"
38 | puts
39 |
40 |
--------------------------------------------------------------------------------
/dev/rails_template.rb:
--------------------------------------------------------------------------------
1 | after_bundle do
2 | gem 'dragonfly', :path => File.expand_path('../..', __FILE__)
3 | generate "dragonfly"
4 | generate "scaffold", "photo image_uid:string image_name:string"
5 | rake "db:migrate"
6 | route %(
7 | get "text/:text" => Dragonfly.app.endpoint { |params, app|
8 | app.generate(:text, params[:text])
9 | }
10 | )
11 | route "root :to => 'photos#index'"
12 | run "rm -rf public/index.html"
13 |
14 | possible_base_classes = ['ActiveRecord::Base', 'ApplicationRecord']
15 | possible_base_classes.each do |base_class|
16 | inject_into_file 'app/models/photo.rb', :after => "class Photo < #{base_class}\n" do
17 | %(
18 | attr_accessible :image rescue nil
19 | dragonfly_accessor :image
20 | )
21 | end
22 | end
23 |
24 | gsub_file 'app/views/photos/_form.html.erb', /^.*:image_.*$/, ''
25 |
26 | inject_into_file 'app/views/photos/_form.html.erb', :before => %(
\n) do
27 | %(
28 |
29 | <%= form.label :image %>
30 | <%= form.file_field :image %>
31 |
32 |
33 | <%= image_tag @photo.image.thumb('100x100').url if @photo.image_stored? %>
34 | )
35 | end
36 |
37 | gsub_file "app/controllers/photos_controller.rb", "permit(", "permit(:image, "
38 |
39 | append_file 'app/views/photos/show.html.erb', %(
40 | <%= image_tag @photo.image.thumb('300x300').url if @photo.image_stored? %>
41 | )
42 | end
--------------------------------------------------------------------------------
/dev/test.ru:
--------------------------------------------------------------------------------
1 | require "rubygems"
2 | require "bundler/setup"
3 | $:.unshift(File.expand_path('../../lib', __FILE__))
4 | require 'dragonfly'
5 |
6 | Dragonfly.logger = Logger.new(STDOUT)
7 | Dragonfly.app.configure do
8 | plugin :imagemagick
9 | url_format '/images/:job'
10 | fetch_file_whitelist [String]
11 | end
12 |
13 | class App
14 | def call(env)
15 | image = Dragonfly.app.fetch_file('grid.jpg')
16 | request = Rack::Request.new(env)
17 | error = nil
18 | if request['code']
19 | begin
20 | img_src = eval("image.#{request['code']}").url
21 | rescue StandardError => e
22 | error = e
23 | end
24 | end
25 | [
26 | 200,
27 | {'content-type' => 'text/html'},
28 | [%(
29 |
38 |
#{error}
39 |
40 |
41 | Original (#{image.width}x#{image.height}) |
42 |  |
43 |
44 |
45 | |
46 |  |
47 |
48 |
49 | )]
50 | ]
51 | end
52 | end
53 |
54 | use Dragonfly::Middleware
55 | run App.new
56 |
57 |
--------------------------------------------------------------------------------
/dev/test_rails:
--------------------------------------------------------------------------------
1 | #!env ruby
2 | dir = ARGV[0]
3 |
4 | unless dir
5 | puts "Usage:"
6 | puts "\t#{$0} DESTINATION"
7 | exit
8 | end
9 |
10 | run = proc{|command|
11 | puts "\n*** Running: #{command} ***\n"
12 | system command
13 | }
14 |
15 | template_path = File.expand_path('../rails_template.rb', __FILE__)
16 |
17 | run["rm -rf #{dir}"]
18 | run["rails new #{dir} -m #{template_path} -J -T"]
19 |
20 |
--------------------------------------------------------------------------------
/dragonfly.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path("../lib", __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require "dragonfly/version"
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "dragonfly"
8 | spec.version = Dragonfly::VERSION
9 | spec.authors = ["Mark Evans"]
10 | spec.email = "mark@new-bamboo.co.uk"
11 | spec.description = "Dragonfly is a framework that enables on-the-fly processing for any content type.\n It is especially suited to image handling. Its uses range from image thumbnails to standard attachments to on-demand text generation."
12 | spec.summary = "Ideal gem for handling attachments in Rails, Sinatra and Rack applications."
13 | spec.homepage = "http://github.com/markevans/dragonfly"
14 | spec.license = "MIT"
15 | spec.files = `git ls-files`.split($/)
16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18 | spec.require_paths = ["lib"]
19 | spec.extra_rdoc_files = [
20 | "LICENSE",
21 | "README.md",
22 | ]
23 |
24 | # Runtime dependencies
25 | spec.add_runtime_dependency("rack", ">= 1.3")
26 | spec.add_runtime_dependency("multi_json", "~> 1.0")
27 | spec.add_runtime_dependency("addressable", "~> 2.3")
28 | spec.add_runtime_dependency("ostruct", "~> 0.6.1")
29 |
30 | # Development dependencies
31 | spec.add_development_dependency("rspec", "~> 3.0")
32 | spec.add_development_dependency("webmock")
33 | spec.add_development_dependency("activemodel")
34 | if RUBY_PLATFORM == "java"
35 | spec.add_development_dependency("jruby-openssl")
36 | else
37 | spec.add_development_dependency("activerecord")
38 | spec.add_development_dependency("sqlite3")
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/dragonfly.rb:
--------------------------------------------------------------------------------
1 | require 'rbconfig'
2 | require 'logger'
3 | require 'dragonfly/version'
4 | require 'dragonfly/core_ext/object'
5 | require 'dragonfly/core_ext/array'
6 | require 'dragonfly/core_ext/hash'
7 | require 'dragonfly/app'
8 | require 'dragonfly/image_magick/plugin'
9 | require 'dragonfly/file_data_store'
10 | require 'dragonfly/memory_data_store'
11 | require 'dragonfly/model'
12 | require 'dragonfly/middleware'
13 |
14 | if defined?(::Rails)
15 | require 'dragonfly/railtie'
16 | require 'dragonfly/model/validations'
17 | end
18 |
19 | module Dragonfly
20 | class << self
21 |
22 | def app(name=nil)
23 | App.instance(name)
24 | end
25 |
26 | def [](name)
27 | App[name]
28 | end
29 |
30 | def running_on_windows?
31 | !!(RbConfig::CONFIG['host_os'] =~ %r!(msdos|mswin|djgpp|mingw)!)
32 | end
33 |
34 | # Logging
35 | def logger
36 | @logger = Logger.new('dragonfly.log') unless instance_variable_defined?(:@logger)
37 | @logger
38 | end
39 | attr_writer :logger
40 |
41 | [:debug, :warn, :info].each do |method|
42 | define_method method do |message|
43 | return unless logger
44 | logger.send(method, "DRAGONFLY: #{message}")
45 | end
46 | end
47 |
48 | # Register plugins so we can do e.g.
49 | # Dragonfly.app.configure do
50 | # plugin :imagemagick
51 | # end
52 | App.register_plugin(:imagemagick){ ImageMagick::Plugin.new }
53 | App.register_plugin(:image_magick){ ImageMagick::Plugin.new }
54 |
55 | # Register saved datastores so we can do e.g.
56 | # Dragonfly.app.configure do
57 | # datastore :file
58 | # end
59 | App.register_datastore(:file){ FileDataStore }
60 | App.register_datastore(:memory){ MemoryDataStore }
61 |
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/dragonfly/configurable.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module Configurable
3 |
4 | # Exceptions
5 | class UnregisteredPlugin < RuntimeError; end
6 |
7 | class Configurer
8 |
9 | class << self
10 | private
11 |
12 | def writer(*args)
13 | names, opts = extract_options(args)
14 | names.each do |name|
15 | define_method name do |value|
16 | if opts[:for]
17 | obj.send(opts[:for]).send("#{name}=", value)
18 | else
19 | obj.send("#{name}=", value)
20 | end
21 | end
22 | end
23 | end
24 |
25 | def meth(*args)
26 | names, opts = extract_options(args)
27 | names.each do |name|
28 | define_method name do |*args, &block|
29 | if opts[:for]
30 | obj.send(opts[:for]).send(name, *args, &block)
31 | else
32 | obj.send(name, *args, &block)
33 | end
34 | end
35 | end
36 | end
37 |
38 | def extract_options(args)
39 | opts = args.last.is_a?(Hash) ? args.pop : {}
40 | [args, opts]
41 | end
42 | end
43 |
44 | def initialize(&block)
45 | (class << self; self; end).class_eval(&block)
46 | end
47 |
48 | def configure(obj, &block)
49 | previous_obj = @obj
50 | @obj = obj
51 | instance_eval(&block)
52 | @obj = previous_obj
53 | end
54 |
55 | def configure_with_plugin(obj, plugin, *args, &block)
56 | if plugin.is_a?(Symbol)
57 | symbol = plugin
58 | raise(UnregisteredPlugin, "plugin #{symbol.inspect} is not registered") unless registered_plugins[symbol]
59 | plugin = registered_plugins[symbol].call
60 | obj.plugins[symbol] = plugin if obj.respond_to?(:plugins)
61 | end
62 | plugin.call(obj, *args, &block)
63 | plugin
64 | end
65 |
66 | def register_plugin(name, &block)
67 | registered_plugins[name] = block
68 | end
69 |
70 | def plugin(plugin, *args, &block)
71 | configure_with_plugin(obj, plugin, *args, &block)
72 | end
73 |
74 | private
75 |
76 | attr_reader :obj
77 |
78 | def registered_plugins
79 | @registered_plugins ||= {}
80 | end
81 | end
82 |
83 | #######
84 |
85 | def set_up_config(&setup_block)
86 | self.configurer = Configurer.new(&setup_block)
87 | class_eval do
88 | def configure(&block)
89 | self.class.configurer.configure(self, &block)
90 | self
91 | end
92 |
93 | def configure_with(plugin, *args, &block)
94 | self.class.configurer.configure_with_plugin(self, plugin, *args, &block)
95 | self
96 | end
97 |
98 | def plugins
99 | @plugins ||= {}
100 | end
101 | end
102 | end
103 |
104 | attr_accessor :configurer
105 |
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/lib/dragonfly/cookie_monster.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | class CookieMonster
3 |
4 | def initialize(app)
5 | @app = app
6 | end
7 |
8 | def call(env)
9 | status, headers, body = @app.call(env)
10 | headers.delete('Set-Cookie') if env['dragonfly.job']
11 | [status, headers, body]
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dragonfly/core_ext/array.rb:
--------------------------------------------------------------------------------
1 | class Array
2 |
3 | def to_dragonfly_unique_s
4 | map{|item| item.to_dragonfly_unique_s }.join
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/dragonfly/core_ext/hash.rb:
--------------------------------------------------------------------------------
1 | class Hash
2 |
3 | def to_dragonfly_unique_s
4 | sort_by{|k, v| k.to_dragonfly_unique_s }.to_dragonfly_unique_s
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/dragonfly/core_ext/object.rb:
--------------------------------------------------------------------------------
1 | class Object
2 |
3 | def to_dragonfly_unique_s
4 | to_s
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/lib/dragonfly/file_data_store.rb:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 | require 'yaml'
3 | require 'fileutils'
4 | require 'dragonfly/utils'
5 |
6 | module Dragonfly
7 | class FileDataStore
8 |
9 | # Exceptions
10 | class BadUID < RuntimeError; end
11 | class UnableToFormUrl < RuntimeError; end
12 |
13 | class MetaStore
14 | def write(data_path, meta)
15 | File.open(meta_path(data_path), 'wb') do |f|
16 | f.write dump(meta)
17 | end
18 | end
19 |
20 | def read(data_path)
21 | path = meta_path(data_path)
22 | File.open(path,'rb'){|f| load(f.read) } if File.exist?(path)
23 | end
24 |
25 | def destroy(path)
26 | FileUtils.rm_f meta_path(path)
27 | end
28 |
29 | private
30 |
31 | def meta_path(data_path)
32 | raise NotImplementedError
33 | end
34 |
35 | def dump(meta)
36 | raise NotImplementedError
37 | end
38 |
39 | def load(string)
40 | raise NotImplementedError
41 | end
42 | end
43 |
44 | class YAMLMetaStore < MetaStore
45 | def meta_path(data_path)
46 | "#{data_path}.meta.yml"
47 | end
48 |
49 | def dump(meta)
50 | YAML.dump(meta)
51 | end
52 |
53 | def load(string)
54 | YAML.load(string)
55 | end
56 | end
57 |
58 | class MarshalMetaStore < MetaStore
59 | def meta_path(data_path)
60 | "#{data_path}.meta"
61 | end
62 |
63 | def dump(meta)
64 | Marshal.dump(meta)
65 | end
66 |
67 | def load(string)
68 | Utils.stringify_keys Marshal.load(string)
69 | end
70 | end
71 |
72 | def initialize(opts={})
73 | self.root_path = opts[:root_path] || 'dragonfly'
74 | self.server_root = opts[:server_root]
75 | self.store_meta = opts[:store_meta]
76 | @meta_store = YAMLMetaStore.new
77 | @deprecated_meta_store = MarshalMetaStore.new
78 | end
79 |
80 | attr_writer :store_meta
81 | attr_reader :root_path, :server_root
82 |
83 | def root_path=(path)
84 | @root_path = path ? path.to_s : nil
85 | end
86 |
87 | def server_root=(path)
88 | @server_root = path ? path.to_s : nil
89 | end
90 |
91 | def store_meta?
92 | @store_meta != false # Default to true if not set
93 | end
94 |
95 | def write(content, opts={})
96 | relative_path = if opts[:path]
97 | opts[:path]
98 | else
99 | filename = content.name || 'file'
100 | relative_path = relative_path_for(filename)
101 | end
102 |
103 | path = absolute(relative_path)
104 | until !File.exist?(path)
105 | path = disambiguate(path)
106 | end
107 | content.to_file(path).close
108 | meta_store.write(path, content.meta) if store_meta?
109 |
110 | relative(path)
111 | end
112 |
113 | def read(relative_path)
114 | validate_path!(relative_path)
115 | path = absolute(relative_path)
116 | pathname = Pathname.new(path)
117 | return nil unless pathname.exist?
118 | [
119 | pathname,
120 | (
121 | if store_meta?
122 | meta_store.read(path) || deprecated_meta_store.read(path) || {}
123 | end
124 | )
125 | ]
126 | end
127 |
128 | def destroy(relative_path)
129 | validate_path!(relative_path)
130 | path = absolute(relative_path)
131 | FileUtils.rm path
132 | meta_store.destroy(path)
133 | purge_empty_directories(relative_path)
134 | rescue Errno::ENOENT => e
135 | Dragonfly.warn("#{self.class.name} destroy error: #{e}")
136 | end
137 |
138 | def url_for(relative_path, opts={})
139 | if server_root.nil?
140 | raise UnableToFormUrl, "you need to configure server_root for #{self.class.name} in order to form urls"
141 | else
142 | _, __, path = absolute(relative_path).partition(server_root)
143 | if path.empty?
144 | raise UnableToFormUrl, "couldn't form url for uid #{relative_path.inspect} with root_path #{root_path.inspect} and server_root #{server_root.inspect}"
145 | else
146 | path
147 | end
148 | end
149 | end
150 |
151 | def disambiguate(path)
152 | dirname = File.dirname(path)
153 | basename = File.basename(path, '.*')
154 | extname = File.extname(path)
155 | "#{dirname}/#{basename}_#{(Time.now.usec*10 + rand(100)).to_s(32)}#{extname}"
156 | end
157 |
158 | private
159 |
160 | attr_reader :meta_store, :deprecated_meta_store
161 |
162 | def absolute(relative_path)
163 | relative_path.to_s == '.' ? root_path : File.join(root_path, relative_path)
164 | end
165 |
166 | def relative(absolute_path)
167 | absolute_path[/^#{Regexp.escape root_path}\/?(.*)$/, 1]
168 | end
169 |
170 | def directory_empty?(path)
171 | Dir.entries(path).sort == ['.','..'].sort
172 | end
173 |
174 | def root_path?(dir)
175 | root_path == dir
176 | end
177 |
178 | def relative_path_for(filename)
179 | time = Time.now
180 | "#{time.strftime '%Y/%m/%d/'}#{rand(1e15).to_s(36)}_#{filename.gsub(/[^\w.]+/,'_')}"
181 | end
182 |
183 | def purge_empty_directories(path)
184 | containing_directory = Pathname.new(path).dirname
185 | containing_directory.ascend do |relative_dir|
186 | dir = absolute(relative_dir)
187 | FileUtils.rmdir dir if directory_empty?(dir) && !root_path?(dir)
188 | end
189 | end
190 |
191 | def validate_path!(uid)
192 | raise BadUID, uid if Utils.blank?(uid) || uid['../']
193 | end
194 |
195 | end
196 |
197 | end
198 |
--------------------------------------------------------------------------------
/lib/dragonfly/has_filename.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | # Convenience methods for setting basename and extension
3 | # Including class needs to define a 'name' accessor
4 | # which is assumed to hold a filename-style string
5 | module HasFilename
6 |
7 | def basename
8 | File.basename(name, '.*') if name
9 | end
10 |
11 | def basename=(basename)
12 | self.name = [basename, ext].compact.join('.')
13 | end
14 |
15 | def ext
16 | File.extname(name)[/\.(.*)/, 1] if name
17 | end
18 |
19 | def ext=(ext)
20 | self.name = [(basename || 'file'), ext].join('.')
21 | end
22 |
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/dragonfly/hash_with_css_style_keys.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 |
3 | # HashWithCssStyleKeys is solely for being able to access a hash
4 | # which has css-style keys (e.g. 'font-size') with the underscore
5 | # symbol version
6 | # @example
7 | # opts = {'font-size' => '23px', :color => 'white'}
8 | # opts = HashWithCssStyleKeys[opts]
9 | # opts[:font_size] # ===> '23px'
10 | # opts[:color] # ===> 'white'
11 | class HashWithCssStyleKeys < Hash
12 | def [](key)
13 | super || (
14 | str_key = key.to_s
15 | css_key = str_key.gsub('_','-')
16 | super(str_key) || super(css_key) || super(css_key.to_sym)
17 | )
18 | end
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/analysers/image_properties.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module ImageMagick
3 | module Analysers
4 | class ImageProperties
5 |
6 | def call(content)
7 | identify_command = content.env[:identify_command] || 'identify'
8 | details = content.shell_eval do |path|
9 | "#{identify_command} -ping -format '%m %w %h' #{path}"
10 | end
11 | format, width, height = details.split
12 | {
13 | 'format' => format.downcase,
14 | 'width' => width.to_i,
15 | 'height' => height.to_i
16 | }
17 | end
18 |
19 | end
20 | end
21 | end
22 | end
23 |
24 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/commands.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module ImageMagick
3 | module Commands
4 | module_function
5 |
6 | def convert(content, args = "", opts = {})
7 | convert_command = content.env[:convert_command] || "convert"
8 | format = opts["format"]
9 |
10 | input_args = opts["input_args"] if opts["input_args"]
11 | delegate_string = "#{opts["delegate"]}:" if opts["delegate"]
12 | frame_string = "[#{opts["frame"]}]" if opts["frame"]
13 |
14 | content.shell_update :ext => format do |old_path, new_path|
15 | "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}"
16 | end
17 |
18 | if format
19 | content.meta["format"] = format.to_s
20 | content.ext = format
21 | content.meta["mime_type"] = nil # don't need it as we have ext now
22 | end
23 | end
24 |
25 | def generate(content, args, format)
26 | format = format.to_s
27 | convert_command = content.env[:convert_command] || "convert"
28 | content.shell_generate :ext => format do |path|
29 | "#{convert_command} #{args} #{path}"
30 | end
31 | content.add_meta("format" => format)
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/generators/plain.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/image_magick/commands"
2 | require "dragonfly/param_validators"
3 |
4 | module Dragonfly
5 | module ImageMagick
6 | module Generators
7 | class Plain
8 | include ParamValidators
9 |
10 | def call(content, width, height, opts = {})
11 | validate_all!([width, height], &is_number)
12 | validate_all_keys!(opts, %w(colour color format), &is_word)
13 | format = extract_format(opts)
14 |
15 | colour = opts["colour"] || opts["color"] || "white"
16 | Commands.generate(content, "-size #{width}x#{height} xc:#{colour}", format)
17 | content.add_meta("format" => format, "name" => "plain.#{format}")
18 | end
19 |
20 | def update_url(url_attributes, width, height, opts = {})
21 | url_attributes.name = "plain.#{extract_format(opts)}"
22 | end
23 |
24 | private
25 |
26 | def extract_format(opts)
27 | opts["format"] || "png"
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/generators/plasma.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/image_magick/commands"
2 |
3 | module Dragonfly
4 | module ImageMagick
5 | module Generators
6 | class Plasma
7 | include ParamValidators
8 |
9 | def call(content, width, height, opts = {})
10 | validate_all!([width, height], &is_number)
11 | validate!(opts["format"], &is_word)
12 | format = extract_format(opts)
13 | Commands.generate(content, "-size #{width}x#{height} plasma:fractal", format)
14 | content.add_meta("format" => format, "name" => "plasma.#{format}")
15 | end
16 |
17 | def update_url(url_attributes, width, height, opts = {})
18 | url_attributes.name = "plasma.#{extract_format(opts)}"
19 | end
20 |
21 | private
22 |
23 | def extract_format(opts)
24 | opts["format"] || "png"
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/generators/text.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/hash_with_css_style_keys"
2 | require "dragonfly/image_magick/commands"
3 | require "dragonfly/param_validators"
4 |
5 | module Dragonfly
6 | module ImageMagick
7 | module Generators
8 | class Text
9 | include ParamValidators
10 |
11 | FONT_STYLES = {
12 | "normal" => "normal",
13 | "italic" => "italic",
14 | "oblique" => "oblique",
15 | }
16 |
17 | FONT_STRETCHES = {
18 | "normal" => "normal",
19 | "semi-condensed" => "semi-condensed",
20 | "condensed" => "condensed",
21 | "extra-condensed" => "extra-condensed",
22 | "ultra-condensed" => "ultra-condensed",
23 | "semi-expanded" => "semi-expanded",
24 | "expanded" => "expanded",
25 | "extra-expanded" => "extra-expanded",
26 | "ultra-expanded" => "ultra-expanded",
27 | }
28 |
29 | FONT_WEIGHTS = {
30 | "normal" => "normal",
31 | "bold" => "bold",
32 | "bolder" => "bolder",
33 | "lighter" => "lighter",
34 | "100" => 100,
35 | "200" => 200,
36 | "300" => 300,
37 | "400" => 400,
38 | "500" => 500,
39 | "600" => 600,
40 | "700" => 700,
41 | "800" => 800,
42 | "900" => 900,
43 | }
44 |
45 | IS_COLOUR = ->(param) {
46 | /\A(#\w+|rgba?\([\d\.,]+\)|\w+)\z/ === param
47 | }
48 |
49 | def update_url(url_attributes, string, opts = {})
50 | url_attributes.name = "text.#{extract_format(opts)}"
51 | end
52 |
53 | def call(content, string, opts = {})
54 | validate_all_keys!(opts, %w(font font_family), &is_words)
55 | validate_all_keys!(opts, %w(color background_color stroke_color), &IS_COLOUR)
56 | validate!(opts["format"], &is_word)
57 |
58 | opts = HashWithCssStyleKeys[opts]
59 | args = []
60 | format = extract_format(opts)
61 | background = opts["background_color"] || "none"
62 | font_size = (opts["font_size"] || 12).to_i
63 | font_family = opts["font_family"] || opts["font"]
64 | escaped_string = "\"#{string.gsub(/"/, '\"')}\""
65 |
66 | # Settings
67 | args.push("-gravity NorthWest")
68 | args.push("-antialias")
69 | args.push("-pointsize #{font_size}")
70 | args.push("-family '#{font_family}'") if font_family
71 | args.push("-fill #{opts["color"]}") if opts["color"]
72 | args.push("-stroke #{opts["stroke_color"]}") if opts["stroke_color"]
73 | args.push("-style #{FONT_STYLES[opts["font_style"]]}") if opts["font_style"]
74 | args.push("-stretch #{FONT_STRETCHES[opts["font_stretch"]]}") if opts["font_stretch"]
75 | args.push("-weight #{FONT_WEIGHTS[opts["font_weight"]]}") if opts["font_weight"]
76 | args.push("-background #{background}")
77 | args.push("label:#{escaped_string}")
78 |
79 | # Padding
80 | pt, pr, pb, pl = parse_padding_string(opts["padding"]) if opts["padding"]
81 | padding_top = (opts["padding_top"] || pt).to_i
82 | padding_right = (opts["padding_right"] || pr).to_i
83 | padding_bottom = (opts["padding_bottom"] || pb).to_i
84 | padding_left = (opts["padding_left"] || pl).to_i
85 |
86 | Commands.generate(content, args.join(" "), format)
87 |
88 | if (padding_top || padding_right || padding_bottom || padding_left)
89 | dimensions = content.analyse(:image_properties)
90 | text_width = dimensions["width"]
91 | text_height = dimensions["height"]
92 | width = padding_left + text_width + padding_right
93 | height = padding_top + text_height + padding_bottom
94 |
95 | args = args.slice(0, args.length - 2)
96 | args.push("-size #{width}x#{height}")
97 | args.push("xc:#{background}")
98 | args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}")
99 | Commands.generate(content, args.join(" "), format)
100 | end
101 |
102 | content.add_meta("format" => format, "name" => "text.#{format}")
103 | end
104 |
105 | private
106 |
107 | def extract_format(opts)
108 | opts["format"] || "png"
109 | end
110 |
111 | # Use css-style padding declaration, i.e.
112 | # 10 (all sides)
113 | # 10 5 (top/bottom, left/right)
114 | # 10 5 10 (top, left/right, bottom)
115 | # 10 5 10 5 (top, right, bottom, left)
116 | def parse_padding_string(str)
117 | padding_parts = str.gsub("px", "").split(/\s+/).map { |px| px.to_i }
118 | case padding_parts.size
119 | when 1
120 | p = padding_parts.first
121 | [p, p, p, p]
122 | when 2
123 | p, q = padding_parts
124 | [p, q, p, q]
125 | when 3
126 | p, q, r = padding_parts
127 | [p, q, r, q]
128 | when 4
129 | padding_parts
130 | else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
131 | end
132 | end
133 | end
134 | end
135 | end
136 | end
137 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/plugin.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/image_magick/analysers/image_properties"
2 | require "dragonfly/image_magick/generators/plain"
3 | require "dragonfly/image_magick/generators/plasma"
4 | require "dragonfly/image_magick/generators/text"
5 | require "dragonfly/image_magick/processors/encode"
6 | require "dragonfly/image_magick/processors/thumb"
7 | require "dragonfly/image_magick/commands"
8 | require "dragonfly/param_validators"
9 |
10 | module Dragonfly
11 | module ImageMagick
12 |
13 | # The ImageMagick Plugin registers an app with generators, analysers and processors.
14 | # Look at the source code for #call to see exactly how it configures the app.
15 | class Plugin
16 | def call(app, opts = {})
17 | # ENV
18 | app.env[:convert_command] = opts[:convert_command] || "convert"
19 | app.env[:identify_command] = opts[:identify_command] || "identify"
20 |
21 | # Analysers
22 | app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new
23 | app.add_analyser :width do |content|
24 | content.analyse(:image_properties)["width"]
25 | end
26 | app.add_analyser :height do |content|
27 | content.analyse(:image_properties)["height"]
28 | end
29 | app.add_analyser :format do |content|
30 | content.analyse(:image_properties)["format"]
31 | end
32 | app.add_analyser :aspect_ratio do |content|
33 | attrs = content.analyse(:image_properties)
34 | attrs["width"].to_f / attrs["height"]
35 | end
36 | app.add_analyser :portrait do |content|
37 | attrs = content.analyse(:image_properties)
38 | attrs["width"] <= attrs["height"]
39 | end
40 | app.add_analyser :landscape do |content|
41 | !content.analyse(:portrait)
42 | end
43 | app.add_analyser :image do |content|
44 | begin
45 | content.analyse(:image_properties)["format"] != "pdf"
46 | rescue Shell::CommandFailed
47 | false
48 | end
49 | end
50 |
51 | # Aliases
52 | app.define(:portrait?) { portrait }
53 | app.define(:landscape?) { landscape }
54 | app.define(:image?) { image }
55 |
56 | # Generators
57 | app.add_generator :plain, ImageMagick::Generators::Plain.new
58 | app.add_generator :plasma, ImageMagick::Generators::Plasma.new
59 | app.add_generator :text, ImageMagick::Generators::Text.new
60 | app.add_generator :convert do
61 | raise "The convert generator is deprecated for better security - use Dragonfly::ImageMagick::Commands.generate(content, args, format) instead."
62 | end
63 |
64 | # Processors
65 | app.add_processor :encode, Processors::Encode.new
66 | app.add_processor :thumb, Processors::Thumb.new
67 | app.add_processor :rotate do |content, amount|
68 | ParamValidators.validate!(amount, &ParamValidators.is_number)
69 | Commands.convert(content, "-rotate #{amount}")
70 | end
71 | app.add_processor :convert do
72 | raise "The convert processor is deprecated for better security - use Dragonfly::ImageMagick::Commands.convert(content, args, opts) instead."
73 | end
74 |
75 | # Extra methods
76 | app.define :identify do |cli_args = nil|
77 | shell_eval do |path|
78 | "#{app.env[:identify_command]} #{cli_args} #{path}"
79 | end
80 | end
81 | end
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/processors/encode.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/image_magick/commands"
2 |
3 | module Dragonfly
4 | module ImageMagick
5 | module Processors
6 | class Encode
7 | include ParamValidators
8 |
9 | WHITELISTED_ARGS = %w(quality)
10 |
11 | IS_IN_WHITELISTED_ARGS = ->(args_string) {
12 | args_string.scan(/-\w+/).all? { |arg|
13 | WHITELISTED_ARGS.include?(arg.sub("-", ""))
14 | }
15 | }
16 |
17 | def update_url(attrs, format, args = "")
18 | attrs.ext = format.to_s
19 | end
20 |
21 | def call(content, format, args = "")
22 | validate!(format, &is_word)
23 | validate!(args, &IS_IN_WHITELISTED_ARGS)
24 | Commands.convert(content, args, "format" => format)
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/dragonfly/image_magick/processors/thumb.rb:
--------------------------------------------------------------------------------
1 | require "dragonfly/image_magick/commands"
2 |
3 | module Dragonfly
4 | module ImageMagick
5 | module Processors
6 | class Thumb
7 | include ParamValidators
8 |
9 | GRAVITIES = {
10 | "nw" => "NorthWest",
11 | "n" => "North",
12 | "ne" => "NorthEast",
13 | "w" => "West",
14 | "c" => "Center",
15 | "e" => "East",
16 | "sw" => "SouthWest",
17 | "s" => "South",
18 | "se" => "SouthEast",
19 | }
20 |
21 | # Geometry string patterns
22 | RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
23 | CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne'
24 | CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
25 |
26 | def update_url(url_attributes, geometry, opts = {})
27 | format = opts["format"]
28 | url_attributes.ext = format if format
29 | end
30 |
31 | def call(content, geometry, opts = {})
32 | validate!(opts["format"], &is_word)
33 | validate!(opts["frame"], &is_number)
34 | Commands.convert(content, args_for_geometry(geometry), {
35 | "format" => opts["format"],
36 | "frame" => opts["frame"],
37 | })
38 | end
39 |
40 | def args_for_geometry(geometry)
41 | case geometry
42 | when RESIZE_GEOMETRY
43 | resize_args(geometry)
44 | when CROPPED_RESIZE_GEOMETRY
45 | resize_and_crop_args($1, $2, $3)
46 | when CROP_GEOMETRY
47 | crop_args(
48 | "width" => $1,
49 | "height" => $2,
50 | "x" => $3,
51 | "y" => $4,
52 | "gravity" => $5,
53 | )
54 | else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
55 | end
56 | end
57 |
58 | private
59 |
60 | def resize_args(geometry)
61 | "-resize #{geometry}"
62 | end
63 |
64 | def crop_args(opts)
65 | raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"]
66 |
67 | width = opts["width"]
68 | height = opts["height"]
69 | gravity = GRAVITIES[opts["gravity"]]
70 | x = "#{opts["x"] || 0}"
71 | x = "+" + x unless x[/\A[+-]/]
72 | y = "#{opts["y"] || 0}"
73 | y = "+" + y unless y[/\A[+-]/]
74 |
75 | "#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage"
76 | end
77 |
78 | def resize_and_crop_args(width, height, gravity)
79 | gravity = GRAVITIES[gravity || "c"]
80 | "-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage"
81 | end
82 | end
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/dragonfly/job.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 | require 'digest/sha1'
3 | require 'dragonfly/serializer'
4 | require 'dragonfly/content'
5 | require 'dragonfly/url_attributes'
6 | require 'dragonfly/job_endpoint'
7 |
8 | module Dragonfly
9 | class Job
10 |
11 | # Exceptions
12 | class AppDoesNotMatch < StandardError; end
13 | class JobAlreadyApplied < StandardError; end
14 | class NothingToProcess < StandardError; end
15 | class InvalidArray < StandardError; end
16 | class NoSHAGiven < StandardError; end
17 | class IncorrectSHA < StandardError; end
18 | class CannotGenerateSha < StandardError; end
19 |
20 | extend Forwardable
21 | def_delegators :result,
22 | :data, :file, :tempfile, :path, :to_file, :size, :each,
23 | :meta, :meta=, :add_meta, :name, :name=, :basename, :basename=, :ext, :ext=, :mime_type,
24 | :analyse, :shell_eval, :store,
25 | :b64_data,
26 | :close
27 |
28 | require 'dragonfly/job/fetch'
29 | require 'dragonfly/job/fetch_file'
30 | require 'dragonfly/job/fetch_url'
31 | require 'dragonfly/job/generate'
32 | require 'dragonfly/job/process'
33 |
34 | STEPS = [
35 | Fetch,
36 | Process,
37 | Generate,
38 | FetchFile,
39 | FetchUrl
40 | ]
41 |
42 | # Class methods
43 | class << self
44 |
45 | def from_a(steps_array, app)
46 | unless steps_array.is_a?(Array) &&
47 | steps_array.all?{|s| s.is_a?(Array) && step_abbreviations[s.first.to_s] }
48 | raise InvalidArray, "can't define a job from #{steps_array.inspect}"
49 | end
50 | job = app.new_job
51 | steps_array.each do |step_array|
52 | step_class = step_abbreviations[step_array.shift.to_s]
53 | job.steps << step_class.new(job, *step_array)
54 | end
55 | job
56 | end
57 |
58 | def deserialize(string, app)
59 | array = begin
60 | Serializer.json_b64_decode(string)
61 | rescue Serializer::BadString
62 | if app.allow_legacy_urls
63 | Serializer.marshal_b64_decode(string) # legacy strings
64 | else
65 | raise
66 | end
67 | end
68 | from_a(array, app)
69 | end
70 |
71 | def step_abbreviations
72 | @step_abbreviations ||= STEPS.inject({}){|hash, step_class| hash[step_class.abbreviation] = step_class; hash }
73 | end
74 |
75 | def step_names
76 | @step_names ||= STEPS.map{|step_class| step_class.step_name }
77 | end
78 |
79 | end
80 |
81 | def initialize(app, content="", meta={})
82 | @app = app
83 | @next_step_index = 0
84 | @steps = []
85 | @content = Content.new(app, content, meta)
86 | @url_attributes = UrlAttributes.new
87 | end
88 |
89 | # Used by 'dup' and 'clone'
90 | def initialize_copy(other)
91 | @steps = other.steps.map do |step|
92 | step.class.new(self, *step.args)
93 | end
94 | @content = other.content.dup
95 | @url_attributes = other.url_attributes.dup
96 | end
97 |
98 | attr_reader :app, :steps, :content
99 |
100 | # define fetch(), fetch!(), process(), etc.
101 | STEPS.each do |step_class|
102 | class_eval %(
103 | def #{step_class.step_name}(*args)
104 | new_job = self.dup
105 | new_job.steps << #{step_class}.new(new_job, *args)
106 | new_job
107 | end
108 |
109 | def #{step_class.step_name}!(*args)
110 | steps << #{step_class}.new(self, *args)
111 | self
112 | end
113 | )
114 | end
115 |
116 | # Applying, etc.
117 |
118 | def apply
119 | pending_steps.each{|step| step.apply }
120 | self.next_step_index = steps.length
121 | self
122 | end
123 |
124 | def applied?
125 | next_step_index == steps.length
126 | end
127 |
128 | def applied_steps
129 | steps[0...next_step_index]
130 | end
131 |
132 | def pending_steps
133 | steps[next_step_index..-1]
134 | end
135 |
136 | def to_a
137 | steps.map{|step| step.to_a }
138 | end
139 |
140 | # Serializing, etc.
141 |
142 | def to_unique_s
143 | to_a.to_dragonfly_unique_s
144 | end
145 |
146 | def serialize
147 | Serializer.json_b64_encode(to_a)
148 | end
149 |
150 | def signature
151 | Digest::SHA1.hexdigest(to_unique_s)
152 | end
153 |
154 | def sha
155 | unless app.secret
156 | raise CannotGenerateSha, "A secret is required to sign and verify Dragonfly job requests. "\
157 | "Use `secret '...'` or `verify_urls false` (not recommended!) in your config."
158 | end
159 | OpenSSL::HMAC.hexdigest('SHA256', app.secret, to_unique_s)[0,16]
160 | end
161 |
162 | def validate_sha!(sha)
163 | case sha
164 | when nil
165 | raise NoSHAGiven
166 | when self.sha
167 | self
168 | else
169 | raise IncorrectSHA, sha
170 | end
171 | end
172 |
173 | # URLs, etc.
174 |
175 | def url(opts={})
176 | app.url_for(self, opts) unless steps.empty?
177 | end
178 |
179 | attr_reader :url_attributes
180 |
181 | def update_url_attributes(hash)
182 | hash.each do |key, value|
183 | url_attributes.send("#{key}=", value)
184 | end
185 | end
186 |
187 | # to_stuff...
188 |
189 | def to_app
190 | JobEndpoint.new(self)
191 | end
192 |
193 | def to_response(env={"REQUEST_METHOD" => "GET"})
194 | to_app.call(env)
195 | end
196 |
197 | def to_fetched_job(uid)
198 | new_job = dup
199 | new_job.steps = []
200 | new_job.fetch!(uid)
201 | new_job.next_step_index = 1
202 | new_job
203 | end
204 |
205 | # Step inspection
206 |
207 | def uid
208 | step = fetch_step
209 | step.uid if step
210 | end
211 |
212 | def fetch_step
213 | last_step_of_type(Fetch)
214 | end
215 |
216 | def generate_step
217 | last_step_of_type(Generate)
218 | end
219 |
220 | def fetch_file_step
221 | last_step_of_type(FetchFile)
222 | end
223 |
224 | def fetch_url_step
225 | last_step_of_type(FetchUrl)
226 | end
227 |
228 | def process_steps
229 | steps.select{|s| s.is_a?(Process) }
230 | end
231 |
232 | def step_types
233 | steps.map{|s| s.class.step_name }
234 | end
235 |
236 | # Misc
237 |
238 | def inspect
239 | "
"
240 | end
241 |
242 | protected
243 |
244 | attr_writer :steps
245 | attr_accessor :next_step_index
246 |
247 | private
248 |
249 | def result
250 | apply
251 | content
252 | end
253 |
254 | def last_step_of_type(type)
255 | steps.select{|s| s.is_a?(type) }.last
256 | end
257 |
258 | end
259 | end
260 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/fetch.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/job/step'
2 |
3 | module Dragonfly
4 | class Job
5 | class Fetch < Step
6 | class NotFound < RuntimeError; end
7 |
8 | def uid
9 | args.first
10 | end
11 |
12 | def apply
13 | content, meta = app.datastore.read(uid)
14 | raise NotFound, "uid #{uid} not found" if content.nil?
15 | job.content.update(content, meta)
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/fetch_file.rb:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 | require 'dragonfly/job/step'
3 |
4 | module Dragonfly
5 | class Job
6 | class FetchFile < Step
7 | def initialize(job, path)
8 | super(job, path.to_s)
9 | end
10 | def init
11 | job.url_attributes.name = filename
12 | end
13 |
14 | def path
15 | @path ||= File.expand_path(args.first)
16 | end
17 |
18 | def filename
19 | @filename ||= File.basename(path)
20 | end
21 |
22 | def apply
23 | job.content.update(Pathname.new(path), 'name' => filename)
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/fetch_url.rb:
--------------------------------------------------------------------------------
1 | require 'uri'
2 | require 'net/http'
3 | require 'base64'
4 | require 'dragonfly/job/step'
5 | require 'addressable/uri'
6 |
7 | module Dragonfly
8 | class Job
9 | class FetchUrl < Step
10 |
11 | class ErrorResponse < RuntimeError
12 | def initialize(status, body)
13 | @status, @body = status, body
14 | end
15 | attr_reader :status, :body
16 | end
17 | class CannotHandle < RuntimeError; end
18 | class TooManyRedirects < RuntimeError; end
19 | class BadURI < RuntimeError; end
20 |
21 | def init
22 | job.url_attributes.name = filename
23 | end
24 |
25 | def uri
26 | args.first
27 | end
28 |
29 | def url
30 | @url ||= uri =~ /\A\w+:[^\d]/ ? uri : "http://#{uri}"
31 | end
32 |
33 | def filename
34 | return if data_uri?
35 | @filename ||= parse_url(url).path[/[^\/]+\z/]
36 | end
37 |
38 | def data_uri?
39 | uri =~ /\Adata:/
40 | end
41 |
42 | def apply
43 | if data_uri?
44 | update_from_data_uri
45 | else
46 | response = get_following_redirects(url)
47 | job.content.update(response.body || "", 'name' => filename, 'mime_type' => response.content_type)
48 | end
49 | end
50 |
51 | private
52 |
53 | def get_following_redirects(url, redirect_limit=10)
54 | raise TooManyRedirects, "url #{url} redirected too many times" if redirect_limit == 0
55 | response = get(url)
56 | case response
57 | when Net::HTTPSuccess then response
58 | when Net::HTTPRedirection then
59 | get_following_redirects(redirect_url(url, response['location']), redirect_limit-1)
60 | else
61 | response.error!
62 | end
63 | rescue Net::HTTPExceptions => e
64 | raise ErrorResponse.new(e.response.code.to_i, e.response.body)
65 | end
66 |
67 | def get(url)
68 | url = parse_url(url)
69 |
70 | http = Net::HTTP.new(url.host, url.port)
71 | http.use_ssl = true if url.scheme == 'https'
72 |
73 | request = Net::HTTP::Get.new(url.request_uri)
74 |
75 | if url.user || url.password
76 | request.basic_auth(url.user, url.password)
77 | end
78 |
79 | http.request(request)
80 | end
81 |
82 | def update_from_data_uri
83 | mime_type, b64_data = uri.scan(/\Adata:([^;]+);base64,(.*)\Z/m)[0]
84 | if mime_type && b64_data
85 | data = Base64.decode64(b64_data)
86 | ext = app.ext_for(mime_type)
87 | job.content.update(data, 'name' => "file.#{ext}", 'mime_type' => mime_type)
88 | else
89 | raise CannotHandle, "fetch_url can only deal with base64-encoded data uris with specified content type"
90 | end
91 | end
92 |
93 | def parse_url(url)
94 | URI.parse(url.to_s)
95 | rescue URI::InvalidURIError
96 | begin
97 | encoded_uri = Addressable::URI.parse(url).normalize.to_s
98 | URI.parse(encoded_uri)
99 | rescue Addressable::URI::InvalidURIError => e
100 | raise BadURI, e.message
101 | rescue URI::InvalidURIError => e
102 | raise BadURI, e.message
103 | end
104 | end
105 |
106 | def redirect_url(current_url, following_url)
107 | redirect_url = URI.parse(following_url)
108 | if redirect_url.relative?
109 | redirect_url = URI::join(current_url, following_url)
110 | end
111 | redirect_url
112 | end
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/generate.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/job/step'
2 |
3 | module Dragonfly
4 | class Job
5 | class Generate < Step
6 | def init
7 | generator.update_url(job.url_attributes, *arguments) if generator.respond_to?(:update_url)
8 | end
9 |
10 | def name
11 | args.first
12 | end
13 |
14 | def generator
15 | @generator ||= app.get_generator(name)
16 | end
17 |
18 | def arguments
19 | args[1..-1]
20 | end
21 |
22 | def apply
23 | generator.call(job.content, *arguments)
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/process.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/job/step'
2 |
3 | module Dragonfly
4 | class Job
5 | class Process < Step
6 | def init
7 | processor.update_url(job.url_attributes, *arguments) if processor.respond_to?(:update_url)
8 | end
9 |
10 | def name
11 | args.first.to_sym
12 | end
13 |
14 | def arguments
15 | args[1..-1]
16 | end
17 |
18 | def processor
19 | @processor ||= app.get_processor(name)
20 | end
21 |
22 | def apply
23 | processor.call(job.content, *arguments)
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/dragonfly/job/step.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | class Job
3 | class Step
4 |
5 | class << self
6 | # Dragonfly::Job::Fetch -> 'Fetch'
7 | def basename
8 | @basename ||= name.split('::').last
9 | end
10 | # Dragonfly::Job::Fetch -> :fetch
11 | def step_name
12 | @step_name ||= basename.gsub(/[A-Z]/){ "_#{$&.downcase}" }.sub('_','').to_sym
13 | end
14 | # Dragonfly::Job::Fetch -> 'f'
15 | def abbreviation
16 | @abbreviation ||= basename.scan(/[A-Z]/).join.downcase
17 | end
18 | end
19 |
20 | def initialize(job, *args)
21 | @job, @args = job, args
22 | init
23 | end
24 |
25 | def init # To be overridden
26 | end
27 |
28 | attr_reader :job, :args
29 |
30 | def app
31 | job.app
32 | end
33 |
34 | def to_a
35 | [self.class.abbreviation, *args]
36 | end
37 |
38 | def inspect
39 | "#{self.class.step_name}(#{args.map{|a| a.inspect }.join(', ')})"
40 | end
41 |
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/dragonfly/job_endpoint.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/response'
2 |
3 | module Dragonfly
4 | class JobEndpoint
5 |
6 | def initialize(job)
7 | @job = job
8 | end
9 |
10 | def call(env={})
11 | Response.new(job, env).to_response
12 | end
13 |
14 | attr_reader :job
15 |
16 | def inspect
17 | "<#{self.class.name} steps=#{job.steps} >"
18 | end
19 |
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/dragonfly/memory_data_store.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | class MemoryDataStore
3 |
4 | def initialize
5 | @content_store = {}
6 | end
7 |
8 | def write(content, opts={})
9 | uid = opts[:uid] || generate_uid
10 | content_store[uid] = {:content => content.data, :meta => content.meta.dup}
11 | uid
12 | end
13 |
14 | def read(uid)
15 | data = content_store[uid]
16 | [data[:content], data[:meta]] if data
17 | end
18 |
19 | def destroy(uid)
20 | content_store.delete(uid)
21 | end
22 |
23 | private
24 |
25 | attr_reader :content_store
26 |
27 | def generate_uid
28 | @uid_count ||= 0
29 | @uid_count += 1
30 | @uid_count.to_s
31 | end
32 |
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/dragonfly/middleware.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly'
2 |
3 | module Dragonfly
4 | class Middleware
5 |
6 | def initialize(app, dragonfly_app_name=nil)
7 | @app = app
8 | @dragonfly_app_name = dragonfly_app_name
9 | end
10 |
11 | def call(env)
12 | response = Dragonfly.app(@dragonfly_app_name).call(env)
13 | headers = response[1].transform_keys(&:downcase)
14 | if headers['x-cascade'] == 'pass'
15 | @app.call(env)
16 | else
17 | response
18 | end
19 | end
20 |
21 | end
22 | end
23 |
24 |
--------------------------------------------------------------------------------
/lib/dragonfly/model.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/model/instance_methods'
2 | require 'dragonfly/model/class_methods'
3 |
4 | module Dragonfly
5 | module Model
6 |
7 | def self.extended(klass)
8 | unless klass.include?(InstanceMethods)
9 | klass.extend(ClassMethods)
10 | klass.class_eval{ include InstanceMethods }
11 | end
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/dragonfly/model/attachment_class_methods.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module Model
3 | class Attachment
4 | class << self
5 |
6 | class ConfigProxy
7 |
8 | def initialize(spec, block)
9 | @spec = spec
10 | instance_eval(&block)
11 | end
12 |
13 | private
14 |
15 | attr_reader :spec
16 |
17 | def after_assign(*callbacks, &block)
18 | add_callbacks(:after_assign, *callbacks, &block)
19 | end
20 |
21 | def after_unassign(*callbacks, &block)
22 | add_callbacks(:after_unassign, *callbacks, &block)
23 | end
24 |
25 | def copy_to(accessor, &block)
26 | after_assign do |a|
27 | self.send "#{accessor}=", (block_given? ? instance_exec(a, &block) : a)
28 | end
29 | after_unassign{|a| self.send("#{accessor}=", nil) }
30 | end
31 |
32 | def default(path)
33 | spec.default_path = path.to_s
34 | end
35 |
36 | def storage_options(opts=nil, &block)
37 | spec.storage_options_specs << (opts || block)
38 | end
39 |
40 | def add_callbacks(name, *callbacks, &block)
41 | if block_given?
42 | spec.callbacks[name] << block
43 | else
44 | spec.callbacks[name].push(*callbacks)
45 | end
46 | end
47 |
48 | def method_missing(meth, *args, &block)
49 | if key = meth.to_s[/^storage_(.*)$/, 1]
50 | raise NoMethodError, "#{meth} is deprecated - use storage_options{|a| {#{key}: ...} } instead"
51 | else
52 | super
53 | end
54 | end
55 |
56 | end
57 |
58 | def init(model_class, attribute, app, config_block)
59 | @model_class, @attribute, @app, @config_block = model_class, attribute, app, config_block
60 | include app.job_methods
61 | ConfigProxy.new(self, config_block) if config_block
62 | self
63 | end
64 |
65 | attr_reader :model_class, :attribute, :app, :config_block, :default_path
66 |
67 | def default_path=(path)
68 | @default_path = path
69 | app.fetch_file_whitelist.push(path)
70 | end
71 |
72 | def default_job
73 | app.fetch_file(default_path) if default_path
74 | end
75 |
76 | # Callbacks
77 | def callbacks
78 | @callbacks ||= Hash.new{|h,k| h[k] = [] }
79 | end
80 |
81 | def run_callbacks(name, model, attachment)
82 | attachment.should_run_callbacks = false
83 | callbacks[name].each do |callback|
84 | case callback
85 | when Symbol then model.send(callback)
86 | when Proc then model.instance_exec(attachment, &callback)
87 | end
88 | end
89 | attachment.should_run_callbacks = true
90 | end
91 |
92 | # Magic attributes
93 | def allowed_magic_attributes
94 | app.analyser_methods + [:size, :name]
95 | end
96 |
97 | def magic_attributes
98 | @magic_attributes ||= begin
99 | prefix = attribute.to_s + '_'
100 | model_class.public_instance_methods.inject([]) do |attrs, name|
101 | _, __, suffix = name.to_s.partition(prefix)
102 | if !suffix.empty? && allowed_magic_attributes.include?(suffix.to_sym)
103 | attrs << suffix.to_sym
104 | end
105 | attrs
106 | end
107 | end
108 | end
109 |
110 | def ensure_uses_cached_magic_attributes
111 | return if @uses_cached_magic_attributes
112 | magic_attributes.each do |attr|
113 | define_method attr do
114 | magic_attribute_for(attr)
115 | end
116 | end
117 | @uses_cached_magic_attributes = true
118 | end
119 |
120 | # Storage options
121 | def storage_options_specs
122 | @storage_options_specs ||= []
123 | end
124 |
125 | def evaluate_storage_options(model, attachment)
126 | storage_options_specs.inject({}) do |opts, spec|
127 | options = case spec
128 | when Proc then model.instance_exec(attachment, &spec)
129 | when Symbol
130 | meth = model.method(spec)
131 | (1 === meth.arity) ? meth.call(attachment) : meth.call
132 | else spec
133 | end
134 | opts.merge!(options)
135 | opts
136 | end
137 | end
138 |
139 | end
140 | end
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/lib/dragonfly/model/class_methods.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly'
2 | require 'dragonfly/serializer'
3 | require 'dragonfly/utils'
4 | require 'dragonfly/model/attachment'
5 |
6 | module Dragonfly
7 | module Model
8 | module ClassMethods
9 |
10 | def dragonfly_attachment_classes
11 | @dragonfly_attachment_classes ||= begin
12 | parent_class = ancestors.select{|a| a.is_a?(Class) }[1]
13 | if parent_class.respond_to?(:dragonfly_attachment_classes)
14 | parent_class.dragonfly_attachment_classes.map do |klass|
15 | new_dragonfly_attachment_class(klass.attribute, klass.app, klass.config_block)
16 | end
17 | else
18 | []
19 | end
20 | end
21 | end
22 |
23 | private
24 |
25 | def dragonfly_accessor(attribute, opts={}, &config_block)
26 | app = case opts[:app]
27 | when Symbol, nil then Dragonfly.app(opts[:app])
28 | else opts[:app]
29 | end
30 |
31 | # Add callbacks
32 | before_save :save_dragonfly_attachments if respond_to?(:before_save)
33 | case
34 | when respond_to?(:after_commit)
35 | after_commit :destroy_dragonfly_attachments, on: :destroy
36 | when respond_to?(:after_destroy)
37 | after_destroy :destroy_dragonfly_attachments
38 | end
39 |
40 | # Register the new attribute
41 | dragonfly_attachment_classes << new_dragonfly_attachment_class(attribute, app, config_block)
42 |
43 | # Define an anonymous module for all of the attribute-specific instance
44 | # methods.
45 | instance_methods = Module.new do
46 | # Define the setter for the attribute
47 | define_method "#{attribute}=" do |value|
48 | dragonfly_attachments[attribute].assign(value)
49 | end
50 |
51 | # Define the getter for the attribute
52 | define_method attribute do
53 | dragonfly_attachments[attribute].to_value
54 | end
55 |
56 | # Define the xxx_stored? method
57 | define_method "#{attribute}_stored?" do
58 | dragonfly_attachments[attribute].stored?
59 | end
60 |
61 | # Define the xxx_changed? method
62 | define_method "#{attribute}_changed?" do
63 | dragonfly_attachments[attribute].changed?
64 | end
65 |
66 | # Define the URL setter
67 | define_method "#{attribute}_url=" do |url|
68 | unless Utils.blank?(url)
69 | dragonfly_attachments[attribute].assign(app.fetch_url(url))
70 | end
71 | end
72 |
73 | # Define the URL getter
74 | define_method "#{attribute}_url" do
75 | nil
76 | end
77 |
78 | # Define the remove setter
79 | define_method "remove_#{attribute}=" do |value|
80 | unless [0, "0", false, "false", "", nil].include?(value)
81 | dragonfly_attachments[attribute].assign(nil)
82 | instance_variable_set("@remove_#{attribute}", true)
83 | end
84 | end
85 |
86 | # Define the remove getter
87 | attr_reader "remove_#{attribute}"
88 |
89 | # Define the retained setter
90 | define_method "retained_#{attribute}=" do |string|
91 | unless Utils.blank?(string)
92 | begin
93 | dragonfly_attachments[attribute].retained_attrs = Serializer.json_b64_decode(string)
94 | rescue Serializer::BadString
95 | Dragonfly.warn("couldn't update attachment with serialized retained_#{attribute} string #{string.inspect}")
96 | end
97 | end
98 | dragonfly_attachments[attribute].should_retain = true
99 | dragonfly_attachments[attribute].retain!
100 | string
101 | end
102 |
103 | # Define the retained getter
104 | define_method "retained_#{attribute}" do
105 | attrs = dragonfly_attachments[attribute].retained_attrs
106 | Serializer.json_b64_encode(attrs) if attrs
107 | end
108 | end
109 |
110 | include instance_methods
111 | end
112 |
113 | def new_dragonfly_attachment_class(attribute, app, config_block)
114 | Class.new(Attachment).init(self, attribute, app, config_block)
115 | end
116 |
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/lib/dragonfly/model/instance_methods.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module Model
3 | module InstanceMethods
4 |
5 | def dragonfly_attachments
6 | @dragonfly_attachments ||= self.class.dragonfly_attachment_classes.inject({}) do |hash, klass|
7 | hash[klass.attribute] = klass.new(self)
8 | hash
9 | end
10 | end
11 |
12 | private
13 |
14 | def save_dragonfly_attachments
15 | dragonfly_attachments.each do |attribute, attachment|
16 | attachment.save!
17 | end
18 | end
19 |
20 | def destroy_dragonfly_attachments
21 | dragonfly_attachments.each do |attribute, attachment|
22 | attachment.destroy!
23 | end
24 | end
25 |
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/dragonfly/model/validations.rb:
--------------------------------------------------------------------------------
1 | require 'active_model/validator'
2 |
3 | module Dragonfly
4 | module Model
5 | module Validations
6 |
7 | class PropertyValidator < ActiveModel::EachValidator
8 |
9 | def validate_each(model, attribute, attachment)
10 | if attachment
11 | property = attachment.send(property_name)
12 | model.errors.add(attribute, message(property, model)) unless matches?(property)
13 | end
14 | rescue RuntimeError => e
15 | Dragonfly.warn("validation of property #{property_name} of #{attribute} failed with error #{e}")
16 | model.errors.add(attribute, message(nil, model))
17 | end
18 |
19 | private
20 |
21 | def matches?(property)
22 | if case_insensitive?
23 | prop = property.to_s.downcase
24 | allowed_values.any?{|v| v.to_s.downcase == prop }
25 | else
26 | allowed_values.include?(property)
27 | end
28 | end
29 |
30 | def message(property, model)
31 | message = options[:message] ||
32 | "#{property_name.to_s.humanize.downcase} is incorrect. " +
33 | "It needs to be #{expected_values_string}" +
34 | (property ? ", but was '#{property}'" : "")
35 | message.respond_to?(:call) ? message.call(property, model) : message
36 | end
37 |
38 | def check_validity!
39 | raise ArgumentError, "you must provide either :in => [, ..] or :as => " unless options[:in] || options[:as]
40 | end
41 |
42 | def property_name
43 | options[:property_name]
44 | end
45 |
46 | def case_insensitive?
47 | options[:case_sensitive] == false
48 | end
49 |
50 | def allowed_values
51 | @allowed_values ||= options[:in] || [options[:as]]
52 | end
53 |
54 | def expected_values_string
55 | if allowed_values.is_a?(Range)
56 | "between #{allowed_values.first} and #{allowed_values.last}"
57 | else
58 | allowed_values.length > 1 ? "one of '#{allowed_values.join('\', \'')}'" : "'#{allowed_values.first.to_s}'"
59 | end
60 | end
61 |
62 | end
63 |
64 | private
65 |
66 | def validates_property(property_name, options)
67 | raise ArgumentError, "you need to provide the attribute which has the property, using :of => " unless options[:of]
68 | validates_with PropertyValidator, options.merge(:attributes => [*options[:of]], :property_name => property_name)
69 | end
70 |
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/dragonfly/param_validators.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | module ParamValidators
3 | class InvalidParameter < RuntimeError; end
4 |
5 | module_function
6 |
7 | IS_NUMBER = ->(param) {
8 | param.is_a?(Numeric) || /\A[\d\.]+\z/ === param
9 | }
10 |
11 | IS_WORD = ->(param) {
12 | /\A\w+\z/ === param
13 | }
14 |
15 | IS_WORDS = ->(param) {
16 | /\A[\w ]+\z/ === param
17 | }
18 |
19 | def is_number; IS_NUMBER; end
20 | def is_word; IS_WORD; end
21 | def is_words; IS_WORDS; end
22 |
23 | def validate!(parameter, &validator)
24 | return if parameter.nil?
25 | raise InvalidParameter unless validator.(parameter)
26 | end
27 |
28 | def validate_all!(parameters, &validator)
29 | parameters.each { |p| validate!(p, &validator) }
30 | end
31 |
32 | def validate_all_keys!(obj, keys, &validator)
33 | parameters = keys.map { |key| obj[key] }
34 | validate_all!(parameters, &validator)
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/dragonfly/rails/images.rb:
--------------------------------------------------------------------------------
1 | raise LoadError, "\n*****\ndragonfly/rails/images no longer exists! Please use the rails generator instead\n\n\trails generate dragonfly\n\nsee the documentation at http://markevans.github.io/dragonfly/rails for more details\n*****\n"
2 |
--------------------------------------------------------------------------------
/lib/dragonfly/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/cookie_monster'
2 |
3 | if defined?(Rails::Railtie)
4 | module Dragonfly
5 | class Railtie < ::Rails::Railtie
6 | initializer "dragonfly.railtie.initializer" do |app|
7 | app.middleware.insert 3, Dragonfly::CookieMonster
8 | end
9 | end
10 | end
11 | end
12 |
13 |
--------------------------------------------------------------------------------
/lib/dragonfly/register.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | class Register
3 |
4 | # Exceptions
5 | class NotFound < RuntimeError; end
6 |
7 | def initialize
8 | @items = {}
9 | end
10 |
11 | attr_reader :items
12 |
13 | def add(name, item=nil, &block)
14 | items[name.to_sym] = item || block || raise(ArgumentError, "you must give either an argument or a block")
15 | end
16 |
17 | def get(name)
18 | items[name.to_sym] || raise(NotFound, "#{name.inspect} not registered")
19 | end
20 |
21 | def names
22 | items.keys
23 | end
24 |
25 | end
26 | end
27 |
28 |
--------------------------------------------------------------------------------
/lib/dragonfly/response.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 | require 'rack'
3 |
4 | module Dragonfly
5 | class Response
6 |
7 | def initialize(job, env)
8 | @job, @env = job, env
9 | @app = @job.app
10 | end
11 |
12 | def to_response
13 | response = begin
14 | if !(request.head? || request.get?)
15 | [405, method_not_allowed_headers, ["method not allowed"]]
16 | elsif etag_matches?
17 | [304, cache_headers, []]
18 | else
19 | job.apply
20 | env['dragonfly.job'] = job
21 | [
22 | 200,
23 | success_headers,
24 | (request.head? ? [] : job)
25 | ]
26 | end
27 | rescue Job::Fetch::NotFound => e
28 | Dragonfly.warn(e.message)
29 | [404, {"content-type" => "text/plain"}, ["Not found"]]
30 | rescue RuntimeError => e
31 | Dragonfly.warn("caught error - #{e.message}")
32 | [500, {"content-type" => "text/plain"}, ["Internal Server Error"]]
33 | end
34 | log_response(response)
35 | response
36 | end
37 |
38 | def will_be_served?
39 | request.get? && !etag_matches?
40 | end
41 |
42 | private
43 |
44 | attr_reader :job, :env, :app
45 |
46 | def request
47 | @request ||= Rack::Request.new(env)
48 | end
49 |
50 | def log_response(response)
51 | r = request
52 | Dragonfly.info [r.request_method, r.fullpath, response[0]].join(' ')
53 | end
54 |
55 | def etag_matches?
56 | return @etag_matches unless @etag_matches.nil?
57 | if_none_match = env['HTTP_IF_NONE_MATCH']
58 | @etag_matches = if if_none_match
59 | if_none_match.tr!('"','')
60 | if_none_match.split(',').include?(job.signature) || if_none_match == '*'
61 | else
62 | false
63 | end
64 | end
65 |
66 | def method_not_allowed_headers
67 | {
68 | 'content-type' => 'text/plain',
69 | 'allow' => 'GET, HEAD'
70 | }
71 | end
72 |
73 | def success_headers
74 | headers = standard_headers.merge(cache_headers)
75 | customize_headers(headers)
76 | headers.delete_if{|k, v| v.nil? }
77 | end
78 |
79 | def standard_headers
80 | {
81 | "content-type" => job.mime_type,
82 | "content-length" => job.size.to_s,
83 | "content-disposition" => filename_string
84 | }
85 | end
86 |
87 | def cache_headers
88 | {
89 | "cache-control" => "public, max-age=31536000", # (1 year)
90 | "etag" => %("#{job.signature}")
91 | }
92 | end
93 |
94 | def customize_headers(headers)
95 | app.response_headers.each do |k, v|
96 | headers[k] = v.respond_to?(:call) ? v.call(job, request, headers) : v
97 | end
98 | end
99 |
100 | def filename_string
101 | return unless job.name
102 | filename = request_from_msie? ? CGI.escape(job.name) : job.name
103 | %(filename="#{filename}")
104 | end
105 |
106 | def request_from_msie?
107 | env["HTTP_USER_AGENT"] =~ /MSIE/
108 | end
109 |
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/dragonfly/routed_endpoint.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'dragonfly/utils'
3 | require 'dragonfly/response'
4 |
5 | module Dragonfly
6 | class RoutedEndpoint
7 |
8 | class NoRoutingParams < RuntimeError; end
9 |
10 | def initialize(app, &block)
11 | @app = app
12 | @block = block
13 | end
14 |
15 | def call(env)
16 | params = Utils.symbolize_keys Rack::Request.new(env).params
17 | value = @block.call(params.merge(routing_params(env)), @app, env)
18 | case value
19 | when nil then plain_response(404, "Not Found")
20 | when Job, Model::Attachment
21 | job = value.is_a?(Model::Attachment) ? value.job : value
22 | Response.new(job, env).to_response
23 | else
24 | Dragonfly.warn("can't handle return value from routed endpoint: #{value.inspect}")
25 | plain_response(500, "Server Error")
26 | end
27 | rescue Job::NoSHAGiven
28 | plain_response(400, "You need to give a SHA parameter")
29 | rescue Job::IncorrectSHA
30 | plain_response(400, "The SHA parameter you gave is incorrect")
31 | end
32 |
33 | def inspect
34 | "<#{self.class.name} for app #{@app.name.inspect} >"
35 | end
36 |
37 | private
38 |
39 | def routing_params(env)
40 | env['rack.routing_args'] ||
41 | env['action_dispatch.request.path_parameters'] ||
42 | env['router.params'] ||
43 | env['dragonfly.params'] ||
44 | raise(NoRoutingParams, "couldn't find any routing parameters in env #{env.inspect}")
45 | end
46 |
47 | def plain_response(status, message)
48 | [status, {"content-type" => "text/plain"}, [message]]
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/dragonfly/serializer.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'base64'
3 | require 'multi_json'
4 | require 'dragonfly/utils'
5 |
6 | module Dragonfly
7 | module Serializer
8 |
9 | # Exceptions
10 | class BadString < RuntimeError; end
11 | class MaliciousString < RuntimeError; end
12 |
13 | extend self # So we can do Serializer.b64_encode, etc.
14 |
15 | def b64_encode(string)
16 | Base64.urlsafe_encode64(string).tr('=','')
17 | end
18 |
19 | def b64_decode(string)
20 | padding_length = (-(string.length % 4)) % 4
21 | string = string.tr('+','-').tr('~/','_')
22 | Base64.urlsafe_decode64(string + '=' * padding_length)
23 | rescue ArgumentError => e
24 | raise BadString, "couldn't b64_decode string - got #{e}"
25 | end
26 |
27 | def marshal_b64_decode(string)
28 | marshal_string = b64_decode(string)
29 | raise MaliciousString, "potentially malicious marshal string #{marshal_string.inspect}" if marshal_string[/@[a-z_]/i]
30 | Marshal.load(marshal_string)
31 | rescue TypeError, ArgumentError => e
32 | raise BadString, "couldn't marshal decode string - got #{e}"
33 | end
34 |
35 | def json_encode(object)
36 | MultiJson.encode(object)
37 | end
38 |
39 | def json_decode(string)
40 | raise BadString, "can't decode blank string" if Utils.blank?(string)
41 | MultiJson.decode(string)
42 | rescue MultiJson::DecodeError => e
43 | raise BadString, "couldn't json decode string - got #{e}"
44 | end
45 |
46 | def json_b64_encode(object)
47 | b64_encode(json_encode(object))
48 | end
49 |
50 | def json_b64_decode(string)
51 | json_decode(b64_decode(string))
52 | end
53 |
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/dragonfly/server.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 | require 'dragonfly/whitelist'
3 | require 'dragonfly/url_mapper'
4 | require 'dragonfly/job'
5 | require 'dragonfly/response'
6 | require 'dragonfly/serializer'
7 |
8 | module Dragonfly
9 | class Server
10 |
11 | # Exceptions
12 | class JobNotAllowed < RuntimeError; end
13 |
14 | extend Forwardable
15 | def_delegator :url_mapper, :params_in_url
16 |
17 | def initialize(app)
18 | @app = app
19 | @dragonfly_url = '/dragonfly'
20 | self.url_format = '/:job/:name'
21 | @fetch_file_whitelist = Whitelist.new
22 | @fetch_url_whitelist = Whitelist.new
23 | @verify_urls = true
24 | end
25 |
26 | attr_accessor :verify_urls, :url_host, :url_path_prefix, :dragonfly_url
27 |
28 | attr_reader :url_format, :fetch_file_whitelist, :fetch_url_whitelist
29 |
30 | def add_to_fetch_file_whitelist(patterns)
31 | fetch_file_whitelist.push(*patterns)
32 | end
33 |
34 | def add_to_fetch_url_whitelist(patterns)
35 | fetch_url_whitelist.push(*patterns)
36 | end
37 |
38 | def url_format=(url_format)
39 | @url_format = url_format
40 | self.url_mapper = UrlMapper.new(url_format)
41 | end
42 |
43 | def before_serve(&block)
44 | self.before_serve_callback = block
45 | end
46 |
47 | def call(env)
48 | if dragonfly_url == env["PATH_INFO"]
49 | dragonfly_response
50 | elsif (params = url_mapper.params_for(env["PATH_INFO"], env["QUERY_STRING"])) && params['job']
51 | job = Job.deserialize(params['job'], app)
52 | validate_job!(job)
53 | job.validate_sha!(params['sha']) if verify_urls
54 | response = Response.new(job, env)
55 | catch(:halt) do
56 | if before_serve_callback && response.will_be_served?
57 | before_serve_callback.call(job, env)
58 | end
59 | response.to_response
60 | end
61 | else
62 | [404, {'content-type' => 'text/plain', 'x-cascade' => 'pass'}, ['Not found']]
63 | end
64 | rescue Job::NoSHAGiven => e
65 | [400, {"content-type" => 'text/plain'}, ["You need to give a SHA parameter"]]
66 | rescue Job::IncorrectSHA => e
67 | [400, {"content-type" => 'text/plain'}, ["The SHA parameter you gave is incorrect"]]
68 | rescue JobNotAllowed => e
69 | Dragonfly.warn(e.message)
70 | [403, {"content-type" => 'text/plain'}, ["Forbidden"]]
71 | rescue Serializer::BadString, Serializer::MaliciousString, Job::InvalidArray => e
72 | Dragonfly.warn(e.message)
73 | [404, {'content-type' => 'text/plain'}, ['Not found']]
74 | end
75 |
76 | def url_for(job, opts={})
77 | opts = opts.dup
78 | host = opts.delete(:host) || url_host
79 | path_prefix = opts.delete(:path_prefix) || url_path_prefix
80 | params = job.url_attributes.extract(url_mapper.params_in_url)
81 | params.merge!(stringify_keys(opts))
82 | params['job'] = job.serialize
83 | params['sha'] = job.sha if verify_urls
84 | url = url_mapper.url_for(params)
85 | "#{host}#{path_prefix}#{url}"
86 | end
87 |
88 | private
89 |
90 | attr_reader :app
91 | attr_accessor :before_serve_callback, :url_mapper
92 |
93 | def stringify_keys(params)
94 | params.inject({}) do |hash, (k, v)|
95 | hash[k.to_s] = v
96 | hash
97 | end
98 | end
99 |
100 | def dragonfly_response
101 | body = <<-DRAGONFLY
102 | _o|o_
103 | _~~---._( )_.---~~_
104 | ( . \\ / . )
105 | `-.~--' |=| '--~.-'
106 | _~-.~'" /|=|\\ "'~.-~_
107 | ( ./ |=| \\. )
108 | `~~`"` |=| `"'ME"
109 | |-|
110 | <->
111 | V
112 | DRAGONFLY
113 | [200, {
114 | 'content-type' => 'text/plain',
115 | 'content-size' => body.bytesize.to_s
116 | },
117 | [body]
118 | ]
119 | end
120 |
121 | def validate_job!(job)
122 | if step = job.fetch_file_step
123 | validate_fetch_file_step!(step)
124 | end
125 | if step = job.fetch_url_step
126 | validate_fetch_url_step!(step)
127 | end
128 | end
129 |
130 | def validate_fetch_file_step!(step)
131 | unless fetch_file_whitelist.include?(step.path)
132 | raise JobNotAllowed, "fetch file #{step.path} disallowed - use fetch_file_whitelist to allow it"
133 | end
134 | end
135 |
136 | def validate_fetch_url_step!(step)
137 | unless fetch_url_whitelist.include?(step.url)
138 | raise JobNotAllowed, "fetch url #{step.url} disallowed - use fetch_url_whitelist to allow it"
139 | end
140 | end
141 | end
142 | end
143 |
144 |
--------------------------------------------------------------------------------
/lib/dragonfly/shell.rb:
--------------------------------------------------------------------------------
1 | require 'shellwords'
2 | require 'open3'
3 | require 'dragonfly'
4 |
5 | module Dragonfly
6 | class Shell
7 |
8 | # Exceptions
9 | class CommandFailed < RuntimeError; end
10 |
11 | def run(command, opts={})
12 | command = escape_args(command) unless opts[:escape] == false
13 | Dragonfly.debug("shell command: #{command}")
14 | run_command(command)
15 | end
16 |
17 | def escape_args(args)
18 | args.shellsplit.map{|arg| escape(arg) }.join(' ')
19 | end
20 |
21 | def escape(string)
22 | Shellwords.escape(string)
23 | end
24 |
25 | private
26 |
27 | # Annoyingly, Open3 seems buggy on jruby:
28 | # Some versions don't yield a wait_thread in the block and
29 | # you can't run sub-shells (if explicitly turning shell-escaping off)
30 | if RUBY_PLATFORM == 'java'
31 |
32 | # Unfortunately we have no control over stderr this way
33 | def run_command(command)
34 | result = `#{command}`
35 | status = $?
36 | raise_command_failed!(command, status.exitstatus) unless status.success?
37 | result
38 | rescue Errno::ENOENT => e
39 | raise_command_failed!(command, nil, e.message)
40 | end
41 |
42 | else
43 |
44 | def run_command(command)
45 | stdout_str, stderr_str, status = Open3.capture3(command)
46 | raise_command_failed!(command, status.exitstatus, stderr_str) unless status.success?
47 | stdout_str
48 | rescue Errno::ENOENT => e
49 | raise_command_failed!(command, nil, e.message)
50 | end
51 |
52 | end
53 |
54 | def raise_command_failed!(command, status=nil, error=nil)
55 | raise CommandFailed, [
56 | "Command failed: #{command}",
57 | ("exit status: #{status}" if status),
58 | ("error: #{error}" if error),
59 | ].join(', ')
60 | end
61 |
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/dragonfly/spec/data_store_examples.rb:
--------------------------------------------------------------------------------
1 | require 'dragonfly/content'
2 |
3 | shared_examples_for "data_store" do
4 |
5 | # Using these shared spec requires you to set the inst var @data_store
6 | let (:app) { Dragonfly.app }
7 | let (:content) { Dragonfly::Content.new(app, "gollum") }
8 | let (:content2) { Dragonfly::Content.new(app, "gollum") }
9 |
10 |
11 | describe "write" do
12 | it "returns a unique identifier for each storage" do
13 | @data_store.write(content).should_not == @data_store.write(content2)
14 | end
15 | it "should return a unique identifier for each storage even when the first is deleted" do
16 | uid1 = @data_store.write(content)
17 | @data_store.destroy(uid1)
18 | uid2 = @data_store.write(content)
19 | uid1.should_not == uid2
20 | end
21 | it "should allow for passing in options as a second argument" do
22 | @data_store.write(content, :some => :option)
23 | end
24 | end
25 |
26 | describe "read" do
27 | before(:each) do
28 | content.add_meta('bitrate' => 35, 'name' => 'danny.boy')
29 | uid = @data_store.write(content)
30 | stuff, meta = @data_store.read(uid)
31 | @retrieved_content = Dragonfly::Content.new(app, stuff, meta)
32 | end
33 |
34 | it "should read the stored data" do
35 | @retrieved_content.data.should == "gollum"
36 | end
37 |
38 | it "should return the stored meta" do
39 | @retrieved_content.meta['bitrate'].should == 35
40 | @retrieved_content.meta['name'].should == 'danny.boy'
41 | end
42 |
43 | it "should return nil if the data doesn't exist" do
44 | @data_store.read('gooble').should be_nil
45 | end
46 | end
47 |
48 | describe "destroy" do
49 |
50 | it "should destroy the stored data" do
51 | uid = @data_store.write(content)
52 | @data_store.destroy(uid)
53 | @data_store.read(uid).should be_nil
54 | end
55 |
56 | it "should do nothing if the data doesn't exist on destroy" do
57 | uid = @data_store.write(content)
58 | @data_store.destroy(uid)
59 | @data_store.destroy(uid)
60 | end
61 |
62 | end
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/lib/dragonfly/temp_object.rb:
--------------------------------------------------------------------------------
1 | require 'stringio'
2 | require 'tempfile'
3 | require 'pathname'
4 | require 'fileutils'
5 |
6 | module Dragonfly
7 |
8 | # A TempObject is used for HOLDING DATA.
9 | # It's the thing that is passed between the datastore and the processor, and is useful
10 | # for separating how the data was created and how it is later accessed.
11 | #
12 | # You can initialize it various ways:
13 | #
14 | # temp_object = Dragonfly::TempObject.new('this is the content') # with a String
15 | # temp_object = Dragonfly::TempObject.new(Pathname.new('path/to/content')) # with a Pathname
16 | # temp_object = Dragonfly::TempObject.new(File.new('path/to/content')) # with a File
17 | # temp_object = Dragonfly::TempObject.new(some_tempfile) # with a Tempfile
18 | # temp_object = Dragonfly::TempObject.new(some_other_temp_object) # with another TempObject
19 | #
20 | # However, no matter how it was initialized, you can always access the data a number of ways:
21 | #
22 | # temp_object.data # returns a data string
23 | # temp_object.file # returns a file object holding the data
24 | # temp_object.path # returns a path for the file
25 | #
26 | # The data/file are created lazily, something which you may wish to take advantage of.
27 | #
28 | # For example, if a TempObject is initialized with a file, and temp_object.data is never called, then
29 | # the data string will never be loaded into memory.
30 | #
31 | # Conversely, if the TempObject is initialized with a data string, and neither temp_object.file nor temp_object.path
32 | # are ever called, then the filesystem will never be hit.
33 | #
34 | class TempObject
35 |
36 | # Exceptions
37 | class Closed < RuntimeError; end
38 |
39 | # Instance Methods
40 |
41 | def initialize(obj, name=nil)
42 | if obj.is_a? TempObject
43 | @data = obj.get_data
44 | @tempfile = obj.get_tempfile
45 | @pathname = obj.get_pathname
46 | elsif obj.is_a? String
47 | @data = obj
48 | elsif obj.is_a? Tempfile
49 | @tempfile = obj
50 | elsif obj.is_a? File
51 | @pathname = Pathname.new(obj.path)
52 | elsif obj.is_a? Pathname
53 | @pathname = obj
54 | elsif obj.respond_to?(:tempfile)
55 | @tempfile = obj.tempfile
56 | elsif obj.respond_to?(:path) # e.g. Rack::Test::UploadedFile
57 | @pathname = Pathname.new(obj.path)
58 | else
59 | raise ArgumentError, "#{self.class.name} must be initialized with a String, a Pathname, a File, a Tempfile, another TempObject, something that responds to .tempfile, or something that responds to .path - you gave #{obj.inspect}"
60 | end
61 |
62 | @tempfile.close if @tempfile
63 |
64 | # Name
65 | @name = if name
66 | name
67 | elsif obj.respond_to?(:original_filename)
68 | obj.original_filename
69 | elsif @pathname
70 | @pathname.basename.to_s
71 | end
72 | end
73 |
74 | attr_reader :name
75 |
76 | def ext
77 | if n = name
78 | n.split('.').last
79 | end
80 | end
81 |
82 | def data
83 | raise Closed, "can't read data as TempObject has been closed" if closed?
84 | @data ||= file{|f| f.read }
85 | end
86 |
87 | def tempfile
88 | raise Closed, "can't read from tempfile as TempObject has been closed" if closed?
89 | @tempfile ||= begin
90 | case
91 | when @data
92 | @tempfile = Utils.new_tempfile(ext, @data)
93 | when @pathname
94 | @tempfile = copy_to_tempfile(@pathname.expand_path)
95 | end
96 | @tempfile
97 | end
98 | end
99 |
100 | def file(&block)
101 | f = tempfile.open
102 | tempfile.binmode
103 | if block_given?
104 | ret = yield f
105 | tempfile.close unless tempfile.closed?
106 | else
107 | ret = f
108 | end
109 | ret
110 | end
111 |
112 | def path
113 | @pathname ? @pathname.expand_path.to_s : tempfile.path
114 | end
115 |
116 | def size
117 | @tempfile && @tempfile.size ||
118 | @data && @data.bytesize ||
119 | File.size(path)
120 | end
121 |
122 | def each(&block)
123 | to_io do |io|
124 | while part = io.read(block_size)
125 | yield part
126 | end
127 | end
128 | end
129 |
130 | def to_file(path, opts={})
131 | mode = opts[:mode] || 0644
132 | prepare_path(path) unless opts[:mkdirs] == false
133 | if @data
134 | File.open(path, 'wb', mode){|f| f.write(@data) }
135 | else
136 | FileUtils.cp(self.path, path)
137 | File.chmod(mode, path)
138 | end
139 | File.new(path, 'rb')
140 | end
141 |
142 | def to_tempfile
143 | tempfile = copy_to_tempfile(path)
144 | tempfile.open
145 | tempfile
146 | end
147 |
148 | def to_io(&block)
149 | @data ? StringIO.open(@data, 'rb', &block) : file(&block)
150 | end
151 |
152 | def close
153 | @tempfile.close! if @tempfile
154 | @data = nil
155 | @closed = true
156 | end
157 |
158 | def closed?
159 | !!@closed
160 | end
161 |
162 | def inspect
163 | content_string = case
164 | when @data
165 | data_string = size > 20 ? "#{@data[0..20]}..." : @data
166 | "data=#{data_string.inspect}"
167 | when @pathname then "pathname=#{@pathname.inspect}"
168 | when @tempfile then "tempfile=#{@tempfile.inspect}"
169 | end
170 | "<#{self.class.name} #{content_string} >"
171 | end
172 |
173 | protected
174 |
175 | # We don't use normal accessors here because #data etc. do more than just return the instance var
176 | def get_data
177 | @data
178 | end
179 |
180 | def get_pathname
181 | @pathname
182 | end
183 |
184 | def get_tempfile
185 | @tempfile
186 | end
187 |
188 | private
189 |
190 | def block_size
191 | 8192
192 | end
193 |
194 | def copy_to_tempfile(path)
195 | tempfile = Utils.new_tempfile(ext)
196 | FileUtils.cp path, tempfile.path
197 | tempfile
198 | end
199 |
200 | def prepare_path(path)
201 | dir = File.dirname(path)
202 | FileUtils.mkdir_p(dir) unless File.exist?(dir)
203 | end
204 |
205 | end
206 | end
207 |
--------------------------------------------------------------------------------
/lib/dragonfly/url_attributes.rb:
--------------------------------------------------------------------------------
1 | require 'ostruct'
2 | require 'dragonfly/has_filename'
3 | require 'dragonfly/utils'
4 |
5 | module Dragonfly
6 | class UrlAttributes < OpenStruct
7 | include HasFilename # Updating ext / basename also updates the name
8 |
9 | def empty?
10 | @table.reject{|k, v| v.nil? }.empty?
11 | end
12 |
13 | # Hack so we can use .send('format') and it not call the private Kernel method
14 | def format
15 | @table[:format]
16 | end
17 |
18 | def extract(keys)
19 | keys.inject({}) do |attrs, key|
20 | value = send(key)
21 | attrs[key] = value unless Utils.blank?(value)
22 | attrs
23 | end
24 | end
25 | end
26 | end
27 |
28 |
--------------------------------------------------------------------------------
/lib/dragonfly/url_mapper.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'dragonfly/utils'
3 |
4 | module Dragonfly
5 | class UrlMapper
6 |
7 | PATTERNS = {
8 | basename: '[^\/]',
9 | name: '[^\/]',
10 | ext: '[^\.]',
11 | format: '[^\.]',
12 | job: '[^\/\.]'
13 | }
14 | DEFAULT_PATTERN = '[^\/\-\.]'
15 |
16 | # Exceptions
17 | class BadUrlFormat < StandardError; end
18 |
19 | class Segment < Struct.new(:param, :seperator, :pattern)
20 | def regexp_string
21 | @regexp_string ||= "(#{Regexp.escape(seperator)}#{pattern}+?)?"
22 | end
23 | end
24 |
25 | def initialize(url_format)
26 | @url_format = url_format
27 | raise BadUrlFormat, "bad url format #{url_format}" if url_format[/\w:\w/]
28 | init_segments
29 | init_url_regexp
30 | end
31 |
32 | attr_reader :url_format, :url_regexp, :segments
33 |
34 | def params_for(path, query=nil)
35 | if path and md = path.match(url_regexp)
36 | params = Rack::Utils.parse_query(query)
37 | params_in_url.each_with_index do |var, i|
38 | value = md[i+1][1..-1] if md[i+1]
39 | params[var] = value && Utils.uri_unescape(value)
40 | end
41 | params
42 | end
43 | end
44 |
45 | def params_in_url
46 | @params_in_url ||= url_format.scan(/\:\w+/).map{|f| f.tr(':','') }
47 | end
48 |
49 | def url_for(params)
50 | params = params.dup
51 | url = url_format.dup
52 | segments.each do |seg|
53 | value = params[seg.param]
54 | value ? url.sub!(/:\w+/, Utils.uri_escape_segment(value.to_s)) : url.sub!(/.:\w+/, '')
55 | params.delete(seg.param)
56 | end
57 | url << "?#{Rack::Utils.build_query(params)}" if params.any?
58 | url
59 | end
60 |
61 | private
62 |
63 | def init_segments
64 | @segments = []
65 | url_format.scan(/([^\w]):(\w+)/).each do |seperator, param|
66 | segments << Segment.new(
67 | param,
68 | seperator,
69 | PATTERNS[param.to_sym] || DEFAULT_PATTERN
70 | )
71 | end
72 | end
73 |
74 | def init_url_regexp
75 | i = -1
76 | regexp_string = url_format.gsub(/[^\w]:\w+/) do
77 | i += 1
78 | segments[i].regexp_string
79 | end
80 | @url_regexp = Regexp.new('\A' + regexp_string + '\z')
81 | end
82 |
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/dragonfly/utils.rb:
--------------------------------------------------------------------------------
1 | require 'tempfile'
2 | require 'uri'
3 | require 'rack'
4 |
5 | module Dragonfly
6 | module Utils
7 |
8 | module_function
9 |
10 | def blank?(obj)
11 | obj.respond_to?(:empty?) ? obj.empty? : !obj
12 | end
13 |
14 | def new_tempfile(ext=nil, content=nil)
15 | tempfile = ext ? Tempfile.new(['dragonfly', ".#{ext}"]) : Tempfile.new('dragonfly')
16 | tempfile.binmode
17 | tempfile.write(content) if content
18 | tempfile.close
19 | tempfile
20 | end
21 |
22 | def symbolize_keys(hash)
23 | hash.inject({}) do |new_hash, (key, value)|
24 | new_hash[key.to_sym] = value
25 | new_hash
26 | end
27 | end
28 |
29 | def stringify_keys(hash)
30 | hash.inject({}) do |new_hash, (key, value)|
31 | new_hash[key.to_s] = value
32 | new_hash
33 | end
34 | end
35 |
36 | def uri_escape_segment(string)
37 | URI.encode_www_form_component(string).gsub('+','%20')
38 | end
39 |
40 | def uri_unescape(string)
41 | URI::DEFAULT_PARSER.unescape(string)
42 | end
43 |
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/dragonfly/version.rb:
--------------------------------------------------------------------------------
1 | module Dragonfly
2 | VERSION = "1.4.1"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/dragonfly/whitelist.rb:
--------------------------------------------------------------------------------
1 | require 'forwardable'
2 |
3 | module Dragonfly
4 | class Whitelist
5 | extend Forwardable
6 | def_delegators :patterns, :push
7 |
8 | def initialize(patterns=[])
9 | @patterns = patterns
10 | end
11 |
12 | attr_reader :patterns
13 |
14 | def include?(string)
15 | patterns.any?{|pattern| pattern === string }
16 | end
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/lib/rails/generators/dragonfly/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Initialize and configure a dragonfly 'app', ready for serving images and other content!
3 |
4 | Example:
5 | rails generate dragonfly
6 |
7 | This will create:
8 | config/initializers/dragonfly.rb
9 |
--------------------------------------------------------------------------------
/lib/rails/generators/dragonfly/dragonfly_generator.rb:
--------------------------------------------------------------------------------
1 | class DragonflyGenerator < Rails::Generators::Base
2 | source_root File.expand_path('../templates', __FILE__)
3 |
4 | def create_initializer
5 | template "initializer.rb.erb", "config/initializers/dragonfly.rb"
6 | end
7 |
8 | private
9 |
10 | def generate_secret
11 | SecureRandom.hex(32)
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/lib/rails/generators/dragonfly/templates/initializer.rb.erb:
--------------------------------------------------------------------------------
1 | require 'dragonfly'
2 |
3 | # Configure
4 | Dragonfly.app.configure do
5 | plugin :imagemagick
6 |
7 | secret "<%= generate_secret %>"
8 |
9 | url_format "/media/:job/:name"
10 |
11 | datastore :file,
12 | root_path: Rails.root.join('public/system/dragonfly', Rails.env),
13 | server_root: Rails.root.join('public')
14 | end
15 |
16 | # Logger
17 | Dragonfly.logger = Rails.logger
18 |
19 | # Mount as middleware
20 | Rails.application.middleware.use Dragonfly::Middleware
21 |
22 | # Add model functionality
23 | ActiveSupport.on_load(:active_record) do
24 | extend Dragonfly::Model
25 | extend Dragonfly::Model::Validations
26 | end
27 |
--------------------------------------------------------------------------------
/samples/DSC02119.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/DSC02119.JPG
--------------------------------------------------------------------------------
/samples/a.jp2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/a.jp2
--------------------------------------------------------------------------------
/samples/beach.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/beach.jpg
--------------------------------------------------------------------------------
/samples/beach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/beach.png
--------------------------------------------------------------------------------
/samples/egg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/egg.png
--------------------------------------------------------------------------------
/samples/gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/gif.gif
--------------------------------------------------------------------------------
/samples/mevs' white pixel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/mevs' white pixel.png
--------------------------------------------------------------------------------
/samples/round.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/round.gif
--------------------------------------------------------------------------------
/samples/sample.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/sample.docx
--------------------------------------------------------------------------------
/samples/taj.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/markevans/dragonfly/ddede200b093ebe09467128bff071b40ad85e382/samples/taj.jpg
--------------------------------------------------------------------------------
/spec/dragonfly/configurable_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Configurable do
4 |
5 | describe "Configurer" do
6 |
7 | let (:configurer) { Dragonfly::Configurable::Configurer.new do
8 | def colour(colour)
9 | obj.colour = colour
10 | end
11 | end }
12 |
13 | let (:obj) { Object.new }
14 |
15 | it "allows configuring" do
16 | obj.should_receive(:colour=).with('red')
17 | configurer.configure(obj) do
18 | colour 'red'
19 | end
20 | end
21 |
22 | it "doesn't allow non-existent methods" do
23 | expect{
24 | configurer.configure(obj) do
25 | color 'red'
26 | end
27 | }.to raise_error(NoMethodError)
28 | end
29 |
30 | it "doesn't call non-existent methods on the object" do
31 | obj.should_not_receive(:color=)
32 | begin
33 | configurer.configure(obj) do
34 | color 'red'
35 | end
36 | rescue NoMethodError
37 | end
38 | end
39 |
40 | it "provides a 'writer' shortcut" do
41 | configurer = Dragonfly::Configurable::Configurer.new do
42 | writer :colour, :size
43 | end
44 | obj.should_receive(:colour=).with('blue')
45 | obj.should_receive(:size=).with('big')
46 | configurer.configure(obj) do
47 | colour 'blue'
48 | size 'big'
49 | end
50 | end
51 |
52 | it "allows using the writer on another object" do
53 | configurer = Dragonfly::Configurable::Configurer.new do
54 | writer :colour, :for => :egg
55 | end
56 | egg = double('egg')
57 | obj.should_receive(:egg).and_return(egg)
58 | egg.should_receive(:colour=).with('pink')
59 | configurer.configure(obj) do
60 | colour 'pink'
61 | end
62 | end
63 |
64 | it "provides a 'meth' shortcut" do
65 | configurer = Dragonfly::Configurable::Configurer.new do
66 | meth :jobby, :nobby
67 | end
68 | obj.should_receive(:jobby).with('beans', {:make => 5})
69 | obj.should_receive(:nobby).with(['nuts'])
70 | configurer.configure(obj) do
71 | jobby 'beans', :make => 5
72 | nobby ['nuts']
73 | end
74 | end
75 |
76 | it "allows using 'meth' on another object" do
77 | configurer = Dragonfly::Configurable::Configurer.new do
78 | meth :jobby, :for => :egg
79 | end
80 | egg = double('egg')
81 | obj.should_receive(:egg).and_return(egg)
82 | egg.should_receive(:jobby).with('beans', {:make => 5})
83 | configurer.configure(obj) do
84 | jobby 'beans', :make => 5
85 | end
86 | end
87 | end
88 |
89 | describe "plugins" do
90 |
91 | let (:configurer) { Dragonfly::Configurable::Configurer.new{} }
92 | let (:obj) { Object.new }
93 |
94 | it "provides 'plugin' for using plugins" do
95 | pluggy = double('plugin')
96 | pluggy.should_receive(:call).with(obj, :a, {'few' => ['args']})
97 | configurer.configure(obj) do
98 | plugin pluggy, :a, 'few' => ['args']
99 | end
100 | end
101 |
102 | it "allows using 'plugin' with symbols" do
103 | pluggy = double('plugin')
104 | pluggy.should_receive(:call).with(obj, :a, {'few' => ['args']})
105 | configurer.register_plugin(:pluggy){ pluggy }
106 | configurer.configure(obj) do
107 | plugin :pluggy, :a, 'few' => ['args']
108 | end
109 | end
110 |
111 | it "adds the plugin to the object's 'plugins' if it responds to it when using symbols" do
112 | def obj.plugins; @plugins ||= {}; end
113 | pluggy = proc{}
114 | configurer.register_plugin(:pluggy){ pluggy }
115 | configurer.configure(obj) do
116 | plugin :pluggy
117 | end
118 | obj.plugins[:pluggy].should == pluggy
119 | end
120 |
121 | it "raises an error when a wrong symbol is used" do
122 | expect{
123 | configurer.configure(obj) do
124 | plugin :pluggy, :a, 'few' => ['args']
125 | end
126 | }.to raise_error(Dragonfly::Configurable::UnregisteredPlugin)
127 | end
128 |
129 | end
130 |
131 | describe "extending with Configurable" do
132 |
133 | let (:car_class) { Class.new do
134 | extend Dragonfly::Configurable
135 | attr_accessor :colour
136 | end }
137 |
138 | it "adds the set_up_config method to the class" do
139 | car_class.set_up_config do
140 | writer :colour
141 | end
142 | end
143 |
144 | it "adds the configure method to the instance" do
145 | car_class.set_up_config do
146 | writer :colour
147 | end
148 | car = car_class.new
149 | car.should_receive(:colour=).with('mauve')
150 | car.configure do
151 | colour 'mauve'
152 | end
153 | end
154 |
155 | it "adds the plugins method to the instance" do
156 | car_class.set_up_config do
157 | writer :colour
158 | end
159 | car = car_class.new
160 | car.plugins.should == {}
161 | end
162 |
163 | it "doesn't allow configuring if the class hasn't been set up" do
164 | car = car_class.new
165 | expect{
166 | car.configure{}
167 | }.to raise_error(NoMethodError)
168 | end
169 |
170 | end
171 |
172 | describe "nested configures" do
173 |
174 | before(:each) do
175 | @car_class = Class.new do
176 | extend Dragonfly::Configurable
177 | def initialize
178 | @numbers = []
179 | end
180 | attr_accessor :numbers
181 | end
182 | @car_class.set_up_config do
183 | def add(number)
184 | obj.numbers << number
185 | end
186 | end
187 | end
188 |
189 | it "should still work (as some plugins will configure inside a configure)" do
190 | car = @car_class.new
191 | car.configure do
192 | add 1
193 | car.configure do
194 | add 2
195 | end
196 | add 3
197 | end
198 | car.numbers.should == [1,2,3]
199 | end
200 | end
201 |
202 | end
203 |
204 |
--------------------------------------------------------------------------------
/spec/dragonfly/cookie_monster_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rack'
3 | require 'dragonfly/cookie_monster'
4 |
5 | describe Dragonfly::CookieMonster do
6 |
7 | def app(extra_env={})
8 | Rack::Builder.new do
9 | use Dragonfly::CookieMonster
10 | run proc{|env| env.merge!(extra_env); [200, {"Set-Cookie" => "blah=thing", "Something" => "else"}, ["body here"]] }
11 | end
12 | end
13 |
14 | it "should not delete the set-cookie header from the response if the response doesn't come from dragonfly" do
15 | response = Rack::MockRequest.new(app).get('')
16 | response.status.should == 200
17 | response.body.should == "body here"
18 | response.headers["Set-Cookie"].should == "blah=thing"
19 | response.headers["Something"].should == "else"
20 | end
21 |
22 | it "should delete the set-cookie header from the response if the response comes from dragonfly" do
23 | response = Rack::MockRequest.new(app('dragonfly.job' => double)).get('')
24 | response.status.should == 200
25 | response.body.should == "body here"
26 | response.headers["Set-Cookie"].should be_nil
27 | response.headers["Something"].should == "else"
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dragonfly/core_ext/array_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Array do
4 |
5 | describe "to_dragonfly_unique_s" do
6 | it "should concatenate the to_s's of each of the elements" do
7 | [:a, true, 2, 5.2, "hello"].to_dragonfly_unique_s.should == 'atrue25.2hello'
8 | end
9 |
10 | it "should nest arrays" do
11 | [:a, [:b, :c], :d].to_dragonfly_unique_s.should == 'abcd'
12 | end
13 |
14 | it "should be empty if empty" do
15 | [].to_dragonfly_unique_s.should == ''
16 | end
17 | end
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/spec/dragonfly/core_ext/hash_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Hash do
4 |
5 | describe "to_dragonfly_unique_s" do
6 | it "should concatenate the to_s's of each of the elements, sorted alphabetically" do
7 | {'z' => nil, :a => 4, false => 'ice', 5 => 6.2}.to_dragonfly_unique_s.should == '56.2a4falseicez'
8 | end
9 |
10 | it "should nest correctly" do
11 | {:m => 1, :a => {:c => 2, :b => 3}, :z => 4}.to_dragonfly_unique_s.should == 'ab3c2m1z4'
12 | end
13 |
14 | it "should be empty if empty" do
15 | {}.to_dragonfly_unique_s.should == ''
16 | end
17 | end
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/spec/dragonfly/has_filename_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::HasFilename do
4 |
5 | before(:each) do
6 | @obj = Object.new
7 | class << @obj
8 | include Dragonfly::HasFilename
9 | attr_accessor :name
10 | end
11 | end
12 |
13 | describe "basename" do
14 | it "should define basename" do
15 | @obj.name = 'meat.balls'
16 | @obj.basename.should == 'meat'
17 | end
18 | it "should all but the last bit" do
19 | @obj.name = 'tooting.meat.balls'
20 | @obj.basename.should == 'tooting.meat'
21 | end
22 | it "should be nil if name not set" do
23 | @obj.basename.should be_nil
24 | end
25 | it "should be the whole name if it has no ext" do
26 | @obj.name = 'eggs'
27 | @obj.basename.should == 'eggs'
28 | end
29 | end
30 |
31 | describe "basename=" do
32 | it "should set the whole name if there isn't one" do
33 | @obj.basename = 'doog'
34 | @obj.name.should == 'doog'
35 | end
36 | it "should replace the whole name if there's no ext" do
37 | @obj.name = 'lungs'
38 | @obj.basename = 'doog'
39 | @obj.name.should == 'doog'
40 | end
41 | it "should replace all but the last bit" do
42 | @obj.name = 'bong.lungs.pig'
43 | @obj.basename = 'smeeg'
44 | @obj.name.should == 'smeeg.pig'
45 | end
46 | end
47 |
48 | describe "ext" do
49 | it "should define ext" do
50 | @obj.name = 'meat.balls'
51 | @obj.ext.should == 'balls'
52 | end
53 | it "should only use the last bit" do
54 | @obj.name = 'tooting.meat.balls'
55 | @obj.ext.should == 'balls'
56 | end
57 | it "should be nil if name not set" do
58 | @obj.ext.should be_nil
59 | end
60 | it "should be nil if name has no ext" do
61 | @obj.name = 'eggs'
62 | @obj.ext.should be_nil
63 | end
64 | end
65 |
66 | describe "ext=" do
67 | it "should use a default basename if there is no name" do
68 | @obj.ext = 'doog'
69 | @obj.name.should == 'file.doog'
70 | end
71 | it "should append the ext if name has none already" do
72 | @obj.name = 'lungs'
73 | @obj.ext = 'doog'
74 | @obj.name.should == 'lungs.doog'
75 | end
76 | it "should replace the ext if name has one already" do
77 | @obj.name = 'lungs.pig'
78 | @obj.ext = 'doog'
79 | @obj.name.should == 'lungs.doog'
80 | end
81 | it "should only replace the last bit" do
82 | @obj.name = 'long.lungs.pig'
83 | @obj.ext = 'doog'
84 | @obj.name.should == 'long.lungs.doog'
85 | end
86 | end
87 |
88 | end
89 |
--------------------------------------------------------------------------------
/spec/dragonfly/hash_with_css_style_keys_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::HashWithCssStyleKeys do
4 |
5 | before(:each) do
6 | @hash = Dragonfly::HashWithCssStyleKeys[
7 | :font_style => 'normal',
8 | :'font-weight' => 'bold',
9 | 'font_colour' => 'white',
10 | 'font-size' => 23,
11 | :hello => 'there'
12 | ]
13 | end
14 |
15 | describe "accessing using underscore symbol style" do
16 | it{ @hash[:font_style].should == 'normal' }
17 | it{ @hash[:font_weight].should == 'bold' }
18 | it{ @hash[:font_colour].should == 'white' }
19 | it{ @hash[:font_size].should == 23 }
20 | it{ @hash[:hello].should == 'there' }
21 | it{ @hash[:non_existent_key].should be_nil }
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/analysers/image_properties_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::ImageMagick::Analysers::ImageProperties do
4 |
5 | let(:app) { test_imagemagick_app }
6 | let(:analyser) { Dragonfly::ImageMagick::Analysers::ImageProperties.new }
7 | let(:content) { Dragonfly::Content.new(app, SAMPLES_DIR.join('beach.png')) } # 280x355
8 |
9 | describe "call" do
10 | it "returns a hash of properties" do
11 | analyser.call(content).should == {
12 | 'width' => 280,
13 | 'height' => 355,
14 | 'format' => 'png'
15 | }
16 | end
17 | end
18 |
19 | end
20 |
21 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/commands_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "dragonfly/image_magick/commands"
3 |
4 | describe Dragonfly::ImageMagick::Commands do
5 | include Dragonfly::ImageMagick::Commands
6 |
7 | let(:app) { test_app }
8 |
9 | def sample_content(name)
10 | Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
11 | end
12 |
13 | describe "convert" do
14 | let(:image) { sample_content("beach.png") } # 280x355
15 |
16 | it "should allow for general convert commands" do
17 | convert(image, "-scale 56x71")
18 | image.should have_width(56)
19 | image.should have_height(71)
20 | end
21 |
22 | it "should allow for general convert commands with added format" do
23 | convert(image, "-scale 56x71", "format" => "gif")
24 | image.should have_width(56)
25 | image.should have_height(71)
26 | image.should have_format("gif")
27 | image.meta["format"].should == "gif"
28 | end
29 |
30 | it "should work for commands with parenthesis" do
31 | convert(image, "\\( +clone -sparse-color Barycentric '0,0 black 0,%[fx:h-1] white' -function polynomial 2,-2,0.5 \\) -compose Blur -set option:compose:args 15 -composite")
32 | image.should have_width(280)
33 | end
34 |
35 | it "should work for files with spaces/apostrophes in the name" do
36 | image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
37 | convert(image, "-resize 2x2!")
38 | image.should have_width(2)
39 | end
40 |
41 | it "allows converting specific frames" do
42 | gif = sample_content("gif.gif")
43 | convert(gif, "-resize 50x50")
44 | all_frames_size = gif.size
45 |
46 | gif = sample_content("gif.gif")
47 | convert(gif, "-resize 50x50", "frame" => 0)
48 | one_frame_size = gif.size
49 |
50 | one_frame_size.should < all_frames_size
51 | end
52 |
53 | it "accepts input arguments for convert commands" do
54 | image2 = image.clone
55 | convert(image, "")
56 | convert(image2, "", "input_args" => "-extract 50x50+10+10")
57 |
58 | image.should_not equal_image(image2)
59 | image2.should have_width(50)
60 | end
61 |
62 | it "allows converting using specific delegates" do
63 | expect {
64 | convert(image, "", "format" => "jpg", "delegate" => "png")
65 | }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
66 | end
67 |
68 | it "maintains the mime_type meta if it exists already" do
69 | convert(image, "-resize 10x")
70 | image.meta["mime_type"].should be_nil
71 |
72 | image.add_meta("mime_type" => "image/png")
73 | convert(image, "-resize 5x")
74 | image.meta["mime_type"].should == "image/png"
75 | image.mime_type.should == "image/png" # sanity check
76 | end
77 |
78 | it "doesn't maintain the mime_type meta on format change" do
79 | image.add_meta("mime_type" => "image/png")
80 | convert(image, "", "format" => "gif")
81 | image.meta["mime_type"].should be_nil
82 | image.mime_type.should == "image/gif" # sanity check
83 | end
84 | end
85 |
86 | describe "generate" do
87 | let (:image) { Dragonfly::Content.new(app) }
88 |
89 | before(:each) do
90 | generate(image, "-size 1x1 xc:white", "png")
91 | end
92 |
93 | it { image.should have_width(1) }
94 | it { image.should have_height(1) }
95 | it { image.should have_format("png") }
96 | it { image.meta.should == { "format" => "png" } }
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/generators/plain_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "dragonfly/param_validators"
3 |
4 | describe Dragonfly::ImageMagick::Generators::Plain do
5 | let (:generator) { Dragonfly::ImageMagick::Generators::Plain.new }
6 | let (:app) { test_imagemagick_app }
7 | let (:image) { Dragonfly::Content.new(app) }
8 |
9 | describe "of given dimensions" do
10 | before(:each) do
11 | generator.call(image, 3, 2)
12 | end
13 | it { image.should have_width(3) }
14 | it { image.should have_height(2) }
15 | it { image.should have_format("png") }
16 | it { image.meta.should == { "format" => "png", "name" => "plain.png" } }
17 | end
18 |
19 | describe "specifying the format" do
20 | before(:each) do
21 | generator.call(image, 1, 1, "format" => "gif")
22 | end
23 | it { image.should have_format("gif") }
24 | it { image.meta.should == { "format" => "gif", "name" => "plain.gif" } }
25 | end
26 |
27 | describe "specifying the colour" do
28 | it "works with English spelling" do
29 | generator.call(image, 1, 1, "colour" => "red")
30 | end
31 |
32 | it "works with American spelling" do
33 | generator.call(image, 1, 1, "color" => "red")
34 | end
35 |
36 | it "blows up with a bad colour" do
37 | expect {
38 | generator.call(image, 1, 1, "colour" => "lardoin")
39 | }.to raise_error(Dragonfly::Shell::CommandFailed)
40 | end
41 | end
42 |
43 | describe "urls" do
44 | it "updates the url" do
45 | url_attributes = Dragonfly::UrlAttributes.new
46 | generator.update_url(url_attributes, 1, 1, "format" => "gif")
47 | url_attributes.name.should == "plain.gif"
48 | end
49 | end
50 |
51 | describe "param validations" do
52 | {
53 | "color" => "white -write bad.png",
54 | "colour" => "white -write bad.png",
55 | "format" => "png -write bad.png",
56 | }.each do |opt, value|
57 | it "validates bad opts like #{opt} = '#{value}'" do
58 | expect {
59 | generator.call(image, 1, 1, opt => value)
60 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
61 | end
62 | end
63 |
64 | it "validates width" do
65 | expect {
66 | generator.call(image, "1 -write bad.png", 1)
67 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
68 | end
69 |
70 | it "validates height" do
71 | expect {
72 | generator.call(image, 1, "1 -write bad.png")
73 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/generators/plasma_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dragonfly::ImageMagick::Generators::Plasma do
4 | let (:generator) { Dragonfly::ImageMagick::Generators::Plasma.new }
5 | let (:app) { test_imagemagick_app }
6 | let (:image) { Dragonfly::Content.new(app) }
7 |
8 | describe "call" do
9 | it "generates a png image" do
10 | generator.call(image, 5, 3)
11 | image.should have_width(5)
12 | image.should have_height(3)
13 | image.should have_format("png")
14 | image.meta.should == { "format" => "png", "name" => "plasma.png" }
15 | end
16 |
17 | it "allows changing the format" do
18 | generator.call(image, 1, 1, "format" => "jpg")
19 | image.should have_format("jpeg")
20 | image.meta.should == { "format" => "jpg", "name" => "plasma.jpg" }
21 | end
22 | end
23 |
24 | describe "urls" do
25 | it "updates the url" do
26 | url_attributes = Dragonfly::UrlAttributes.new
27 | generator.update_url(url_attributes, 1, 1, "format" => "jpg")
28 | url_attributes.name.should == "plasma.jpg"
29 | end
30 | end
31 |
32 | describe "param validations" do
33 | it "validates format" do
34 | expect {
35 | generator.call(image, 1, 1, "format" => "png -write bad.png")
36 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
37 | end
38 |
39 | it "validates width" do
40 | expect {
41 | generator.call(image, "1 -write bad.png", 1)
42 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
43 | end
44 |
45 | it "validates height" do
46 | expect {
47 | generator.call(image, 1, "1 -write bad.png")
48 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/generators/text_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dragonfly::ImageMagick::Generators::Text do
4 | let (:generator) { Dragonfly::ImageMagick::Generators::Text.new }
5 | let (:app) { test_imagemagick_app }
6 | let (:image) { Dragonfly::Content.new(app) }
7 |
8 | describe "creating a text image" do
9 | before(:each) do
10 | generator.call(image, "mmm", "font_size" => 12)
11 | end
12 | it { image.should have_width(20..40) } # approximate
13 | it { image.should have_height(10..20) }
14 | it { image.should have_format("png") }
15 | it { image.meta.should == { "format" => "png", "name" => "text.png" } }
16 | end
17 |
18 | describe "specifying the format" do
19 | before(:each) do
20 | generator.call(image, "mmm", "format" => "gif")
21 | end
22 | it { image.should have_format("gif") }
23 | it { image.meta.should == { "format" => "gif", "name" => "text.gif" } }
24 | end
25 |
26 | describe "padding" do
27 | before(:each) do
28 | image_without_padding = image.clone
29 | generator.call(image_without_padding, "mmm", "font_size" => 12)
30 | @width = image_properties(image_without_padding)[:width].to_i
31 | @height = image_properties(image_without_padding)[:height].to_i
32 | end
33 | it "1 number shortcut" do
34 | generator.call(image, "mmm", "padding" => "10")
35 | image.should have_width(@width + 20)
36 | image.should have_height(@height + 20)
37 | end
38 | it "2 numbers shortcut" do
39 | generator.call(image, "mmm", "padding" => "10 5")
40 | image.should have_width(@width + 10)
41 | image.should have_height(@height + 20)
42 | end
43 | it "3 numbers shortcut" do
44 | generator.call(image, "mmm", "padding" => "10 5 8")
45 | image.should have_width(@width + 10)
46 | image.should have_height(@height + 18)
47 | end
48 | it "4 numbers shortcut" do
49 | generator.call(image, "mmm", "padding" => "1 2 3 4")
50 | image.should have_width(@width + 6)
51 | image.should have_height(@height + 4)
52 | end
53 | it "should override the general padding declaration with the specific one (e.g. 'padding-left')" do
54 | generator.call(image, "mmm", "padding" => "10", "padding-left" => 9)
55 | image.should have_width(@width + 19)
56 | image.should have_height(@height + 20)
57 | end
58 | it "should ignore 'px' suffixes" do
59 | generator.call(image, "mmm", "padding" => "1px 2px 3px 4px")
60 | image.should have_width(@width + 6)
61 | image.should have_height(@height + 4)
62 | end
63 | it "bad padding string" do
64 | lambda {
65 | generator.call(image, "mmm", "padding" => "1 2 3 4 5")
66 | }.should raise_error(ArgumentError)
67 | end
68 | end
69 |
70 | describe "urls" do
71 | it "updates the url" do
72 | url_attributes = Dragonfly::UrlAttributes.new
73 | generator.update_url(url_attributes, "mmm", "format" => "gif")
74 | url_attributes.name.should == "text.gif"
75 | end
76 | end
77 |
78 | describe "param validations" do
79 | {
80 | "font" => "Times New Roman -write bad.png",
81 | "font_family" => "Times New Roman -write bad.png",
82 | "color" => "rgb(255, 34, 55) -write bad.png",
83 | "background_color" => "rgb(255, 52, 55) -write bad.png",
84 | "stroke_color" => "rgb(255, 52, 55) -write bad.png",
85 | "format" => "png -write bad.png",
86 | }.each do |opt, value|
87 | it "validates bad opts like #{opt} = '#{value}'" do
88 | expect {
89 | generator.call(image, "some text", opt => value)
90 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
91 | end
92 | end
93 |
94 | ["rgb(33,33,33)", "rgba(33,33,33,0.5)", "rgb(33.5,33.5,33.5)", "#fff", "#efefef", "blue"].each do |colour|
95 | it "allows #{colour.inspect} as a colour specification" do
96 | generator.call(image, "mmm", "color" => colour)
97 | end
98 | end
99 |
100 | ["rgb(33, 33, 33)", "something else", "blue:", "f#ff"].each do |colour|
101 | it "disallows #{colour.inspect} as a colour specification" do
102 | expect {
103 | generator.call(image, "mmm", "color" => colour)
104 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
105 | end
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/plugin_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "a configured imagemagick app" do
4 | let(:app) { test_app.configure_with(:imagemagick) }
5 | let(:image) { app.fetch_file(SAMPLES_DIR.join("beach.png")) }
6 |
7 | describe "env variables" do
8 | let(:app) { test_app }
9 |
10 | it "allows setting the convert command" do
11 | app.configure do
12 | plugin :imagemagick, :convert_command => "/bin/convert"
13 | end
14 | app.env[:convert_command].should == "/bin/convert"
15 | end
16 |
17 | it "allows setting the identify command" do
18 | app.configure do
19 | plugin :imagemagick, :identify_command => "/bin/identify"
20 | end
21 | app.env[:identify_command].should == "/bin/identify"
22 | end
23 | end
24 |
25 | describe "analysers" do
26 | it "should return the width" do
27 | image.width.should == 280
28 | end
29 |
30 | it "should return the height" do
31 | image.height.should == 355
32 | end
33 |
34 | it "should return the aspect ratio" do
35 | image.aspect_ratio.should == (280.0 / 355.0)
36 | end
37 |
38 | it "should say if it's portrait" do
39 | image.portrait?.should be_truthy
40 | image.portrait.should be_truthy # for using with magic attributes
41 | end
42 |
43 | it "should say if it's landscape" do
44 | image.landscape?.should be_falsey
45 | image.landscape.should be_falsey # for using with magic attributes
46 | end
47 |
48 | it "should return the format" do
49 | image.format.should == "png"
50 | end
51 |
52 | it "should say if it's an image" do
53 | image.image?.should be_truthy
54 | image.image.should be_truthy # for using with magic attributes
55 | end
56 |
57 | it "should say if it's not an image" do
58 | app.create("blah").image?.should be_falsey
59 | end
60 |
61 | it "should return false for pdfs" do
62 | image.encode("pdf").image?.should be_falsey
63 | end unless ENV["SKIP_FLAKY_TESTS"]
64 | end
65 |
66 | describe "processors that change the url" do
67 | before do
68 | app.configure { url_format "/:name" }
69 | end
70 |
71 | describe "thumb" do
72 | it "sanity check with format" do
73 | thumb = image.thumb("1x1!", "format" => "jpg")
74 | thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
75 | thumb.width.should == 1
76 | thumb.format.should == "jpeg"
77 | thumb.meta["format"].should == "jpg"
78 | end
79 |
80 | it "sanity check without format" do
81 | thumb = image.thumb("1x1!")
82 | thumb.url.should =~ /^\/beach\.png\?.*job=\w+/
83 | thumb.width.should == 1
84 | thumb.format.should == "png"
85 | thumb.meta["format"].should be_nil
86 | end
87 | end
88 |
89 | describe "encode" do
90 | it "sanity check" do
91 | thumb = image.encode("jpg")
92 | thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
93 | thumb.format.should == "jpeg"
94 | thumb.meta["format"].should == "jpg"
95 | end
96 | end
97 | end
98 |
99 | describe "other processors" do
100 | describe "encode" do
101 | it "should encode the image to the correct format" do
102 | image.encode!("gif")
103 | image.format.should == "gif"
104 | end
105 |
106 | it "should allow for extra args" do
107 | image.encode!("jpg", "-quality 1")
108 | image.format.should == "jpeg"
109 | image.size.should < 2000
110 | end
111 | end
112 |
113 | describe "rotate" do
114 | it "should rotate by 90 degrees" do
115 | image.rotate!(90)
116 | image.width.should == 355
117 | image.height.should == 280
118 | end
119 |
120 | it "disallows bad parameters" do
121 | expect {
122 | image.rotate!("90 -write bad.png").apply
123 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
124 | end
125 | end
126 | end
127 |
128 | describe "identify" do
129 | it "gives the output of the command line" do
130 | image.identify.should =~ /280/
131 | image.identify("-format %h").chomp.should == "355"
132 | end
133 | end
134 |
135 | describe "deprecated convert commands" do
136 | it "raises a deprecated message if using the convert processor" do
137 | expect {
138 | image.convert!("into something").apply
139 | }.to raise_error(/deprecated/i)
140 | end
141 |
142 | it "raises a deprecated message if using the convert generator" do
143 | expect {
144 | image.generate!(:convert, "into something").apply
145 | }.to raise_error(/deprecated/i)
146 | end
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/processors/encode_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dragonfly::ImageMagick::Processors::Encode do
4 | let (:app) { test_imagemagick_app }
5 | let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
6 | let (:processor) { Dragonfly::ImageMagick::Processors::Encode.new }
7 |
8 | it "encodes to a different format" do
9 | processor.call(image, "jpeg")
10 | image.should have_format("jpeg")
11 | end
12 |
13 | describe "param validations" do
14 | it "validates the format param" do
15 | expect {
16 | processor.call(image, "jpeg -write bad.png")
17 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
18 | end
19 |
20 | it "allows good args" do
21 | processor.call(image, "jpeg", "-quality 10")
22 | end
23 |
24 | it "disallows bad args" do
25 | expect {
26 | processor.call(image, "jpeg", "-write bad.png")
27 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/dragonfly/image_magick/processors/thumb_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "ostruct"
3 |
4 | describe Dragonfly::ImageMagick::Processors::Thumb do
5 | let (:app) { test_imagemagick_app }
6 | let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
7 | let (:processor) { Dragonfly::ImageMagick::Processors::Thumb.new }
8 |
9 | it "raises an error if an unrecognized string is given" do
10 | expect {
11 | processor.call(image, "30x40#ne!")
12 | }.to raise_error(ArgumentError)
13 | end
14 |
15 | describe "resizing" do
16 | it "works with xNN" do
17 | processor.call(image, "x30")
18 | image.should have_width(24)
19 | image.should have_height(30)
20 | end
21 |
22 | it "works with NNx" do
23 | processor.call(image, "30x")
24 | image.should have_width(30)
25 | image.should have_height(38)
26 | end
27 |
28 | it "works with NNxNN" do
29 | processor.call(image, "30x30")
30 | image.should have_width(24)
31 | image.should have_height(30)
32 | end
33 |
34 | it "works with NNxNN!" do
35 | processor.call(image, "30x30!")
36 | image.should have_width(30)
37 | image.should have_height(30)
38 | end
39 |
40 | it "works with NNxNN%" do
41 | processor.call(image, "25x50%")
42 | image.should have_width(70)
43 | image.should have_height(178)
44 | end
45 |
46 | describe "NNxNN>" do
47 | it "doesn't resize if the image is smaller than specified" do
48 | processor.call(image, "1000x1000>")
49 | image.should have_width(280)
50 | image.should have_height(355)
51 | end
52 |
53 | it "resizes if the image is larger than specified" do
54 | processor.call(image, "30x30>")
55 | image.should have_width(24)
56 | image.should have_height(30)
57 | end
58 | end
59 |
60 | describe "NNxNN<" do
61 | it "doesn't resize if the image is larger than specified" do
62 | processor.call(image, "10x10<")
63 | image.should have_width(280)
64 | image.should have_height(355)
65 | end
66 |
67 | it "resizes if the image is smaller than specified" do
68 | processor.call(image, "400x400<")
69 | image.should have_width(315)
70 | image.should have_height(400)
71 | end
72 | end
73 | end
74 |
75 | describe "cropping" do # Difficult to test here other than dimensions
76 | it "crops" do
77 | processor.call(image, "10x20+30+30")
78 | image.should have_width(10)
79 | image.should have_height(20)
80 | end
81 |
82 | it "crops with gravity" do
83 | image2 = image.clone
84 |
85 | processor.call(image, "10x8nw")
86 | image.should have_width(10)
87 | image.should have_height(8)
88 |
89 | processor.call(image2, "10x8se")
90 | image2.should have_width(10)
91 | image2.should have_height(8)
92 |
93 | image2.should_not equal_image(image)
94 | end
95 |
96 | it "raises if given both gravity and offset" do
97 | expect {
98 | processor.call(image, "100x100+10+10se")
99 | }.to raise_error(ArgumentError)
100 | end
101 |
102 | it "works when the crop area is outside the image" do
103 | processor.call(image, "100x100+250+300")
104 | image.should have_width(30)
105 | image.should have_height(55)
106 | end
107 |
108 | it "crops twice in a row correctly" do
109 | processor.call(image, "100x100+10+10")
110 | processor.call(image, "50x50+0+0")
111 | image.should have_width(50)
112 | image.should have_height(50)
113 | end
114 | end
115 |
116 | describe "resize_and_crop" do
117 | it "crops to the correct dimensions" do
118 | processor.call(image, "100x100#")
119 | image.should have_width(100)
120 | image.should have_height(100)
121 | end
122 |
123 | it "resizes before cropping" do
124 | image2 = image.clone
125 | processor.call(image, "100x100#")
126 | processor.call(image2, "100x100c")
127 | image2.should_not equal_image(image)
128 | end
129 |
130 | it "works with gravity" do
131 | image2 = image.clone
132 | processor.call(image, "10x10#nw")
133 | processor.call(image, "10x10#se")
134 | image2.should_not equal_image(image)
135 | end
136 | end
137 |
138 | describe "format" do
139 | let (:url_attributes) { OpenStruct.new }
140 |
141 | it "changes the format if passed in" do
142 | processor.call(image, "2x2", "format" => "jpeg")
143 | image.should have_format("jpeg")
144 | end
145 |
146 | it "doesn't change the format if not passed in" do
147 | processor.call(image, "2x2")
148 | image.should have_format("png")
149 | end
150 |
151 | it "updates the url ext if passed in" do
152 | processor.update_url(url_attributes, "2x2", "format" => "png")
153 | url_attributes.ext.should == "png"
154 | end
155 |
156 | it "doesn't update the url ext if not passed in" do
157 | processor.update_url(url_attributes, "2x2")
158 | url_attributes.ext.should be_nil
159 | end
160 | end
161 |
162 | describe "args_for_geometry" do
163 | it "returns the convert arguments used for a given geometry" do
164 | expect(processor.args_for_geometry("30x40")).to eq("-resize 30x40")
165 | end
166 | end
167 |
168 | describe "param validations" do
169 | {
170 | "format" => "png -write bad.png",
171 | "frame" => "0] -write bad.png [",
172 | }.each do |opt, value|
173 | it "validates bad opts like #{opt} = '#{value}'" do
174 | expect {
175 | processor.call(image, "30x30", opt => value)
176 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
177 | end
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/dragonfly/job/fetch_file_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Job::FetchFile do
4 |
5 | let (:app) { test_app }
6 | let (:job) { Dragonfly::Job.new(app) }
7 |
8 | before(:each) do
9 | job.fetch_file!(SAMPLES_DIR.join('egg.png'))
10 | end
11 |
12 | it { job.steps.should match_steps([Dragonfly::Job::FetchFile]) }
13 |
14 | it "should fetch the specified file when applied" do
15 | job.size.should == 62664
16 | end
17 |
18 | it "should set the url_attributes" do
19 | job.url_attributes.name.should == 'egg.png'
20 | end
21 |
22 | it "should set the name" do
23 | job.name.should == 'egg.png'
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/spec/dragonfly/job/fetch_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Job::Fetch do
4 |
5 | let (:app) { test_app }
6 | let (:job) { Dragonfly::Job.new(app) }
7 |
8 | before(:each) do
9 | job.fetch!('some_uid')
10 | end
11 |
12 | it { job.steps.should match_steps([Dragonfly::Job::Fetch]) }
13 |
14 | it "should read from the app's datastore when applied" do
15 | app.datastore.should_receive(:read).with('some_uid').and_return ["", {}]
16 | job.apply
17 | end
18 |
19 | it "raises NotFound if the datastore returns nil" do
20 | app.datastore.should_receive(:read).and_return(nil)
21 | expect {
22 | job.apply
23 | }.to raise_error(Dragonfly::Job::Fetch::NotFound)
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/spec/dragonfly/job/fetch_url_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'base64'
3 |
4 | describe Dragonfly::Job::FetchUrl do
5 |
6 | let (:app) { test_app }
7 | let (:job) { Dragonfly::Job.new(app) }
8 |
9 | before(:each) do
10 | stub_request(:get, %r{http://place\.com.*}).to_return(:body => 'result!')
11 | end
12 |
13 | it "adds a step" do
14 | job.fetch_url!('some.url')
15 | job.steps.should match_steps([Dragonfly::Job::FetchUrl])
16 | end
17 |
18 | it "should fetch the specified url when applied" do
19 | job.fetch_url!('http://place.com')
20 | job.data.should == "result!"
21 | end
22 |
23 | it "should set the mime_type when returned by the request" do
24 | stub_request(:get, %r{http://thing\.com.*}).to_return(:body => '', :headers => {'content-type' => 'text/html'})
25 | expect(job.fetch_url('http://place.com').mime_type).to eq('application/octet-stream')
26 | expect(job.fetch_url('http://thing.com').mime_type).to eq('text/html')
27 | end
28 |
29 | it "should default to http" do
30 | job.fetch_url!('place.com')
31 | job.data.should == "result!"
32 | end
33 |
34 | it "should also work with https" do
35 | stub_request(:get, 'https://place.com').to_return(:body => 'secure result!')
36 | job.fetch_url!('https://place.com')
37 | job.data.should == "secure result!"
38 | end
39 |
40 | it 'should also work with basic auth' do
41 | stub_request(:get, 'http://example.com').with(basic_auth: ['user', 'pass']).to_return(:body => 'basic auth')
42 | job.fetch_url!('http://user:pass@example.com')
43 | job.data.should == 'basic auth'
44 | end
45 |
46 | [
47 | "place.com",
48 | "http://place.com",
49 | "place.com/",
50 | "place.com/stuff/",
51 | "place.com/?things",
52 | "place.com:8080"
53 | ].each do |url|
54 | it "doesn't set the name if there isn't one, e.g. for #{url}" do
55 | job.fetch_url!(url)
56 | job.name.should be_nil
57 | end
58 |
59 | it "doesn't set the name url_attr if there isn't one, e.g. for #{url}" do
60 | job.fetch_url!(url)
61 | job.url_attributes.name.should be_nil
62 | end
63 | end
64 |
65 | [
66 | "place.com/dung.beetle",
67 | "http://place.com/dung.beetle",
68 | "place.com/stuff/dung.beetle",
69 | "place.com/dung.beetle?morethings",
70 | "place.com:8080/dung.beetle"
71 | ].each do |url|
72 | it "sets the name if there is one, e.g. for #{url}" do
73 | job.fetch_url!(url)
74 | job.name.should == 'dung.beetle'
75 | end
76 |
77 | it "sets the name url_attr if there is one, e.g. for #{url}" do
78 | job.fetch_url!(url)
79 | job.url_attributes.name.should == 'dung.beetle'
80 | end
81 | end
82 |
83 | it "works with domain:port" do
84 | stub_request(:get, "localhost:8080").to_return(:body => "okay!")
85 | thing = job.fetch_url('localhost:8080')
86 | thing.name.should be_nil
87 | thing.data.should == 'okay!'
88 | end
89 |
90 | it "should raise an error if not found" do
91 | stub_request(:get, "notfound.com").to_return(:status => 404, :body => "BLAH")
92 | expect{
93 | job.fetch_url!('notfound.com').apply
94 | }.to raise_error(Dragonfly::Job::FetchUrl::ErrorResponse){|error|
95 | error.status.should == 404
96 | error.body.should == "BLAH"
97 | }
98 | end
99 |
100 | it "should raise an error if server error" do
101 | stub_request(:get, "error.com").to_return(:status => 500, :body => "BLAH")
102 | expect{
103 | job.fetch_url!('error.com').apply
104 | }.to raise_error(Dragonfly::Job::FetchUrl::ErrorResponse){|error|
105 | error.status.should == 500
106 | error.body.should == "BLAH"
107 | }
108 | end
109 |
110 | describe "escaping" do
111 | before do
112 | stub_request(:get, "escapedurl.com/escaped%20url%5B1%5D.jpg").to_return(:body => "OK!")
113 | end
114 |
115 | it "works with escaped urls" do
116 | job.fetch_url('escapedurl.com/escaped%20url%5B1%5D.jpg').data.should == 'OK!'
117 | end
118 |
119 | it "tries to escape unescaped urls" do
120 | job.fetch_url('escapedurl.com/escaped url[1].jpg').data.should == 'OK!'
121 | end
122 |
123 | it "still blows up with bad urls" do
124 | expect {
125 | job.fetch_url('1:{')
126 | }.to raise_error(Dragonfly::Job::FetchUrl::BadURI)
127 | end
128 | end
129 |
130 | describe "redirects" do
131 | it "should follow redirects" do
132 | stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'http://ok.com'})
133 | stub_request(:get, "ok.com").to_return(:body => "OK!")
134 | job.fetch_url('redirectme.com').data.should == 'OK!'
135 | end
136 |
137 | it "follows redirects to https" do
138 | stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'https://ok.com'})
139 | stub_request(:get, /ok.com/).to_return(:body => "OK!")
140 | job.fetch_url('redirectme.com').data.should == 'OK!'
141 | end
142 |
143 | it "raises if redirecting too many times" do
144 | stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'http://redirectme-back.com'})
145 | stub_request(:get, "redirectme-back.com").to_return(:status => 302, :headers => {'Location' => 'http://redirectme.com'})
146 | expect {
147 | job.fetch_url('redirectme.com').apply
148 | }.to raise_error(Dragonfly::Job::FetchUrl::TooManyRedirects)
149 | end
150 |
151 | it "follows relative responses" do
152 | stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'relative-path.html'})
153 | stub_request(:get, "redirectme.com/relative-path.html").to_return(:body => "OK!")
154 | job.fetch_url('redirectme.com').data.should == 'OK!'
155 | end
156 | end
157 |
158 | describe "data uris" do
159 | it "accepts standard base64 encoded data uris" do
160 | job.fetch_url!("data:text/plain;base64,aGVsbG8=\n")
161 | job.data.should == 'hello'
162 | job.mime_type.should == 'text/plain'
163 | job.ext.should == 'txt'
164 | end
165 |
166 | it "accepts long base64 encoded data uris with newline" do
167 | str = 'hello' * 10
168 | job.fetch_url!("data:text/plain;base64,#{Base64.encode64(str)}")
169 | job.data.should == str
170 | end
171 |
172 | it "accepts long base64 encoded data uris without newline" do
173 | str = 'hello' * 10
174 | job.fetch_url!("data:text/plain;base64,#{Base64.strict_encode64(str)}")
175 | job.data.should == str
176 | end
177 |
178 | it "doesn't accept other data uris" do
179 | expect {
180 | job.fetch_url!("data:text/html;charset=utf-8,").apply
181 | }.to raise_error(Dragonfly::Job::FetchUrl::CannotHandle)
182 | end
183 | end
184 |
185 | end
186 |
--------------------------------------------------------------------------------
/spec/dragonfly/job/generate_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Job::Generate do
4 |
5 | let (:app) { test_app }
6 | let (:job) { Dragonfly::Job.new(app) }
7 |
8 | before :each do
9 | app.add_generator(:plasma){}
10 | end
11 |
12 | it "adds a step" do
13 | job.generate!(:plasma, 20)
14 | job.steps.should match_steps([Dragonfly::Job::Generate])
15 | end
16 |
17 | it "uses the generator when applied" do
18 | job.generate!(:plasma, 20)
19 | app.get_generator(:plasma).should_receive(:call).with(job.content, 20)
20 | job.apply
21 | end
22 |
23 | it "updates the url if method exists" do
24 | app.get_generator(:plasma).should_receive(:update_url).with(job.url_attributes, 20)
25 | job.generate!(:plasma, 20)
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/spec/dragonfly/job/process_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Job::Process do
4 |
5 | let (:app) { test_app }
6 | let (:job) { Dragonfly::Job.new(app) }
7 |
8 | before :each do
9 | app.add_processor(:resize){}
10 | end
11 |
12 | it "adds a step" do
13 | job.process!(:resize, '20x30')
14 | job.steps.should match_steps([Dragonfly::Job::Process])
15 | end
16 |
17 | it "should use the processor when applied" do
18 | job.process!(:resize, '20x30')
19 | app.get_processor(:resize).should_receive(:call).with(job.content, '20x30')
20 | job.apply
21 | end
22 |
23 | it "should call update_url immediately with the url_attributes" do
24 | app.get_processor(:resize).should_receive(:update_url).with(job.url_attributes, '20x30')
25 | job.process!(:resize, '20x30')
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/spec/dragonfly/job_endpoint_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | ## General tests for Response go here as it's a pretty simple wrapper around that
5 |
6 | describe "Dragonfly::JobEndpoint Rack::Lint tests" do
7 | before(:each) do
8 | @app = test_app
9 | @app.add_generator(:test_data){|content| content.update("Test Data") }
10 | @job = @app.generate(:test_data)
11 | @endpoint = Rack::Lint.new(Dragonfly::JobEndpoint.new(@job))
12 | end
13 |
14 | it "should pass for HEAD requests" do
15 | Rack::MockRequest.new(@endpoint).request("HEAD", '')
16 | end
17 |
18 | it "should pass for GET requests" do
19 | Rack::MockRequest.new(@endpoint).request("GET", '')
20 | end
21 |
22 | it "should pass for POST requests" do
23 | Rack::MockRequest.new(@endpoint).request("POST", '')
24 | end
25 |
26 | it "should pass for PUT requests" do
27 | Rack::MockRequest.new(@endpoint).request("PUT", '')
28 | end
29 |
30 | it "should pass for DELETE requests" do
31 | Rack::MockRequest.new(@endpoint).request("DELETE", '')
32 | end
33 | end
34 |
35 | describe Dragonfly::JobEndpoint do
36 |
37 | def make_request(job, opts={})
38 | endpoint = Dragonfly::JobEndpoint.new(job)
39 | method = (opts.delete(:method) || :get).to_s.upcase
40 | uri = opts[:path] || ""
41 | Rack::MockRequest.new(endpoint).request(method, uri, opts)
42 | end
43 |
44 | before(:each) do
45 | @app = test_app
46 | uid = @app.store("GUNGLE", 'name' => 'gung.txt')
47 | @job = @app.fetch(uid)
48 | end
49 |
50 | it "should return a correct response to a successful GET request" do
51 | response = make_request(@job)
52 | response.status.should == 200
53 | response['etag'].should =~ /^"\w+"$/
54 | response['cache-control'].should == "public, max-age=31536000"
55 | response['content-type'].should == 'text/plain'
56 | response['content-length'].should == '6'
57 | response['content-disposition'].should == 'filename="gung.txt"'
58 | response.body.should == 'GUNGLE'
59 | end
60 |
61 | it "should return the correct headers and no content to a successful HEAD request" do
62 | response = make_request(@job, :method => :head)
63 | response.status.should == 200
64 | response['etag'].should =~ /^"\w+"$/
65 | response['cache-control'].should == "public, max-age=31536000"
66 | response['content-type'].should == 'text/plain'
67 | response['content-length'].should == '6'
68 | response['content-disposition'].should == 'filename="gung.txt"'
69 | response.body.should == ''
70 | end
71 |
72 | %w(POST PUT DELETE CUSTOM_METHOD).each do |method|
73 |
74 | it "should return a 405 error for a #{method} request" do
75 | response = make_request(@job, :method => method)
76 | response.status.should == 405
77 | response['allow'].should == "GET, HEAD"
78 | response['content-type'].should == 'text/plain'
79 | response.body.should == "method not allowed"
80 | end
81 |
82 | end
83 |
84 | it "should return 404 if the datastore raises NotFound" do
85 | @job.should_receive(:apply).and_raise(Dragonfly::Job::Fetch::NotFound)
86 | response = make_request(@job)
87 | response.status.should == 404
88 | end
89 |
90 | it "returns a 500 for any runtime error" do
91 | @job.should_receive(:apply).and_raise(RuntimeError, "oh dear")
92 | Dragonfly.should_receive(:warn).with(/oh dear/)
93 | response = make_request(@job)
94 | response.status.should == 500
95 | end
96 |
97 | describe "default content disposition file name" do
98 | before do
99 | uid = @app.store("GUNGLE", 'name' => 'güng.txt')
100 | @job = @app.fetch(uid)
101 | end
102 |
103 | it "doesn't encode utf8 characters" do
104 | response = make_request(@job)
105 | response['content-disposition'].should == 'filename="güng.txt"'
106 | end
107 |
108 | it "does encode them if the request is from IE" do
109 | response = make_request(@job, 'HTTP_USER_AGENT' => "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; el-GR)")
110 | response['content-disposition'].should == 'filename="g%C3%BCng.txt"'
111 | end
112 | end
113 |
114 | describe "logging" do
115 | it "logs successful requests" do
116 | Dragonfly.should_receive(:info).with("GET /something?great 200")
117 | make_request(@job, :path => '/something?great')
118 | end
119 | end
120 |
121 | describe "ETag" do
122 | it "should return an ETag" do
123 | response = make_request(@job)
124 | response.headers['etag'].should =~ /^"\w+"$/
125 | end
126 |
127 | [
128 | "dingle",
129 | "dingle, eggheads",
130 | '"dingle", "eggheads"',
131 | '*'
132 | ].each do |header|
133 | it "should return a 304 if the correct ETag is specified in HTTP_IF_NONE_MATCH header e.g. #{header}" do
134 | @job.should_receive(:signature).at_least(:once).and_return('dingle')
135 | response = make_request(@job, 'HTTP_IF_NONE_MATCH' => header)
136 | response.status.should == 304
137 | response['etag'].should == '"dingle"'
138 | response['cache-control'].should == "public, max-age=31536000"
139 | response.body.should be_empty
140 | end
141 | end
142 |
143 | it "should not have applied any steps if the correct ETag is specified in HTTP_IF_NONE_MATCH header" do
144 | response = make_request(@job, 'HTTP_IF_NONE_MATCH' => @job.signature)
145 | @job.applied_steps.should be_empty
146 | end
147 | end
148 |
149 | describe "custom headers" do
150 | before(:each) do
151 | @app.configure{ response_header 'This-is', 'brill' }
152 | end
153 | it "should allow specifying custom headers" do
154 | make_request(@job).headers['this-is'].should == 'brill'
155 | end
156 | it "should not interfere with other headers" do
157 | make_request(@job).headers['content-length'].should == '6'
158 | end
159 | it "should allow overridding other headers" do
160 | @app.response_headers['cache-control'] = 'try me'
161 | make_request(@job).headers['cache-control'].should == 'try me'
162 | end
163 | it "should allow giving a proc" do
164 | @app.response_headers['cache-control'] = proc{|job, request, headers|
165 | [job.basename.reverse.upcase, request.params['a'], headers['cache-control'].chars.first].join(',')
166 | }
167 | response = make_request(@job, 'QUERY_STRING' => 'a=egg')
168 | response['cache-control'].should == 'GNUG,egg,p'
169 | end
170 | it "should allow removing by setting to nil" do
171 | @app.response_headers['cache-control'] = nil
172 | make_request(@job).headers.should_not have_key('cache-control')
173 | end
174 | end
175 |
176 | describe "setting the job in the env for communicating with other rack middlewares" do
177 | before(:each) do
178 | @app.add_generator(:test_data){ "TEST DATA" }
179 | @job = @app.generate(:test_data)
180 | @endpoint = Dragonfly::JobEndpoint.new(@job)
181 | @middleware = Class.new do
182 | def initialize(app)
183 | @app = app
184 | end
185 |
186 | def call(env)
187 | @app.call(env)
188 | throw :result, env['dragonfly.job']
189 | end
190 | end
191 | end
192 | it "should add the job to env" do
193 | middleware, endpoint = @middleware, @endpoint
194 | app = Rack::Builder.new do
195 | use middleware
196 | run endpoint
197 | end
198 | result = catch(:result){ Rack::MockRequest.new(app).get('/') }
199 | result.should == @job
200 | end
201 | end
202 |
203 | describe "inspect" do
204 | it "should be pretty yo" do
205 | @job.to_app.inspect.should =~ %r{}
206 | end
207 | end
208 |
209 | end
210 |
--------------------------------------------------------------------------------
/spec/dragonfly/memory_data_store_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'dragonfly/spec/data_store_examples'
3 |
4 | describe Dragonfly::MemoryDataStore do
5 |
6 | before(:each) do
7 | @data_store = Dragonfly::MemoryDataStore.new
8 | end
9 |
10 | it_should_behave_like 'data_store'
11 |
12 | it "allows setting the uid" do
13 | uid = @data_store.write(Dragonfly::Content.new(test_app, "Hello"), :uid => 'some_uid')
14 | uid.should == 'some_uid'
15 | data, meta = @data_store.read(uid)
16 | data.should == 'Hello'
17 | end
18 |
19 | end
20 |
21 |
--------------------------------------------------------------------------------
/spec/dragonfly/middleware_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rack'
3 |
4 | def dummy_rack_app
5 | lambda{|env| [200, {"content-type" => "text/html"}, ["dummy_rack_app body"]] }
6 | end
7 |
8 | describe Dragonfly::Middleware do
9 |
10 | def make_request(app, url)
11 | Rack::MockRequest.new(app).get(url)
12 | end
13 |
14 | describe "using the default app" do
15 | before(:each) do
16 | @stack = Rack::Builder.new do
17 | use Dragonfly::Middleware
18 | run dummy_rack_app
19 | end
20 | end
21 |
22 | it "should pass through if the app returns x-cascade: pass" do
23 | Dragonfly.app.should_receive(:call).and_return(
24 | [404, {"content-type" => 'text/plain', 'x-cascade' => 'pass'}, ['Not found']]
25 | )
26 | response = make_request(@stack, '/media/hello.png?howare=you')
27 | response.body.should == 'dummy_rack_app body'
28 | response.status.should == 200
29 | end
30 |
31 | it "should still pass through using deprecated uppercase X-Cascade: pass" do
32 | Dragonfly.app.should_receive(:call).and_return(
33 | [404, {"content-type" => 'text/plain', 'X-Cascade' => 'pass'}, ['Not found']]
34 | )
35 | response = make_request(@stack, '/media/hello.png?howare=you')
36 | response.body.should == 'dummy_rack_app body'
37 | response.status.should == 200
38 | end
39 |
40 | it "should return a 404 if the app returns a 404" do
41 | Dragonfly.app.should_receive(:call).and_return(
42 | [404, {"content-type" => 'text/plain'}, ['Not found']]
43 | )
44 | response = make_request(@stack, '/media/hello.png?howare=you')
45 | response.status.should == 404
46 | end
47 |
48 | it "should return as per the dragonfly app if the app returns a 200" do
49 | Dragonfly.app.should_receive(:call).and_return(
50 | [200, {"content-type" => 'text/plain'}, ['ABCD']]
51 | )
52 | response = make_request(@stack, '/media/hello.png?howare=you')
53 | response.status.should == 200
54 | response.body.should == 'ABCD'
55 | end
56 | end
57 |
58 | describe "using a custom app" do
59 | before(:each) do
60 | @stack = Rack::Builder.new do
61 | use Dragonfly::Middleware, :images
62 | run dummy_rack_app
63 | end
64 | end
65 |
66 | it "should use the specified dragonfly app" do
67 | Dragonfly.app.should_not_receive(:call)
68 | Dragonfly.app(:images).should_receive(:call).and_return([
69 | 200, {"content-type" => 'text/plain'}, ['booboo']
70 | ])
71 | response = make_request(@stack, '/media/hello.png?howare=you')
72 | response.body.should == 'booboo'
73 | end
74 | end
75 |
76 | end
77 |
--------------------------------------------------------------------------------
/spec/dragonfly/model/active_record_spec.rb:
--------------------------------------------------------------------------------
1 | # jruby has problems with installing sqlite3 - don't bother with these tests for jruby
2 | unless RUBY_PLATFORM == "java"
3 | require "spec_helper"
4 | require "active_record"
5 | require "sqlite3"
6 |
7 | # ActiveRecord specific stuff goes here (there should be very little!)
8 | describe "ActiveRecord models" do
9 | let! :dragonfly_app do test_app(:test_ar) end
10 |
11 | before :all do
12 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
13 |
14 | ActiveRecord::Migration.verbose = false
15 |
16 | ActiveRecord::Schema.define(:version => 1) do
17 | create_table :photos do |t|
18 | t.string :image_uid
19 | end
20 | end
21 |
22 | class Photo < ActiveRecord::Base
23 | extend Dragonfly::Model
24 | dragonfly_accessor :image, app: :test_ar
25 | end
26 | end
27 |
28 | after :all do
29 | Photo.destroy_all
30 | ActiveRecord::Base.remove_connection()
31 | end
32 |
33 | describe "destroying" do
34 | before do
35 | Photo.destroy_all
36 | @photo = Photo.create(image: "some data")
37 | end
38 |
39 | def data_exists(uid)
40 | !!dragonfly_app.datastore.read(uid)
41 | end
42 |
43 | it "should not remove the attachment if a transaction is cancelled" do
44 | Photo.transaction do
45 | @photo.destroy
46 | raise ActiveRecord::Rollback
47 | end
48 | photo = Photo.last
49 | expect(photo.image_uid).not_to be_nil
50 | expect(data_exists(photo.image_uid)).to eq(true)
51 | end
52 |
53 | it "should remove the attachment as per usual otherwise" do
54 | uid = @photo.image_uid
55 | @photo.destroy
56 | photo = Photo.last
57 | expect(photo).to be_nil
58 | expect(data_exists(uid)).to eq(false)
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/spec/dragonfly/model/validations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'dragonfly/model/validations'
3 |
4 | describe Dragonfly::Model::Validations do
5 |
6 | before(:each) do
7 | @app = test_app
8 | end
9 |
10 | describe "validates_presence_of" do
11 | before(:each) do
12 | @item_class = new_model_class('Item', :preview_image_uid) do
13 | dragonfly_accessor :preview_image
14 | validates_presence_of :preview_image
15 | end
16 | end
17 |
18 | it "should be valid if set" do
19 | @item_class.new(:preview_image => "1234567890").should be_valid
20 | end
21 |
22 | it "should be invalid if not set" do
23 | @item_class.new.should_not be_valid
24 | end
25 | end
26 |
27 | describe "validates_size_of" do
28 | before(:each) do
29 | @item_class = new_model_class('Item', :preview_image_uid) do
30 | dragonfly_accessor :preview_image
31 | validates_size_of :preview_image, :within => (6..10)
32 | end
33 | end
34 |
35 | it "should be valid if ok" do
36 | @item_class.new(:preview_image => "1234567890").should be_valid
37 | end
38 |
39 | it "should be invalid if too small" do
40 | @item_class.new(:preview_image => "12345").should_not be_valid
41 | end
42 | end
43 |
44 | describe "validates_property" do
45 |
46 | before(:each) do
47 | @item_class = new_model_class('Item',
48 | :preview_image_uid,
49 | :other_image_uid,
50 | :title
51 | ) do
52 | extend Dragonfly::Model::Validations
53 |
54 | dragonfly_accessor :preview_image
55 | dragonfly_accessor :other_image
56 | end
57 | end
58 |
59 | before(:each) do
60 | @item = @item_class.new(:preview_image => "1234567890")
61 | end
62 |
63 | it "should be valid if the property is correct" do
64 | @item_class.class_eval do
65 | validates_property :gungle, :of => :preview_image, :as => 'bungo'
66 | end
67 | @item.preview_image = "something"
68 | @item.preview_image.should_receive(:gungle).and_return('bungo')
69 | @item.should be_valid
70 | end
71 |
72 | it "should be valid if nil, if not validated on presence (even with validates_property)" do
73 | @item_class.class_eval do
74 | validates_property :size, :of => :preview_image, :as => 234
75 | end
76 | @item.preview_image = nil
77 | @item.should be_valid
78 | end
79 |
80 | it "should be invalid if the property is nil" do
81 | @item_class.class_eval do
82 | validates_property :gungle, :of => :preview_image, :in => ['bungo', 'jerry']
83 | end
84 | @item.preview_image = "something"
85 | @item.preview_image.should_receive(:gungle).and_return(nil)
86 | @item.should_not be_valid
87 | @item.errors[:preview_image].should == ["gungle is incorrect. It needs to be one of 'bungo', 'jerry'"]
88 | end
89 |
90 | it "should be invalid if the property is wrong" do
91 | @item_class.class_eval do
92 | validates_property :gungle, :of => :preview_image, :in => ['bungo', 'jerry']
93 | end
94 | @item.preview_image = "something"
95 | @item.preview_image.should_receive(:gungle).and_return('spangle')
96 | @item.should_not be_valid
97 | @item.errors[:preview_image].should == ["gungle is incorrect. It needs to be one of 'bungo', 'jerry', but was 'spangle'"]
98 | end
99 |
100 | it "is invalid if the property raises" do
101 | @item_class.class_eval do
102 | validates_property :gungle, :of => :preview_image, :as => 'bungo'
103 | end
104 | @item.preview_image = "something"
105 | @item.preview_image.should_receive(:gungle).and_raise(RuntimeError, "yikes!")
106 | @item.should_not be_valid
107 | @item.errors[:preview_image].should == ["gungle is incorrect. It needs to be 'bungo'"]
108 | end
109 |
110 | it "should work for a range" do
111 | @item_class.class_eval do
112 | validates_property :gungle, :of => :preview_image, :in => (0..2)
113 | end
114 | @item.preview_image = "something"
115 | @item.preview_image.should_receive(:gungle).and_return(3)
116 | @item.should_not be_valid
117 | @item.errors[:preview_image].should == ["gungle is incorrect. It needs to be between 0 and 2, but was '3'"]
118 | end
119 |
120 | it "should validate individually" do
121 | @item_class.class_eval do
122 | validates_property :size, :of => [:preview_image, :other_image], :as => 9
123 | end
124 | @item.preview_image = "something"
125 | @item.other_image = "something else"
126 | @item.should_not be_valid
127 | @item.errors[:preview_image].should == []
128 | @item.errors[:other_image].should == ["size is incorrect. It needs to be '9', but was '14'"]
129 | end
130 |
131 | it "should include standard extra options like 'if' on mime type validation" do
132 | @item_class.class_eval do
133 | validates_property :size, :of => :preview_image, :as => 4, :if => :its_friday
134 | end
135 | @item.preview_image = '13 characters'
136 | @item.should_receive(:its_friday).and_return(false)
137 | @item.should be_valid
138 | end
139 |
140 | it "should allow case sensitivity to be turned off when :as is specified" do
141 | @item_class.class_eval do
142 | validates_property :gungle, :of => :preview_image, :as => 'oKtHeN', :case_sensitive => false
143 | end
144 | @item.preview_image = "something"
145 | @item.preview_image.should_receive(:gungle).and_return('OKTHEN')
146 | @item.should be_valid
147 | end
148 |
149 | it "should allow case sensitivity to be turned off when :in is specified" do
150 | @item_class.class_eval do
151 | validates_property :gungle, :of => :preview_image, :in => ['oKtHeN'], :case_sensitive => false
152 | end
153 | @item.preview_image = "something"
154 | @item.preview_image.should_receive(:gungle).and_return('OKTHEN')
155 | @item.should be_valid
156 | end
157 |
158 | it "should require either :as or :in as an argument" do
159 | lambda{
160 | @item_class.class_eval do
161 | validates_property :mime_type, :of => :preview_image
162 | end
163 | }.should raise_error(ArgumentError)
164 | end
165 |
166 | it "should require :of as an argument" do
167 | lambda{
168 | @item_class.class_eval do
169 | validates_property :mime_type, :as => 'hi/there'
170 | end
171 | }.should raise_error(ArgumentError)
172 | end
173 |
174 | it "should allow for custom messages" do
175 | @item_class.class_eval do
176 | validates_property :size, :of => :preview_image, :as => 4, :message => "errado, seu burro"
177 | end
178 | @item.preview_image = "something"
179 | @item.should_not be_valid
180 | @item.errors[:preview_image].should == ["errado, seu burro"]
181 | end
182 |
183 | it "should allow for custom messages including access to the property name and expected/allowed values" do
184 | @item_class.class_eval do
185 | validates_property :size, :of => :preview_image, :as => 4,
186 | :message => proc{|actual, model| "Unlucky #{model.title}! Was #{actual}" }
187 | end
188 | @item.title = 'scubby'
189 | @item.preview_image = "too long"
190 | @item.should_not be_valid
191 | @item.errors[:preview_image].should == ["Unlucky scubby! Was 8"]
192 | end
193 |
194 | end
195 |
196 | end
197 |
--------------------------------------------------------------------------------
/spec/dragonfly/param_validators_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "dragonfly/param_validators"
3 |
4 | describe Dragonfly::ParamValidators do
5 | include Dragonfly::ParamValidators
6 |
7 | describe "validate!" do
8 | it "does nothing if the parameter meets the condition" do
9 | validate!("thing") { |t| t === "thing" }
10 | end
11 |
12 | it "raises if the parameter doesn't meet the condition" do
13 | expect {
14 | validate!("thing") { |t| t === "ting" }
15 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
16 | end
17 |
18 | it "does nothing if the parameter is nil" do
19 | validate!(nil) { |t| t === "thing" }
20 | end
21 | end
22 |
23 | describe "validate_all!" do
24 | it "allows passing an array of parameters to validate" do
25 | validate_all!(["a", "b"]) { |p| /\w/ === p }
26 | expect {
27 | validate_all!(["a", " "]) { |p| /\w/ === p }
28 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
29 | end
30 | end
31 |
32 | describe "validate_all_keys!" do
33 | it "allows passing an array of parameters to validate" do
34 | obj = { "a" => "A", "b" => "B" }
35 | validate_all_keys!(obj, ["a", "b"]) { |p| /\w/ === p }
36 | expect {
37 | validate_all_keys!(obj, ["a", "b"]) { |p| /[a-z]/ === p }
38 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
39 | end
40 | end
41 |
42 | describe "is_number" do
43 | [3, 3.14, "3", "3.2"].each do |val|
44 | it "validates #{val.inspect}" do
45 | validate!(val, &is_number)
46 | end
47 | end
48 |
49 | ["", "3 2", "hello4", {}, []].each do |val|
50 | it "validates #{val.inspect}" do
51 | expect {
52 | validate!(val, &is_number)
53 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
54 | end
55 | end
56 | end
57 |
58 | describe "is_word" do
59 | ["hello", "helLo", "HELLO"].each do |val|
60 | it "validates #{val.inspect}" do
61 | validate!(val, &is_word)
62 | end
63 | end
64 |
65 | ["", "hel%$lo", "hel lo", "hel-lo", {}, []].each do |val|
66 | it "validates #{val.inspect}" do
67 | expect {
68 | validate!(val, &is_word)
69 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
70 | end
71 | end
72 | end
73 |
74 | describe "is_words" do
75 | ["hello there", "Hi", " What is Up "].each do |val|
76 | it "validates #{val.inspect}" do
77 | validate!(val, &is_words)
78 | end
79 | end
80 |
81 | ["", "hel%$lo", "What's up", "hel-lo", {}, []].each do |val|
82 | it "validates #{val.inspect}" do
83 | expect {
84 | validate!(val, &is_words)
85 | }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/spec/dragonfly/register_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Register do
4 |
5 | let (:register) { Dragonfly::Register.new }
6 | let (:thing) { proc{ "BOO" } }
7 |
8 | it "adds an item" do
9 | register.add(:thing, thing)
10 | register.get(:thing).should == thing
11 | end
12 |
13 | it "adds from a block" do
14 | register.add(:thing, &thing)
15 | register.get(:thing).should == thing
16 | end
17 |
18 | it "raises an error if neither are given" do
19 | expect {
20 | register.add(:something)
21 | }.to raise_error(ArgumentError)
22 | end
23 |
24 | it "raises an error if getting one that doesn't exist" do
25 | expect {
26 | register.get(:thing)
27 | }.to raise_error(Dragonfly::Register::NotFound)
28 | end
29 |
30 | it "allows getting with a string" do
31 | register.add(:thing, thing)
32 | register.get('thing').should == thing
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dragonfly/routed_endpoint_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | def response_for(array)
4 | Rack::MockResponse.new(*array)
5 | end
6 |
7 | describe Dragonfly::RoutedEndpoint do
8 |
9 | def env_for(url, opts={})
10 | Rack::MockRequest.env_for(url, opts)
11 | end
12 |
13 | let (:app) { test_app }
14 | let (:uid) { app.store('wassup') }
15 |
16 | describe "endpoint returning a job" do
17 | let (:endpoint) {
18 | Dragonfly::RoutedEndpoint.new(app) {|params, app|
19 | app.fetch(params[:uid])
20 | }
21 | }
22 |
23 | it "should raise an error when there are no routing parameters" do
24 | lambda{
25 | endpoint.call(env_for('/blah'))
26 | }.should raise_error(Dragonfly::RoutedEndpoint::NoRoutingParams)
27 | end
28 |
29 | {
30 | 'Rails' => 'action_dispatch.request.path_parameters',
31 | 'HTTP Router' => 'router.params',
32 | 'Rack-Mount' => 'rack.routing_args',
33 | 'Dragonfly' => 'dragonfly.params'
34 | }.each do |name, key|
35 |
36 | it "should work with #{name} routing args" do
37 | response = response_for endpoint.call(env_for('/blah', key => {:uid => uid}))
38 | response.body.should == 'wassup'
39 | end
40 |
41 | end
42 |
43 | it "should merge with query parameters" do
44 | env = Rack::MockRequest.env_for("/big/buns?uid=#{uid}", 'dragonfly.params' => {:something => 'else'})
45 | response = response_for endpoint.call(env)
46 | response.body.should == 'wassup'
47 | end
48 |
49 | it "should have nice inspect output" do
50 | endpoint.inspect.should =~ //
51 | end
52 | end
53 |
54 | describe "env argument" do
55 | let (:endpoint) {
56 | Dragonfly::RoutedEndpoint.new(app) {|params, app, env|
57 | app.fetch(env['THE_UID'])
58 | }
59 | }
60 |
61 | it "adds the env to the arguments" do
62 | response = response_for endpoint.call(env_for('/blah', {"THE_UID" => uid, 'dragonfly.params' => {}}))
63 | response.body.should == 'wassup'
64 | end
65 | end
66 |
67 | describe "endpoint returning other things" do
68 | let (:model_class) {
69 | Class.new do
70 | extend Dragonfly::Model
71 | dragonfly_accessor :image
72 | attr_accessor :image_uid
73 | end
74 | }
75 | let (:model) {
76 | model_class.new
77 | }
78 | let (:endpoint) {
79 | Dragonfly::RoutedEndpoint.new(app) {|params, app|
80 | model.image
81 | }
82 | }
83 |
84 | it "acts like the job one" do
85 | model.image = "wassup"
86 | response = response_for endpoint.call(env_for('/blah', 'dragonfly.params' => {}))
87 | response.body.should == 'wassup'
88 | end
89 |
90 | it "returns 404 if nil is returned from the endpoint" do
91 | endpoint = Dragonfly::RoutedEndpoint.new(app) { nil }
92 | response = response_for endpoint.call(env_for('/blah', 'dragonfly.params' => {}))
93 | response.status.should == 404
94 | end
95 |
96 | it "returns 500 if something else is returned from the endpoint" do
97 | endpoint = Dragonfly::RoutedEndpoint.new(app) { "ASDF" }
98 | response = response_for endpoint.call(env_for('/blah', 'dragonfly.params' => {}))
99 | response.status.should == 500
100 | end
101 | end
102 |
103 | end
104 |
--------------------------------------------------------------------------------
/spec/dragonfly/serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | describe Dragonfly::Serializer do
5 |
6 | include Dragonfly::Serializer
7 |
8 | describe "base 64 encoding/decoding" do
9 | [
10 | 'a',
11 | 'sdhflasd',
12 | '/2010/03/01/hello.png',
13 | '//..',
14 | 'whats/up.egg.frog',
15 | '£ñçùí;',
16 | '~',
17 | '-umlaut_ö'
18 | ].each do |string|
19 | it "should encode #{string.inspect} properly with no padding/line break or slash" do
20 | b64_encode(string).should_not =~ /\n|=|\//
21 | end
22 | it "should correctly encode and decode #{string.inspect} to the same string" do
23 | str = b64_decode(b64_encode(string))
24 | str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
25 | str.should == string
26 | end
27 | end
28 |
29 | describe "b64_decode" do
30 | if RUBY_PLATFORM != 'java'
31 | # jruby doesn't seem to throw anything - it just removes non b64 characters
32 | it "raises an error if the string passed in is not base 64" do
33 | expect {
34 | b64_decode("eggs for breakfast")
35 | }.to raise_error(Dragonfly::Serializer::BadString)
36 | end
37 | end
38 | it "converts (deprecated) '~' and '/' characters to '_' characters" do
39 | b64_decode('LXVtbGF1dF~Dtg').should == b64_decode('LXVtbGF1dF_Dtg')
40 | b64_decode('LXVtbGF1dF/Dtg').should == b64_decode('LXVtbGF1dF_Dtg')
41 | end
42 | it "converts '+' characters to '-' characters" do
43 | b64_decode('LXVtbGF1dF+Dtg').should == b64_decode('LXVtbGF1dF-Dtg')
44 | end
45 | end
46 | end
47 |
48 | describe "marshal_b64_decode" do
49 | it "should raise an error if the string passed in is empty" do
50 | lambda{
51 | marshal_b64_decode('')
52 | }.should raise_error(Dragonfly::Serializer::BadString)
53 | end
54 | it "should raise an error if the string passed in is gobbledeegook" do
55 | lambda{
56 | marshal_b64_decode('ahasdkjfhasdkfjh')
57 | }.should raise_error(Dragonfly::Serializer::BadString)
58 | end
59 | describe "potentially harmful strings" do
60 | ['_', 'hello', 'h2', '__send__', 'F'].each do |variable_name|
61 | it "raises if it finds a malicious string" do
62 | class C; end
63 | c = C.new
64 | c.instance_eval{ instance_variable_set("@#{variable_name}", 1) }
65 | string = Dragonfly::Serializer.b64_encode(Marshal.dump(c))
66 | lambda{
67 | marshal_b64_decode(string)
68 | }.should raise_error(Dragonfly::Serializer::MaliciousString)
69 | end
70 | end
71 | end
72 | end
73 |
74 | [
75 | [3,4,5],
76 | {'wo' => 'there'},
77 | [{'this' => 'should', 'work' => [3, 5.3, nil, {'egg' => false}]}, [], true]
78 | ].each do |object|
79 | it "should correctly json encode #{object.inspect} properly with no padding/line break" do
80 | encoded = json_b64_encode(object)
81 | encoded.should be_a(String)
82 | encoded.should_not =~ /\n|=/
83 | end
84 |
85 | it "should correctly json encode and decode #{object.inspect} to the same object" do
86 | json_b64_decode(json_b64_encode(object)).should == object
87 | end
88 | end
89 |
90 | describe "json_b64_decode" do
91 | it "should raise an error if the string passed in is empty" do
92 | lambda{
93 | json_b64_decode('')
94 | }.should raise_error(Dragonfly::Serializer::BadString)
95 | end
96 | it "should raise an error if the string passed in is gobbledeegook" do
97 | lambda{
98 | json_b64_decode('ahasdkjfhasdkfjh')
99 | }.should raise_error(Dragonfly::Serializer::BadString)
100 | end
101 | end
102 |
103 | end
104 |
--------------------------------------------------------------------------------
/spec/dragonfly/shell_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Shell do
4 |
5 | let(:shell){ Dragonfly::Shell.new }
6 |
7 | it "returns the result of the command" do
8 | shell.run("echo 10").strip.should == '10'
9 | end
10 |
11 | it "should raise an error if the command isn't found" do
12 | lambda{
13 | shell.run "non-existent-command"
14 | }.should raise_error(Dragonfly::Shell::CommandFailed)
15 | end
16 |
17 | it "should raise an error if the command fails" do
18 | lambda{
19 | shell.run "ls -j"
20 | }.should raise_error(Dragonfly::Shell::CommandFailed)
21 | end
22 |
23 | unless Dragonfly.running_on_windows?
24 |
25 | # NOTE: every \\ translates to a single \ on the command line
26 | describe "escaping args" do
27 | {
28 | %q(hello there) => %q(hello there),
29 | %q('hello' 'there') => %q(hello there),
30 | %q(he\\'llo there) => %q(he\\'llo there),
31 | %q(he\\ llo there) => %q(he\\ llo there),
32 | %q("he'llo" there) => %q(he\\'llo there),
33 | %q('he'\\''llo' there) => %q(he\\'llo there),
34 | %q(hel$(lo) there) => %q(hel\\$\\(lo\\) there),
35 | %q(hel\\$(lo) > there) => %q(hel\\$\\(lo\\) \\> there),
36 | %q('hel$(lo) > there') => %q(hel\\$\\(lo\\)\\ \\>\\ there),
37 | %q(hello -there) => %q(hello -there),
38 | }.each do |args, escaped_args|
39 | it "should escape #{args} -> #{escaped_args}" do
40 | shell.escape_args(args).should == escaped_args
41 | end
42 | end
43 | end
44 |
45 | it "escapes commands by default" do
46 | shell.run("echo `echo 1`").strip.should == "`echo 1`"
47 | end
48 |
49 | it "allows running non-escaped commands" do
50 | shell.run("echo `echo 1`", :escape => false).strip.should == "1"
51 | end
52 |
53 | end
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/spec/dragonfly/url_attributes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::UrlAttributes do
4 |
5 | let(:url_attributes) { Dragonfly::UrlAttributes.new }
6 |
7 | describe "empty" do
8 | it "returns true when empty" do
9 | url_attributes.empty?.should be_truthy
10 | end
11 |
12 | it "returns false when not empty" do
13 | url_attributes.some = 'thing'
14 | url_attributes.empty?.should be_falsey
15 | end
16 |
17 | it "returns true if all values are nil" do
18 | url_attributes.some = nil
19 | url_attributes.empty?.should be_truthy
20 | end
21 | end
22 |
23 | describe "format" do
24 | # because 'format' is already private on kernel, using 'send' calls it so we need a workaround
25 | it "acts like other openstruct attributes when using 'send'" do
26 | url_attributes.send(:format).should be_nil
27 | url_attributes.format = "clive"
28 | url_attributes.send(:format).should == "clive"
29 | url_attributes.should_not be_empty
30 | end
31 | end
32 |
33 | describe "extract" do
34 | it "returns a hash for the given keys" do
35 | url_attributes.egg = 'boiled'
36 | url_attributes.veg = 'beef'
37 | url_attributes.lard = 'lean'
38 | url_attributes.extract(['egg', 'veg']).should == {'egg' => 'boiled', 'veg' => 'beef'}
39 | end
40 |
41 | it "excludes blank values" do
42 | url_attributes.egg = ''
43 | url_attributes.veg = nil
44 | url_attributes.extract(['egg', 'veg']).should == {}
45 | end
46 | end
47 |
48 | end
49 |
50 |
--------------------------------------------------------------------------------
/spec/dragonfly/url_mapper_spec.rb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require 'spec_helper'
3 |
4 | describe Dragonfly::UrlMapper do
5 |
6 | describe "validating the url format" do
7 | it "should be ok with a valid one" do
8 | Dragonfly::UrlMapper.new('/media/:job/:name')
9 | end
10 | it "should throw an error if params aren't separated" do
11 | lambda{
12 | Dragonfly::UrlMapper.new('/media/:job:name')
13 | }.should raise_error(Dragonfly::UrlMapper::BadUrlFormat)
14 | end
15 | end
16 |
17 | describe "params_in_url" do
18 | it "should return everything specified in the url" do
19 | url_mapper = Dragonfly::UrlMapper.new('/media/:job/:basename.:ext')
20 | url_mapper.params_in_url.should == ['job', 'basename', 'ext']
21 | end
22 | end
23 |
24 | describe "url_regexp" do
25 | it "should return a regexp with non-greedy optional groups that include the preceding slash/dot/dash" do
26 | url_mapper = Dragonfly::UrlMapper.new('/media/:job/:basename-:size.:format')
27 | url_mapper.url_regexp.should == %r{\A/media(/[^\/\.]+?)?(/[^\/]+?)?(\-[^\/\-\.]+?)?(\.[^\.]+?)?\z}
28 | end
29 | end
30 |
31 | describe "url_for" do
32 | before(:each) do
33 | @url_mapper = Dragonfly::UrlMapper.new('/media/:job-:size')
34 | end
35 |
36 | it "should map correctly" do
37 | @url_mapper.url_for('job' => 'asdf', 'size' => '30x30').should == '/media/asdf-30x30'
38 | end
39 |
40 | it "should add extra params as query parameters" do
41 | @url_mapper.url_for('job' => 'asdf', 'size' => '30x30', 'when' => 'now').should == '/media/asdf-30x30?when=now'
42 | end
43 |
44 | it "should not worry if params aren't given" do
45 | @url_mapper.url_for('job' => 'asdf', 'when' => 'now', 'then' => 'soon').should match_url '/media/asdf?when=now&then=soon'
46 | end
47 |
48 | it "should call to_s on non-string values" do
49 | @url_mapper.url_for('job' => 'asdf', 'size' => 500).should == '/media/asdf-500'
50 | end
51 |
52 | it "should url-escape funny characters in the path" do
53 | @url_mapper.url_for('job' => 'a#c').should == '/media/a%23c'
54 | end
55 |
56 | it "should url-escape funny characters in the query string" do
57 | @url_mapper.url_for('bigjobs' => 'a#c').should == '/media?bigjobs=a%23c'
58 | end
59 |
60 | it "should url-escape the / character in a single segment" do
61 | @url_mapper.url_for('job' => 'a/c').should == '/media/a%2Fc'
62 | end
63 |
64 | end
65 |
66 | describe "params_for" do
67 | before(:each) do
68 | @url_mapper = Dragonfly::UrlMapper.new('/media/:job')
69 | end
70 |
71 | it "should map correctly" do
72 | @url_mapper.params_for('/media/asdf').should == {'job' => 'asdf'}
73 | end
74 |
75 | it "should include query parameters" do
76 | @url_mapper.params_for('/media/asdf', 'when=now').should == {'job' => 'asdf', 'when' => 'now'}
77 | end
78 |
79 | it "should generally be ok with wierd characters" do
80 | @url_mapper = Dragonfly::UrlMapper.new('/media/:doobie')
81 | @url_mapper.params_for('/media/sd sdf jl£@$ sdf:_', 'job=goodun').should == {'job' => 'goodun', 'doobie' => 'sd sdf jl£@$ sdf:_'}
82 | end
83 |
84 | it "should correctly url-unescape funny characters" do
85 | @url_mapper.params_for('/media/a%23c').should == {'job' => 'a#c'}
86 | end
87 |
88 | it "should work when the job contains the '-' character" do
89 | @url_mapper = Dragonfly::UrlMapper.new('/media/:job-:size.:format')
90 | @url_mapper.params_for('/media/asdf-30x30.jpg').should == {'job' => 'asdf', 'size' => '30x30', 'format' => 'jpg'}
91 | @url_mapper.params_for('/media/as-df-30x30.jpg').should == {'job' => 'as-df', 'size' => '30x30', 'format' => 'jpg'}
92 | end
93 | end
94 |
95 | describe "matching urls with standard format /media/:job/:name" do
96 | before(:each) do
97 | @url_mapper = Dragonfly::UrlMapper.new('/media/:job/:name')
98 | end
99 |
100 | {
101 | '' => nil,
102 | '/' => nil,
103 | '/media' => {'job' => nil, 'name' => nil},
104 | '/media/' => nil,
105 | '/moodia/asdf' => nil,
106 | '/media/asdf/' => nil,
107 | '/mount/media/asdf' => nil,
108 | '/media/asdf/stuff.egg' => {'job' => 'asdf', 'name' => 'stuff.egg'},
109 | '/media/asdf' => {'job' => 'asdf', 'name' => nil},
110 | '/media/asdf/stuff' => {'job' => 'asdf', 'name' => 'stuff'},
111 | '/media/asdf.egg' => {'job' => nil, 'name' => 'asdf.egg'},
112 | '/media/asdf/stuff/egg' => nil,
113 | '/media/asdf/stuff.dog.egg' => {'job' => 'asdf', 'name' => 'stuff.dog.egg'},
114 | '/media/asdf/s%3D2%2B-.d.e' => {'job' => 'asdf', 'name' => 's=2+-.d.e'},
115 | '/media/asdf-40x40/stuff.egg' => {'job' => 'asdf-40x40', 'name' => 'stuff.egg'},
116 | '/media/a%23c' => {'job' => 'a#c', 'name' => nil}
117 | }.each do |path, params|
118 |
119 | it "should turn the url #{path} into params #{params.inspect}" do
120 | @url_mapper.params_for(path).should == params
121 | end
122 |
123 | if params
124 | it "should turn the params #{params.inspect} into url #{path}" do
125 | @url_mapper.url_for(params).should == path
126 | end
127 | end
128 |
129 | end
130 |
131 | end
132 |
133 | end
134 |
--------------------------------------------------------------------------------
/spec/dragonfly/utils_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Utils do
4 |
5 | describe "blank?" do
6 | [
7 | nil,
8 | false,
9 | "",
10 | [],
11 | {}
12 | ].each do |obj|
13 | it "returns true for #{obj.inspect}" do
14 | obj.blank?.should be_truthy
15 | end
16 | end
17 |
18 | [
19 | "a",
20 | [1],
21 | {1 => 2},
22 | Object.new,
23 | true,
24 | 7.3
25 | ].each do |obj|
26 | it "returns false for #{obj.inspect}" do
27 | obj.blank?.should be_falsey
28 | end
29 | end
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/spec/dragonfly/whitelist_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Dragonfly::Whitelist do
4 | it "matches regexps" do
5 | whitelist = Dragonfly::Whitelist.new([/platipus/])
6 | whitelist.include?("platipus").should be_truthy
7 | whitelist.include?("small platipus in the bath").should be_truthy
8 | whitelist.include?("baloney").should be_falsey
9 | end
10 |
11 | it "matches strings" do
12 | whitelist = Dragonfly::Whitelist.new(["platipus"])
13 | whitelist.include?("platipus").should be_truthy
14 | whitelist.include?("small platipus in the bath").should be_falsey
15 | whitelist.include?("baloney").should be_falsey
16 | end
17 |
18 | it "only needs one match" do
19 | Dragonfly::Whitelist.new(%w(a b)).include?("c").should be_falsey
20 | Dragonfly::Whitelist.new(%w(a b c)).include?("c").should be_truthy
21 | end
22 |
23 | it "allows pushing" do
24 | whitelist = Dragonfly::Whitelist.new(["platipus"])
25 | whitelist.push("duck")
26 | whitelist.should include "platipus"
27 | whitelist.should include "duck"
28 | end
29 | end
30 |
31 |
--------------------------------------------------------------------------------
/spec/dragonfly_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'logger'
3 | require 'stringio'
4 |
5 | describe Dragonfly do
6 | it "returns a default app" do
7 | Dragonfly.app.should == Dragonfly::App.instance
8 | end
9 |
10 | it "returns a named app" do
11 | Dragonfly.app(:mine).should == Dragonfly::App.instance(:mine)
12 | end
13 |
14 | describe "logging" do
15 | context "logger exists" do
16 | before do
17 | Dragonfly.logger = Logger.new(StringIO.new)
18 | end
19 |
20 | it "debugs" do
21 | Dragonfly.logger.should_receive(:debug).with(/something/)
22 | Dragonfly.debug("something")
23 | end
24 |
25 | it "warns" do
26 | Dragonfly.logger.should_receive(:warn).with(/something/)
27 | Dragonfly.warn("something")
28 | end
29 |
30 | it "shows info" do
31 | Dragonfly.logger.should_receive(:info).with(/something/)
32 | Dragonfly.info("something")
33 | end
34 | end
35 |
36 | context "logger is nil" do
37 | before do
38 | allow_message_expectations_on_nil
39 | Dragonfly.logger = nil
40 | end
41 |
42 | it "does not call debug" do
43 | Dragonfly.logger.should_not_receive(:debug)
44 | Dragonfly.debug("something")
45 | end
46 |
47 | it "does not warn" do
48 | Dragonfly.logger.should_not_receive(:warn)
49 | Dragonfly.warn("something")
50 | end
51 |
52 | it "does not show info" do
53 | Dragonfly.logger.should_not_receive(:info)
54 | Dragonfly.info("something")
55 | end
56 | end
57 | end
58 |
59 | describe "deprecations" do
60 | it "raises a message when using Dragonfly[:name]" do
61 | expect {
62 | Dragonfly[:images]
63 | }.to raise_error(/deprecated/)
64 | end
65 | end
66 | end
67 |
68 |
--------------------------------------------------------------------------------
/spec/fixtures/deprecated_stored_content/eggs.bonus:
--------------------------------------------------------------------------------
1 | Barnicle
--------------------------------------------------------------------------------
/spec/fixtures/deprecated_stored_content/eggs.bonus.meta:
--------------------------------------------------------------------------------
1 | {: nameI"eggs.bonus:ET: someI" meta;T:numberi
2 |
--------------------------------------------------------------------------------
/spec/functional/cleanup_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "cleaning up tempfiles" do
4 |
5 | let (:app) {
6 | test_app.configure{
7 | processor :copy do |content|
8 | content.shell_update do |old_path, new_path|
9 | "cp #{old_path} #{new_path}"
10 | end
11 | end
12 | }
13 | }
14 |
15 | it "unlinks tempfiles on each request" do
16 | expect {
17 | uid = app.store("blug")
18 | url = app.fetch(uid).copy.url
19 | request(app, url)
20 | }.not_to increase_num_tempfiles
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/functional/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "configuration" do
4 | let (:app) { test_app }
5 |
6 | it "adds to fetch_file_whitelist" do
7 | app.configure do
8 | fetch_file_whitelist ['something']
9 | end
10 | app.fetch_file_whitelist.should include 'something'
11 | end
12 |
13 | it "adds to fetch_url_whitelist" do
14 | app.configure do
15 | fetch_url_whitelist ['http://something']
16 | end
17 | app.fetch_url_whitelist.should include 'http://something'
18 | end
19 |
20 | describe "deprecations" do
21 | it "protect_from_dos_attacks" do
22 | Dragonfly.should_receive(:warn).with(/deprecated/)
23 | expect {
24 | app.configure do
25 | protect_from_dos_attacks false
26 | end
27 | }.to change(app.server, :verify_urls)
28 | end
29 | end
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/spec/functional/model_urls_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'tempfile'
3 |
4 | describe "model urls" do
5 |
6 | def new_tempfile(data='HELLO', filename='hello.txt')
7 | tempfile = Tempfile.new('test')
8 | tempfile.write(data)
9 | tempfile.rewind
10 | tempfile.stub(:original_filename).and_return(filename)
11 | tempfile
12 | end
13 |
14 | before(:each) do
15 | @app = test_app.configure do
16 | plugin :imagemagick
17 | url_format '/media/:job/:name'
18 | analyser :some_analyser_method do |t|
19 | 53
20 | end
21 | end
22 | @item_class = new_model_class('Item',
23 | :preview_image_uid,
24 | :preview_image_name,
25 | :preview_image_some_analyser_method,
26 | :other_image_uid
27 | ) do
28 | dragonfly_accessor :preview_image # has name, some_analyser_method, etc.
29 | dragonfly_accessor :other_image # doesn't have magic stuff
30 | end
31 | @item = @item_class.new
32 | end
33 |
34 | it "should include the name in the url if it has the magic attribute" do
35 | @item.preview_image = new_tempfile
36 | @item.save!
37 | @item.preview_image.url.should =~ %r{^/media/\w+/hello\.txt}
38 | end
39 |
40 | it "should still include the name in the url if it has the magic attribute on reload" do
41 | @item.preview_image = new_tempfile
42 | @item.save!
43 | item = @item_class.find(@item.id)
44 | item.preview_image.url.should =~ %r{^/media/\w+/hello\.txt}
45 | end
46 |
47 | it "should work for other magic attributes in the url" do
48 | @app.server.url_format = '/:job/:some_analyser_method'
49 | @item.preview_image = new_tempfile
50 | @item.save!
51 | @item.preview_image.url.should =~ %r{^/\w+/53}
52 | @item_class.find(@item.id).preview_image.url.should =~ %r{^/\w+/53}
53 | end
54 |
55 | it "should work without the name if the name magic attr doesn't exist" do
56 | @item.other_image = new_tempfile
57 | @item.save!
58 | item = @item_class.find(@item.id)
59 | item.other_image.url.should =~ %r{^/media/\w+}
60 | end
61 |
62 | it "should not add the name when there's no magic attr, even if the name is set (for consistency)" do
63 | @item.other_image = new_tempfile
64 | @item.save!
65 | @item.other_image.name = 'test.txt'
66 | @item.other_image.url.should =~ %r{^/media/\w+}
67 | end
68 |
69 | it "should include the name in the url even if it has no ext" do
70 | @item.preview_image = new_tempfile("hello", 'hello')
71 | @item.save!
72 | item = @item_class.find(@item.id)
73 | item.preview_image.url.should =~ %r{^/media/\w+/hello}
74 | end
75 |
76 | it "should change the ext when there's an encoding step" do
77 | @item.preview_image = new_tempfile
78 | @item.save!
79 | item = @item_class.find(@item.id)
80 | item.preview_image.encode(:bum).url.should =~ %r{^/media/\w+/hello\.bum}
81 | end
82 |
83 | it "should not include the name if it has none" do
84 | @item.preview_image = "HELLO"
85 | @item.save!
86 | item = @item_class.find(@item.id)
87 | item.preview_image.url.should =~ %r{^/media/\w+}
88 | end
89 |
90 | it "should have an ext when there's an encoding step but no name" do
91 | @item.preview_image = "HELLO"
92 | @item.save!
93 | item = @item_class.find(@item.id)
94 | item.preview_image.encode(:bum).url.should =~ %r{^/media/\w+/file\.bum}
95 | end
96 |
97 | it "should allow configuring the url" do
98 | @app.configure do
99 | url_format '/img/:job'
100 | end
101 | @item.preview_image = new_tempfile
102 | @item.save!
103 | item = @item_class.find(@item.id)
104 | item.preview_image.url.should =~ %r{^/img/\w+}
105 | end
106 |
107 | it "should still get params from magic attributes even when chained" do
108 | @item.preview_image = new_tempfile
109 | @item.save!
110 | item = @item_class.find(@item.id)
111 | item.preview_image.thumb('30x30').url.should =~ %r{^/media/\w+/hello\.txt}
112 | end
113 |
114 | end
115 |
--------------------------------------------------------------------------------
/spec/functional/remote_on_the_fly_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "remote on-the-fly urls" do
4 |
5 | before(:each) do
6 | @thumbs = thumbs = {}
7 | @app = test_app.configure do
8 | generator :test do |content|
9 | content.update("TEST")
10 | end
11 | before_serve do |job, env|
12 | uid = job.store(:path => 'yay.txt')
13 | thumbs[job.serialize] = uid
14 | end
15 | define_url do |app, job, opts|
16 | uid = thumbs[job.serialize]
17 | if uid
18 | app.datastore.url_for(uid)
19 | else
20 | app.server.url_for(job)
21 | end
22 | end
23 | datastore :file,
24 | :root_path => 'tmp/dragonfly_test_urls',
25 | :server_root => 'tmp'
26 | end
27 | @job = @app.generate(:test)
28 | end
29 |
30 | after(:each) do
31 | FileUtils.rm_f('tmp/dragonfly_test_urls/yay.txt')
32 | end
33 |
34 | it "should give the url for the server" do
35 | @job.url.should == "/#{@job.serialize}?sha=#{@job.sha}"
36 | end
37 |
38 | it "should store the content when first called" do
39 | File.exist?('tmp/dragonfly_test_urls/yay.txt').should be_falsey
40 | request(@app, @job.url)
41 | File.read('tmp/dragonfly_test_urls/yay.txt').should == 'TEST'
42 | end
43 |
44 | it "should point to the external url the second time" do
45 | request(@app, @job.url)
46 | @job.url.should == '/dragonfly_test_urls/yay.txt'
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/spec/functional/shell_commands_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe "using the shell" do
4 | let (:app) { test_app }
5 |
6 | describe "shell injection" do
7 | it "should not allow it!" do
8 | app.configure_with(:imagemagick)
9 | begin
10 | app.generate(:plain, 10, 10, "white; touch tmp/stuff").apply
11 | rescue Dragonfly::Shell::CommandFailed
12 | end
13 | File.exist?("tmp/stuff").should be_falsey
14 | end
15 | end
16 |
17 | describe "env variables with imagemagick" do
18 | it "allows configuring the convert path" do
19 | app.configure_with(:imagemagick, :convert_command => "/bin/convert")
20 | app.shell.should_receive(:run).with(%r[/bin/convert], hash_including)
21 | app.create("").thumb("30x30").apply
22 | end
23 |
24 | it "allows configuring the identify path" do
25 | app.configure_with(:imagemagick, :identify_command => "/bin/identify")
26 | app.shell.should_receive(:run).with(%r[/bin/identify], hash_including).and_return("JPG 1 1")
27 | app.create("").width
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/functional/to_response_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "getting rack response directly" do
4 |
5 | before(:each) do
6 | @app = test_app.configure do
7 | generator :test do |content|
8 | content.update("bunheads")
9 | end
10 | end
11 | end
12 |
13 | it "should give a rack response" do
14 | response = @app.generate(:test).to_response
15 | response.should be_a(Array)
16 | response.length.should == 3
17 | response[0].should == 200
18 | response[1]['content-type'].should == 'application/octet-stream'
19 | response[2].data.should == 'bunheads'
20 | end
21 |
22 | it "should allow passing in the env" do
23 | response = @app.generate(:test).to_response('REQUEST_METHOD' => 'POST')
24 | response.should be_a(Array)
25 | response.length.should == 3
26 | response[0].should == 405
27 | response[1]['content-type'].should == 'text/plain'
28 | response[2].should == ["method not allowed"]
29 | end
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/spec/functional/urls_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "urls" do
4 |
5 | def job_should_match(array)
6 | Dragonfly::Response.should_receive(:new).with(
7 | satisfy{|job| job.to_a == array },
8 | instance_of(Hash)
9 | ).and_return(double('response', :to_response => [200, {'content-type' => 'text/plain'}, ["OK"]]))
10 | end
11 |
12 | let (:app) {
13 | test_app.configure{
14 | processor(:thumb){}
15 | verify_urls false
16 | }
17 | }
18 |
19 | it "works with old marshalled urls (including with tildes in them)" do
20 | app.allow_legacy_urls = true
21 | url = "/BAhbBlsHOgZmSSIIPD4~BjoGRVQ"
22 | job_should_match [["f", "<>?"]]
23 | response = request(app, url)
24 | end
25 |
26 | it "blows up if it detects bad objects" do
27 | app.allow_legacy_urls = true
28 | url = "/BAhvOhpEcmFnb25mbHk6OlRlbXBPYmplY3QIOgpAZGF0YUkiCWJsYWgGOgZFVDoXQG9yaWdpbmFsX2ZpbGVuYW1lMDoKQG1ldGF7AA"
29 | Dragonfly::Job.should_not_receive(:from_a)
30 | response = request(app, url)
31 | response.status.should == 404
32 | end
33 |
34 | it "works with the '%2B' character" do
35 | url = "/W1siZiIsIjIwMTIvMTEvMDMvMTdfMzhfMDhfNTc4X19NR181ODk5Xy5qcGciXSxbInAiLCJ0aHVtYiIsIjQ1MHg0NTA%2BIl1d/_MG_5899+.jpg"
36 | job_should_match [["f", "2012/11/03/17_38_08_578__MG_5899_.jpg"], ["p", "thumb", "450x450>"]]
37 | response = request(app, url)
38 | end
39 |
40 | it "works when '%2B' has been converted to + (e.g. with nginx)" do
41 | url = "/W1siZiIsIjIwMTIvMTEvMDMvMTdfMzhfMDhfNTc4X19NR181ODk5Xy5qcGciXSxbInAiLCJ0aHVtYiIsIjQ1MHg0NTA+Il1d/_MG_5899+.jpg"
42 | job_should_match [["f", "2012/11/03/17_38_08_578__MG_5899_.jpg"], ["p", "thumb", "450x450>"]]
43 | response = request(app, url)
44 | end
45 |
46 | it "works with potentially tricky url characters for the url" do
47 | url = app.fetch('uid []=~/+').url(:name => 'name []=~/+')
48 | url.should =~ %r(^/[\w-]+/name%20%5B%5D%3D%7E%2F%2B$)
49 | job_should_match [["f", "uid []=~/+"]]
50 | response = request(app, url)
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "rubygems"
2 | require "bundler"
3 | Bundler.setup(:default, :test)
4 |
5 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
6 | $LOAD_PATH.unshift(File.dirname(__FILE__))
7 | require "rspec"
8 | require "dragonfly"
9 | require "fileutils"
10 | require "tempfile"
11 | require "webmock/rspec"
12 | require "pry"
13 |
14 | # Requires supporting files with custom matchers and macros, etc,
15 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
16 |
17 | SAMPLES_DIR = Pathname.new(File.expand_path("../../samples", __FILE__))
18 |
19 | RSpec.configure do |c|
20 | c.expect_with(:rspec) do |expectations|
21 | expectations.syntax = [:should, :expect]
22 | end
23 | c.mock_with(:rspec) do |mocks|
24 | mocks.syntax = [:should, :expect]
25 | end
26 | c.include ModelHelpers
27 | c.include RackHelpers
28 | end
29 |
30 | def todo
31 | raise "TODO"
32 | end
33 |
34 | require "logger"
35 | LOG_FILE = "tmp/test.log"
36 | FileUtils.rm_rf(LOG_FILE)
37 | Dragonfly.logger = Logger.new(LOG_FILE)
38 |
39 | RSpec.configure do |c|
40 | c.after(:each) do
41 | Dragonfly::App.destroy_apps
42 | end
43 | end
44 |
45 | def test_app(name = nil)
46 | app = Dragonfly::App.instance(name)
47 | app.datastore = Dragonfly::MemoryDataStore.new
48 | app.secret = "test secret"
49 | app
50 | end
51 |
52 | def test_imagemagick_app
53 | test_app.configure do
54 | analyser :image_properties, Dragonfly::ImageMagick::Analysers::ImageProperties.new
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/support/argument_matchers.rb:
--------------------------------------------------------------------------------
1 | def string_matching(regexp)
2 | Spec::Mocks::ArgumentMatchers::RegexpMatcher.new(regexp)
3 | end
4 |
5 | class ContentArgumentMatcher
6 | def initialize(data)
7 | @data = data
8 | end
9 | def ==(actual)
10 | actual.is_a?(Dragonfly::Content) &&
11 | actual.data == @data
12 | end
13 | end
14 |
15 | def content_with_data(data)
16 | ContentArgumentMatcher.new(data)
17 | end
18 |
19 |
--------------------------------------------------------------------------------
/spec/support/image_matchers.rb:
--------------------------------------------------------------------------------
1 | def image_properties(image)
2 | details = `identify #{image.path}`
3 | raise "couldn't identify #{image.path} in image_properties" if details.empty?
4 | # example of details string:
5 | # myimage.png PNG 200x100 200x100+0+0 8-bit DirectClass 31.2kb
6 | filename, format, geometry, geometry_2, depth, image_class, size = details.split(' ')
7 | width, height = geometry.split('x')
8 | {
9 | :filename => filename,
10 | :format => format.downcase,
11 | :width => width.to_i,
12 | :height => height.to_i,
13 | :depth => depth,
14 | :image_class => image_class,
15 | :size => size.to_i
16 | }
17 | end
18 |
19 | [:width, :height, :format, :size].each do |property|
20 |
21 | RSpec::Matchers.define "have_#{property}" do |value|
22 | match do |actual|
23 | value.should === image_properties(actual)[property]
24 | end
25 | failure_message do |actual|
26 | "expected image to have #{property} #{value.inspect}, but it had #{image_properties(actual)[property].inspect}"
27 | end
28 | end
29 |
30 | end
31 |
32 | RSpec::Matchers.define :equal_image do |other|
33 | match do |given|
34 | given.data == other.data
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/spec/support/model_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_model'
2 | require 'dragonfly/model/validations'
3 |
4 | # Generic activemodel model
5 | class MyModel
6 |
7 | extend Dragonfly::Model
8 |
9 | # Callbacks
10 | extend ActiveModel::Callbacks
11 | define_model_callbacks :save, :destroy
12 |
13 | include ActiveModel::Dirty
14 |
15 | class << self
16 | attr_writer :attribute_names
17 | def attribute_names
18 | @attribute_names ||= (superclass.attribute_names if superclass.respond_to?(:attribute_names))
19 | end
20 |
21 | attr_accessor :name
22 |
23 | def create!(attrs={})
24 | new(attrs).save!
25 | end
26 |
27 | def find(id)
28 | new(instances[id])
29 | end
30 |
31 | def instances
32 | @instances ||= {}
33 | end
34 | end
35 |
36 | def initialize(attrs={})
37 | attrs.each do |key, value|
38 | send("#{key}=", value)
39 | end
40 | end
41 |
42 | attr_accessor :id
43 |
44 | def to_hash
45 | self.class.attribute_names.inject({}) do |hash, attr|
46 | hash[attr] = send(attr)
47 | hash
48 | end
49 | end
50 |
51 | def save
52 | run_save_callbacks {
53 | self.id ||= rand(1000)
54 | self.class.instances[id] = self.to_hash
55 | }
56 | end
57 | def save!
58 | save
59 | self
60 | end
61 |
62 | def destroy
63 | run_destroy_callbacks {}
64 | end
65 |
66 | private
67 |
68 | def run_save_callbacks(&block)
69 | if respond_to?(:run_callbacks) # Rails 4
70 | run_callbacks :save, &block
71 | else
72 | _run_save_callbacks(&block)
73 | end
74 | end
75 |
76 | def run_destroy_callbacks(&block)
77 | if respond_to?(:run_callbacks) # Rails 4
78 | run_callbacks :destroy, &block
79 | else
80 | _run_destroy_callbacks(&block)
81 | end
82 | end
83 | end
84 |
85 | module ModelHelpers
86 | def new_model_class(name="TestModel", *attribute_names, &block)
87 | klass = Class.new(MyModel) do
88 | self.name = name
89 | include ActiveModel::Validations # Doing this here because it needs 'name' to be set
90 | self.attribute_names = attribute_names
91 | define_attribute_methods attribute_names
92 | attr_accessor *attribute_names
93 | end
94 | klass.class_eval(&block) if block
95 | klass
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/spec/support/rack_helpers.rb:
--------------------------------------------------------------------------------
1 | module RackHelpers
2 |
3 | def request(app, path)
4 | Rack::MockRequest.new(app).get(path)
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/simple_matchers.rb:
--------------------------------------------------------------------------------
1 | RSpec::Matchers.define :match_url do |url|
2 | match do |given|
3 | given_path, given_query_string = given.split('?')
4 | path, query_string = url.split('?')
5 |
6 | path == given_path && given_query_string.split('&').sort == query_string.split('&').sort
7 | end
8 | end
9 |
10 | RSpec::Matchers.define :be_an_empty_directory do
11 | match do |given|
12 | !!ENV['TRAVIS'] || (Dir.entries(given).sort == ['.','..'].sort)
13 | end
14 | end
15 |
16 | RSpec::Matchers.define :include_hash do |hash|
17 | match do |given|
18 | given.merge(hash) == given
19 | end
20 | end
21 |
22 | RSpec::Matchers.define :match_attachment_classes do |classes|
23 | match do |given_classes|
24 | given_classes.length == classes.length &&
25 | classes.zip(given_classes).all? do |(klass, given)|
26 | given.model_class == klass[0] && given.attribute == klass[1] && given.app == klass[2]
27 | end
28 | end
29 | end
30 |
31 | RSpec::Matchers.define :be_a_text_response do
32 | match do |given_response|
33 | given_response.status.should == 200
34 | given_response.body.length.should > 0
35 | given_response.content_type.should == 'text/plain'
36 | end
37 | end
38 |
39 | RSpec::Matchers.define :have_keys do |*keys|
40 | match do |given|
41 | given.keys.map{|sym| sym.to_s }.sort == keys.map{|sym| sym.to_s }.sort
42 | end
43 | end
44 |
45 | RSpec::Matchers.define :match_steps do |steps|
46 | match do |given|
47 | given.map{|step| step.class } == steps
48 | end
49 | end
50 |
51 | RSpec::Matchers.define :increase_num_tempfiles do
52 | match do |block|
53 | num_tempfiles_before = Dir.entries(Dir.tmpdir).size
54 | block.call
55 | num_tempfiles_after = Dir.entries(Dir.tmpdir).size
56 | increased = num_tempfiles_after > num_tempfiles_before
57 | puts "Num tempfiles increased: #{num_tempfiles_before} -> #{num_tempfiles_after}" if increased
58 | increased
59 | end
60 |
61 | def supports_block_expectations?
62 | true
63 | end
64 | end
65 |
66 | RSpec::Matchers.define :call_command do |shell, command|
67 | match do |block|
68 | # run_command is private so this is slightly naughty but it does the job
69 | allow(shell).to receive(:run_command).and_call_original
70 | block.call
71 | expect(shell).to have_received(:run_command).with(command)
72 | end
73 |
74 | def supports_block_expectations?
75 | true
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/tmp/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------