├── .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 | [![Build Status](https://travis-ci.org/markevans/dragonfly.svg?branch=master)](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 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
Original (#{image.width}x#{image.height})
image.
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 | --------------------------------------------------------------------------------