├── .gitignore ├── .travis.yml ├── Appraisals ├── CONTRIBUTING ├── ChangeLog ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── delayed_paperclip.gemspec ├── gemfiles ├── .bundle │ └── config ├── 4.2.gemfile ├── 5.0.gemfile └── 5.0_paperclip_master.gemfile ├── lib ├── delayed_paperclip.rb └── delayed_paperclip │ ├── attachment.rb │ ├── process_job.rb │ ├── railtie.rb │ ├── url_generator.rb │ └── version.rb └── spec ├── delayed_paperclip ├── attachment_spec.rb ├── class_methods_spec.rb ├── instance_methods_spec.rb └── url_generator_spec.rb ├── delayed_paperclip_spec.rb ├── fixtures ├── 12k.png └── missing.png ├── integration ├── base_delayed_paperclip_spec.rb ├── examples │ └── base.rb └── process_job_spec.rb ├── spec_helper.rb └── tmp └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | pkg/ 4 | public/ 5 | Gemfile.lock 6 | *.gem 7 | gemfiles/*.lock 8 | .ruby-gemset 9 | .ruby-version 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | sudo: false 4 | 5 | rvm: 6 | - 2.3.1 7 | - 2.2.5 8 | - 2.1.10 9 | - 2.0 10 | 11 | gemfile: 12 | - gemfiles/4.2.gemfile 13 | - gemfiles/5.0.gemfile 14 | - gemfiles/5.0_paperclip_master.gemfile 15 | 16 | # Rails 5.0 requires Ruby >= 2.2.2 17 | matrix: 18 | exclude: 19 | - rvm: 2.1.10 20 | gemfile: gemfiles/5.0.gemfile 21 | - rvm: 2.0 22 | gemfile: gemfiles/5.0.gemfile 23 | - rvm: 2.1.10 24 | gemfile: gemfiles/5.0_paperclip_master.gemfile 25 | - rvm: 2.0 26 | gemfile: gemfiles/5.0_paperclip_master.gemfile 27 | # Paperclip >= 5.0 requires Ruby 2.1 28 | - rvm: 2.0 29 | gemfile: gemfiles/4.2.gemfile 30 | 31 | script: "bundle exec rake clean spec" 32 | 33 | notifications: 34 | email: 35 | - james@jamesrgifford.com 36 | - scott@artsicle.com 37 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "4.2" do 2 | gem "rails", "~> 4.2.0" 3 | end 4 | 5 | appraise "5.0" do 6 | gem "rails", "~> 5.0.0" 7 | end 8 | 9 | appraise "5.0-paperclip-master" do 10 | gem "rails", "~> 5.0.0" 11 | gem "paperclip", github: "thoughtbot/paperclip" 12 | end 13 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributor Policy 2 | ================= 3 | 4 | Commit bit. If you have a commit accepted into the project then you get full git access to the repo. If I don't give you this in a timely manner just send me a message. 5 | 6 | Testing 7 | ====== 8 | 9 | Please don't commit code without tests. You can bootstrap the development environment by running `bundle install`. After that, running `rake` should just work. If it doesn't then file a bug. 10 | 11 | Versioning 12 | ========= 13 | 14 | Don't bump the version in any changes you make or pull in to the project. I'll maintain rights to push the gem to rubygems.org and make releases when appropriate. 15 | 16 | And please keep the README up to date. Thank you! 17 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | -> https://github.com/jrgifford/delayed_paperclip/releases 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Jesse Storimer 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 PURPOa AND 17 | NONINFRINGEMENT. IN NO EVENT SaALL 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Delayed::Paperclip [![Build Status](https://travis-ci.org/jrgifford/delayed_paperclip.svg?branch=master)](https://travis-ci.org/jrgifford/delayed_paperclip) [![Code Climate](https://codeclimate.com/github/jrgifford/delayed_paperclip.png)](https://codeclimate.com/github/jrgifford/delayed_paperclip) 2 | ====================================================================================== 3 | 4 | 5 | DelayedPaperclip lets you process your [Paperclip](http://github.com/thoughtbot/paperclip) 6 | attachments in a background task with [ActiveJob](https://github.com/rails/rails/tree/master/activejob) 7 | 8 | Why? 9 | ---- 10 | 11 | The most common use case for Paperclip is to easily attach image files 12 | to ActiveRecord models. Most of the time these image files will have 13 | multiple styles and will need to be resized when they are created. This 14 | is usually a pretty slow operation and should be handled in a background task. 15 | 16 | I’m sure that everyone knows this, this gem just makes it easy to do. 17 | 18 | Installation 19 | ------------ 20 | 21 | Install the gem: 22 | 23 | ``` 24 | gem install delayed_paperclip 25 | ``` 26 | 27 | Or even better, add it to your Gemfile. 28 | 29 | ``` 30 | source "https://rubygems.org" 31 | gem "delayed_paperclip" 32 | ``` 33 | 34 | Usage 35 | ----- 36 | 37 | In your model: 38 | 39 | ```ruby 40 | class User < ActiveRecord::Base 41 | has_attached_file :avatar, styles: { 42 | medium: "300x300>", 43 | thumb: "100x100>" 44 | } 45 | 46 | process_in_background :avatar 47 | end 48 | ``` 49 | 50 | Use your Paperclip attachment just like always in controllers and views. 51 | 52 | ### Displaying images during processing 53 | 54 | In the default setup, when you upload an image for the first time and 55 | try to display it before the job has been completed, Paperclip will be 56 | none the wiser and output the url of the image which is yet to be 57 | processed, which will result in a broken image link being displayed on 58 | the page. 59 | 60 | To have the missing image url be outputted by paperclip while the image is being processed, all you need to do is add a 61 | `#{attachment_name}_processing` column to the specific model you want 62 | to enable this feature for. This feature gracefully degrades and will not affect models which do not have the column added to them. 63 | 64 | ```ruby 65 | class AddAvatarProcessingToUser < ActiveRecord::Migration 66 | def self.up 67 | add_column :users, :avatar_processing, :boolean 68 | end 69 | 70 | def self.down 71 | remove_column :users, :avatar_processing 72 | end 73 | end 74 | 75 | @user = User.new(avatar: File.new(...)) 76 | @user.save 77 | @user.avatar.url #=> "/images/original/missing.png" 78 | 79 | # Process job 80 | 81 | @user.reload 82 | @user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148" 83 | ``` 84 | 85 | #### Custom image for processing 86 | 87 | This is useful if you have a difference between missing images and 88 | images currently being processed. 89 | 90 | ```ruby 91 | class User < ActiveRecord::Base 92 | has_attached_file :avatar 93 | 94 | process_in_background :avatar, processing_image_url: "/images/:style/processing.jpg" 95 | end 96 | 97 | @user = User.new(avatar: File.new(...)) 98 | @user.save 99 | @user.avatar.url #=> "/images/original/processing.png" 100 | 101 | # Process job 102 | 103 | @user.reload 104 | @user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148" 105 | ``` 106 | 107 | You can also define a custom logic for `processing_image_url`, for 108 | example to display the original picture while specific formats are being processed. 109 | 110 | ```ruby 111 | class Item < ActiveRecord::Base 112 | has_attached_file :photo 113 | 114 | process_in_background :photo, processing_image_url: :processing_image_fallback 115 | 116 | def processing_image_fallback 117 | options = photo.options 118 | options[:interpolator].interpolate(options[:url], photo, :original) 119 | end 120 | end 121 | ``` 122 | 123 | Another option is to provide an object which responds to `call` to `processing_image_url` and returns the image url. The method will be called with the attachment as the argument. 124 | 125 | ```ruby 126 | class Item < ActiveRecord::Base 127 | has_attached_file :photo 128 | 129 | process_in_background :photo, processing_image_url: ->(attachment) { 130 | ActionController::Base.helpers.image_path("processing.gif") 131 | } 132 | end 133 | ``` 134 | 135 | #### Have processing? status available, but construct image URLs as if delayed_paperclip wasn’t present 136 | 137 | If you define the `#{attachment_name}_processing` column, but set the 138 | `url_with_processing` option to false, this opens up other options (other than modifying the url that paperclip returns) for giving feedback to the user while the image is processing. This is useful for advanced situations, for example when dealing with caching systems. 139 | 140 | Note especially the method #processing? which passes through the value 141 | of the boolean created via migration. 142 | 143 | ```ruby 144 | class User < ActiveRecord::Base 145 | has_attached_file :avatar 146 | 147 | process_in_background :avatar, url_with_processing: false 148 | end 149 | 150 | @user = User.new(avatar: File.new(...)) 151 | @user.save 152 | @user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148" 153 | @user.avatar.processing? #=> true 154 | 155 | # Process job 156 | 157 | @user.reload 158 | @user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148" 159 | @user.avatar.processing? #=> false 160 | ``` 161 | 162 | #### Only process certain styles 163 | 164 | This is useful if you don’t want the background job to reprocess all 165 | styles. 166 | 167 | ```ruby 168 | class User < ActiveRecord::Base 169 | has_attached_file :avatar, styles: { small: "25x25#", medium: "50x50#" } 170 | 171 | process_in_background :avatar, only_process: [:small] 172 | end 173 | ``` 174 | 175 | Like paperclip, you could also supply a lambda function to define 176 | `only_process` dynamically. 177 | 178 | ```ruby 179 | class User < ActiveRecord::Base 180 | has_attached_file :avatar, styles: { small: "25x25#", medium: "50x50#" } 181 | 182 | process_in_background :avatar, only_process: lambda { |a| a.instance.small_supported? ? [:small, :large] : [:large] } 183 | end 184 | ``` 185 | 186 | #### Split processing 187 | 188 | You can process some styles in the foreground and some in the background 189 | by setting `only_process` on both `has_attached_file` and 190 | `process_in_background`. 191 | 192 | ```ruby 193 | class User < ActiveRecord::Base 194 | has_attached_file :avatar, styles: { small: "25x25#", medium: "50x50#" }, only_process: [:small] 195 | 196 | process_in_background :avatar, only_process: [:medium] 197 | end 198 | ``` 199 | 200 | #### Reprocess Without Delay 201 | 202 | This is useful if you don’t want the background job. It accepts 203 | individual styles too. Take note, normal `reprocess!` does not accept styles as arguments anymore. It will delegate to DelayedPaperclip and 204 | reprocess all styles. 205 | 206 | ```ruby 207 | class User < ActiveRecord::Base 208 | has_attached_file :avatar, styles: { small: "25x25#", medium: "50x50#" } 209 | 210 | process_in_background :avatar 211 | end 212 | 213 | @user.avatar.url #=> "/system/images/3/original/IMG_2772.JPG?1267562148" 214 | @user.avatar.reprocess_without_delay!(:medium) 215 | ``` 216 | 217 | #### Set queue name 218 | 219 | You can set queue name for background job. By default it's called "paperclip". 220 | You can set it by changing global default options or by: 221 | 222 | ```ruby 223 | class User < ActiveRecord::Base 224 | has_attached_file :avatar 225 | 226 | process_in_background :avatar, queue: "default" 227 | end 228 | ``` 229 | 230 | Defaults 231 | -------- 232 | 233 | Global defaults for all delayed_paperclip instances in your app can be 234 | defined by changing the DelayedPaperclip.options Hash, this can be useful for setting a default ‘processing image,’ so you won’t have to define it in every `process_in_background` definition. 235 | 236 | If you’re using Rails you can define a Hash with default options in 237 | config/application.rb or in any of the config/environments/\*.rb files on `config.delayed_paperclip_defaults`, these will get merged into DelayedPaperclip.options as your Rails app boots. An example: 238 | 239 | ```ruby 240 | module YourApp 241 | class Application < Rails::Application 242 | # Other code... 243 | 244 | config.delayed_paperclip_defaults = { 245 | url_with_processing: true, 246 | processing_image_url: 'custom_processing.png' 247 | } 248 | end 249 | end 250 | ``` 251 | 252 | What if I’m not using images? 253 | ----------------------------- 254 | 255 | This library works no matter what kind of post-processing you are doing 256 | with Paperclip. 257 | 258 | Paperclip Post-processors are not working 259 | ----------------------------------------- 260 | 261 | If you are using custom [post-processing processors](https://github.com/thoughtbot/paperclip#post-processing) 262 | like this: 263 | 264 | ```ruby 265 | # ... 266 | 267 | has_attached_file :avatar, styles: { thumb: '100x100>' }, processors: [:rotator] 268 | process_in_background :avatar 269 | 270 | def rotate! 271 | # ... 272 | avatar.reprocess! 273 | # ... 274 | end 275 | 276 | # ... 277 | ``` 278 | 279 | ...you may encounter an issue where your post-processors are ignored 280 | ([more info](https://github.com/jrgifford/delayed_paperclip/issues/171)). 281 | In order to avoid this use `reprocess_without_delay!` 282 | 283 | ```ruby 284 | # ... 285 | 286 | def rotate! 287 | # ... 288 | avatar.reprocess_without_delay! 289 | # ... 290 | end 291 | 292 | # ... 293 | ``` 294 | 295 | Does it work with s3? 296 | --------------------- 297 | 298 | Yes. 299 | 300 | Contributing 301 | ------------ 302 | 303 | Checkout out [CONTRIBUTING](https://github.com/jrgifford/delayed_paperclip/blob/master/CONTRIBUTING). Run specs with: 304 | 305 | ``` 306 | # Rspec on all versions 307 | bundle exec appraisal install 308 | bundle exec appraisal rake 309 | 310 | # Rspec on latest stable gems 311 | bundle exec rake 312 | 313 | # Rspec on specific rails version 314 | bundle exec appraisal 5.0 rake 315 | ``` 316 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | require 'rake' 4 | require 'rspec/core/rake_task' 5 | 6 | desc 'Default: run specs' 7 | task default: [:clean, :spec] 8 | 9 | desc 'Clean up files' 10 | task :clean do |t| 11 | FileUtils.rm_rf "doc" 12 | FileUtils.rm_rf "tmp" 13 | FileUtils.rm_rf "pkg" 14 | FileUtils.rm_rf "public" 15 | Dir.glob("paperclip-*.gem").each { |f| FileUtils.rm f } 16 | end 17 | 18 | RSpec::Core::RakeTask.new do |t| 19 | t.pattern = 'spec/**/*_spec.rb' 20 | end 21 | -------------------------------------------------------------------------------- /delayed_paperclip.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "delayed_paperclip/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = %q{delayed_paperclip} 6 | s.version = DelayedPaperclip::VERSION 7 | 8 | s.authors = ["Jesse Storimer", "Bert Goethals", "James Gifford", "Scott Carleton"] 9 | s.summary = %q{Process your Paperclip attachments in the background} 10 | s.description = %q{Process your Paperclip attachments in the background with ActiveJob} 11 | s.email = %w{james@jamesrgifford.com scott@artsicle.com} 12 | s.homepage = %q{https://github.com/jrgifford/delayed_paperclip} 13 | 14 | s.required_ruby_version = ">= 2.0.0" 15 | 16 | s.add_dependency 'paperclip', [">= 3.3"] 17 | s.add_dependency 'activejob', ">= 4.2" 18 | 19 | s.add_development_dependency 'mocha' 20 | s.add_development_dependency "rspec", '< 3.0' 21 | s.add_development_dependency 'sqlite3' 22 | s.add_development_dependency 'appraisal' 23 | s.add_development_dependency 'rake', '~> 10.5.0' 24 | s.add_development_dependency 'bundler' 25 | s.add_development_dependency 'activerecord' 26 | s.add_development_dependency 'railties' 27 | 28 | s.files = `git ls-files`.split("\n") 29 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 30 | end 31 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- {} 2 | -------------------------------------------------------------------------------- /gemfiles/4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.2.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/5.0_paperclip_master.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.0.0" 6 | gem "paperclip", :github => "thoughtbot/paperclip" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /lib/delayed_paperclip.rb: -------------------------------------------------------------------------------- 1 | require 'delayed_paperclip/process_job' 2 | require 'delayed_paperclip/attachment' 3 | require 'delayed_paperclip/url_generator' 4 | require 'delayed_paperclip/railtie' if defined?(Rails) 5 | 6 | module DelayedPaperclip 7 | class << self 8 | def options 9 | @options ||= { 10 | :background_job_class => DelayedPaperclip::ProcessJob, 11 | :url_with_processing => true, 12 | :processing_image_url => nil, 13 | :queue => "paperclip" 14 | } 15 | end 16 | 17 | def processor 18 | options[:background_job_class] 19 | end 20 | 21 | def enqueue(instance_klass, instance_id, attachment_name) 22 | processor.enqueue_delayed_paperclip(instance_klass, instance_id, attachment_name) 23 | end 24 | 25 | def process_job(instance_klass, instance_id, attachment_name) 26 | instance = instance_klass.constantize.unscoped.where(id: instance_id).first 27 | return if instance.blank? 28 | 29 | instance. 30 | send(attachment_name). 31 | process_delayed! 32 | end 33 | 34 | end 35 | 36 | module Glue 37 | def self.included(base) 38 | base.extend(ClassMethods) 39 | base.send :include, InstanceMethods 40 | end 41 | end 42 | 43 | module ClassMethods 44 | 45 | def process_in_background(name, options = {}) 46 | # initialize as hash 47 | paperclip_definitions[name][:delayed] = {} 48 | 49 | # Set Defaults 50 | only_process_default = paperclip_definitions[name][:only_process] 51 | only_process_default ||= [] 52 | { 53 | :priority => 0, 54 | :only_process => only_process_default, 55 | :url_with_processing => DelayedPaperclip.options[:url_with_processing], 56 | :processing_image_url => DelayedPaperclip.options[:processing_image_url], 57 | :queue => DelayedPaperclip.options[:queue] 58 | }.each do |option, default| 59 | paperclip_definitions[name][:delayed][option] = options.key?(option) ? options[option] : default 60 | end 61 | 62 | # Sets callback 63 | if respond_to?(:after_commit) 64 | after_commit :enqueue_delayed_processing 65 | else 66 | after_save :enqueue_delayed_processing 67 | end 68 | end 69 | 70 | def paperclip_definitions 71 | if respond_to? :attachment_definitions 72 | attachment_definitions 73 | else 74 | Paperclip::Tasks::Attachments.definitions_for(self) 75 | end 76 | end 77 | end 78 | 79 | module InstanceMethods 80 | 81 | # First mark processing 82 | # then enqueue 83 | def enqueue_delayed_processing 84 | mark_enqueue_delayed_processing 85 | 86 | (@_enqued_for_processing || []).each do |name| 87 | enqueue_post_processing_for(name) 88 | end 89 | @_enqued_for_processing_with_processing = [] 90 | @_enqued_for_processing = [] 91 | end 92 | 93 | # setting each inididual NAME_processing to true, skipping the ActiveModel dirty setter 94 | # Then immediately push the state to the database 95 | def mark_enqueue_delayed_processing 96 | unless @_enqued_for_processing_with_processing.blank? # catches nil and empty arrays 97 | updates = @_enqued_for_processing_with_processing.collect{|n| "#{n}_processing = :true" }.join(", ") 98 | updates = ActiveRecord::Base.send(:sanitize_sql_array, [updates, {:true => true}]) 99 | self.class.unscoped.where(:id => self.id).update_all(updates) 100 | end 101 | end 102 | 103 | def enqueue_post_processing_for name 104 | DelayedPaperclip.enqueue(self.class.name, read_attribute(:id), name.to_sym) 105 | end 106 | 107 | def prepare_enqueueing_for name 108 | if self.attributes.has_key? "#{name}_processing" 109 | write_attribute("#{name}_processing", true) 110 | @_enqued_for_processing_with_processing ||= [] 111 | @_enqued_for_processing_with_processing << name 112 | end 113 | 114 | @_enqued_for_processing ||= [] 115 | @_enqued_for_processing << name 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/delayed_paperclip/attachment.rb: -------------------------------------------------------------------------------- 1 | module DelayedPaperclip 2 | module Attachment 3 | attr_accessor :job_is_processing 4 | 5 | def delayed_options 6 | options[:delayed] 7 | end 8 | 9 | # Attr accessor in Paperclip 10 | def post_processing 11 | !delay_processing? || split_processing? 12 | end 13 | 14 | def post_processing=(value) 15 | @post_processing_with_delay = value 16 | end 17 | 18 | # if nil, returns whether it has delayed options 19 | # if set, then it returns 20 | def delay_processing? 21 | if @post_processing_with_delay.nil? 22 | !!delayed_options 23 | else 24 | !@post_processing_with_delay 25 | end 26 | end 27 | 28 | def split_processing? 29 | options[:only_process] && delayed_options && 30 | options[:only_process] != delayed_only_process 31 | end 32 | 33 | def processing? 34 | column_name = :"#{@name}_processing?" 35 | @instance.respond_to?(column_name) && @instance.send(column_name) 36 | end 37 | 38 | def processing_style?(style) 39 | return false if !processing? 40 | 41 | !split_processing? || delayed_only_process.include?(style) 42 | end 43 | 44 | def delayed_only_process 45 | only_process = delayed_options.fetch(:only_process, []).dup 46 | only_process = only_process.call(self) if only_process.respond_to?(:call) 47 | only_process.map(&:to_sym) 48 | end 49 | 50 | def process_delayed! 51 | self.job_is_processing = true 52 | self.post_processing = true 53 | reprocess!(*delayed_only_process) 54 | self.job_is_processing = false 55 | update_processing_column 56 | end 57 | 58 | def processing_image_url 59 | processing_image_url = delayed_options[:processing_image_url] 60 | processing_image_url = processing_image_url.call(self) if processing_image_url.respond_to?(:call) 61 | processing_image_url 62 | end 63 | 64 | def save 65 | was_dirty = @dirty 66 | 67 | super.tap do 68 | if delay_processing? && was_dirty 69 | instance.prepare_enqueueing_for name 70 | end 71 | end 72 | end 73 | 74 | def reprocess_without_delay!(*style_args) 75 | @post_processing_with_delay = true 76 | reprocess!(*style_args) 77 | end 78 | 79 | private 80 | 81 | def update_processing_column 82 | if instance.respond_to?(:"#{name}_processing?") 83 | instance.send("#{name}_processing=", false) 84 | instance.class.unscoped.where(instance.class.primary_key => instance.id).update_all({ "#{name}_processing" => false }) 85 | end 86 | end 87 | 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/delayed_paperclip/process_job.rb: -------------------------------------------------------------------------------- 1 | require "active_job" 2 | 3 | module DelayedPaperclip 4 | class ProcessJob < ActiveJob::Base 5 | def self.enqueue_delayed_paperclip(instance_klass, instance_id, attachment_name) 6 | queue_name = instance_klass.constantize.paperclip_definitions[attachment_name][:delayed][:queue] 7 | set(:queue => queue_name).perform_later(instance_klass, instance_id, attachment_name.to_s) 8 | end 9 | 10 | def perform(instance_klass, instance_id, attachment_name) 11 | DelayedPaperclip.process_job(instance_klass, instance_id, attachment_name.to_sym) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/delayed_paperclip/railtie.rb: -------------------------------------------------------------------------------- 1 | require "paperclip" 2 | require "delayed_paperclip" 3 | 4 | module DelayedPaperclip 5 | # On initialzation, include DelayedPaperclip 6 | class Railtie < Rails::Railtie 7 | initializer "delayed_paperclip.insert_into_active_record" do |app| 8 | ActiveSupport.on_load :active_record do 9 | DelayedPaperclip::Railtie.insert 10 | end 11 | 12 | if app.config.respond_to?(:delayed_paperclip_defaults) 13 | DelayedPaperclip.options.merge!(app.config.delayed_paperclip_defaults) 14 | end 15 | end 16 | end 17 | 18 | class Railtie 19 | # Glue includes DelayedPaperclip Class Methods and Instance Methods into ActiveRecord 20 | # Attachment and URL Generator extends Paperclip 21 | def self.insert 22 | ActiveRecord::Base.send(:include, DelayedPaperclip::Glue) 23 | Paperclip::Attachment.prepend(DelayedPaperclip::Attachment) 24 | Paperclip::Attachment.default_options[:url_generator] = DelayedPaperclip::UrlGenerator 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/delayed_paperclip/url_generator.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'paperclip/url_generator' 3 | 4 | module DelayedPaperclip 5 | class UrlGenerator < ::Paperclip::UrlGenerator 6 | def initialize(attachment, _compatibility = nil) 7 | @attachment = attachment 8 | @attachment_options = attachment.options 9 | end 10 | 11 | def for(style_name, options) 12 | most_appropriate_url = @attachment.processing_style?(style_name) ? most_appropriate_url(style_name) : most_appropriate_url() 13 | timestamp_as_needed( 14 | escape_url_as_needed( 15 | @attachment_options[:interpolator].interpolate(most_appropriate_url, @attachment, style_name), 16 | options 17 | ), 18 | options) 19 | end 20 | 21 | # This method is a mess 22 | def most_appropriate_url(style = nil) 23 | if @attachment.processing_style?(style) 24 | if @attachment.original_filename.nil? || delayed_default_url?(style) 25 | 26 | if @attachment.delayed_options.nil? || 27 | @attachment.processing_image_url.nil? || 28 | !@attachment.processing? 29 | default_url 30 | else 31 | @attachment.processing_image_url 32 | end 33 | 34 | else 35 | @attachment_options[:url] 36 | end 37 | else 38 | super() 39 | end 40 | end 41 | 42 | def timestamp_possible? 43 | delayed_default_url? ? false : super 44 | end 45 | 46 | def delayed_default_url?(style = nil) 47 | return false if @attachment.job_is_processing 48 | return false if @attachment.dirty? 49 | return false if not @attachment.delayed_options.try(:[], :url_with_processing) 50 | return false if not processing?(style) 51 | true 52 | end 53 | 54 | private 55 | 56 | def processing?(style) 57 | return true if @attachment.processing? 58 | return @attachment.processing_style?(style) if style 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/delayed_paperclip/version.rb: -------------------------------------------------------------------------------- 1 | module DelayedPaperclip 2 | VERSION = "3.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/delayed_paperclip/attachment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DelayedPaperclip::Attachment do 4 | before :each do 5 | reset_dummy(dummy_options) 6 | end 7 | 8 | let(:dummy_options) { {} } 9 | let(:dummy) { Dummy.create } 10 | 11 | describe "#delayed_options" do 12 | it "returns the specific options for delayed paperclip" do 13 | expect(dummy.image.delayed_options).to eq({ 14 | :priority => 0, 15 | :only_process => [], 16 | :url_with_processing => true, 17 | :processing_image_url => nil, 18 | :queue => "paperclip" 19 | }) 20 | end 21 | end 22 | 23 | describe "#post_processing_with_delay" do 24 | it "is true if delay_processing? is false" do 25 | dummy.image.stubs(:delay_processing?).returns false 26 | dummy.image.post_processing.should be_truthy 27 | end 28 | 29 | it "is false if delay_processing? is true" do 30 | dummy.image.stubs(:delay_processing?).returns true 31 | dummy.image.post_processing.should be_falsey 32 | end 33 | 34 | context "on a non-delayed image" do 35 | let(:dummy_options) { { with_processed: false } } 36 | 37 | it "is false if delay_processing? is true" do 38 | dummy.image.stubs(:delay_processing?).returns true 39 | dummy.image.post_processing.should be_falsey 40 | end 41 | end 42 | end 43 | 44 | describe "#delay_processing?" do 45 | it "returns delayed_options existence if post_processing is nil" do 46 | dummy.image.post_processing = nil 47 | dummy.image.delay_processing?.should be_truthy 48 | end 49 | 50 | it "returns inverse of post_processing if it's set" do 51 | dummy.image.post_processing = true 52 | dummy.image.delay_processing?.should be_falsey 53 | end 54 | end 55 | 56 | describe "#processing?" do 57 | it "delegates to the dummy instance" do 58 | dummy.expects(:image_processing?) 59 | dummy.image.processing? 60 | end 61 | 62 | context "without a processing column" do 63 | let(:dummy_options) { { with_processed: false } } 64 | 65 | it "returns false" do 66 | expect(dummy.image.processing?).to be_falsey 67 | end 68 | end 69 | end 70 | 71 | describe "#processing_style?" do 72 | let(:style) { :background } 73 | let(:processing_style?) { dummy.image.processing_style?(style) } 74 | 75 | context "without a processing column" do 76 | let(:dummy_options) { { with_processed: true, process_column: false } } 77 | 78 | specify { expect(processing_style?).to be_falsey } 79 | end 80 | 81 | context "with a processing column" do 82 | context "when not processing" do 83 | before { dummy.image_processing = false } 84 | 85 | specify { expect(processing_style?).to be_falsey } 86 | end 87 | 88 | context "when processing" do 89 | before { dummy.image_processing = true } 90 | 91 | context "when not split processing" do 92 | specify { expect(processing_style?).to be_truthy } 93 | end 94 | 95 | context "when split processing" do 96 | context "when delayed :only_process is an Array" do 97 | let(:dummy_options) { { 98 | paperclip: { 99 | styles: { 100 | online: "400x400x", 101 | background: "600x600x" 102 | }, 103 | only_process: [:online] 104 | }, 105 | 106 | only_process: [:background] 107 | }} 108 | 109 | specify { expect(processing_style?).to be } 110 | end 111 | 112 | context "when delayed :only_process is callable" do 113 | let(:dummy_options) { { 114 | paperclip: { 115 | styles: { 116 | online: "400x400x", 117 | background: "600x600x" 118 | }, 119 | only_process: [:online] 120 | }, 121 | 122 | only_process: lambda { |a| [:background] } 123 | }} 124 | 125 | specify { expect(processing_style?).to be } 126 | end 127 | end 128 | end 129 | end 130 | end 131 | 132 | describe "#delayed_only_process" do 133 | context "without only_process options" do 134 | it "returns []" do 135 | expect(dummy.image.delayed_only_process).to eq [] 136 | end 137 | end 138 | 139 | context "with only_process options" do 140 | before :each do 141 | reset_dummy(paperclip: { only_process: [:small, :large] } ) 142 | end 143 | 144 | it "returns [:small, :large]" do 145 | expect(dummy.image.delayed_only_process).to eq [:small, :large] 146 | end 147 | end 148 | 149 | context "with only_process set with callable" do 150 | before :each do 151 | reset_dummy(paperclip: { only_process: lambda { |a| [:small, :large] } } ) 152 | end 153 | 154 | # Enable when https://github.com/thoughtbot/paperclip/pull/2289 is resolved 155 | xit "returns [:small, :large]" do 156 | expect(dummy.image.delayed_only_process).to eq [:small, :large] 157 | end 158 | end 159 | end 160 | 161 | describe "#process_delayed!" do 162 | it "sets job_is_processing to true" do 163 | dummy.image.expects(:job_is_processing=).with(true).once 164 | dummy.image.expects(:job_is_processing=).with(false).once 165 | dummy.image.process_delayed! 166 | end 167 | 168 | it "sets post_processing to true" do 169 | dummy.image.expects(:post_processing=).with(true).once 170 | dummy.image.process_delayed! 171 | end 172 | 173 | context "without only_process options" do 174 | it "calls reprocess!" do 175 | dummy.image.expects(:reprocess!) 176 | dummy.image.process_delayed! 177 | end 178 | end 179 | 180 | context "with only_process options" do 181 | before :each do 182 | reset_dummy(paperclip: { only_process: [:small, :large] } ) 183 | end 184 | 185 | it "calls reprocess! with options" do 186 | dummy.image.expects(:reprocess!).with(:small, :large) 187 | dummy.image.process_delayed! 188 | end 189 | end 190 | 191 | context "with only_process set with callable" do 192 | before :each do 193 | reset_dummy(paperclip: { only_process: lambda { |a| [:small, :large] } } ) 194 | end 195 | 196 | # Enable when https://github.com/thoughtbot/paperclip/pull/2289 is resolved 197 | xit "calls reprocess! with options" do 198 | dummy.image.expects(:reprocess!).with(:small, :large) 199 | dummy.image.process_delayed! 200 | end 201 | end 202 | end 203 | 204 | describe "#processing_image_url" do 205 | context "no url" do 206 | it "returns nil" do 207 | dummy.image.processing_image_url.should be_nil 208 | end 209 | end 210 | 211 | context "static url" do 212 | before :each do 213 | reset_dummy(:processing_image_url => "/foo/bar.jpg") 214 | end 215 | 216 | it "returns given url" do 217 | dummy.image.processing_image_url.should == "/foo/bar.jpg" 218 | end 219 | end 220 | 221 | context "proc" do 222 | before :each do 223 | reset_dummy(:processing_image_url => proc { "Hello/World" } ) 224 | end 225 | 226 | it "returns evaluates proc" do 227 | dummy.image.processing_image_url.should == "Hello/World" 228 | end 229 | end 230 | end 231 | 232 | describe "#update_processing_column" do 233 | it "updates the column to false" do 234 | dummy.update_attribute(:image_processing, true) 235 | 236 | dummy.image.send(:update_processing_column) 237 | 238 | dummy.reload.image_processing.should be_falsey 239 | end 240 | 241 | context 'with a default scope on the model excluding the instance' do 242 | let(:dummy_options) do 243 | { :default_scope => lambda { Dummy.where(hidden: false) } } 244 | end 245 | 246 | let!(:dummy) { Dummy.create(hidden: true) } 247 | 248 | specify { Dummy.count.should be 0 } 249 | specify { Dummy.unscoped.count.should be 1 } 250 | 251 | it "ignores the default scope and updates the column to false" do 252 | dummy.update_attribute(:image_processing, true) 253 | dummy.image.send(:update_processing_column) 254 | dummy.reload.image_processing.should be_falsey 255 | end 256 | end 257 | end 258 | 259 | describe "#save" do 260 | context "delay processing and it was dirty" do 261 | before :each do 262 | dummy.image.stubs(:delay_processing?).returns true 263 | dummy.image.instance_variable_set(:@dirty, true) 264 | end 265 | 266 | it "prepares the enqueing" do 267 | dummy.expects(:prepare_enqueueing_for).with(:image) 268 | dummy.image.save 269 | end 270 | end 271 | 272 | context "without dirty or delay_processing" do 273 | it "does not prepare_enqueueing" do 274 | dummy.expects(:prepare_enqueueing_for).with(:image).never 275 | dummy.image.save 276 | end 277 | end 278 | end 279 | 280 | describe "#reprocess_without_delay!" do 281 | it "sets post post_processing_with_delay and reprocesses with given args" do 282 | dummy.image.expects(:reprocess!).with(:small) 283 | dummy.image.reprocess_without_delay!(:small) 284 | dummy.image.instance_variable_get(:@post_processing_with_delay).should == true 285 | end 286 | end 287 | 288 | describe "#split_processing?" do 289 | let(:split_processing?) { dummy.image.split_processing? } 290 | 291 | let(:paperclip_styles) { { 292 | online: "400x400x", 293 | background: "600x600x" 294 | } } 295 | 296 | context ":only_process option is set on attachment" do 297 | let(:dummy_options) { { 298 | paperclip: { 299 | styles: paperclip_styles, 300 | only_process: [:online] 301 | }, 302 | 303 | only_process: delayed_only_process 304 | }} 305 | 306 | context "processing different styles in background" do 307 | context "when delayed :only_process is an Array" do 308 | let(:delayed_only_process) { [:background] } 309 | 310 | specify { expect(split_processing?).to be true } 311 | end 312 | 313 | context "when delayed :only_process is callable" do 314 | let(:delayed_only_process) { lambda { |a| [:background] } } 315 | 316 | specify { expect(split_processing?).to be true } 317 | end 318 | end 319 | 320 | context "processing same styles in background" do 321 | context "when delayed :only_process is an Array" do 322 | let(:delayed_only_process) { [:online] } 323 | 324 | specify { expect(split_processing?).to be false } 325 | end 326 | 327 | context "when delayed :only_process is callable" do 328 | let(:delayed_only_process) { lambda { |a| [:online] } } 329 | 330 | specify { expect(split_processing?).to be false } 331 | end 332 | end 333 | end 334 | 335 | context ":only_process option is not set on attachment" do 336 | let(:dummy_options) { { 337 | paperclip: { 338 | styles: paperclip_styles 339 | } 340 | }} 341 | 342 | specify { expect(split_processing?).to be false } 343 | end 344 | end 345 | end 346 | -------------------------------------------------------------------------------- /spec/delayed_paperclip/class_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DelayedPaperclip::ClassMethods do 4 | before :each do 5 | reset_dummy(with_processed: false) 6 | end 7 | 8 | describe ".process_in_background" do 9 | it "is empty to start" do 10 | Dummy.paperclip_definitions.should == { :image => {} } 11 | end 12 | 13 | it "adds basics to paperclip_definitions" do 14 | Dummy.process_in_background(:image) 15 | Dummy.paperclip_definitions.should == { :image => { 16 | :delayed => { 17 | :priority => 0, 18 | :only_process => [], 19 | :url_with_processing => true, 20 | :processing_image_url => nil, 21 | :queue => "paperclip"} 22 | } 23 | } 24 | end 25 | 26 | it "allows to set queue name" do 27 | Dummy.process_in_background(:image, :queue => "custom") 28 | expect(Dummy.paperclip_definitions).to eq({ :image => { 29 | :delayed => { 30 | :priority => 0, 31 | :only_process => [], 32 | :url_with_processing => true, 33 | :processing_image_url => nil, 34 | :queue => "custom"} 35 | } 36 | }) 37 | end 38 | 39 | context "with processing_image_url" do 40 | before :each do 41 | Dummy.process_in_background(:image, processing_image_url: "/processing/url") 42 | end 43 | 44 | it "incorporates processing url" do 45 | Dummy.paperclip_definitions.should == { :image => { 46 | :delayed => { 47 | :priority => 0, 48 | :only_process => [], 49 | :url_with_processing => true, 50 | :processing_image_url => "/processing/url", 51 | :queue => "paperclip"} 52 | } 53 | } 54 | end 55 | end 56 | 57 | context "inherits only_process options" do 58 | before :each do 59 | reset_class("Dummy", paperclip: { only_process: [:small, :large] } ) 60 | Dummy.process_in_background(:image) 61 | end 62 | 63 | it "incorporates processing url" do 64 | Dummy.paperclip_definitions.should == { :image => { 65 | :only_process => [:small, :large], 66 | :delayed => { 67 | :priority => 0, 68 | :only_process => [:small, :large], 69 | :url_with_processing => true, 70 | :processing_image_url => nil, 71 | :queue => "paperclip"} 72 | } 73 | } 74 | end 75 | end 76 | 77 | context "sets callback" do 78 | context "commit" do 79 | it "sets after_commit callback" do 80 | Dummy.expects(:after_commit).with(:enqueue_delayed_processing) 81 | Dummy.process_in_background(:image) 82 | end 83 | end 84 | 85 | context "save" do 86 | it "sets after_save callback" do 87 | Dummy.stubs(:respond_to?).with(:attachment_definitions).returns(true) 88 | Dummy.stubs(:respond_to?).with(:after_commit).returns(false) 89 | Dummy.expects(:after_save).with(:enqueue_delayed_processing) 90 | Dummy.process_in_background(:image) 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/delayed_paperclip/instance_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DelayedPaperclip::InstanceMethods do 4 | before :each do 5 | reset_dummy 6 | end 7 | 8 | let(:dummy) { Dummy.create } 9 | 10 | describe "#enqueue_delayed_processing" do 11 | it "marks enqueue_delayed_processing" do 12 | dummy.expects(:mark_enqueue_delayed_processing) 13 | dummy.enqueue_delayed_processing 14 | end 15 | 16 | it "enqueues post processing for all enqueued" do 17 | dummy.instance_variable_set(:@_enqued_for_processing, ['image']) 18 | dummy.expects(:enqueue_post_processing_for).with('image') 19 | dummy.enqueue_delayed_processing 20 | end 21 | 22 | it "clears instance variables" do 23 | dummy.instance_variable_set(:@_enqued_for_processing, ['foo']) 24 | dummy.instance_variable_set(:@_enqued_for_processing_with_processing, ['image']) 25 | dummy.expects(:enqueue_post_processing_for).with('foo') 26 | dummy.enqueue_delayed_processing 27 | dummy.instance_variable_get(:@_enqued_for_processing).should == [] 28 | dummy.instance_variable_get(:@_enqued_for_processing_with_processing).should == [] 29 | end 30 | end 31 | 32 | describe "#mark_enqueue_delayed_processing" do 33 | it "updates columns of _processing" do 34 | dummy.image_processing.should be_falsey 35 | dummy.instance_variable_set(:@_enqued_for_processing_with_processing, ['image']) 36 | dummy.mark_enqueue_delayed_processing 37 | dummy.reload.image_processing.should be_truthy 38 | end 39 | 40 | it "does nothing if instance variable not set" do 41 | dummy.image_processing.should be_falsey 42 | dummy.mark_enqueue_delayed_processing 43 | dummy.reload.image_processing.should be_falsey 44 | end 45 | end 46 | 47 | describe "#enqueue_post_processing_for" do 48 | it "enqueues the instance and image" do 49 | DelayedPaperclip.expects(:enqueue).with("Dummy", dummy.id, :image) 50 | dummy.enqueue_post_processing_for("image") 51 | end 52 | end 53 | 54 | describe "#prepare_enqueueing_for" do 55 | it "updates processing column to true" do 56 | dummy.image_processing.should be_falsey 57 | dummy.prepare_enqueueing_for("image") 58 | dummy.image_processing.should be_truthy 59 | end 60 | 61 | it "sets instance variables for column updating" do 62 | dummy.prepare_enqueueing_for("image") 63 | dummy.instance_variable_get(:@_enqued_for_processing_with_processing).should == ["image"] 64 | end 65 | 66 | it "sets instance variables for processing" do 67 | dummy.prepare_enqueueing_for("image") 68 | dummy.instance_variable_get(:@_enqued_for_processing).should == ["image"] 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/delayed_paperclip/url_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DelayedPaperclip::UrlGenerator do 4 | before :each do 5 | reset_dummy({}) 6 | end 7 | 8 | def make_attachment(options = {}) 9 | reset_dummy(options) 10 | Dummy.create.image 11 | end 12 | 13 | describe "for" do 14 | before :each do 15 | attachment.stubs(:original_filename).returns "12k.png" 16 | end 17 | 18 | context "with split processing" do 19 | let(:attachment) do 20 | make_attachment( 21 | { 22 | paperclip: { 23 | styles: { 24 | online: "400x400x", 25 | background: "600x600x" 26 | }, 27 | only_process: [:online] 28 | }, 29 | 30 | only_process: [:background] 31 | } 32 | ) 33 | end 34 | 35 | context "processing" do 36 | before :each do 37 | attachment.stubs(:processing?).returns true 38 | end 39 | 40 | it "returns the default_url when the style is still being processed" do 41 | expect(attachment.url(:background)).to eql "/images/background/missing.png" 42 | end 43 | 44 | it "returns the attachment url when the style is not set for background processing" do 45 | expect(attachment.url(:online)).to eql "/system/dummies/images/000/000/001/online/12k.png" 46 | end 47 | end 48 | 49 | context "not processing" do 50 | before :each do 51 | attachment.stubs(:processing?).returns false 52 | end 53 | 54 | it "returns the attachment url even when the style is set for background processing" do 55 | expect(attachment.url(:background)).to eql "/system/dummies/images/000/000/001/background/12k.png" 56 | end 57 | 58 | it "returns the generated url when the style is not set for background processing" do 59 | expect(attachment.url(:online)).to eql "/system/dummies/images/000/000/001/online/12k.png" 60 | end 61 | end 62 | 63 | context "should be able to escape (, ), [, and ]." do 64 | def generate(expected, updated_at = nil) 65 | attachment = make_attachment( 66 | { 67 | paperclip: { 68 | styles: { 69 | online: "400x400x", 70 | background: "600x600x" 71 | }, 72 | only_process: [:online], 73 | interpolator: MockInterpolator.new(result: expected) 74 | }, 75 | 76 | only_process: [:background] 77 | } 78 | ) 79 | url_generator = DelayedPaperclip::UrlGenerator.new(attachment, {}) 80 | attachment.stubs(:updated_at).returns updated_at 81 | url_generator.for(:style_name, {escape: true, timestamp: !!updated_at}) 82 | end 83 | 84 | it "interpolates correctly without timestamp" do 85 | expect( 86 | "the%28expected%29result%5B%5D" 87 | ).to eq(generate("the(expected)result[]")) 88 | end 89 | 90 | it "does not interpolate timestamp" do 91 | expected = "the(expected)result[]" 92 | updated_at = 1231231234 93 | 94 | expect( 95 | "the%28expected%29result%5B%5D?#{updated_at}" 96 | ).to eq(generate(expected, updated_at)) 97 | end 98 | end 99 | end 100 | end 101 | 102 | describe "#most_appropriate_url" do 103 | context "without delayed_default_url" do 104 | let(:attachment) { make_attachment(paperclip: {url: "/blah/url.jpg"}) } 105 | 106 | subject { DelayedPaperclip::UrlGenerator.new(attachment) } 107 | 108 | before :each do 109 | subject.stubs(:delayed_default_url?).returns false 110 | end 111 | 112 | context "with original file name" do 113 | it "returns options url" do 114 | attachment.stubs(:original_filename).returns "blah" 115 | subjec = DelayedPaperclip::UrlGenerator.new(attachment) 116 | subjec.stubs(:delayed_default_url?).returns false 117 | subjec.most_appropriate_url.should == "/blah/url.jpg" 118 | end 119 | end 120 | 121 | context "without original_filename" do 122 | before :each do 123 | attachment.stubs(:original_filename).returns nil 124 | end 125 | 126 | context "without delayed_options" do 127 | before :each do 128 | attachment.stubs(:delayed_options).returns nil 129 | end 130 | 131 | it "gets default url" do 132 | subject.expects(:default_url) 133 | subject.most_appropriate_url 134 | end 135 | end 136 | 137 | context "with delayed_options" do 138 | before :each do 139 | attachment.stubs(:delayed_options).returns(some: 'thing') 140 | end 141 | 142 | context "without processing_image_url" do 143 | before :each do 144 | attachment.stubs(:processing_image_url).returns nil 145 | end 146 | 147 | it "gets default url" do 148 | subject.expects(:default_url) 149 | subject.most_appropriate_url 150 | end 151 | end 152 | 153 | context "with processing_image_url" do 154 | before :each do 155 | attachment.stubs(:processing_image_url).returns "/processing/image.jpg" 156 | end 157 | 158 | context "and is processing" do 159 | before :each do 160 | attachment.stubs(:processing?).returns true 161 | end 162 | 163 | it "gets processing url" do 164 | subject.most_appropriate_url.should == "/processing/image.jpg" 165 | end 166 | end 167 | 168 | context "and is not processing" do 169 | it "gets default url" do 170 | subject.expects(:default_url) 171 | subject.most_appropriate_url 172 | end 173 | end 174 | end 175 | end 176 | end 177 | end 178 | end 179 | 180 | describe "#timestamp_possible?" do 181 | let(:attachment) { make_attachment } 182 | subject { DelayedPaperclip::UrlGenerator.new(attachment) } 183 | 184 | context "with delayed_default_url" do 185 | before :each do 186 | subject.stubs(:delayed_default_url?).returns true 187 | end 188 | 189 | it "is false" do 190 | subject.timestamp_possible?.should be_falsey 191 | end 192 | end 193 | 194 | context "without delayed_default_url" do 195 | before :each do 196 | subject.stubs(:delayed_default_url?).returns false 197 | end 198 | 199 | it "goes up the chain" do 200 | subject.class.superclass.any_instance.expects(:timestamp_possible?) 201 | subject.timestamp_possible? 202 | end 203 | end 204 | end 205 | 206 | describe "#delayed_default_url?" do 207 | let(:attachment) { make_attachment } 208 | subject { DelayedPaperclip::UrlGenerator.new(attachment) } 209 | 210 | before :each do 211 | attachment.stubs(:job_is_processing).returns false 212 | attachment.stubs(:dirty?).returns false 213 | attachment.delayed_options[:url_with_processing] = true 214 | attachment.instance.stubs(:respond_to?).with(:image_processing?).returns true 215 | attachment.stubs(:processing?).returns true 216 | attachment.stubs(:processing_style?).with(anything).returns true 217 | end 218 | 219 | it "has all false, delayed_default_url returns true" do 220 | subject.delayed_default_url?.should be_truthy 221 | end 222 | 223 | context "job is processing" do 224 | before :each do 225 | attachment.stubs(:job_is_processing).returns true 226 | end 227 | 228 | it "returns true" do 229 | subject.delayed_default_url?.should be_falsey 230 | end 231 | end 232 | 233 | context "attachment is dirty" do 234 | before :each do 235 | attachment.stubs(:dirty?).returns true 236 | end 237 | 238 | it "returns true" do 239 | subject.delayed_default_url?.should be_falsey 240 | end 241 | end 242 | 243 | context "attachment has delayed_options without url_with_processing" do 244 | before :each do 245 | attachment.delayed_options[:url_with_processing] = false 246 | end 247 | 248 | it "returns true" do 249 | subject.delayed_default_url?.should be_falsey 250 | end 251 | end 252 | 253 | context "attachment does not responds to name_processing and is not processing" do 254 | before :each do 255 | attachment.instance.stubs(:respond_to?).with(:image_processing?).returns false 256 | attachment.stubs(:processing?).returns false 257 | end 258 | 259 | it "returns true" do 260 | subject.delayed_default_url?.should be_falsey 261 | end 262 | end 263 | 264 | context "style is provided and is being processed" do 265 | let(:style) { :main } 266 | before :each do 267 | attachment.stubs(:processing_style?).with(style).returns(true) 268 | end 269 | 270 | specify { expect(subject.delayed_default_url?(style)).to be } 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /spec/delayed_paperclip_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DelayedPaperclip do 4 | before :each do 5 | reset_dummy 6 | end 7 | 8 | describe ".options" do 9 | it ".options returns basic options" do 10 | DelayedPaperclip.options.should == {:background_job_class => DelayedPaperclip::ProcessJob, 11 | :url_with_processing => true, 12 | :processing_image_url => nil, 13 | :queue => "paperclip"} 14 | end 15 | end 16 | 17 | describe ".processor" do 18 | it ".processor returns processor" do 19 | DelayedPaperclip.processor.should == DelayedPaperclip::ProcessJob 20 | end 21 | end 22 | 23 | describe ".enqueue" do 24 | it "delegates to processor" do 25 | DelayedPaperclip::ProcessJob.expects(:enqueue_delayed_paperclip).with("Dummy", 1, :image) 26 | DelayedPaperclip.enqueue("Dummy", 1, :image) 27 | end 28 | end 29 | 30 | describe ".process_job" do 31 | let(:dummy) { Dummy.create! } 32 | 33 | it "returns early on invalid id" do 34 | dummy_stub = stub 35 | dummy_stub.expects(:where).with(id: dummy.id).returns([]) 36 | Dummy.expects(:unscoped).returns(dummy_stub) 37 | dummy.image.expects(:process_delayed!).never 38 | DelayedPaperclip.process_job("Dummy", dummy.id, :image) 39 | end 40 | 41 | it "finds dummy and calls #process_delayed!" do 42 | dummy_stub = stub 43 | dummy_stub.expects(:where).with(id: dummy.id).returns([dummy]) 44 | Dummy.expects(:unscoped).returns(dummy_stub) 45 | dummy.image.expects(:process_delayed!) 46 | DelayedPaperclip.process_job("Dummy", dummy.id, :image) 47 | end 48 | end 49 | 50 | describe "paperclip definitions" do 51 | before :each do 52 | reset_dummy :paperclip => { styles: { thumbnail: "25x25"} } 53 | end 54 | 55 | it "returns paperclip options regardless of version" do 56 | expect(Dummy.paperclip_definitions).to eq({:image => { :styles => { :thumbnail => "25x25" }, 57 | :delayed => { :priority => 0, 58 | :only_process => [], 59 | :url_with_processing => true, 60 | :processing_image_url => nil, 61 | :queue => "paperclip"} 62 | } 63 | }) 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /spec/fixtures/12k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrgifford/delayed_paperclip/fcf574c1188213d3c8fce934fd52861a8ba080cb/spec/fixtures/12k.png -------------------------------------------------------------------------------- /spec/fixtures/missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrgifford/delayed_paperclip/fcf574c1188213d3c8fce934fd52861a8ba080cb/spec/fixtures/missing.png -------------------------------------------------------------------------------- /spec/integration/base_delayed_paperclip_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Base Delayed Paperclip Integration" do 4 | let(:dummy) { Dummy.create } 5 | 6 | before :each do 7 | reset_dummy(paperclip: { default_url: "/../../fixtures/missing.png" }) 8 | end 9 | 10 | describe "double save" do 11 | before :each do 12 | dummy.image_processing.should be_falsey 13 | dummy.image = File.open("#{ROOT}/fixtures/12k.png") 14 | dummy.save! 15 | end 16 | 17 | it "processing column remains true" do 18 | dummy.image_processing.should be_truthy 19 | dummy.save! 20 | dummy.image_processing.should be_truthy 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/integration/examples/base.rb: -------------------------------------------------------------------------------- 1 | shared_examples "base usage" do 2 | before :each do 3 | DelayedPaperclip.options[:url_with_processing] = true 4 | reset_dummy 5 | end 6 | 7 | describe "normal paperclip" do 8 | before :each do 9 | DelayedPaperclip.options[:url_with_processing] = false 10 | reset_dummy :with_processed => false 11 | end 12 | 13 | it "allows normal paperclip functionality" do 14 | Paperclip::Attachment.any_instance.expects(:post_process) 15 | dummy.image.delay_processing?.should be_falsey 16 | dummy.image.post_processing.should be_truthy 17 | dummy.save.should be_truthy 18 | File.exists?(dummy.image.path).should be_truthy 19 | end 20 | 21 | context "missing url" do 22 | it "does not return missing url if false globally" do 23 | dummy.save! 24 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 25 | process_jobs 26 | dummy.reload 27 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 28 | end 29 | 30 | it "does not return missing url if false on instance" do 31 | reset_dummy :with_processed => false, :url_with_processing => false 32 | dummy.save! 33 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 34 | process_jobs # There aren't any 35 | dummy.reload 36 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 37 | end 38 | end 39 | 40 | # TODO: somewhat duplicate test of the above 41 | context "original url without processing column" do 42 | it "works normally" do 43 | dummy.save! 44 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 45 | end 46 | end 47 | end 48 | 49 | describe "set post processing" do 50 | before :each do 51 | reset_dummy :with_processed => true 52 | dummy.image.post_processing = true 53 | end 54 | it "has delay_processing is false" do 55 | dummy.image.delay_processing?.should be_falsey 56 | end 57 | 58 | it "post processing returns true" do 59 | dummy.image.post_processing.should be_truthy 60 | end 61 | 62 | it "writes the file" do 63 | dummy.save 64 | File.exists?(dummy.image.path).should be_truthy 65 | end 66 | end 67 | 68 | describe "without processing column" do 69 | before :each do 70 | build_dummy_table(false) 71 | reset_class "Dummy", :with_processed => true 72 | Paperclip::Attachment.any_instance.expects(:post_process).never 73 | end 74 | 75 | it "delays processing" do 76 | dummy.image.delay_processing?.should be_truthy 77 | end 78 | 79 | it "post_processing is false" do 80 | dummy.image.post_processing.should be_falsey 81 | end 82 | 83 | it "has file after save" do 84 | dummy.save 85 | File.exists?(dummy.image.path).should be_truthy 86 | end 87 | 88 | end 89 | 90 | describe "jobs count" do 91 | it "increments by 1" do 92 | original_job_count = jobs_count 93 | dummy.save 94 | jobs_count.should == original_job_count + 1 95 | end 96 | end 97 | 98 | describe "processing column not altered" do 99 | it "resets after job finished" do 100 | dummy.save! 101 | dummy.image_processing?.should be_truthy 102 | process_jobs 103 | dummy.reload.image_processing?.should be_falsey 104 | end 105 | 106 | context "with error" do 107 | it "stays true even if errored" do 108 | Paperclip::Attachment.any_instance.stubs(:reprocess!).raises(StandardError.new('oops')) 109 | 110 | dummy.save! 111 | dummy.image_processing?.should be_truthy 112 | 113 | expect do 114 | process_jobs 115 | end.to raise_error(StandardError) 116 | 117 | dummy.image_processing?.should be_truthy 118 | dummy.reload.image_processing?.should be_truthy 119 | end 120 | end 121 | end 122 | 123 | # TODO: test appears redundant 124 | describe "processing is true for new record" do 125 | it "is true" do 126 | dummy.image_processing?.should be_falsey 127 | dummy.new_record?.should be_truthy 128 | dummy.save! 129 | dummy.reload.image_processing?.should be_truthy 130 | end 131 | end 132 | 133 | describe "urls" do 134 | it "returns missing url until job is finished" do 135 | dummy.save! 136 | dummy.image.url.should start_with("/images/original/missing.png") 137 | process_jobs 138 | dummy.reload 139 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 140 | end 141 | 142 | context "processing url" do 143 | before :each do 144 | reset_dummy :processing_image_url => "/images/original/processing.png" 145 | end 146 | 147 | it "returns processing url while processing" do 148 | dummy.save! 149 | dummy.image.url.should start_with("/images/original/processing.png") 150 | process_jobs 151 | dummy.reload 152 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 153 | end 154 | 155 | context "defaults to missing when no file" do 156 | it "fallsback gracefully" do 157 | dummy = Dummy.new() 158 | dummy.save! 159 | dummy.reload.image.url.should start_with("/images/original/missing.png") 160 | end 161 | end 162 | end 163 | 164 | context "same url if same file assigned" do 165 | it "falls to missing while processing" do 166 | dummy.save! 167 | dummy.image = File.open("#{ROOT}/fixtures/12k.png") 168 | dummy.save! 169 | dummy.image.url.should start_with("/images/original/missing.png") 170 | process_jobs 171 | dummy.reload.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 172 | end 173 | end 174 | 175 | end 176 | 177 | describe "callbacks" do 178 | context "paperclip callback" do 179 | before :each do 180 | Dummy.send(:define_method, :done_processing) { puts 'done' } 181 | Dummy.send(:after_image_post_process, :done_processing) 182 | Dummy.any_instance.expects(:done_processing).once 183 | end 184 | 185 | it "observes after_image_post_process" do 186 | dummy.save! 187 | process_jobs 188 | end 189 | end 190 | 191 | context "after_update callback" do 192 | before :each do 193 | reset_class "Dummy", :with_processed => true, 194 | :with_after_update_callback => true 195 | end 196 | 197 | it "hits after_update" do 198 | Dummy.any_instance.expects(:reprocess).once 199 | dummy.save! 200 | process_jobs 201 | end 202 | end 203 | end 204 | 205 | describe "only_process option" do 206 | 207 | # TODO: This test must be faulty 208 | # https://github.com/jrgifford/delayed_paperclip/issues/40 209 | context "passed just to delayed_paperclip argument" do 210 | before :each do 211 | reset_class "Dummy", :with_processed => true, :only_process => [:thumbnail] 212 | end 213 | 214 | it "reprocesses just those" do 215 | Paperclip::Attachment.any_instance.expects(:reprocess!).with(:thumbnail).once 216 | dummy.save! 217 | process_jobs 218 | end 219 | end 220 | 221 | context "inherits from paperclip options" do 222 | before :each do 223 | reset_class "Dummy", :with_processed => true, :paperclip => { :only_process => [:thumbnail] } 224 | end 225 | 226 | it "reprocesses just those" do 227 | Paperclip::Attachment.any_instance.expects(:reprocess!).with(:thumbnail).once 228 | dummy.save! 229 | process_jobs 230 | end 231 | end 232 | end 233 | 234 | describe "converts image formats" do 235 | before :each do 236 | reset_class "Dummy", :with_processed => true, 237 | :paperclip => { 238 | :styles => { 239 | :thumbnail => ['12x12', :jpg] 240 | } 241 | } 242 | end 243 | 244 | it "observes the option" do 245 | dummy.save! 246 | process_jobs 247 | dummy.reload.image.url(:thumbnail).should start_with("/system/dummies/images/000/000/001/thumbnail/12k.jpg") 248 | File.exists?(dummy.image.path).should be_truthy 249 | end 250 | end 251 | 252 | describe "reprocess_without_delay" do 253 | before :each do 254 | DelayedPaperclip.options[:url_with_processing] = true 255 | reset_dummy :paperclip => { 256 | :styles => { 257 | :thumbnail => '12x12' 258 | } 259 | } 260 | end 261 | 262 | it "does not increase jobs count" do 263 | dummy.save! 264 | dummy.image_processing?.should be_truthy 265 | process_jobs 266 | dummy.reload.image_processing?.should be_falsey 267 | 268 | Paperclip::Attachment.any_instance.expects(:reprocess!).once 269 | 270 | existing_jobs = jobs_count 271 | dummy.image.reprocess_without_delay!(:thumbnail) 272 | existing_jobs.should == jobs_count 273 | 274 | dummy.image_processing?.should be_falsey 275 | File.exists?(dummy.image.path).should be_truthy 276 | end 277 | 278 | end 279 | 280 | describe "reprocessing_url" do 281 | 282 | context "interpolation of styles" do 283 | before :each do 284 | reset_dummy :processing_image_url => "/images/:style/processing.png", 285 | :paperclip => { 286 | :styles => { 287 | :thumbnail => '12x12' 288 | } 289 | } 290 | end 291 | 292 | it "interpolates unporcessed image" do 293 | dummy.save! 294 | dummy.image.url.should start_with("/images/original/processing.png") 295 | dummy.image.url(:thumbnail).should start_with("/images/thumbnail/processing.png") 296 | process_jobs 297 | dummy.reload.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 298 | end 299 | end 300 | 301 | context "proc for reprocessing_url" do 302 | before :each do 303 | reset_dummy :processing_image_url => lambda { |attachment| attachment.instance.reprocessing_url } 304 | Dummy.send(:define_method, :reprocessing_url) { 'done' } 305 | end 306 | 307 | it "calls it correctly" do 308 | dummy.save! 309 | dummy.image.url.should start_with("done") 310 | process_jobs 311 | dummy.reload 312 | dummy.image.url.should start_with("/system/dummies/images/000/000/001/original/12k.png") 313 | end 314 | end 315 | end 316 | 317 | describe "queue option" do 318 | it "enqueues job with given queue name" do 319 | reset_dummy :queue => "custom" 320 | 321 | expect do 322 | dummy.save! 323 | end.to change { jobs_count("custom") }.by(1) 324 | end 325 | end 326 | end 327 | -------------------------------------------------------------------------------- /spec/integration/process_job_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe "DelayedJob::ProcessJob" do 4 | before :each do 5 | ActiveJob::Base.queue_adapter = :test 6 | ActiveJob::Base.logger = nil 7 | end 8 | 9 | let(:dummy) { Dummy.new(:image => File.open("#{ROOT}/fixtures/12k.png")) } 10 | 11 | describe "integration tests" do 12 | include_examples "base usage" 13 | end 14 | 15 | def process_jobs 16 | ActiveJob::Base.queue_adapter.enqueued_jobs.each do |job| 17 | job[:job].send(:perform_now, *job[:args]) 18 | end 19 | end 20 | 21 | def jobs_count(queue = "paperclip") 22 | ActiveJob::Base.queue_adapter.enqueued_jobs.count do |job| 23 | job[:queue] == queue 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 3 | 4 | require 'rails' 5 | require 'active_record' 6 | require 'rspec' 7 | require 'mocha/api' 8 | 9 | begin 10 | require 'pry' 11 | rescue LoadError 12 | # Pry is not available, just ignore. 13 | end 14 | 15 | require 'paperclip/railtie' 16 | Paperclip::Railtie.insert 17 | 18 | require 'delayed_paperclip/railtie' 19 | DelayedPaperclip::Railtie.insert 20 | 21 | # silence deprecation warnings in rails 4.2 22 | # in Rails 5 this setting is deprecated and has no effect 23 | if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=) && Rails::VERSION::MAJOR < 5 24 | ActiveRecord::Base.raise_in_transactional_callbacks = true 25 | end 26 | 27 | # Connect to sqlite 28 | ActiveRecord::Base.establish_connection( 29 | "adapter" => "sqlite3", 30 | "database" => ":memory:" 31 | ) 32 | 33 | # Path for filesystem writing 34 | ROOT = Pathname.new(File.expand_path("../.", __FILE__)) 35 | 36 | logger = Logger.new(ROOT.join("tmp/debug.log")) 37 | ActiveRecord::Base.logger = logger 38 | ActiveJob::Base.logger = logger 39 | Paperclip.logger = logger 40 | 41 | RSpec.configure do |config| 42 | config.mock_with :mocha 43 | 44 | config.order = :random 45 | 46 | config.filter_run focus: true 47 | config.run_all_when_everything_filtered = true 48 | 49 | config.before(:each) do 50 | reset_global_default_options 51 | end 52 | end 53 | 54 | def reset_global_default_options 55 | DelayedPaperclip.options.merge!({ 56 | :background_job_class => DelayedPaperclip::ProcessJob, 57 | :url_with_processing => true, 58 | :processing_image_url => nil 59 | }) 60 | end 61 | 62 | # In order to not duplicate code directly from Paperclip's spec support 63 | # We're requiring the MockInterpolator object to be used 64 | require Gem.find_files("../spec/support/mock_interpolator").first 65 | 66 | Dir["./spec/integration/examples/*.rb"].sort.each { |f| require f } 67 | 68 | # Reset table and class with image_processing column or not 69 | def reset_dummy(options = {}) 70 | options[:with_processed] = true unless options.key?(:with_processed) 71 | options[:processed_column] = options[:with_processed] unless options.has_key?(:processed_column) 72 | 73 | build_dummy_table(options.delete(:processed_column)) 74 | reset_class("Dummy", options) 75 | end 76 | 77 | # Dummy Table for images 78 | # with or without image_processing column 79 | def build_dummy_table(with_column) 80 | ActiveRecord::Base.connection.create_table :dummies, :force => true do |t| 81 | t.string :name 82 | t.string :image_file_name 83 | t.string :image_content_type 84 | t.integer :image_file_size 85 | t.datetime :image_updated_at 86 | t.boolean :hidden, :default => false 87 | t.boolean(:image_processing, :default => false) if with_column 88 | end 89 | end 90 | 91 | def reset_class(class_name, options) 92 | # setup class and include paperclip 93 | options[:paperclip] = {} if options[:paperclip].nil? 94 | ActiveRecord::Base.send(:include, Paperclip::Glue) 95 | Object.send(:remove_const, class_name) rescue nil 96 | 97 | # Set class as a constant 98 | klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) 99 | 100 | # Setup class with paperclip and delayed paperclip 101 | klass.class_eval do 102 | include Paperclip::Glue 103 | 104 | has_attached_file :image, options.delete(:paperclip) 105 | 106 | validates_attachment :image, :content_type => { :content_type => "image/png" } 107 | 108 | process_in_background :image, options if options[:with_processed] 109 | 110 | after_update :reprocess if options[:with_after_update_callback] 111 | 112 | default_scope options[:default_scope] if options[:default_scope] 113 | 114 | def reprocess 115 | image.reprocess! 116 | end 117 | end 118 | 119 | Rails.stubs(:root).returns(ROOT.join("tmp")) 120 | klass.reset_column_information 121 | klass 122 | end 123 | -------------------------------------------------------------------------------- /spec/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrgifford/delayed_paperclip/fcf574c1188213d3c8fce934fd52861a8ba080cb/spec/tmp/.keep --------------------------------------------------------------------------------