Repository: thoughtbot/paperclip
Branch: main
Commit: c769382c9b70
Files: 178
Total size: 647.5 KB
Directory structure:
gitextract_33jzlcve/
├── .codeclimate.yml
├── .github/
│ └── issue_template.md
├── .gitignore
├── .hound.yml
├── .rubocop.yml
├── .travis.yml
├── Appraisals
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── MIGRATING-ES.md
├── MIGRATING.md
├── NEWS
├── README.md
├── RELEASING.md
├── Rakefile
├── UPGRADING
├── features/
│ ├── basic_integration.feature
│ ├── migration.feature
│ ├── rake_tasks.feature
│ ├── step_definitions/
│ │ ├── attachment_steps.rb
│ │ ├── html_steps.rb
│ │ ├── rails_steps.rb
│ │ ├── s3_steps.rb
│ │ └── web_steps.rb
│ └── support/
│ ├── env.rb
│ ├── fakeweb.rb
│ ├── file_helpers.rb
│ ├── fixtures/
│ │ ├── boot_config.txt
│ │ ├── gemfile.txt
│ │ └── preinitializer.txt
│ ├── paths.rb
│ ├── rails.rb
│ └── selectors.rb
├── gemfiles/
│ ├── 4.2.gemfile
│ └── 5.0.gemfile
├── lib/
│ ├── generators/
│ │ └── paperclip/
│ │ ├── USAGE
│ │ ├── paperclip_generator.rb
│ │ └── templates/
│ │ └── paperclip_migration.rb.erb
│ ├── paperclip/
│ │ ├── attachment.rb
│ │ ├── attachment_registry.rb
│ │ ├── callbacks.rb
│ │ ├── content_type_detector.rb
│ │ ├── errors.rb
│ │ ├── file_command_content_type_detector.rb
│ │ ├── filename_cleaner.rb
│ │ ├── geometry.rb
│ │ ├── geometry_detector_factory.rb
│ │ ├── geometry_parser_factory.rb
│ │ ├── glue.rb
│ │ ├── has_attached_file.rb
│ │ ├── helpers.rb
│ │ ├── interpolations/
│ │ │ └── plural_cache.rb
│ │ ├── interpolations.rb
│ │ ├── io_adapters/
│ │ │ ├── abstract_adapter.rb
│ │ │ ├── attachment_adapter.rb
│ │ │ ├── data_uri_adapter.rb
│ │ │ ├── empty_string_adapter.rb
│ │ │ ├── file_adapter.rb
│ │ │ ├── http_url_proxy_adapter.rb
│ │ │ ├── identity_adapter.rb
│ │ │ ├── nil_adapter.rb
│ │ │ ├── registry.rb
│ │ │ ├── stringio_adapter.rb
│ │ │ ├── uploaded_file_adapter.rb
│ │ │ └── uri_adapter.rb
│ │ ├── locales/
│ │ │ └── en.yml
│ │ ├── logger.rb
│ │ ├── matchers/
│ │ │ ├── have_attached_file_matcher.rb
│ │ │ ├── validate_attachment_content_type_matcher.rb
│ │ │ ├── validate_attachment_presence_matcher.rb
│ │ │ └── validate_attachment_size_matcher.rb
│ │ ├── matchers.rb
│ │ ├── media_type_spoof_detector.rb
│ │ ├── missing_attachment_styles.rb
│ │ ├── processor.rb
│ │ ├── processor_helpers.rb
│ │ ├── rails_environment.rb
│ │ ├── railtie.rb
│ │ ├── schema.rb
│ │ ├── storage/
│ │ │ ├── filesystem.rb
│ │ │ ├── fog.rb
│ │ │ └── s3.rb
│ │ ├── storage.rb
│ │ ├── style.rb
│ │ ├── tempfile.rb
│ │ ├── tempfile_factory.rb
│ │ ├── thumbnail.rb
│ │ ├── url_generator.rb
│ │ ├── validators/
│ │ │ ├── attachment_content_type_validator.rb
│ │ │ ├── attachment_file_name_validator.rb
│ │ │ ├── attachment_file_type_ignorance_validator.rb
│ │ │ ├── attachment_presence_validator.rb
│ │ │ ├── attachment_size_validator.rb
│ │ │ └── media_type_spoof_detection_validator.rb
│ │ ├── validators.rb
│ │ └── version.rb
│ ├── paperclip.rb
│ └── tasks/
│ └── paperclip.rake
├── paperclip.gemspec
├── shoulda_macros/
│ └── paperclip.rb
└── spec/
├── database.yml
├── paperclip/
│ ├── attachment_definitions_spec.rb
│ ├── attachment_processing_spec.rb
│ ├── attachment_registry_spec.rb
│ ├── attachment_spec.rb
│ ├── content_type_detector_spec.rb
│ ├── file_command_content_type_detector_spec.rb
│ ├── filename_cleaner_spec.rb
│ ├── geometry_detector_spec.rb
│ ├── geometry_parser_spec.rb
│ ├── geometry_spec.rb
│ ├── glue_spec.rb
│ ├── has_attached_file_spec.rb
│ ├── integration_spec.rb
│ ├── interpolations_spec.rb
│ ├── io_adapters/
│ │ ├── abstract_adapter_spec.rb
│ │ ├── attachment_adapter_spec.rb
│ │ ├── data_uri_adapter_spec.rb
│ │ ├── empty_string_adapter_spec.rb
│ │ ├── file_adapter_spec.rb
│ │ ├── http_url_proxy_adapter_spec.rb
│ │ ├── identity_adapter_spec.rb
│ │ ├── nil_adapter_spec.rb
│ │ ├── registry_spec.rb
│ │ ├── stringio_adapter_spec.rb
│ │ ├── uploaded_file_adapter_spec.rb
│ │ └── uri_adapter_spec.rb
│ ├── matchers/
│ │ ├── have_attached_file_matcher_spec.rb
│ │ ├── validate_attachment_content_type_matcher_spec.rb
│ │ ├── validate_attachment_presence_matcher_spec.rb
│ │ └── validate_attachment_size_matcher_spec.rb
│ ├── media_type_spoof_detector_spec.rb
│ ├── meta_class_spec.rb
│ ├── paperclip_missing_attachment_styles_spec.rb
│ ├── paperclip_spec.rb
│ ├── plural_cache_spec.rb
│ ├── processor_helpers_spec.rb
│ ├── processor_spec.rb
│ ├── rails_environment_spec.rb
│ ├── rake_spec.rb
│ ├── schema_spec.rb
│ ├── storage/
│ │ ├── filesystem_spec.rb
│ │ ├── fog_spec.rb
│ │ ├── s3_live_spec.rb
│ │ └── s3_spec.rb
│ ├── style_spec.rb
│ ├── tempfile_factory_spec.rb
│ ├── tempfile_spec.rb
│ ├── thumbnail_spec.rb
│ ├── url_generator_spec.rb
│ ├── validators/
│ │ ├── attachment_content_type_validator_spec.rb
│ │ ├── attachment_file_name_validator_spec.rb
│ │ ├── attachment_presence_validator_spec.rb
│ │ ├── attachment_size_validator_spec.rb
│ │ └── media_type_spoof_detection_validator_spec.rb
│ └── validators_spec.rb
├── spec_helper.rb
└── support/
├── assertions.rb
├── fake_model.rb
├── fake_rails.rb
├── fixtures/
│ ├── animated
│ ├── animated.unknown
│ ├── empty.html
│ ├── empty.xlsx
│ ├── fog.yml
│ ├── s3.yml
│ └── text.txt
├── matchers/
│ ├── accept.rb
│ ├── exist.rb
│ └── have_column.rb
├── mock_attachment.rb
├── mock_interpolator.rb
├── mock_url_generator_builder.rb
├── model_reconstruction.rb
├── reporting.rb
├── test_data.rb
└── version_helper.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeclimate.yml
================================================
---
engines:
duplication:
enabled: true
config:
languages:
- ruby
fixme:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- "**.rb"
exclude_paths:
- features/
- spec/
================================================
FILE: .github/issue_template.md
================================================
## Deprecation notice
Paperclip is currently undergoing [deprecation in favor of ActiveStorage](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md). Maintainers of this repository will no longer be tending to new issues. We're leaving the issues page open so Paperclip users can still see & search through old issues, and continue existing discussions if they wish.
================================================
FILE: .gitignore
================================================
*~
*.swp
.rvmrc
.bundle
tmp
.DS_Store
*.log
public
paperclip*.gem
capybara*.html
*.rbc
.rbx
*SPIKE*
*emfile.lock
tags
================================================
FILE: .hound.yml
================================================
AllCops:
Include:
- "**/*.gemspec"
- "**/*.podspec"
- "**/*.jbuilder"
- "**/*.rake"
- "**/*.opal"
- "**/Gemfile"
- "**/Rakefile"
- "**/Capfile"
- "**/Guardfile"
- "**/Podfile"
- "**/Thorfile"
- "**/Vagrantfile"
- "**/Berksfile"
- "**/Cheffile"
- "**/Vagabondfile"
Exclude:
- "vendor/**/*"
- "db/schema.rb"
- 'vendor/**/*'
- 'gemfiles/vendor/**/*'
Rails:
Enabled: false
DisplayCopNames: false
StyleGuideCopsOnly: false
Style/AccessModifierIndentation:
Description: Check indentation of private/protected visibility modifiers.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#indent-public-private-protected
Enabled: true
EnforcedStyle: indent
SupportedStyles:
- outdent
- indent
Style/AlignHash:
Description: Align the elements of a hash literal if they span more than one line.
Enabled: true
EnforcedHashRocketStyle: key
EnforcedColonStyle: key
EnforcedLastArgumentHashStyle: always_inspect
SupportedLastArgumentHashStyles:
- always_inspect
- always_ignore
- ignore_implicit
- ignore_explicit
Style/AlignParameters:
Description: Align the parameters of a method call if they span more than one line.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-double-indent
Enabled: true
EnforcedStyle: with_first_parameter
SupportedStyles:
- with_first_parameter
- with_fixed_indentation
Style/AndOr:
Description: Use &&/|| instead of and/or.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-and-or-or
Enabled: true
EnforcedStyle: always
SupportedStyles:
- always
- conditionals
Style/BarePercentLiterals:
Description: Checks if usage of %() or %Q() matches configuration.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-q-shorthand
Enabled: true
EnforcedStyle: bare_percent
SupportedStyles:
- percent_q
- bare_percent
Style/BracesAroundHashParameters:
Description: Enforce braces style around hash parameters.
Enabled: true
EnforcedStyle: no_braces
SupportedStyles:
- braces
- no_braces
- context_dependent
Style/CaseIndentation:
Description: Indentation of when in a case/when/[else/]end.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#indent-when-to-case
Enabled: true
IndentWhenRelativeTo: case
SupportedStyles:
- case
- end
IndentOneStep: false
Style/ClassAndModuleChildren:
Description: Checks style of children classes and modules.
Enabled: false
EnforcedStyle: nested
SupportedStyles:
- nested
- compact
Style/ClassCheck:
Description: Enforces consistent use of `Object#is_a?` or `Object#kind_of?`.
Enabled: true
EnforcedStyle: is_a?
SupportedStyles:
- is_a?
- kind_of?
Style/CollectionMethods:
Description: Preferred collection methods.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size
Enabled: true
PreferredMethods:
collect: map
collect!: map!
find: detect
find_all: select
reduce: inject
Style/CommentAnnotation:
Description: Checks formatting of special comments (TODO, FIXME, OPTIMIZE, HACK,
REVIEW).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#annotate-keywords
Enabled: false
Keywords:
- TODO
- FIXME
- OPTIMIZE
- HACK
- REVIEW
Style/DotPosition:
Description: Checks the position of the dot in multi-line method calls.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains
Enabled: true
EnforcedStyle: trailing
SupportedStyles:
- leading
- trailing
Style/EmptyLineBetweenDefs:
Description: Use empty lines between defs.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#empty-lines-between-methods
Enabled: true
AllowAdjacentOneLineDefs: false
Style/EmptyLinesAroundBlockBody:
Description: Keeps track of empty lines around block bodies.
Enabled: true
EnforcedStyle: no_empty_lines
SupportedStyles:
- empty_lines
- no_empty_lines
Style/EmptyLinesAroundClassBody:
Description: Keeps track of empty lines around class bodies.
Enabled: true
EnforcedStyle: no_empty_lines
SupportedStyles:
- empty_lines
- no_empty_lines
Style/EmptyLinesAroundModuleBody:
Description: Keeps track of empty lines around module bodies.
Enabled: true
EnforcedStyle: no_empty_lines
SupportedStyles:
- empty_lines
- no_empty_lines
Style/Encoding:
Description: Use UTF-8 as the source file encoding.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#utf-8
Enabled: false
EnforcedStyle: always
SupportedStyles:
- when_needed
- always
Style/FileName:
Description: Use snake_case for source file names.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files
Enabled: false
Exclude: []
Style/FirstParameterIndentation:
Description: Checks the indentation of the first parameter in a method call.
Enabled: true
EnforcedStyle: special_for_inner_method_call_in_parentheses
SupportedStyles:
- consistent
- special_for_inner_method_call
- special_for_inner_method_call_in_parentheses
Style/For:
Description: Checks use of for or each in multiline loops.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-for-loops
Enabled: true
EnforcedStyle: each
SupportedStyles:
- for
- each
Style/FormatString:
Description: Enforce the use of Kernel#sprintf, Kernel#format or String#%.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#sprintf
Enabled: false
EnforcedStyle: format
SupportedStyles:
- format
- sprintf
- percent
Style/GlobalVars:
Description: Do not introduce global variables.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#instance-vars
Enabled: false
AllowedVariables: []
Style/GuardClause:
Description: Check for conditionals that can be replaced with guard clauses
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals
Enabled: false
MinBodyLength: 1
Style/HashSyntax:
Description: 'Prefer Ruby 1.9 hash syntax { a: 1, b: 2 } over 1.8 syntax { :a =>
1, :b => 2 }.'
StyleGuide: https://github.com/bbatsov/ruby-style-guide#hash-literals
Enabled: true
EnforcedStyle: ruby19
SupportedStyles:
- ruby19
- hash_rockets
Style/IfUnlessModifier:
Description: Favor modifier if/unless usage when you have a single-line body.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier
Enabled: false
MaxLineLength: 80
Style/IndentationWidth:
Description: Use 2 spaces for indentation.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-indentation
Enabled: true
Width: 2
Style/IndentHash:
Description: Checks the indentation of the first key in a hash literal.
Enabled: true
EnforcedStyle: special_inside_parentheses
SupportedStyles:
- special_inside_parentheses
- consistent
Style/LambdaCall:
Description: Use lambda.call(...) instead of lambda.(...).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#proc-call
Enabled: false
EnforcedStyle: call
SupportedStyles:
- call
- braces
Style/Next:
Description: Use `next` to skip iteration instead of a condition at the end.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals
Enabled: false
EnforcedStyle: skip_modifier_ifs
MinBodyLength: 3
SupportedStyles:
- skip_modifier_ifs
- always
Style/NonNilCheck:
Description: Checks for redundant nil checks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks
Enabled: true
IncludeSemanticChanges: false
Style/MethodDefParentheses:
Description: Checks if the method definitions have or don't have parentheses.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#method-parens
Enabled: true
EnforcedStyle: require_parentheses
SupportedStyles:
- require_parentheses
- require_no_parentheses
Style/MethodName:
Description: Use the configured style when naming methods.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars
Enabled: true
EnforcedStyle: snake_case
SupportedStyles:
- snake_case
- camelCase
Style/MultilineOperationIndentation:
Description: Checks indentation of binary operations that span more than one line.
Enabled: true
EnforcedStyle: aligned
SupportedStyles:
- aligned
- indented
Style/NumericLiterals:
Description: Add underscores to large numeric literals to improve their readability.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics
Enabled: false
MinDigits: 5
Style/ParenthesesAroundCondition:
Description: Don't use parentheses around the condition of an if/unless/while.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-parens-if
Enabled: true
AllowSafeAssignment: true
Style/PercentLiteralDelimiters:
Description: Use `%`-literal delimiters consistently
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces
Enabled: false
PreferredDelimiters:
"%": "()"
"%i": "()"
"%q": "()"
"%Q": "()"
"%r": "{}"
"%s": "()"
"%w": "()"
"%W": "()"
"%x": "()"
Style/PercentQLiterals:
Description: Checks if uses of %Q/%q match the configured preference.
Enabled: true
EnforcedStyle: lower_case_q
SupportedStyles:
- lower_case_q
- upper_case_q
Style/PredicateName:
Description: Check the names of predicate methods.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark
Enabled: true
NamePrefix:
- is_
- has_
- have_
NamePrefixBlacklist:
- is_
Style/RaiseArgs:
Description: Checks the arguments passed to raise/fail.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages
Enabled: false
EnforcedStyle: exploded
SupportedStyles:
- compact
- exploded
Style/RedundantReturn:
Description: Don't use return where it's not required.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-explicit-return
Enabled: true
AllowMultipleReturnValues: false
Style/RegexpLiteral:
Description: Use %r for regular expressions matching more than `MaxSlashes` '/'
characters. Use %r only for regular expressions matching more than `MaxSlashes`
'/' character.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-r
Enabled: false
MaxSlashes: 1
Style/Semicolon:
Description: Don't use semicolons to terminate expressions.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-semicolon
Enabled: true
AllowAsExpressionSeparator: false
Style/SignalException:
Description: Checks for proper usage of fail and raise.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method
Enabled: false
EnforcedStyle: semantic
SupportedStyles:
- only_raise
- only_fail
- semantic
Style/SingleLineBlockParams:
Description: Enforces the names of some block params.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks
Enabled: false
Methods:
- reduce:
- a
- e
- inject:
- a
- e
Style/SingleLineMethods:
Description: Avoid single-line methods.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods
Enabled: false
AllowIfMethodIsEmpty: true
Style/StringLiterals:
Description: Checks if uses of quotes match the configured preference.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals
Enabled: true
EnforcedStyle: double_quotes
SupportedStyles:
- single_quotes
- double_quotes
Style/StringLiteralsInInterpolation:
Description: Checks if uses of quotes inside expressions in interpolated strings
match the configured preference.
Enabled: true
EnforcedStyle: single_quotes
SupportedStyles:
- single_quotes
- double_quotes
Style/SpaceAroundBlockParameters:
Description: Checks the spacing inside and after block parameters pipes.
Enabled: true
EnforcedStyleInsidePipes: no_space
SupportedStyles:
- space
- no_space
Style/SpaceAroundEqualsInParameterDefault:
Description: Checks that the equals signs in parameter default assignments have
or don't have surrounding space depending on configuration.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-around-equals
Enabled: true
EnforcedStyle: space
SupportedStyles:
- space
- no_space
Style/SpaceBeforeBlockBraces:
Description: Checks that the left block brace has or doesn't have space before it.
Enabled: true
EnforcedStyle: space
SupportedStyles:
- space
- no_space
Style/SpaceInsideBlockBraces:
Description: Checks that block braces have or don't have surrounding space. For
blocks taking parameters, checks that the left brace has or doesn't have trailing
space.
Enabled: true
EnforcedStyle: space
SupportedStyles:
- space
- no_space
EnforcedStyleForEmptyBraces: no_space
SpaceBeforeBlockParameters: true
Style/SpaceInsideHashLiteralBraces:
Description: Use spaces inside hash literal braces - or don't.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-operators
Enabled: true
EnforcedStyle: space
EnforcedStyleForEmptyBraces: no_space
SupportedStyles:
- space
- no_space
Style/SymbolProc:
Description: Use symbols as procs instead of blocks when possible.
Enabled: true
IgnoredMethods:
- respond_to
Style/TrailingBlankLines:
Description: Checks trailing blank lines and final newline.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#newline-eof
Enabled: true
EnforcedStyle: final_newline
SupportedStyles:
- final_newline
- final_blank_line
Style/TrailingCommaInLiteral:
Description: Checks for trailing comma in parameter lists and literals.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas
Enabled: false
EnforcedStyleForMultiline: no_comma
SupportedStyles:
- comma
- no_comma
Style/TrivialAccessors:
Description: Prefer attr_* methods to trivial readers/writers.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#attr_family
Enabled: false
ExactNameMatch: false
AllowPredicates: false
AllowDSLWriters: false
Whitelist:
- to_ary
- to_a
- to_c
- to_enum
- to_h
- to_hash
- to_i
- to_int
- to_io
- to_open
- to_path
- to_proc
- to_r
- to_regexp
- to_str
- to_s
- to_sym
Style/VariableName:
Description: Use the configured style when naming variables.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-symbols-methods-vars
Enabled: true
EnforcedStyle: snake_case
SupportedStyles:
- snake_case
- camelCase
Style/WhileUntilModifier:
Description: Favor modifier while/until usage when you have a single-line body.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier
Enabled: false
MaxLineLength: 80
Style/WordArray:
Description: Use %w or %W for arrays of words.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-w
Enabled: false
MinSize: 0
WordRegex: !ruby/regexp /\A[\p{Word}]+\z/
Metrics/AbcSize:
Description: A calculated magnitude based on number of assignments, branches, and
conditions.
Enabled: true
Max: 15
Metrics/BlockNesting:
Description: Avoid excessive block nesting
StyleGuide: https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count
Enabled: false
Max: 3
Metrics/ClassLength:
Description: Avoid classes longer than 100 lines of code.
Enabled: false
CountComments: false
Max: 100
Metrics/CyclomaticComplexity:
Description: A complexity metric that is strongly correlated to the number of test
cases needed to validate a method.
Enabled: false
Max: 6
Metrics/LineLength:
Description: Limit lines to 80 characters.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#80-character-limits
Enabled: true
Max: 80
AllowURI: true
URISchemes:
- http
- https
Metrics/MethodLength:
Description: Avoid methods longer than 10 lines of code.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods
Enabled: false
CountComments: false
Max: 10
Metrics/ParameterLists:
Description: Avoid parameter lists longer than three or four parameters.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params
Enabled: false
Max: 5
CountKeywordArgs: true
Metrics/PerceivedComplexity:
Description: A complexity metric geared towards measuring complexity for a human
reader.
Enabled: false
Max: 7
Lint/AssignmentInCondition:
Description: Don't use assignment in conditions.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition
Enabled: false
AllowSafeAssignment: true
Lint/EndAlignment:
Description: Align ends correctly.
Enabled: true
AlignWith: keyword
SupportedStyles:
- keyword
- variable
Lint/DefEndAlignment:
Description: Align ends corresponding to defs correctly.
Enabled: true
AlignWith: start_of_line
SupportedStyles:
- start_of_line
- def
Rails/ActionFilter:
Description: Enforces consistent use of action filter methods.
Enabled: false
EnforcedStyle: action
SupportedStyles:
- action
- filter
Include:
- app/controllers/**/*.rb
Rails/HasAndBelongsToMany:
Description: Prefer has_many :through to has_and_belongs_to_many.
Enabled: true
Include:
- app/models/**/*.rb
Rails/Output:
Description: Checks for calls to puts, print, etc.
Enabled: true
Include:
- app/**/*.rb
- config/**/*.rb
- db/**/*.rb
- lib/**/*.rb
Rails/ReadWriteAttribute:
Description: Checks for read_attribute(:attr) and write_attribute(:attr, val).
Enabled: true
Include:
- app/models/**/*.rb
Rails/ScopeArgs:
Description: Checks the arguments of ActiveRecord scopes.
Enabled: true
Include:
- app/models/**/*.rb
Rails/Validation:
Description: Use validates :attribute, hash of validations.
Enabled: true
Include:
- app/models/**/*.rb
Style/InlineComment:
Description: Avoid inline comments.
Enabled: false
Style/MethodCalledOnDoEndBlock:
Description: Avoid chaining a method call on a do...end block.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#single-line-blocks
Enabled: false
Style/SymbolArray:
Description: Use %i or %I for arrays of symbols.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-i
Enabled: false
Style/ExtraSpacing:
Description: Do not use unnecessary spacing.
Enabled: true
Style/AccessorMethodName:
Description: Check the naming of accessor methods for get_/set_.
Enabled: false
Style/Alias:
Description: Use alias_method instead of alias.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method
Enabled: false
Style/AlignArray:
Description: Align the elements of an array literal if they span more than one line.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays
Enabled: true
Style/ArrayJoin:
Description: Use Array#join instead of Array#*.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#array-join
Enabled: false
Style/AsciiComments:
Description: Use only ascii symbols in comments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#english-comments
Enabled: false
Style/AsciiIdentifiers:
Description: Use only ascii symbols in identifiers.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#english-identifiers
Enabled: false
Style/Attr:
Description: Checks for uses of Module#attr.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#attr
Enabled: false
Style/BeginBlock:
Description: Avoid the use of BEGIN blocks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-BEGIN-blocks
Enabled: true
Style/BlockComments:
Description: Do not use block comments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-block-comments
Enabled: true
Style/BlockEndNewline:
Description: Put end statement of multiline block on its own line.
Enabled: true
Style/Blocks:
Description: Avoid using {...} for multi-line blocks (multiline chaining is always
ugly). Prefer {...} over do...end for single-line blocks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#single-line-blocks
Enabled: true
Style/CaseEquality:
Description: Avoid explicit use of the case equality operator(===).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-case-equality
Enabled: false
Style/CharacterLiteral:
Description: Checks for uses of character literals.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-character-literals
Enabled: false
Style/ClassAndModuleCamelCase:
Description: Use CamelCase for classes and modules.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#camelcase-classes
Enabled: true
Style/ClassMethods:
Description: Use self when defining module/class methods.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#def-self-singletons
Enabled: true
Style/ClassVars:
Description: Avoid the use of class variables.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-class-vars
Enabled: false
Style/ColonMethodCall:
Description: 'Do not use :: for method call.'
StyleGuide: https://github.com/bbatsov/ruby-style-guide#double-colons
Enabled: false
Style/CommentIndentation:
Description: Indentation of comments.
Enabled: true
Style/ConstantName:
Description: Constants should use SCREAMING_SNAKE_CASE.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#screaming-snake-case
Enabled: true
Style/DefWithParentheses:
Description: Use def with parentheses when there are arguments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#method-parens
Enabled: true
Style/Documentation:
Description: Document classes and non-namespace modules.
Enabled: false
Style/DoubleNegation:
Description: Checks for uses of double negation (!!).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang
Enabled: false
Style/EachWithObject:
Description: Prefer `each_with_object` over `inject` or `reduce`.
Enabled: false
Style/ElseAlignment:
Description: Align elses and elsifs correctly.
Enabled: true
Style/EmptyElse:
Description: Avoid empty else-clauses.
Enabled: true
Style/EmptyLines:
Description: Don't use several empty lines in a row.
Enabled: true
Style/EmptyLinesAroundAccessModifier:
Description: Keep blank lines around access modifiers.
Enabled: true
Style/EmptyLinesAroundMethodBody:
Description: Keeps track of empty lines around method bodies.
Enabled: true
Style/EmptyLiteral:
Description: Prefer literals to Array.new/Hash.new/String.new.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash
Enabled: false
Style/EndBlock:
Description: Avoid the use of END blocks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-END-blocks
Enabled: true
Style/EndOfLine:
Description: Use Unix-style line endings.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#crlf
Enabled: true
Style/EvenOdd:
Description: Favor the use of Fixnum#even? && Fixnum#odd?
StyleGuide: https://github.com/bbatsov/ruby-style-guide#predicate-methods
Enabled: false
Style/FlipFlop:
Description: Checks for flip flops
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-flip-flops
Enabled: false
Style/IfWithSemicolon:
Description: Do not use if x; .... Use the ternary operator instead.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs
Enabled: false
Style/IndentationConsistency:
Description: Keep indentation straight.
Enabled: true
Style/IndentArray:
Description: Checks the indentation of the first element in an array literal.
Enabled: true
Style/InfiniteLoop:
Description: Use Kernel#loop for infinite loops.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#infinite-loop
Enabled: true
Style/Lambda:
Description: Use the new lambda literal syntax for single-line blocks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#lambda-multi-line
Enabled: false
Style/LeadingCommentSpace:
Description: Comments should start with a space.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#hash-space
Enabled: true
Style/LineEndConcatenation:
Description: Use \ instead of + or << to concatenate two string literals at line
end.
Enabled: false
Style/MethodCallParentheses:
Description: Do not use parentheses for method calls with no arguments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-args-no-parens
Enabled: true
Style/ModuleFunction:
Description: Checks for usage of `extend self` in modules.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function
Enabled: false
Style/MultilineBlockChain:
Description: Avoid multi-line chains of blocks.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#single-line-blocks
Enabled: true
Style/MultilineBlockLayout:
Description: Ensures newlines after multiline block do statements.
Enabled: true
Style/MultilineIfThen:
Description: Do not use then for multi-line if/unless.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-then
Enabled: true
Style/MultilineTernaryOperator:
Description: 'Avoid multi-line ?: (the ternary operator); use if/unless instead.'
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-multiline-ternary
Enabled: true
Style/NegatedIf:
Description: Favor unless over if for negative conditions (or control flow or).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#unless-for-negatives
Enabled: false
Style/NegatedWhile:
Description: Favor until over while for negative conditions.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#until-for-negatives
Enabled: false
Style/NestedTernaryOperator:
Description: Use one expression per branch in a ternary operator.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-ternary
Enabled: true
Style/NilComparison:
Description: Prefer x.nil? to x == nil.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#predicate-methods
Enabled: false
Style/Not:
Description: Use ! instead of not.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#bang-not-not
Enabled: false
Style/OneLineConditional:
Description: Favor the ternary operator(?:) over if/then/else/end constructs.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator
Enabled: false
Style/OpMethod:
Description: When defining binary operators, name the argument other.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#other-arg
Enabled: false
Style/PerlBackrefs:
Description: Avoid Perl-style regex back references.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers
Enabled: false
Style/Proc:
Description: Use proc instead of Proc.new.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#proc
Enabled: false
Style/RedundantBegin:
Description: Don't use begin blocks when they are not needed.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#begin-implicit
Enabled: true
Style/RedundantException:
Description: Checks for an obsolete RuntimeException argument in raise/fail.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-explicit-runtimeerror
Enabled: true
Style/RedundantSelf:
Description: Don't use self where it's not needed.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-self-unless-required
Enabled: true
Style/RescueModifier:
Description: Avoid using rescue in its modifier form.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-rescue-modifiers
Enabled: true
Style/SelfAssignment:
Description: Checks for places where self-assignment shorthand should have been
used.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#self-assignment
Enabled: false
Style/SpaceBeforeFirstArg:
Description: Checks that exactly one space is used between a method name and the
first argument for method calls without parentheses.
Enabled: true
Style/SpaceAfterColon:
Description: Use spaces after colons.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-operators
Enabled: true
Style/SpaceAfterComma:
Description: Use spaces after commas.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-operators
Enabled: true
Style/SpaceAroundKeyword:
Description: Use spaces after if/elsif/unless/while/until/case/when.
Enabled: true
Style/SpaceAfterMethodName:
Description: Do not put a space between a method name and the opening parenthesis
in a method definition.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#parens-no-spaces
Enabled: true
Style/SpaceAfterNot:
Description: Tracks redundant space after the ! operator.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-space-bang
Enabled: true
Style/SpaceAfterSemicolon:
Description: Use spaces after semicolons.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-operators
Enabled: true
Style/SpaceBeforeComma:
Description: No spaces before commas.
Enabled: true
Style/SpaceBeforeComment:
Description: Checks for missing space between code and a comment on the same line.
Enabled: true
Style/SpaceBeforeSemicolon:
Description: No spaces before semicolons.
Enabled: true
Style/SpaceAroundOperators:
Description: Use spaces around operators.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-operators
Enabled: true
Style/SpaceInsideBrackets:
Description: No spaces after [ or before ].
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-spaces-braces
Enabled: true
Style/SpaceInsideParens:
Description: No spaces after ( or before ).
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-spaces-braces
Enabled: true
Style/SpaceInsideRangeLiteral:
Description: No spaces inside range literals.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-space-inside-range-literals
Enabled: true
Style/SpecialGlobalVars:
Description: Avoid Perl-style global variables.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms
Enabled: false
Style/StructInheritance:
Description: Checks for inheritance from Struct.new.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-extend-struct-new
Enabled: true
Style/Tab:
Description: No hard tabs.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#spaces-indentation
Enabled: true
Style/TrailingWhitespace:
Description: Avoid trailing whitespace.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace
Enabled: true
Style/UnlessElse:
Description: Do not use unless with else. Rewrite these with the positive case first.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-else-with-unless
Enabled: true
Style/UnneededCapitalW:
Description: Checks for %W when interpolation is not needed.
Enabled: true
Style/UnneededPercentQ:
Description: Checks for %q/%Q when single quotes or double quotes would do.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-q
Enabled: true
Style/UnneededPercentX:
Description: Checks for %x when `` would do.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-x
Enabled: true
Style/VariableInterpolation:
Description: Don't interpolate global, instance and class variables directly in
strings.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate
Enabled: false
Style/WhenThen:
Description: Use when x then ... for one-line cases.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases
Enabled: false
Style/WhileUntilDo:
Description: Checks for redundant do after while or until.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-multiline-while-do
Enabled: true
Lint/AmbiguousOperator:
Description: Checks for ambiguous operators in the first argument of a method invocation
without parentheses.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#parens-as-args
Enabled: false
Lint/AmbiguousRegexpLiteral:
Description: Checks for ambiguous regexp literals in the first argument of a method
invocation without parenthesis.
Enabled: false
Lint/BlockAlignment:
Description: Align block ends correctly.
Enabled: true
Lint/ConditionPosition:
Description: Checks for condition placed in a confusing position relative to the
keyword.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#same-line-condition
Enabled: false
Lint/Debugger:
Description: Check for debugger calls.
Enabled: true
Lint/DeprecatedClassMethods:
Description: Check for deprecated class method calls.
Enabled: false
Lint/DuplicateMethods:
Description: Check for duplicate methods calls.
Enabled: true
Lint/ElseLayout:
Description: Check for odd code arrangement in an else block.
Enabled: false
Lint/EmptyEnsure:
Description: Checks for empty ensure block.
Enabled: true
Lint/EmptyInterpolation:
Description: Checks for empty string interpolation.
Enabled: true
Lint/EndInMethod:
Description: END blocks should not be placed inside method definitions.
Enabled: true
Lint/EnsureReturn:
Description: Do not use return in an ensure block.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-return-ensure
Enabled: true
Lint/Eval:
Description: The use of eval represents a serious security risk.
Enabled: true
Lint/HandleExceptions:
Description: Don't suppress exception.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions
Enabled: false
Lint/InvalidCharacterLiteral:
Description: Checks for invalid character literals with a non-escaped whitespace
character.
Enabled: false
Lint/LiteralInCondition:
Description: Checks of literals used in conditions.
Enabled: false
Lint/LiteralInInterpolation:
Description: Checks for literals used in interpolation.
Enabled: false
Lint/Loop:
Description: Use Kernel#loop with break rather than begin/end/until or begin/end/while
for post-loop tests.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#loop-with-break
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Description: Checks for method calls with a space before the opening parenthesis.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#parens-no-spaces
Enabled: false
Lint/RequireParentheses:
Description: Use parentheses in the method call to avoid confusion about precedence.
Enabled: false
Lint/RescueException:
Description: Avoid rescuing the Exception class.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-blind-rescues
Enabled: true
Lint/ShadowingOuterLocalVariable:
Description: Do not use the same name as outer local variable for block arguments
or block local variables.
Enabled: true
Lint/SpaceBeforeFirstArg:
Description: Put a space between a method name and the first argument in a method
call without parentheses.
Enabled: true
Lint/StringConversionInInterpolation:
Description: Checks for Object#to_s usage in string interpolation.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-to-s
Enabled: true
Lint/UnderscorePrefixedVariableName:
Description: Do not use prefix `_` for a variable that is used.
Enabled: false
Lint/UnusedBlockArgument:
Description: Checks for unused block arguments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars
Enabled: true
Lint/UnusedMethodArgument:
Description: Checks for unused method arguments.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars
Enabled: true
Lint/UnreachableCode:
Description: Unreachable code.
Enabled: true
Lint/UselessAccessModifier:
Description: Checks for useless access modifiers.
Enabled: true
Lint/UselessAssignment:
Description: Checks for useless assignment to a local variable.
StyleGuide: https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars
Enabled: true
Lint/UselessComparison:
Description: Checks for comparison of something with itself.
Enabled: true
Lint/UselessElseWithoutRescue:
Description: Checks for useless `else` in `begin..end` without `rescue`.
Enabled: true
Lint/UselessSetterCall:
Description: Checks for useless setter call to a local variable.
Enabled: true
Lint/Void:
Description: Possible use of operator/literal/variable in void context.
Enabled: false
Rails/Delegate:
Description: Prefer delegate method for delegations.
Enabled: false
================================================
FILE: .rubocop.yml
================================================
inherit_from: .hound.yml
================================================
FILE: .travis.yml
================================================
language: ruby
sudo: false
rvm:
- 2.1
- 2.2
- 2.3
- 2.4
script: "bundle exec rake clean spec cucumber"
addons:
apt:
packages:
- ghostscript
gemfile:
- gemfiles/4.2.gemfile
- gemfiles/5.0.gemfile
matrix:
fast_finish: true
exclude:
- gemfile: gemfiles/5.0.gemfile
rvm: 2.1
================================================
FILE: Appraisals
================================================
appraise "4.2" do
gem "rails", "~> 4.2.0"
end
appraise "5.0" do
gem "rails", "~> 5.0.0"
end
================================================
FILE: CONTRIBUTING.md
================================================
Contributing
============
We love pull requests from everyone. By participating in this project, you agree
to abide by the thoughtbot [code of conduct].
[code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
Here's a quick guide for contributing:
1. Fork the repo.
1. Make sure you have ImageMagick and Ghostscript installed. See [this section]
(./README.md#image-processor) of the README.
1. Run the tests. We only take pull requests with passing tests, and it's great
to know that you have a clean slate: `bundle && bundle exec rake`
1. Add a test for your change. Only refactoring and documentation changes
require no new tests. If you are adding functionality or fixing a bug, we need
a test!
1. Make the test pass.
1. Mention how your changes affect the project to other developers and users in
the `NEWS.md` file.
1. Push to your fork and submit a pull request.
At this point you're waiting on us. We like to at least comment on, if not
accept, pull requests within seven business days (most of the work on Paperclip
gets done on Fridays). We may suggest some changes or improvements or
alternatives.
Some things that will increase the chance that your pull request is accepted,
taken straight from the Ruby on Rails guide:
* Use Rails idioms and helpers
* Include tests that fail without your code, and pass with it
* Update the documentation, the surrounding one, examples elsewhere, guides,
whatever is affected by your contribution
Running Tests
-------------
Paperclip uses [Appraisal](https://github.com/thoughtbot/appraisal) to aid
testing against multiple version of Ruby on Rails. This helps us to make sure
that Paperclip performs correctly with them.
Paperclip also uses [RSpec](http://rspec.info) for its unit tests. If you submit
tests that are not written for Cucumber or RSpec without a very good reason, you
will be asked to rewrite them before we'll accept.
### Bootstrapping your test suite:
bundle install
bundle exec appraisal install
This will install all the required gems that requires to test against each
version of Rails, which defined in `gemfiles/*.gemfile`.
### To run a full test suite:
bundle exec appraisal rake
This will run RSpec and Cucumber against all version of Rails
### To run single Test::Unit or Cucumber test
You need to specify a `BUNDLE_GEMFILE` pointing to the gemfile before running
the normal test command:
BUNDLE_GEMFILE=gemfiles/4.1.gemfile rspec spec/paperclip/attachment_spec.rb
BUNDLE_GEMFILE=gemfiles/4.1.gemfile cucumber features/basic_integration.feature
Syntax
------
* Two spaces, no tabs.
* No trailing whitespace. Blank lines should not have any space.
* Prefer &&/|| over and/or.
* MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
* a = b and not a=b.
* Follow the conventions you see used in the source already.
And in case we didn't emphasize it enough: we love tests!
================================================
FILE: Gemfile
================================================
source "https://rubygems.org"
gemspec
gem 'sqlite3', '~> 1.3.8', :platforms => :ruby
gem 'pry'
# Hinting at development dependencies
# Prevents bundler from taking a long-time to resolve
group :development, :test do
gem 'activerecord-import'
gem 'mime-types'
gem 'builder'
gem 'rubocop', require: false
gem 'rspec'
end
================================================
FILE: LICENSE
================================================
LICENSE
The MIT License
Copyright (c) 2008-2016 Jon Yurek and thoughtbot, inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: MIGRATING-ES.md
================================================
# Migrando de Paperclip a ActiveStorage
Paperclip y ActiveStorage resuelven problemas similares con soluciones
similares, por lo que pasar de uno a otro es simple.
El proceso de ir desde Paperclip hacia ActiveStorage es como sigue:
1. Implementa las migraciones a la base de datos de ActiveStorage.
2. Configura el almacenamiento.
3. Copia la base de datos.
4. Copia los archivos.
5. Actualiza tus pruebas.
6. Actualiza tus vistas.
7. Actualiza tus controladores.
8. Actualiza tus modelos.
## Implementa las migraciones a la base de datos de ActiveStorage
Sigue [las instrucciones para instalar ActiveStorage]. Muy probablemente vas a
querer agregar la gema `mini_magick` a tu Gemfile.
```sh
rails active_storage:install
```
[las instrucciones para instalar ActiveStorage]: https://github.com/rails/rails/blob/master/activestorage/README.md#installation
## Configura el almacenamiento
De nuevo, sigue [las instrucciones para configurar ActiveStorage].
[las instrucciones para configurar ActiveStorage]: http://edgeguides.rubyonrails.org/active_storage_overview.html#setup
## Copia la base de datos.
Las tablas `active_storage_blobs` y`active_storage_attachments` son en donde
ActiveStorage espera encontrar los metadatos del archivo. Paperclip almacena los
metadatos del archivo directamente en en la tabla del objeto asociado.
Vas a necesitar escribir una migración para esta conversión. Proveer un script
simple, es complicado porque están involucrados tus modelos. ¡Pero lo
intentaremos!
Así sería para un `User` con un `avatar` en Paperclip:
```ruby
class User < ApplicationRecord
has_attached_file :avatar
end
```
Tus migraciones de Paperclip producirán una tabla como la siguiente:
```ruby
create_table "users", force: :cascade do |t|
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
end
```
Y tu la convertirás en estas tablas:
```ruby
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.integer "record_id", null: false
t.integer "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
```
```ruby
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
```
Así que asumiendo que quieres dejar los archivos en el mismo lugar, _esta es tu
migración_. De otra forma, ve la siguiente sección primero y modifica la
migración como corresponda.
```ruby
Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }
class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
require 'open-uri'
def up
# postgres
get_blob_id = 'LASTVAL()'
# mariadb
# get_blob_id = 'LAST_INSERT_ID()'
# sqlite
# get_blob_id = 'LAST_INSERT_ROWID()'
active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_blobs (
key, filename, content_type, metadata, byte_size,
checksum, created_at
) VALUES (?, ?, ?, '{}', ?, ?, ?)
SQL
active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
INSERT INTO active_storage_attachments (
name, record_type, record_id, blob_id, created_at
) VALUES (?, ?, ?, #{get_blob_id}, ?)
SQL
models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
transaction do
models.each do |model|
attachments = model.column_names.map do |c|
if c =~ /(.+)_file_name$/
$1
end
end.compact
model.find_each.each do |instance|
attachments.each do |attachment|
active_storage_blob_statement.execute(
key(instance, attachment),
instance.send("#{attachment}_file_name"),
instance.send("#{attachment}_content_type"),
instance.send("#{attachment}_file_size"),
checksum(instance.send(attachment)),
instance.updated_at.iso8601
)
active_storage_attachment_statement.
execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
end
end
end
end
active_storage_attachment_statement.close
active_storage_blob_statement.close
end
def down
raise ActiveRecord::IrreversibleMigration
end
private
def key(instance, attachment)
SecureRandom.uuid
# Alternativamente:
# instance.send("#{attachment}_file_name")
end
def checksum(attachment)
# archivos locales almacenados en disco:
url = attachment.path
Digest::MD5.base64digest(File.read(url))
# archivos remotos almacenados en la computadora de alguién más:
# url = attachment.url
# Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
end
end
```
## Copia los archivos
La migración de arriba deja los archivos como estaban. Sin embargo,
los servicios de Paperclip y ActiveStorage utilizan diferentes ubicaciones.
Por defecto, Paperclip se ve así:
```
public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
```
Y ActiveStorage se ve así:
```
storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
```
Ese `xMRXuT6nqpoiConJFQJFt6c9` es el valor de `active_storage_blobs.key`. En la
migración de arriba usamos simplemente el nombre del archivo, pero tal vez
quieras usar un UUID.
Migrando los archivos en un hospedaje externo (S3, Azure Storage, GCS, etc.)
está fuera del alcance de este documento inicial. Así es como se vería para un
almacenamiento local:
```ruby
#!bin/rails runner
class ActiveStorageBlob < ActiveRecord::Base
end
class ActiveStorageAttachment < ActiveRecord::Base
belongs_to :blob, class_name: 'ActiveStorageBlob'
belongs_to :record, polymorphic: true
end
ActiveStorageAttachment.find_each do |attachment|
name = attachment.name
source = attachment.record.send(name).path
dest_dir = File.join(
"storage",
attachment.blob.key.first(2),
attachment.blob.key.first(4).last(2))
dest = File.join(dest_dir, attachment.blob.key)
FileUtils.mkdir_p(dest_dir)
puts "Moving #{source} to #{dest}"
FileUtils.cp(source, dest)
end
```
## Actualiza tus pruebas
En lugar de utilizar `have_attached_file`, será necesario que escribas tu propio
matcher. Aquí hay un matcher similar _en espíritu_ al que Paperclip provee:
```ruby
RSpec::Matchers.define :have_attached_file do |name|
matches do |record|
file = record.send(name)
file.respond_to?(:variant) && file.respond_to?(:attach)
end
end
```
## Actualiza tus vistas
En Paperclip se ven así:
```ruby
image_tag @user.avatar.url(:medium)
```
En ActiveStorage se ven así:
```ruby
image_tag @user.avatar.variant(resize: "250x250")
```
## Actualiza tus controladores
Esto no debería _requerir_ ningúna actualización. Sin embargo, si te fijas en
el schema de tu base de datos, notaras un join.
Por ejemplo si tu controlador tiene:
```ruby
def index
@users = User.all.order(:name)
end
```
Y tu vista tiene:
```
<% @users.each do |user| %>
- <%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %>
<% end %>
```
Vas a terminar con un n+1, ya que descargas cada archivo adjunto dentro del
bucle.
Así que mientras que el controlador y el modelo funcionarán sin ningún cambio,
tal vez quieras revisar dos veces tus bucles y agregar `includes` en dónde haga
falta.
ActiveStorage agrega `avatar_attachment` y `avatar_blob` a las relaciones del
tipo `has-one`, así como `avatar_attachments` y `avatar_blobs` a las relaciones
de tipo `has-many`:
```ruby
def index
@users = User.all.order(:name).includes(:avatar_attachment)
end
```
## Actualiza tus modelos
Sigue [la guía sobre cómo adjuntar archivos a los registros]. Por ejemplo, un
`User` con un `avatar` se representa como:
```ruby
class User < ApplicationRecord
has_one_attached :avatar
end
```
Cualquier cambio de tamaño se hace en la vista como un `variant`.
[la guía sobre cómo adjuntar archivos a los registros]: http://edgeguides.rubyonrails.org/active_storage_overview.html#attaching-files-to-records
## Quita Paperclip
Quita la gema de tu `Gemfile` y corre `bundle`. Corre tus pruebas porque ya
terminaste!
================================================
FILE: MIGRATING.md
================================================
# Migrating from Paperclip to ActiveStorage
* [Video presentation](https://www.youtube.com/watch?v=tZ_WNUytO9o).
* [En español](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING-ES.md).
Paperclip and ActiveStorage solve similar problems with similar solutions, so
transitioning from one to the other is straightforward data re-writing.
The process of going from Paperclip to ActiveStorage is as follows:
1. Apply the ActiveStorage database migrations.
2. Configure storage.
3. Copy the database data over.
4. Copy the files over.
5. Update your tests.
6. Update your views.
7. Update your controllers.
8. Update your models.
## Apply the ActiveStorage database migrations
You'll very likely want to add the `mini_magick` gem to your Gemfile.
Make sure your `config/application.rb` requires the ActiveStorage engine:
```rb
# config/application.rb
require "active_storage/engine"
```
Then, follow [the instructions for installing ActiveStorage].
```sh
rails active_storage:install
```
[the instructions for installing ActiveStorage]: https://github.com/rails/rails/tree/5-2-stable/activestorage#installation
## Configure storage
Again, follow [the instructions for configuring ActiveStorage].
It's worth highlighting that, by default, ActiveStorage's
[`DiskService`][active-storage-service] will store files locally in
`Rails.root.join("storage")`. When storing files locally, Paperclip, by default,
writes to `Rails.root.join("public", "system")`.
Make sure to exclude your locally stored files from version control.
For instance, if you're using Git, add `storage/` to your `.gitignore`.
```diff
!.keep
/.bundle
/.byebug_history
/.tmp/*
/log/*
/public/system/
+ storage/
```
[the instructions for configuring ActiveStorage]: https://guides.rubyonrails.org/v5.2/active_storage_overview.html#setup
[active-storage-service]: https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Service.html
## Copy the database data over
The `active_storage_blobs` and `active_storage_attachments` tables are where
ActiveStorage expects to find file metadata. Paperclip stores the file metadata
directly on the associated object's table.
You'll need to write a migration for this conversion. Because the models for
your domain are involved, it's tricky to supply a simple script. But we'll try!
Here's how it would go for a `User` with an `avatar`, that is this in
Paperclip:
```ruby
class User < ApplicationRecord
has_attached_file :avatar
end
```
Your Paperclip migrations will produce a table like so:
```ruby
create_table "users", force: :cascade do |t|
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
end
```
And you'll be converting into these tables:
```ruby
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.integer "record_id", null: false
t.integer "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
```
```ruby
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
```
So, assuming you want to leave the files in the exact same place, _this is
your migration_. Otherwise, see the next section first and modify the migration
to taste.
```ruby
class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
require 'open-uri'
def up
# postgres
get_blob_id = 'LASTVAL()'
# mariadb
# get_blob_id = 'LAST_INSERT_ID()'
# sqlite
# get_blob_id = 'LAST_INSERT_ROWID()'
active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare('active_storage_blob_statement', <<-SQL)
INSERT INTO active_storage_blobs (
`key`, filename, content_type, metadata, byte_size, checksum, created_at
) VALUES ($1, $2, $3, '{}', $4, $5, $6)
SQL
active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare('active_storage_attachment_statement', <<-SQL)
INSERT INTO active_storage_attachments (
name, record_type, record_id, blob_id, created_at
) VALUES ($1, $2, $3, #{get_blob_id}, $4)
SQL
Rails.application.eager_load!
models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
transaction do
models.each do |model|
attachments = model.column_names.map do |c|
if c =~ /(.+)_file_name$/
$1
end
end.compact
if attachments.blank?
next
end
model.find_each.each do |instance|
attachments.each do |attachment|
if instance.send(attachment).path.blank?
next
end
ActiveRecord::Base.connection.execute_prepared(
'active_storage_blob_statement', [
key(instance, attachment),
instance.send("#{attachment}_file_name"),
instance.send("#{attachment}_content_type"),
instance.send("#{attachment}_file_size"),
checksum(instance.send(attachment)),
instance.updated_at.iso8601
])
ActiveRecord::Base.connection.execute_prepared(
'active_storage_attachment_statement', [
attachment,
model.name,
instance.id,
instance.updated_at.iso8601,
])
end
end
end
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
private
def key(instance, attachment)
SecureRandom.uuid
# Alternatively:
# instance.send("#{attachment}_file_name")
end
def checksum(attachment)
# local files stored on disk:
url = attachment.path
Digest::MD5.base64digest(File.read(url))
# remote files stored on another person's computer:
# url = attachment.url
# Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
end
end
```
## Copy the files over
The above migration leaves the files as they are. However, the default
Paperclip and ActiveStorage storage services use different locations.
By default, Paperclip looks like this:
```
public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
```
And ActiveStorage looks like this:
```
storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
```
That `xMRXuT6nqpoiConJFQJFt6c9` is the `active_storage_blobs.key` value. In the
migration above we simply used the filename but you may wish to use a UUID
instead.
### Moving local storage files
```ruby
#!bin/rails runner
ActiveStorage::Attachment.find_each do |attachment|
name = attachment.name
source = attachment.record.send(name).path
dest_dir = File.join(
"storage",
attachment.blob.key.first(2),
attachment.blob.key.first(4).last(2))
dest = File.join(dest_dir, attachment.blob.key)
FileUtils.mkdir_p(dest_dir)
puts "Moving #{source} to #{dest}"
FileUtils.cp(source, dest)
end
```
### Moving files on a remote host (S3, Azure Storage, GCS, etc.)
One of the most straightforward ways to move assets stored on a remote host is
to use a rake task that regenerates the file names and places them in the
proper file structure/hierarchy.
Assuming you have a model configured similarly to the example below:
```ruby
class Organization < ApplicationRecord
# New ActiveStorage declaration
has_one_attached :logo
# Old Paperclip config
# must be removed BEFORE to running the rake task so that
# all of the new ActiveStorage goodness can be used when
# calling organization.logo
has_attached_file :logo,
path: "/organizations/:id/:basename_:style.:extension",
default_url: "https://s3.amazonaws.com/xxxxx/organizations/missing_:style.jpg",
default_style: :normal,
styles: { thumb: "64x64#", normal: "400x400>" },
convert_options: { thumb: "-quality 100 -strip", normal: "-quality 75 -strip" }
end
```
The following rake task would migrate all of your assets:
```ruby
namespace :organizations do
task migrate_to_active_storage: :environment do
Organization.where.not(logo_file_name: nil).find_each do |organization|
# This step helps us catch any attachments we might have uploaded that
# don't have an explicit file extension in the filename
image = organization.logo_file_name
ext = File.extname(image)
image_original = CGI.unescape(image.gsub(ext, "_original#{ext}"))
# this url pattern can be changed to reflect whatever service you use
logo_url = "https://s3.amazonaws.com/xxxxx/organizations/#{organization.id}/#{image_original}"
organization.logo.attach(io: open(logo_url),
filename: organization.logo_file_name,
content_type: organization.logo_content_type)
end
end
end
```
An added advantage of this method is that you're creating a copy of all assets,
which is handy in the event you need to rollback your deploy.
This also means that you can run the rake task from your development machine
and completely migrate the assets before your deploy, minimizing the chances
that you'll have a timed-out deployment.
The main drawback of this method is the same as its benefit - you are
essentially duplicating all of your assets. These days storage and bandwidth
are relatively cheap, but in some instances where you have a huge volume of
files, or very large file sizes, this might get a little less feasible.
In my experience I was able to move tens of thousands of images in a matter of
a couple of hours, just by running the migration overnight on my MacBook Pro.
Once you've confirmed that the migration and deploy have gone successfully you
can safely delete the old assets from your remote host.
## Update your tests
Instead of the `have_attached_file` matcher, you'll need to write your own.
Here's one that is similar in spirit to the Paperclip-supplied matcher:
```ruby
RSpec::Matchers.define :have_attached_file do |name|
match do |record|
file = record.send(name)
file.respond_to?(:variant) && file.respond_to?(:attach)
end
end
```
If you were using a Factory or a Fixture that set the Paperclip-generated
columns' values directly, you'll likely need to attach the Files instead.
For example, you could replace a `FactoryBot` factory definition's Paperclip
attributes with File I/O using
[`ActiveSupport::Testing::FixtureFiles#file_fixture`][file-fixture]:
```diff
factory :user do
trait :with_avatar do
- avatar_file_name { "avatar.jpg" }
- avatar_file_type { "image/jpg" }
- avatar_file_size { 1024 }
+ transient do
+ avatar_file { file_fixture("avatar.jpg") }
+
+ after :build do |user, evaluator|
+ user.avatar.attach(
+ io: evaluator.avatar_file.open,
+ filename: evaluator.avatar_file.basename.to_s,
+ )
+ end
+ end
end
end
```
[file-fixture]: https://api.rubyonrails.org/v5.2/classes/ActiveSupport/Testing/FileFixtures.html
## Update your views
In Paperclip it looks like this:
```ruby
image_tag @user.avatar.url(:medium)
```
In ActiveStorage it looks like this:
```ruby
image_tag @user.avatar.variant(resize: "250x250")
```
## Update your controllers
This should _require_ no update. However, if you glance back at the database
schema above, you may notice a join.
For example, if your controller has
```ruby
def index
@users = User.all.order(:name)
end
```
And your view has
```
<% @users.each do |user| %>
- <%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %>
<% end %>
```
Then you'll end up with an n+1 as you load each attachment in the loop.
So while the controller and model will work without change, you will want to
double-check your loops and add `includes` as needed.
ActiveStorage automatically declares `ActiveStorage::Attachment` and
`ActiveStorage::Blob` relationships to your models, along with eager-loading
scopes.
For example, a `has_one_attached :avatar` declaration will generate a `has_one
:avatar_attachment` relationship along with a
[`.with_attached_avatar`][has-one-eager-loading-scope] scope for eager loading
attachments and blobs.
A `has_many_attached :avatars` declaration will generate a `has_many
:avatar_attachments` relationship along with a
[`.with_attached_avatars`][has-many-eager-loading-scope] scope for eager loading
attachments and blobs.
When eager-loading transitive relationships, you'll need to specify the
relationship names directly, like `includes(avatar_attachment: :blob)` or
`includes(avatar_attachments: :blob)`:
```ruby
def index
@users = User.all.order(:name).includes(avatar_attachment: :blob)
end
```
[has-one-eager-loading-scope]: https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Attached/Macros.html#method-i-has_one_attached
[has-many-eager-loading-scope]: https://api.rubyonrails.org/v5.2/classes/ActiveStorage/Attached/Macros.html#method-i-has_many_attached
## Update your models
Follow [the guide on attaching files to records]. For example, a `User` with an
`avatar` is represented as:
```ruby
class User < ApplicationRecord
has_one_attached :avatar
end
```
Any resizing is done in the view as a variant.
[the guide on attaching files to records]: https://guides.rubyonrails.org/v5.2/active_storage_overview.html#attaching-files-to-records
### Validations
Unlike Paperclip, [which shipped with built-in attachment
validations][paperclip-validations], ActiveStorage does not have built-in support
for validating an attachment's content type or file size (which can be useful for
[preventing content type spoofing][security-validations]).
There are alternatives that support some of Paperclip's file validations.
For instance, here are some changes you could make to migrate a
Paperclip-enabled model to use validations provided by the [`file_validators`
gem][file-validators]:
```diff
class User < ApplicationRecord
# ...
- validates_attachment_content_type :avatar, content_type: /\Aimage/
- validates_attachment_file_name :avatar, matches: /jpe?g\z/
+ validates :avatar, file_content_type: {
+ allow: ["image/jpeg", "image/png"],
+ if: -> { avatar.attached? },
+ }
```
[paperclip-validations]: https://github.com/thoughtbot/paperclip/tree/v6.1.0#validations
[security-validations]: https://github.com/thoughtbot/paperclip/tree/v6.1.0#security-validations
[file-validators]: https://github.com/musaffa/file_validators/tree/v2.3.0#examples
## Remove Paperclip
Make sure to delete any files Paperclip was storing locally. You can also update
your version control to no longer ignore the directory.
For instance, if you're using Git, remove `public/system/` from your
`.gitignore`.
```diff
!.keep
/.bundle
/.byebug_history
/.tmp/*
/log/*
- /public/system/
storage/
```
Remove the Gem from your `Gemfile` and run `bundle`. Run your tests because
you're done!
================================================
FILE: NEWS
================================================
6.1.0 (2018-07-27):
* BUGFIX: Don't double-encode URLs (Roderick Monje).
* BUGFIX: Only use the content_type when it exists (Jean-Philippe Doyle).
* STABILITY: Better handling of the content-disposition header. Now supports
file name that is either enclosed or not in double quotes and is case
insensitive as per RC6266 grammar (Hasan Kumar, Yves Riel).
* STABILITY: Change database column type of attachment file size from unsigned 4-byte
`integer` to unsigned 8-byte `bigint`. The former type limits attachment size
to just over 2GB, which can easily be exceeded by a large video file (Laurent
Arnoud, Alen Zamanyan).
* STABILITY: Better error message when thumbnail processing errors (Hayden Ball).
* STABILITY: Fix file linking issues around Windows (Akihiko Odaki).
* STABILITY: Files without an extension will now be checked for spoofing attempts
(George Walters II).
* STABILITY: Manually close Tempfiles when we are done with them (Erkki Eilonen).
6.0.0 (2018-03-09):
* Improvement: Depend only on `aws-sdk-s3` instead of `aws-sdk` (https://github.com/thoughtbot/paperclip/pull/2481)
5.3.0 (2018-03-09):
* Improvement: Use `FactoryBot` instead of `FactoryGirl` (https://github.com/thoughtbot/paperclip/pull/2501)
* Improvement: README updates (https://github.com/thoughtbot/paperclip/pull/2411, https://github.com/thoughtbot/paperclip/pull/2433, https://github.com/thoughtbot/paperclip/pull/2374, https://github.com/thoughtbot/paperclip/pull/2417, https://github.com/thoughtbot/paperclip/pull/2536)
* Improvement: Remove Ruby 2.4 deprecation warning (https://github.com/thoughtbot/paperclip/pull/2401)
* Improvement: Rails 5 migration compatibility (https://github.com/thoughtbot/paperclip/pull/2470)
* Improvement: Documentation around post processing (https://github.com/thoughtbot/paperclip/pull/2381)
* Improvement: S3 hostname example documentation (https://github.com/thoughtbot/paperclip/pull/2379)
* Bugfix: Allow paperclip to load in IRB (https://github.com/thoughtbot/paperclip/pull/2369)
* Bugfix: MIME type detection (https://github.com/thoughtbot/paperclip/issues/2527)
* Bugfix: Bad tempfile state after symlink failure (https://github.com/thoughtbot/paperclip/pull/2540)
* Bugfix: Rewind file after Fog bucket creation (https://github.com/thoughtbot/paperclip/pull/2572)
* Improvement: Use `Terrapin` instead of `Cocaine` (https://github.com/thoughtbot/paperclip/pull/2553)
5.2.1 (2018-01-25):
* Bugfix: Fix copying files on Windows. (#2532)
5.2.0 (2018-01-23):
* Security: Remove the automatic loading of URI adapters. Some of these
adapters can be specially crafted to expose your network topology. (#2435)
* Bugfix: The rake task no longer rescues `Exception`. (#2476)
* Bugfix: Handle malformed `Content-Disposition` headers (#2283)
* Bugfix: The `:only_process` option works when passed a lambda again. (#2289)
* Improvement: Added `:use_accelerate_endpoint` option when using S3 to enable
[Amazon S3 Transfer Acceleration](http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html)
(#2291)
* Improvement: Make the fingerprint digest configurable per attachment. The
default remains MD5. Making this configurable means it can change in a future
version because it is not considered secure anymore against intentional file
corruption. For more info, see https://en.wikipedia.org/wiki/MD5#Security
You can change the digest used for an attachment by adding the
`:adapter_options` parameter to the `has_attached_file` options like this:
`has_attached_file :avatar, adapter_options: { hash_digest: Digest::SHA256 }`
Use the rake task to regenerate fingerprints with the new digest for a given
class. Note that this does **not** check the file integrity using the old
fingerprint. Run the following command to regenerate fingerprints for all
User attachments:
`CLASS=User rake paperclip:refresh:fingerprints`
You can optionally limit the attachment that will be processed, e.g:
`CLASS=User ATTACHMENT=avatar rake paperclip:refresh:fingerprints` (#2229)
* Improvement: The new `frame_index` option on the thumbnail processor allows
you to select a specific frame from an animated upload to use as a thumbnail.
Initial support is for mkv, avi, MP4, mov, MPEG, and GIF. (#2155)
* Improvement: Instead of copying files, use hard links. This is an
optimization. (#2120)
* Improvement: S3 storage option `:s3_prefixes_in_alias`. (#2287)
* Improvement: Fog option `:fog_public` can be a lambda. (#2302)
* Improvement: One fewer warning on JRuby. (#2352)
* Ruby 2.4.0 compatibility (doesn't use Fixnum anymore)
5.1.0 (2016-08-19):
* Add default `content_type_detector` to `UploadedFileAdapter` (#2270)
* Default S3 protocol to empty string (#2038)
* Don't write original file if it wasn't reprocessed (#1993)
* Disallow trailing newlines in regular expressions (#2266)
* Support for readbyte in Paperclip attachments (#2034)
* (port from 4.3) Uri io adapter uses the content-disposition filename (#2250)
* General refactors and documentation improvements
5.0.0 (2016-07-01):
* Improvement: Add `read_timeout` configuration for URI Adapter download_content method.
* README adjustments for Ruby beginners (add links, elucidate model in Quick Start)
* Bugfix: Now it's possible to save images from URLs with special characters [#1932]
* Bugfix: Return false when file to copy is not present in cloud storage [#2173]
* Automatically close file while checking mime type [#2016]
* Add `read_timeout` option to `UriAdapter#download_content` method [#2232]
* Fix a nil error in content type validation matcher [#1910]
* Documentation improvements
5.0.0.beta2 (2016-04-01):
* Bugfix: Dynamic fog directory option is now respected
* Bugfix: Fixes cocaine duplicated paths [#2169]
* Removal of dead code (older versions of Rails and AWS SDK)
* README adjustments
5.0.0.beta1 (2016-03-13):
* Bug Fix: megabytes of mime-types info in logs when a spoofed media type is detected.
* Drop support to end-of-life'd ruby 2.0.
* Drop support for end-of-life'd Rails 3.2 and 4.1
* Drop support for AWS v1
* Remove tests for JRuby and Rubinius from Travis CI (they were failing)
* Improvement: Add `fog_options` configuration to send options to fog when
storing files.
* Extracted repository for locales only: https://github.com/thoughtbot/paperclip-i18n
* Bugfix: Original file could be unlinked during `post_process_style`, producing failures
* Bugfix for image magick scaling images up
* Memory consumption improvements
* `url` on a unpersisted record returns `default_url` rather than `nil`
* Improvement: aws-sdk v2 support
https://github.com/thoughtbot/paperclip/pull/1903
If your Gemfile contains aws-sdk (>= 2.0.0) and aws-sdk-v1, paperclip will use
aws-sdk v2. With aws-sdk v2, S3 storage requires you to set the s3_region.
s3_region may be nested in s3_credentials, and (if not nested in
s3_credentials) it may be a Proc.
4.3
See patch versions in v4.3 NEWS:
https://github.com/thoughtbot/paperclip/blob/v4.3/NEWS
4.3.0 (2015-06-18):
* Improvement: Update aws-sdk and cucumber gem versions.
* Improvement: Add `length` alias for `size` method in AbstractAdapter.
* Improvement: Removed some cruft
* Improvement: deep_merge! Attachment definitions
* Improvement: Switch to mimemagic gem for content-type detection
* Improvement: Allows multiple content types for spoof detector
* Bug Fix: Don't assume we have Rails.env if we have Rails
* Performance: Decrease Memory footprint
* Ruby Versioning: Drop support for 1.9.3 (EOL'ed)
* Rails Versioning: Drop support for 4.0.0 (EOL'ed)
4.2.4 (2015-06-05):
* Rollback backwards incompatible change, allowing paperclip to run on
Ruby >= 1.9.2.
4.2.3:
* Fix dependency specifications (didn't work with Rails 4.1)
* Fix paperclip tests in CI
4.2.2:
* Security fix: Fix a potential security issue with spoofing
4.2.1:
* Improvement: Added `validate_media_type` options to allow/bypass spoof check
* Improvement: Added incremental backoff when AWS gives us a SlowDown error.
* Improvement: Stream downloads when usign aws-sdk.
* Improvement: Documentation fixes, includes Windows instructions.
* Improvement: Added pt-BR, zh-HK, zh-CN, zh-TW, and ja-JP locales.
* Improvement: Better escaping for characters in URLs
* Improvement: Honor `fog_credentials[:scheme]`
* Improvement: Also look for custom processors in lib/paperclip
* Improvement: id partitioning for string IDs works like integer id
* Improvement: Can pass options to DB adapters in migrations
* Improvement: Update expiring_url creation for later versions of fog
* Improvement: `path` can be a Proc in S3 attachments
* Test Fix: Improves speed and reliability of the specs
* Bug Fix: #original_filename= does not error when passed `nil`
4.2.0:
* Improvement: Converted test suite from test/unit to RSpec
* Improvement: Refactored Paperclip::Attachment#assign
* Improvement: Added Spanish and German locales
* Improvement: Required Validators accept validator subclasses
* Improvement: EXIF orientation checking can be turned off for performance
* Improvement: Documentation updates
* Improvement: Better #human_size method for AttachmentSizeValidators
* Bug Fix: Allow MIME-types with dots in them
* Improvement: Travis CI updates
* Improvement: Validators can take multiple messages
* Improvement: Per-style options for S3 storage
* Improvement: Allow `nil` geometry strings
* Improvement: Use `eager_load!`
4.1.1:
* Improvement: Add default translations for spoof validation
* Bug Fix: Don't check for spoofs if the file hasn't changed
* Bug Fix: Callback chain terminator is different in Rails 4.1, remove warnings
* Improvement: Fixed various Ruby warnings
* Bug Fix: Give bundler a hint, so it doesn't run forever on a fresh bundle
* Improvement: Documentation fixes
* Improvement: Allow travis-ci to finish-fast
4.1.0:
* Improvement: Add :content_type_mappings to correct for missing spoof types
* Improvement: Credit Egor Homakov with discovering the content_type spoof bug
* Improvement: Memoize calls to identify in the thumbnail processor
* Improvement: Make MIME type optional for Data URIs.
* Improvement: Add default format for styles
4.0.0:
* Security: Attachments are checked to make sure they're not pulling a fast one.
* Security: It is now *enforced* that every attachment has a file/mime validation.
* Bug Fix: Removed a call to IOAdapter#close that was causing issues.
* Improvement: Added bullets to the 3.5.3 list of changes. Very important.
* Improvement: Updated the copyright to 2014
3.5.3:
* Improvement: After three long, hard years... we know how to upgrade
* Bug Fix: #expiring_url returns 'missing' urls if nothing is attached
* Improvement: Lots of documentation fixes
* Improvement: Lots of fixes for Ruby warnings
* Improvement: Test the most appropriate Ruby/Rails comobinations on Travis
* Improvement: Delegate more IO methods through IOAdapters
* Improvement: Remove Rails 4 deprecations
* Improvement: Both S3's and Fog's #expiring_url can take a Time or Int
* Bug Fix: Both S3's and Fog's expiring_url respect style when missing the file
* Bug Fix: Timefiles will have a reasonable-length name. They're all MD5 hashes now
* Bug Fix: Don't delete files off S3 when reprocessing due to AWS inconsistencies
* Bug Fix: "swallow_stream" isn't thread dafe. Use :swallow_stderr
* Improvement: Regexps use \A and \Z instead of ^ and $
* Improvement: :s3_credentials can take a lambda as an argument
* Improvement: Search up the class heirarchy for attachments
* Improvement: deep_merge options instead of regular merge
* Bug Fix: Prevent file deletion on transaction rollback
* Test Improvement: Ensure more files are properly closed during tests
* Test Bug Fix: Return the gemfile's syntax to normal
3.5.2:
* Security: Force cocaine to at least 0.5.3 to include a security fix
* Improvement: Fixed some README exmaples
* Feature: Added HTTP URL Proxy Adapter, can assign string URLs as attachments
* Improvement: Put validation errors on the base attribute and the sub-attribute
3.5.1:
* Bug Fix: Returned the class-level `attachment_definitions` method for compatability.
* Improvement: Ensured compatability with Rails 4
* Improvement: Added Rails 4 to the Appraisals
* Bug Fix: #1296, where validations were generating errors
* Improvement: Specify MIT license in the gemspec
3.5.0:
* Feature: Handle Base64-encoded data URIs as uploads
* Feature: Add a FilenameCleaner class to allow custom filename sanitation
* Improvement: Satisfied Mocha deprecation warnings
* Bug Fix: Allow empty string to be submitted and ignored, as some forms do this
* Improvement: Make #expiring_url behavior consistent with #url
* Bug Fix: "Validate" attachments without invoking AR's validations
* Improvement: Various refactorings for a cleaner codebase
* Improvement: Be agnostic, use ActiveModel when appropriate
* Improvement: Add validation errors to the base attachment attribute
* Improvement: Handle errors in rake tasks
* Improvement: Largely refactor has_attached_file into a new class
* Improvement: Added Ruby 2.0.0 as a supported platform and removed 1.8.7
* Improvement: Fixed some incompatabilities in the test suite
3.4.2:
* Improvement: Use https for Gemfile urls
* Improvement: Updated and more correct documentation
* Improvement: Use the -optimize flag on animated GIFs
* Improvement: Remove the Gemfile.lock
* Improvement: Add #expiring_url as an alias for #url until the storage defines it
* Improvement: Remove path clash checking, as it's unnecessary
* Bug Fix: Do not rely on checking version numbers for aws-sdk
3.4.1:
* Improvement: Various documentation fixes and improvements
* Bug Fix: Clearing an attachment with `preserve_files` on should still clear the attachment
* Bug Fix: Instances are #changed? when a new file is assigned
* Bug Fix: Correctly deal with S3 styles when using a lambda
* Improvement: Accept and pass :credential_provider option to AWS-SDK
* Bug Fix: Sanitize original_filename more correctly in IO Adapters
* Improvement: s3_host_name can be a lambda
* Improvement: Cache some interpolations for speed
* Improvement: Update to latest cocaine
* Improvement: Update copyrights, various typos
3.4.0:
* Bug Fix: Allow UploadedFileAdapter to force the use of `file`
* Bug Fix: Close the file handle when dealing with URIs
* Bug Fix: Ensure files are closed for writing when we're done.
* Bug Fix: Fixed 'type' being nil on Windows 7 error.
* Bug Fix: Fixed nil access when no s3 headers are defined
* Bug Fix: Fixes auto_orientation
* Bug Fix: Prevent a missing method error when switching from aws_sdk to fog
* Bug Fix: Properly fail to process invalid attachments
* Bug Fix: Server-side encryption is specified correctly
* Bug Fix: fog_public returned to true by default
* Bug Fix: Check attachment paths for duplicates, not URLs
* Feature: Add Attachment#blank?
* Feature: Add support for blacklisting certain content_types
* Feature: Add support for style-specific s3 headers and meta data
* Feature: Allow only_process to be a lambda
* Feature: Allow setting of escape url as a default option
* Feature: Create :override_file_permissions option for filesystem attachments
* Improvement: Add Attachment#as_json
* Improvement: Evaluate lambdas for fog_file properties
* Improvement: Extract geometry parsing into factories
* Improvement: Fixed various typos
* Improvement: Refactored some tests
* Improvement: Reuse S3 connections
New In 3.3.1:
* Bug Fix: Moved Filesystem's copy_to_local_file to the right place.
3.3.0:
* Improvement: Upgrade cocaine to 0.4
3.2.0:
* Bug Fix: Use the new correct Amazon S3 encryption header.
* Bug Fix: The rake task respects the updated_at column.
* Bug Fix: Strip newline from content type.
* Feature: Fog file visibility can be specified per style.
* Feature: Automatically rotate images.
* Feature: Reduce class-oriented programming of the attachment definitions.
3.1.4:
* Bug Fix: Allow user to be able to set path without `:style` attribute and not raising an error.
This is a regression introduced in 3.1.3, and that feature will be postponed to another minor
release instead.
* Feature: Allow for URI Adapter as an optional paperclip io adapter.
3.1.3:
* Bug Fix: Copy empty attachment between instances is now working.
* Bug Fix: Correctly rescue Fog error.
* Bug Fix: Using default path and url options in Fog storage now work as expected.
* Bug Fix: `Attachment#s3_protocol` now returns a protocol without colon suffix.
* Feature: Paperclip will now raise an error if multiple styles are defined but no `:style`
interpolation exists in `:path`.
* Feature: Add support for `#{attachment}_created_at` field
* Bug Fix: Paperclip now gracefully handles msising file command.
* Bug Fix: `StringIOAdapter` now accepts content type.
3.1.2:
* Bug Fix: #remove_attachment on 3.1.0 and 3.1.1 mistakenly trying to remove the column that has
the same name as data type (such as :string, :datetime, :interger.) You're advised to update to
Paperclip 3.1.2 as soon as possible.
3.1.1:
* Bug Fix: Paperclip will only load Paperclip::Schema only when Active Record is available.
3.1.0:
* Feature: Paperclip now support new migration syntax (sexy migration) that reads better:
class AddAttachmentToUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.attachment :avatar
end
end
end
Also, schema-definition level syntax has been added:
add_attachment :users, :avatar
remove_attachment :users, :avatar
* Feature: Migration now support Rails 3.2+ `change` method.
* API CHANGE: Old `t.has_attached_file` and `drop_attached_file` are now deprecated. You're advised
to update your migration file before the next MAJOR version.
* Bug Fix: Tempfile now rewinded before generating fingerprint
* API CHANGE: Tempfiles are now unlinked after `after_flush_writes`
If you need to interact with the generated tempfiles, please define an `after_flush_writes` method
in your model. You'll be able to access files via `@queue_for_write` instance variable.
* Bug Fix: `:s3_protocol` can now be defined as either String or Symbol
* Bug Fix: Tempfiles are now rewinded before get passed into `after_flush_writes`
* Feature: Added expiring_url method to Fog Storage
* API CHANGE: Paperclip now tested against AWS::SDK 1.5.2 onward
* Bug Fix: Improved the output of the content_type validator so the actual failure is displayed
* Feature: Animated formats now identified using ImageMagick.
* Feature: AttachmentAdapter now support fetching attachment with specific style.
* Feature: Paperclip default options can now be configured in Rails.configuration.
* Feature: add Geometry#resize_to to calculate dimensions of new source.
* Bug Fix: Fixed a bug whereby a file type with multiple mime types but no official type would cause
the best_content_type to throw an error on trying nil.content_type.
* Bug Fix: Fix problem when the gem cannot be installed on the system that has Asepsis installed.
3.0.4:
* Feature: Adds support for S3 scheme-less URL generation.
3.0.3:
* Bug Fix: ThumbnailProcessor now correctly detects and preserve animated GIF.
* Bug Fix: File extension is now preserved in generated Tempfile from adapter.
* Bug Fix: Uploading file with unicode file name now won't raise an error when
logging in the AWS is turned on.
* Bug Fix: Task "paperclip:refresh:missing_styles" now work correctly.
* Bug Fix: Handle the case when :restricted_characters is nil.
* Bug Fix: Don't delete all the existing styles if we reprocess.
* Bug Fix: Content type is now ensured to not having a new line character.
* API CHANGE: Non-Rails usage should include Paperclip::Glue directly.
`Paperclip::Railtie` was intended to be used with Ruby on Rails only. If you're
using Paperclip without Rails, you should include `Paperclip::Glue` into
`ActiveRecord::Base` instead of requiring `paperclip/railtie`:
ActiveRecord::Base.send :include, Paperclip::Glue
* Bug Fix: AttachmentContentTypeValidator now allow you to specify :allow_blank/:allow_nil
* Bug Fix: Make sure content type always a String.
* Bug Fix: Fix attachment.reprocess! when using storage providers fog and s3.
* Bug Fix: Fix a problem with incorrect content_type detected with 'file' command for an empty file on Mac.
3.0.2:
* API CHANGE: Generated migration class name is now plural (AddAttachmentToUsers instead of AddAttachmentToUser)
* API CHANGE: Remove Rails plugin initialization code.
* API CHANGE: Explicitly require Ruby 1.9.2 in the Gemfile.
* Bug Fix: Fixes AWS::S3::Errors::RequestTimeout on Model#save.
* Bug Fix: Fix a problem when there's no logger specified.
* Bug Fix: Fix a problem when attaching Rack::Test::UploadedFile instance.
3.0.1:
* Feature: Introduce Paperlip IO adapter.
* Bug Fix: Regression in AttachmentContentTypeValidator has been fixed.
* API CHANGE: #to_file has been removed. Use the #copy_to_local_file method instead.
3.0.0:
* API CHANGE: Paperclip now requires at least Ruby on Rails version 3.0.0
* API CHANGE: The default :url and :path have changed. The new scheme avoids
filesystem conflicts and scales to handle larger numbers of uploads.
The easiest way to upgrade is to add an explicit :url and :path to your
has_attached_file calls:
has_attached_file :avatar,
:path => ":rails_root/public/system/:attachment/:id/:style/:filename",
:url => "/system/:attachment/:id/:style/:filename"
* Feature: Adding Rails 3 style validators, and adding `validates_attachment` method as a shorthand.
* Bug Fix: Paperclip's rake tasks now loading records in batch.
* Bug Fix: Attachment style name with leading number now not raising an error.
* Bug Fix: File given to S3 and Fog storage will now be rewinded after flush_write.
* Feature: You can now pass addional parameter to S3 expiring URL, such as :content_type.
2.7.0:
* Bug Fix: Checking the existence of a file on S3 handles all AWS errors.
* Bug Fix: Clear the fingerprint when removing an attachment.
* Bug Fix: Attachment size validation message reads more nicely now.
* Feature: Style names can be either symbols or strings.
* Compatibility: Support for ActiveSupport < 2.3.12.
* Compatibility: Support for Rails 3.2.
2.6.0:
* Bug Fix: Files are re-wound after reading.
* Feature: Remove Rails dependency from specs that need Paperclip.
* Feature: Validation matchers support conditionals.
2.5.2:
* Bug Fix: Can be installed on Windows.
* Feature: The Fog bucket name, authentication, and host can be determined at runtime via Proc.
* Feature: Special characters are replaced with underscores in #url and #path.
2.5.1:
* Feature: After we've computed the content type, pass it to Fog.
* Feature: S3 encryption with the new :s3_server_side_encryption option.
* Feature: Works without ActiveRecord, allowing for e.g. mongo backends.
2.5.0:
* Performance: Only connect to S3 when absolutely needed.
* Bug Fix: STI with cached classes respect new options.
* Bug Fix: conditional validations broke, and now work again.
* Feature: URL generation is now parameterized and can be changed with plugins or custom code.
* Feature: :convert_options and :source_file_options to control the ImageMagick processing.
* Performance: String geometry specifications now parse more quickly.
* Bug Fix: Handle files with question marks in the filename.
* Bug Fix: Don't raise an error when generating an expiring URL on an unassigned attachment.
* Bug Fix: The rake task runs over all instances of an ActiveRecord model, ignoring default scopes.
* Feature: DB migration has_attached_file and drop_attached_file methods.
* Bug Fix: Switch from AWS::S3 to AWS::SDK for the S3 backend.
* Bug Fix: URL generator uses '?' in the URL unless it already appears and there is no prior '='.
* Bug Fix: Always convert the content type to a string before stripping blanks.
* Feature: The :keep_old_files option preserves the files in storage even when the attachment is cleared or changed.
* Performance: Optimize Fog's public_url access by avoiding it when possible.
* Bug Fix: Avoid a runtime error when generating the ID partition for an unsaved attachment.
* Performance: Do not calculate the fingerprint if it is never persisted.
* Bug Fix: Process the :original style before all others, in case of a dependency.
* Feature: S3 headers can be set at runtime by passing a proc object as the value.
* Bug Fix: Generating missing attachment styles for a model which has had its attachment changed should not raise.
* Bug Fix: Do not collide with the built-in Ruby hashing method.
================================================
FILE: README.md
================================================
Paperclip
=========
# Deprecated
**[Paperclip is deprecated]**.
For new projects, we recommend Rails' own [ActiveStorage].
For existing projects, please consult and contribute to the migration guide,
available [in English], [en español], and as [a video] recorded at RailsConf
2019. You may also prefer [an alternative migration tutorial used by
Doorkeeper][].
Alternatively, for existing projects, [Kreeti] is maintaining [kt-paperclip],
an ongoing [fork of Paperclip].
We will leave the Issues open as a discussion forum _only_. We do _not_
guarantee a response from us in the Issues. All bug reports should go to
kt-paperclip.
We are no longer accepting pull requests _except_ pull requests against the
migration guide. All other pull requests will be closed without merging.
[Paperclip is deprecated]: https://robots.thoughtbot.com/closing-the-trombone
[ActiveStorage]: http://guides.rubyonrails.org/active_storage_overview.html
[in English]: https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md
[en español]: https://github.com/thoughtbot/paperclip/blob/master/MIGRATING-ES.md
[a video]: https://www.youtube.com/watch?v=tZ_WNUytO9o
[Kreeti]: https://www.kreeti.com/
[kt-paperclip]: https://rubygems.org/gems/kt-paperclip
[fork of Paperclip]: https://github.com/kreeti/kt-paperclip
[an alternative migration tutorial used by Doorkeeper]: https://www.tokyodev.com/2021/03/23/paperclip-activestorage/
# Existing documentation
## Documentation valid for `master` branch
Please check the documentation for the paperclip version you are using:
https://github.com/thoughtbot/paperclip/releases
---
[](http://travis-ci.org/thoughtbot/paperclip)
[](https://gemnasium.com/thoughtbot/paperclip)
[](https://codeclimate.com/github/thoughtbot/paperclip)
[](http://inch-ci.org/github/thoughtbot/paperclip)
[](https://hakiri.io/github/thoughtbot/paperclip/master)
- [Requirements](#requirements)
- [Ruby and Rails](#ruby-and-rails)
- [Image Processor](#image-processor)
- [`file`](#file)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Models](#models)
- [Migrations](#migrations)
- [Edit and New Views](#edit-and-new-views)
- [Edit and New Views with Simple Form](#edit-and-new-views-with-simple-form)
- [Controller](#controller)
- [View Helpers](#view-helpers)
- [Checking a File Exists](#checking-a-file-exists)
- [Deleting an Attachment](#deleting-an-attachment)
- [Usage](#usage)
- [Validations](#validations)
- [Internationalization (I18n)](#internationalization-i18n)
- [Security Validations](#security-validations)
- [Defaults](#defaults)
- [Migrations](#migrations-1)
- [Add Attachment Column To A Table](#add-attachment-column-to-a-table)
- [Schema Definition](#schema-definition)
- [Vintage Syntax](#vintage-syntax)
- [Storage](#storage)
- [Understanding Storage](#understanding-storage)
- [IO Adapters](#io-adapters)
- [Post Processing](#post-processing)
- [Custom Attachment Processors](#custom-attachment-processors)
- [Events](#events)
- [URI Obfuscation](#uri-obfuscation)
- [Checksum / Fingerprint](#checksum--fingerprint)
- [File Preservation for Soft-Delete](#file-preservation-for-soft-delete)
- [Dynamic Configuration](#dynamic-configuration)
- [Dynamic Styles:](#dynamic-styles)
- [Dynamic Processors:](#dynamic-processors)
- [Logging](#logging)
- [Deployment](#deployment)
- [Attachment Styles](#attachment-styles)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
- [About thoughtbot](#about-thoughtbot)
Paperclip is intended as an easy file attachment library for ActiveRecord. The
intent behind it was to keep setup as easy as possible and to treat files as
much like other attributes as possible. This means they aren't saved to their
final locations on disk, nor are they deleted if set to nil, until
ActiveRecord::Base#save is called. It manages validations based on size and
presence, if required. It can transform its assigned image into thumbnails if
needed, and the prerequisites are as simple as installing ImageMagick (which,
for most modern Unix-based systems, is as easy as installing the right
packages). Attached files are saved to the filesystem and referenced in the
browser by an easily understandable specification, which has sensible and
useful defaults.
See the documentation for `has_attached_file` in [`Paperclip::ClassMethods`](http://www.rubydoc.info/gems/paperclip/Paperclip/ClassMethods) for
more detailed options.
The complete [RDoc](http://www.rubydoc.info/gems/paperclip) is online.
---
Requirements
------------
### Ruby and Rails
Paperclip now requires Ruby version **>= 2.1** and Rails version **>= 4.2**
(only if you're going to use Paperclip with Ruby on Rails).
### Image Processor
[ImageMagick](http://www.imagemagick.org) must be installed and Paperclip must have access to it. To ensure
that it does, on your command line, run `which convert` (one of the ImageMagick
utilities). This will give you the path where that utility is installed. For
example, it might return `/usr/local/bin/convert`.
Then, in your environment config file, let Paperclip know to look there by adding that
directory to its path.
In development mode, you might add this line to `config/environments/development.rb)`:
```ruby
Paperclip.options[:command_path] = "/usr/local/bin/"
```
If you're on Mac OS X, you'll want to run the following with [Homebrew](http://www.brew.sh):
brew install imagemagick
If you are dealing with pdf uploads or running the test suite, you'll also need
to install GhostScript. On Mac OS X, you can also install that using Homebrew:
brew install gs
If you are on Ubuntu (or any Debian base Linux distribution), you'll want to run
the following with apt-get:
sudo apt-get install imagemagick -y
### `file`
The Unix [`file` command](https://en.wikipedia.org/wiki/File_(command)) is required for content-type checking.
This utility isn't available in Windows, but comes bundled with Ruby [Devkit](https://github.com/oneclick/rubyinstaller/wiki/Development-Kit),
so Windows users must make sure that the devkit is installed and added to the system `PATH`.
**Manual Installation**
If you're using Windows 7+ as a development environment, you may need to install the `file.exe` application manually. The `file spoofing` system in Paperclip 4+ relies on this; if you don't have it working, you'll receive `Validation failed: Upload file has an extension that does not match its contents.` errors.
To manually install, you should perform the following:
> **Download & install `file` from [this URL](http://gnuwin32.sourceforge.net/packages/file.htm)**
To test, you can use the image below:

Next, you need to integrate with your environment - preferably through the `PATH` variable, or by changing your `config/environments/development.rb` file
**PATH**
1. Click "Start"
2. On "Computer", right-click and select "Properties"
3. In Properties, select "Advanced System Settings"
4. Click the "Environment Variables" button
5. Locate the "PATH" var - at the end, add the path to your newly installed `file.exe` (typically `C:\Program Files (x86)\GnuWin32\bin`)
6. Restart any CMD shells you have open & see if it works
OR
**Environment**
1. Open `config/environments/development.rb`
2. Add the following line: `Paperclip.options[:command_path] = 'C:\Program Files (x86)\GnuWin32\bin'`
3. Restart your Rails server
Either of these methods will give your Rails setup access to the `file.exe` functionality, thus providing the ability to check the contents of a file (fixing the spoofing problem)
---
Installation
------------
Paperclip is distributed as a gem, which is how it should be used in your app.
Include the gem in your Gemfile:
```ruby
gem "paperclip", "~> 6.0.0"
```
Or, if you want to get the latest, you can get master from the main paperclip repository:
```ruby
gem "paperclip", git: "git://github.com/thoughtbot/paperclip.git"
```
If you're trying to use features that don't seem to be in the latest released gem, but are
mentioned in this README, then you probably need to specify the master branch if you want to
use them. This README is probably ahead of the latest released version if you're reading it
on GitHub.
For Non-Rails usage:
```ruby
class ModuleName < ActiveRecord::Base
include Paperclip::Glue
...
end
```
---
Quick Start
-----------
### Models
```ruby
class User < ActiveRecord::Base
has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }, default_url: "/images/:style/missing.png"
validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\z/
end
```
### Migrations
Assuming you have a `users` table, add an `avatar` column to the `users` table:
```ruby
class AddAvatarColumnsToUsers < ActiveRecord::Migration
def up
add_attachment :users, :avatar
end
def down
remove_attachment :users, :avatar
end
end
```
(Or you can use the Rails migration generator: `rails generate paperclip user avatar`)
### Edit and New Views
Make sure you have corresponding methods in your controller:
```erb
<%= form_for @user, url: users_path, html: { multipart: true } do |form| %>
<%= form.file_field :avatar %>
<%= form.submit %>
<% end %>
```
### Edit and New Views with [Simple Form](https://github.com/plataformatec/simple_form)
```erb
<%= simple_form_for @user, url: users_path do |form| %>
<%= form.input :avatar, as: :file %>
<%= form.submit %>
<% end %>
```
### Controller
```ruby
def create
@user = User.create(user_params)
end
private
# Use strong_parameters for attribute whitelisting
# Be sure to update your create() and update() controller methods.
def user_params
params.require(:user).permit(:avatar)
end
```
### View Helpers
Add these to the view where you want your images displayed:
```erb
<%= image_tag @user.avatar.url %>
<%= image_tag @user.avatar.url(:medium) %>
<%= image_tag @user.avatar.url(:thumb) %>
```
### Checking a File Exists
There are two methods for checking if a file exists:
- `file?` and `present?` checks if the `_file_name` field is populated
- `exists?` checks if the file exists (will perform a TCP connection if stored in the cloud)
Keep this in mind if you are checking if files are present in a loop. The first
version is significantly more performant, but has different semantics.
### Deleting an Attachment
Set the attribute to `nil` and save.
```ruby
@user.avatar = nil
@user.save
```
---
Usage
-----
The basics of Paperclip are quite simple: Declare that your model has an
attachment with the `has_attached_file` method, and give it a name.
Paperclip will wrap up to four attributes (all prefixed with that attachment's name,
so you can have multiple attachments per model if you wish) and give them a
friendly front end. These attributes are:
* `_file_name`
* `_file_size`
* `_content_type`
* `_updated_at`
By default, only `_file_name` is required for Paperclip to operate.
You'll need to add `_content_type` in case you want to use content type
validation.
More information about the options passed to `has_attached_file` is available in the
documentation of [`Paperclip::ClassMethods`](http://www.rubydoc.info/gems/paperclip/Paperclip/ClassMethods).
Validations
-----------
For validations, Paperclip introduces several validators to validate your attachment:
* `AttachmentContentTypeValidator`
* `AttachmentPresenceValidator`
* `AttachmentSizeValidator`
Example Usage:
```ruby
validates :avatar, attachment_presence: true
validates_with AttachmentPresenceValidator, attributes: :avatar
validates_with AttachmentSizeValidator, attributes: :avatar, less_than: 1.megabytes
```
Validators can also be defined using the old helper style:
* `validates_attachment_presence`
* `validates_attachment_content_type`
* `validates_attachment_size`
Example Usage:
```ruby
validates_attachment_presence :avatar
```
Lastly, you can also define multiple validations on a single attachment using `validates_attachment`:
```ruby
validates_attachment :avatar, presence: true,
content_type: "image/jpeg",
size: { in: 0..10.kilobytes }
```
_NOTE: Post-processing will not even **start** if the attachment is not valid
according to the validations. Your callbacks and processors will **only** be
called with valid attachments._
```ruby
class Message < ActiveRecord::Base
has_attached_file :asset, styles: { thumb: "100x100#" }
before_post_process :skip_for_audio
def skip_for_audio
! %w(audio/ogg application/ogg).include?(asset_content_type)
end
end
```
If you have other validations that depend on assignment order, the recommended
course of action is to prevent the assignment of the attachment until
afterwards, then assign manually:
```ruby
class Book < ActiveRecord::Base
has_attached_file :document, styles: { thumbnail: "60x60#" }
validates_attachment :document, content_type: "application/pdf"
validates_something_else # Other validations that conflict with Paperclip's
end
class BooksController < ApplicationController
def create
@book = Book.new(book_params)
@book.document = params[:book][:document]
@book.save
respond_with @book
end
private
def book_params
params.require(:book).permit(:title, :author)
end
end
```
**A note on content_type validations and security**
You should ensure that you validate files to be only those MIME types you
explicitly want to support. If you don't, you could be open to
XSS attacks
if a user uploads a file with a malicious HTML payload.
If you're only interested in images, restrict your allowed content_types to
image-y ones:
```ruby
validates_attachment :avatar,
content_type: ["image/jpeg", "image/gif", "image/png"]
```
`Paperclip::ContentTypeDetector` will attempt to match a file's extension to an
inferred content_type, regardless of the actual contents of the file.
---
Internationalization (I18n)
---------------------------
For using or adding locale files in different languages, check the project
https://github.com/thoughtbot/paperclip-i18n.
Security Validations
====================
Thanks to a report from [Egor Homakov](http://homakov.blogspot.com/) we have
taken steps to prevent people from spoofing Content-Types and getting data
you weren't expecting onto your server.
NOTE: Starting at version 4.0.0, all attachments are *required* to include a
content_type validation, a file_name validation, or to explicitly state that
they're not going to have either. *Paperclip will raise an error* if you do not
do this.
```ruby
class ActiveRecord::Base
has_attached_file :avatar
# Validate content type
validates_attachment_content_type :avatar, content_type: /\Aimage/
# Validate filename
validates_attachment_file_name :avatar, matches: [/png\z/, /jpe?g\z/]
# Explicitly do not validate
do_not_validate_attachment_file_type :avatar
end
```
This keeps Paperclip secure-by-default, and will prevent people trying to mess
with your filesystem.
NOTE: Also starting at version 4.0.0, Paperclip has another validation that
cannot be turned off. This validation will prevent content type spoofing. That
is, uploading a PHP document (for example) as part of the EXIF tags of a
well-formed JPEG. This check is limited to the media type (the first part of the
MIME type, so, 'text' in `text/plain`). This will prevent HTML documents from
being uploaded as JPEGs, but will not prevent GIFs from being uploaded with a
`.jpg` extension. This validation will only add validation errors to the form. It
will not cause errors to be raised.
This can sometimes cause false validation errors in applications that use custom
file extensions. In these cases you may wish to add your custom extension to the
list of content type mappings by creating `config/initializers/paperclip.rb`:
```ruby
# Allow ".foo" as an extension for files with the MIME type "text/plain".
Paperclip.options[:content_type_mappings] = {
foo: %w(text/plain)
}
```
---
Defaults
--------
Global defaults for all your Paperclip attachments can be defined by changing the Paperclip::Attachment.default_options Hash. This can be useful for setting your default storage settings per example so you won't have to define them in every `has_attached_file` definition.
If you're using Rails, you can define a Hash with default options in `config/application.rb` or in any of the `config/environments/*.rb` files on config.paperclip_defaults. These will get merged into `Paperclip::Attachment.default_options` as your Rails app boots. An example:
```ruby
module YourApp
class Application < Rails::Application
# Other code...
config.paperclip_defaults = { storage: :fog, fog_credentials: { provider: "Local", local_root: "#{Rails.root}/public"}, fog_directory: "", fog_host: "localhost"}
end
end
```
Another option is to directly modify the `Paperclip::Attachment.default_options` Hash - this method works for non-Rails applications or is an option if you prefer to place the Paperclip default settings in an initializer.
An example Rails initializer would look something like this:
```ruby
Paperclip::Attachment.default_options[:storage] = :fog
Paperclip::Attachment.default_options[:fog_credentials] = { provider: "Local", local_root: "#{Rails.root}/public"}
Paperclip::Attachment.default_options[:fog_directory] = ""
Paperclip::Attachment.default_options[:fog_host] = "http://localhost:3000"
```
---
Migrations
----------
Paperclip defines several migration methods which can be used to create the necessary columns in your
model. There are two types of helper methods to aid in this, as follows:
### Add Attachment Column To A Table
The `attachment` helper can be used when creating a table:
```ruby
class CreateUsersWithAttachments < ActiveRecord::Migration
def up
create_table :users do |t|
t.attachment :avatar
end
end
# This is assuming you are only using the users table for Paperclip attachment. Drop with care!
def down
drop_table :users
end
end
```
You can also use the `change` method, instead of the `up`/`down` combination above, as shown below:
```ruby
class CreateUsersWithAttachments < ActiveRecord::Migration
def change
create_table :users do |t|
t.attachment :avatar
end
end
end
```
### Schema Definition
Alternatively, the `add_attachment` and `remove_attachment` methods can be used to add new Paperclip columns to an existing table:
```ruby
class AddAttachmentColumnsToUsers < ActiveRecord::Migration
def up
add_attachment :users, :avatar
end
def down
remove_attachment :users, :avatar
end
end
```
Or you can do this with the `change` method:
```ruby
class AddAttachmentColumnsToUsers < ActiveRecord::Migration
def change
add_attachment :users, :avatar
end
end
```
### Vintage Syntax
Vintage syntax (such as `t.has_attached_file` and `drop_attached_file`) is still supported in
Paperclip 3.x, but you're advised to update those migration files to use this new syntax.
---
Storage
-------
Paperclip ships with 3 storage adapters:
* File Storage
* S3 Storage (via `aws-sdk-s3`)
* Fog Storage
If you would like to use Paperclip with another storage, you can install these
gems along side with Paperclip:
* [paperclip-azure](https://github.com/supportify/paperclip-azure)
* [paperclip-azure-storage](https://github.com/gmontard/paperclip-azure-storage)
* [paperclip-dropbox](https://github.com/janko-m/paperclip-dropbox)
### Understanding Storage
The files that are assigned as attachments are, by default, placed in the
directory specified by the `:path` option to `has_attached_file`. By default, this
location is `:rails_root/public/system/:class/:attachment/:id_partition/:style/:filename`.
This location was chosen because, on standard Capistrano deployments, the
`public/system` directory can be symlinked to the app's shared directory, meaning it
survives between deployments. For example, using that `:path`, you may have a
file at
/data/myapp/releases/20081229172410/public/system/users/avatar/000/000/013/small/my_pic.png
_**NOTE**: This is a change from previous versions of Paperclip, but is overall a
safer choice for the default file store._
You may also choose to store your files using Amazon's S3 service. To do so, include
the `aws-sdk-s3` gem in your Gemfile:
```ruby
gem 'aws-sdk-s3'
```
And then you can specify using S3 from `has_attached_file`.
You can find more information about configuring and using S3 storage in
[the `Paperclip::Storage::S3` documentation](http://www.rubydoc.info/gems/paperclip/Paperclip/Storage/S3).
Files on the local filesystem (and in the Rails app's public directory) will be
available to the internet at large. If you require access control, it's
possible to place your files in a different location. You will need to change
both the `:path` and `:url` options in order to make sure the files are unavailable
to the public. Both `:path` and `:url` allow the same set of interpolated
variables.
---
IO Adapters
-----------
When a file is uploaded or attached, it can be in one of a few different input
forms, from Rails' UploadedFile object to a StringIO to a Tempfile or even a
simple String that is a URL that points to an image.
Paperclip will accept, by default, many of these sources. It also is capable of
handling even more with a little configuration. The IO Adapters that handle
images from non-local sources are not enabled by default. They can be enabled by
adding a line similar to the following into `config/initializers/paperclip.rb`:
```ruby
Paperclip::DataUriAdapter.register
```
It's best to only enable a remote-loading adapter if you need it. Otherwise
there's a chance that someone can gain insight into your internal network
structure using it as a vector.
The following adapters are *not* loaded by default:
* `Paperclip::UriAdapter` - which accepts a `URI` instance.
* `Paperclip::HttpUrlProxyAdapter` - which accepts a `http` string.
* `Paperclip::DataUriAdapter` - which accepts a Base64-encoded `data:` string.
---
Post Processing
---------------
Paperclip supports an extensible selection of post-processors. When you define
a set of styles for an attachment, by default it is expected that those
"styles" are actually "thumbnails." These are processed by
`Paperclip::Thumbnail`. For backward compatibility reasons you can pass either
a single geometry string, or an array containing a geometry and a format that
the file will be converted to, like so:
```ruby
has_attached_file :avatar, styles: { thumb: ["32x32#", :png] }
```
This will convert the "thumb" style to a 32x32 square in PNG format, regardless
of what was uploaded. If the format is not specified, it is kept the same (e.g.
JPGs will remain JPGs). `Paperclip::Thumbnail` uses ImageMagick to process
images; [ImageMagick's geometry documentation](http://www.imagemagick.org/script/command-line-processing.php#geometry)
has more information on the accepted style formats.
For more fine-grained control of the conversion process, `source_file_options` and `convert_options` can be used to pass flags and settings directly to ImageMagick's powerful Convert tool, [documented here](https://www.imagemagick.org/script/convert.php). For example:
```ruby
has_attached_file :image, styles: { regular: ['800x800>', :png]},
source_file_options: { regular: "-density 96 -depth 8 -quality 85" },
convert_options: { regular: "-posterize 3"}
```
ImageMagick supports a number of environment variables for controlling its resource limits. For example, you can enforce memory or execution time limits by setting the following variables in your application's process environment:
* `MAGICK_MEMORY_LIMIT=128MiB`
* `MAGICK_MAP_LIMIT=64MiB`
* `MAGICK_TIME_LIMIT=30`
For a full list of variables and description, see [ImageMagick's resources documentation](http://www.imagemagick.org/script/resources.php).
---
Custom Attachment Processors
-------
You can write your own custom attachment processors to carry out tasks like
adding watermarks, compressing images, or encrypting files. Custom processors
must be defined within the `Paperclip` module, inherit from
`Paperclip::Processor` (see [`lib/paperclip/processor.rb`](https://github.com/thoughtbot/paperclip/blob/master/lib/paperclip/processor.rb)),
and implement a `make` method that returns a `File`. All files in your Rails
app's `lib/paperclip` and `lib/paperclip_processors` directories will be
automatically loaded by Paperclip. Processors are specified using the
`:processors` option to `has_attached_file`:
```ruby
has_attached_file :scan, styles: { text: { quality: :better } },
processors: [:ocr]
```
This would load the hypothetical class `Paperclip::Ocr`, and pass it the
options hash `{ quality: :better }`, along with the uploaded file.
Multiple processors can be specified, and they will be invoked in the order
they are defined in the `:processors` array. Each successive processor is given
the result from the previous processor. All processors receive the same
parameters, which are defined in the `:styles` hash. For example, assuming we
had this definition:
```ruby
has_attached_file :scan, styles: { text: { quality: :better } },
processors: [:rotator, :ocr]
```
Both the `:rotator` processor and the `:ocr` processor would receive the
options `{ quality: :better }`. If a processor receives an option it doesn't
recognise, it's expected to ignore it.
_NOTE: Because processors operate by turning the original attachment into the
styles, no processors will be run if there are no styles defined._
If you're interested in caching your thumbnail's width, height and size in the
database, take a look at the [paperclip-meta](https://github.com/teeparham/paperclip-meta)
gem.
Also, if you're interested in generating the thumbnail on-the-fly, you might want
to look into the [attachment_on_the_fly](https://github.com/drpentode/Attachment-on-the-Fly)
gem.
Paperclip's thumbnail generator (see [`lib/paperclip/thumbnail.rb`](lib/paperclip/thumbnail.rb))
is implemented as a processor, and may be a good reference for writing your own
processors.
---
Events
------
Before and after the Post Processing step, Paperclip calls back to the model
with a few callbacks, allowing the model to change or cancel the processing
step. The callbacks are `before_post_process` and `after_post_process` (which
are called before and after the processing of each attachment), and the
attachment-specific `before__post_process` and
`after__post_process`. The callbacks are intended to be as close to
normal ActiveRecord callbacks as possible, so if you return false (specifically
\- returning nil is not the same) in a `before_filter`, the post processing step
will halt. Returning false in an `after_filter` will not halt anything, but you
can access the model and the attachment if necessary.
_NOTE: Post processing will not even **start** if the attachment is not valid
according to the validations. Your callbacks and processors will **only** be
called with valid attachments._
```ruby
class Message < ActiveRecord::Base
has_attached_file :asset, styles: { thumb: "100x100#" }
before_post_process :skip_for_audio
def skip_for_audio
! %w(audio/ogg application/ogg).include?(asset_content_type)
end
end
```
---
URI Obfuscation
---------------
Paperclip has an interpolation called `:hash` for obfuscating filenames of
publicly-available files.
Example Usage:
```ruby
has_attached_file :avatar, {
url: "/system/:hash.:extension",
hash_secret: "longSecretString"
}
```
The `:hash` interpolation will be replaced with a unique hash made up of whatever
is specified in `:hash_data`. The default value for `:hash_data` is `":class/:attachment/:id/:style/:updated_at"`.
`:hash_secret` is required - an exception will be raised if `:hash` is used without `:hash_secret` present.
For more on this feature, read [the author's own explanation](https://github.com/thoughtbot/paperclip/pull/416)
Checksum / Fingerprint
-------
A checksum of the original file assigned will be placed in the model if it
has an attribute named fingerprint. Following the user model migration example
above, the migration would look like the following:
```ruby
class AddAvatarFingerprintColumnToUser < ActiveRecord::Migration
def up
add_column :users, :avatar_fingerprint, :string
end
def down
remove_column :users, :avatar_fingerprint
end
end
```
The algorithm can be specified using a configuration option; it defaults to MD5
for backwards compatibility with Paperclip 5 and earlier.
```ruby
has_attached_file :some_attachment, adapter_options: { hash_digest: Digest::SHA256 }
```
Run `CLASS=User ATTACHMENT=avatar rake paperclip:refresh:fingerprints` after
changing the digest on existing attachments to update the fingerprints in the
database.
File Preservation for Soft-Delete
-------
An option is available to preserve attachments in order to play nicely with soft-deleted models. (acts_as_paranoid, paranoia, etc.)
```ruby
has_attached_file :some_attachment, {
preserve_files: true,
}
```
This will prevent ```some_attachment``` from being wiped out when the model gets destroyed, so it will still exist when the object is restored later.
---
Dynamic Configuration
---------------------
Callable objects (lambdas, Procs) can be used in a number of places for dynamic
configuration throughout Paperclip. This strategy exists in a number of
components of the library but is most significant in the possibilities for
allowing custom styles and processors to be applied for specific model
instances, rather than applying defined styles and processors across all
instances.
### Dynamic Styles:
Imagine a user model that had different styles based on the role of the user.
Perhaps some users are bosses (e.g. a User model instance responds to `#boss?`)
and merit a bigger avatar thumbnail than regular users. The configuration to
determine what style parameters are to be used based on the user role might
look as follows where a boss will receive a `300x300` thumbnail otherwise a
`100x100` thumbnail will be created.
```ruby
class User < ActiveRecord::Base
has_attached_file :avatar, styles: lambda { |attachment| { thumb: (attachment.instance.boss? ? "300x300>" : "100x100>") } }
end
```
### Dynamic Processors:
Another contrived example is a user model that is aware of which file processors
should be applied to it (beyond the implied `thumbnail` processor invoked when
`:styles` are defined). Perhaps we have a watermark processor available and it is
only used on the avatars of certain models. The configuration for this might be
where the instance is queried for which processors should be applied to it.
Presumably some users might return `[:thumbnail, :watermark]` for its
processors, where a defined `watermark` processor is invoked after the
`thumbnail` processor already defined by Paperclip.
```ruby
class User < ActiveRecord::Base
has_attached_file :avatar, processors: lambda { |instance| instance.processors }
attr_accessor :processors
end
```
---
Logging
----------
By default, Paperclip outputs logging according to your logger level. If you want to disable logging (e.g. during testing) add this into your environment's configuration:
```ruby
Your::Application.configure do
...
Paperclip.options[:log] = false
...
end
```
More information in the [rdocs](http://www.rubydoc.info/github/thoughtbot/paperclip/Paperclip.options)
---
Deployment
----------
To make Capistrano symlink the `public/system` directory so that attachments
survive new deployments, set the `linked_dirs` option in your `config/deploy.rb`
file:
```ruby
set :linked_dirs, fetch(:linked_dirs, []).push('public/system')
```
### Attachment Styles
Paperclip is aware of new attachment styles you have added in previous deploys. The only thing you should do after each deployment is to call
`rake paperclip:refresh:missing_styles`. It will store current attachment styles in `RAILS_ROOT/public/system/paperclip_attachments.yml`
by default. You can change it by:
```ruby
Paperclip.registered_attachments_styles_path = '/tmp/config/paperclip_attachments.yml'
```
Here is an example for Capistrano:
```ruby
namespace :paperclip do
desc "build missing paperclip styles"
task :build_missing_styles do
on roles(:app) do
within release_path do
with rails_env: fetch(:rails_env) do
execute :rake, "paperclip:refresh:missing_styles"
end
end
end
end
end
after("deploy:compile_assets", "paperclip:build_missing_styles")
```
Now you don't have to remember to refresh thumbnails in production every time you add a new style.
Unfortunately, it does not work with dynamic styles - it just ignores them.
If you already have a working app and don't want `rake paperclip:refresh:missing_styles` to refresh old pictures, you need to tell
Paperclip about existing styles. Simply create a `paperclip_attachments.yml` file by hand. For example:
```ruby
class User < ActiveRecord::Base
has_attached_file :avatar, styles: { thumb: 'x100', croppable: '600x600>', big: '1000x1000>' }
end
class Book < ActiveRecord::Base
has_attached_file :cover, styles: { small: 'x100', large: '1000x1000>' }
has_attached_file :sample, styles: { thumb: 'x100' }
end
```
Then in `RAILS_ROOT/public/system/paperclip_attachments.yml`:
```yml
---
:User:
:avatar:
- :thumb
- :croppable
- :big
:Book:
:cover:
- :small
- :large
:sample:
- :thumb
```
---
Testing
-------
Paperclip provides rspec-compatible matchers for testing attachments. See the
documentation on [Paperclip::Shoulda::Matchers](http://www.rubydoc.info/gems/paperclip/Paperclip/Shoulda/Matchers)
for more information.
**Parallel Tests**
Because of the default `path` for Paperclip storage, if you try to run tests in
parallel, you may find that files get overwritten because the same path is being
calculated for them in each test process. While this fix works for
parallel_tests, a similar concept should be used for any other mechanism for
running tests concurrently.
```ruby
if ENV['PARALLEL_TEST_GROUPS']
Paperclip::Attachment.default_options[:path] = ":rails_root/public/system/:rails_env/#{ENV['TEST_ENV_NUMBER'].to_i}/:class/:attachment/:id_partition/:filename"
else
Paperclip::Attachment.default_options[:path] = ":rails_root/public/system/:rails_env/:class/:attachment/:id_partition/:filename"
end
```
The important part here being the inclusion of `ENV['TEST_ENV_NUMBER']`, or a
similar mechanism for whichever parallel testing library you use.
**Integration Tests**
Using integration tests with FactoryBot may save multiple copies of
your test files within the app. To avoid this, specify a custom path in
the `config/environments/test.rb` like so:
```ruby
Paperclip::Attachment.default_options[:path] = "#{Rails.root}/spec/test_files/:class/:id_partition/:style.:extension"
```
Then, make sure to delete that directory after the test suite runs by adding
this to `spec_helper.rb`.
```ruby
config.after(:suite) do
FileUtils.rm_rf(Dir["#{Rails.root}/spec/test_files/"])
end
```
**Example of test configuration with Factory Bot**
```ruby
FactoryBot.define do
factory :user do
avatar { File.new("#{Rails.root}/spec/support/fixtures/image.jpg") }
end
end
```
---
Contributing
------------
If you'd like to contribute a feature or bugfix: Thanks! To make sure your
fix/feature has a high chance of being included, please read the following
guidelines:
1. Post a [pull request](https://github.com/thoughtbot/paperclip/compare/).
2. Make sure there are tests! We will not accept any patch that is not tested.
It's a rare time when explicit tests aren't needed. If you have questions
about writing tests for paperclip, please open a
[GitHub issue](https://github.com/thoughtbot/paperclip/issues/new).
Please see [`CONTRIBUTING.md`](./CONTRIBUTING.md) for more details on contributing and running test.
Thank you to all [the contributors](https://github.com/thoughtbot/paperclip/graphs/contributors)!
License
-------
Paperclip is Copyright © 2008-2017 thoughtbot, inc. It is free software, and may be
redistributed under the terms specified in the MIT-LICENSE file.
About thoughtbot
----------------

Paperclip is maintained and funded by thoughtbot.
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
We love open source software!
See [our other projects][community] or
[hire us][hire] to design, develop, and grow your product.
[community]: https://thoughtbot.com/community?utm_source=github
[hire]: https://thoughtbot.com?utm_source=github
================================================
FILE: RELEASING.md
================================================
Releasing paperclip
1. Update `lib/paperclip/version.rb` file accordingly.
2. Update `NEWS` to reflect the changes since last release.
3. Commit changes. There shouldn’t be code changes, and thus CI doesn’t need to
run, you can then add “[ci skip]” to the commit message.
4. Tag the release: `git tag -m 'vVERSION' vVERSION`
5. Push changes: `git push --tags`
6. Build and publish the gem:
```bash
gem build paperclip.gemspec
gem push paperclip-VERSION.gem
```
7. Announce the new release, making sure to say “thank you” to the contributors
who helped shape this version.
================================================
FILE: Rakefile
================================================
require 'bundler/gem_tasks'
require 'appraisal'
require 'rspec/core/rake_task'
require 'cucumber/rake/task'
desc 'Default: run unit tests.'
task :default => [:clean, :all]
desc 'Test the paperclip plugin under all supported Rails versions.'
task :all do |t|
if ENV['BUNDLE_GEMFILE']
exec('rake spec && cucumber')
else
exec("rm -f gemfiles/*.lock")
Rake::Task["appraisal:gemfiles"].execute
Rake::Task["appraisal:install"].execute
exec('rake appraisal')
end
end
desc 'Test the paperclip plugin.'
RSpec::Core::RakeTask.new(:spec)
desc 'Run integration test'
Cucumber::Rake::Task.new do |t|
t.cucumber_opts = %w{--format progress}
end
desc 'Start an IRB session with all necessary files required.'
task :shell do |t|
chdir File.dirname(__FILE__)
exec 'irb -I lib/ -I lib/paperclip -r rubygems -r active_record -r tempfile -r init'
end
desc 'Clean up files.'
task :clean do |t|
FileUtils.rm_rf "doc"
FileUtils.rm_rf "tmp"
FileUtils.rm_rf "pkg"
FileUtils.rm_rf "public"
FileUtils.rm "test/debug.log" rescue nil
FileUtils.rm "test/paperclip.db" rescue nil
Dir.glob("paperclip-*.gem").each{|f| FileUtils.rm f }
end
================================================
FILE: UPGRADING
================================================
##################################################
# NOTE FOR UPGRADING FROM 4.3.0 OR EARLIER #
##################################################
Paperclip is now compatible with aws-sdk-s3.
If you are using S3 storage, aws-sdk-s3 requires you to make a few small
changes:
* You must set the `s3_region`
* If you are explicitly setting permissions anywhere, such as in an initializer,
note that the format of the permissions changed from using an underscore to
using a hyphen. For example, `:public_read` needs to be changed to
`public-read`.
For a walkthrough of upgrading from 4 to *5* (not 6) and aws-sdk >= 2.0 you can watch
http://rubythursday.com/episodes/ruby-snack-27-upgrade-paperclip-and-aws-sdk-in-prep-for-rails-5
================================================
FILE: features/basic_integration.feature
================================================
Feature: Rails integration
Background:
Given I generate a new rails application
And I run a rails generator to generate a "User" scaffold with "name:string"
And I run a paperclip generator to add a paperclip "attachment" to the "User" model
And I run a migration
And I update my new user view to include the file upload field
And I update my user view to include the attachment
And I allow the attachment to be submitted
Scenario: Configure defaults for all attachments through Railtie
Given I add this snippet to config/application.rb:
"""
config.paperclip_defaults = {
:url => "/paperclip/custom/:attachment/:style/:filename",
:validate_media_type => false
}
"""
And I attach :attachment
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "spec/support/fixtures/animated.unknown" to "Attachment"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "/paperclip/custom/attachments/original/animated.unknown"
And the file at "/paperclip/custom/attachments/original/animated.unknown" should be the same as "spec/support/fixtures/animated.unknown"
Scenario: Add custom processors
Given I add a "test" processor in "lib/paperclip"
And I add a "cool" processor in "lib/paperclip_processors"
And I attach :attachment with:
"""
styles: { original: {} }, processors: [:test, :cool]
"""
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "spec/support/fixtures/5k.png" to "Attachment"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "/paperclip/custom/attachments/original/5k.png"
Scenario: Filesystem integration test
Given I attach :attachment with:
"""
:url => "/system/:attachment/:style/:filename"
"""
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "spec/support/fixtures/5k.png" to "Attachment"
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "/system/attachments/original/5k.png"
And the file at "/system/attachments/original/5k.png" should be the same as "spec/support/fixtures/5k.png"
Scenario: S3 Integration test
Given I attach :attachment with:
"""
:storage => :s3,
:path => "/:attachment/:style/:filename",
:s3_credentials => Rails.root.join("config/s3.yml"),
:styles => { :square => "100x100#" }
"""
And I write to "config/s3.yml" with:
"""
bucket: paperclip
access_key_id: access_key
secret_access_key: secret_key
s3_region: us-west-2
"""
And I start the rails application
When I go to the new user page
And I fill in "Name" with "something"
And I attach the file "spec/support/fixtures/5k.png" to "Attachment" on S3
And I press "Submit"
Then I should see "Name: something"
And I should see an image with a path of "//s3.amazonaws.com/paperclip/attachments/original/5k.png"
And the file at "//s3.amazonaws.com/paperclip/attachments/original/5k.png" should be uploaded to S3
================================================
FILE: features/migration.feature
================================================
Feature: Migration
Background:
Given I generate a new rails application
And I write to "app/models/user.rb" with:
"""
class User < ActiveRecord::Base; end
"""
Scenario: Vintage syntax
When I write to "db/migrate/01_add_attachment_to_users.rb" with:
"""
class AddAttachmentToUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.has_attached_file :avatar
end
end
def self.down
drop_attached_file :users, :avatar
end
end
"""
And I run a migration
Then I should have attachment columns for "avatar"
When I rollback a migration
Then I should not have attachment columns for "avatar"
Scenario: New syntax with create_table
When I write to "db/migrate/01_add_attachment_to_users.rb" with:
"""
class AddAttachmentToUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.attachment :avatar
end
end
end
"""
And I run a migration
Then I should have attachment columns for "avatar"
Scenario: New syntax outside of create_table
When I write to "db/migrate/01_create_users.rb" with:
"""
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users
end
end
"""
And I write to "db/migrate/02_add_attachment_to_users.rb" with:
"""
class AddAttachmentToUsers < ActiveRecord::Migration
def self.up
add_attachment :users, :avatar
end
def self.down
remove_attachment :users, :avatar
end
end
"""
And I run a migration
Then I should have attachment columns for "avatar"
When I rollback a migration
Then I should not have attachment columns for "avatar"
================================================
FILE: features/rake_tasks.feature
================================================
Feature: Rake tasks
Background:
Given I generate a new rails application
And I run a rails generator to generate a "User" scaffold with "name:string"
And I run a paperclip generator to add a paperclip "attachment" to the "User" model
And I run a migration
And I attach :attachment with:
"""
:path => ":rails_root/public/system/:attachment/:style/:filename"
"""
Scenario: Paperclip refresh thumbnails task
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "200x200#" }
"""
And I upload the fixture "5k.png"
Then the attachment "medium/5k.png" should have a dimension of 200x200
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "100x100#" }
"""
When I successfully run `bundle exec rake paperclip:refresh:thumbnails CLASS=User --trace`
Then the attachment "original/5k.png" should exist
And the attachment "medium/5k.png" should have a dimension of 100x100
Scenario: Paperclip refresh metadata task
When I upload the fixture "5k.png"
And I swap the attachment "original/5k.png" with the fixture "12k.png"
And I successfully run `bundle exec rake paperclip:refresh:metadata CLASS=User --trace`
Then the attachment should have the same content type as the fixture "12k.png"
And the attachment should have the same file size as the fixture "12k.png"
Scenario: Paperclip refresh missing styles task
When I upload the fixture "5k.png"
Then the attachment file "original/5k.png" should exist
And the attachment file "medium/5k.png" should not exist
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename",
:styles => { :medium => "200x200#" }
"""
When I successfully run `bundle exec rake paperclip:refresh:missing_styles --trace`
Then the attachment file "original/5k.png" should exist
And the attachment file "medium/5k.png" should exist
Scenario: Paperclip clean task
When I upload the fixture "5k.png"
And I upload the fixture "12k.png"
Then the attachment file "original/5k.png" should exist
And the attachment file "original/12k.png" should exist
When I modify my attachment definition to:
"""
has_attached_file :attachment, :path => ":rails_root/public/system/:attachment/:style/:filename"
validates_attachment_size :attachment, :less_than => 10.kilobytes
"""
And I successfully run `bundle exec rake paperclip:clean CLASS=User --trace`
Then the attachment file "original/5k.png" should exist
But the attachment file "original/12k.png" should not exist
================================================
FILE: features/step_definitions/attachment_steps.rb
================================================
module AttachmentHelpers
def fixture_path(filename)
File.expand_path("#{PROJECT_ROOT}/spec/support/fixtures/#{filename}")
end
def attachment_path(filename)
File.expand_path("public/system/attachments/#{filename}")
end
end
World(AttachmentHelpers)
When /^I modify my attachment definition to:$/ do |definition|
content = cd(".") { File.read("app/models/user.rb") }
name = content[/has_attached_file :\w+/][/:\w+/]
content.gsub!(/has_attached_file.+end/m, <<-FILE)
#{definition}
do_not_validate_attachment_file_type #{name}
end
FILE
write_file "app/models/user.rb", content
cd(".") { FileUtils.rm_rf ".rbx" }
end
When /^I upload the fixture "([^"]*)"$/ do |filename|
run_simple %(bundle exec rails runner "User.create!(:attachment => File.open('#{fixture_path(filename)}'))")
end
Then /^the attachment "([^"]*)" should have a dimension of (\d+x\d+)$/ do |filename, dimension|
cd(".") do
geometry = `identify -format "%wx%h" "#{attachment_path(filename)}"`.strip
expect(geometry).to eq(dimension)
end
end
Then /^the attachment "([^"]*)" should exist$/ do |filename|
cd(".") do
expect(File.exist?(attachment_path(filename))).to be true
end
end
When /^I swap the attachment "([^"]*)" with the fixture "([^"]*)"$/ do |attachment_filename, fixture_filename|
cd(".") do
require 'fileutils'
FileUtils.rm_f attachment_path(attachment_filename)
FileUtils.cp fixture_path(fixture_filename), attachment_path(attachment_filename)
end
end
Then /^the attachment should have the same content type as the fixture "([^"]*)"$/ do |filename|
cd(".") do
begin
# Use mime/types/columnar if available, for reduced memory usage
require "mime/types/columnar"
rescue LoadError
require "mime/types"
end
attachment_content_type = `bundle exec rails runner "puts User.last.attachment_content_type"`.strip
expected = MIME::Types.type_for(filename).first.content_type
expect(attachment_content_type).to eq(expected)
end
end
Then /^the attachment should have the same file name as the fixture "([^"]*)"$/ do |filename|
cd(".") do
attachment_file_name = `bundle exec rails runner "puts User.last.attachment_file_name"`.strip
expect(attachment_file_name).to eq(File.name(fixture_path(filename)).to_s)
end
end
Then /^the attachment should have the same file size as the fixture "([^"]*)"$/ do |filename|
cd(".") do
attachment_file_size = `bundle exec rails runner "puts User.last.attachment_file_size"`.strip
expect(attachment_file_size).to eq(File.size(fixture_path(filename)).to_s)
end
end
Then /^the attachment file "([^"]*)" should (not )?exist$/ do |filename, not_exist|
cd(".") do
expect(attachment_path(filename)).not_to be_an_existing_file
end
end
Then /^I should have attachment columns for "([^"]*)"$/ do |attachment_name|
cd(".") do
columns = eval(`bundle exec rails runner "puts User.columns.map{ |column| [column.name, column.sql_type] }.inspect"`.strip)
expect_columns = [
["#{attachment_name}_file_name", "varchar"],
["#{attachment_name}_content_type", "varchar"],
["#{attachment_name}_file_size", "bigint"],
["#{attachment_name}_updated_at", "datetime"]
]
expect(columns).to include(*expect_columns)
end
end
Then /^I should not have attachment columns for "([^"]*)"$/ do |attachment_name|
cd(".") do
columns = eval(`bundle exec rails runner "puts User.columns.map{ |column| [column.name, column.sql_type] }.inspect"`.strip)
expect_columns = [
["#{attachment_name}_file_name", "varchar"],
["#{attachment_name}_content_type", "varchar"],
["#{attachment_name}_file_size", "bigint"],
["#{attachment_name}_updated_at", "datetime"]
]
expect(columns).not_to include(*expect_columns)
end
end
================================================
FILE: features/step_definitions/html_steps.rb
================================================
Then %r{I should see an image with a path of "([^"]*)"} do |path|
expect(page).to have_css("img[src^='#{path}']")
end
Then %r{^the file at "([^"]*)" is the same as "([^"]*)"$} do |web_file, path|
expected = IO.read(path)
actual = if web_file.match %r{^https?://}
Net::HTTP.get(URI.parse(web_file))
else
visit(web_file)
page.body
end
actual.force_encoding("UTF-8") if actual.respond_to?(:force_encoding)
expect(actual).to eq(expected)
end
================================================
FILE: features/step_definitions/rails_steps.rb
================================================
Given /^I generate a new rails application$/ do
steps %{
When I successfully run `rails new #{APP_NAME} --skip-bundle`
And I cd to "#{APP_NAME}"
}
FileUtils.chdir("tmp/aruba/testapp/")
steps %{
And I turn off class caching
And I write to "Gemfile" with:
"""
source "http://rubygems.org"
gem "rails", "#{framework_version}"
gem "sqlite3", :platform => [:ruby, :rbx]
gem "activerecord-jdbcsqlite3-adapter", :platform => :jruby
gem "jruby-openssl", :platform => :jruby
gem "capybara"
gem "gherkin"
gem "aws-sdk-s3"
gem "racc", :platform => :rbx
gem "rubysl", :platform => :rbx
"""
And I remove turbolinks
And I comment out lines that contain "action_mailer" in "config/environments/*.rb"
And I empty the application.js file
And I configure the application to use "paperclip" from this project
}
FileUtils.chdir("../../..")
end
Given "I allow the attachment to be submitted" do
cd(".") do
transform_file("app/controllers/users_controller.rb") do |content|
content.gsub("params.require(:user).permit(:name)",
"params.require(:user).permit!")
end
end
end
Given "I remove turbolinks" do
cd(".") do
transform_file("app/assets/javascripts/application.js") do |content|
content.gsub("//= require turbolinks", "")
end
transform_file("app/views/layouts/application.html.erb") do |content|
content.gsub(', "data-turbolinks-track" => true', "")
end
end
end
Given /^I comment out lines that contain "([^"]+)" in "([^"]+)"$/ do |contains, glob|
cd(".") do
Dir.glob(glob).each do |file|
transform_file(file) do |content|
content.gsub(/^(.*?#{contains}.*?)$/) { |line| "# #{line}" }
end
end
end
end
Given /^I attach :attachment$/ do
attach_attachment("attachment")
end
Given /^I attach :attachment with:$/ do |definition|
attach_attachment("attachment", definition)
end
def attach_attachment(name, definition = nil)
snippet = "has_attached_file :#{name}"
if definition
snippet += ", \n"
snippet += definition
end
snippet += "\ndo_not_validate_attachment_file_type :#{name}\n"
cd(".") do
transform_file("app/models/user.rb") do |content|
content.sub(/end\Z/, "#{snippet}\nend")
end
end
end
Given "I empty the application.js file" do
cd(".") do
transform_file("app/assets/javascripts/application.js") do |content|
""
end
end
end
Given /^I run a rails generator to generate a "([^"]*)" scaffold with "([^"]*)"$/ do |model_name, attributes|
step %[I successfully run `rails generate scaffold #{model_name} #{attributes}`]
end
Given /^I run a paperclip generator to add a paperclip "([^"]*)" to the "([^"]*)" model$/ do |attachment_name, model_name|
step %[I successfully run `rails generate paperclip #{model_name} #{attachment_name}`]
end
Given /^I run a migration$/ do
step %[I successfully run `rake db:migrate --trace`]
end
When /^I rollback a migration$/ do
step %[I successfully run `rake db:rollback STEPS=1 --trace`]
end
Given /^I update my new user view to include the file upload field$/ do
steps %{
Given I overwrite "app/views/users/new.html.erb" with:
"""
<%= form_for @user, :html => { :multipart => true } do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :attachment %>
<%= f.file_field :attachment %>
<%= submit_tag "Submit" %>
<% end %>
"""
}
end
Given /^I update my user view to include the attachment$/ do
steps %{
Given I overwrite "app/views/users/show.html.erb" with:
"""
Name: <%= @user.name %>
Attachment: <%= image_tag @user.attachment.url %>
"""
}
end
Given /^I add this snippet to the User model:$/ do |snippet|
file_name = "app/models/user.rb"
cd(".") do
content = File.read(file_name)
File.open(file_name, 'w') { |f| f << content.sub(/end\Z/, "#{snippet}\nend") }
end
end
Given /^I add this snippet to config\/application.rb:$/ do |snippet|
file_name = "config/application.rb"
cd(".") do
content = File.read(file_name)
File.open(file_name, 'w') {|f| f << content.sub(/class Application < Rails::Application.*$/, "class Application < Rails::Application\n#{snippet}\n")}
end
end
Given /^I start the rails application$/ do
cd(".") do
require "rails"
require "./config/environment"
require "capybara"
Capybara.app = Rails.application
end
end
Given /^I reload my application$/ do
Rails::Application.reload!
end
When /^I turn off class caching$/ do
cd(".") do
file = "config/environments/test.rb"
config = IO.read(file)
config.gsub!(%r{^\s*config.cache_classes.*$},
"config.cache_classes = false")
File.open(file, "w"){|f| f.write(config) }
end
end
Then /^the file at "([^"]*)" should be the same as "([^"]*)"$/ do |web_file, path|
expected = IO.read(path)
actual = read_from_web(web_file)
expect(actual).to eq(expected)
end
When /^I configure the application to use "([^\"]+)" from this project$/ do |name|
append_to_gemfile "gem '#{name}', :path => '#{PROJECT_ROOT}'"
steps %{And I successfully run `bundle install --local`}
end
When /^I configure the application to use "([^\"]+)"$/ do |gem_name|
append_to_gemfile "gem '#{gem_name}'"
end
When /^I append gems from Appraisal Gemfile$/ do
File.read(ENV['BUNDLE_GEMFILE']).split(/\n/).each do |line|
if line =~ /^gem "(?!rails|appraisal)/
append_to_gemfile line.strip
end
end
end
When /^I comment out the gem "([^"]*)" from the Gemfile$/ do |gemname|
comment_out_gem_in_gemfile gemname
end
Given(/^I add a "(.*?)" processor in "(.*?)"$/) do |processor, directory|
filename = "#{directory}/#{processor}.rb"
cd(".") do
FileUtils.mkdir_p directory
File.open(filename, "w") do |f|
f.write(<<-CLASS)
module Paperclip
class #{processor.capitalize} < Processor
def make
basename = File.basename(file.path, File.extname(file.path))
dst_format = options[:format] ? ".\#{options[:format]}" : ''
dst = Tempfile.new([basename, dst_format])
dst.binmode
convert(':src :dst',
src: File.expand_path(file.path),
dst: File.expand_path(dst.path)
)
dst
end
end
end
CLASS
end
end
end
def transform_file(filename)
if File.exist?(filename)
content = File.read(filename)
File.open(filename, "w") do |f|
content = yield(content)
f.write(content)
end
end
end
================================================
FILE: features/step_definitions/s3_steps.rb
================================================
When /^I attach the file "([^"]*)" to "([^"]*)" on S3$/ do |file_path, field|
definition = Paperclip::AttachmentRegistry.definitions_for(User)[field.downcase.to_sym]
path = "https://paperclip.s3.us-west-2.amazonaws.com#{definition[:path]}"
path.gsub!(':filename', File.basename(file_path))
path.gsub!(/:([^\/\.]+)/) do |match|
"([^\/\.]+)"
end
FakeWeb.register_uri(:put, Regexp.new(path), :body => "")
step "I attach the file \"#{file_path}\" to \"#{field}\""
end
Then /^the file at "([^"]*)" should be uploaded to S3$/ do |url|
FakeWeb.registered_uri?(:put, url)
end
================================================
FILE: features/step_definitions/web_steps.rb
================================================
# TL;DR: YOU SHOULD DELETE THIS FILE
#
# This file was generated by Cucumber-Rails and is only here to get you a head start
# These step definitions are thin wrappers around the Capybara/Webrat API that lets you
# visit pages, interact with widgets and make assertions about page content.
#
# If you use these step definitions as basis for your features you will quickly end up
# with features that are:
#
# * Hard to maintain
# * Verbose to read
#
# A much better approach is to write your own higher level step definitions, following
# the advice in the following blog posts:
#
# * http://benmabey.com/2008/05/19/imperative-vs-declarative-scenarios-in-user-stories.html
# * http://dannorth.net/2011/01/31/whose-domain-is-it-anyway/
# * http://elabs.se/blog/15-you-re-cuking-it-wrong
#
require 'uri'
require 'cgi'
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths"))
require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors"))
module WithinHelpers
def with_scope(locator)
locator ? within(*selector_for(locator)) { yield } : yield
end
end
World(WithinHelpers)
# Single-line step scoper
When /^(.*) within (.*[^:])$/ do |step, parent|
with_scope(parent) { When step }
end
# Multi-line step scoper
When /^(.*) within (.*[^:]):$/ do |step, parent, table_or_string|
with_scope(parent) { When "#{step}:", table_or_string }
end
Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name)
end
When /^(?:|I )go to (.+)$/ do |page_name|
visit path_to(page_name)
end
When /^(?:|I )press "([^"]*)"$/ do |button|
click_button(button)
end
When /^(?:|I )follow "([^"]*)"$/ do |link|
click_link(link)
end
When /^(?:|I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value|
fill_in(field, :with => value)
end
When /^(?:|I )fill in "([^"]*)" for "([^"]*)"$/ do |value, field|
fill_in(field, :with => value)
end
# Use this to fill in an entire form with data from a table. Example:
#
# When I fill in the following:
# | Account Number | 5002 |
# | Expiry date | 2009-11-01 |
# | Note | Nice guy |
# | Wants Email? | |
#
# TODO: Add support for checkbox, select og option
# based on naming conventions.
#
When /^(?:|I )fill in the following:$/ do |fields|
fields.rows_hash.each do |name, value|
When %{I fill in "#{name}" with "#{value}"}
end
end
When /^(?:|I )select "([^"]*)" from "([^"]*)"$/ do |value, field|
select(value, :from => field)
end
When /^(?:|I )check "([^"]*)"$/ do |field|
check(field)
end
When /^(?:|I )uncheck "([^"]*)"$/ do |field|
uncheck(field)
end
When /^(?:|I )choose "([^"]*)"$/ do |field|
choose(field)
end
When /^(?:|I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field|
attach_file(field, File.expand_path(path))
end
Then /^(?:|I )should see "([^"]*)"$/ do |text|
expect(page).to have_content(text)
end
================================================
FILE: features/support/env.rb
================================================
require 'aruba/cucumber'
require 'capybara/cucumber'
require 'rspec/matchers'
$CUCUMBER=1
World(RSpec::Matchers)
Before do
aruba.config.command_launcher = ENV.fetch("DEBUG", nil) ? :debug : :spawn
@aruba_timeout_seconds = 120
end
================================================
FILE: features/support/fakeweb.rb
================================================
require 'fake_web'
FakeWeb.allow_net_connect = false
module FakeWeb
class StubSocket
def read_timeout=(_ignored)
end
def continue_timeout=(_ignored)
end
end
end
================================================
FILE: features/support/file_helpers.rb
================================================
module FileHelpers
def append_to(path, contents)
cd(".") do
File.open(path, "a") do |file|
file.puts
file.puts contents
end
end
end
def append_to_gemfile(contents)
append_to('Gemfile', contents)
end
def comment_out_gem_in_gemfile(gemname)
cd(".") do
gemfile = File.read("Gemfile")
gemfile.sub!(/^(\s*)(gem\s*['"]#{gemname})/, "\\1# \\2")
File.open("Gemfile", 'w'){ |file| file.write(gemfile) }
end
end
def read_from_web(url)
file = if url.match %r{^https?://}
Net::HTTP.get(URI.parse(url))
else
visit(url)
page.source
end
file.force_encoding("UTF-8") if file.respond_to?(:force_encoding)
end
end
World(FileHelpers)
================================================
FILE: features/support/fixtures/boot_config.txt
================================================
class Rails::Boot
def run
load_initializer
Rails::Initializer.class_eval do
def load_gems
@bundler_loaded ||= Bundler.require :default, Rails.env
end
end
Rails::Initializer.run(:set_load_path)
end
end
Rails.boot!
================================================
FILE: features/support/fixtures/gemfile.txt
================================================
source "http://rubygems.org"
gem "rails", "RAILS_VERSION"
gem "rdoc"
gem "sqlite3", "1.3.8"
================================================
FILE: features/support/fixtures/preinitializer.txt
================================================
begin
require "rubygems"
require "bundler"
rescue LoadError
raise "Could not load the bundler gem. Install it with `gem install bundler`."
end
if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24")
raise RuntimeError, "Your bundler version is too old for Rails 2.3." +
"Run `gem install bundler` to upgrade."
end
begin
# Set up load paths for all bundled gems
ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
Bundler.setup
rescue Bundler::GemNotFound
raise RuntimeError, "Bundler couldn't find some gems." +
"Did you run `bundle install`?"
end
================================================
FILE: features/support/paths.rb
================================================
module NavigationHelpers
# Maps a name to a path. Used by the
#
# When /^I go to (.+)$/ do |page_name|
#
# step definition in web_steps.rb
#
def path_to(page_name)
case page_name
when /the home\s?page/
'/'
when /the new user page/
'/users/new'
else
begin
page_name =~ /the (.*) page/
path_components = $1.split(/\s+/)
self.send(path_components.push('path').join('_').to_sym)
rescue Object
raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
end
World(NavigationHelpers)
================================================
FILE: features/support/rails.rb
================================================
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..')).freeze
APP_NAME = 'testapp'.freeze
BUNDLE_ENV_VARS = %w(RUBYOPT BUNDLE_PATH BUNDLE_BIN_PATH BUNDLE_GEMFILE)
ORIGINAL_BUNDLE_VARS = Hash[ENV.select{ |key,value| BUNDLE_ENV_VARS.include?(key) }]
ENV['RAILS_ENV'] = 'test'
Before do
gemfile = ENV['BUNDLE_GEMFILE'].to_s
ENV['BUNDLE_GEMFILE'] = File.join(Dir.pwd, gemfile) unless gemfile.start_with?(Dir.pwd)
@framework_version = nil
end
After do
ORIGINAL_BUNDLE_VARS.each_pair do |key, value|
ENV[key] = value
end
end
When /^I reset Bundler environment variable$/ do
BUNDLE_ENV_VARS.each do |key|
ENV[key] = nil
end
end
module RailsCommandHelpers
def framework_version?(version_string)
framework_version =~ /^#{version_string}/
end
def framework_version
@framework_version ||= `rails -v`[/^Rails (.+)$/, 1]
end
def framework_major_version
framework_version.split(".").first.to_i
end
end
World(RailsCommandHelpers)
================================================
FILE: features/support/selectors.rb
================================================
module HtmlSelectorsHelpers
# Maps a name to a selector. Used primarily by the
#
# When /^(.+) within (.+)$/ do |step, scope|
#
# step definitions in web_steps.rb
#
def selector_for(locator)
case locator
when "the page"
"html > body"
else
raise "Can't find mapping from \"#{locator}\" to a selector.\n" +
"Now, go and add a mapping in #{__FILE__}"
end
end
end
World(HtmlSelectorsHelpers)
================================================
FILE: gemfiles/4.2.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "sqlite3", "~> 1.3.8", :platforms => :ruby
gem "pry"
gem "rails", "~> 4.2.0"
group :development, :test do
gem "activerecord-import"
gem "mime-types"
gem "builder"
gem "rubocop", :require => false
gem "rspec"
end
gemspec :path => "../"
================================================
FILE: gemfiles/5.0.gemfile
================================================
# This file was generated by Appraisal
source "https://rubygems.org"
gem "sqlite3", "~> 1.3.8", :platforms => :ruby
gem "pry"
gem "rails", "~> 5.0.0"
group :development, :test do
gem "activerecord-import"
gem "mime-types"
gem "builder"
gem "rubocop", :require => false
gem "rspec"
end
gemspec :path => "../"
================================================
FILE: lib/generators/paperclip/USAGE
================================================
Description:
Explain the generator
Example:
rails generate paperclip Thing
This will create:
what/will/it/create
================================================
FILE: lib/generators/paperclip/paperclip_generator.rb
================================================
require 'rails/generators/active_record'
class PaperclipGenerator < ActiveRecord::Generators::Base
desc "Create a migration to add paperclip-specific fields to your model. " +
"The NAME argument is the name of your model, and the following " +
"arguments are the name of the attachments"
argument :attachment_names, :required => true, :type => :array, :desc => "The names of the attachment(s) to add.",
:banner => "attachment_one attachment_two attachment_three ..."
def self.source_root
@source_root ||= File.expand_path('../templates', __FILE__)
end
def generate_migration
migration_template("paperclip_migration.rb.erb",
"db/migrate/#{migration_file_name}",
migration_version: migration_version)
end
def migration_name
"add_attachment_#{attachment_names.join("_")}_to_#{name.underscore.pluralize}"
end
def migration_file_name
"#{migration_name}.rb"
end
def migration_class_name
migration_name.camelize
end
def migration_version
if Rails.version.start_with? "5"
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
end
end
end
================================================
FILE: lib/generators/paperclip/templates/paperclip_migration.rb.erb
================================================
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
def self.up
change_table :<%= table_name %> do |t|
<% attachment_names.each do |attachment| -%>
t.attachment :<%= attachment %>
<% end -%>
end
end
def self.down
<% attachment_names.each do |attachment| -%>
remove_attachment :<%= table_name %>, :<%= attachment %>
<% end -%>
end
end
================================================
FILE: lib/paperclip/attachment.rb
================================================
require 'uri'
require 'paperclip/url_generator'
require 'active_support/deprecation'
require 'active_support/core_ext/string/inflections'
module Paperclip
# The Attachment class manages the files for a given attachment. It saves
# when the model saves, deletes when the model is destroyed, and processes
# the file upon assignment.
class Attachment
def self.default_options
@default_options ||= {
:convert_options => {},
:default_style => :original,
:default_url => "/:attachment/:style/missing.png",
:escape_url => true,
:restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
:filename_cleaner => nil,
:hash_data => ":class/:attachment/:id/:style/:updated_at",
:hash_digest => "SHA1",
:interpolator => Paperclip::Interpolations,
:only_process => [],
:path => ":rails_root/public:url",
:preserve_files => false,
:processors => [:thumbnail],
:source_file_options => {},
:storage => :filesystem,
:styles => {},
:url => "/system/:class/:attachment/:id_partition/:style/:filename",
:url_generator => Paperclip::UrlGenerator,
:use_default_time_zone => true,
:use_timestamp => true,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
:validate_media_type => true,
:adapter_options => { hash_digest: Digest::MD5 },
:check_validity_before_processing => true
}
end
attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny,
:options, :interpolator, :source_file_options
attr_accessor :post_processing
# Creates an Attachment object. +name+ is the name of the attachment,
# +instance+ is the model object instance it's attached to, and
# +options+ is the same as the hash passed to +has_attached_file+.
#
# Options include:
#
# +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
# +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
# +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
# +only_process+ - style args to be run through the post-processor. This defaults to the empty list (which is
# a special case that indicates all styles should be processed)
# +default_url+ - a URL for the missing image
# +default_style+ - the style to use when an argument is not specified e.g. #url, #path
# +storage+ - the storage mechanism. Defaults to :filesystem
# +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
# +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
# +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
# +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
# +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
# +hash_secret+ - a secret passed to the +hash_digest+
# +convert_options+ - flags passed to the +convert+ command for processing
# +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
# +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
# +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
# +filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
# +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
# +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
# +escape_url+ - Perform URI escaping to URLs. Defaults to true
def initialize(name, instance, options = {})
@name = name.to_sym
@name_string = name.to_s
@instance = instance
options = self.class.default_options.deep_merge(options)
@options = options
@post_processing = true
@queued_for_delete = []
@queued_for_write = {}
@errors = {}
@dirty = false
@interpolator = options[:interpolator]
@url_generator = options[:url_generator].new(self)
@source_file_options = options[:source_file_options]
@whiny = options[:whiny]
initialize_storage
end
# What gets called when you call instance.attachment = File. It clears
# errors, assigns attributes, and processes the file. It also queues up the
# previous file for deletion, to be flushed away on #save of its host. In
# addition to form uploads, you can also assign another Paperclip
# attachment:
# new_user.avatar = old_user.avatar
def assign(uploaded_file)
@file = Paperclip.io_adapters.for(uploaded_file,
@options[:adapter_options])
ensure_required_accessors!
ensure_required_validations!
if @file.assignment?
clear(*only_process)
if @file.nil?
nil
else
assign_attributes
post_process_file
reset_file_if_original_reprocessed
end
else
nil
end
end
# Returns the public URL of the attachment with a given style. This does
# not necessarily need to point to a file that your Web server can access
# and can instead point to an action in your app, for example for fine grained
# security; this has a serious performance tradeoff.
#
# Options:
#
# +timestamp+ - Add a timestamp to the end of the URL. Default: true.
# +escape+ - Perform URI escaping to the URL. Default: true.
#
# Global controls (set on has_attached_file):
#
# +interpolator+ - The object that fills in a URL pattern's variables.
# +default_url+ - The image to show when the attachment has no image.
# +url+ - The URL for a saved image.
# +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
#
# As mentioned just above, the object that generates this URL can be passed
# in, for finer control. This object must respond to two methods:
#
# +#new(Paperclip::Attachment, options_hash)+
# +#for(style_name, options_hash)+
def url(style_name = default_style, options = {})
if options == true || options == false # Backwards compatibility.
@url_generator.for(style_name, default_options.merge(:timestamp => options))
else
@url_generator.for(style_name, default_options.merge(options))
end
end
def default_options
{
:timestamp => @options[:use_timestamp],
:escape => @options[:escape_url]
}
end
# Alias to +url+ that allows using the expiring_url method provided by the cloud
# storage implementations, but keep using filesystem storage for development and
# testing.
def expiring_url(time = 3600, style_name = default_style)
url(style_name)
end
# Returns the path of the attachment as defined by the :path option. If the
# file is stored in the filesystem the path refers to the path of the file
# on disk. If the file is stored in S3, the path is the "key" part of the
# URL, and the :bucket option refers to the S3 bucket.
def path(style_name = default_style)
path = original_filename.nil? ? nil : interpolate(path_option, style_name)
path.respond_to?(:unescape) ? path.unescape : path
end
# :nodoc:
def staged_path(style_name = default_style)
if staged?
@queued_for_write[style_name].path
end
end
# :nodoc:
def staged?
! @queued_for_write.empty?
end
# Alias to +url+
def to_s style_name = default_style
url(style_name)
end
def as_json(options = nil)
to_s((options && options[:style]) || default_style)
end
def default_style
@options[:default_style]
end
def styles
if @options[:styles].respond_to?(:call) || @normalized_styles.nil?
styles = @options[:styles]
styles = styles.call(self) if styles.respond_to?(:call)
@normalized_styles = styles.dup
styles.each_pair do |name, options|
@normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
end
end
@normalized_styles
end
def only_process
only_process = @options[:only_process].dup
only_process = only_process.call(self) if only_process.respond_to?(:call)
only_process.map(&:to_sym)
end
def processors
processing_option = @options[:processors]
if processing_option.respond_to?(:call)
processing_option.call(instance)
else
processing_option
end
end
# Returns an array containing the errors on this attachment.
def errors
@errors
end
# Returns true if there are changes that need to be saved.
def dirty?
@dirty
end
# Saves the file, if there are no errors. If there are, it flushes them to
# the instance's errors and returns false, cancelling the save.
def save
flush_deletes unless @options[:keep_old_files]
process = only_process
if process.any? && !process.include?(:original)
@queued_for_write.except!(:original)
end
flush_writes
@dirty = false
true
end
# Clears out the attachment. Has the same effect as previously assigning
# nil to the attachment. Does NOT save. If you wish to clear AND save,
# use #destroy.
def clear(*styles_to_clear)
if styles_to_clear.any?
queue_some_for_delete(*styles_to_clear)
else
queue_all_for_delete
@queued_for_write = {}
@errors = {}
end
end
# Destroys the attachment. Has the same effect as previously assigning
# nil to the attachment *and saving*. This is permanent. If you wish to
# wipe out the existing attachment but not save, use #clear.
def destroy
clear
save
end
# Returns the uploaded file if present.
def uploaded_file
instance_read(:uploaded_file)
end
# Returns the name of the file as originally assigned, and lives in the
# _file_name attribute of the model.
def original_filename
instance_read(:file_name)
end
# Returns the size of the file as originally assigned, and lives in the
# _file_size attribute of the model.
def size
instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
end
# Returns the fingerprint of the file, if one's defined. The fingerprint is
# stored in the _fingerprint attribute of the model.
def fingerprint
instance_read(:fingerprint)
end
# Returns the content_type of the file as originally assigned, and lives
# in the _content_type attribute of the model.
def content_type
instance_read(:content_type)
end
# Returns the creation time of the file as originally assigned, and
# lives in the _created_at attribute of the model.
def created_at
if able_to_store_created_at?
time = instance_read(:created_at)
time && time.to_f.to_i
end
end
# Returns the last modified time of the file as originally assigned, and
# lives in the _updated_at attribute of the model.
def updated_at
time = instance_read(:updated_at)
time && time.to_f.to_i
end
# The time zone to use for timestamp interpolation. Using the default
# time zone ensures that results are consistent across all threads.
def time_zone
@options[:use_default_time_zone] ? Time.zone_default : Time.zone
end
# Returns a unique hash suitable for obfuscating the URL of an otherwise
# publicly viewable attachment.
def hash_key(style_name = default_style)
raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret]
require 'openssl' unless defined?(OpenSSL)
data = interpolate(@options[:hash_data], style_name)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options[:hash_digest]).new, @options[:hash_secret], data)
end
# This method really shouldn't be called that often. Its expected use is
# in the paperclip:refresh rake task and that's it. It will regenerate all
# thumbnails forcefully, by reobtaining the original file and going through
# the post-process again.
# NOTE: Calling reprocess WILL NOT delete existing files. This is due to
# inconsistencies in timing of S3 commands. It's possible that calling
# #reprocess! will lose data if the files are not kept.
def reprocess!(*style_args)
saved_flags = @options.slice(
:only_process,
:preserve_files,
:check_validity_before_processing
)
@options[:only_process] = style_args
@options[:preserve_files] = true
@options[:check_validity_before_processing] = false
begin
assign(self)
save
instance.save
rescue Errno::EACCES => e
warn "#{e} - skipping file."
false
ensure
@options.merge!(saved_flags)
end
end
# Returns true if a file has been assigned.
def file?
original_filename.present?
end
alias :present? :file?
def blank?
not present?
end
# Determines whether the instance responds to this attribute. Used to prevent
# calculations on fields we won't even store.
def instance_respond_to?(attr)
instance.respond_to?(:"#{name}_#{attr}")
end
# Writes the attachment-specific attribute on the instance. For example,
# instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
# "avatar_file_name" field (assuming the attachment is called avatar).
def instance_write(attr, value)
setter = :"#{@name_string}_#{attr}="
if instance.respond_to?(setter)
instance.send(setter, value)
end
end
# Reads the attachment-specific attribute on the instance. See instance_write
# for more details.
def instance_read(attr)
getter = :"#{@name_string}_#{attr}"
if instance.respond_to?(getter)
instance.send(getter)
end
end
private
def path_option
@options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
end
def active_validator_classes
@instance.class.validators.map(&:class)
end
def missing_required_validator?
(active_validator_classes.flat_map(&:ancestors) & Paperclip::REQUIRED_VALIDATORS).empty?
end
def ensure_required_validations!
if missing_required_validator?
raise Paperclip::Errors::MissingRequiredValidatorError
end
end
def ensure_required_accessors! #:nodoc:
%w(file_name).each do |field|
unless @instance.respond_to?("#{@name_string}_#{field}") && @instance.respond_to?("#{@name_string}_#{field}=")
raise Paperclip::Error.new("#{@instance.class} model missing required attr_accessor for '#{@name_string}_#{field}'")
end
end
end
def log message #:nodoc:
Paperclip.log(message)
end
def initialize_storage #:nodoc:
storage_class_name = @options[:storage].to_s.downcase.camelize
begin
storage_module = Paperclip::Storage.const_get(storage_class_name)
rescue NameError
raise Errors::StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
end
self.extend(storage_module)
end
def assign_attributes
@queued_for_write[:original] = @file
assign_file_information
assign_fingerprint { @file.fingerprint }
assign_timestamps
end
def assign_file_information
instance_write(:file_name, cleanup_filename(@file.original_filename))
instance_write(:content_type, @file.content_type.to_s.strip)
instance_write(:file_size, @file.size)
end
def assign_fingerprint
if instance_respond_to?(:fingerprint)
instance_write(:fingerprint, yield)
end
end
def assign_timestamps
if has_enabled_but_unset_created_at?
instance_write(:created_at, Time.now)
end
instance_write(:updated_at, Time.now)
end
def post_process_file
dirty!
if post_processing
post_process(*only_process)
end
end
def dirty!
@dirty = true
end
def reset_file_if_original_reprocessed
instance_write(:file_size, @queued_for_write[:original].size)
assign_fingerprint { @queued_for_write[:original].fingerprint }
reset_updater
end
def reset_updater
if instance.respond_to?(updater)
instance.send(updater)
end
end
def updater
:"#{name}_file_name_will_change!"
end
def extra_options_for(style) #:nodoc:
process_options(:convert_options, style)
end
def extra_source_file_options_for(style) #:nodoc:
process_options(:source_file_options, style)
end
def process_options(options_type, style) #:nodoc:
all_options = @options[options_type][:all]
all_options = all_options.call(instance) if all_options.respond_to?(:call)
style_options = @options[options_type][style]
style_options = style_options.call(instance) if style_options.respond_to?(:call)
[ style_options, all_options ].compact.join(" ")
end
def post_process(*style_args) #:nodoc:
return if @queued_for_write[:original].nil?
instance.run_paperclip_callbacks(:post_process) do
instance.run_paperclip_callbacks(:"#{name}_post_process") do
if !@options[:check_validity_before_processing] || !instance.errors.any?
post_process_styles(*style_args)
end
end
end
end
def post_process_styles(*style_args) #:nodoc:
post_process_style(:original, styles[:original]) if styles.include?(:original) && process_style?(:original, style_args)
styles.reject{ |name, style| name == :original }.each do |name, style|
post_process_style(name, style) if process_style?(name, style_args)
end
end
def post_process_style(name, style) #:nodoc:
begin
raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
intermediate_files = []
original = @queued_for_write[:original]
@queued_for_write[name] = style.processors.
reduce(original) do |file, processor|
file = Paperclip.processor(processor).make(file, style.processor_options, self)
intermediate_files << file unless file == @queued_for_write[:original]
# if we're processing the original, close + unlink the source tempfile
if name == :original
@queued_for_write[:original].close(true)
end
file
end
unadapted_file = @queued_for_write[name]
@queued_for_write[name] = Paperclip.io_adapters.
for(@queued_for_write[name], @options[:adapter_options])
unadapted_file.close if unadapted_file.respond_to?(:close)
@queued_for_write[name]
rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e
log("An error was received while processing: #{e.inspect}")
(@errors[:processing] ||= []) << e.message if @options[:whiny]
ensure
unlink_files(intermediate_files)
end
end
def process_style?(style_name, style_args) #:nodoc:
style_args.empty? || style_args.include?(style_name)
end
def interpolate(pattern, style_name = default_style) #:nodoc:
interpolator.interpolate(pattern, self, style_name)
end
def queue_some_for_delete(*styles)
@queued_for_delete += styles.uniq.map do |style|
path(style) if exists?(style)
end.compact
end
def queue_all_for_delete #:nodoc:
return if !file?
unless @options[:preserve_files]
@queued_for_delete += [:original, *styles.keys].uniq.map do |style|
path(style) if exists?(style)
end.compact
end
instance_write(:file_name, nil)
instance_write(:content_type, nil)
instance_write(:file_size, nil)
instance_write(:fingerprint, nil)
instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
instance_write(:updated_at, nil)
end
def flush_errors #:nodoc:
@errors.each do |error, message|
[message].flatten.each {|m| instance.errors.add(name, m) }
end
end
# called by storage after the writes are flushed and before @queued_for_write is cleared
def after_flush_writes
unlink_files(@queued_for_write.values)
end
def unlink_files(files)
Array(files).each do |file|
file.close unless file.closed?
begin
file.unlink if file.respond_to?(:unlink)
rescue Errno::ENOENT
end
end
end
# You can either specifiy :restricted_characters or you can define your own
# :filename_cleaner object. This object needs to respond to #call and takes
# the filename that will be cleaned. It should return the cleaned filename.
def filename_cleaner
@options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
end
def cleanup_filename(filename)
filename_cleaner.call(filename)
end
# Check if attachment database table has a created_at field
def able_to_store_created_at?
@instance.respond_to?("#{name}_created_at".to_sym)
end
# Check if attachment database table has a created_at field which is not yet set
def has_enabled_but_unset_created_at?
able_to_store_created_at? && !instance_read(:created_at)
end
end
end
================================================
FILE: lib/paperclip/attachment_registry.rb
================================================
require 'singleton'
module Paperclip
class AttachmentRegistry
include Singleton
def self.register(klass, attachment_name, attachment_options)
instance.register(klass, attachment_name, attachment_options)
end
def self.clear
instance.clear
end
def self.names_for(klass)
instance.names_for(klass)
end
def self.each_definition(&block)
instance.each_definition(&block)
end
def self.definitions_for(klass)
instance.definitions_for(klass)
end
def initialize
clear
end
def register(klass, attachment_name, attachment_options)
@attachments ||= {}
@attachments[klass] ||= {}
@attachments[klass][attachment_name] = attachment_options
end
def clear
@attachments = Hash.new { |h,k| h[k] = {} }
end
def names_for(klass)
@attachments[klass].keys
end
def each_definition
@attachments.each do |klass, attachments|
attachments.each do |name, options|
yield klass, name, options
end
end
end
def definitions_for(klass)
parent_classes = klass.ancestors.reverse
parent_classes.each_with_object({}) do |ancestor, inherited_definitions|
inherited_definitions.deep_merge! @attachments[ancestor]
end
end
end
end
================================================
FILE: lib/paperclip/callbacks.rb
================================================
module Paperclip
module Callbacks
def self.included(base)
base.extend(Defining)
base.send(:include, Running)
end
module Defining
def define_paperclip_callbacks(*callbacks)
define_callbacks(*[callbacks, { terminator: hasta_la_vista_baby }].flatten)
callbacks.each do |callback|
eval <<-end_callbacks
def before_#{callback}(*args, &blk)
set_callback(:#{callback}, :before, *args, &blk)
end
def after_#{callback}(*args, &blk)
set_callback(:#{callback}, :after, *args, &blk)
end
end_callbacks
end
end
private
def hasta_la_vista_baby
lambda do |_, result|
if result.respond_to?(:call)
result.call == false
else
result == false
end
end
end
end
module Running
def run_paperclip_callbacks(callback, &block)
run_callbacks(callback, &block)
end
end
end
end
================================================
FILE: lib/paperclip/content_type_detector.rb
================================================
module Paperclip
class ContentTypeDetector
# The content-type detection strategy is as follows:
#
# 1. Blank/Empty files: If there's no filepath or the file is empty,
# provide a sensible default (application/octet-stream or inode/x-empty)
#
# 2. Calculated match: Return the first result that is found by both the
# `file` command and MIME::Types.
#
# 3. Standard types: Return the first standard (without an x- prefix) entry
# in MIME::Types
#
# 4. Experimental types: If there were no standard types in MIME::Types
# list, try to return the first experimental one
#
# 5. Raw `file` command: Just use the output of the `file` command raw, or
# a sensible default. This is cached from Step 2.
EMPTY_TYPE = "inode/x-empty"
SENSIBLE_DEFAULT = "application/octet-stream"
def initialize(filepath)
@filepath = filepath
end
# Returns a String describing the file's content type
def detect
if blank_name?
SENSIBLE_DEFAULT
elsif empty_file?
EMPTY_TYPE
elsif calculated_type_matches.any?
calculated_type_matches.first
else
type_from_file_contents || SENSIBLE_DEFAULT
end.to_s
end
private
def blank_name?
@filepath.nil? || @filepath.empty?
end
def empty_file?
File.exist?(@filepath) && File.size(@filepath) == 0
end
alias :empty? :empty_file?
def calculated_type_matches
possible_types.select do |content_type|
content_type == type_from_file_contents
end
end
def possible_types
MIME::Types.type_for(@filepath).collect(&:content_type)
end
def type_from_file_contents
type_from_mime_magic || type_from_file_command
rescue Errno::ENOENT => e
Paperclip.log("Error while determining content type: #{e}")
SENSIBLE_DEFAULT
end
def type_from_mime_magic
@type_from_mime_magic ||= File.open(@filepath) do |file|
MimeMagic.by_magic(file).try(:type)
end
end
def type_from_file_command
@type_from_file_command ||=
FileCommandContentTypeDetector.new(@filepath).detect
end
end
end
================================================
FILE: lib/paperclip/errors.rb
================================================
module Paperclip
# A base error class for Paperclip. Most of the error that will be thrown
# from Paperclip will inherits from this class.
class Error < StandardError
end
module Errors
# Will be thrown when a storage method is not found.
class StorageMethodNotFound < Paperclip::Error
end
# Will be thrown when a command or executable is not found.
class CommandNotFoundError < Paperclip::Error
end
# Attachments require a content_type or file_name validator,
# or to have explicitly opted out of them.
class MissingRequiredValidatorError < Paperclip::Error
end
# Will be thrown when ImageMagic cannot determine the uploaded file's
# metadata, usually this would mean the file is not an image. If you are
# consistently receiving this error on PDFs make sure that you have
# installed Ghostscript.
class NotIdentifiedByImageMagickError < Paperclip::Error
end
# Will be thrown if the interpolation is creating an infinite loop. If you
# are creating an interpolator which might cause an infinite loop, you
# should be throwing this error upon the infinite loop as well.
class InfiniteInterpolationError < Paperclip::Error
end
end
end
================================================
FILE: lib/paperclip/file_command_content_type_detector.rb
================================================
module Paperclip
class FileCommandContentTypeDetector
SENSIBLE_DEFAULT = "application/octet-stream"
def initialize(filename)
@filename = filename
end
def detect
type_from_file_command
end
private
def type_from_file_command
# On BSDs, `file` doesn't give a result code of 1 if the file doesn't exist.
type = begin
Paperclip.run("file", "-b --mime :file", file: @filename)
rescue Terrapin::CommandLineError => e
Paperclip.log("Error while determining content type: #{e}")
SENSIBLE_DEFAULT
end
if type.nil? || type.match(/\(.*?\)/)
type = SENSIBLE_DEFAULT
end
type.split(/[:;\s]+/)[0]
end
end
end
================================================
FILE: lib/paperclip/filename_cleaner.rb
================================================
module Paperclip
class FilenameCleaner
def initialize(invalid_character_regex)
@invalid_character_regex = invalid_character_regex
end
def call(filename)
if @invalid_character_regex
filename.gsub(@invalid_character_regex, "_")
else
filename
end
end
end
end
================================================
FILE: lib/paperclip/geometry.rb
================================================
module Paperclip
# Defines the geometry of an image.
class Geometry
attr_accessor :height, :width, :modifier
EXIF_ROTATED_ORIENTATION_VALUES = [5, 6, 7, 8]
# Gives a Geometry representing the given height and width
def initialize(width = nil, height = nil, modifier = nil)
if width.is_a?(Hash)
options = width
@height = options[:height].to_f
@width = options[:width].to_f
@modifier = options[:modifier]
@orientation = options[:orientation].to_i
else
@height = height.to_f
@width = width.to_f
@modifier = modifier
end
end
# Extracts the Geometry from a file (or path to a file)
def self.from_file(file)
GeometryDetector.new(file).make
end
# Extracts the Geometry from a "WxH,O" string
# Where W is the width, H is the height,
# and O is the EXIF orientation
def self.parse(string)
GeometryParser.new(string).make
end
# Swaps the height and width if necessary
def auto_orient
if EXIF_ROTATED_ORIENTATION_VALUES.include?(@orientation)
@height, @width = @width, @height
@orientation -= 4
end
end
# True if the dimensions represent a square
def square?
height == width
end
# True if the dimensions represent a horizontal rectangle
def horizontal?
height < width
end
# True if the dimensions represent a vertical rectangle
def vertical?
height > width
end
# The aspect ratio of the dimensions.
def aspect
width / height
end
# Returns the larger of the two dimensions
def larger
[height, width].max
end
# Returns the smaller of the two dimensions
def smaller
[height, width].min
end
# Returns the width and height in a format suitable to be passed to Geometry.parse
def to_s
s = ""
s << width.to_i.to_s if width > 0
s << "x#{height.to_i}" if height > 0
s << modifier.to_s
s
end
# Same as to_s
def inspect
to_s
end
# Returns the scaling and cropping geometries (in string-based ImageMagick format)
# neccessary to transform this Geometry into the Geometry given. If crop is true,
# then it is assumed the destination Geometry will be the exact final resolution.
# In this case, the source Geometry is scaled so that an image containing the
# destination Geometry would be completely filled by the source image, and any
# overhanging image would be cropped. Useful for square thumbnail images. The cropping
# is weighted at the center of the Geometry.
def transformation_to dst, crop = false
if crop
ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
scale_geometry, scale = scaling(dst, ratio)
crop_geometry = cropping(dst, ratio, scale)
else
scale_geometry = dst.to_s
end
[ scale_geometry, crop_geometry ]
end
# resize to a new geometry
# @param geometry [String] the Paperclip geometry definition to resize to
# @example
# Paperclip::Geometry.new(150, 150).resize_to('50x50!')
# #=> Paperclip::Geometry(50, 50)
def resize_to(geometry)
new_geometry = Paperclip::Geometry.parse geometry
case new_geometry.modifier
when '!', '#'
new_geometry
when '>'
if new_geometry.width >= self.width && new_geometry.height >= self.height
self
else
scale_to new_geometry
end
when '<'
if new_geometry.width <= self.width || new_geometry.height <= self.height
self
else
scale_to new_geometry
end
else
scale_to new_geometry
end
end
private
def scaling dst, ratio
if ratio.horizontal? || ratio.square?
[ "%dx" % dst.width, ratio.width ]
else
[ "x%d" % dst.height, ratio.height ]
end
end
def cropping dst, ratio, scale
if ratio.horizontal? || ratio.square?
"%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
else
"%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
end
end
# scale to the requested geometry and preserve the aspect ratio
def scale_to(new_geometry)
scale = [new_geometry.width.to_f / self.width.to_f , new_geometry.height.to_f / self.height.to_f].min
Paperclip::Geometry.new((self.width * scale).round, (self.height * scale).round)
end
end
end
================================================
FILE: lib/paperclip/geometry_detector_factory.rb
================================================
module Paperclip
class GeometryDetector
def initialize(file)
@file = file
raise_if_blank_file
end
def make
geometry = GeometryParser.new(geometry_string.strip).make
geometry || raise(Errors::NotIdentifiedByImageMagickError.new)
end
private
def geometry_string
begin
orientation = Paperclip.options[:use_exif_orientation] ?
"%[exif:orientation]" : "1"
Paperclip.run(
Paperclip.options[:is_windows] ? "magick identify" : "identify",
"-format '%wx%h,#{orientation}' :file", {
:file => "#{path}[0]"
}, {
:swallow_stderr => true
}
)
rescue Terrapin::ExitStatusError
""
rescue Terrapin::CommandNotFoundError => e
raise_because_imagemagick_missing
end
end
def path
@file.respond_to?(:path) ? @file.path : @file
end
def raise_if_blank_file
if path.blank?
raise Errors::NotIdentifiedByImageMagickError.new("Cannot find the geometry of a file with a blank name")
end
end
def raise_because_imagemagick_missing
raise Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
end
end
end
================================================
FILE: lib/paperclip/geometry_parser_factory.rb
================================================
module Paperclip
class GeometryParser
FORMAT = /\b(\d*)x?(\d*)\b(?:,(\d?))?(\@\>|\>\@|[\>\<\#\@\%^!])?/i
def initialize(string)
@string = string
end
def make
if match
Geometry.new(
:height => @height,
:width => @width,
:modifier => @modifier,
:orientation => @orientation
)
end
end
private
def match
if actual_match = @string && @string.match(FORMAT)
@width = actual_match[1]
@height = actual_match[2]
@orientation = actual_match[3]
@modifier = actual_match[4]
end
actual_match
end
end
end
================================================
FILE: lib/paperclip/glue.rb
================================================
require 'paperclip/callbacks'
require 'paperclip/validators'
require 'paperclip/schema'
module Paperclip
module Glue
def self.included(base)
base.extend ClassMethods
base.send :include, Callbacks
base.send :include, Validators
base.send :include, Schema if defined? ActiveRecord::Base
locale_path = Dir.glob(File.dirname(__FILE__) + "/locales/*.{rb,yml}")
I18n.load_path += locale_path unless I18n.load_path.include?(locale_path)
end
end
end
================================================
FILE: lib/paperclip/has_attached_file.rb
================================================
module Paperclip
class HasAttachedFile
def self.define_on(klass, name, options)
new(klass, name, options).define
end
def initialize(klass, name, options)
@klass = klass
@name = name
@options = options
end
def define
define_flush_errors
define_getters
define_setter
define_query
register_new_attachment
add_active_record_callbacks
add_paperclip_callbacks
add_required_validations
end
private
def define_flush_errors
@klass.send(:validates_each, @name) do |record, attr, value|
attachment = record.send(@name)
attachment.send(:flush_errors)
end
end
def define_getters
define_instance_getter
define_class_getter
end
def define_instance_getter
name = @name
options = @options
@klass.send :define_method, @name do |*args|
ivar = "@attachment_#{name}"
attachment = instance_variable_get(ivar)
if attachment.nil?
attachment = Attachment.new(name, self, options)
instance_variable_set(ivar, attachment)
end
if args.length > 0
attachment.to_s(args.first)
else
attachment
end
end
end
def define_class_getter
@klass.extend(ClassMethods)
end
def define_setter
name = @name
@klass.send :define_method, "#{@name}=" do |file|
send(name).assign(file)
end
end
def define_query
name = @name
@klass.send :define_method, "#{@name}?" do
send(name).file?
end
end
def register_new_attachment
Paperclip::AttachmentRegistry.register(@klass, @name, @options)
end
def add_required_validations
options = Paperclip::Attachment.default_options.deep_merge(@options)
if options[:validate_media_type] != false
name = @name
@klass.validates_media_type_spoof_detection name,
:if => ->(instance){ instance.send(name).dirty? }
end
end
def add_active_record_callbacks
name = @name
@klass.send(:after_save) { send(name).send(:save) }
@klass.send(:before_destroy) { send(name).send(:queue_all_for_delete) }
if @klass.respond_to?(:after_commit)
@klass.send(:after_commit, on: :destroy) do
send(name).send(:flush_deletes)
end
else
@klass.send(:after_destroy) { send(name).send(:flush_deletes) }
end
end
def add_paperclip_callbacks
@klass.send(
:define_paperclip_callbacks,
:post_process, :"#{@name}_post_process")
end
module ClassMethods
def attachment_definitions
Paperclip::AttachmentRegistry.definitions_for(self)
end
end
end
end
================================================
FILE: lib/paperclip/helpers.rb
================================================
module Paperclip
module Helpers
def configure
yield(self) if block_given?
end
def interpolates key, &block
Paperclip::Interpolations[key] = block
end
# The run method takes the name of a binary to run, the arguments
# to that binary, the values to interpolate and some local options.
#
# :cmd -> The name of a binary to run.
#
# :arguments -> The command line arguments to that binary.
#
# :interpolation_values -> Values to be interpolated into the arguments.
#
# :local_options -> The options to be used by Cocain::CommandLine.
# These could be: runner
# logger
# swallow_stderr
# expected_outcodes
# environment
# runner_options
#
def run(cmd, arguments = "", interpolation_values = {}, local_options = {})
command_path = options[:command_path]
terrapin_path_array = Terrapin::CommandLine.path.try(:split, Terrapin::OS.path_separator)
Terrapin::CommandLine.path = [terrapin_path_array, command_path].flatten.compact.uniq
if logging? && (options[:log_command] || local_options[:log_command])
local_options = local_options.merge(:logger => logger)
end
Terrapin::CommandLine.new(cmd, arguments, local_options).run(interpolation_values)
end
# Find all instances of the given Active Record model +klass+ with attachment +name+.
# This method is used by the refresh rake tasks.
def each_instance_with_attachment(klass, name)
class_for(klass).unscoped.where("#{name}_file_name IS NOT NULL").find_each do |instance|
yield(instance)
end
end
def class_for(class_name)
class_name.split('::').inject(Object) do |klass, partial_class_name|
if klass.const_defined?(partial_class_name)
klass.const_get(partial_class_name, false)
else
klass.const_missing(partial_class_name)
end
end
end
def reset_duplicate_clash_check!
@names_url = nil
end
end
end
================================================
FILE: lib/paperclip/interpolations/plural_cache.rb
================================================
module Paperclip
module Interpolations
class PluralCache
def initialize
@symbol_cache = {}.compare_by_identity
@klass_cache = {}.compare_by_identity
end
def pluralize_symbol(symbol)
@symbol_cache[symbol] ||= symbol.to_s.downcase.pluralize
end
def underscore_and_pluralize_class(klass)
@klass_cache[klass] ||= klass.name.underscore.pluralize
end
end
end
end
================================================
FILE: lib/paperclip/interpolations.rb
================================================
module Paperclip
# This module contains all the methods that are available for interpolation
# in paths and urls. To add your own (or override an existing one), you
# can either open this module and define it, or call the
# Paperclip.interpolates method.
module Interpolations
extend self
ID_PARTITION_LIMIT = 1_000_000_000
# Hash assignment of interpolations. Included only for compatibility,
# and is not intended for normal use.
def self.[]= name, block
define_method(name, &block)
@interpolators_cache = nil
end
# Hash access of interpolations. Included only for compatibility,
# and is not intended for normal use.
def self.[] name
method(name)
end
# Returns a sorted list of all interpolations.
def self.all
self.instance_methods(false).sort!
end
# Perform the actual interpolation. Takes the pattern to interpolate
# and the arguments to pass, which are the attachment and style name.
# You can pass a method name on your record as a symbol, which should turn
# an interpolation pattern for Paperclip to use.
def self.interpolate pattern, *args
pattern = args.first.instance.send(pattern) if pattern.kind_of? Symbol
result = pattern.dup
interpolators_cache.each do |method, token|
result.gsub!(token) { send(method, *args) } if result.include?(token)
end
result
end
def self.interpolators_cache
@interpolators_cache ||= all.reverse!.map! { |method| [method, ":#{method}"] }
end
def self.plural_cache
@plural_cache ||= PluralCache.new
end
# Returns the filename, the same way as ":basename.:extension" would.
def filename attachment, style_name
[ basename(attachment, style_name), extension(attachment, style_name) ].delete_if(&:empty?).join(".".freeze)
end
# Returns the interpolated URL. Will raise an error if the url itself
# contains ":url" to prevent infinite recursion. This interpolation
# is used in the default :path to ease default specifications.
RIGHT_HERE = "#{__FILE__.gsub(%r{\A\./}, "")}:#{__LINE__ + 3}"
def url attachment, style_name
raise Errors::InfiniteInterpolationError if caller.any?{|b| b.index(RIGHT_HERE) }
attachment.url(style_name, :timestamp => false, :escape => false)
end
# Returns the timestamp as defined by the _updated_at field
# in the server default time zone unless :use_global_time_zone is set
# to false. Note that a Rails.config.time_zone change will still
# invalidate any path or URL that uses :timestamp. For a
# time_zone-agnostic timestamp, use #updated_at.
def timestamp attachment, style_name
attachment.instance_read(:updated_at).in_time_zone(attachment.time_zone).to_s
end
# Returns an integer timestamp that is time zone-neutral, so that paths
# remain valid even if a server's time zone changes.
def updated_at attachment, style_name
attachment.updated_at
end
# Returns the Rails.root constant.
def rails_root attachment, style_name
Rails.root
end
# Returns the Rails.env constant.
def rails_env attachment, style_name
Rails.env
end
# Returns the underscored, pluralized version of the class name.
# e.g. "users" for the User class.
# NOTE: The arguments need to be optional, because some tools fetch
# all class names. Calling #class will return the expected class.
def class attachment = nil, style_name = nil
return super() if attachment.nil? && style_name.nil?
plural_cache.underscore_and_pluralize_class(attachment.instance.class)
end
# Returns the basename of the file. e.g. "file" for "file.jpg"
def basename attachment, style_name
File.basename(attachment.original_filename, ".*".freeze)
end
# Returns the extension of the file. e.g. "jpg" for "file.jpg"
# If the style has a format defined, it will return the format instead
# of the actual extension.
def extension attachment, style_name
((style = attachment.styles[style_name.to_s.to_sym]) && style[:format]) ||
File.extname(attachment.original_filename).sub(/\A\.+/, "".freeze)
end
# Returns the dot+extension of the file. e.g. ".jpg" for "file.jpg"
# If the style has a format defined, it will return the format instead
# of the actual extension. If the extension is empty, no dot is added.
def dotextension attachment, style_name
ext = extension(attachment, style_name)
ext.empty? ? ext : ".#{ext}"
end
# Returns an extension based on the content type. e.g. "jpeg" for
# "image/jpeg". If the style has a specified format, it will override the
# content-type detection.
#
# Each mime type generally has multiple extensions associated with it, so
# if the extension from the original filename is one of these extensions,
# that extension is used, otherwise, the first in the list is used.
def content_type_extension attachment, style_name
mime_type = MIME::Types[attachment.content_type]
extensions_for_mime_type = unless mime_type.empty?
mime_type.first.extensions
else
[]
end
original_extension = extension(attachment, style_name)
style = attachment.styles[style_name.to_s.to_sym]
if style && style[:format]
style[:format].to_s
elsif extensions_for_mime_type.include? original_extension
original_extension
elsif !extensions_for_mime_type.empty?
extensions_for_mime_type.first
else
# It's possible, though unlikely, that the mime type is not in the
# database, so just use the part after the '/' in the mime type as the
# extension.
%r{/([^/]*)\z}.match(attachment.content_type)[1]
end
end
# Returns the id of the instance.
def id attachment, style_name
attachment.instance.id
end
# Returns the #to_param of the instance.
def param attachment, style_name
attachment.instance.to_param
end
# Returns the fingerprint of the instance.
def fingerprint attachment, style_name
attachment.fingerprint
end
# Returns a the attachment hash. See Paperclip::Attachment#hash_key for
# more details.
def hash attachment=nil, style_name=nil
if attachment && style_name
attachment.hash_key(style_name)
else
super()
end
end
# Returns the id of the instance in a split path form. e.g. returns
# 000/001/234 for an id of 1234.
def id_partition attachment, style_name
case id = attachment.instance.id
when Integer
if id < ID_PARTITION_LIMIT
("%09d".freeze % id).scan(/\d{3}/).join("/".freeze)
else
("%012d".freeze % id).scan(/\d{3}/).join("/".freeze)
end
when String
id.scan(/.{3}/).first(3).join("/".freeze)
else
nil
end
end
# Returns the pluralized form of the attachment name. e.g.
# "avatars" for an attachment of :avatar
def attachment attachment, style_name
plural_cache.pluralize_symbol(attachment.name)
end
# Returns the style, or the default style if nil is supplied.
def style attachment, style_name
style_name || attachment.default_style
end
end
end
================================================
FILE: lib/paperclip/io_adapters/abstract_adapter.rb
================================================
require 'active_support/core_ext/module/delegation'
module Paperclip
class AbstractAdapter
OS_RESTRICTED_CHARACTERS = %r{[/:]}
attr_reader :content_type, :original_filename, :size, :tempfile
delegate :binmode, :binmode?, :close, :close!, :closed?, :eof?, :path, :readbyte, :rewind, :unlink, :to => :@tempfile
alias :length :size
def initialize(target, options = {})
@target = target
@options = options
end
def fingerprint
@fingerprint ||= begin
digest = @options.fetch(:hash_digest).new
File.open(path, "rb") do |f|
buf = ""
digest.update(buf) while f.read(16384, buf)
end
digest.hexdigest
end
end
def read(length = nil, buffer = nil)
@tempfile.read(length, buffer)
end
def inspect
"#{self.class}: #{self.original_filename}"
end
def original_filename=(new_filename)
return unless new_filename
@original_filename = new_filename.gsub(OS_RESTRICTED_CHARACTERS, "_")
end
def nil?
false
end
def assignment?
true
end
private
def destination
@destination ||= TempfileFactory.new.generate(@original_filename.to_s)
end
def copy_to_tempfile(src)
link_or_copy_file(src.path, destination.path)
destination
end
def link_or_copy_file(src, dest)
begin
Paperclip.log("Trying to link #{src} to #{dest}")
FileUtils.ln(src, dest, force: true) # overwrite existing
rescue Errno::EXDEV, Errno::EPERM, Errno::ENOENT, Errno::EEXIST => e
Paperclip.log(
"Link failed with #{e.message}; copying link #{src} to #{dest}"
)
FileUtils.cp(src, dest)
end
@destination.close
@destination.open.binmode
end
end
end
================================================
FILE: lib/paperclip/io_adapters/attachment_adapter.rb
================================================
module Paperclip
class AttachmentAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
Paperclip::Attachment === target || Paperclip::Style === target
end
end
def initialize(target, options = {})
super
@target, @style = case target
when Paperclip::Attachment
[target, :original]
when Paperclip::Style
[target.attachment, target.name]
end
cache_current_values
end
private
def cache_current_values
self.original_filename = @target.original_filename
@content_type = @target.content_type
@tempfile = copy_to_tempfile(@target)
@size = @tempfile.size || @target.size
end
def copy_to_tempfile(source)
if source.staged?
link_or_copy_file(source.staged_path(@style), destination.path)
else
begin
source.copy_to_local_file(@style, destination.path)
rescue Errno::EACCES
# clean up lingering tempfile if we cannot access source file
destination.close(true)
raise
end
end
destination
end
end
end
Paperclip::AttachmentAdapter.register
================================================
FILE: lib/paperclip/io_adapters/data_uri_adapter.rb
================================================
module Paperclip
class DataUriAdapter < StringioAdapter
def self.register
Paperclip.io_adapters.register self do |target|
String === target && target =~ REGEXP
end
end
REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m
def initialize(target_uri, options = {})
super(extract_target(target_uri), options)
end
private
def extract_target(uri)
data_uri_parts = uri.match(REGEXP) || []
StringIO.new(Base64.decode64(data_uri_parts[2] || ""))
end
end
end
================================================
FILE: lib/paperclip/io_adapters/empty_string_adapter.rb
================================================
module Paperclip
class EmptyStringAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
target.is_a?(String) && target.empty?
end
end
def nil?
false
end
def assignment?
false
end
end
end
Paperclip::EmptyStringAdapter.register
================================================
FILE: lib/paperclip/io_adapters/file_adapter.rb
================================================
module Paperclip
class FileAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
File === target || ::Tempfile === target
end
end
def initialize(target, options = {})
super
cache_current_values
end
private
def cache_current_values
if @target.respond_to?(:original_filename)
self.original_filename = @target.original_filename
end
self.original_filename ||= File.basename(@target.path)
@tempfile = copy_to_tempfile(@target)
@content_type = ContentTypeDetector.new(@target.path).detect
@size = File.size(@target)
end
end
end
Paperclip::FileAdapter.register
================================================
FILE: lib/paperclip/io_adapters/http_url_proxy_adapter.rb
================================================
module Paperclip
class HttpUrlProxyAdapter < UriAdapter
def self.register
Paperclip.io_adapters.register self do |target|
String === target && target =~ REGEXP
end
end
REGEXP = /\Ahttps?:\/\//
def initialize(target, options = {})
escaped = URI.escape(target)
super(URI(target == URI.unescape(target) ? escaped : target), options)
end
end
end
================================================
FILE: lib/paperclip/io_adapters/identity_adapter.rb
================================================
module Paperclip
class IdentityAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register Paperclip::IdentityAdapter.new do |target|
Paperclip.io_adapters.registered?(target)
end
end
def initialize
end
def new(target, _)
target
end
end
end
Paperclip::IdentityAdapter.register
================================================
FILE: lib/paperclip/io_adapters/nil_adapter.rb
================================================
module Paperclip
class NilAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
target.nil? || ((Paperclip::Attachment === target) && !target.present?)
end
end
def initialize(_target, _options = {}); end
def original_filename
""
end
def content_type
""
end
def size
0
end
def nil?
true
end
def read(*_args)
nil
end
def eof?
true
end
end
end
Paperclip::NilAdapter.register
================================================
FILE: lib/paperclip/io_adapters/registry.rb
================================================
module Paperclip
class AdapterRegistry
class NoHandlerError < Paperclip::Error; end
attr_reader :registered_handlers
def initialize
@registered_handlers = []
end
def register(handler_class, &block)
@registered_handlers << [block, handler_class]
end
def unregister(handler_class)
@registered_handlers.reject! { |_, klass| klass == handler_class }
end
def handler_for(target)
@registered_handlers.each do |tester, handler|
return handler if tester.call(target)
end
raise NoHandlerError.new("No handler found for #{target.inspect}")
end
def registered?(target)
@registered_handlers.any? do |tester, handler|
handler === target
end
end
def for(target, options = {})
handler_for(target).new(target, options)
end
end
end
================================================
FILE: lib/paperclip/io_adapters/stringio_adapter.rb
================================================
module Paperclip
class StringioAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
StringIO === target
end
end
def initialize(target, options = {})
super
cache_current_values
end
attr_writer :content_type
private
def cache_current_values
self.original_filename = @target.original_filename if @target.respond_to?(:original_filename)
self.original_filename ||= "data"
@tempfile = copy_to_tempfile(@target)
@content_type = ContentTypeDetector.new(@tempfile.path).detect
@size = @target.size
end
def copy_to_tempfile(source)
while data = source.read(16*1024)
destination.write(data)
end
destination.rewind
destination
end
end
end
Paperclip::StringioAdapter.register
================================================
FILE: lib/paperclip/io_adapters/uploaded_file_adapter.rb
================================================
module Paperclip
class UploadedFileAdapter < AbstractAdapter
def self.register
Paperclip.io_adapters.register self do |target|
target.class.name.include?("UploadedFile")
end
end
def initialize(target, options = {})
super
cache_current_values
if @target.respond_to?(:tempfile)
@tempfile = copy_to_tempfile(@target.tempfile)
else
@tempfile = copy_to_tempfile(@target)
end
end
class << self
attr_accessor :content_type_detector
end
private
def cache_current_values
self.original_filename = @target.original_filename
@content_type = determine_content_type
@size = File.size(@target.path)
end
def content_type_detector
self.class.content_type_detector || Paperclip::ContentTypeDetector
end
def determine_content_type
content_type = @target.content_type.to_s.strip
if content_type_detector
content_type = content_type_detector.new(@target.path).detect
end
content_type
end
end
end
Paperclip::UploadedFileAdapter.register
================================================
FILE: lib/paperclip/io_adapters/uri_adapter.rb
================================================
require "open-uri"
module Paperclip
class UriAdapter < AbstractAdapter
attr_writer :content_type
def self.register
Paperclip.io_adapters.register self do |target|
target.is_a?(URI)
end
end
def initialize(target, options = {})
super
@content = download_content
cache_current_values
@tempfile = copy_to_tempfile(@content)
end
private
def cache_current_values
self.content_type = content_type_from_content || "text/html"
self.original_filename = filename_from_content_disposition ||
filename_from_path || default_filename
@size = @content.size
end
def content_type_from_content
@content.meta["content-type"].presence
end
def filename_from_content_disposition
if @content.meta.key?("content-disposition") && @content.meta["content-disposition"].match(/filename/i)
# can include both filename and filename* values according to RCF6266. filename should come first
_, filename = @content.meta["content-disposition"].split(/filename\*?\s*=\s*/i)
# filename can be enclosed in quotes or not
matches = filename.match(/"(.*)"/)
matches ? matches[1] : filename.split(';')[0]
end
end
def filename_from_path
@target.path.split("/").last
end
def default_filename
"index.html"
end
def download_content
options = { read_timeout: Paperclip.options[:read_timeout] }.compact
open(@target, **options)
end
def copy_to_tempfile(src)
while data = src.read(16 * 1024)
destination.write(data)
end
src.close
destination.rewind
destination
end
end
end
================================================
FILE: lib/paperclip/locales/en.yml
================================================
en:
errors:
messages:
in_between: "must be in between %{min} and %{max}"
spoofed_media_type: "has contents that are not what they are reported to be"
number:
human:
storage_units:
format: "%n %u"
units:
byte:
one: "Byte"
other: "Bytes"
kb: "KB"
mb: "MB"
gb: "GB"
tb: "TB"
================================================
FILE: lib/paperclip/logger.rb
================================================
module Paperclip
module Logger
# Log a paperclip-specific line. This will log to STDOUT
# by default. Set Paperclip.options[:log] to false to turn off.
def log(message)
logger.info("[paperclip] #{message}") if logging?
end
def logger #:nodoc:
@logger ||= options[:logger] || ::Logger.new(STDOUT)
end
def logger=(logger)
@logger = logger
end
def logging? #:nodoc:
options[:log]
end
end
end
================================================
FILE: lib/paperclip/matchers/have_attached_file_matcher.rb
================================================
module Paperclip
module Shoulda
module Matchers
# Ensures that the given instance or class has an attachment with the
# given name.
#
# Example:
# describe User do
# it { should have_attached_file(:avatar) }
# end
def have_attached_file name
HaveAttachedFileMatcher.new(name)
end
class HaveAttachedFileMatcher
def initialize attachment_name
@attachment_name = attachment_name
end
def matches? subject
@subject = subject
@subject = @subject.class unless Class === @subject
responds? && has_column?
end
def failure_message
"Should have an attachment named #{@attachment_name}"
end
def failure_message_when_negated
"Should not have an attachment named #{@attachment_name}"
end
alias negative_failure_message failure_message_when_negated
def description
"have an attachment named #{@attachment_name}"
end
protected
def responds?
methods = @subject.instance_methods.map(&:to_s)
methods.include?("#{@attachment_name}") &&
methods.include?("#{@attachment_name}=") &&
methods.include?("#{@attachment_name}?")
end
def has_column?
@subject.column_names.include?("#{@attachment_name}_file_name")
end
end
end
end
end
================================================
FILE: lib/paperclip/matchers/validate_attachment_content_type_matcher.rb
================================================
module Paperclip
module Shoulda
module Matchers
# Ensures that the given instance or class validates the content type of
# the given attachment as specified.
#
# Example:
# describe User do
# it { should validate_attachment_content_type(:icon).
# allowing('image/png', 'image/gif').
# rejecting('text/plain', 'text/xml') }
# end
def validate_attachment_content_type name
ValidateAttachmentContentTypeMatcher.new(name)
end
class ValidateAttachmentContentTypeMatcher
def initialize attachment_name
@attachment_name = attachment_name
@allowed_types = []
@rejected_types = []
end
def allowing *types
@allowed_types = types.flatten
self
end
def rejecting *types
@rejected_types = types.flatten
self
end
def matches? subject
@subject = subject
@subject = @subject.new if @subject.class == Class
@allowed_types && @rejected_types &&
allowed_types_allowed? && rejected_types_rejected?
end
def failure_message
"#{expected_attachment}\n".tap do |message|
message << accepted_types_and_failures.to_s
message << "\n\n" if @allowed_types.present? && @rejected_types.present?
message << rejected_types_and_failures.to_s
end
end
def description
"validate the content types allowed on attachment #{@attachment_name}"
end
protected
def accepted_types_and_failures
if @allowed_types.present?
"Accept content types: #{@allowed_types.join(", ")}\n".tap do |message|
if @missing_allowed_types.present?
message << " #{@missing_allowed_types.join(", ")} were rejected."
else
message << " All were accepted successfully."
end
end
end
end
def rejected_types_and_failures
if @rejected_types.present?
"Reject content types: #{@rejected_types.join(", ")}\n".tap do |message|
if @missing_rejected_types.present?
message << " #{@missing_rejected_types.join(", ")} were accepted."
else
message << " All were rejected successfully."
end
end
end
end
def expected_attachment
"Expected #{@attachment_name}:\n"
end
def type_allowed?(type)
@subject.send("#{@attachment_name}_content_type=", type)
@subject.valid?
@subject.errors[:"#{@attachment_name}_content_type"].blank?
end
def allowed_types_allowed?
@missing_allowed_types ||= @allowed_types.reject { |type| type_allowed?(type) }
@missing_allowed_types.none?
end
def rejected_types_rejected?
@missing_rejected_types ||= @rejected_types.select { |type| type_allowed?(type) }
@missing_rejected_types.none?
end
end
end
end
end
================================================
FILE: lib/paperclip/matchers/validate_attachment_presence_matcher.rb
================================================
module Paperclip
module Shoulda
module Matchers
# Ensures that the given instance or class validates the presence of the
# given attachment.
#
# describe User do
# it { should validate_attachment_presence(:avatar) }
# end
def validate_attachment_presence name
ValidateAttachmentPresenceMatcher.new(name)
end
class ValidateAttachmentPresenceMatcher
def initialize attachment_name
@attachment_name = attachment_name
end
def matches? subject
@subject = subject
@subject = subject.new if subject.class == Class
error_when_not_valid? && no_error_when_valid?
end
def failure_message
"Attachment #{@attachment_name} should be required"
end
def failure_message_when_negated
"Attachment #{@attachment_name} should not be required"
end
alias negative_failure_message failure_message_when_negated
def description
"require presence of attachment #{@attachment_name}"
end
protected
def error_when_not_valid?
@subject.send(@attachment_name).assign(nil)
@subject.valid?
@subject.errors[:"#{@attachment_name}"].present?
end
def no_error_when_valid?
@file = StringIO.new(".")
@subject.send(@attachment_name).assign(@file)
@subject.valid?
expected_message = [
@attachment_name.to_s.titleize,
I18n.t(:blank, scope: [:errors, :messages])
].join(' ')
@subject.errors.full_messages.exclude?(expected_message)
end
end
end
end
end
================================================
FILE: lib/paperclip/matchers/validate_attachment_size_matcher.rb
================================================
module Paperclip
module Shoulda
module Matchers
# Ensures that the given instance or class validates the size of the
# given attachment as specified.
#
# Examples:
# it { should validate_attachment_size(:avatar).
# less_than(2.megabytes) }
# it { should validate_attachment_size(:icon).
# greater_than(1024) }
# it { should validate_attachment_size(:icon).
# in(0..100) }
def validate_attachment_size name
ValidateAttachmentSizeMatcher.new(name)
end
class ValidateAttachmentSizeMatcher
def initialize attachment_name
@attachment_name = attachment_name
end
def less_than size
@high = size
self
end
def greater_than size
@low = size
self
end
def in range
@low, @high = range.first, range.last
self
end
def matches? subject
@subject = subject
@subject = @subject.new if @subject.class == Class
lower_than_low? && higher_than_low? && lower_than_high? && higher_than_high?
end
def failure_message
"Attachment #{@attachment_name} must be between #{@low} and #{@high} bytes"
end
def failure_message_when_negated
"Attachment #{@attachment_name} cannot be between #{@low} and #{@high} bytes"
end
alias negative_failure_message failure_message_when_negated
def description
"validate the size of attachment #{@attachment_name}"
end
protected
def override_method object, method, &replacement
(class << object; self; end).class_eval do
define_method(method, &replacement)
end
end
def passes_validation_with_size(new_size)
file = StringIO.new(".")
override_method(file, :size){ new_size }
override_method(file, :to_tempfile){ file }
@subject.send(@attachment_name).post_processing = false
@subject.send(@attachment_name).assign(file)
@subject.valid?
@subject.errors[:"#{@attachment_name}_file_size"].blank?
ensure
@subject.send(@attachment_name).post_processing = true
end
def lower_than_low?
@low.nil? || !passes_validation_with_size(@low - 1)
end
def higher_than_low?
@low.nil? || passes_validation_with_size(@low + 1)
end
def lower_than_high?
@high.nil? || @high == Float::INFINITY || passes_validation_with_size(@high - 1)
end
def higher_than_high?
@high.nil? || @high == Float::INFINITY || !passes_validation_with_size(@high + 1)
end
end
end
end
end
================================================
FILE: lib/paperclip/matchers.rb
================================================
require 'paperclip/matchers/have_attached_file_matcher'
require 'paperclip/matchers/validate_attachment_presence_matcher'
require 'paperclip/matchers/validate_attachment_content_type_matcher'
require 'paperclip/matchers/validate_attachment_size_matcher'
module Paperclip
module Shoulda
# Provides RSpec-compatible & Test::Unit-compatible matchers for testing Paperclip attachments.
#
# *RSpec*
#
# In spec_helper.rb, you'll need to require the matchers:
#
# require "paperclip/matchers"
#
# And _include_ the module:
#
# RSpec.configure do |config|
# config.include Paperclip::Shoulda::Matchers
# end
#
# Example:
# describe User do
# it { should have_attached_file(:avatar) }
# it { should validate_attachment_presence(:avatar) }
# it { should validate_attachment_content_type(:avatar).
# allowing('image/png', 'image/gif').
# rejecting('text/plain', 'text/xml') }
# it { should validate_attachment_size(:avatar).
# less_than(2.megabytes) }
# end
#
#
# *TestUnit*
#
# In test_helper.rb, you'll need to require the matchers as well:
#
# require "paperclip/matchers"
#
# And _extend_ the module:
#
# class ActiveSupport::TestCase
# extend Paperclip::Shoulda::Matchers
#
# #...other initializers...#
# end
#
# Example:
# require 'test_helper'
#
# class UserTest < ActiveSupport::TestCase
# should have_attached_file(:avatar)
# should validate_attachment_presence(:avatar)
# should validate_attachment_content_type(:avatar).
# allowing('image/png', 'image/gif').
# rejecting('text/plain', 'text/xml')
# should validate_attachment_size(:avatar).
# less_than(2.megabytes)
# end
#
module Matchers
end
end
end
================================================
FILE: lib/paperclip/media_type_spoof_detector.rb
================================================
module Paperclip
class MediaTypeSpoofDetector
def self.using(file, name, content_type)
new(file, name, content_type)
end
def initialize(file, name, content_type)
@file = file
@name = name
@content_type = content_type || ""
end
def spoofed?
if has_name? && media_type_mismatch? && mapping_override_mismatch?
Paperclip.log("Content Type Spoof: Filename #{File.basename(@name)} (#{supplied_content_type} from Headers, #{content_types_from_name.map(&:to_s)} from Extension), content type discovered from file command: #{calculated_content_type}. See documentation to allow this combination.")
true
else
false
end
end
private
def has_name?
@name.present?
end
def has_extension?
File.extname(@name).present?
end
def media_type_mismatch?
extension_type_mismatch? || calculated_type_mismatch?
end
def extension_type_mismatch?
supplied_media_type.present? &&
has_extension? &&
!media_types_from_name.include?(supplied_media_type)
end
def calculated_type_mismatch?
supplied_media_type.present? &&
!calculated_content_type.include?(supplied_media_type)
end
def mapping_override_mismatch?
!Array(mapped_content_type).include?(calculated_content_type)
end
def supplied_content_type
@content_type
end
def supplied_media_type
@content_type.split("/").first
end
def content_types_from_name
@content_types_from_name ||= MIME::Types.type_for(@name)
end
def media_types_from_name
@media_types_from_name ||= content_types_from_name.collect(&:media_type)
end
def calculated_content_type
@calculated_content_type ||= type_from_file_command.chomp
end
def calculated_media_type
@calculated_media_type ||= calculated_content_type.split("/").first
end
def type_from_file_command
begin
Paperclip.run("file", "-b --mime :file", file: @file.path).
split(/[:;\s]+/).first
rescue Terrapin::CommandLineError
""
end
end
def mapped_content_type
Paperclip.options[:content_type_mappings][filename_extension]
end
def filename_extension
File.extname(@name.to_s.downcase).sub(/^\./, '').to_sym
end
end
end
================================================
FILE: lib/paperclip/missing_attachment_styles.rb
================================================
require 'paperclip/attachment_registry'
require 'set'
module Paperclip
class << self
attr_writer :registered_attachments_styles_path
def registered_attachments_styles_path
@registered_attachments_styles_path ||= Rails.root.join('public/system/paperclip_attachments.yml').to_s
end
end
# Get list of styles saved on previous deploy (running rake paperclip:refresh:missing_styles)
def self.get_registered_attachments_styles
YAML.load_file(Paperclip.registered_attachments_styles_path)
rescue Errno::ENOENT
nil
end
private_class_method :get_registered_attachments_styles
def self.save_current_attachments_styles!
File.open(Paperclip.registered_attachments_styles_path, 'w') do |f|
YAML.dump(current_attachments_styles, f)
end
end
# Returns hash with styles for all classes using Paperclip.
# Unfortunately current version does not work with lambda styles:(
# {
# :User => {:avatar => [:small, :big]},
# :Book => {
# :cover => [:thumb, :croppable]},
# :sample => [:thumb, :big]},
# }
# }
def self.current_attachments_styles
Hash.new.tap do |current_styles|
Paperclip::AttachmentRegistry.each_definition do |klass, attachment_name, attachment_attributes|
# TODO: is it even possible to take into account Procs?
next if attachment_attributes[:styles].kind_of?(Proc)
attachment_attributes[:styles].try(:keys).try(:each) do |style_name|
klass_sym = klass.to_s.to_sym
current_styles[klass_sym] ||= Hash.new
current_styles[klass_sym][attachment_name.to_sym] ||= Array.new
current_styles[klass_sym][attachment_name.to_sym] << style_name.to_sym
current_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
end
end
end
end
private_class_method :current_attachments_styles
# Returns hash with styles missing from recent run of rake paperclip:refresh:missing_styles
# {
# :User => {:avatar => [:big]},
# :Book => {
# :cover => [:croppable]},
# }
# }
def self.missing_attachments_styles
current_styles = current_attachments_styles
registered_styles = get_registered_attachments_styles
Hash.new.tap do |missing_styles|
current_styles.each do |klass, attachment_definitions|
attachment_definitions.each do |attachment_name, styles|
registered = registered_styles[klass][attachment_name] || [] rescue []
missed = styles - registered
if missed.present?
klass_sym = klass.to_s.to_sym
missing_styles[klass_sym] ||= Hash.new
missing_styles[klass_sym][attachment_name.to_sym] ||= Array.new
missing_styles[klass_sym][attachment_name.to_sym].concat(missed.to_a)
missing_styles[klass_sym][attachment_name.to_sym].map!(&:to_s).sort!.map!(&:to_sym).uniq!
end
end
end
end
end
end
================================================
FILE: lib/paperclip/processor.rb
================================================
module Paperclip
# Paperclip processors allow you to modify attached files when they are
# attached in any way you are able. Paperclip itself uses command-line
# programs for its included Thumbnail processor, but custom processors
# are not required to follow suit.
#
# Processors are required to be defined inside the Paperclip module and
# are also required to be a subclass of Paperclip::Processor. There is
# only one method you *must* implement to properly be a subclass:
# #make, but #initialize may also be of use. #initialize accepts 3
# arguments: the file that will be operated on (which is an instance of
# File), a hash of options that were defined in has_attached_file's
# style hash, and the Paperclip::Attachment itself. These are set as
# instance variables that can be used within `#make`.
#
# #make must return an instance of File (Tempfile is acceptable) which
# contains the results of the processing.
#
# See Paperclip.run for more information about using command-line
# utilities from within Processors.
class Processor
attr_accessor :file, :options, :attachment
def initialize file, options = {}, attachment = nil
@file = file
@options = options
@attachment = attachment
end
def make
end
def self.make file, options = {}, attachment = nil
new(file, options, attachment).make
end
# The convert method runs the convert binary with the provided arguments.
# See Paperclip.run for the available options.
def convert(arguments = "", local_options = {})
Paperclip.run(
Paperclip.options[:is_windows] ? "magick convert" : "convert",
arguments,
local_options,
)
end
# The identify method runs the identify binary with the provided arguments.
# See Paperclip.run for the available options.
def identify(arguments = "", local_options = {})
Paperclip.run(
Paperclip.options[:is_windows] ? "magick identify" : "identify",
arguments,
local_options,
)
end
end
end
================================================
FILE: lib/paperclip/processor_helpers.rb
================================================
module Paperclip
module ProcessorHelpers
class NoSuchProcessor < StandardError; end
def processor(name) #:nodoc:
@known_processors ||= {}
if @known_processors[name.to_s]
@known_processors[name.to_s]
else
name = name.to_s.camelize
load_processor(name) unless Paperclip.const_defined?(name)
processor = Paperclip.const_get(name)
@known_processors[name.to_s] = processor
end
end
def load_processor(name)
if defined?(Rails.root) && Rails.root
filename = "#{name.to_s.underscore}.rb"
directories = %w(lib/paperclip lib/paperclip_processors)
required = directories.map do |directory|
pathname = File.expand_path(Rails.root.join(directory, filename))
file_exists = File.exist?(pathname)
require pathname if file_exists
file_exists
end
raise LoadError, "Could not find the '#{name}' processor in any of these paths: #{directories.join(', ')}" unless required.any?
end
end
def clear_processors!
@known_processors.try(:clear)
end
# You can add your own processor via the Paperclip configuration. Normally
# Paperclip will load all processors from the
# Rails.root/lib/paperclip_processors directory, but here you can add any
# existing class using this mechanism.
#
# Paperclip.configure do |c|
# c.register_processor :watermarker, WatermarkingProcessor.new
# end
def register_processor(name, processor)
@known_processors ||= {}
@known_processors[name.to_s] = processor
end
end
end
================================================
FILE: lib/paperclip/rails_environment.rb
================================================
module Paperclip
class RailsEnvironment
def self.get
new.get
end
def get
if rails_exists? && rails_environment_exists?
Rails.env
else
nil
end
end
private
def rails_exists?
Object.const_defined?(:Rails)
end
def rails_environment_exists?
Rails.respond_to?(:env)
end
end
end
================================================
FILE: lib/paperclip/railtie.rb
================================================
require 'paperclip'
require 'paperclip/schema'
module Paperclip
require 'rails'
class Railtie < Rails::Railtie
initializer 'paperclip.insert_into_active_record' do |app|
ActiveSupport.on_load :active_record do
Paperclip::Railtie.insert
end
if app.config.respond_to?(:paperclip_defaults)
Paperclip::Attachment.default_options.merge!(app.config.paperclip_defaults)
end
end
rake_tasks { load "tasks/paperclip.rake" }
end
class Railtie
def self.insert
Paperclip.options[:logger] = Rails.logger
if defined?(ActiveRecord)
Paperclip.options[:logger] = ActiveRecord::Base.logger
ActiveRecord::Base.send(:include, Paperclip::Glue)
end
end
end
end
================================================
FILE: lib/paperclip/schema.rb
================================================
require 'active_support/deprecation'
module Paperclip
# Provides helper methods that can be used in migrations.
module Schema
COLUMNS = {:file_name => :string,
:content_type => :string,
:file_size => :bigint,
:updated_at => :datetime}
def self.included(base)
ActiveRecord::ConnectionAdapters::Table.send :include, TableDefinition
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TableDefinition
ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Statements
ActiveRecord::Migration::CommandRecorder.send :include, CommandRecorder
end
module Statements
def add_attachment(table_name, *attachment_names)
raise ArgumentError, "Please specify attachment name in your add_attachment call in your migration." if attachment_names.empty?
options = attachment_names.extract_options!
attachment_names.each do |attachment_name|
COLUMNS.each_pair do |column_name, column_type|
column_options = options.merge(options[column_name.to_sym] || {})
add_column(table_name, "#{attachment_name}_#{column_name}", column_type, column_options)
end
end
end
def remove_attachment(table_name, *attachment_names)
raise ArgumentError, "Please specify attachment name in your remove_attachment call in your migration." if attachment_names.empty?
attachment_names.each do |attachment_name|
COLUMNS.keys.each do |column_name|
remove_column(table_name, "#{attachment_name}_#{column_name}")
end
end
end
def drop_attached_file(*args)
ActiveSupport::Deprecation.warn "Method `drop_attached_file` in the migration has been deprecated and will be replaced by `remove_attachment`."
remove_attachment(*args)
end
end
module TableDefinition
def attachment(*attachment_names)
options = attachment_names.extract_options!
attachment_names.each do |attachment_name|
COLUMNS.each_pair do |column_name, column_type|
column_options = options.merge(options[column_name.to_sym] || {})
column("#{attachment_name}_#{column_name}", column_type, column_options)
end
end
end
def has_attached_file(*attachment_names)
ActiveSupport::Deprecation.warn "Method `t.has_attached_file` in the migration has been deprecated and will be replaced by `t.attachment`."
attachment(*attachment_names)
end
end
module CommandRecorder
def add_attachment(*args)
record(:add_attachment, args)
end
private
def invert_add_attachment(args)
[:remove_attachment, args]
end
end
end
end
================================================
FILE: lib/paperclip/storage/filesystem.rb
================================================
module Paperclip
module Storage
# The default place to store attachments is in the filesystem. Files on the local
# filesystem can be very easily served by Apache without requiring a hit to your app.
# They also can be processed more easily after they've been saved, as they're just
# normal files. There are two Filesystem-specific options for has_attached_file:
# * +path+: The location of the repository of attachments on disk. This can (and, in
# almost all cases, should) be coordinated with the value of the +url+ option to
# allow files to be saved into a place where Apache can serve them without
# hitting your app. Defaults to
# ":rails_root/public/:attachment/:id/:style/:basename.:extension"
# By default this places the files in the app's public directory which can be served
# directly. If you are using capistrano for deployment, a good idea would be to
# make a symlink to the capistrano-created system directory from inside your app's
# public directory.
# See Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
# * +override_file_permissions+: This allows you to override the file permissions for files
# saved by paperclip. If you set this to an explicit octal value (0755, 0644, etc) then
# that value will be used to set the permissions for an uploaded file. The default is 0666.
# If you set :override_file_permissions to false, the chmod will be skipped. This allows
# you to use paperclip on filesystems that don't understand unix file permissions, and has the
# added benefit of using the storage directories default umask on those that do.
module Filesystem
def self.extended base
end
def exists?(style_name = default_style)
if original_filename
File.exist?(path(style_name))
else
false
end
end
def flush_writes #:nodoc:
@queued_for_write.each do |style_name, file|
FileUtils.mkdir_p(File.dirname(path(style_name)))
begin
move_file(file.path, path(style_name))
rescue SystemCallError
File.open(path(style_name), "wb") do |new_file|
while chunk = file.read(16 * 1024)
new_file.write(chunk)
end
end
end
unless @options[:override_file_permissions] == false
resolved_chmod = (@options[:override_file_permissions] & ~0111) || (0666 & ~File.umask)
FileUtils.chmod( resolved_chmod, path(style_name) )
end
file.rewind
end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
FileUtils.rm(path) if File.exist?(path)
rescue Errno::ENOENT => e
# ignore file-not-found, let everything else pass
end
begin
while(true)
path = File.dirname(path)
FileUtils.rmdir(path)
break if File.exist?(path) # Ruby 1.9.2 does not raise if the removal failed.
end
rescue Errno::EEXIST, Errno::ENOTEMPTY, Errno::ENOENT, Errno::EINVAL, Errno::ENOTDIR, Errno::EACCES
# Stop trying to remove parent directories
rescue SystemCallError => e
log("There was an unexpected error while deleting directories: #{e.class}")
# Ignore it
end
end
@queued_for_delete = []
end
def copy_to_local_file(style, local_dest_path)
FileUtils.cp(path(style), local_dest_path)
end
private
def move_file(src, dest)
# Support hardlinked files
if File.identical?(src, dest)
File.unlink(src)
else
FileUtils.mv(src, dest)
end
end
end
end
end
================================================
FILE: lib/paperclip/storage/fog.rb
================================================
module Paperclip
module Storage
# fog is a modern and versatile cloud computing library for Ruby.
# Among others, it supports Amazon S3 to store your files. In
# contrast to the outdated AWS-S3 gem it is actively maintained and
# supports multiple locations.
# Amazon's S3 file hosting service is a scalable, easy place to
# store files for distribution. You can find out more about it at
# http://aws.amazon.com/s3 There are a few fog-specific options for
# has_attached_file, which will be explained using S3 as an example:
# * +fog_credentials+: Takes a Hash with your credentials. For S3,
# you can use the following format:
# aws_access_key_id: ''
# aws_secret_access_key: ''
# provider: 'AWS'
# region: 'eu-west-1'
# scheme: 'https'
# * +fog_directory+: This is the name of the S3 bucket that will
# store your files. Remember that the bucket must be unique across
# all of Amazon S3. If the bucket does not exist, Paperclip will
# attempt to create it.
# * +fog_file+: This can be hash or lambda returning hash. The
# value is used as base properties for new uploaded file.
# * +path+: This is the key under the bucket in which the file will
# be stored. The URL will be constructed from the bucket and the
# path. This is what you will want to interpolate. Keys should be
# unique, like filenames, and despite the fact that S3 (strictly
# speaking) does not support directories, you can still use a / to
# separate parts of your file name.
# * +fog_public+: (optional, defaults to true) Should the uploaded
# files be public or not? (true/false)
# * +fog_host+: (optional) The fully-qualified domain name (FQDN)
# that is the alias to the S3 domain of your bucket, e.g.
# 'http://images.example.com'. This can also be used in
# conjunction with Cloudfront (http://aws.amazon.com/cloudfront)
# * +fog_options+: (optional) A hash of options that are passed
# to fog when the file is created. For example, you could set
# the multipart-chunk size to 100MB with a hash:
# { :multipart_chunk_size => 104857600 }
module Fog
def self.extended base
begin
require 'fog'
rescue LoadError => e
e.message << " (You may need to install the fog gem)"
raise e
end unless defined?(Fog)
base.instance_eval do
unless @options[:url].to_s.match(/\A:fog.*url\z/)
@options[:path] = @options[:path].gsub(/:url/, @options[:url]).gsub(/\A:rails_root\/public\/system\//, '')
@options[:url] = ':fog_public_url'
end
Paperclip.interpolates(:fog_public_url) do |attachment, style|
attachment.public_url(style)
end unless Paperclip::Interpolations.respond_to? :fog_public_url
end
end
AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX = /\A(?:[a-z]|\d(?!\d{0,2}(?:\.\d{1,3}){3}\z))(?:[a-z0-9]|\.(?![\.\-])|\-(?![\.])){1,61}[a-z0-9]\z/
def exists?(style = default_style)
if original_filename
!!directory.files.head(path(style))
else
false
end
end
def fog_credentials
@fog_credentials ||= parse_credentials(@options[:fog_credentials])
end
def fog_file
@fog_file ||= begin
value = @options[:fog_file]
if !value
{}
elsif value.respond_to?(:call)
value.call(self)
else
value
end
end
end
def fog_public(style = default_style)
if @options.key?(:fog_public)
value = @options[:fog_public]
if value.respond_to?(:key?) && value.key?(style)
value[style]
elsif value.respond_to?(:call)
value.call(self)
else
value
end
else
true
end
end
def flush_writes
for style, file in @queued_for_write do
log("saving #{path(style)}")
retried = false
begin
attributes = fog_file.merge(
:body => file,
:key => path(style),
:public => fog_public(style),
:content_type => file.content_type
)
attributes.merge!(@options[:fog_options]) if @options[:fog_options]
directory.files.create(attributes)
rescue Excon::Errors::NotFound
raise if retried
retried = true
directory.save
file.rewind
retry
ensure
file.rewind
end
end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {}
end
def flush_deletes
for path in @queued_for_delete do
log("deleting #{path}")
directory.files.new(:key => path).destroy
end
@queued_for_delete = []
end
def public_url(style = default_style)
if @options[:fog_host]
"#{dynamic_fog_host_for_style(style)}/#{path(style)}"
else
if fog_credentials[:provider] == 'AWS'
"#{scheme}://#{host_name_for_directory}/#{path(style)}"
else
directory.files.new(:key => path(style)).public_url
end
end
end
def expiring_url(time = (Time.now + 3600), style_name = default_style)
time = convert_time(time)
http_url_method = "get_#{scheme}_url"
if path(style_name) && directory.files.respond_to?(http_url_method)
expiring_url = directory.files.public_send(http_url_method, path(style_name), time)
if @options[:fog_host]
expiring_url.gsub!(/#{host_name_for_directory}/, dynamic_fog_host_for_style(style_name))
end
else
expiring_url = url(style_name)
end
return expiring_url
end
def parse_credentials(creds)
creds = find_credentials(creds).stringify_keys
(creds[RailsEnvironment.get] || creds).symbolize_keys
end
def copy_to_local_file(style, local_dest_path)
log("copying #{path(style)} to local file #{local_dest_path}")
::File.open(local_dest_path, 'wb') do |local_file|
file = directory.files.get(path(style))
return false unless file
local_file.write(file.body)
end
rescue ::Fog::Errors::Error => e
warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}")
false
end
private
def convert_time(time)
if time.is_a?(Integer)
time = Time.now + time
end
time
end
def dynamic_fog_host_for_style(style)
if @options[:fog_host].respond_to?(:call)
@options[:fog_host].call(self)
else
(@options[:fog_host] =~ /%d/) ? @options[:fog_host] % (path(style).hash % 4) : @options[:fog_host]
end
end
def host_name_for_directory
if directory_name.to_s =~ Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX
"#{directory_name}.s3.amazonaws.com"
else
"s3.amazonaws.com/#{directory_name}"
end
end
def find_credentials(creds)
case creds
when File
YAML::load(ERB.new(File.read(creds.path)).result)
when String, Pathname
YAML::load(ERB.new(File.read(creds)).result)
when Hash
creds
else
if creds.respond_to?(:call)
creds.call(self)
else
raise ArgumentError, "Credentials are not a path, file, hash or proc."
end
end
end
def connection
@connection ||= ::Fog::Storage.new(fog_credentials)
end
def directory
@directory ||= connection.directories.new(key: directory_name)
end
def directory_name
if @options[:fog_directory].respond_to?(:call)
@options[:fog_directory].call(self)
else
@options[:fog_directory]
end
end
def scheme
@scheme ||= fog_credentials[:scheme] || 'https'
end
end
end
end
================================================
FILE: lib/paperclip/storage/s3.rb
================================================
module Paperclip
module Storage
# Amazon's S3 file hosting service is a scalable, easy place to store files for
# distribution. You can find out more about it at http://aws.amazon.com/s3
#
# To use Paperclip with S3, include the +aws-sdk-s3+ gem in your Gemfile:
# gem 'aws-sdk-s3'
# There are a few S3-specific options for has_attached_file:
# * +s3_credentials+: Takes a path, a File, a Hash or a Proc. The path (or File) must point
# to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
# gives you. You can 'environment-space' this just like you do to your
# database.yml file, so different environments can use different accounts:
# development:
# access_key_id: 123...
# secret_access_key: 123...
# test:
# access_key_id: abc...
# secret_access_key: abc...
# production:
# access_key_id: 456...
# secret_access_key: 456...
# This is not required, however, and the file may simply look like this:
# access_key_id: 456...
# secret_access_key: 456...
# In which case, those access keys will be used in all environments. You can also
# put your bucket name in this file, instead of adding it to the code directly.
# This is useful when you want the same account but a different bucket for
# development versus production.
# When using a Proc it provides a single parameter which is the attachment itself. A
# method #instance is available on the attachment which will take you back to your
# code. eg.
# class User
# has_attached_file :download,
# :storage => :s3,
# :s3_credentials => Proc.new{|a| a.instance.s3_credentials }
#
# def s3_credentials
# {:bucket => "xxx", :access_key_id => "xxx", :secret_access_key => "xxx"}
# end
# end
# * +s3_permissions+: This is a String that should be one of the "canned" access
# policies that S3 provides (more information can be found here:
# http://docs.aws.amazon.com/AmazonS3/latest/dev/ACLOverview.html)
# The default for Paperclip is public-read.
#
# You can set permission on a per style bases by doing the following:
# :s3_permissions => {
# :original => "private"
# }
# Or globally:
# :s3_permissions => "private"
#
# * +s3_protocol+: The protocol for the URLs generated to your S3 assets.
# Can be either 'http', 'https', or an empty string to generate
# protocol-relative URLs. Defaults to empty string.
# * +s3_headers+: A hash of headers or a Proc. You may specify a hash such as
# {'Expires' => 1.year.from_now.httpdate}. If you use a Proc, headers are determined at
# runtime. Paperclip will call that Proc with attachment as the only argument.
# Can be defined both globally and within a style-specific hash.
# * +bucket+: This is the name of the S3 bucket that will store your files. Remember
# that the bucket must be unique across all of Amazon S3. If the bucket does not exist
# Paperclip will attempt to create it. The bucket name will not be interpolated.
# You can define the bucket as a Proc if you want to determine its name at runtime.
# Paperclip will call that Proc with attachment as the only argument.
# * +s3_host_alias+: The fully-qualified domain name (FQDN) that is the alias to the
# S3 domain of your bucket. Used with the :s3_alias_url url interpolation. See the
# link in the +url+ entry for more information about S3 domains and buckets.
# * +s3_prefixes_in_alias+: The number of prefixes that is prepended by
# s3_host_alias. This will remove the prefixes from the path in
# :s3_alias_url url interpolation
# * +url+: There are four options for the S3 url. You can choose to have the bucket's name
# placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
# You can also specify a CNAME (which requires the CNAME to be specified as
# :s3_alias_url. You can read more about CNAMEs and S3 at
# http://docs.amazonwebservices.com/AmazonS3/latest/index.html?VirtualHosting.html
# Normally, this won't matter in the slightest and you can leave the default (which is
# path-style, or :s3_path_url). But in some cases paths don't work and you need to use
# the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
#
# Notes:
# * The value of this option is a string, not a symbol.
# right: ":s3_domain_url"
# wrong: :s3_domain_url
# * If you use a CNAME for use with CloudFront, you can NOT specify https as your
# :s3_protocol;
# This is *not supported* by S3/CloudFront. Finally, when using the host
# alias, the :bucket parameter is ignored, as the hostname is used as the bucket name
# by S3. The fourth option for the S3 url is :asset_host, which uses Rails' built-in
# asset_host settings.
# * To get the full url from a paperclip'd object, use the
# image_path helper; this is what image_tag uses to generate the url for an img tag.
# * +path+: This is the key under the bucket in which the file will be stored. The
# URL will be constructed from the bucket and the path. This is what you will want
# to interpolate. Keys should be unique, like filenames, and despite the fact that
# S3 (strictly speaking) does not support directories, you can still use a / to
# separate parts of your file name.
# * +s3_host_name+: If you are using your bucket in Tokyo region
# etc, write host_name (e.g., 's3-ap-northeast-1.amazonaws.com').
# * +s3_region+: For aws-sdk-s3, s3_region is required.
# * +s3_metadata+: These key/value pairs will be stored with the
# object. This option works by prefixing each key with
# "x-amz-meta-" before sending it as a header on the object
# upload request. Can be defined both globally and within a style-specific hash.
# * +s3_storage_class+: If this option is set to
# :REDUCED_REDUNDANCY, the object will be stored using Reduced
# Redundancy Storage. RRS enables customers to reduce their
# costs by storing non-critical, reproducible data at lower
# levels of redundancy than Amazon S3's standard storage.
# * +use_accelerate_endpoint+: Use accelerate endpoint
# http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
#
# You can set storage class on a per style bases by doing the following:
# :s3_storage_class => {
# :thumb => :REDUCED_REDUNDANCY
# }
#
# Or globally:
# :s3_storage_class => :REDUCED_REDUNDANCY
#
# Other storage classes, such as :STANDARD_IA, are also available—see the
# documentation for the aws-sdk-s3 gem for the full list.
module S3
def self.extended base
begin
require "aws-sdk-s3"
rescue LoadError => e
e.message << " (You may need to install the aws-sdk-s3 gem)"
raise e
end
base.instance_eval do
@s3_options = @options[:s3_options] || {}
@s3_permissions = set_permissions(@options[:s3_permissions])
@s3_protocol = @options[:s3_protocol] || "".freeze
@s3_metadata = @options[:s3_metadata] || {}
@s3_headers = {}
merge_s3_headers(@options[:s3_headers], @s3_headers, @s3_metadata)
@s3_storage_class = set_storage_class(@options[:s3_storage_class])
@s3_server_side_encryption = "AES256"
if @options[:s3_server_side_encryption].blank?
@s3_server_side_encryption = false
end
if @s3_server_side_encryption
@s3_server_side_encryption = @options[:s3_server_side_encryption]
end
unless @options[:url].to_s.match(/\A:s3.*url\z/) || @options[:url] == ":asset_host".freeze
@options[:path] = path_option.gsub(/:url/, @options[:url]).sub(/\A:rails_root\/public\/system/, "".freeze)
@options[:url] = ":s3_path_url".freeze
end
@options[:url] = @options[:url].inspect if @options[:url].is_a?(Symbol)
@http_proxy = @options[:http_proxy] || nil
@use_accelerate_endpoint = @options[:use_accelerate_endpoint]
end
Paperclip.interpolates(:s3_alias_url) do |attachment, style|
protocol = attachment.s3_protocol(style, true)
host = attachment.s3_host_alias
path = attachment.path(style).
split("/")[attachment.s3_prefixes_in_alias..-1].
join("/").
sub(%r{\A/}, "".freeze)
"#{protocol}//#{host}/#{path}"
end unless Paperclip::Interpolations.respond_to? :s3_alias_url
Paperclip.interpolates(:s3_path_url) do |attachment, style|
"#{attachment.s3_protocol(style, true)}//#{attachment.s3_host_name}/#{attachment.bucket_name}/#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
end unless Paperclip::Interpolations.respond_to? :s3_path_url
Paperclip.interpolates(:s3_domain_url) do |attachment, style|
"#{attachment.s3_protocol(style, true)}//#{attachment.bucket_name}.#{attachment.s3_host_name}/#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
end unless Paperclip::Interpolations.respond_to? :s3_domain_url
Paperclip.interpolates(:asset_host) do |attachment, style|
"#{attachment.path(style).sub(%r{\A/}, "".freeze)}"
end unless Paperclip::Interpolations.respond_to? :asset_host
end
def expiring_url(time = 3600, style_name = default_style)
if path(style_name)
base_options = { expires_in: time }
s3_object(style_name).presigned_url(
:get,
base_options.merge(s3_url_options),
).to_s
else
url(style_name)
end
end
def s3_credentials
@s3_credentials ||= parse_credentials(@options[:s3_credentials])
end
def s3_host_name
host_name = @options[:s3_host_name]
host_name = host_name.call(self) if host_name.is_a?(Proc)
host_name || s3_credentials[:s3_host_name] || "s3.amazonaws.com".freeze
end
def s3_region
region = @options[:s3_region]
region = region.call(self) if region.is_a?(Proc)
region || s3_credentials[:s3_region]
end
def s3_host_alias
@s3_host_alias = @options[:s3_host_alias]
@s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.respond_to?(:call)
@s3_host_alias
end
def s3_prefixes_in_alias
@s3_prefixes_in_alias ||= @options[:s3_prefixes_in_alias].to_i
end
def s3_url_options
s3_url_options = @options[:s3_url_options] || {}
s3_url_options = s3_url_options.call(instance) if s3_url_options.respond_to?(:call)
s3_url_options
end
def bucket_name
@bucket = @options[:bucket] || s3_credentials[:bucket]
@bucket = @bucket.call(self) if @bucket.respond_to?(:call)
@bucket or raise ArgumentError, "missing required :bucket option"
end
def s3_interface
@s3_interface ||= begin
config = { region: s3_region }
if using_http_proxy?
proxy_opts = { :host => http_proxy_host }
proxy_opts[:port] = http_proxy_port if http_proxy_port
if http_proxy_user
userinfo = http_proxy_user.to_s
userinfo += ":#{http_proxy_password}" if http_proxy_password
proxy_opts[:userinfo] = userinfo
end
config[:proxy_uri] = URI::HTTP.build(proxy_opts)
end
config[:use_accelerate_endpoint] = use_accelerate_endpoint?
[:access_key_id, :secret_access_key, :credential_provider, :credentials].each do |opt|
config[opt] = s3_credentials[opt] if s3_credentials[opt]
end
obtain_s3_instance_for(config.merge(@s3_options))
end
end
def obtain_s3_instance_for(options)
instances = (Thread.current[:paperclip_s3_instances] ||= {})
instances[options] ||= ::Aws::S3::Resource.new(options)
end
def s3_bucket
@s3_bucket ||= s3_interface.bucket(bucket_name)
end
def style_name_as_path(style_name)
path(style_name).sub(%r{\A/},'')
end
def s3_object style_name = default_style
s3_bucket.object style_name_as_path(style_name)
end
def use_accelerate_endpoint?
!!@use_accelerate_endpoint
end
def using_http_proxy?
!!@http_proxy
end
def http_proxy_host
using_http_proxy? ? @http_proxy[:host] : nil
end
def http_proxy_port
using_http_proxy? ? @http_proxy[:port] : nil
end
def http_proxy_user
using_http_proxy? ? @http_proxy[:user] : nil
end
def http_proxy_password
using_http_proxy? ? @http_proxy[:password] : nil
end
def set_permissions permissions
permissions = { :default => permissions } unless permissions.respond_to?(:merge)
permissions.merge :default => (permissions[:default] || :"public-read")
end
def set_storage_class(storage_class)
storage_class = {:default => storage_class} unless storage_class.respond_to?(:merge)
storage_class
end
def parse_credentials creds
creds = creds.respond_to?(:call) ? creds.call(self) : creds
creds = find_credentials(creds).stringify_keys
(creds[RailsEnvironment.get] || creds).symbolize_keys
end
def exists?(style = default_style)
if original_filename
s3_object(style).exists?
else
false
end
rescue Aws::Errors::ServiceError => e
false
end
def s3_permissions(style = default_style)
s3_permissions = @s3_permissions[style] || @s3_permissions[:default]
s3_permissions = s3_permissions.call(self, style) if s3_permissions.respond_to?(:call)
s3_permissions
end
def s3_storage_class(style = default_style)
@s3_storage_class[style] || @s3_storage_class[:default]
end
def s3_protocol(style = default_style, with_colon = false)
protocol = @s3_protocol
protocol = protocol.call(style, self) if protocol.respond_to?(:call)
if with_colon && !protocol.empty?
"#{protocol}:"
else
protocol.to_s
end
end
def create_bucket
s3_interface.bucket(bucket_name).create
end
def flush_writes #:nodoc:
@queued_for_write.each do |style, file|
retries = 0
begin
log("saving #{path(style)}")
write_options = {
:content_type => file.content_type,
:acl => s3_permissions(style)
}
# add storage class for this style if defined
storage_class = s3_storage_class(style)
write_options.merge!(:storage_class => storage_class) if storage_class
if @s3_server_side_encryption
write_options[:server_side_encryption] = @s3_server_side_encryption
end
style_specific_options = styles[style]
if style_specific_options
merge_s3_headers( style_specific_options[:s3_headers], @s3_headers, @s3_metadata) if style_specific_options[:s3_headers]
@s3_metadata.merge!(style_specific_options[:s3_metadata]) if style_specific_options[:s3_metadata]
end
write_options[:metadata] = @s3_metadata unless @s3_metadata.empty?
write_options.merge!(@s3_headers)
s3_object(style).upload_file(file.path, write_options)
rescue ::Aws::S3::Errors::NoSuchBucket
create_bucket
retry
rescue ::Aws::S3::Errors::SlowDown
retries += 1
if retries <= 5
sleep((2 ** retries) * 0.5)
retry
else
raise
end
ensure
file.rewind
end
end
after_flush_writes # allows attachment to clean up temp files
@queued_for_write = {}
end
def flush_deletes #:nodoc:
@queued_for_delete.each do |path|
begin
log("deleting #{path}")
s3_bucket.object(path.sub(%r{\A/}, "")).delete
rescue Aws::Errors::ServiceError => e
# Ignore this.
end
end
@queued_for_delete = []
end
def copy_to_local_file(style, local_dest_path)
log("copying #{path(style)} to local file #{local_dest_path}")
::File.open(local_dest_path, 'wb') do |local_file|
s3_object(style).get do |chunk|
local_file.write(chunk)
end
end
rescue Aws::Errors::ServiceError => e
warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}")
false
end
private
def find_credentials creds
case creds
when File
YAML::load(ERB.new(File.read(creds.path)).result)
when String, Pathname
YAML::load(ERB.new(File.read(creds)).result)
when Hash
creds
when NilClass
{}
else
raise ArgumentError, "Credentials given are not a path, file, proc, or hash."
end
end
def use_secure_protocol?(style_name)
s3_protocol(style_name) == "https"
end
def merge_s3_headers(http_headers, s3_headers, s3_metadata)
return if http_headers.nil?
http_headers = http_headers.call(instance) if http_headers.respond_to?(:call)
http_headers.inject({}) do |headers,(name,value)|
case name.to_s
when /\Ax-amz-meta-(.*)/i
s3_metadata[$1.downcase] = value
else
s3_headers[name.to_s.downcase.sub(/\Ax-amz-/,'').tr("-","_").to_sym] = value
end
end
end
end
end
end
================================================
FILE: lib/paperclip/storage.rb
================================================
require "paperclip/storage/filesystem"
require "paperclip/storage/fog"
require "paperclip/storage/s3"
================================================
FILE: lib/paperclip/style.rb
================================================
module Paperclip
# The Style class holds the definition of a thumbnail style, applying
# whatever processing is required to normalize the definition and delaying
# the evaluation of block parameters until useful context is available.
class Style
attr_reader :name, :attachment, :format
# Creates a Style object. +name+ is the name of the attachment,
# +definition+ is the style definition from has_attached_file, which
# can be string, array or hash
def initialize name, definition, attachment
@name = name
@attachment = attachment
if definition.is_a? Hash
@geometry = definition.delete(:geometry)
@format = definition.delete(:format)
@processors = definition.delete(:processors)
@convert_options = definition.delete(:convert_options)
@source_file_options = definition.delete(:source_file_options)
@other_args = definition
elsif definition.is_a? String
@geometry = definition
@format = nil
@other_args = {}
else
@geometry, @format = [definition, nil].flatten[0..1]
@other_args = {}
end
@format = default_format if @format.blank?
end
# retrieves from the attachment the processors defined in the has_attached_file call
# (which method (in the attachment) will call any supplied procs)
# There is an important change of interface here: a style rule can set its own processors
# by default we behave as before, though.
# if a proc has been supplied, we call it here
def processors
@processors.respond_to?(:call) ? @processors.call(attachment.instance) : (@processors || attachment.processors)
end
# retrieves from the attachment the whiny setting
def whiny
attachment.whiny
end
# returns true if we're inclined to grumble
def whiny?
!!whiny
end
def convert_options
@convert_options.respond_to?(:call) ? @convert_options.call(attachment.instance) :
(@convert_options || attachment.send(:extra_options_for, name))
end
def source_file_options
@source_file_options.respond_to?(:call) ? @source_file_options.call(attachment.instance) :
(@source_file_options || attachment.send(:extra_source_file_options_for, name))
end
# returns the geometry string for this style
# if a proc has been supplied, we call it here
def geometry
@geometry.respond_to?(:call) ? @geometry.call(attachment.instance) : @geometry
end
# Supplies the hash of options that processors expect to receive as their second argument
# Arguments other than the standard geometry, format etc are just passed through from
# initialization and any procs are called here, just before post-processing.
def processor_options
args = {:style => name}
@other_args.each do |k,v|
args[k] = v.respond_to?(:call) ? v.call(attachment) : v
end
[:processors, :geometry, :format, :whiny, :convert_options, :source_file_options].each do |k|
(arg = send(k)) && args[k] = arg
end
args
end
# Supports getting and setting style properties with hash notation to ensure backwards-compatibility
# eg. @attachment.styles[:large][:geometry]@ will still work
def [](key)
if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
send(key)
elsif defined? @other_args[key]
@other_args[key]
end
end
def []=(key, value)
if [:name, :convert_options, :whiny, :processors, :geometry, :format, :animated, :source_file_options].include?(key)
send("#{key}=".intern, value)
else
@other_args[key] = value
end
end
# defaults to default format (nil by default)
def default_format
base = attachment.options[:default_format]
base.respond_to?(:call) ? base.call(attachment, name) : base
end
end
end
================================================
FILE: lib/paperclip/tempfile.rb
================================================
module Paperclip
# Overriding some implementation of Tempfile
class Tempfile < ::Tempfile
# Due to how ImageMagick handles its image format conversion and how
# Tempfile handles its naming scheme, it is necessary to override how
# Tempfile makes # its names so as to allow for file extensions. Idea
# taken from the comments on this blog post:
# http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
#
# This is Ruby 1.9.3's implementation.
def make_tmpname(prefix_suffix, n)
if RUBY_PLATFORM =~ /java/
case prefix_suffix
when String
prefix, suffix = prefix_suffix, ''
when Array
prefix, suffix = *prefix_suffix
else
raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
end
t = Time.now.strftime("%y%m%d")
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}-#{n}#{suffix}"
else
super
end
end
end
module TempfileEncoding
# This overrides Tempfile#binmode to make sure that the extenal encoding
# for binary mode is ASCII-8BIT. This behavior is what's in CRuby, but not
# in JRuby
def binmode
set_encoding('ASCII-8BIT')
super
end
end
end
if RUBY_PLATFORM =~ /java/
::Tempfile.send :include, Paperclip::TempfileEncoding
end
================================================
FILE: lib/paperclip/tempfile_factory.rb
================================================
module Paperclip
class TempfileFactory
def generate(name = random_name)
@name = name
file = Tempfile.new([basename, extension])
file.binmode
file
end
def extension
File.extname(@name)
end
def basename
Digest::MD5.hexdigest(File.basename(@name, extension))
end
def random_name
SecureRandom.uuid
end
end
end
================================================
FILE: lib/paperclip/thumbnail.rb
================================================
module Paperclip
# Handles thumbnailing images that are uploaded.
class Thumbnail < Processor
attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options,
:source_file_options, :animated, :auto_orient, :frame_index
# List of formats that we need to preserve animation
ANIMATED_FORMATS = %w(gif)
MULTI_FRAME_FORMATS = %w(.mkv .avi .mp4 .mov .mpg .mpeg .gif)
# Creates a Thumbnail object set to work on the +file+ given. It
# will attempt to transform the image into one defined by +target_geometry+
# which is a "WxH"-style string. +format+ will be inferred from the +file+
# unless specified. Thumbnail creation will raise no errors unless
# +whiny+ is true (which it is, by default. If +convert_options+ is
# set, the options will be appended to the convert command upon image conversion
#
# Options include:
#
# +geometry+ - the desired width and height of the thumbnail (required)
# +file_geometry_parser+ - an object with a method named +from_file+ that takes an image file and produces its geometry and a +transformation_to+. Defaults to Paperclip::Geometry
# +string_geometry_parser+ - an object with a method named +parse+ that takes a string and produces an object with +width+, +height+, and +to_s+ accessors. Defaults to Paperclip::Geometry
# +source_file_options+ - flags passed to the +convert+ command that influence how the source file is read
# +convert_options+ - flags passed to the +convert+ command that influence how the image is processed
# +whiny+ - whether to raise an error when processing fails. Defaults to true
# +format+ - the desired filename extension
# +animated+ - whether to merge all the layers in the image. Defaults to true
# +frame_index+ - the frame index of the source file to render as the thumbnail
def initialize(file, options = {}, attachment = nil)
super
geometry = options[:geometry].to_s
@crop = geometry[-1,1] == '#'
@target_geometry = options.fetch(:string_geometry_parser, Geometry).parse(geometry)
@current_geometry = options.fetch(:file_geometry_parser, Geometry).from_file(@file)
@source_file_options = options[:source_file_options]
@convert_options = options[:convert_options]
@whiny = options.fetch(:whiny, true)
@format = options[:format]
@animated = options.fetch(:animated, true)
@auto_orient = options.fetch(:auto_orient, true)
if @auto_orient && @current_geometry.respond_to?(:auto_orient)
@current_geometry.auto_orient
end
@source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
@convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
@current_format = File.extname(@file.path)
@basename = File.basename(@file.path, @current_format)
@frame_index = multi_frame_format? ? options.fetch(:frame_index, 0) : 0
end
# Returns true if the +target_geometry+ is meant to crop.
def crop?
@crop
end
# Returns true if the image is meant to make use of additional convert options.
def convert_options?
!@convert_options.nil? && !@convert_options.empty?
end
# Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
# that contains the new image.
def make
src = @file
filename = [@basename, @format ? ".#{@format}" : ""].join
dst = TempfileFactory.new.generate(filename)
begin
parameters = []
parameters << source_file_options
parameters << ":source"
parameters << transformation_command
parameters << convert_options
parameters << ":dest"
parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
frame = animated? ? "" : "[#{@frame_index}]"
convert(
parameters,
source: "#{File.expand_path(src.path)}#{frame}",
dest: File.expand_path(dst.path),
)
rescue Terrapin::ExitStatusError => e
if @whiny
message = "There was an error processing the thumbnail for #{@basename}:\n" + e.message
raise Paperclip::Error, message
end
rescue Terrapin::CommandNotFoundError => e
raise Paperclip::Errors::CommandNotFoundError.new("Could not run the `convert` command. Please install ImageMagick.")
end
dst
end
# Returns the command ImageMagick's +convert+ needs to transform the image
# into the thumbnail.
def transformation_command
scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
trans = []
trans << "-coalesce" if animated?
trans << "-auto-orient" if auto_orient
trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
trans << "-crop" << %["#{crop}"] << "+repage" if crop
trans << '-layers "optimize"' if animated?
trans
end
protected
def multi_frame_format?
MULTI_FRAME_FORMATS.include? @current_format
end
def animated?
@animated && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?) && identified_as_animated?
end
# Return true if ImageMagick's +identify+ returns an animated format
def identified_as_animated?
if @identified_as_animated.nil?
@identified_as_animated = ANIMATED_FORMATS.include? identify("-format %m :file", :file => "#{@file.path}[0]").to_s.downcase.strip
end
@identified_as_animated
rescue Terrapin::ExitStatusError => e
raise Paperclip::Error, "There was an error running `identify` for #{@basename}" if @whiny
rescue Terrapin::CommandNotFoundError => e
raise Paperclip::Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
end
end
end
================================================
FILE: lib/paperclip/url_generator.rb
================================================
require 'uri'
require 'active_support/core_ext/module/delegation'
module Paperclip
class UrlGenerator
def initialize(attachment)
@attachment = attachment
end
def for(style_name, options)
interpolated = attachment_options[:interpolator].interpolate(
most_appropriate_url, @attachment, style_name
)
escaped = escape_url_as_needed(interpolated, options)
timestamp_as_needed(escaped, options)
end
private
attr_reader :attachment
delegate :options, to: :attachment, prefix: true
# This method is all over the place.
def default_url
if attachment_options[:default_url].respond_to?(:call)
attachment_options[:default_url].call(@attachment)
elsif attachment_options[:default_url].is_a?(Symbol)
@attachment.instance.send(attachment_options[:default_url])
else
attachment_options[:default_url]
end
end
def most_appropriate_url
if @attachment.original_filename.nil?
default_url
else
attachment_options[:url]
end
end
def timestamp_as_needed(url, options)
if options[:timestamp] && timestamp_possible?
delimiter_char = url.match(/\?.+=/) ? '&' : '?'
"#{url}#{delimiter_char}#{@attachment.updated_at.to_s}"
else
url
end
end
def timestamp_possible?
@attachment.respond_to?(:updated_at) && @attachment.updated_at.present?
end
def escape_url_as_needed(url, options)
if options[:escape]
escape_url(url)
else
url
end
end
def escape_url(url)
if url.respond_to?(:escape)
url.escape
else
URI.escape(url).gsub(escape_regex){|m| "%#{m.ord.to_s(16).upcase}" }
end
end
def escape_regex
/[\?\(\)\[\]\+]/
end
end
end
================================================
FILE: lib/paperclip/validators/attachment_content_type_validator.rb
================================================
module Paperclip
module Validators
class AttachmentContentTypeValidator < ActiveModel::EachValidator
def initialize(options)
options[:allow_nil] = true unless options.has_key?(:allow_nil)
super
end
def self.helper_method_name
:validates_attachment_content_type
end
def validate_each(record, attribute, value)
base_attribute = attribute.to_sym
attribute = "#{attribute}_content_type".to_sym
value = record.send :read_attribute_for_validation, attribute
return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
validate_whitelist(record, attribute, value)
validate_blacklist(record, attribute, value)
if record.errors.include? attribute
record.errors[attribute].each do |error|
record.errors.add base_attribute, error
end
end
end
def validate_whitelist(record, attribute, value)
if allowed_types.present? && allowed_types.none? { |type| type === value }
mark_invalid record, attribute, allowed_types
end
end
def validate_blacklist(record, attribute, value)
if forbidden_types.present? && forbidden_types.any? { |type| type === value }
mark_invalid record, attribute, forbidden_types
end
end
def mark_invalid(record, attribute, types)
record.errors.add attribute, :invalid, options.merge(:types => types.join(', '))
end
def allowed_types
[options[:content_type]].flatten.compact
end
def forbidden_types
[options[:not]].flatten.compact
end
def check_validity!
unless options.has_key?(:content_type) || options.has_key?(:not)
raise ArgumentError, "You must pass in either :content_type or :not to the validator"
end
end
end
module HelperMethods
# Places ActiveModel validations on the content type of the file
# assigned. The possible options are:
# * +content_type+: Allowed content types. Can be a single content type
# or an array. Each type can be a String or a Regexp. It should be
# noted that Internet Explorer uploads files with content_types that you
# may not expect. For example, JPEG images are given image/pjpeg and
# PNGs are image/x-png, so keep that in mind when determining how you
# match. Allows all by default.
# * +not+: Forbidden content types.
# * +message+: The message to display when the uploaded file has an invalid
# content type.
# * +if+: A lambda or name of an instance method. Validation will only
# be run is this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
# NOTE: If you do not specify an [attachment]_content_type field on your
# model, content_type validation will work _ONLY upon assignment_ and
# re-validation after the instance has been reloaded will always succeed.
# You'll still need to have a virtual attribute (created by +attr_accessor+)
# name +[attachment]_content_type+ to be able to use this validator.
def validates_attachment_content_type(*attr_names)
options = _merge_attributes(attr_names)
validates_with AttachmentContentTypeValidator, options.dup
validate_before_processing AttachmentContentTypeValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators/attachment_file_name_validator.rb
================================================
module Paperclip
module Validators
class AttachmentFileNameValidator < ActiveModel::EachValidator
def initialize(options)
options[:allow_nil] = true unless options.has_key?(:allow_nil)
super
end
def self.helper_method_name
:validates_attachment_file_name
end
def validate_each(record, attribute, value)
base_attribute = attribute.to_sym
attribute = "#{attribute}_file_name".to_sym
value = record.send :read_attribute_for_validation, attribute
return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
validate_whitelist(record, attribute, value)
validate_blacklist(record, attribute, value)
if record.errors.include? attribute
record.errors[attribute].each do |error|
record.errors.add base_attribute, error
end
end
end
def validate_whitelist(record, attribute, value)
if allowed.present? && allowed.none? { |type| type === value }
mark_invalid record, attribute, allowed
end
end
def validate_blacklist(record, attribute, value)
if forbidden.present? && forbidden.any? { |type| type === value }
mark_invalid record, attribute, forbidden
end
end
def mark_invalid(record, attribute, patterns)
record.errors.add attribute, :invalid, options.merge(:names => patterns.join(', '))
end
def allowed
[options[:matches]].flatten.compact
end
def forbidden
[options[:not]].flatten.compact
end
def check_validity!
unless options.has_key?(:matches) || options.has_key?(:not)
raise ArgumentError, "You must pass in either :matches or :not to the validator"
end
end
end
module HelperMethods
# Places ActiveModel validations on the name of the file
# assigned. The possible options are:
# * +matches+: Allowed filename patterns as Regexps. Can be a single one
# or an array.
# * +not+: Forbidden file name patterns, specified the same was as +matches+.
# * +message+: The message to display when the uploaded file has an invalid
# name.
# * +if+: A lambda or name of an instance method. Validation will only
# be run is this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_attachment_file_name(*attr_names)
options = _merge_attributes(attr_names)
validates_with AttachmentFileNameValidator, options.dup
validate_before_processing AttachmentFileNameValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators/attachment_file_type_ignorance_validator.rb
================================================
require 'active_model/validations/presence'
module Paperclip
module Validators
class AttachmentFileTypeIgnoranceValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# This doesn't do anything. It's just to mark that you don't care about
# the file_names or content_types of your incoming attachments.
end
def self.helper_method_name
:do_not_validate_attachment_file_type
end
end
module HelperMethods
# Places ActiveModel validations on the presence of a file.
# Options:
# * +if+: A lambda or name of an instance method. Validation will only
# be run if this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def do_not_validate_attachment_file_type(*attr_names)
options = _merge_attributes(attr_names)
validates_with AttachmentFileTypeIgnoranceValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators/attachment_presence_validator.rb
================================================
require 'active_model/validations/presence'
module Paperclip
module Validators
class AttachmentPresenceValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.send("#{attribute}_file_name").blank?
record.errors.add(attribute, :blank, options)
end
end
def self.helper_method_name
:validates_attachment_presence
end
end
module HelperMethods
# Places ActiveModel validations on the presence of a file.
# Options:
# * +if+: A lambda or name of an instance method. Validation will only
# be run if this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_attachment_presence(*attr_names)
options = _merge_attributes(attr_names)
validates_with AttachmentPresenceValidator, options.dup
validate_before_processing AttachmentPresenceValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators/attachment_size_validator.rb
================================================
require 'active_model/validations/numericality'
module Paperclip
module Validators
class AttachmentSizeValidator < ActiveModel::Validations::NumericalityValidator
AVAILABLE_CHECKS = [:less_than, :less_than_or_equal_to, :greater_than, :greater_than_or_equal_to]
def initialize(options)
extract_options(options)
super
end
def self.helper_method_name
:validates_attachment_size
end
def validate_each(record, attr_name, value)
base_attr_name = attr_name
attr_name = "#{attr_name}_file_size".to_sym
value = record.send(:read_attribute_for_validation, attr_name)
unless value.blank?
options.slice(*AVAILABLE_CHECKS).each do |option, option_value|
option_value = option_value.call(record) if option_value.is_a?(Proc)
option_value = extract_option_value(option, option_value)
unless value.send(CHECKS[option], option_value)
error_message_key = options[:in] ? :in_between : option
[ attr_name, base_attr_name ].each do |error_attr_name|
record.errors.add(error_attr_name, error_message_key, filtered_options(value).merge(
:min => min_value_in_human_size(record),
:max => max_value_in_human_size(record),
:count => human_size(option_value)
))
end
end
end
end
end
def check_validity!
unless (AVAILABLE_CHECKS + [:in]).any? { |argument| options.has_key?(argument) }
raise ArgumentError, "You must pass either :less_than, :greater_than, or :in to the validator"
end
end
private
def extract_options(options)
if range = options[:in]
if !options[:in].respond_to?(:call)
options[:less_than_or_equal_to] = range.max
options[:greater_than_or_equal_to] = range.min
else
options[:less_than_or_equal_to] = range
options[:greater_than_or_equal_to] = range
end
end
end
def extract_option_value(option, option_value)
if option_value.is_a?(Range)
if [:less_than, :less_than_or_equal_to].include?(option)
option_value.max
else
option_value.min
end
else
option_value
end
end
def human_size(size)
ActiveSupport::NumberHelper.number_to_human_size(size)
end
def min_value_in_human_size(record)
value = options[:greater_than_or_equal_to] || options[:greater_than]
value = value.call(record) if value.respond_to?(:call)
value = value.min if value.respond_to?(:min)
human_size(value)
end
def max_value_in_human_size(record)
value = options[:less_than_or_equal_to] || options[:less_than]
value = value.call(record) if value.respond_to?(:call)
value = value.max if value.respond_to?(:max)
human_size(value)
end
end
module HelperMethods
# Places ActiveModel validations on the size of the file assigned. The
# possible options are:
# * +in+: a Range of bytes (i.e. +1..1.megabyte+),
# * +less_than+: equivalent to :in => 0..options[:less_than]
# * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
# * +message+: error message to display, use :min and :max as replacements
# * +if+: A lambda or name of an instance method. Validation will only
# be run if this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_attachment_size(*attr_names)
options = _merge_attributes(attr_names)
validates_with AttachmentSizeValidator, options.dup
validate_before_processing AttachmentSizeValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators/media_type_spoof_detection_validator.rb
================================================
require 'active_model/validations/presence'
module Paperclip
module Validators
class MediaTypeSpoofDetectionValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
adapter = Paperclip.io_adapters.for(value)
if Paperclip::MediaTypeSpoofDetector.using(adapter, value.original_filename, value.content_type).spoofed?
record.errors.add(attribute, :spoofed_media_type)
end
if adapter.tempfile
adapter.tempfile.close(true)
end
end
end
module HelperMethods
# Places ActiveModel validations on the presence of a file.
# Options:
# * +if+: A lambda or name of an instance method. Validation will only
# be run if this lambda or method returns true.
# * +unless+: Same as +if+ but validates if lambda or method returns false.
def validates_media_type_spoof_detection(*attr_names)
options = _merge_attributes(attr_names)
validates_with MediaTypeSpoofDetectionValidator, options.dup
validate_before_processing MediaTypeSpoofDetectionValidator, options.dup
end
end
end
end
================================================
FILE: lib/paperclip/validators.rb
================================================
require 'active_model'
require 'active_support/concern'
require 'active_support/core_ext/array/wrap'
require 'paperclip/validators/attachment_content_type_validator'
require 'paperclip/validators/attachment_file_name_validator'
require 'paperclip/validators/attachment_presence_validator'
require 'paperclip/validators/attachment_size_validator'
require 'paperclip/validators/media_type_spoof_detection_validator'
require 'paperclip/validators/attachment_file_type_ignorance_validator'
module Paperclip
module Validators
extend ActiveSupport::Concern
included do
extend HelperMethods
include HelperMethods
end
::Paperclip::REQUIRED_VALIDATORS = [AttachmentFileNameValidator, AttachmentContentTypeValidator, AttachmentFileTypeIgnoranceValidator]
module ClassMethods
# This method is a shortcut to validator classes that is in
# "Attachment...Validator" format. It is almost the same thing as the
# +validates+ method that shipped with Rails, but this is customized to
# be using with attachment validators. This is helpful when you're using
# multiple attachment validators on a single attachment.
#
# Example of using the validator:
#
# validates_attachment :avatar, :presence => true,
# :content_type => { :content_type => "image/jpg" },
# :size => { :in => 0..10.kilobytes }
#
def validates_attachment(*attributes)
options = attributes.extract_options!.dup
Paperclip::Validators.constants.each do |constant|
if constant.to_s =~ /\AAttachment(.+)Validator\z/
validator_kind = $1.underscore.to_sym
if options.has_key?(validator_kind)
validator_options = options.delete(validator_kind)
validator_options = {} if validator_options == true
conditional_options = options.slice(:if, :unless)
Array.wrap(validator_options).each do |local_options|
method_name = Paperclip::Validators.const_get(constant.to_s).helper_method_name
send(method_name, attributes, local_options.merge(conditional_options))
end
end
end
end
end
def validate_before_processing(validator_class, options)
options = options.dup
attributes = options.delete(:attributes)
attributes.each do |attribute|
options[:attributes] = [attribute]
create_validating_before_filter(attribute, validator_class, options)
end
end
def create_validating_before_filter(attribute, validator_class, options)
if_clause = options.delete(:if)
unless_clause = options.delete(:unless)
send(:"before_#{attribute}_post_process", :if => if_clause, :unless => unless_clause) do |*args|
validator_class.new(options.dup).validate(self)
end
end
end
end
end
================================================
FILE: lib/paperclip/version.rb
================================================
module Paperclip
unless defined?(Paperclip::VERSION)
VERSION = "6.1.0".freeze
end
end
================================================
FILE: lib/paperclip.rb
================================================
# Paperclip allows file attachments that are stored in the filesystem. All graphical
# transformations are done using the Graphics/ImageMagick command line utilities and
# are stored in Tempfiles until the record is saved. Paperclip does not require a
# separate model for storing the attachment's information, instead adding a few simple
# columns to your table.
#
# Author:: Jon Yurek
# Copyright:: Copyright (c) 2008-2011 thoughtbot, inc.
# License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
#
# Paperclip defines an attachment as any file, though it makes special considerations
# for image files. You can declare that a model has an attached file with the
# +has_attached_file+ method:
#
# class User < ActiveRecord::Base
# has_attached_file :avatar, :styles => { :thumb => "100x100" }
# end
#
# user = User.new
# user.avatar = params[:user][:avatar]
# user.avatar.url
# # => "/users/avatars/4/original_me.jpg"
# user.avatar.url(:thumb)
# # => "/users/avatars/4/thumb_me.jpg"
#
# See the +has_attached_file+ documentation for more details.
require 'erb'
require 'digest'
require 'tempfile'
require 'paperclip/version'
require 'paperclip/geometry_parser_factory'
require 'paperclip/geometry_detector_factory'
require 'paperclip/geometry'
require 'paperclip/processor'
require 'paperclip/processor_helpers'
require 'paperclip/tempfile'
require 'paperclip/thumbnail'
require 'paperclip/interpolations/plural_cache'
require 'paperclip/interpolations'
require 'paperclip/tempfile_factory'
require 'paperclip/style'
require 'paperclip/attachment'
require 'paperclip/storage'
require 'paperclip/callbacks'
require 'paperclip/file_command_content_type_detector'
require 'paperclip/media_type_spoof_detector'
require 'paperclip/content_type_detector'
require 'paperclip/glue'
require 'paperclip/errors'
require 'paperclip/missing_attachment_styles'
require 'paperclip/validators'
require 'paperclip/logger'
require 'paperclip/helpers'
require 'paperclip/has_attached_file'
require 'paperclip/attachment_registry'
require 'paperclip/filename_cleaner'
require 'paperclip/rails_environment'
begin
# Use mime/types/columnar if available, for reduced memory usage
require "mime/types/columnar"
rescue LoadError
require "mime/types"
end
require 'mimemagic'
require 'mimemagic/overlay'
require 'logger'
require 'terrapin'
require 'paperclip/railtie' if defined?(Rails::Railtie)
# The base module that gets included in ActiveRecord::Base. See the
# documentation for Paperclip::ClassMethods for more useful information.
module Paperclip
extend Helpers
extend Logger
extend ProcessorHelpers
# Provides configurability to Paperclip. The options available are:
# * whiny: Will raise an error if Paperclip cannot process thumbnails of
# an uploaded image. Defaults to true.
# * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
# log levels, etc. Defaults to true.
# * command_path: Defines the path at which to find the command line
# programs if they are not visible to Rails the system's search path. Defaults to
# nil, which uses the first executable found in the user's search path.
# * use_exif_orientation: Whether to inspect EXIF data to determine an
# image's orientation. Defaults to true.
def self.options
@options ||= {
command_path: nil,
content_type_mappings: {},
log: true,
log_command: true,
read_timeout: nil,
swallow_stderr: true,
use_exif_orientation: true,
whiny: true,
is_windows: Gem.win_platform?
}
end
def self.io_adapters=(new_registry)
@io_adapters = new_registry
end
def self.io_adapters
@io_adapters ||= Paperclip::AdapterRegistry.new
end
module ClassMethods
# +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
# is typically a file stored somewhere on the filesystem and has been uploaded by a user.
# The attribute returns a Paperclip::Attachment object which handles the management of
# that file. The intent is to make the attachment as much like a normal attribute. The
# thumbnails will be created when the new file is assigned, but they will *not* be saved
# until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
# called on it, the attachment will *not* be deleted until +save+ is called. See the
# Paperclip::Attachment documentation for more specifics. There are a number of options
# you can set to change the behavior of a Paperclip attachment:
# * +url+: The full URL of where the attachment is publicly accessible. This can just
# as easily point to a directory served directly through Apache as it can to an action
# that can control permissions. You can specify the full domain and path, but usually
# just an absolute path is sufficient. The leading slash *must* be included manually for
# absolute paths. The default value is
# "/system/:class/:attachment/:id_partition/:style/:filename". See
# Paperclip::Attachment#interpolate for more information on variable interpolaton.
# :url => "/:class/:attachment/:id/:style_:filename"
# :url => "http://some.other.host/stuff/:class/:id_:extension"
# Note: When using the +s3+ storage option, the +url+ option expects
# particular values. See the Paperclip::Storage::S3#url documentation for
# specifics.
# * +default_url+: The URL that will be returned if there is no attachment assigned.
# This field is interpolated just as the url is. The default value is
# "/:attachment/:style/missing.png"
# has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
# User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
# * +styles+: A hash of thumbnail styles and their geometries. You can find more about
# geometry strings at the ImageMagick website
# (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
# also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
# inside the dimensions and then crop the rest off (weighted at the center). The
# default value is to generate no thumbnails.
# * +default_style+: The thumbnail style that will be used by default URLs.
# Defaults to +original+.
# has_attached_file :avatar, :styles => { :normal => "100x100#" },
# :default_style => :normal
# user.avatar.url # => "/avatars/23/normal_me.png"
# * +keep_old_files+: Keep the existing attachment files (original + resized) from
# being automatically deleted when an attachment is cleared or updated. Defaults to +false+.
# * +preserve_files+: Keep the existing attachment files in all cases, even if the parent
# record is destroyed. Defaults to +false+.
# * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
# to a command line error. This will override the global setting for this attachment.
# Defaults to true.
# * +convert_options+: When creating thumbnails, use this free-form options
# array to pass in various convert command options. Typical options are "-strip" to
# remove all Exif data from the image (save space for thumbnails and avatars) or
# "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
# convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
# Note that this option takes a hash of options, each of which correspond to the style
# of thumbnail being generated. You can also specify :all as a key, which will apply
# to all of the thumbnails being generated. If you specify options for the :original,
# it would be best if you did not specify destructive options, as the intent of keeping
# the original around is to regenerate all the thumbnails when requirements change.
# has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
# :convert_options => {
# :all => "-strip",
# :negative => "-negate"
# }
# NOTE: While not deprecated yet, it is not recommended to specify options this way.
# It is recommended that :convert_options option be included in the hash passed to each
# :styles for compatibility with future versions.
# NOTE: Strings supplied to :convert_options are split on space in order to undergo
# shell quoting for safety. If your options require a space, please pre-split them
# and pass an array to :convert_options instead.
# * +storage+: Chooses the storage backend where the files will be stored. The current
# choices are :filesystem, :fog and :s3. The default is :filesystem. Make sure you read the
# documentation for Paperclip::Storage::Filesystem, Paperclip::Storage::Fog and Paperclip::Storage::S3
# for backend-specific options.
#
# It's also possible for you to dynamically define your interpolation string for :url,
# :default_url, and :path in your model by passing a method name as a symbol as a argument
# for your has_attached_file definition:
#
# class Person
# has_attached_file :avatar, :default_url => :default_url_by_gender
#
# private
#
# def default_url_by_gender
# "/assets/avatars/default_#{gender}.png"
# end
# end
def has_attached_file(name, options = {})
HasAttachedFile.define_on(self, name, options)
end
end
end
# This stuff needs to be run after Paperclip is defined.
require 'paperclip/io_adapters/registry'
require 'paperclip/io_adapters/abstract_adapter'
require 'paperclip/io_adapters/empty_string_adapter'
require 'paperclip/io_adapters/identity_adapter'
require 'paperclip/io_adapters/file_adapter'
require 'paperclip/io_adapters/stringio_adapter'
require 'paperclip/io_adapters/data_uri_adapter'
require 'paperclip/io_adapters/nil_adapter'
require 'paperclip/io_adapters/attachment_adapter'
require 'paperclip/io_adapters/uploaded_file_adapter'
require 'paperclip/io_adapters/uri_adapter'
require 'paperclip/io_adapters/http_url_proxy_adapter'
================================================
FILE: lib/tasks/paperclip.rake
================================================
require 'paperclip/attachment_registry'
module Paperclip
module Task
def self.obtain_class
class_name = ENV['CLASS'] || ENV['class']
raise "Must specify CLASS" unless class_name
class_name
end
def self.obtain_attachments(klass)
klass = Paperclip.class_for(klass.to_s)
name = ENV['ATTACHMENT'] || ENV['attachment']
attachment_names = Paperclip::AttachmentRegistry.names_for(klass)
if attachment_names.empty?
raise "Class #{klass.name} has no attachments specified"
end
if name.present? && attachment_names.map(&:to_s).include?(name.to_s)
[ name ]
else
attachment_names
end
end
def self.log_error(error)
$stderr.puts error
end
end
end
namespace :paperclip do
desc "Refreshes both metadata and thumbnails."
task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
namespace :refresh do
desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT and STYLES splitted by comma)."
task :thumbnails => :environment do
klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
styles = (ENV['STYLES'] || ENV['styles'] || '').split(',').map(&:to_sym)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
attachment = instance.send(name)
begin
attachment.reprocess!(*styles)
rescue StandardError => e
Paperclip::Task.log_error("exception while processing #{klass} ID #{instance.id}:")
Paperclip::Task.log_error(" " + e.message + "\n")
end
unless instance.errors.blank?
Paperclip::Task.log_error("errors while processing #{klass} ID #{instance.id}:")
Paperclip::Task.log_error(" " + instance.errors.full_messages.join("\n ") + "\n")
end
end
end
end
desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
task :metadata => :environment do
klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
attachment = instance.send(name)
if file = Paperclip.io_adapters.for(attachment, attachment.options[:adapter_options])
instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
instance.send("#{name}_content_type=", file.content_type.to_s.strip)
instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
instance.save(:validate => false)
else
true
end
end
end
end
desc "Regenerates missing thumbnail styles for all classes using Paperclip."
task :missing_styles => :environment do
Rails.application.eager_load!
Paperclip.missing_attachments_styles.each do |klass, attachment_definitions|
attachment_definitions.each do |attachment_name, missing_styles|
puts "Regenerating #{klass} -> #{attachment_name} -> #{missing_styles.inspect}"
ENV['CLASS'] = klass.to_s
ENV['ATTACHMENT'] = attachment_name.to_s
ENV['STYLES'] = missing_styles.join(',')
Rake::Task['paperclip:refresh:thumbnails'].execute
end
end
Paperclip.save_current_attachments_styles!
end
desc "Regenerates fingerprints for a given CLASS (and optional ATTACHMENT). Useful when changing digest."
task :fingerprints => :environment do
klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
attachment = instance.send(name)
attachment.assign(attachment)
instance.save(:validate => false)
end
end
end
end
desc "Cleans out invalid attachments. Useful after you've added new validations."
task :clean => :environment do
klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
unless instance.valid?
attributes = %w(file_size file_name content_type).map{ |suffix| "#{name}_#{suffix}".to_sym }
if attributes.any?{ |attribute| instance.errors[attribute].present? }
instance.send("#{name}=", nil)
instance.save(:validate => false)
end
end
end
end
end
desc "find missing attachments. Useful to know which attachments are broken"
task :find_broken_attachments => :environment do
klass = Paperclip::Task.obtain_class
names = Paperclip::Task.obtain_attachments(klass)
names.each do |name|
Paperclip.each_instance_with_attachment(klass, name) do |instance|
attachment = instance.send(name)
if attachment.exists?
print "."
else
Paperclip::Task.log_error("#{instance.class}##{attachment.name}, #{instance.id}, #{attachment.url}")
end
end
end
end
end
================================================
FILE: paperclip.gemspec
================================================
$LOAD_PATH.push File.expand_path("../lib", __FILE__)
require 'paperclip/version'
Gem::Specification.new do |s|
s.name = "paperclip"
s.version = Paperclip::VERSION
s.platform = Gem::Platform::RUBY
s.author = "Jon Yurek"
s.email = ["jyurek@thoughtbot.com"]
s.homepage = "https://github.com/thoughtbot/paperclip"
s.summary = "File attachments as attributes for ActiveRecord"
s.description = "Easy upload management for ActiveRecord"
s.license = "MIT"
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
if File.exist?('UPGRADING')
s.post_install_message = File.read("UPGRADING")
end
s.requirements << "ImageMagick"
s.required_ruby_version = ">= 2.1.0"
s.add_dependency('activemodel', '>= 4.2.0')
s.add_dependency('activesupport', '>= 4.2.0')
s.add_dependency('terrapin', '~> 0.6.0')
s.add_dependency('mime-types')
s.add_dependency('mimemagic', '~> 0.3.0')
s.add_development_dependency('activerecord', '>= 4.2.0')
s.add_development_dependency('shoulda')
s.add_development_dependency('rspec', '~> 3.0')
s.add_development_dependency('appraisal')
s.add_development_dependency('mocha')
s.add_development_dependency('aws-sdk-s3')
s.add_development_dependency('bourne')
s.add_development_dependency('cucumber-rails')
s.add_development_dependency('cucumber-expressions', '4.0.3') # TODO: investigate failures on 4.0.4
s.add_development_dependency('aruba', '~> 0.9.0')
s.add_development_dependency('nokogiri')
s.add_development_dependency('capybara')
s.add_development_dependency('bundler')
s.add_development_dependency('fog-aws')
s.add_development_dependency('fog-local')
s.add_development_dependency('launchy')
s.add_development_dependency('rake')
s.add_development_dependency('fakeweb')
s.add_development_dependency('railties')
s.add_development_dependency('generator_spec')
s.add_development_dependency('timecop')
end
================================================
FILE: shoulda_macros/paperclip.rb
================================================
require 'paperclip/matchers'
module Paperclip
# =Paperclip Shoulda Macros
#
# These macros are intended for use with shoulda, and will be included into
# your tests automatically. All of the macros use the standard shoulda
# assumption that the name of the test is based on the name of the model
# you're testing (that is, UserTest is the test for the User model), and
# will load that class for testing purposes.
module Shoulda
include Matchers
# This will test whether you have defined your attachment correctly by
# checking for all the required fields exist after the definition of the
# attachment.
def should_have_attached_file name
klass = self.name.gsub(/Test$/, '').constantize
matcher = have_attached_file name
should matcher.description do
assert_accepts(matcher, klass)
end
end
# Tests for validations on the presence of the attachment.
def should_validate_attachment_presence name
klass = self.name.gsub(/Test$/, '').constantize
matcher = validate_attachment_presence name
should matcher.description do
assert_accepts(matcher, klass)
end
end
# Tests that you have content_type validations specified. There are two
# options, :valid and :invalid. Both accept an array of strings. The
# strings should be a list of content types which will pass and fail
# validation, respectively.
def should_validate_attachment_content_type name, options = {}
klass = self.name.gsub(/Test$/, '').constantize
valid = [options[:valid]].flatten
invalid = [options[:invalid]].flatten
matcher = validate_attachment_content_type(name).allowing(valid).rejecting(invalid)
should matcher.description do
assert_accepts(matcher, klass)
end
end
# Tests to ensure that you have file size validations turned on. You
# can pass the same options to this that you can to
# validate_attachment_file_size - :less_than, :greater_than, and :in.
# :less_than checks that a file is less than a certain size, :greater_than
# checks that a file is more than a certain size, and :in takes a Range or
# Array which specifies the lower and upper limits of the file size.
def should_validate_attachment_size name, options = {}
klass = self.name.gsub(/Test$/, '').constantize
min = options[:greater_than] || (options[:in] && options[:in].first) || 0
max = options[:less_than] || (options[:in] && options[:in].last) || (1.0/0)
range = (min..max)
matcher = validate_attachment_size(name).in(range)
should matcher.description do
assert_accepts(matcher, klass)
end
end
# Stubs the HTTP PUT for an attachment using S3 storage.
#
# @example
# stub_paperclip_s3('user', 'avatar', 'png')
def stub_paperclip_s3(model, attachment, extension)
definition = model.gsub(" ", "_").classify.constantize.
attachment_definitions[attachment.to_sym]
path = "http://s3.amazonaws.com/:id/#{definition[:path]}"
path.gsub!(/:([^\/\.]+)/) do |match|
"([^\/\.]+)"
end
begin
FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
rescue NameError
raise NameError, "the stub_paperclip_s3 shoulda macro requires the fakeweb gem."
end
end
# Stub S3 and return a file for attachment. Best with Factory Girl.
# Uses a strict directory convention:
#
# features/support/paperclip
#
# This method is used by the Paperclip-provided Cucumber step:
#
# When I attach a "demo_tape" "mp3" file to a "band" on S3
#
# @example
# Factory.define :band_with_demo_tape, :parent => :band do |band|
# band.demo_tape { band.paperclip_fixture("band", "demo_tape", "png") }
# end
def paperclip_fixture(model, attachment, extension)
stub_paperclip_s3(model, attachment, extension)
base_path = File.join(File.dirname(__FILE__), "..", "..",
"features", "support", "paperclip")
File.new(File.join(base_path, model, "#{attachment}.#{extension}"))
end
end
end
if defined?(ActionDispatch::Integration::Session)
class ActionDispatch::IntegrationTest::Session #:nodoc:
include Paperclip::Shoulda
end
elsif defined?(ActionController::Integration::Session)
class ActionController::Integration::Session #:nodoc:
include Paperclip::Shoulda
end
end
if defined?(FactoryGirl::Factory)
class FactoryGirl::Factory
include Paperclip::Shoulda #:nodoc:
end
else
class Factory
include Paperclip::Shoulda #:nodoc:
end
end
if defined?(Minitest)
class Minitest::Unit::TestCase #:nodoc:
extend Paperclip::Shoulda
end
elsif defined?(Test)
class Test::Unit::TestCase #:nodoc:
extend Paperclip::Shoulda
end
end
================================================
FILE: spec/database.yml
================================================
test:
adapter: sqlite3
database: ":memory:"
================================================
FILE: spec/paperclip/attachment_definitions_spec.rb
================================================
require 'spec_helper'
describe "Attachment Definitions" do
it 'returns all of the attachments on the class' do
reset_class "Dummy"
Dummy.has_attached_file :avatar, {path: "abc"}
Dummy.has_attached_file :other_attachment, {url: "123"}
Dummy.do_not_validate_attachment_file_type :avatar
expected = {avatar: {path: "abc"}, other_attachment: {url: "123"}}
expect(Dummy.attachment_definitions).to eq expected
end
end
================================================
FILE: spec/paperclip/attachment_processing_spec.rb
================================================
require 'spec_helper'
describe 'Attachment Processing' do
before { rebuild_class }
context 'using validates_attachment_content_type' do
it 'processes attachments given a valid assignment' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment_content_type :avatar, content_type: "image/png"
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles)
attachment.assign(file)
end
it 'does not process attachments given an invalid assignment with :not' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment_content_type :avatar, not: "image/png"
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles).never
attachment.assign(file)
end
it 'does not process attachments given an invalid assignment with :content_type' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment_content_type :avatar, content_type: "image/tiff"
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles).never
attachment.assign(file)
end
it 'allows what would be an invalid assignment when validation :if clause returns false' do
invalid_assignment = File.new(fixture_file("5k.png"))
Dummy.validates_attachment_content_type :avatar, content_type: "image/tiff", if: lambda{false}
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles)
attachment.assign(invalid_assignment)
end
end
context 'using validates_attachment' do
it 'processes attachments given a valid assignment' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment :avatar, content_type: {content_type: "image/png"}
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles)
attachment.assign(file)
end
it 'does not process attachments given an invalid assignment with :not' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment :avatar, content_type: {not: "image/png"}
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles).never
attachment.assign(file)
end
it 'does not process attachments given an invalid assignment with :content_type' do
file = File.new(fixture_file("5k.png"))
Dummy.validates_attachment :avatar, content_type: {content_type: "image/tiff"}
instance = Dummy.new
attachment = instance.avatar
attachment.expects(:post_process_styles).never
attachment.assign(file)
end
end
end
================================================
FILE: spec/paperclip/attachment_registry_spec.rb
================================================
require 'spec_helper'
describe 'Attachment Registry' do
before do
Paperclip::AttachmentRegistry.clear
end
context '.names_for' do
it 'includes attachment names for the given class' do
foo = Class.new
Paperclip::AttachmentRegistry.register(foo, :avatar, {})
assert_equal [:avatar], Paperclip::AttachmentRegistry.names_for(foo)
end
it 'does not include attachment names for other classes' do
foo = Class.new
bar = Class.new
Paperclip::AttachmentRegistry.register(foo, :avatar, {})
Paperclip::AttachmentRegistry.register(bar, :lover, {})
assert_equal [:lover], Paperclip::AttachmentRegistry.names_for(bar)
end
it 'produces the empty array for a missing key' do
assert_empty Paperclip::AttachmentRegistry.names_for(Class.new)
end
end
context '.each_definition' do
it 'calls the block with the class, attachment name, and options' do
foo = Class.new
expected_accumulations = [
[foo, :avatar, { yo: "greeting" }],
[foo, :greeter, { ciao: "greeting" }]
]
expected_accumulations.each do |args|
Paperclip::AttachmentRegistry.register(*args)
end
accumulations = []
Paperclip::AttachmentRegistry.each_definition do |*args|
accumulations << args
end
assert_equal expected_accumulations, accumulations
end
end
context '.definitions_for' do
it 'produces the attachment name and options' do
expected_definitions = {
avatar: { yo: "greeting" },
greeter: { ciao: "greeting" }
}
foo = Class.new
Paperclip::AttachmentRegistry.register(
foo,
:avatar,
yo: "greeting"
)
Paperclip::AttachmentRegistry.register(
foo,
:greeter,
ciao: "greeting"
)
definitions = Paperclip::AttachmentRegistry.definitions_for(foo)
assert_equal expected_definitions, definitions
end
it 'produces defintions for subclasses' do
expected_definitions = { avatar: { yo: "greeting" } }
foo = Class.new
bar = Class.new(foo)
Paperclip::AttachmentRegistry.register(
foo,
:avatar,
expected_definitions[:avatar]
)
definitions = Paperclip::AttachmentRegistry.definitions_for(bar)
assert_equal expected_definitions, definitions
end
it 'produces defintions for subclasses but deep merging them' do
foo_definitions = { avatar: { yo: "greeting" } }
bar_definitions = { avatar: { ciao: "greeting" } }
expected_definitions = {
avatar: {
yo: "greeting",
ciao: "greeting"
}
}
foo = Class.new
bar = Class.new(foo)
Paperclip::AttachmentRegistry.register(
foo,
:avatar,
foo_definitions[:avatar]
)
Paperclip::AttachmentRegistry.register(
bar,
:avatar,
bar_definitions[:avatar]
)
definitions = Paperclip::AttachmentRegistry.definitions_for(bar)
assert_equal expected_definitions, definitions
end
it 'allows subclasses to override attachment defitions' do
foo_definitions = { avatar: { yo: "greeting" } }
bar_definitions = { avatar: { yo: "hello" } }
expected_definitions = {
avatar: {
yo: "hello"
}
}
foo = Class.new
bar = Class.new(foo)
Paperclip::AttachmentRegistry.register(
foo,
:avatar,
foo_definitions[:avatar]
)
Paperclip::AttachmentRegistry.register(
bar,
:avatar,
bar_definitions[:avatar]
)
definitions = Paperclip::AttachmentRegistry.definitions_for(bar)
assert_equal expected_definitions, definitions
end
end
context '.clear' do
it 'removes all of the existing attachment definitions' do
foo = Class.new
Paperclip::AttachmentRegistry.register(
foo,
:greeter,
ciao: "greeting"
)
Paperclip::AttachmentRegistry.clear
assert_empty Paperclip::AttachmentRegistry.names_for(foo)
end
end
end
================================================
FILE: spec/paperclip/attachment_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Attachment do
it "is not present when file not set" do
rebuild_class
dummy = Dummy.new
expect(dummy.avatar).to be_blank
expect(dummy.avatar).to_not be_present
end
it "is present when the file is set" do
rebuild_class
dummy = Dummy.new
dummy.avatar = File.new(fixture_file("50x50.png"), "rb")
expect(dummy.avatar).to_not be_blank
expect(dummy.avatar).to be_present
end
it "processes :original style first" do
file = File.new(fixture_file("50x50.png"), 'rb')
rebuild_class styles: { small: '100x>', original: '42x42#' }
dummy = Dummy.new
dummy.avatar = file
dummy.save
# :small avatar should be 42px wide (processed original), not 50px (preprocessed original)
expect(`identify -format "%w" "#{dummy.avatar.path(:small)}"`.strip).to eq "42"
file.close
end
it "does not delete styles that don't get reprocessed" do
file = File.new(fixture_file("50x50.png"), 'rb')
rebuild_class styles: {
small: "100x>",
large: "500x>",
original: "42x42#"
}
dummy = Dummy.new
dummy.avatar = file
dummy.save
expect(dummy.avatar.path(:small)).to exist
expect(dummy.avatar.path(:large)).to exist
expect(dummy.avatar.path(:original)).to exist
dummy.avatar.reprocess!(:small)
expect(dummy.avatar.path(:small)).to exist
expect(dummy.avatar.path(:large)).to exist
expect(dummy.avatar.path(:original)).to exist
end
it "reprocess works with virtual content_type attribute" do
rebuild_class styles: { small: "100x>" }
modify_table { |t| t.remove :avatar_content_type }
Dummy.send :attr_accessor, :avatar_content_type
Dummy.validates_attachment_content_type(
:avatar,
content_type: %w(image/jpeg image/png)
)
Dummy.create!(avatar: File.new(fixture_file("50x50.png"), "rb"))
dummy = Dummy.first
dummy.avatar.reprocess!(:small)
expect(dummy.avatar.path(:small)).to exist
end
context "having a not empty hash as a default option" do
before do
@old_default_options = Paperclip::Attachment.default_options.dup
@new_default_options = { convert_options: { all: "-background white" } }
Paperclip::Attachment.default_options.merge!(@new_default_options)
end
after do
Paperclip::Attachment.default_options.merge!(@old_default_options)
end
it "deep merges when it is overridden" do
new_options = { convert_options: { thumb: "-thumbnailize" } }
attachment = Paperclip::Attachment.new(:name, :instance, new_options)
expect(Paperclip::Attachment.default_options.deep_merge(new_options)).to eq attachment.instance_variable_get("@options")
end
end
it "handles a boolean second argument to #url" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(
:name,
FakeModel.new,
url_generator: mock_url_generator_builder
)
attachment.url(:style_name, true)
expect(mock_url_generator_builder.has_generated_url_with_options?(timestamp: true, escape: true)).to eq true
attachment.url(:style_name, false)
expect(mock_url_generator_builder.has_generated_url_with_options?(timestamp: false, escape: true)).to eq true
end
it "passes the style and options through to the URL generator on #url" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(
:name,
FakeModel.new,
url_generator: mock_url_generator_builder
)
attachment.url(:style_name, options: :values)
expect(mock_url_generator_builder.has_generated_url_with_options?(options: :values)).to eq true
end
it "passes default options through when #url is given one argument" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
use_timestamp: true)
attachment.url(:style_name)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true)
end
it "passes default style and options through when #url is given no arguments" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
default_style: 'default style',
url_generator: mock_url_generator_builder,
use_timestamp: true)
attachment.url
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true)
assert mock_url_generator_builder.has_generated_url_with_style_name?('default style')
end
it "passes the option timestamp: true if :use_timestamp is true and :timestamp is not passed" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
use_timestamp: true)
attachment.url(:style_name)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true)
end
it "passes the option timestamp: false if :use_timestamp is false and :timestamp is not passed" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
use_timestamp: false)
attachment.url(:style_name)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: false)
end
it "does not change the :timestamp if :timestamp is passed" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
use_timestamp: false)
attachment.url(:style_name, timestamp: true)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true, timestamp: true)
end
it "renders JSON as default style" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
default_style: 'default style',
url_generator: mock_url_generator_builder)
attachment.as_json
assert mock_url_generator_builder.has_generated_url_with_style_name?('default style')
end
it "passes the option escape: true if :escape_url is true and :escape is not passed" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
escape_url: true)
attachment.url(:style_name)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: true)
end
it "passes the option escape: false if :escape_url is false and :escape is not passed" do
mock_url_generator_builder = MockUrlGeneratorBuilder.new
attachment = Paperclip::Attachment.new(:name,
FakeModel.new,
url_generator: mock_url_generator_builder,
escape_url: false)
attachment.url(:style_name)
assert mock_url_generator_builder.has_generated_url_with_options?(escape: false)
end
it "returns the path based on the url by default" do
@attachment = attachment url: "/:class/:id/:basename"
@model = @attachment.instance
@model.id = 1234
@model.avatar_file_name = "fake.jpg"
assert_equal "#{Rails.root}/public/fake_models/1234/fake", @attachment.path
end
it "defaults to a path that scales" do
avatar_attachment = attachment
model = avatar_attachment.instance
model.id = 1234
model.avatar_file_name = "fake.jpg"
expected_path = "#{Rails.root}/public/system/fake_models/avatars/000/001/234/original/fake.jpg"
assert_equal expected_path, avatar_attachment.path
end
it "renders JSON as the URL to the attachment" do
avatar_attachment = attachment
model = avatar_attachment.instance
model.id = 1234
model.avatar_file_name = "fake.jpg"
assert_equal attachment.url, attachment.as_json
end
it "renders JSON from the model when requested by :methods" do
rebuild_model
dummy = Dummy.new
dummy.id = 1234
dummy.avatar_file_name = "fake.jpg"
dummy.stubs(:new_record?).returns(false)
expected_string = '{"avatar":"/system/dummies/avatars/000/001/234/original/fake.jpg"}'
# active_model pre-3.2 checks only by calling any? on it, thus it doesn't work if it is empty
assert_equal expected_string, dummy.to_json(only: [:dummy_key_for_old_active_model], methods: [:avatar])
end
context "Attachment default_options" do
before do
rebuild_model
@old_default_options = Paperclip::Attachment.default_options.dup
@new_default_options = @old_default_options.merge({
path: "argle/bargle",
url: "fooferon",
default_url: "not here.png"
})
end
after do
Paperclip::Attachment.default_options.merge! @old_default_options
end
it "is overrideable" do
Paperclip::Attachment.default_options.merge!(@new_default_options)
@new_default_options.keys.each do |key|
assert_equal @new_default_options[key],
Paperclip::Attachment.default_options[key]
end
end
context "without an Attachment" do
before do
rebuild_model default_url: "default.url"
@dummy = Dummy.new
end
it "returns false when asked exists?" do
assert !@dummy.avatar.exists?
end
it "#url returns the default_url" do
expect(@dummy.avatar.url).to eq "default.url"
end
end
context "on an Attachment" do
before do
@dummy = Dummy.new
@attachment = @dummy.avatar
end
Paperclip::Attachment.default_options.keys.each do |key|
it "is the default_options for #{key}" do
assert_equal @old_default_options[key],
@attachment.instance_variable_get("@options")[key],
key.to_s
end
end
context "when redefined" do
before do
Paperclip::Attachment.default_options.merge!(@new_default_options)
@dummy = Dummy.new
@attachment = @dummy.avatar
end
Paperclip::Attachment.default_options.keys.each do |key|
it "is the new default_options for #{key}" do
assert_equal @new_default_options[key],
@attachment.instance_variable_get("@options")[key],
key.to_s
end
end
end
end
end
context "An attachment with similarly named interpolations" do
before do
rebuild_model path: ":id.omg/:id-bbq/:idwhat/:id_partition.wtf"
@dummy = Dummy.new
@dummy.stubs(:id).returns(1024)
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
end
after { @file.close }
it "makes sure that they are interpolated correctly" do
assert_equal "1024.omg/1024-bbq/1024what/000/001/024.wtf", @dummy.avatar.path
end
end
context "An attachment with :timestamp interpolations" do
before do
@file = StringIO.new("...")
@zone = 'UTC'
Time.stubs(:zone).returns(@zone)
@zone_default = 'Eastern Time (US & Canada)'
Time.stubs(:zone_default).returns(@zone_default)
end
context "using default time zone" do
before do
rebuild_model path: ":timestamp", use_default_time_zone: true
@dummy = Dummy.new
@dummy.avatar = @file
end
it "returns a time in the default zone" do
assert_equal @dummy.avatar_updated_at.in_time_zone(@zone_default).to_s, @dummy.avatar.path
end
end
context "using per-thread time zone" do
before do
rebuild_model path: ":timestamp", use_default_time_zone: false
@dummy = Dummy.new
@dummy.avatar = @file
end
it "returns a time in the per-thread zone" do
assert_equal @dummy.avatar_updated_at.in_time_zone(@zone).to_s, @dummy.avatar.path
end
end
end
context "An attachment with :hash interpolations" do
before do
@file = File.open(fixture_file("5k.png"))
end
after do
@file.close
end
it "raises if no secret is provided" do
rebuild_model path: ":hash"
@attachment = Dummy.new.avatar
@attachment.assign @file
assert_raises ArgumentError do
@attachment.path
end
end
context "when secret is set" do
before do
rebuild_model path: ":hash",
hash_secret: "w00t",
hash_data: ":class/:attachment/:style/:filename"
@attachment = Dummy.new.avatar
@attachment.assign @file
end
it "results in the correct interpolation" do
assert_equal "dummies/avatars/original/5k.png",
@attachment.send(:interpolate, @attachment.options[:hash_data])
assert_equal "dummies/avatars/thumb/5k.png",
@attachment.send(:interpolate, @attachment.options[:hash_data], :thumb)
end
it "results in a correct hash" do
assert_equal "0a59e9142bba11576de1d353d8747b1acad5ad34", @attachment.path
assert_equal "b39a062c1e62e85a6c785ed00cf3bebf5f850e2b", @attachment.path(:thumb)
end
end
end
context "An attachment with a :rails_env interpolation" do
before do
@rails_env = "blah"
@id = 1024
rebuild_model path: ":rails_env/:id.png"
@dummy = Dummy.new
@dummy.stubs(:id).returns(@id)
@file = StringIO.new(".")
@dummy.avatar = @file
Rails.stubs(:env).returns(@rails_env)
end
it "returns the proper path" do
assert_equal "#{@rails_env}/#{@id}.png", @dummy.avatar.path
end
end
context "An attachment with a default style and an extension interpolation" do
before do
rebuild_model path: ":basename.:extension",
styles: { default: ["100x100", :jpg] },
default_style: :default
@attachment = Dummy.new.avatar
@file = File.open(fixture_file("5k.png"))
@file.stubs(:original_filename).returns("file.png")
end
it "returns the right extension for the path" do
@attachment.assign(@file)
assert_equal "file.jpg", @attachment.path
end
end
context "An attachment with :convert_options" do
before do
rebuild_model styles: {
thumb: "100x100",
large: "400x400"
},
convert_options: {
all: "-do_stuff",
thumb: "-thumbnailize"
}
@dummy = Dummy.new
@dummy.avatar
end
it "reports the correct options when sent #extra_options_for(:thumb)" do
assert_equal "-thumbnailize -do_stuff", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
end
it "reports the correct options when sent #extra_options_for(:large)" do
assert_equal "-do_stuff", @dummy.avatar.send(:extra_options_for, :large)
end
end
context "An attachment with :source_file_options" do
before do
rebuild_model styles: {
thumb: "100x100",
large: "400x400"
},
source_file_options: {
all: "-density 400",
thumb: "-depth 8"
}
@dummy = Dummy.new
@dummy.avatar
end
it "reports the correct options when sent #extra_source_file_options_for(:thumb)" do
assert_equal "-depth 8 -density 400", @dummy.avatar.send(:extra_source_file_options_for, :thumb), @dummy.avatar.source_file_options.inspect
end
it "reports the correct options when sent #extra_source_file_options_for(:large)" do
assert_equal "-density 400", @dummy.avatar.send(:extra_source_file_options_for, :large)
end
end
context "An attachment with :only_process" do
before do
rebuild_model styles: {
thumb: "100x100",
large: "400x400"
},
only_process: [:thumb]
@file = StringIO.new("...")
@attachment = Dummy.new.avatar
end
it "only processes the provided style" do
@attachment.expects(:post_process).with(:thumb)
@attachment.expects(:post_process).with(:large).never
@attachment.assign(@file)
end
end
context "An attachment with :only_process that is a proc" do
before do
rebuild_model styles: {
thumb: "100x100",
large: "400x400"
},
only_process: lambda { |attachment| [:thumb] }
@file = StringIO.new("...")
@attachment = Dummy.new.avatar
end
it "only processes the provided style" do
@attachment.expects(:post_process).with(:thumb)
@attachment.expects(:post_process).with(:large).never
@attachment.assign(@file)
@attachment.save
end
end
context "An attachment with :convert_options that is a proc" do
before do
rebuild_model styles: {
thumb: "100x100",
large: "400x400"
},
convert_options: {
all: lambda{|i| i.all },
thumb: lambda{|i| i.thumb }
}
Dummy.class_eval do
def all; "-all"; end
def thumb; "-thumb"; end
end
@dummy = Dummy.new
@dummy.avatar
end
it "reports the correct options when sent #extra_options_for(:thumb)" do
assert_equal "-thumb -all", @dummy.avatar.send(:extra_options_for, :thumb), @dummy.avatar.convert_options.inspect
end
it "reports the correct options when sent #extra_options_for(:large)" do
assert_equal "-all", @dummy.avatar.send(:extra_options_for, :large)
end
end
context "An attachment with :path that is a proc" do
before do
rebuild_model path: lambda{ |attachment| "path/#{attachment.instance.other}.:extension" }
@file = File.new(fixture_file("5k.png"), 'rb')
@dummyA = Dummy.new(other: 'a')
@dummyA.avatar = @file
@dummyB = Dummy.new(other: 'b')
@dummyB.avatar = @file
end
after { @file.close }
it "returns correct path" do
assert_equal "path/a.png", @dummyA.avatar.path
assert_equal "path/b.png", @dummyB.avatar.path
end
end
context "An attachment with :styles that is a proc" do
before do
rebuild_model styles: lambda{ |attachment| {thumb: "50x50#", large: "400x400"} }
@attachment = Dummy.new.avatar
end
it "has the correct geometry" do
assert_equal "50x50#", @attachment.styles[:thumb][:geometry]
end
end
context "An attachment with conditional :styles that is a proc" do
before do
rebuild_model styles: lambda{ |attachment| attachment.instance.other == 'a' ? {thumb: "50x50#"} : {large: "400x400"} }
@dummy = Dummy.new(other: 'a')
end
it "has the correct styles for the assigned instance values" do
assert_equal "50x50#", @dummy.avatar.styles[:thumb][:geometry]
assert_nil @dummy.avatar.styles[:large]
@dummy.other = 'b'
assert_equal "400x400", @dummy.avatar.styles[:large][:geometry]
assert_nil @dummy.avatar.styles[:thumb]
end
end
geometry_specs = [
[ lambda{|z| "50x50#" }, :png ],
lambda{|z| "50x50#" },
{ geometry: lambda{|z| "50x50#" } }
]
geometry_specs.each do |geometry_spec|
context "An attachment geometry like #{geometry_spec}" do
before do
rebuild_model styles: { normal: geometry_spec }
@attachment = Dummy.new.avatar
end
context "when assigned" do
before do
@file = StringIO.new(".")
@attachment.assign(@file)
end
it "has the correct geometry" do
assert_equal "50x50#", @attachment.styles[:normal][:geometry]
end
end
end
end
context "An attachment with both 'normal' and hash-style styles" do
before do
rebuild_model styles: {
normal: ["50x50#", :png],
hash: { geometry: "50x50#", format: :png }
}
@dummy = Dummy.new
@attachment = @dummy.avatar
end
[:processors, :whiny, :convert_options, :geometry, :format].each do |field|
it "has the same #{field} field" do
assert_equal @attachment.styles[:normal][field], @attachment.styles[:hash][field]
end
end
end
context "An attachment with :processors that is a proc" do
before do
class Paperclip::Test < Paperclip::Processor; end
@file = StringIO.new("...")
Paperclip::Test.stubs(:make).returns(@file)
rebuild_model styles: { normal: '' }, processors: lambda { |a| [ :test ] }
@attachment = Dummy.new.avatar
end
context "when assigned" do
before do
@attachment.assign(StringIO.new("."))
end
it "has the correct processors" do
assert_equal [ :test ], @attachment.styles[:normal][:processors]
end
end
end
context "An attachment with erroring processor" do
before do
rebuild_model processor: [:thumbnail], styles: { small: '' }, whiny_thumbnails: true
@dummy = Dummy.new
@file = StringIO.new("...")
@file.stubs(:to_tempfile).returns(@file)
end
context "when error is meaningful for the end user" do
before do
Paperclip::Thumbnail.expects(:make).raises(
Paperclip::Errors::NotIdentifiedByImageMagickError,
"cannot be processed."
)
end
it "correctly forwards processing error message to the instance" do
@dummy.avatar = @file
@dummy.valid?
assert_contains(
@dummy.errors.full_messages,
"Avatar cannot be processed."
)
end
end
context "when error is intended for the developer" do
before do
Paperclip::Thumbnail.expects(:make).raises(
Paperclip::Errors::CommandNotFoundError
)
end
it "propagates the error" do
assert_raises(Paperclip::Errors::CommandNotFoundError) do
@dummy.avatar = @file
end
end
end
end
context "An attachment with multiple processors" do
before do
class Paperclip::Test < Paperclip::Processor; end
@style_params = { once: {one: 1, two: 2} }
rebuild_model processors: [:thumbnail, :test], styles: @style_params
@dummy = Dummy.new
@file = StringIO.new("...")
@file.stubs(:close)
Paperclip::Test.stubs(:make).returns(@file)
Paperclip::Thumbnail.stubs(:make).returns(@file)
end
context "when assigned" do
it "calls #make on all specified processors" do
@dummy.avatar = @file
expect(Paperclip::Thumbnail).to have_received(:make)
expect(Paperclip::Test).to have_received(:make)
end
it "calls #make with the right parameters passed as second argument" do
expected_params = @style_params[:once].merge({
style: :once,
processors: [:thumbnail, :test],
whiny: true,
convert_options: "",
source_file_options: ""
})
@dummy.avatar = @file
expect(Paperclip::Thumbnail).to have_received(:make).with(anything, expected_params, anything)
end
it "calls #make with attachment passed as third argument" do
@dummy.avatar = @file
expect(Paperclip::Test).to have_received(:make).with(anything, anything, @dummy.avatar)
end
it "calls #make and unlinks intermediary files afterward" do
@dummy.avatar.expects(:unlink_files).with([@file, @file])
@dummy.avatar = @file
end
end
end
context "An attachment with a processor that returns original file" do
before do
class Paperclip::Test < Paperclip::Processor
def make; @file; end
end
rebuild_model processors: [:test], styles: { once: "100x100" }
@file = StringIO.new("...")
@file.stubs(:close)
@dummy = Dummy.new
end
context "when assigned" do
it "#calls #make and doesn't unlink the original file" do
@dummy.avatar.expects(:unlink_files).with([])
@dummy.avatar = @file
end
end
end
it "includes the filesystem module when loading the filesystem storage" do
rebuild_model storage: :filesystem
@dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
end
it "includes the filesystem module even if capitalization is wrong" do
rebuild_model storage: :FileSystem
@dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
rebuild_model storage: :Filesystem
@dummy = Dummy.new
assert @dummy.avatar.is_a?(Paperclip::Storage::Filesystem)
end
it "converts underscored storage name to camelcase" do
rebuild_model storage: :not_here
@dummy = Dummy.new
exception = assert_raises(Paperclip::Errors::StorageMethodNotFound, /NotHere/) do
@dummy.avatar
end
end
it "raises an error if you try to include a storage module that doesn't exist" do
rebuild_model storage: :not_here
@dummy = Dummy.new
assert_raises(Paperclip::Errors::StorageMethodNotFound) do
@dummy.avatar
end
end
context "An attachment with styles but no processors defined" do
before do
rebuild_model processors: [], styles: {something: '1'}
@dummy = Dummy.new
@file = StringIO.new("...")
end
it "raises when assigned to" do
assert_raises(RuntimeError){ @dummy.avatar = @file }
end
end
context "An attachment without styles and with no processors defined" do
before do
rebuild_model processors: [], styles: {}
@dummy = Dummy.new
@file = StringIO.new("...")
end
it "does not raise when assigned to" do
@dummy.avatar = @file
end
end
context "Assigning an attachment with post_process hooks" do
before do
rebuild_class styles: { something: "100x100#" }
Dummy.class_eval do
before_avatar_post_process :do_before_avatar
after_avatar_post_process :do_after_avatar
before_post_process :do_before_all
after_post_process :do_after_all
def do_before_avatar; end
def do_after_avatar; end
def do_before_all; end
def do_after_all; end
end
@file = StringIO.new(".")
@file.stubs(:to_tempfile).returns(@file)
@dummy = Dummy.new
Paperclip::Thumbnail.stubs(:make).returns(@file)
@attachment = @dummy.avatar
end
it "calls the defined callbacks when assigned" do
@dummy.expects(:do_before_avatar).with()
@dummy.expects(:do_after_avatar).with()
@dummy.expects(:do_before_all).with()
@dummy.expects(:do_after_all).with()
Paperclip::Thumbnail.expects(:make).returns(@file)
@dummy.avatar = @file
end
it "does not cancel the processing if a before_post_process returns nil" do
@dummy.expects(:do_before_avatar).with().returns(nil)
@dummy.expects(:do_after_avatar).with()
@dummy.expects(:do_before_all).with().returns(nil)
@dummy.expects(:do_after_all).with()
Paperclip::Thumbnail.expects(:make).returns(@file)
@dummy.avatar = @file
end
it "cancels the processing if a before_post_process returns false" do
@dummy.expects(:do_before_avatar).never
@dummy.expects(:do_after_avatar).never
@dummy.expects(:do_before_all).with().returns(false)
@dummy.expects(:do_after_all)
Paperclip::Thumbnail.expects(:make).never
@dummy.avatar = @file
end
it "cancels the processing if a before_avatar_post_process returns false" do
@dummy.expects(:do_before_avatar).with().returns(false)
@dummy.expects(:do_after_avatar)
@dummy.expects(:do_before_all).with().returns(true)
@dummy.expects(:do_after_all)
Paperclip::Thumbnail.expects(:make).never
@dummy.avatar = @file
end
end
context "Assigning an attachment" do
before do
rebuild_model styles: { something: "100x100#" }
@file = File.new(fixture_file("5k.png"), "rb")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "strips whitespace from original_filename field" do
assert_equal "5k.png", @dummy.avatar.original_filename
end
it "strips whitespace from content_type field" do
assert_equal "image/png", @dummy.avatar.instance.avatar_content_type
end
end
context "Assigning an attachment" do
before do
rebuild_model styles: { something: "100x100#" }
@file = File.new(fixture_file("5k.png"), "rb")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "makes sure the content_type is a string" do
assert_equal "image/png", @dummy.avatar.instance.avatar_content_type
end
end
context "Attachment with strange letters" do
before do
rebuild_model
@file = File.new(fixture_file("5k.png"), "rb")
@file.stubs(:original_filename).returns("sheep_say_bæ.png")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "does not remove strange letters" do
assert_equal "sheep_say_bæ.png", @dummy.avatar.original_filename
end
end
context "Attachment with reserved filename" do
before do
rebuild_model
@file = Tempfile.new(["filename","png"])
end
after do
@file.unlink
end
context "with default configuration" do
"&$+,/:;=?@<>[]{}|\^~%# ".split(//).each do |character|
context "with character #{character}" do
context "at beginning of filename" do
before do
@file.stubs(:original_filename).returns("#{character}filename.png")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "converts special character into underscore" do
assert_equal "_filename.png", @dummy.avatar.original_filename
end
end
context "at end of filename" do
before do
@file.stubs(:original_filename).returns("filename.png#{character}")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "converts special character into underscore" do
assert_equal "filename.png_", @dummy.avatar.original_filename
end
end
context "in the middle of filename" do
before do
@file.stubs(:original_filename).returns("file#{character}name.png")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "converts special character into underscore" do
assert_equal "file_name.png", @dummy.avatar.original_filename
end
end
end
end
end
context "with specified regexp replacement" do
before do
@old_defaults = Paperclip::Attachment.default_options.dup
end
after do
Paperclip::Attachment.default_options.merge! @old_defaults
end
context 'as another regexp' do
before do
Paperclip::Attachment.default_options.merge! restricted_characters: /o/
@file.stubs(:original_filename).returns("goood.png")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "matches and converts that character" do
assert_equal "g___d.png", @dummy.avatar.original_filename
end
end
context 'as nil' do
before do
Paperclip::Attachment.default_options.merge! restricted_characters: nil
@file.stubs(:original_filename).returns("goood.png")
@dummy = Dummy.new
@dummy.avatar = @file
end
it "ignores and returns the original file name" do
assert_equal "goood.png", @dummy.avatar.original_filename
end
end
end
context 'with specified cleaner' do
before do
@old_defaults = Paperclip::Attachment.default_options.dup
end
after do
Paperclip::Attachment.default_options.merge! @old_defaults
end
it 'calls the given proc and take the result as cleaned filename' do
Paperclip::Attachment.default_options[:filename_cleaner] = lambda do |str|
"from_proc_#{str}"
end
@file.stubs(:original_filename).returns("goood.png")
@dummy = Dummy.new
@dummy.avatar = @file
assert_equal "from_proc_goood.png", @dummy.avatar.original_filename
end
it 'calls the given object and take the result as the cleaned filename' do
class MyCleaner
def call(filename)
"foo"
end
end
Paperclip::Attachment.default_options[:filename_cleaner] = MyCleaner.new
@file.stubs(:original_filename).returns("goood.png")
@dummy = Dummy.new
@dummy.avatar = @file
assert_equal "foo", @dummy.avatar.original_filename
end
end
end
context "Attachment with uppercase extension and a default style" do
before do
@old_defaults = Paperclip::Attachment.default_options.dup
Paperclip::Attachment.default_options.merge!({
path: ":rails_root/:attachment/:class/:style/:id/:basename.:extension"
})
FileUtils.rm_rf("tmp")
rebuild_model styles: { large: ["400x400", :jpg],
medium: ["100x100", :jpg],
small: ["32x32#", :jpg]},
default_style: :small
@instance = Dummy.new
@instance.stubs(:id).returns 123
@file = File.new(fixture_file("uppercase.PNG"), 'rb')
@attachment = @instance.avatar
now = Time.now
Time.stubs(:now).returns(now)
@attachment.assign(@file)
@attachment.save
end
after do
@file.close
Paperclip::Attachment.default_options.merge!(@old_defaults)
end
it "has matching to_s and url methods" do
assert_equal @attachment.to_s, @attachment.url
assert_equal @attachment.to_s(:small), @attachment.url(:small)
end
it "has matching expiring_url and url methods when using the filesystem storage" do
assert_equal @attachment.expiring_url, @attachment.url
end
end
context "An attachment" do
before do
@old_defaults = Paperclip::Attachment.default_options.dup
Paperclip::Attachment.default_options.merge!({
path: ":rails_root/:attachment/:class/:style/:id/:basename.:extension"
})
FileUtils.rm_rf("tmp")
rebuild_model
@instance = Dummy.new
@instance.stubs(:id).returns 123
# @attachment = Paperclip::Attachment.new(:avatar, @instance)
@attachment = @instance.avatar
@file = File.new(fixture_file("5k.png"), 'rb')
end
after do
@file.close
Paperclip::Attachment.default_options.merge!(@old_defaults)
end
it "raises if there are not the correct columns when you try to assign" do
@other_attachment = Paperclip::Attachment.new(:not_here, @instance)
assert_raises(Paperclip::Error) do
@other_attachment.assign(@file)
end
end
it 'clears out the previous assignment when assigned nil' do
@attachment.assign(@file)
@attachment.queued_for_write[:original]
@attachment.assign(nil)
assert_nil @attachment.queued_for_write[:original]
end
it 'does not do anything when it is assigned an empty string' do
@attachment.assign(@file)
original_file = @attachment.queued_for_write[:original]
@attachment.assign("")
assert_equal original_file, @attachment.queued_for_write[:original]
end
it "returns nil as path when no file assigned" do
assert_equal nil, @attachment.path
assert_equal nil, @attachment.path(:blah)
end
context "with a file assigned but not saved yet" do
it "clears out any attached files" do
@attachment.assign(@file)
assert @attachment.queued_for_write.present?
@attachment.clear
assert @attachment.queued_for_write.blank?
end
end
context "with a file assigned in the database" do
before do
@attachment.stubs(:instance_read).with(:file_name).returns("5k.png")
@attachment.stubs(:instance_read).with(:content_type).returns("image/png")
@attachment.stubs(:instance_read).with(:file_size).returns(12345)
dtnow = DateTime.now
@now = Time.now
Time.stubs(:now).returns(@now)
@attachment.stubs(:instance_read).with(:updated_at).returns(dtnow)
end
it "returns the proper path when filename has a single .'s" do
assert_equal File.expand_path("tmp/avatars/dummies/original/#{@instance.id}/5k.png"), File.expand_path(@attachment.path)
end
it "returns the proper path when filename has multiple .'s" do
@attachment.stubs(:instance_read).with(:file_name).returns("5k.old.png")
assert_equal File.expand_path("tmp/avatars/dummies/original/#{@instance.id}/5k.old.png"), File.expand_path(@attachment.path)
end
context "when expecting three styles" do
before do
rebuild_class styles: {
large: ["400x400", :png],
medium: ["100x100", :gif],
small: ["32x32#", :jpg]
}
@instance = Dummy.new
@instance.stubs(:id).returns 123
@file = File.new(fixture_file("5k.png"), 'rb')
@attachment = @instance.avatar
end
context "and assigned a file" do
before do
now = Time.now
Time.stubs(:now).returns(now)
@attachment.assign(@file)
end
it "is dirty" do
assert @attachment.dirty?
end
context "and saved" do
before do
@attachment.save
end
it "commits the files to disk" do
[:large, :medium, :small].each do |style|
expect(@attachment.path(style)).to exist
end
end
it "saves the files as the right formats and sizes" do
[[:large, 400, 61, "PNG"],
[:medium, 100, 15, "GIF"],
[:small, 32, 32, "JPEG"]].each do |style|
cmd = %Q[identify -format "%w %h %b %m" "#{@attachment.path(style.first)}"]
out = `#{cmd}`
width, height, _size, format = out.split(" ")
assert_equal style[1].to_s, width.to_s
assert_equal style[2].to_s, height.to_s
assert_equal style[3].to_s, format.to_s
end
end
context "and trying to delete" do
before do
@existing_names = @attachment.styles.keys.collect do |style|
@attachment.path(style)
end
end
it "deletes the files after assigning nil" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.assign nil
@attachment.save
@existing_names.each{|f| assert_file_not_exists(f) }
end
it "deletes the files when you call #clear and #save" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.clear
@attachment.save
@existing_names.each{|f| assert_file_not_exists(f) }
end
it "deletes the files when you call #delete" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.destroy
@existing_names.each{|f| assert_file_not_exists(f) }
end
context "when keeping old files" do
before do
@attachment.options[:keep_old_files] = true
end
it "keeps the files after assigning nil" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.assign nil
@attachment.save
@existing_names.each{|f| assert_file_exists(f) }
end
it "keeps the files when you call #clear and #save" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.clear
@attachment.save
@existing_names.each{|f| assert_file_exists(f) }
end
it "keeps the files when you call #delete" do
@attachment.expects(:instance_write).with(:file_name, nil)
@attachment.expects(:instance_write).with(:content_type, nil)
@attachment.expects(:instance_write).with(:file_size, nil)
@attachment.expects(:instance_write).with(:fingerprint, nil)
@attachment.expects(:instance_write).with(:updated_at, nil)
@attachment.destroy
@existing_names.each{|f| assert_file_exists(f) }
end
end
end
end
end
end
end
context "when trying a nonexistant storage type" do
before do
rebuild_model storage: :not_here
end
it "is not able to find the module" do
assert_raises(Paperclip::Errors::StorageMethodNotFound){ Dummy.new.avatar }
end
end
end
context "An attachment with only a avatar_file_name column" do
before do
ActiveRecord::Base.connection.create_table :dummies, force: true do |table|
table.column :avatar_file_name, :string
end
rebuild_class
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
end
after { @file.close }
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
it "does not return the time when sent #avatar_updated_at" do
@dummy.avatar = @file
assert_nil @dummy.avatar.updated_at
end
it "returns the right value when sent #avatar_file_size" do
@dummy.avatar = @file
assert_equal File.size(@file), @dummy.avatar.size
end
context "and avatar_created_at column" do
before do
ActiveRecord::Base.connection.add_column :dummies, :avatar_created_at, :timestamp
rebuild_class
@dummy = Dummy.new
end
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
it "returns the creation time when sent #avatar_created_at" do
now = Time.now
Time.stubs(:now).returns(now)
@dummy.avatar = @file
assert_equal now.to_i, @dummy.avatar.created_at
end
it "returns the creation time when sent #avatar_created_at and the entry has been updated" do
creation = 2.hours.ago
now = Time.now
Time.stubs(:now).returns(creation)
@dummy.avatar = @file
Time.stubs(:now).returns(now)
@dummy.avatar = @file
assert_equal creation.to_i, @dummy.avatar.created_at
assert_not_equal now.to_i, @dummy.avatar.created_at
end
it "sets changed? to true on attachment assignment" do
@dummy.avatar = @file
@dummy.save!
@dummy.avatar = @file
assert @dummy.changed?
end
end
context "and avatar_updated_at column" do
before do
ActiveRecord::Base.connection.add_column :dummies, :avatar_updated_at, :timestamp
rebuild_class
@dummy = Dummy.new
end
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
it "returns the right value when sent #avatar_updated_at" do
now = Time.now
Time.stubs(:now).returns(now)
@dummy.avatar = @file
assert_equal now.to_i, @dummy.avatar.updated_at
end
end
it "does not calculate fingerprint" do
Digest::MD5.stubs(:file)
@dummy.avatar = @file
expect(Digest::MD5).to have_received(:file).never
end
it "does not assign fingerprint" do
@dummy.avatar = @file
assert_nil @dummy.avatar.fingerprint
end
context "and avatar_content_type column" do
before do
ActiveRecord::Base.connection.add_column :dummies, :avatar_content_type, :string
rebuild_class
@dummy = Dummy.new
end
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
it "returns the right value when sent #avatar_content_type" do
@dummy.avatar = @file
assert_equal "image/png", @dummy.avatar.content_type
end
end
context "and avatar_file_size column" do
before do
ActiveRecord::Base.connection.add_column :dummies, :avatar_file_size, :bigint
rebuild_class
@dummy = Dummy.new
end
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
it "returns the right value when sent #avatar_file_size" do
@dummy.avatar = @file
assert_equal File.size(@file), @dummy.avatar.size
end
it "returns the right value when saved, reloaded, and sent #avatar_file_size" do
@dummy.avatar = @file
@dummy.save
@dummy = Dummy.find(@dummy.id)
assert_equal File.size(@file), @dummy.avatar.size
end
end
context "and avatar_fingerprint column" do
before do
ActiveRecord::Base.connection.add_column :dummies, :avatar_fingerprint, :string
rebuild_class
@dummy = Dummy.new
end
it "does not error when assigned an attachment" do
assert_nothing_raised { @dummy.avatar = @file }
end
context "with explicitly set digest" do
before do
rebuild_class adapter_options: { hash_digest: Digest::SHA256 }
@dummy = Dummy.new
end
it "returns the right value when sent #avatar_fingerprint" do
@dummy.avatar = @file
assert_equal "734016d801a497f5579cdd4ef2ae1d020088c1db754dc434482d76dd5486520a",
@dummy.avatar_fingerprint
end
it "returns the right value when saved, reloaded, and sent #avatar_fingerprint" do
@dummy.avatar = @file
@dummy.save
@dummy = Dummy.find(@dummy.id)
assert_equal "734016d801a497f5579cdd4ef2ae1d020088c1db754dc434482d76dd5486520a",
@dummy.avatar_fingerprint
end
end
context "with the default digest" do
before do
rebuild_class # MD5 is the default
@dummy = Dummy.new
end
it "returns the right value when sent #avatar_fingerprint" do
@dummy.avatar = @file
assert_equal "aec488126c3b33c08a10c3fa303acf27",
@dummy.avatar_fingerprint
end
it "returns the right value when saved, reloaded, and sent #avatar_fingerprint" do
@dummy.avatar = @file
@dummy.save
@dummy = Dummy.find(@dummy.id)
assert_equal "aec488126c3b33c08a10c3fa303acf27",
@dummy.avatar_fingerprint
end
end
end
end
context "an attachment with delete_file option set to false" do
before do
rebuild_model preserve_files: true
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
@dummy.save!
@attachment = @dummy.avatar
@path = @attachment.path
end
after { @file.close }
it "does not delete the files from storage when attachment is destroyed" do
@attachment.destroy
assert_file_exists(@path)
end
it "clears out attachment data when attachment is destroyed" do
@attachment.destroy
assert !@attachment.exists?
assert_nil @dummy.avatar_file_name
end
it "does not delete the file when model is destroyed" do
@dummy.destroy
assert_file_exists(@path)
end
end
context "An attached file" do
before do
rebuild_model
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
@dummy.save!
@attachment = @dummy.avatar
@path = @attachment.path
end
after { @file.close }
it "is not deleted when the model fails to destroy" do
@dummy.stubs(:destroy).raises(Exception)
assert_raises Exception do
@dummy.destroy
end
assert_file_exists(@path)
end
it "is deleted when the model is destroyed" do
@dummy.destroy
assert_file_not_exists(@path)
end
it "is not deleted when transaction rollbacks after model is destroyed" do
ActiveRecord::Base.transaction do
@dummy.destroy
raise ActiveRecord::Rollback
end
assert_file_exists(@path)
end
end
end
================================================
FILE: spec/paperclip/content_type_detector_spec.rb
================================================
require 'spec_helper'
describe Paperclip::ContentTypeDetector do
it 'returns a meaningful content type for open xml spreadsheets' do
file = File.new(fixture_file("empty.xlsx"))
assert_equal "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Paperclip::ContentTypeDetector.new(file.path).detect
end
it 'gives a sensible default when the name is empty' do
assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new("").detect
end
it 'returns the empty content type when the file is empty' do
tempfile = Tempfile.new("empty")
assert_equal "inode/x-empty", Paperclip::ContentTypeDetector.new(tempfile.path).detect
tempfile.close
end
it 'returns content type of file if it is an acceptable type' do
MIME::Types.stubs(:type_for).returns([MIME::Type.new('application/mp4'), MIME::Type.new('video/mp4'), MIME::Type.new('audio/mp4')])
Paperclip::ContentTypeDetector.any_instance
.stubs(:type_from_file_contents).returns("video/mp4")
@filename = "my_file.mp4"
assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect
end
it 'finds the right type in the list via the file command' do
@filename = "#{Dir.tmpdir}/something.hahalolnotreal"
File.open(@filename, "w+") do |file|
file.puts "This is a text file."
file.rewind
assert_equal "text/plain", Paperclip::ContentTypeDetector.new(file.path).detect
end
FileUtils.rm @filename
end
it 'returns a sensible default if something is wrong, like the file is gone' do
@filename = "/path/to/nothing"
assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new(@filename).detect
end
it 'returns a sensible default when the file command is missing' do
Paperclip.stubs(:run).raises(Terrapin::CommandLineError.new)
@filename = "/path/to/something"
assert_equal "application/octet-stream", Paperclip::ContentTypeDetector.new(@filename).detect
end
end
================================================
FILE: spec/paperclip/file_command_content_type_detector_spec.rb
================================================
require 'spec_helper'
describe Paperclip::FileCommandContentTypeDetector do
it 'returns a content type based on the content of the file' do
tempfile = Tempfile.new("something")
tempfile.write("This is a file.")
tempfile.rewind
assert_equal "text/plain", Paperclip::FileCommandContentTypeDetector.new(tempfile.path).detect
tempfile.close
end
it 'returns a sensible default when the file command is missing' do
Paperclip.stubs(:run).raises(Terrapin::CommandLineError.new)
@filename = "/path/to/something"
assert_equal "application/octet-stream",
Paperclip::FileCommandContentTypeDetector.new(@filename).detect
end
it 'returns a sensible default on the odd chance that run returns nil' do
Paperclip.stubs(:run).returns(nil)
assert_equal "application/octet-stream",
Paperclip::FileCommandContentTypeDetector.new("windows").detect
end
context "#type_from_file_command" do
let(:detector) { Paperclip::FileCommandContentTypeDetector.new("html") }
it "does work with the output of old versions of file" do
Paperclip.stubs(:run).returns("text/html charset=us-ascii")
expect(detector.detect).to eq("text/html")
end
it "does work with the output of new versions of file" do
Paperclip.stubs(:run).returns("text/html; charset=us-ascii")
expect(detector.detect).to eq("text/html")
end
end
end
================================================
FILE: spec/paperclip/filename_cleaner_spec.rb
================================================
require 'spec_helper'
describe Paperclip::FilenameCleaner do
it 'converts invalid characters to underscores' do
cleaner = Paperclip::FilenameCleaner.new(/[aeiou]/)
expect(cleaner.call("baseball")).to eq "b_s_b_ll"
end
it 'does not convert anything if the character regex is nil' do
cleaner = Paperclip::FilenameCleaner.new(nil)
expect(cleaner.call("baseball")).to eq "baseball"
end
end
================================================
FILE: spec/paperclip/geometry_detector_spec.rb
================================================
require 'spec_helper'
describe Paperclip::GeometryDetector do
it 'identifies an image and extract its dimensions' do
Paperclip::GeometryParser.stubs(:new).with("434x66,").returns(stub(make: :correct))
file = fixture_file("5k.png")
factory = Paperclip::GeometryDetector.new(file)
output = factory.make
expect(output).to eq :correct
end
it 'identifies an image and extract its dimensions and orientation' do
Paperclip::GeometryParser.stubs(:new).with("300x200,6").returns(stub(make: :correct))
file = fixture_file("rotated.jpg")
factory = Paperclip::GeometryDetector.new(file)
output = factory.make
expect(output).to eq :correct
end
it 'avoids reading EXIF orientation if so configured' do
begin
Paperclip.options[:use_exif_orientation] = false
Paperclip::GeometryParser.stubs(:new).with("300x200,1").returns(stub(make: :correct))
file = fixture_file("rotated.jpg")
factory = Paperclip::GeometryDetector.new(file)
output = factory.make
expect(output).to eq :correct
ensure
Paperclip.options[:use_exif_orientation] = true
end
end
end
================================================
FILE: spec/paperclip/geometry_parser_spec.rb
================================================
require 'spec_helper'
describe Paperclip::GeometryParser do
it 'identifies an image and extract its dimensions with no orientation' do
Paperclip::Geometry.stubs(:new).with(
height: '73',
width: '434',
modifier: nil,
orientation: nil
).returns(:correct)
factory = Paperclip::GeometryParser.new("434x73")
output = factory.make
assert_equal :correct, output
end
it 'identifies an image and extract its dimensions with an empty orientation' do
Paperclip::Geometry.stubs(:new).with(
height: '73',
width: '434',
modifier: nil,
orientation: ''
).returns(:correct)
factory = Paperclip::GeometryParser.new("434x73,")
output = factory.make
assert_equal :correct, output
end
it 'identifies an image and extract its dimensions and orientation' do
Paperclip::Geometry.stubs(:new).with(
height: '200',
width: '300',
modifier: nil,
orientation: '6'
).returns(:correct)
factory = Paperclip::GeometryParser.new("300x200,6")
output = factory.make
assert_equal :correct, output
end
it 'identifies an image and extract its dimensions and modifier' do
Paperclip::Geometry.stubs(:new).with(
height: '64',
width: '64',
modifier: '#',
orientation: nil
).returns(:correct)
factory = Paperclip::GeometryParser.new("64x64#")
output = factory.make
assert_equal :correct, output
end
it 'identifies an image and extract its dimensions, orientation, and modifier' do
Paperclip::Geometry.stubs(:new).with(
height: '50',
width: '100',
modifier: '>',
orientation: '7'
).returns(:correct)
factory = Paperclip::GeometryParser.new("100x50,7>")
output = factory.make
assert_equal :correct, output
end
end
================================================
FILE: spec/paperclip/geometry_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Geometry do
context "Paperclip::Geometry" do
it "correctly reports its given dimensions" do
assert @geo = Paperclip::Geometry.new(1024, 768)
assert_equal 1024, @geo.width
assert_equal 768, @geo.height
end
it "sets height to 0 if height dimension is missing" do
assert @geo = Paperclip::Geometry.new(1024)
assert_equal 1024, @geo.width
assert_equal 0, @geo.height
end
it "sets width to 0 if width dimension is missing" do
assert @geo = Paperclip::Geometry.new(nil, 768)
assert_equal 0, @geo.width
assert_equal 768, @geo.height
end
it "is generated from a WxH-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x600")
assert_equal 800, @geo.width
assert_equal 600, @geo.height
end
it "is generated from a xH-formatted string" do
assert @geo = Paperclip::Geometry.parse("x600")
assert_equal 0, @geo.width
assert_equal 600, @geo.height
end
it "is generated from a Wx-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x")
assert_equal 800, @geo.width
assert_equal 0, @geo.height
end
it "is generated from a W-formatted string" do
assert @geo = Paperclip::Geometry.parse("800")
assert_equal 800, @geo.width
assert_equal 0, @geo.height
end
it "ensures the modifier is nil if not present" do
assert @geo = Paperclip::Geometry.parse("123x456")
assert_nil @geo.modifier
end
it "recognizes an EXIF orientation and not rotate with auto_orient if not necessary" do
geo = Paperclip::Geometry.new(width: 1024, height: 768, orientation: 1)
assert geo
assert_equal 1024, geo.width
assert_equal 768, geo.height
geo.auto_orient
assert_equal 1024, geo.width
assert_equal 768, geo.height
end
it "recognizes an EXIF orientation and rotate with auto_orient if necessary" do
geo = Paperclip::Geometry.new(width: 1024, height: 768, orientation: 6)
assert geo
assert_equal 1024, geo.width
assert_equal 768, geo.height
geo.auto_orient
assert_equal 768, geo.width
assert_equal 1024, geo.height
end
it "treats x and X the same in geometries" do
@lower = Paperclip::Geometry.parse("123x456")
@upper = Paperclip::Geometry.parse("123X456")
assert_equal 123, @lower.width
assert_equal 123, @upper.width
assert_equal 456, @lower.height
assert_equal 456, @upper.height
end
['>', '<', '#', '@', '@>', '>@', '%', '^', '!', nil].each do |mod|
it "ensures the modifier #{description} is preserved" do
assert @geo = Paperclip::Geometry.parse("123x456#{mod}")
assert_equal mod, @geo.modifier
assert_equal "123x456#{mod}", @geo.to_s
end
it "ensures the modifier #{description} is preserved with no height" do
assert @geo = Paperclip::Geometry.parse("123x#{mod}")
assert_equal mod, @geo.modifier
assert_equal "123#{mod}", @geo.to_s
end
end
it "makes sure the modifier gets passed during transformation_to" do
assert @src = Paperclip::Geometry.parse("123x456")
assert @dst = Paperclip::Geometry.parse("123x456>")
assert_equal ["123x456>", nil], @src.transformation_to(@dst)
end
it "generates correct ImageMagick formatting string for W-formatted string" do
assert @geo = Paperclip::Geometry.parse("800")
assert_equal "800", @geo.to_s
end
it "generates correct ImageMagick formatting string for Wx-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x")
assert_equal "800", @geo.to_s
end
it "generates correct ImageMagick formatting string for xH-formatted string" do
assert @geo = Paperclip::Geometry.parse("x600")
assert_equal "x600", @geo.to_s
end
it "generates correct ImageMagick formatting string for WxH-formatted string" do
assert @geo = Paperclip::Geometry.parse("800x600")
assert_equal "800x600", @geo.to_s
end
it "is generated from a file" do
file = fixture_file("5k.png")
file = File.new(file, 'rb')
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
assert_equal 66, @geo.height
assert_equal 434, @geo.width
end
it "is generated from a file path" do
file = fixture_file("5k.png")
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
assert_equal 66, @geo.height
assert_equal 434, @geo.width
end
it 'calculates an EXIF-rotated image dimensions from a path' do
file = fixture_file("rotated.jpg")
assert_nothing_raised{ @geo = Paperclip::Geometry.from_file(file) }
@geo.auto_orient
assert_equal 300, @geo.height
assert_equal 200, @geo.width
end
it "does not generate from a bad file" do
file = "/home/This File Does Not Exist.omg"
expect { @geo = Paperclip::Geometry.from_file(file) }.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError)
end
it "does not generate from a blank filename" do
file = ""
expect { @geo = Paperclip::Geometry.from_file(file) }.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError)
end
it "does not generate from a nil file" do
file = nil
expect { @geo = Paperclip::Geometry.from_file(file) }.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError)
end
it "does not generate from a file with no path" do
file = mock("file", path: "")
file.stubs(:respond_to?).with(:path).returns(true)
expect { @geo = Paperclip::Geometry.from_file(file) }.to raise_error(Paperclip::Errors::NotIdentifiedByImageMagickError)
end
it "lets us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
ENV['PATH'] = ''
assert_raises(Paperclip::Errors::CommandNotFoundError) do
file = fixture_file("5k.png")
@geo = Paperclip::Geometry.from_file(file)
end
ensure
ENV['PATH'] = old_path
end
end
[['vertical', 900, 1440, true, false, false, 1440, 900, 0.625],
['horizontal', 1024, 768, false, true, false, 1024, 768, 1.3333],
['square', 100, 100, false, false, true, 100, 100, 1]].each do |args|
context "performing calculations on a #{args[0]} viewport" do
before do
@geo = Paperclip::Geometry.new(args[1], args[2])
end
it "is #{args[3] ? "" : "not"} vertical" do
assert_equal args[3], @geo.vertical?
end
it "is #{args[4] ? "" : "not"} horizontal" do
assert_equal args[4], @geo.horizontal?
end
it "is #{args[5] ? "" : "not"} square" do
assert_equal args[5], @geo.square?
end
it "reports that #{args[6]} is the larger dimension" do
assert_equal args[6], @geo.larger
end
it "reports that #{args[7]} is the smaller dimension" do
assert_equal args[7], @geo.smaller
end
it "has an aspect ratio of #{args[8]}" do
expect(@geo.aspect).to be_within(0.0001).of(args[8])
end
end
end
[[ [1000, 100], [64, 64], "x64", "64x64+288+0" ],
[ [100, 1000], [50, 950], "x950", "50x950+22+0" ],
[ [100, 1000], [50, 25], "50x", "50x25+0+237" ]]. each do |args|
context "of #{args[0].inspect} and given a Geometry #{args[1].inspect} and sent transform_to" do
before do
@geo = Paperclip::Geometry.new(*args[0])
@dst = Paperclip::Geometry.new(*args[1])
@scale, @crop = @geo.transformation_to @dst, true
end
it "is able to return the correct scaling transformation geometry #{args[2]}" do
assert_equal args[2], @scale
end
it "is able to return the correct crop transformation geometry #{args[3]}" do
assert_equal args[3], @crop
end
end
end
[['256x256', {'150x150!' => [150, 150], '150x150#' => [150, 150], '150x150>' => [150, 150], '150x150<' => [256, 256], '150x150' => [150, 150]}],
['256x256', {'512x512!' => [512, 512], '512x512#' => [512, 512], '512x512>' => [256, 256], '512x512<' => [512, 512], '512x512' => [512, 512]}],
['600x400', {'512x512!' => [512, 512], '512x512#' => [512, 512], '512x512>' => [512, 341], '512x512<' => [600, 400], '512x512' => [512, 341]}]].each do |original_size, options|
options.each_pair do |size, dimensions|
context "#{original_size} resize_to #{size}" do
before do
@source = Paperclip::Geometry.parse original_size
@new_geometry = @source.resize_to size
end
it "has #{dimensions.first} width" do
assert_equal dimensions.first, @new_geometry.width
end
it "has #{dimensions.last} height" do
assert_equal dimensions.last, @new_geometry.height
end
end
end
end
end
end
================================================
FILE: spec/paperclip/glue_spec.rb
================================================
# require "spec_helper"
describe Paperclip::Glue do
describe "when ActiveRecord does not exist" do
before do
ActiveRecordSaved = ActiveRecord
Object.send :remove_const, "ActiveRecord"
end
after do
ActiveRecord = ActiveRecordSaved
Object.send :remove_const, "ActiveRecordSaved"
end
it "does not fail" do
NonActiveRecordModel = Class.new
NonActiveRecordModel.send :include, Paperclip::Glue
Object.send :remove_const, "NonActiveRecordModel"
end
end
describe "when ActiveRecord does exist" do
before do
if Object.const_defined?("ActiveRecord")
@defined_active_record = false
else
ActiveRecord = :defined
@defined_active_record = true
end
end
after do
if @defined_active_record
Object.send :remove_const, "ActiveRecord"
end
end
it "does not fail" do
NonActiveRecordModel = Class.new
NonActiveRecordModel.send :include, Paperclip::Glue
Object.send :remove_const, "NonActiveRecordModel"
end
end
end
================================================
FILE: spec/paperclip/has_attached_file_spec.rb
================================================
require 'spec_helper'
describe Paperclip::HasAttachedFile do
context '#define_on' do
it 'defines a setter on the class object' do
assert_adding_attachment('avatar').defines_method('avatar=')
end
it 'defines a getter on the class object' do
assert_adding_attachment('avatar').defines_method('avatar')
end
it 'defines a query on the class object' do
assert_adding_attachment('avatar').defines_method('avatar?')
end
it 'defines a method on the class to get all of its attachments' do
assert_adding_attachment('avatar').defines_class_method('attachment_definitions')
end
it 'flushes errors as part of validations' do
assert_adding_attachment('avatar').defines_validation
end
it 'registers the attachment with Paperclip::AttachmentRegistry' do
assert_adding_attachment('avatar').registers_attachment
end
it 'defines an after_save callback' do
assert_adding_attachment('avatar').defines_callback('after_save')
end
it 'defines a before_destroy callback' do
assert_adding_attachment('avatar').defines_callback('before_destroy')
end
it 'defines an after_commit callback' do
assert_adding_attachment('avatar').defines_callback('after_commit')
end
context 'when the class does not allow after_commit callbacks' do
it 'defines an after_destroy callback' do
assert_adding_attachment(
'avatar',
unstub_methods: [:after_commit]
).defines_callback('after_destroy')
end
end
it 'defines the Paperclip-specific callbacks' do
assert_adding_attachment('avatar').defines_callback('define_paperclip_callbacks')
end
it 'does not define a media_type check if told not to' do
assert_adding_attachment('avatar').does_not_set_up_media_type_check_validation
end
it 'does define a media_type check if told to' do
assert_adding_attachment('avatar').sets_up_media_type_check_validation
end
end
private
def assert_adding_attachment(attachment_name, options={})
AttachmentAdder.new(attachment_name, options)
end
class AttachmentAdder
include Mocha::API
include RSpec::Matchers
def initialize(attachment_name, options = {})
@attachment_name = attachment_name
@stubbed_class = stub_class
if options.present?
options[:unstub_methods].each do |method|
@stubbed_class.unstub(method)
end
end
end
def defines_method(method_name)
a_class = @stubbed_class
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, {})
expect(a_class).to have_received(:define_method).with(method_name)
end
def defines_class_method(method_name)
a_class = @stubbed_class
a_class.class.stubs(:define_method)
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, {})
expect(a_class).to have_received(:extend).with(Paperclip::HasAttachedFile::ClassMethods)
end
def defines_validation
a_class = @stubbed_class
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, {})
expect(a_class).to have_received(:validates_each).with(@attachment_name)
end
def registers_attachment
a_class = @stubbed_class
Paperclip::AttachmentRegistry.stubs(:register)
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, {size: 1})
expect(Paperclip::AttachmentRegistry).to have_received(:register).with(a_class, @attachment_name, {size: 1})
end
def defines_callback(callback_name)
a_class = @stubbed_class
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, {})
expect(a_class).to have_received(callback_name.to_sym)
end
def does_not_set_up_media_type_check_validation
a_class = stub_class
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, { validate_media_type: false })
expect(a_class).to have_received(:validates_media_type_spoof_detection).never
end
def sets_up_media_type_check_validation
a_class = stub_class
Paperclip::HasAttachedFile.define_on(a_class, @attachment_name, { validate_media_type: true })
expect(a_class).to have_received(:validates_media_type_spoof_detection)
end
private
def stub_class
stub('class',
validates_each: nil,
define_method: nil,
after_save: nil,
before_destroy: nil,
after_commit: nil,
after_destroy: nil,
define_paperclip_callbacks: nil,
extend: nil,
name: 'Billy',
validates_media_type_spoof_detection: nil
)
end
end
end
================================================
FILE: spec/paperclip/integration_spec.rb
================================================
require 'spec_helper'
require 'open-uri'
describe 'Paperclip' do
around do |example|
files_before = ObjectSpace.each_object(Tempfile).select do |file|
file.path && File.file?(file.path)
end
example.run
files_after = ObjectSpace.each_object(Tempfile).select do |file|
file.path && File.file?(file.path)
end
diff = files_after - files_before
expect(diff).to eq([]), "Leaked tempfiles: #{diff.inspect}"
end
context "Many models at once" do
before do
rebuild_model
@file = File.new(fixture_file("5k.png"), 'rb')
# Deals with `Too many open files` error
dummies = Array.new(300) { Dummy.new avatar: @file }
Dummy.import dummies
# save attachment instances to run after hooks including tempfile cleanup
# since activerecord-import does not use our usually hooked-in hooks
# (such as after_save)
dummies.each { |dummy| dummy.avatar.save }
end
after { @file.close }
it "does not exceed the open file limit" do
assert_nothing_raised do
Dummy.all.each { |dummy| dummy.avatar }
end
end
end
context "An attachment" do
before do
rebuild_model styles: { thumb: "50x50#" }
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
assert @dummy.save
end
after { @file.close }
it "creates its thumbnails properly" do
assert_match(/\b50x50\b/, `identify "#{@dummy.avatar.path(:thumb)}"`)
end
context 'reprocessing with unreadable original' do
before { File.chmod(0000, @dummy.avatar.path) }
it "does not raise an error" do
assert_nothing_raised do
silence_stream(STDERR) do
@dummy.avatar.reprocess!
end
end
end
it "returns false" do
silence_stream(STDERR) do
assert !@dummy.avatar.reprocess!
end
end
after { File.chmod(0644, @dummy.avatar.path) }
end
context "redefining its attachment styles" do
before do
Dummy.class_eval do
has_attached_file :avatar, styles: { thumb: "150x25#", dynamic: lambda { |a| '50x50#' } }
end
@d2 = Dummy.find(@dummy.id)
@original_timestamp = @d2.avatar_updated_at
@d2.avatar.reprocess!
@d2.save
end
it "creates its thumbnails properly" do
assert_match(/\b150x25\b/, `identify "#{@dummy.avatar.path(:thumb)}"`)
assert_match(/\b50x50\b/, `identify "#{@dummy.avatar.path(:dynamic)}"`)
end
it "changes the timestamp" do
assert_not_equal @original_timestamp, @d2.avatar_updated_at
end
end
end
context "Attachment" do
before do
@thumb_path = "tmp/public/system/dummies/avatars/000/000/001/thumb/5k.png"
File.delete(@thumb_path) if File.exist?(@thumb_path)
rebuild_model styles: { thumb: "50x50#" }
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
end
after { @file.close }
it "does not create the thumbnails upon saving when post-processing is disabled" do
@dummy.avatar.post_processing = false
@dummy.avatar = @file
assert @dummy.save
assert_file_not_exists @thumb_path
end
it "creates the thumbnails upon saving when post_processing is enabled" do
@dummy.avatar.post_processing = true
@dummy.avatar = @file
assert @dummy.save
assert_file_exists @thumb_path
end
end
context "Attachment with no generated thumbnails" do
before do
@thumb_small_path = "tmp/public/system/dummies/avatars/000/000/001/thumb_small/5k.png"
@thumb_large_path = "tmp/public/system/dummies/avatars/000/000/001/thumb_large/5k.png"
File.delete(@thumb_small_path) if File.exist?(@thumb_small_path)
File.delete(@thumb_large_path) if File.exist?(@thumb_large_path)
rebuild_model styles: { thumb_small: "50x50#", thumb_large: "60x60#" }
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar.post_processing = false
@dummy.avatar = @file
assert @dummy.save
@dummy.avatar.post_processing = true
end
after { @file.close }
it "allows us to create all thumbnails in one go" do
assert_file_not_exists(@thumb_small_path)
assert_file_not_exists(@thumb_large_path)
@dummy.avatar.reprocess!
assert_file_exists(@thumb_small_path)
assert_file_exists(@thumb_large_path)
end
it "allows us to selectively create each thumbnail" do
skip <<-EXPLANATION
#reprocess! calls #assign which calls Paperclip.io_adapters.for
which creates the tempfile. #assign then calls #post_process_file which
calls MediaTypeSpoofDetectionValidator#validate_each which calls
Paperclip.io_adapters.for, which creates another tempfile. That first
tempfile is the one that leaks.
EXPLANATION
assert_file_not_exists(@thumb_small_path)
assert_file_not_exists(@thumb_large_path)
@dummy.avatar.reprocess! :thumb_small
assert_file_exists(@thumb_small_path)
assert_file_not_exists(@thumb_large_path)
@dummy.avatar.reprocess! :thumb_large
assert_file_exists(@thumb_large_path)
end
end
context "A model that modifies its original" do
before do
rebuild_model styles: { original: "2x2#" }
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
end
it "reports the file size of the processed file and not the original" do
assert_not_equal File.size(@file.path), @dummy.avatar.size
end
after do
@file.close
# save attachment instance to run after hooks (including tempfile cleanup)
@dummy.avatar.save
end
end
context "A model with attachments scoped under an id" do
before do
rebuild_model styles: { large: "100x100",
medium: "50x50" },
path: ":rails_root/tmp/:id/:attachments/:style.:extension"
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy.avatar = @file
end
after { @file.close }
context "when saved" do
before do
@dummy.save
@saved_path = @dummy.avatar.path(:large)
end
it "has a large file in the right place" do
assert_file_exists(@dummy.avatar.path(:large))
end
context "and deleted" do
before do
@dummy.avatar.clear
@dummy.save
end
it "does not have a large file in the right place anymore" do
assert_file_not_exists(@saved_path)
end
it "does not have its next two parent directories" do
assert_file_not_exists(File.dirname(@saved_path))
assert_file_not_exists(File.dirname(File.dirname(@saved_path)))
end
end
context 'and deleted where the delete fails' do
it "does not die if an unexpected SystemCallError happens" do
FileUtils.stubs(:rmdir).raises(Errno::EPIPE)
assert_nothing_raised do
@dummy.avatar.clear
@dummy.save
end
end
end
end
end
[000,002,022].each do |umask|
context "when the umask is #{umask}" do
before do
rebuild_model
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@umask = File.umask(umask)
end
after do
File.umask @umask
@file.close
end
it "respects the current umask" do
@dummy.avatar = @file
@dummy.save
assert_equal 0666&~umask, 0666&File.stat(@dummy.avatar.path).mode
end
end
end
[0666,0664,0640].each do |perms|
context "when the perms are #{perms}" do
before do
rebuild_model override_file_permissions: perms
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
end
after do
@file.close
end
it "respects the current perms" do
@dummy.avatar = @file
@dummy.save
assert_equal perms, File.stat(@dummy.avatar.path).mode & 0777
end
end
end
it "skips chmod operation, when override_file_permissions is set to false (e.g. useful when using CIFS mounts)" do
FileUtils.expects(:chmod).never
rebuild_model override_file_permissions: false
dummy = Dummy.create!
dummy.avatar = @file
dummy.save
end
context "A model with a filesystem attachment" do
before do
rebuild_model styles: { large: "300x300>",
medium: "100x100",
thumb: ["32x32#", :gif] },
default_style: :medium,
url: "/:attachment/:class/:style/:id/:basename.:extension",
path: ":rails_root/tmp/:attachment/:class/:style/:id/:basename.:extension"
@dummy = Dummy.new
@file = File.new(fixture_file("5k.png"), 'rb')
@bad_file = File.new(fixture_file("bad.png"), 'rb')
assert @dummy.avatar = @file
assert @dummy.valid?, @dummy.errors.full_messages.join(", ")
assert @dummy.save
end
after { [@file, @bad_file].each(&:close) }
it "writes and delete its files" do
[["434x66", :original],
["300x46", :large],
["100x15", :medium],
["32x32", :thumb]].each do |geo, style|
cmd = %Q[identify -format "%wx%h" "#{@dummy.avatar.path(style)}"]
assert_equal geo, `#{cmd}`.chomp, cmd
end
saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) }
@d2 = Dummy.find(@dummy.id)
assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path}"`.chomp
assert_equal "434x66", `identify -format "%wx%h" "#{@d2.avatar.path(:original)}"`.chomp
assert_equal "300x46", `identify -format "%wx%h" "#{@d2.avatar.path(:large)}"`.chomp
assert_equal "100x15", `identify -format "%wx%h" "#{@d2.avatar.path(:medium)}"`.chomp
assert_equal "32x32", `identify -format "%wx%h" "#{@d2.avatar.path(:thumb)}"`.chomp
assert @dummy.valid?
assert @dummy.save
saved_paths.each do |p|
assert_file_exists(p)
end
@dummy.avatar.clear
assert_nil @dummy.avatar_file_name
assert @dummy.valid?
assert @dummy.save
saved_paths.each do |p|
assert_file_not_exists(p)
end
@d2 = Dummy.find(@dummy.id)
assert_nil @d2.avatar_file_name
end
it "works exactly the same when new as when reloaded" do
@d2 = Dummy.find(@dummy.id)
assert_equal @dummy.avatar_file_name, @d2.avatar_file_name
[:thumb, :medium, :large, :original].each do |style|
assert_equal @dummy.avatar.path(style), @d2.avatar.path(style)
end
saved_paths = [:thumb, :medium, :large, :original].collect{|s| @dummy.avatar.path(s) }
@d2.avatar.clear
assert @d2.save
saved_paths.each do |p|
assert_file_not_exists(p)
end
end
it "does not abide things that don't have adapters" do
assert_raises(Paperclip::AdapterRegistry::NoHandlerError) do
@dummy.avatar = "not a file"
end
end
it "is not ok with bad files" do
@dummy.avatar = @bad_file
assert ! @dummy.valid?
# save attachment instance to run after hooks (including tempfile cleanup)
@dummy.avatar.save
end
it "knows the difference between good files, bad files, and not files when validating" do
Dummy.validates_attachment_presence :avatar
@d2 = Dummy.find(@dummy.id)
@d2.avatar = @file
assert @d2.valid?, @d2.errors.full_messages.inspect
# save attachment instance to run after hooks (including tempfile cleanup)
@d2.avatar.save
@d2.avatar = @bad_file
assert ! @d2.valid?
# save attachment instance to run after hooks (including tempfile cleanup)
@d2.avatar.save
end
it "is able to reload without saving and not have the file disappear" do
@dummy.avatar = @file
assert @dummy.save, @dummy.errors.full_messages.inspect
@dummy.avatar.clear
assert_nil @dummy.avatar_file_name
@dummy.reload
assert_equal "5k.png", @dummy.avatar_file_name
end
context "that is assigned its file from another Paperclip attachment" do
before do
@dummy2 = Dummy.new
@file2 = File.new(fixture_file("12k.png"), 'rb')
assert @dummy2.avatar = @file2
@dummy2.save
end
after { @file2.close }
it "works when assigned a file" do
assert_not_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`,
`identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"`
assert @dummy.avatar = @dummy2.avatar
@dummy.save
assert_equal @dummy.avatar_file_name, @dummy2.avatar_file_name
assert_equal `identify -format "%wx%h" "#{@dummy.avatar.path(:original)}"`,
`identify -format "%wx%h" "#{@dummy2.avatar.path(:original)}"`
end
end
end
context "A model with an attachments association and a Paperclip attachment" do
before do
Dummy.class_eval do
has_many :attachments, class_name: 'Dummy'
end
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
it "does not error when saving" do
@dummy.save!
end
end
context "A model with an attachment with hash in file name" do
before do
@settings = { styles: { thumb: "50x50#" },
path: ":rails_root/public/system/:attachment/:id_partition/:style/:hash.:extension",
url: "/system/:attachment/:id_partition/:style/:hash.:extension",
hash_secret: "somesecret" }
rebuild_model @settings
@file = File.new(fixture_file("5k.png"), 'rb')
@dummy = Dummy.create! avatar: @file
end
after do
@file.close
end
it "is accessible" do
assert_file_exists(@dummy.avatar.path(:original))
assert_file_exists(@dummy.avatar.path(:thumb))
end
context "when new style is added" do
before do
@dummy.avatar.options[:styles][:mini] = "25x25#"
@dummy.avatar.instance_variable_set :@normalized_styles, nil
Time.stubs(now: Time.now + 10)
@dummy.avatar.reprocess!
@dummy.reload
end
it "makes all the styles accessible" do
assert_file_exists(@dummy.avatar.path(:original))
assert_file_exists(@dummy.avatar.path(:thumb))
assert_file_exists(@dummy.avatar.path(:mini))
end
end
end
if ENV['S3_BUCKET']
def s3_files_for attachment
[:thumb, :medium, :large, :original].inject({}) do |files, style|
data = `curl "#{attachment.url(style)}" 2>/dev/null`.chomp
t = Tempfile.new("paperclip-test")
t.binmode
t.write(data)
t.rewind
files[style] = t
files
end
end
def s3_headers_for attachment, style
`curl --head "#{attachment.url(style)}" 2>/dev/null`.split("\n").inject({}) do |h,head|
split_head = head.chomp.split(/\s*:\s*/, 2)
h[split_head.first.downcase] = split_head.last unless split_head.empty?
h
end
end
context "A model with an S3 attachment" do
before do
rebuild_model(
styles: {
large: "300x300>",
medium: "100x100",
thumb: ["32x32#", :gif],
custom: {
geometry: "32x32#",
s3_headers: { 'Cache-Control' => 'max-age=31557600' },
s3_metadata: { 'foo' => 'bar'}
}
},
storage: :s3,
s3_credentials: File.new(fixture_file('s3.yml')),
s3_options: { logger: Paperclip.logger },
default_style: :medium,
bucket: ENV['S3_BUCKET'],
path: ":class/:attachment/:id/:style/:basename.:extension"
)
@dummy = Dummy.new
@file = File.new(fixture_file('5k.png'), 'rb')
@bad_file = File.new(fixture_file('bad.png'), 'rb')
@dummy.avatar = @file
@dummy.valid?
@dummy.save!
@files_on_s3 = s3_files_for(@dummy.avatar)
end
after do
@file.close
@bad_file.close
@files_on_s3.values.each(&:close) if @files_on_s3
end
context 'assigning itself to a new model' do
before do
@d2 = Dummy.new
@d2.avatar = @dummy.avatar
@d2.save
end
it "has the same name as the old file" do
assert_equal @d2.avatar.original_filename, @dummy.avatar.original_filename
end
end
it "has the same contents as the original" do
assert_equal @file.read, @files_on_s3[:original].read
end
it "writes and delete its files" do
[["434x66", :original],
["300x46", :large],
["100x15", :medium],
["32x32", :thumb]].each do |geo, style|
cmd = %Q[identify -format "%wx%h" "#{@files_on_s3[style].path}"]
assert_equal geo, `#{cmd}`.chomp, cmd
end
@d2 = Dummy.find(@dummy.id)
@d2_files = s3_files_for @d2.avatar
[["434x66", :original],
["300x46", :large],
["100x15", :medium],
["32x32", :thumb]].each do |geo, style|
cmd = %Q[identify -format "%wx%h" "#{@d2_files[style].path}"]
assert_equal geo, `#{cmd}`.chomp, cmd
end
@dummy.avatar.clear
assert_nil @dummy.avatar_file_name
assert @dummy.valid?
assert @dummy.save
[:thumb, :medium, :large, :original].each do |style|
assert ! @dummy.avatar.exists?(style)
end
@d2 = Dummy.find(@dummy.id)
assert_nil @d2.avatar_file_name
end
it "works exactly the same when new as when reloaded" do
@d2 = Dummy.find(@dummy.id)
assert_equal @dummy.avatar_file_name, @d2.avatar_file_name
[:thumb, :medium, :large, :original].each do |style|
begin
first_file = open(@dummy.avatar.url(style))
second_file = open(@dummy.avatar.url(style))
assert_equal first_file.read, second_file.read
ensure
first_file.close if first_file
second_file.close if second_file
end
end
@d2.avatar.clear
assert @d2.save
[:thumb, :medium, :large, :original].each do |style|
assert ! @dummy.avatar.exists?(style)
end
end
it "knows the difference between good files, bad files, and nil" do
@dummy.avatar = @bad_file
assert ! @dummy.valid?
@dummy.avatar = nil
assert @dummy.valid?
Dummy.validates_attachment_presence :avatar
@d2 = Dummy.find(@dummy.id)
@d2.avatar = @file
assert @d2.valid?
@d2.avatar = @bad_file
assert ! @d2.valid?
@d2.avatar = nil
assert ! @d2.valid?
end
it "is able to reload without saving and not have the file disappear" do
@dummy.avatar = @file
assert @dummy.save
@dummy.avatar = nil
assert_nil @dummy.avatar_file_name
@dummy.reload
assert_equal "5k.png", @dummy.avatar_file_name
end
it "has the right content type" do
headers = s3_headers_for(@dummy.avatar, :original)
assert_equal 'image/png', headers['content-type']
end
it "has the right style-specific headers" do
headers = s3_headers_for(@dummy.avatar, :custom)
assert_equal 'max-age=31557600', headers['cache-control']
end
it "has the right style-specific metadata" do
headers = s3_headers_for(@dummy.avatar, :custom)
assert_equal 'bar', headers['x-amz-meta-foo']
end
context "with non-english character in the file name" do
before do
@file.stubs(:original_filename).returns("クリップ.png")
@dummy.avatar = @file
end
it "does not raise any error" do
@dummy.save!
end
end
end
end
context "Copying attachments between models" do
before do
rebuild_model
@file = File.new(fixture_file("5k.png"), 'rb')
end
after { @file.close }
it "succeeds when original attachment is a file" do
original = Dummy.new
original.avatar = @file
assert original.save
copy = Dummy.new
copy.avatar = original.avatar
assert copy.save
assert copy.avatar.present?
end
it "succeeds when original attachment is empty" do
original = Dummy.create!
copy = Dummy.new
copy.avatar = @file
assert copy.save
assert copy.avatar.present?
copy.avatar = original.avatar
assert copy.save
assert !copy.avatar.present?
end
end
end
================================================
FILE: spec/paperclip/interpolations_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Interpolations do
it "returns all methods but the infrastructure when sent #all" do
methods = Paperclip::Interpolations.all
assert ! methods.include?(:[])
assert ! methods.include?(:[]=)
assert ! methods.include?(:all)
methods.each do |m|
assert Paperclip::Interpolations.respond_to?(m)
end
end
it "returns the Rails.root" do
assert_equal Rails.root, Paperclip::Interpolations.rails_root(:attachment, :style)
end
it "returns the Rails.env" do
assert_equal Rails.env, Paperclip::Interpolations.rails_env(:attachment, :style)
end
it "returns the class of the Interpolations module when called with no params" do
assert_equal Module, Paperclip::Interpolations.class
end
it "returns the class of the instance" do
class Thing ; end
attachment = mock
attachment.expects(:instance).returns(attachment)
attachment.expects(:class).returns(Thing)
assert_equal "things", Paperclip::Interpolations.class(attachment, :style)
end
it "returns the basename of the file" do
attachment = mock
attachment.expects(:original_filename).returns("one.jpg").times(1)
assert_equal "one", Paperclip::Interpolations.basename(attachment, :style)
end
it "returns the extension of the file" do
attachment = mock
attachment.expects(:original_filename).returns("one.jpg")
attachment.expects(:styles).returns({})
assert_equal "jpg", Paperclip::Interpolations.extension(attachment, :style)
end
it "returns the extension of the file as the format if defined in the style" do
attachment = mock
attachment.expects(:original_filename).never
attachment.expects(:styles).twice.returns({style: {format: "png"}})
[:style, 'style'].each do |style|
assert_equal "png", Paperclip::Interpolations.extension(attachment, style)
end
end
it "returns the extension of the file based on the content type" do
attachment = mock
attachment.expects(:content_type).returns('image/png')
attachment.expects(:styles).returns({})
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('random')
assert_equal "png", interpolations.content_type_extension(attachment, :style)
end
it "returns the original extension of the file if it matches a content type extension" do
attachment = mock
attachment.expects(:content_type).returns('image/jpeg')
attachment.expects(:styles).returns({})
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('jpe')
assert_equal "jpe", interpolations.content_type_extension(attachment, :style)
end
it "returns the extension of the file with a dot" do
attachment = mock
attachment.expects(:original_filename).returns("one.jpg")
attachment.expects(:styles).returns({})
assert_equal ".jpg", Paperclip::Interpolations.dotextension(attachment, :style)
end
it "returns the extension of the file without a dot if the extension is empty" do
attachment = mock
attachment.expects(:original_filename).returns("one")
attachment.expects(:styles).returns({})
assert_equal "", Paperclip::Interpolations.dotextension(attachment, :style)
end
it "returns the latter half of the content type of the extension if no match found" do
attachment = mock
attachment.expects(:content_type).at_least_once().returns('not/found')
attachment.expects(:styles).returns({})
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('random')
assert_equal "found", interpolations.content_type_extension(attachment, :style)
end
it "returns the format if defined in the style, ignoring the content type" do
attachment = mock
attachment.expects(:content_type).returns('image/jpeg')
attachment.expects(:styles).returns({style: {format: "png"}})
interpolations = Paperclip::Interpolations
interpolations.expects(:extension).returns('random')
assert_equal "png", interpolations.content_type_extension(attachment, :style)
end
it "is able to handle numeric style names" do
attachment = mock(
styles: {:"4" => {format: :expected_extension}}
)
assert_equal :expected_extension, Paperclip::Interpolations.extension(attachment, 4)
end
it "returns the #to_param of the attachment" do
attachment = mock
attachment.expects(:to_param).returns("23-awesome")
attachment.expects(:instance).returns(attachment)
assert_equal "23-awesome", Paperclip::Interpolations.param(attachment, :style)
end
it "returns the id of the attachment" do
attachment = mock
attachment.expects(:id).returns(23)
attachment.expects(:instance).returns(attachment)
assert_equal 23, Paperclip::Interpolations.id(attachment, :style)
end
it "returns nil for attachments to new records" do
attachment = mock
attachment.expects(:id).returns(nil)
attachment.expects(:instance).returns(attachment)
assert_nil Paperclip::Interpolations.id(attachment, :style)
end
it "returns the partitioned id of the attachment when the id is an integer" do
attachment = mock
attachment.expects(:id).returns(23)
attachment.expects(:instance).returns(attachment)
assert_equal "000/000/023", Paperclip::Interpolations.id_partition(attachment, :style)
end
it "returns the partitioned id when the id is above 999_999_999" do
attachment = mock
attachment.expects(:id).
returns(Paperclip::Interpolations::ID_PARTITION_LIMIT)
attachment.expects(:instance).returns(attachment)
assert_equal "001/000/000/000",
Paperclip::Interpolations.id_partition(attachment, :style)
end
it "returns the partitioned id of the attachment when the id is a string" do
attachment = mock
attachment.expects(:id).returns("32fnj23oio2f")
attachment.expects(:instance).returns(attachment)
assert_equal "32f/nj2/3oi", Paperclip::Interpolations.id_partition(attachment, :style)
end
it "returns nil for the partitioned id of an attachment to a new record (when the id is nil)" do
attachment = mock
attachment.expects(:id).returns(nil)
attachment.expects(:instance).returns(attachment)
assert_nil Paperclip::Interpolations.id_partition(attachment, :style)
end
it "returns the name of the attachment" do
attachment = mock
attachment.expects(:name).returns("file")
assert_equal "files", Paperclip::Interpolations.attachment(attachment, :style)
end
it "returns the style" do
assert_equal :style, Paperclip::Interpolations.style(:attachment, :style)
end
it "returns the default style" do
attachment = mock
attachment.expects(:default_style).returns(:default_style)
assert_equal :default_style, Paperclip::Interpolations.style(attachment, nil)
end
it "reinterpolates :url" do
attachment = mock
attachment.expects(:url).with(:style, timestamp: false, escape: false).returns("1234")
assert_equal "1234", Paperclip::Interpolations.url(attachment, :style)
end
it "raises if infinite loop detcted reinterpolating :url" do
attachment = Object.new
class << attachment
def url(*args)
Paperclip::Interpolations.url(self, :style)
end
end
assert_raises(Paperclip::Errors::InfiniteInterpolationError){ Paperclip::Interpolations.url(attachment, :style) }
end
it "returns the filename as basename.extension" do
attachment = mock
attachment.expects(:styles).returns({})
attachment.expects(:original_filename).returns("one.jpg").times(2)
assert_equal "one.jpg", Paperclip::Interpolations.filename(attachment, :style)
end
it "returns the filename as basename.extension when format supplied" do
attachment = mock
attachment.expects(:styles).returns({style: {format: :png}})
attachment.expects(:original_filename).returns("one.jpg").times(1)
assert_equal "one.png", Paperclip::Interpolations.filename(attachment, :style)
end
it "returns the filename as basename when extension is blank" do
attachment = mock
attachment.stubs(:styles).returns({})
attachment.stubs(:original_filename).returns("one")
assert_equal "one", Paperclip::Interpolations.filename(attachment, :style)
end
it "returns the basename when the extension contains regexp special characters" do
attachment = mock
attachment.stubs(:styles).returns({})
attachment.stubs(:original_filename).returns("one.ab)")
assert_equal "one", Paperclip::Interpolations.basename(attachment, :style)
end
it "returns the timestamp" do
now = Time.now
zone = 'UTC'
attachment = mock
attachment.expects(:instance_read).with(:updated_at).returns(now)
attachment.expects(:time_zone).returns(zone)
assert_equal now.in_time_zone(zone).to_s, Paperclip::Interpolations.timestamp(attachment, :style)
end
it "returns updated_at" do
attachment = mock
seconds_since_epoch = 1234567890
attachment.expects(:updated_at).returns(seconds_since_epoch)
assert_equal seconds_since_epoch, Paperclip::Interpolations.updated_at(attachment, :style)
end
it "returns attachment's hash when passing both arguments" do
attachment = mock
fake_hash = "a_wicked_secure_hash"
attachment.expects(:hash_key).returns(fake_hash)
assert_equal fake_hash, Paperclip::Interpolations.hash(attachment, :style)
end
it "returns Object#hash when passing no argument" do
attachment = mock
fake_hash = "a_wicked_secure_hash"
attachment.expects(:hash_key).never.returns(fake_hash)
assert_not_equal fake_hash, Paperclip::Interpolations.hash
end
it "calls all expected interpolations with the given arguments" do
Paperclip::Interpolations.expects(:id).with(:attachment, :style).returns(1234)
Paperclip::Interpolations.expects(:attachment).with(:attachment, :style).returns("attachments")
Paperclip::Interpolations.expects(:notreal).never
value = Paperclip::Interpolations.interpolate(":notreal/:id/:attachment", :attachment, :style)
assert_equal ":notreal/1234/attachments", value
end
it "handles question marks" do
Paperclip.interpolates :foo? do
"bar"
end
Paperclip::Interpolations.expects(:fool).never
value = Paperclip::Interpolations.interpolate(":fo/:foo?")
assert_equal ":fo/bar", value
end
end
================================================
FILE: spec/paperclip/io_adapters/abstract_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::AbstractAdapter do
class TestAdapter < Paperclip::AbstractAdapter
attr_accessor :tempfile
def content_type
Paperclip::ContentTypeDetector.new(path).detect
end
end
subject { TestAdapter.new(nil) }
context "content type from file contents" do
before do
subject.stubs(:path).returns("image.png")
Paperclip.stubs(:run).returns("image/png\n")
Paperclip::ContentTypeDetector.any_instance.stubs(:type_from_mime_magic).returns("image/png")
end
it "returns the content type without newline" do
assert_equal "image/png", subject.content_type
end
end
context "nil?" do
it "returns false" do
assert !subject.nil?
end
end
context "delegation" do
before do
subject.tempfile = stub("Tempfile")
end
[:binmode, :binmode?, :close, :close!, :closed?, :eof?, :path, :readbyte, :rewind, :unlink].each do |method|
it "delegates #{method} to @tempfile" do
subject.tempfile.stubs(method)
subject.public_send(method)
assert_received subject.tempfile, method
end
end
end
it 'gets rid of slashes and colons in filenames' do
subject.original_filename = "awesome/file:name.png"
assert_equal "awesome_file_name.png", subject.original_filename
end
it 'is an assignment' do
assert subject.assignment?
end
it 'is not nil' do
assert !subject.nil?
end
it "generates a destination filename with no original filename" do
expect(subject.send(:destination).path).to_not be_nil
end
it 'uses the original filename to generate the tempfile' do
subject.original_filename = "file.png"
expect(subject.send(:destination).path).to end_with(".png")
end
context "generates a fingerprint" do
subject { TestAdapter.new(nil, options) }
before do
subject.stubs(:path).returns(fixture_file("50x50.png"))
end
context "MD5" do
let(:options) { { hash_digest: Digest::MD5 } }
it "returns a fingerprint" do
expect(subject.fingerprint).to be_a String
expect(subject.fingerprint).to eq "a790b00c9b5d58a8fd17a1ec5a187129"
end
end
context "SHA256" do
let(:options) { { hash_digest: Digest::SHA256 } }
it "returns a fingerprint" do
expect(subject.fingerprint).to be_a String
expect(subject.fingerprint).
to eq "243d7ce1099719df25f600f1c369c629fb979f88d5a01dbe7d0d48c8e6715bb1"
end
end
end
context "#copy_to_tempfile" do
around do |example|
FileUtils.module_eval do
class << self
alias paperclip_ln ln
def ln(*)
raise Errno::EXDEV
end
end
end
example.run
FileUtils.module_eval do
class << self
alias ln paperclip_ln
undef paperclip_ln
end
end
end
it "should return a readable file even when linking fails" do
src = open(fixture_file("5k.png"), "rb")
expect(subject.send(:copy_to_tempfile, src).read).to eq src.read
end
end
context "#original_filename=" do
it "should not fail with a nil original filename" do
expect { subject.original_filename = nil }.not_to raise_error
end
end
context "#link_or_copy_file" do
class TestLinkOrCopyAdapter < Paperclip::AbstractAdapter
public :copy_to_tempfile, :destination
end
subject { TestLinkOrCopyAdapter.new(nil) }
let(:body) { "body" }
let(:file) do
t = Tempfile.new("destination")
t.print(body)
t.rewind
t
end
after do
file.close
file.unlink
end
it "should be able to read the file" do
expect(subject.copy_to_tempfile(file).read).to eq(body)
end
it "should be able to reopen the file after symlink has failed" do
FileUtils.expects(:ln).raises(Errno::EXDEV)
expect(subject.copy_to_tempfile(file).read).to eq(body)
end
end
end
================================================
FILE: spec/paperclip/io_adapters/attachment_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::AttachmentAdapter do
before do
rebuild_model path: "tmp/:class/:attachment/:style/:filename", styles: {thumb: '50x50'}
@attachment = Dummy.new.avatar
end
context "for an attachment" do
before do
@file = File.new(fixture_file("5k.png"))
@file.binmode
@attachment.assign(@file)
@attachment.save
@subject = Paperclip.io_adapters.for(@attachment,
hash_digest: Digest::MD5)
end
after do
@file.close
@subject.close
end
it "gets the right filename" do
assert_equal "5k.png", @subject.original_filename
end
it "forces binmode on tempfile" do
assert @subject.instance_variable_get("@tempfile").binmode?
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
it "gets the file's size" do
assert_equal 4456, @subject.size
end
it "returns false for a call to nil?" do
assert ! @subject.nil?
end
it "generates a MD5 hash of the contents" do
expected = Digest::MD5.file(@file.path).to_s
assert_equal expected, @subject.fingerprint
end
it "reads the contents of the file" do
expected = @file.read
actual = @subject.read
assert expected.length > 0
assert_equal expected.length, actual.length
assert_equal expected, actual
end
end
context "for a file with restricted characters in the name" do
before do
file_contents = IO.read(fixture_file("animated.gif"))
@file = StringIO.new(file_contents)
@file.stubs(:original_filename).returns('image:restricted.gif')
@file.binmode
@attachment.assign(@file)
@attachment.save
@subject = Paperclip.io_adapters.for(@attachment,
hash_digest: Digest::MD5)
end
after do
@subject.close
end
it "does not generate paths that include restricted characters" do
expect(@subject.path).to_not match(/:/)
end
it "does not generate filenames that include restricted characters" do
assert_equal 'image_restricted.gif', @subject.original_filename
end
end
context "for a style" do
before do
@file = File.new(fixture_file("5k.png"))
@file.binmode
@attachment.assign(@file)
@thumb = Tempfile.new("thumbnail").tap(&:binmode)
FileUtils.cp @attachment.queued_for_write[:thumb].path, @thumb.path
@attachment.save
@subject = Paperclip.io_adapters.for(@attachment.styles[:thumb],
hash_digest: Digest::MD5)
end
after do
@file.close
@thumb.close
@subject.close
end
it "gets the original filename" do
assert_equal "5k.png", @subject.original_filename
end
it "forces binmode on tempfile" do
assert @subject.instance_variable_get("@tempfile").binmode?
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
it "gets the thumbnail's file size" do
assert_equal @thumb.size, @subject.size
end
it "returns false for a call to nil?" do
assert ! @subject.nil?
end
it "generates a MD5 hash of the contents" do
expected = Digest::MD5.file(@thumb.path).to_s
assert_equal expected, @subject.fingerprint
end
it "reads the contents of the thumbnail" do
@thumb.rewind
expected = @thumb.read
actual = @subject.read
assert expected.length > 0
assert_equal expected.length, actual.length
assert_equal expected, actual
end
end
end
================================================
FILE: spec/paperclip/io_adapters/data_uri_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::DataUriAdapter do
before do
Paperclip::DataUriAdapter.register
end
after do
Paperclip.io_adapters.unregister(described_class)
if @subject
@subject.close
end
end
it 'allows a missing mime-type' do
adapter = Paperclip.io_adapters.for("data:;base64,#{original_base64_content}")
assert_equal Paperclip::DataUriAdapter, adapter.class
end
it 'alows mime type that has dot in it' do
adapter = Paperclip.io_adapters.for("data:image/vnd.microsoft.icon;base64,#{original_base64_content}")
assert_equal Paperclip::DataUriAdapter, adapter.class
end
context "a new instance" do
before do
@contents = "data:image/png;base64,#{original_base64_content}"
@subject = Paperclip.io_adapters.for(@contents, hash_digest: Digest::MD5)
end
it "returns a nondescript file name" do
assert_equal "data", @subject.original_filename
end
it "returns a content type" do
assert_equal "image/png", @subject.content_type
end
it "returns the size of the data" do
assert_equal 4456, @subject.size
end
it "generates a correct MD5 hash of the contents" do
assert_equal(
Digest::MD5.hexdigest(Base64.decode64(original_base64_content)),
@subject.fingerprint
)
end
it "generates correct fingerprint after read" do
fingerprint = Digest::MD5.hexdigest(@subject.read)
assert_equal fingerprint, @subject.fingerprint
end
it "generates same fingerprint" do
assert_equal @subject.fingerprint, @subject.fingerprint
end
it 'accepts a content_type' do
@subject.content_type = 'image/png'
assert_equal 'image/png', @subject.content_type
end
it 'accepts an original_filename' do
@subject.original_filename = 'image.png'
assert_equal 'image.png', @subject.original_filename
end
it "does not generate filenames that include restricted characters" do
@subject.original_filename = 'image:restricted.png'
assert_equal 'image_restricted.png', @subject.original_filename
end
it "does not generate paths that include restricted characters" do
@subject.original_filename = 'image:restricted.png'
expect(@subject.path).to_not match(/:/)
end
end
def original_base64_content
Base64.encode64(original_file_contents)
end
def original_file_contents
@original_file_contents ||= File.read(fixture_file('5k.png'))
end
end
================================================
FILE: spec/paperclip/io_adapters/empty_string_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::EmptyStringAdapter do
context 'a new instance' do
before do
@subject = Paperclip.io_adapters.for('')
end
it "returns false for a call to nil?" do
assert !@subject.nil?
end
it 'returns false for a call to assignment?' do
assert !@subject.assignment?
end
end
end
================================================
FILE: spec/paperclip/io_adapters/file_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::FileAdapter do
context "a new instance" do
context "with normal file" do
before do
@file = File.new(fixture_file("5k.png"))
@file.binmode
end
after do
@file.close
@subject.close if @subject
end
context 'doing normal things' do
before do
@subject = Paperclip.io_adapters.for(@file, hash_digest: Digest::MD5)
end
it 'uses the original filename to generate the tempfile' do
assert @subject.path.ends_with?(".png")
end
it "gets the right filename" do
assert_equal "5k.png", @subject.original_filename
end
it "forces binmode on tempfile" do
assert @subject.instance_variable_get("@tempfile").binmode?
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
it "returns content type as a string" do
expect(@subject.content_type).to be_a String
end
it "gets the file's size" do
assert_equal 4456, @subject.size
end
it "returns false for a call to nil?" do
assert ! @subject.nil?
end
it "generates a MD5 hash of the contents" do
expected = Digest::MD5.file(@file.path).to_s
assert_equal expected, @subject.fingerprint
end
it "reads the contents of the file" do
expected = @file.read
assert expected.length > 0
assert_equal expected, @subject.read
end
end
context "file with multiple possible content type" do
before do
MIME::Types.stubs(:type_for).returns([MIME::Type.new('image/x-png'), MIME::Type.new('image/png')])
@subject = Paperclip.io_adapters.for(@file, hash_digest: Digest::MD5)
end
it "prefers officially registered mime type" do
assert_equal "image/png", @subject.content_type
end
it "returns content type as a string" do
expect(@subject.content_type).to be_a String
end
end
context "file with content type derived from file contents on *nix" do
before do
MIME::Types.stubs(:type_for).returns([])
Paperclip.stubs(:run).returns("application/vnd.ms-office\n")
Paperclip::ContentTypeDetector.any_instance
.stubs(:type_from_mime_magic).returns("application/vnd.ms-office")
@subject = Paperclip.io_adapters.for(@file)
end
it "returns content type without newline character" do
assert_equal "application/vnd.ms-office", @subject.content_type
end
end
end
context "filename with restricted characters" do
before do
@file = File.open(fixture_file("animated.gif")) do |file|
StringIO.new(file.read)
end
@file.stubs(:original_filename).returns('image:restricted.gif')
@subject = Paperclip.io_adapters.for(@file)
end
after do
@file.close
@subject.close
end
it "does not generate filenames that include restricted characters" do
assert_equal 'image_restricted.gif', @subject.original_filename
end
it "does not generate paths that include restricted characters" do
expect(@subject.path).to_not match(/:/)
end
end
context "empty file" do
before do
@file = Tempfile.new("file_adapter_test")
@subject = Paperclip.io_adapters.for(@file)
end
after do
@file.close
@subject.close
end
it "provides correct mime-type" do
assert_match %r{.*/x-empty}, @subject.content_type
end
end
end
end
================================================
FILE: spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::HttpUrlProxyAdapter do
before do
@open_return = StringIO.new("xxx")
@open_return.stubs(:meta).returns("content-type" => "image/png")
Paperclip::HttpUrlProxyAdapter.any_instance.stubs(:download_content).
returns(@open_return)
Paperclip::HttpUrlProxyAdapter.register
end
after do
Paperclip.io_adapters.unregister(described_class)
end
context "a new instance" do
before do
@url = "http://thoughtbot.com/images/thoughtbot-logo.png"
@subject = Paperclip.io_adapters.for(@url, hash_digest: Digest::MD5)
end
after do
@subject.close
end
it "returns a file name" do
assert_equal "thoughtbot-logo.png", @subject.original_filename
end
it 'closes open handle after reading' do
assert_equal true, @open_return.closed?
end
it "returns a content type" do
assert_equal "image/png", @subject.content_type
end
it "returns the size of the data" do
assert_equal @open_return.size, @subject.size
end
it "generates an MD5 hash of the contents" do
assert_equal Digest::MD5.hexdigest("xxx"), @subject.fingerprint
end
it "generates correct fingerprint after read" do
fingerprint = Digest::MD5.hexdigest(@subject.read)
assert_equal fingerprint, @subject.fingerprint
end
it "generates same fingerprint" do
assert_equal @subject.fingerprint, @subject.fingerprint
end
it "returns the data contained in the StringIO" do
assert_equal "xxx", @subject.read
end
it 'accepts a content_type' do
@subject.content_type = 'image/png'
assert_equal 'image/png', @subject.content_type
end
it 'accepts an original_filename' do
@subject.original_filename = 'image.png'
assert_equal 'image.png', @subject.original_filename
end
end
context "a url with query params" do
subject { Paperclip.io_adapters.for(url) }
after { subject.close }
let(:url) { "https://github.com/thoughtbot/paperclip?file=test" }
it "returns a file name" do
assert_equal "paperclip", subject.original_filename
end
it "preserves params" do
assert_equal url, subject.instance_variable_get(:@target).to_s
end
end
context "a url with restricted characters in the filename" do
before do
@url = "https://github.com/thoughtbot/paper:clip.jpg"
@subject = Paperclip.io_adapters.for(@url)
end
after do
begin
@subject.close
rescue Exception
true
end
end
it "does not generate filenames that include restricted characters" do
assert_equal "paper_clip.jpg", @subject.original_filename
end
it "does not generate paths that include restricted characters" do
expect(@subject.path).to_not match(/:/)
end
end
context "a url with special characters in the filename" do
before do
Paperclip::HttpUrlProxyAdapter.any_instance.stubs(:download_content).
returns(@open_return)
end
let(:filename) do
"paperclip-%C3%B6%C3%A4%C3%BC%E5%AD%97%C2%B4%C2%BD%E2%99%A5"\
"%C3%98%C2%B2%C3%88.png"
end
let(:url) { "https://github.com/thoughtbot/paperclip-öäü字´½♥زÈ.png" }
subject { Paperclip.io_adapters.for(url) }
it "returns a encoded filename" do
assert_equal filename, subject.original_filename
end
context "when already URI encoded" do
let(:url) do
"https://github.com/thoughtbot/paperclip-%C3%B6%C3%A4%C3%BC%E5%AD%97"\
"%C2%B4%C2%BD%E2%99%A5%C3%98%C2%B2%C3%88.png"
end
it "returns a encoded filename" do
assert_equal filename, subject.original_filename
end
end
end
end
================================================
FILE: spec/paperclip/io_adapters/identity_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::IdentityAdapter do
it "responds to #new by returning the argument" do
adapter = Paperclip::IdentityAdapter.new
assert_equal :target, adapter.new(:target, nil)
end
end
================================================
FILE: spec/paperclip/io_adapters/nil_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::NilAdapter do
context 'a new instance' do
before do
@subject = Paperclip.io_adapters.for(nil)
end
it "gets the right filename" do
assert_equal "", @subject.original_filename
end
it "gets the content type" do
assert_equal "", @subject.content_type
end
it "gets the file's size" do
assert_equal 0, @subject.size
end
it "returns true for a call to nil?" do
assert @subject.nil?
end
end
end
================================================
FILE: spec/paperclip/io_adapters/registry_spec.rb
================================================
require 'spec_helper'
describe Paperclip::AttachmentRegistry do
context "for" do
before do
class AdapterTest
def initialize(_target, _ = {}); end
end
@subject = Paperclip::AdapterRegistry.new
@subject.register(AdapterTest){|t| Symbol === t }
end
it "returns the class registered for the adapted type" do
assert_equal AdapterTest, @subject.for(:target).class
end
end
context "registered?" do
before do
class AdapterTest
def initialize(_target, _ = {}); end
end
@subject = Paperclip::AdapterRegistry.new
@subject.register(AdapterTest){|t| Symbol === t }
end
it "returns true when the class of this adapter has been registered" do
assert @subject.registered?(AdapterTest.new(:target))
end
it "returns false when the adapter has not been registered" do
assert ! @subject.registered?(Object)
end
end
end
================================================
FILE: spec/paperclip/io_adapters/stringio_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::StringioAdapter do
context "a new instance" do
before do
@contents = "abc123"
@stringio = StringIO.new(@contents)
@subject = Paperclip.io_adapters.for(@stringio, hash_digest: Digest::MD5)
end
it "returns a file name" do
assert_equal "data", @subject.original_filename
end
it "returns a content type" do
assert_equal "text/plain", @subject.content_type
end
it "returns the size of the data" do
assert_equal 6, @subject.size
end
it "returns the length of the data" do
assert_equal 6, @subject.length
end
it "generates an MD5 hash of the contents" do
assert_equal Digest::MD5.hexdigest(@contents), @subject.fingerprint
end
it "generates correct fingerprint after read" do
fingerprint = Digest::MD5.hexdigest(@subject.read)
assert_equal fingerprint, @subject.fingerprint
end
it "generates same fingerprint" do
assert_equal @subject.fingerprint, @subject.fingerprint
end
it "returns the data contained in the StringIO" do
assert_equal "abc123", @subject.read
end
it 'accepts a content_type' do
@subject.content_type = 'image/png'
assert_equal 'image/png', @subject.content_type
end
it 'accepts an original_filename' do
@subject.original_filename = 'image.png'
assert_equal 'image.png', @subject.original_filename
end
it "does not generate filenames that include restricted characters" do
@subject.original_filename = 'image:restricted.png'
assert_equal 'image_restricted.png', @subject.original_filename
end
it "does not generate paths that include restricted characters" do
@subject.original_filename = 'image:restricted.png'
expect(@subject.path).to_not match(/:/)
end
end
end
================================================
FILE: spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::UploadedFileAdapter do
context "a new instance" do
context "with UploadedFile responding to #tempfile" do
before do
Paperclip::UploadedFileAdapter.content_type_detector = nil
class UploadedFile < OpenStruct; end
tempfile = File.new(fixture_file("5k.png"))
tempfile.binmode
@file = UploadedFile.new(
original_filename: "5k.png",
content_type: "image/x-png-by-browser\r",
head: "",
tempfile: tempfile,
path: tempfile.path
)
@subject = Paperclip.io_adapters.for(@file, hash_digest: Digest::MD5)
end
it "gets the right filename" do
assert_equal "5k.png", @subject.original_filename
end
it "forces binmode on tempfile" do
assert @subject.instance_variable_get("@tempfile").binmode?
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
it "gets the file's size" do
assert_equal 4456, @subject.size
end
it "returns false for a call to nil?" do
assert ! @subject.nil?
end
it "generates a MD5 hash of the contents" do
expected = Digest::MD5.file(@file.tempfile.path).to_s
assert_equal expected, @subject.fingerprint
end
it "reads the contents of the file" do
expected = @file.tempfile.read
assert expected.length > 0
assert_equal expected, @subject.read
end
end
context "with UploadedFile that has restricted characters" do
before do
Paperclip::UploadedFileAdapter.content_type_detector = nil
class UploadedFile < OpenStruct; end
@file = UploadedFile.new(
original_filename: "image:restricted.gif",
content_type: "image/x-png-by-browser",
head: "",
path: fixture_file("5k.png")
)
@subject = Paperclip.io_adapters.for(@file, hash_digest: Digest::MD5)
end
it "does not generate paths that include restricted characters" do
expect(@subject.path).to_not match(/:/)
end
it "does not generate filenames that include restricted characters" do
assert_equal 'image_restricted.gif', @subject.original_filename
end
end
context "with UploadFile responding to #path" do
before do
Paperclip::UploadedFileAdapter.content_type_detector = nil
class UploadedFile < OpenStruct; end
@file = UploadedFile.new(
original_filename: "5k.png",
content_type: "image/x-png-by-browser",
head: "",
path: fixture_file("5k.png")
)
@subject = Paperclip.io_adapters.for(@file, hash_digest: Digest::MD5)
end
it "gets the right filename" do
assert_equal "5k.png", @subject.original_filename
end
it "forces binmode on tempfile" do
assert @subject.instance_variable_get("@tempfile").binmode?
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
it "gets the file's size" do
assert_equal 4456, @subject.size
end
it "returns false for a call to nil?" do
assert ! @subject.nil?
end
it "generates a MD5 hash of the contents" do
expected = Digest::MD5.file(@file.path).to_s
assert_equal expected, @subject.fingerprint
end
it "reads the contents of the file" do
expected_file = File.new(@file.path)
expected_file.binmode
expected = expected_file.read
assert expected.length > 0
assert_equal expected, @subject.read
end
context "don't trust client-given MIME type" do
before do
Paperclip::UploadedFileAdapter.content_type_detector =
Paperclip::FileCommandContentTypeDetector
class UploadedFile < OpenStruct; end
@file = UploadedFile.new(
original_filename: "5k.png",
content_type: "image/x-png-by-browser",
head: "",
path: fixture_file("5k.png")
)
@subject = Paperclip.io_adapters.for(@file)
end
it "gets the content type" do
assert_equal "image/png", @subject.content_type
end
end
end
end
end
================================================
FILE: spec/paperclip/io_adapters/uri_adapter_spec.rb
================================================
require 'spec_helper'
describe Paperclip::UriAdapter do
let(:content_type) { "image/png" }
let(:meta) { {} }
before do
@open_return = StringIO.new("xxx")
@open_return.stubs(:content_type).returns(content_type)
@open_return.stubs(:meta).returns(meta)
Paperclip::UriAdapter.register
end
after do
Paperclip.io_adapters.unregister(described_class)
end
context "a new instance" do
let(:meta) { { "content-type" => "image/png" } }
before do
Paperclip::UriAdapter.any_instance.
stubs(:download_content).returns(@open_return)
@uri = URI.parse("http://thoughtbot.com/images/thoughtbot-logo.png")
@subject = Paperclip.io_adapters.for(@uri, hash_digest: Digest::MD5)
end
it "returns a file name" do
assert_equal "thoughtbot-logo.png", @subject.original_filename
end
it 'closes open handle after reading' do
assert_equal true, @open_return.closed?
end
it "returns a content type" do
assert_equal "image/png", @subject.content_type
end
it "returns the size of the data" do
assert_equal @open_return.size, @subject.size
end
it "generates an MD5 hash of the contents" do
assert_equal Digest::MD5.hexdigest("xxx"), @subject.fingerprint
end
it "generates correct fingerprint after read" do
fingerprint = Digest::MD5.hexdigest(@subject.read)
assert_equal fingerprint, @subject.fingerprint
end
it "generates same fingerprint" do
assert_equal @subject.fingerprint, @subject.fingerprint
end
it "returns the data contained in the StringIO" do
assert_equal "xxx", @subject.read
end
it 'accepts a content_type' do
@subject.content_type = 'image/png'
assert_equal 'image/png', @subject.content_type
end
it "accepts an original_filename" do
@subject.original_filename = 'image.png'
assert_equal 'image.png', @subject.original_filename
end
end
context "a directory index url" do
let(:content_type) { "text/html" }
let(:meta) { { "content-type" => "text/html" } }
before do
Paperclip::UriAdapter.any_instance.
stubs(:download_content).returns(@open_return)
@uri = URI.parse("http://thoughtbot.com")
@subject = Paperclip.io_adapters.for(@uri)
end
it "returns a file name" do
assert_equal "index.html", @subject.original_filename
end
it "returns a content type" do
assert_equal "text/html", @subject.content_type
end
end
context "a url with query params" do
before do
Paperclip::UriAdapter.any_instance.
stubs(:download_content).returns(@open_return)
@uri = URI.parse("https://github.com/thoughtbot/paperclip?file=test")
@subject = Paperclip.io_adapters.for(@uri)
end
it "returns a file name" do
assert_equal "paperclip", @subject.original_filename
end
end
context "a url with content disposition headers" do
let(:file_name) { "test_document.pdf" }
let(:filename_from_path) { "paperclip" }
before do
Paperclip::UriAdapter.any_instance.
stubs(:download_content).returns(@open_return)
@uri = URI.parse(
"https://github.com/thoughtbot/#{filename_from_path}?file=test")
end
it "returns file name from path" do
meta["content-disposition"] = "inline;"
@subject = Paperclip.io_adapters.for(@uri)
assert_equal filename_from_path, @subject.original_filename
end
it "returns a file name enclosed in double quotes" do
file_name = "john's test document.pdf"
meta["content-disposition"] = "attachment; filename=\"#{file_name}\";"
@subject = Paperclip.io_adapters.for(@uri)
assert_equal file_name, @subject.original_filename
end
it "returns a file name not enclosed in double quotes" do
meta["content-disposition"] = "ATTACHMENT; FILENAME=#{file_name};"
@subject = Paperclip.io_adapters.for(@uri)
assert_equal file_name, @subject.original_filename
end
it "does not crash when an empty filename is given" do
meta["content-disposition"] = "ATTACHMENT; FILENAME=\"\";"
@subject = Paperclip.io_adapters.for(@uri)
assert_equal "", @subject.original_filename
end
it "returns a file name ignoring RFC 5987 encoding" do
meta["content-disposition"] =
"attachment; filename=#{file_name}; filename* = utf-8''%e2%82%ac%20rates"
@subject = Paperclip.io_adapters.for(@uri)
assert_equal file_name, @subject.original_filename
end
context "when file name has consecutive periods" do
let(:file_name) { "test_document..pdf" }
it "returns a file name" do
@uri = URI.parse(
"https://github.com/thoughtbot/#{file_name}?file=test")
@subject = Paperclip.io_adapters.for(@uri)
assert_equal file_name, @subject.original_filename
end
end
end
context "a url with restricted characters in the filename" do
before do
Paperclip::UriAdapter.any_instance.
stubs(:download_content).returns(@open_return)
@uri = URI.parse("https://github.com/thoughtbot/paper:clip.jpg")
@subject = Paperclip.io_adapters.for(@uri)
end
it "does not generate filenames that include restricted characters" do
assert_equal "paper_clip.jpg", @subject.original_filename
end
it "does not generate paths that include restricted characters" do
expect(@subject.path).to_not match(/:/)
end
end
describe "#download_content" do
before do
Paperclip::UriAdapter.any_instance.stubs(:open).returns(@open_return)
@uri = URI.parse("https://github.com/thoughtbot/paper:clip.jpg")
@subject = Paperclip.io_adapters.for(@uri)
end
after do
@subject.send(:download_content)
end
context "with default read_timeout" do
it "calls open without options" do
@subject.expects(:open).with(@uri, {}).at_least_once
end
end
context "with custom read_timeout" do
before do
Paperclip.options[:read_timeout] = 120
end
it "calls open with read_timeout option" do
@subject.expects(:open).with(@uri, read_timeout: 120).at_least_once
end
end
end
end
================================================
FILE: spec/paperclip/matchers/have_attached_file_matcher_spec.rb
================================================
require 'spec_helper'
require 'paperclip/matchers'
describe Paperclip::Shoulda::Matchers::HaveAttachedFileMatcher do
extend Paperclip::Shoulda::Matchers
it "rejects the dummy class if it has no attachment" do
reset_table "dummies"
reset_class "Dummy"
matcher = self.class.have_attached_file(:avatar)
expect(matcher).to_not accept(Dummy)
end
it 'accepts the dummy class if it has an attachment' do
rebuild_model
matcher = self.class.have_attached_file(:avatar)
expect(matcher).to accept(Dummy)
end
end
================================================
FILE: spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb
================================================
require 'spec_helper'
require 'paperclip/matchers'
describe Paperclip::Shoulda::Matchers::ValidateAttachmentContentTypeMatcher do
extend Paperclip::Shoulda::Matchers
before do
reset_table("dummies") do |d|
d.string :title
d.string :avatar_file_name
d.string :avatar_content_type
end
reset_class "Dummy"
Dummy.do_not_validate_attachment_file_type :avatar
Dummy.has_attached_file :avatar
end
it "rejects a class with no validation" do
expect(matcher).to_not accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it 'rejects a class when the validation fails' do
Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*}
expect(matcher).to_not accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "accepts a class with a matching validation" do
Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*}
expect(matcher).to accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "accepts a class with other validations but matching types" do
Dummy.validates_presence_of :title
Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*}
expect(matcher).to accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "accepts a class that matches and a matcher that only specifies 'allowing'" do
Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*}
matcher = plain_matcher.allowing(%w(image/png image/jpeg))
expect(matcher).to accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "rejects a class that does not match and a matcher that only specifies 'allowing'" do
Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*}
matcher = plain_matcher.allowing(%w(image/png image/jpeg))
expect(matcher).to_not accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "accepts a class that matches and a matcher that only specifies 'rejecting'" do
Dummy.validates_attachment_content_type :avatar, content_type: %r{image/.*}
matcher = plain_matcher.rejecting(%w(audio/mp3 application/octet-stream))
expect(matcher).to accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "rejects a class that does not match and a matcher that only specifies 'rejecting'" do
Dummy.validates_attachment_content_type :avatar, content_type: %r{audio/.*}
matcher = plain_matcher.rejecting(%w(audio/mp3 application/octet-stream))
expect(matcher).to_not accept(Dummy)
expect { matcher.failure_message }.to_not raise_error
end
context "using an :if to control the validation" do
before do
Dummy.class_eval do
validates_attachment_content_type :avatar, content_type: %r{image/*} , if: :go
attr_accessor :go
end
end
it "runs the validation if the control is true" do
dummy = Dummy.new
dummy.go = true
expect(matcher).to accept(dummy)
expect { matcher.failure_message }.to_not raise_error
end
it "does not run the validation if the control is false" do
dummy = Dummy.new
dummy.go = false
expect(matcher).to_not accept(dummy)
expect { matcher.failure_message }.to_not raise_error
end
end
private
def plain_matcher
self.class.validate_attachment_content_type(:avatar)
end
def matcher
plain_matcher.
allowing(%w(image/png image/jpeg)).
rejecting(%w(audio/mp3 application/octet-stream))
end
end
================================================
FILE: spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb
================================================
require 'spec_helper'
require 'paperclip/matchers'
describe Paperclip::Shoulda::Matchers::ValidateAttachmentPresenceMatcher do
extend Paperclip::Shoulda::Matchers
before do
reset_table("dummies") do |d|
d.string :avatar_file_name
end
reset_class "Dummy"
Dummy.has_attached_file :avatar
Dummy.do_not_validate_attachment_file_type :avatar
end
it "rejects a class with no validation" do
expect(matcher).to_not accept(Dummy)
end
it "accepts a class with a matching validation" do
Dummy.validates_attachment_presence :avatar
expect(matcher).to accept(Dummy)
end
it "accepts an instance with other attachment validations" do
reset_table("dummies") do |d|
d.string :avatar_file_name
d.string :avatar_content_type
end
Dummy.class_eval do
validates_attachment_presence :avatar
validates_attachment_content_type :avatar, content_type: 'image/gif'
end
dummy = Dummy.new
dummy.avatar = File.new fixture_file('5k.png')
expect(matcher).to accept(dummy)
end
context "using an :if to control the validation" do
before do
Dummy.class_eval do
validates_attachment_presence :avatar, if: :go
attr_accessor :go
end
end
it "runs the validation if the control is true" do
dummy = Dummy.new
dummy.avatar = nil
dummy.go = true
expect(matcher).to accept(dummy)
end
it "does not run the validation if the control is false" do
dummy = Dummy.new
dummy.avatar = nil
dummy.go = false
expect(matcher).to_not accept(dummy)
end
end
private
def matcher
self.class.validate_attachment_presence(:avatar)
end
end
================================================
FILE: spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb
================================================
require 'spec_helper'
require 'paperclip/matchers'
describe Paperclip::Shoulda::Matchers::ValidateAttachmentSizeMatcher do
extend Paperclip::Shoulda::Matchers
before do
reset_table("dummies") do |d|
d.string :avatar_file_name
d.bigint :avatar_file_size
end
reset_class "Dummy"
Dummy.do_not_validate_attachment_file_type :avatar
Dummy.has_attached_file :avatar
end
context "Limiting size" do
it "rejects a class with no validation" do
expect(matcher.in(256..1024)).to_not accept(Dummy)
end
it "rejects a class with a validation that's too high" do
Dummy.validates_attachment_size :avatar, in: 256..2048
expect(matcher.in(256..1024)).to_not accept(Dummy)
end
it "accepts a class with a validation that's too low" do
Dummy.validates_attachment_size :avatar, in: 0..1024
expect(matcher.in(256..1024)).to_not accept(Dummy)
end
it "accepts a class with a validation that matches" do
Dummy.validates_attachment_size :avatar, in: 256..1024
expect(matcher.in(256..1024)).to accept(Dummy)
end
end
context "allowing anything" do
it "given a class with an upper limit" do
Dummy.validates_attachment_size :avatar, less_than: 1
expect(matcher).to accept(Dummy)
end
it "given a class with a lower limit" do
Dummy.validates_attachment_size :avatar, greater_than: 1
expect(matcher).to accept(Dummy)
end
end
context "using an :if to control the validation" do
before do
Dummy.class_eval do
validates_attachment_size :avatar, greater_than: 1024, if: :go
attr_accessor :go
end
end
it "run the validation if the control is true" do
dummy = Dummy.new
dummy.go = true
expect(matcher.greater_than(1024)).to accept(dummy)
end
it "not run the validation if the control is false" do
dummy = Dummy.new
dummy.go = false
expect(matcher.greater_than(1024)).to_not accept(dummy)
end
end
context "post processing" do
before do
Dummy.validates_attachment_size :avatar, greater_than: 1024
end
it "be skipped" do
dummy = Dummy.new
dummy.avatar.expects(:post_process).never
expect(matcher.greater_than(1024)).to accept(dummy)
end
end
private
def matcher
self.class.validate_attachment_size(:avatar)
end
end
================================================
FILE: spec/paperclip/media_type_spoof_detector_spec.rb
================================================
require 'spec_helper'
describe Paperclip::MediaTypeSpoofDetector do
it 'rejects a file that is named .html and identifies as PNG' do
file = File.open(fixture_file("5k.png"))
assert Paperclip::MediaTypeSpoofDetector.using(file, "5k.html", "image/png").spoofed?
end
it 'does not reject a file that is named .jpg and identifies as PNG' do
file = File.open(fixture_file("5k.png"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "5k.jpg", "image/png").spoofed?
end
it 'does not reject a file that is named .html and identifies as HTML' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "text/html").spoofed?
end
it 'does not reject a file that does not have a name' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "", "text/html").spoofed?
end
it 'does not reject a file that does have an extension' do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "data", "text/html").spoofed?
end
it 'does not reject when the supplied file is an IOAdapter' do
adapter = Paperclip.io_adapters.for(File.new(fixture_file("5k.png")))
assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename, adapter.content_type).spoofed?
end
it 'does not reject when the extension => content_type is in :content_type_mappings' do
begin
Paperclip.options[:content_type_mappings] = { pem: "text/plain" }
file = Tempfile.open(["test", ".PEM"])
file.puts "Certificate!"
file.close
adapter = Paperclip.io_adapters.for(File.new(file.path));
assert ! Paperclip::MediaTypeSpoofDetector.using(adapter, adapter.original_filename, adapter.content_type).spoofed?
ensure
Paperclip.options[:content_type_mappings] = {}
end
end
context "file named .html and is as HTML, but we're told JPG" do
let(:file) { File.open(fixture_file("empty.html")) }
let(:spoofed?) { Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "image/jpg").spoofed? }
it "rejects the file" do
assert spoofed?
end
it "logs info about the detected spoof" do
Paperclip.expects(:log).with('Content Type Spoof: Filename empty.html (image/jpg from Headers, ["text/html"] from Extension), content type discovered from file command: text/html. See documentation to allow this combination.')
spoofed?
end
end
context "GIF file named without extension, but we're told GIF" do
let(:file) { File.open(fixture_file("animated")) }
let(:spoofed?) do
Paperclip::MediaTypeSpoofDetector.
using(file, "animated", "image/gif").
spoofed?
end
it "accepts the file" do
assert !spoofed?
end
end
context "GIF file named without extension, but we're told HTML" do
let(:file) { File.open(fixture_file("animated")) }
let(:spoofed?) do
Paperclip::MediaTypeSpoofDetector.
using(file, "animated", "text/html").
spoofed?
end
it "rejects the file" do
assert spoofed?
end
end
it "does not reject if content_type is empty but otherwise checks out" do
file = File.open(fixture_file("empty.html"))
assert ! Paperclip::MediaTypeSpoofDetector.using(file, "empty.html", "").spoofed?
end
it 'does allow array as :content_type_mappings' do
begin
Paperclip.options[:content_type_mappings] = {
html: ['binary', 'text/html']
}
file = File.open(fixture_file('empty.html'))
spoofed = Paperclip::MediaTypeSpoofDetector
.using(file, "empty.html", "text/html").spoofed?
assert !spoofed
ensure
Paperclip.options[:content_type_mappings] = {}
end
end
context "#type_from_file_command" do
let(:file) { File.new(fixture_file("empty.html")) }
let(:detector) { Paperclip::MediaTypeSpoofDetector.new(file, "html", "") }
it "does work with the output of old versions of file" do
Paperclip.stubs(:run).returns("text/html charset=us-ascii")
expect(detector.send(:type_from_file_command)).to eq("text/html")
end
it "does work with the output of new versions of file" do
Paperclip.stubs(:run).returns("text/html; charset=us-ascii")
expect(detector.send(:type_from_file_command)).to eq("text/html")
end
end
end
================================================
FILE: spec/paperclip/meta_class_spec.rb
================================================
require 'spec_helper'
describe 'Metaclasses' do
context "A meta-class of dummy" do
if active_support_version >= "4.1" || ruby_version < "2.1"
before do
rebuild_model
reset_class("Dummy")
end
it "is able to use Paperclip like a normal class" do
@dummy = Dummy.new
assert_nothing_raised do
rebuild_meta_class_of(@dummy)
end
end
it "works like any other instance" do
@dummy = Dummy.new
rebuild_meta_class_of(@dummy)
assert_nothing_raised do
@dummy.avatar = File.new(fixture_file("5k.png"), 'rb')
end
assert @dummy.save
end
end
end
end
================================================
FILE: spec/paperclip/paperclip_missing_attachment_styles_spec.rb
================================================
require 'spec_helper'
describe 'Missing Attachment Styles' do
before do
Paperclip::AttachmentRegistry.clear
end
after do
File.unlink(Paperclip.registered_attachments_styles_path) rescue nil
end
it "enables to get and set path to registered styles file" do
assert_equal ROOT.join('tmp/public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path
Paperclip.registered_attachments_styles_path = '/tmp/config/paperclip_attachments.yml'
assert_equal '/tmp/config/paperclip_attachments.yml', Paperclip.registered_attachments_styles_path
Paperclip.registered_attachments_styles_path = nil
assert_equal ROOT.join('tmp/public/system/paperclip_attachments.yml').to_s, Paperclip.registered_attachments_styles_path
end
it "is able to get current attachment styles" do
assert_equal Hash.new, Paperclip.send(:current_attachments_styles)
rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'}
expected_hash = { Dummy: {avatar: [:big, :croppable]}}
assert_equal expected_hash, Paperclip.send(:current_attachments_styles)
end
it "is able to save current attachment styles for further comparison" do
rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'}
Paperclip.save_current_attachments_styles!
expected_hash = { Dummy: {avatar: [:big, :croppable]}}
assert_equal expected_hash, YAML.load_file(Paperclip.registered_attachments_styles_path)
end
it "is able to read registered attachment styles from file" do
rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'}
Paperclip.save_current_attachments_styles!
expected_hash = { Dummy: {avatar: [:big, :croppable]}}
assert_equal expected_hash, Paperclip.send(:get_registered_attachments_styles)
end
it "is able to calculate differences between registered styles and current styles" do
rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'}
Paperclip.save_current_attachments_styles!
rebuild_model styles: {thumb: 'x100', export: 'x400>', croppable: '600x600>', big: '1000x1000>'}
expected_hash = { Dummy: {avatar: [:export, :thumb]} }
assert_equal expected_hash, Paperclip.missing_attachments_styles
ActiveRecord::Base.connection.create_table :books, force: true
class ::Book < ActiveRecord::Base
has_attached_file :cover, styles: {small: 'x100', large: '1000x1000>'}
has_attached_file :sample, styles: {thumb: 'x100'}
end
expected_hash = {
Dummy: {avatar: [:export, :thumb]},
Book: {sample: [:thumb], cover: [:large, :small]}
}
assert_equal expected_hash, Paperclip.missing_attachments_styles
Paperclip.save_current_attachments_styles!
assert_equal Hash.new, Paperclip.missing_attachments_styles
end
it "is able to calculate differences when a new attachment is added to a model" do
rebuild_model styles: {croppable: '600x600>', big: '1000x1000>'}
Paperclip.save_current_attachments_styles!
class ::Dummy
has_attached_file :photo, styles: {small: 'x100', large: '1000x1000>'}
end
expected_hash = {
Dummy: {photo: [:large, :small]}
}
assert_equal expected_hash, Paperclip.missing_attachments_styles
Paperclip.save_current_attachments_styles!
assert_equal Hash.new, Paperclip.missing_attachments_styles
end
# It's impossible to build styles hash without loading from database whole bunch of records
it "skips lambda-styles" do
rebuild_model styles: lambda{ |attachment| attachment.instance.other == 'a' ? {thumb: "50x50#"} : {large: "400x400"} }
assert_equal Hash.new, Paperclip.send(:current_attachments_styles)
end
end
================================================
FILE: spec/paperclip/paperclip_spec.rb
================================================
require 'spec_helper'
describe Paperclip do
context ".run" do
before do
Paperclip.options[:log_command] = false
Terrapin::CommandLine.expects(:new).with("convert", "stuff", {}).returns(stub(:run))
@original_command_line_path = Terrapin::CommandLine.path
end
after do
Paperclip.options[:log_command] = true
Terrapin::CommandLine.path = @original_command_line_path
end
it "runs the command with Terrapin" do
Paperclip.run("convert", "stuff")
end
it "saves Terrapin::CommandLine.path that set before" do
Terrapin::CommandLine.path = "/opt/my_app/bin"
Paperclip.run("convert", "stuff")
expect(Terrapin::CommandLine.path).to match("/opt/my_app/bin")
end
it "does not duplicate Terrapin::CommandLine.path on multiple runs" do
Terrapin::CommandLine.expects(:new).with("convert", "more_stuff", {}).returns(stub(:run))
Terrapin::CommandLine.path = nil
Paperclip.options[:command_path] = "/opt/my_app/bin"
Paperclip.run("convert", "stuff")
Paperclip.run("convert", "more_stuff")
cmd_path = Paperclip.options[:command_path]
assert_equal 1, Terrapin::CommandLine.path.scan(cmd_path).count
end
end
it 'does not raise errors when doing a lot of running' do
Paperclip.options[:command_path] = ["/usr/local/bin"] * 1024
Terrapin::CommandLine.path = "/something/else"
100.times do |x|
Paperclip.run("echo", x.to_s)
end
end
context "Calling Paperclip.log without options[:logger] set" do
before do
Paperclip.logger = nil
Paperclip.options[:logger] = nil
end
after do
Paperclip.options[:logger] = ActiveRecord::Base.logger
Paperclip.logger = ActiveRecord::Base.logger
end
it "does not raise an error when log is called" do
silence_stream(STDOUT) do
Paperclip.log('something')
end
end
end
context "Calling Paperclip.run with a logger" do
it "passes the defined logger if :log_command is set" do
Paperclip.options[:log_command] = true
Terrapin::CommandLine.expects(:new).with("convert", "stuff", logger: Paperclip.logger).returns(stub(:run))
Paperclip.run("convert", "stuff")
end
end
context "Paperclip.each_instance_with_attachment" do
before do
@file = File.new(fixture_file("5k.png"), 'rb')
d1 = Dummy.create(avatar: @file)
d2 = Dummy.create
d3 = Dummy.create(avatar: @file)
@expected = [d1, d3]
end
after { @file.close }
it "yields every instance of a model that has an attachment" do
actual = []
Paperclip.each_instance_with_attachment("Dummy", "avatar") do |instance|
actual << instance
end
expect(actual).to match_array @expected
end
end
it "raises when sent #processor and the name of a class that doesn't exist" do
assert_raises(LoadError){ Paperclip.processor(:boogey_man) }
end
it "returns a class when sent #processor and the name of a class under Paperclip" do
assert_equal ::Paperclip::Thumbnail, Paperclip.processor(:thumbnail)
end
it "gets a class from a namespaced class name" do
class ::One; class Two; end; end
assert_equal ::One::Two, Paperclip.class_for("One::Two")
end
it "raises when class doesn't exist in specified namespace" do
class ::Three; end
class ::Four; end
assert_raises NameError do
Paperclip.class_for("Three::Four")
end
end
context "An ActiveRecord model with an 'avatar' attachment" do
before do
rebuild_model path: "tmp/:class/omg/:style.:extension"
@file = File.new(fixture_file("5k.png"), 'rb')
end
after { @file.close }
it "does not error when trying to also create a 'blah' attachment" do
assert_nothing_raised do
Dummy.class_eval do
has_attached_file :blah
end
end
end
context "with a subclass" do
before do
class ::SubDummy < Dummy; end
end
it "is able to use the attachment from the subclass" do
assert_nothing_raised do
@subdummy = SubDummy.create(avatar: @file)
end
end
after do
SubDummy.delete_all
Object.send(:remove_const, "SubDummy") rescue nil
end
end
it "has an avatar getter method" do
assert Dummy.new.respond_to?(:avatar)
end
it "has an avatar setter method" do
assert Dummy.new.respond_to?(:avatar=)
end
context "that is valid" do
before do
@dummy = Dummy.new
@dummy.avatar = @file
end
it "is valid" do
assert @dummy.valid?
end
end
it "does not have Attachment in the ActiveRecord::Base namespace" do
assert_raises(NameError) do
ActiveRecord::Base::Attachment
end
end
end
context "configuring a custom processor" do
before do
@freedom_processor = Class.new do
def make(file, options = {}, attachment = nil)
file
end
end.new
Paperclip.configure do |config|
config.register_processor(:freedom, @freedom_processor)
end
end
it "is able to find the custom processor" do
assert_equal @freedom_processor, Paperclip.processor(:freedom)
end
after do
Paperclip.clear_processors!
end
end
end
================================================
FILE: spec/paperclip/plural_cache_spec.rb
================================================
require 'spec_helper'
describe 'Plural cache' do
it 'caches pluralizations' do
cache = Paperclip::Interpolations::PluralCache.new
symbol = :box
first = cache.pluralize_symbol(symbol)
second = cache.pluralize_symbol(symbol)
expect(first).to equal(second)
end
it 'caches pluralizations and underscores' do
class BigBox ; end
cache = Paperclip::Interpolations::PluralCache.new
klass = BigBox
first = cache.underscore_and_pluralize_class(klass)
second = cache.underscore_and_pluralize_class(klass)
expect(first).to equal(second)
end
it 'pluralizes words' do
cache = Paperclip::Interpolations::PluralCache.new
symbol = :box
expect(cache.pluralize_symbol(symbol)).to eq("boxes")
end
it 'pluralizes and underscore class names' do
class BigBox ; end
cache = Paperclip::Interpolations::PluralCache.new
klass = BigBox
expect(cache.underscore_and_pluralize_class(klass)).to eq("big_boxes")
end
end
================================================
FILE: spec/paperclip/processor_helpers_spec.rb
================================================
require 'spec_helper'
describe Paperclip::ProcessorHelpers do
describe '.load_processor' do
context 'when the file exists in lib/paperclip' do
it 'loads it correctly' do
pathname = Pathname.new('my_app')
main_path = 'main_path'
alternate_path = 'alternate_path'
Rails.stubs(:root).returns(pathname)
File.expects(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path)
File.expects(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path)
File.expects(:exist?).with(main_path).returns(true)
File.expects(:exist?).with(alternate_path).returns(false)
Paperclip.expects(:require).with(main_path)
Paperclip.load_processor(:custom)
end
end
context 'when the file exists in lib/paperclip_processors' do
it 'loads it correctly' do
pathname = Pathname.new('my_app')
main_path = 'main_path'
alternate_path = 'alternate_path'
Rails.stubs(:root).returns(pathname)
File.expects(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path)
File.expects(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path)
File.expects(:exist?).with(main_path).returns(false)
File.expects(:exist?).with(alternate_path).returns(true)
Paperclip.expects(:require).with(alternate_path)
Paperclip.load_processor(:custom)
end
end
context 'when the file does not exist in lib/paperclip_processors' do
it 'raises an error' do
pathname = Pathname.new('my_app')
main_path = 'main_path'
alternate_path = 'alternate_path'
Rails.stubs(:root).returns(pathname)
File.stubs(:expand_path).with(pathname.join('lib/paperclip', 'custom.rb')).returns(main_path)
File.stubs(:expand_path).with(pathname.join('lib/paperclip_processors', 'custom.rb')).returns(alternate_path)
File.stubs(:exist?).with(main_path).returns(false)
File.stubs(:exist?).with(alternate_path).returns(false)
assert_raises(LoadError) { Paperclip.processor(:custom) }
end
end
end
end
================================================
FILE: spec/paperclip/processor_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Processor do
it "instantiates and call #make when sent #make to the class" do
processor = mock
processor.expects(:make).with()
Paperclip::Processor.expects(:new).with(:one, :two, :three).returns(processor)
Paperclip::Processor.make(:one, :two, :three)
end
context "Calling #convert" do
it "runs the convert command with Terrapin" do
Paperclip.options[:log_command] = false
Terrapin::CommandLine.expects(:new).with("convert", "stuff", {}).returns(stub(:run))
Paperclip::Processor.new('filename').convert("stuff")
end
end
context "Calling #identify" do
it "runs the identify command with Terrapin" do
Paperclip.options[:log_command] = false
Terrapin::CommandLine.expects(:new).with("identify", "stuff", {}).returns(stub(:run))
Paperclip::Processor.new('filename').identify("stuff")
end
end
end
================================================
FILE: spec/paperclip/rails_environment_spec.rb
================================================
require 'spec_helper'
describe Paperclip::RailsEnvironment do
it "returns nil when Rails isn't defined" do
resetting_rails_to(nil) do
expect(Paperclip::RailsEnvironment.get).to be_nil
end
end
it "returns nil when Rails.env isn't defined" do
resetting_rails_to({}) do
expect(Paperclip::RailsEnvironment.get).to be_nil
end
end
it "returns the value of Rails.env if it is set" do
resetting_rails_to(OpenStruct.new(env: "foo")) do
expect(Paperclip::RailsEnvironment.get).to eq "foo"
end
end
def resetting_rails_to(new_value)
begin
previous_rails = Object.send(:remove_const, "Rails")
Object.const_set("Rails", new_value) unless new_value.nil?
yield
ensure
Object.send(:remove_const, "Rails") if Object.const_defined?("Rails")
Object.const_set("Rails", previous_rails)
end
end
end
================================================
FILE: spec/paperclip/rake_spec.rb
================================================
require 'spec_helper'
require 'rake'
load './lib/tasks/paperclip.rake'
describe Rake do
context "calling `rake paperclip:refresh:thumbnails`" do
before do
rebuild_model
Paperclip::Task.stubs(:obtain_class).returns('Dummy')
@bogus_instance = Dummy.new
@bogus_instance.id = 'some_id'
@bogus_instance.avatar.stubs(:reprocess!)
@valid_instance = Dummy.new
@valid_instance.avatar.stubs(:reprocess!)
Paperclip::Task.stubs(:log_error)
Paperclip.stubs(:each_instance_with_attachment).multiple_yields @bogus_instance, @valid_instance
end
context "when there is an exception in reprocess!" do
before do
@bogus_instance.avatar.stubs(:reprocess!).raises
end
it "catches the exception" do
assert_nothing_raised do
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
end
it "continues to the next instance" do
@valid_instance.avatar.expects(:reprocess!)
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the exception" do
exception_msg = 'Some Exception'
@bogus_instance.avatar.stubs(:reprocess!).raises(exception_msg)
Paperclip::Task.expects(:log_error).with do |str|
str.match exception_msg
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the class name" do
Paperclip::Task.expects(:log_error).with do |str|
str.match 'Dummy'
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the instance ID" do
Paperclip::Task.expects(:log_error).with do |str|
str.match "ID #{@bogus_instance.id}"
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
end
context "when there is an error in reprocess!" do
before do
@errors = mock('errors')
@errors.stubs(:full_messages).returns([''])
@errors.stubs(:blank?).returns(false)
@bogus_instance.stubs(:errors).returns(@errors)
end
it "continues to the next instance" do
@valid_instance.avatar.expects(:reprocess!)
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the error" do
error_msg = 'Some Error'
@errors.stubs(:full_messages).returns([error_msg])
Paperclip::Task.expects(:log_error).with do |str|
str.match error_msg
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the class name" do
Paperclip::Task.expects(:log_error).with do |str|
str.match 'Dummy'
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
it "prints the instance ID" do
Paperclip::Task.expects(:log_error).with do |str|
str.match "ID #{@bogus_instance.id}"
end
::Rake::Task['paperclip:refresh:thumbnails'].execute
end
end
end
context "Paperclip::Task.log_error method" do
it "prints its argument to STDERR" do
msg = 'Some Message'
$stderr.expects(:puts).with(msg)
Paperclip::Task.log_error(msg)
end
end
end
================================================
FILE: spec/paperclip/schema_spec.rb
================================================
require 'spec_helper'
require 'paperclip/schema'
require 'active_support/testing/deprecation'
describe Paperclip::Schema do
include ActiveSupport::Testing::Deprecation
before do
rebuild_class
end
after do
Dummy.connection.drop_table :dummies rescue nil
end
context "within table definition" do
context "using #has_attached_file" do
before do
ActiveSupport::Deprecation.silenced = false
end
it "creates attachment columns" do
Dummy.connection.create_table :dummies, force: true do |t|
ActiveSupport::Deprecation.silence do
t.has_attached_file :avatar
end
end
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to include(['avatar_file_name', "varchar"])
expect(columns).to include(['avatar_content_type', "varchar"])
expect(columns).to include(['avatar_file_size', "bigint"])
expect(columns).to include(['avatar_updated_at', "datetime"])
end
it "displays deprecation warning" do
Dummy.connection.create_table :dummies, force: true do |t|
assert_deprecated do
t.has_attached_file :avatar
end
end
end
end
context "using #attachment" do
before do
Dummy.connection.create_table :dummies, force: true do |t|
t.attachment :avatar
end
end
it "creates attachment columns" do
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to include(['avatar_file_name', "varchar"])
expect(columns).to include(['avatar_content_type', "varchar"])
expect(columns).to include(['avatar_file_size', "bigint"])
expect(columns).to include(['avatar_updated_at', "datetime"])
end
end
context "using #attachment with options" do
before do
Dummy.connection.create_table :dummies, force: true do |t|
t.attachment :avatar, default: 1, file_name: { default: 'default' }
end
end
it "sets defaults on columns" do
defaults_columns = ["avatar_file_name", "avatar_content_type", "avatar_file_size"]
columns = Dummy.columns.select { |e| defaults_columns.include? e.name }
expect(columns).to have_column("avatar_file_name").with_default("default")
expect(columns).to have_column("avatar_content_type").with_default("1")
expect(columns).to have_column("avatar_file_size").with_default(1)
end
end
end
context "within schema statement" do
before do
Dummy.connection.create_table :dummies, force: true
end
context "migrating up" do
context "with single attachment" do
before do
Dummy.connection.add_attachment :dummies, :avatar
end
it "creates attachment columns" do
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to include(['avatar_file_name', "varchar"])
expect(columns).to include(['avatar_content_type', "varchar"])
expect(columns).to include(['avatar_file_size', "bigint"])
expect(columns).to include(['avatar_updated_at', "datetime"])
end
end
context "with single attachment and options" do
before do
Dummy.connection.add_attachment :dummies, :avatar, default: '1', file_name: { default: 'default' }
end
it "sets defaults on columns" do
defaults_columns = ["avatar_file_name", "avatar_content_type", "avatar_file_size"]
columns = Dummy.columns.select { |e| defaults_columns.include? e.name }
expect(columns).to have_column("avatar_file_name").with_default("default")
expect(columns).to have_column("avatar_content_type").with_default("1")
expect(columns).to have_column("avatar_file_size").with_default(1)
end
end
context "with multiple attachments" do
before do
Dummy.connection.add_attachment :dummies, :avatar, :photo
end
it "creates attachment columns" do
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to include(['avatar_file_name', "varchar"])
expect(columns).to include(['avatar_content_type', "varchar"])
expect(columns).to include(['avatar_file_size', "bigint"])
expect(columns).to include(['avatar_updated_at', "datetime"])
expect(columns).to include(['photo_file_name', "varchar"])
expect(columns).to include(['photo_content_type', "varchar"])
expect(columns).to include(['photo_file_size', "bigint"])
expect(columns).to include(['photo_updated_at', "datetime"])
end
end
context "with multiple attachments and options" do
before do
Dummy.connection.add_attachment :dummies, :avatar, :photo, default: '1', file_name: { default: 'default' }
end
it "sets defaults on columns" do
defaults_columns = ["avatar_file_name", "avatar_content_type", "avatar_file_size", "photo_file_name", "photo_content_type", "photo_file_size"]
columns = Dummy.columns.select { |e| defaults_columns.include? e.name }
expect(columns).to have_column("avatar_file_name").with_default("default")
expect(columns).to have_column("avatar_content_type").with_default("1")
expect(columns).to have_column("avatar_file_size").with_default(1)
expect(columns).to have_column("photo_file_name").with_default("default")
expect(columns).to have_column("photo_content_type").with_default("1")
expect(columns).to have_column("photo_file_size").with_default(1)
end
end
context "with no attachment" do
it "raises an error" do
assert_raises ArgumentError do
Dummy.connection.add_attachment :dummies
end
end
end
end
context "migrating down" do
before do
Dummy.connection.change_table :dummies do |t|
t.column :avatar_file_name, :string
t.column :avatar_content_type, :string
t.column :avatar_file_size, :bigint
t.column :avatar_updated_at, :datetime
end
end
context "using #drop_attached_file" do
before do
ActiveSupport::Deprecation.silenced = false
end
it "removes the attachment columns" do
ActiveSupport::Deprecation.silence do
Dummy.connection.drop_attached_file :dummies, :avatar
end
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to_not include(['avatar_file_name', "varchar"])
expect(columns).to_not include(['avatar_content_type', "varchar"])
expect(columns).to_not include(['avatar_file_size', "bigint"])
expect(columns).to_not include(['avatar_updated_at', "datetime"])
end
it "displays a deprecation warning" do
assert_deprecated do
Dummy.connection.drop_attached_file :dummies, :avatar
end
end
end
context "using #remove_attachment" do
context "with single attachment" do
before do
Dummy.connection.remove_attachment :dummies, :avatar
end
it "removes the attachment columns" do
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to_not include(['avatar_file_name', "varchar"])
expect(columns).to_not include(['avatar_content_type', "varchar"])
expect(columns).to_not include(['avatar_file_size', "bigint"])
expect(columns).to_not include(['avatar_updated_at', "datetime"])
end
end
context "with multiple attachments" do
before do
Dummy.connection.change_table :dummies do |t|
t.column :photo_file_name, :string
t.column :photo_content_type, :string
t.column :photo_file_size, :bigint
t.column :photo_updated_at, :datetime
end
Dummy.connection.remove_attachment :dummies, :avatar, :photo
end
it "removes the attachment columns" do
columns = Dummy.columns.map{ |column| [column.name, column.sql_type] }
expect(columns).to_not include(['avatar_file_name', "varchar"])
expect(columns).to_not include(['avatar_content_type', "varchar"])
expect(columns).to_not include(['avatar_file_size', "bigint"])
expect(columns).to_not include(['avatar_updated_at', "datetime"])
expect(columns).to_not include(['photo_file_name', "varchar"])
expect(columns).to_not include(['photo_content_type', "varchar"])
expect(columns).to_not include(['photo_file_size', "bigint"])
expect(columns).to_not include(['photo_updated_at', "datetime"])
end
end
context "with no attachment" do
it "raises an error" do
assert_raises ArgumentError do
Dummy.connection.remove_attachment :dummies
end
end
end
end
end
end
end
================================================
FILE: spec/paperclip/storage/filesystem_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Storage::Filesystem do
context "Filesystem" do
context "normal file" do
before do
rebuild_model styles: { thumbnail: "25x25#" }
@dummy = Dummy.create!
@file = File.open(fixture_file('5k.png'))
@dummy.avatar = @file
end
after { @file.close }
it "allows file assignment" do
assert @dummy.save
end
it "stores the original" do
@dummy.save
assert_file_exists(@dummy.avatar.path)
end
it "stores the thumbnail" do
@dummy.save
assert_file_exists(@dummy.avatar.path(:thumbnail))
end
it "is rewinded after flush_writes" do
@dummy.avatar.instance_eval "def after_flush_writes; end"
files = @dummy.avatar.queued_for_write.values
@dummy.save
assert files.none?(&:eof?), "Expect all the files to be rewinded."
end
it "is removed after after_flush_writes" do
paths = @dummy.avatar.queued_for_write.values.map(&:path)
@dummy.save
assert paths.none?{ |path| File.exist?(path) },
"Expect all the files to be deleted."
end
it 'copies the file to a known location with copy_to_local_file' do
tempfile = Tempfile.new("known_location")
@dummy.avatar.copy_to_local_file(:original, tempfile.path)
tempfile.rewind
assert_equal @file.read, tempfile.read
tempfile.close
end
end
context "with file that has space in file name" do
before do
rebuild_model styles: { thumbnail: "25x25#" }
@dummy = Dummy.create!
@file = File.open(fixture_file('spaced file.png'))
@dummy.avatar = @file
@dummy.save
end
after { @file.close }
it "stores the file" do
assert_file_exists(@dummy.avatar.path)
end
it "returns a replaced version for path" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.path
end
it "returns a replaced version for url" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.url
end
end
end
end
================================================
FILE: spec/paperclip/storage/fog_spec.rb
================================================
require 'spec_helper'
require 'fog/aws'
require 'fog/local'
require 'timecop'
describe Paperclip::Storage::Fog do
context "" do
before { Fog.mock! }
context "with credentials provided in a path string" do
before do
rebuild_model styles: { medium: "300x300>", thumb: "100x100>" },
storage: :fog,
url: '/:attachment/:filename',
fog_directory: "paperclip",
fog_credentials: fixture_file('fog.yml')
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
it "has the proper information loading credentials from a file" do
assert_equal @dummy.avatar.fog_credentials[:provider], 'AWS'
end
end
context "with credentials provided in a File object" do
before do
rebuild_model styles: { medium: "300x300>", thumb: "100x100>" },
storage: :fog,
url: '/:attachment/:filename',
fog_directory: "paperclip",
fog_credentials: File.open(fixture_file('fog.yml'))
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
it "has the proper information loading credentials from a file" do
assert_equal @dummy.avatar.fog_credentials[:provider], 'AWS'
end
end
context "with default values for path and url" do
before do
rebuild_model styles: { medium: "300x300>", thumb: "100x100>" },
storage: :fog,
url: '/:attachment/:filename',
fog_directory: "paperclip",
fog_credentials: {
provider: 'AWS',
aws_access_key_id: 'AWS_ID',
aws_secret_access_key: 'AWS_SECRET'
}
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
it "is able to interpolate the path without blowing up" do
assert_equal File.expand_path(File.join(File.dirname(__FILE__), "../../../tmp/public/avatars/5k.png")),
@dummy.avatar.path
end
end
context "with no path or url given and using defaults" do
before do
rebuild_model styles: { medium: "300x300>", thumb: "100x100>" },
storage: :fog,
fog_directory: "paperclip",
fog_credentials: {
provider: 'AWS',
aws_access_key_id: 'AWS_ID',
aws_secret_access_key: 'AWS_SECRET'
}
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.id = 1
@dummy.avatar = @file
end
after { @file.close }
it "has correct path and url from interpolated defaults" do
assert_equal "dummies/avatars/000/000/001/original/5k.png", @dummy.avatar.path
end
end
context "with file params provided as lambda" do
before do
fog_file = lambda{ |a| { custom_header: a.instance.custom_method }}
klass = rebuild_model storage: :fog,
fog_file: fog_file
klass.class_eval do
def custom_method
'foobar'
end
end
@dummy = Dummy.new
end
it "is able to evaluate correct values for file headers" do
assert_equal @dummy.avatar.send(:fog_file), { custom_header: 'foobar' }
end
end
before do
@fog_directory = 'papercliptests'
@credentials = {
provider: 'AWS',
aws_access_key_id: 'ID',
aws_secret_access_key: 'SECRET'
}
@connection = Fog::Storage.new(@credentials)
@connection.directories.create(
key: @fog_directory
)
@options = {
fog_directory: @fog_directory,
fog_credentials: @credentials,
fog_host: nil,
fog_file: {cache_control: 1234},
path: ":attachment/:basename:dotextension",
storage: :fog
}
rebuild_model(@options)
end
it "is extended by the Fog module" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::Fog)
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after do
@file.close
directory = @connection.directories.new(key: @fog_directory)
directory.files.each {|file| file.destroy}
directory.destroy
end
it "is rewound after flush_writes" do
@dummy.avatar.instance_eval "def after_flush_writes; end"
files = @dummy.avatar.queued_for_write.values
@dummy.save
assert files.none?(&:eof?), "Expect all the files to be rewinded."
end
it "is removed after after_flush_writes" do
paths = @dummy.avatar.queued_for_write.values.map(&:path)
@dummy.save
assert paths.none?{ |path| File.exist?(path) },
"Expect all the files to be deleted."
end
it 'is able to be copied to a local file' do
@dummy.save
tempfile = Tempfile.new("known_location")
tempfile.binmode
@dummy.avatar.copy_to_local_file(:original, tempfile.path)
tempfile.rewind
assert_equal @connection.directories.get(@fog_directory).files.get(@dummy.avatar.path).body,
tempfile.read
tempfile.close
end
it 'is able to be handled when missing while copying to a local file' do
tempfile = Tempfile.new("known_location")
tempfile.binmode
assert_equal false, @dummy.avatar.copy_to_local_file(:original, tempfile.path)
tempfile.close
end
it "passes the content type to the Fog::Storage::AWS::Files instance" do
Fog::Storage::AWS::Files.any_instance.expects(:create).with do |hash|
hash[:content_type]
end
@dummy.save
end
context "without a bucket" do
before do
@connection.directories.get(@fog_directory).destroy
end
it "creates the bucket" do
assert @dummy.save
assert @connection.directories.get(@fog_directory)
end
it "sucessfully rewinds the file during bucket creation" do
assert @dummy.save
expect(Paperclip.io_adapters.for(@dummy.avatar).read.length).to be > 0
end
end
context "with a bucket" do
it "succeeds" do
assert @dummy.save
end
end
context "without a fog_host" do
before do
rebuild_model(@options.merge(fog_host: nil))
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
it "provides a public url" do
assert !@dummy.avatar.url.nil?
end
end
context "with a fog_host" do
before do
rebuild_model(@options.merge(fog_host: 'http://example.com'))
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".\n")
@dummy.save
end
it "provides a public url" do
expect(@dummy.avatar.url).to match(/^http:\/\/example\.com\/avatars\/data\?\d*$/)
end
end
context "with a fog_host that includes a wildcard placeholder" do
before do
rebuild_model(
fog_directory: @fog_directory,
fog_credentials: @credentials,
fog_host: 'http://img%d.example.com',
path: ":attachment/:basename:dotextension",
storage: :fog
)
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".\n")
@dummy.save
end
it "provides a public url" do
expect(@dummy.avatar.url).to match(/^http:\/\/img[0123]\.example\.com\/avatars\/data\?\d*$/)
end
end
context "with fog_public set to false" do
before do
rebuild_model(@options.merge(fog_public: false))
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
it 'sets the @fog_public instance variable to false' do
assert_equal false, @dummy.avatar.instance_variable_get('@options')[:fog_public]
assert_equal false, @dummy.avatar.fog_public
end
end
context "with fog_public as a proc" do
let(:proc) { ->(attachment) { !attachment } }
before do
rebuild_model(@options.merge(fog_public: proc))
@dummy = Dummy.new
@dummy.avatar = StringIO.new(".")
@dummy.save
end
it "sets the @fog_public instance variable to false" do
assert_equal proc, @dummy.avatar.instance_variable_get("@options")[:fog_public]
assert_equal false, @dummy.avatar.fog_public
end
end
context "with styles set and fog_public set to false" do
before do
rebuild_model(@options.merge(fog_public: false, styles: { medium: "300x300>", thumb: "100x100>" }))
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it 'sets the @fog_public for a particular style to false' do
assert_equal false, @dummy.avatar.instance_variable_get('@options')[:fog_public]
assert_equal false, @dummy.avatar.fog_public(:thumb)
end
end
context "with styles set and fog_public set per-style" do
before do
rebuild_model(@options.merge(fog_public: { medium: false, thumb: true}, styles: { medium: "300x300>", thumb: "100x100>" }))
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it 'sets the fog_public for a particular style to correct value' do
assert_equal false, @dummy.avatar.fog_public(:medium)
assert_equal true, @dummy.avatar.fog_public(:thumb)
end
end
context "with fog_public not set" do
before do
rebuild_model(@options)
@dummy = Dummy.new
@dummy.avatar = StringIO.new('.')
@dummy.save
end
it "defaults fog_public to true" do
assert_equal true, @dummy.avatar.fog_public
end
end
context "with scheme set" do
before do
rebuild_model(@options.merge(:fog_credentials => @credentials.merge(:scheme => 'http')))
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "honors the scheme in public url" do
assert_match(/^http:\/\//, @dummy.avatar.url)
end
it "honors the scheme in expiring url" do
assert_match(/^http:\/\//, @dummy.avatar.expiring_url)
end
end
context "with scheme not set" do
before do
rebuild_model(@options)
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "provides HTTPS public url" do
assert_match(/^https:\/\//, @dummy.avatar.url)
end
it "provides HTTPS expiring url" do
assert_match(/^https:\/\//, @dummy.avatar.expiring_url)
end
end
context "with a valid bucket name for a subdomain" do
before { @dummy.stubs(:new_record?).returns(false) }
it "provides an url in subdomain style" do
assert_match(/^https:\/\/papercliptests.s3.amazonaws.com\/avatars\/5k.png/, @dummy.avatar.url)
end
it "provides an url that expires in subdomain style" do
assert_match(/^https:\/\/papercliptests.s3.amazonaws.com\/avatars\/5k.png.+Expires=.+$/, @dummy.avatar.expiring_url)
end
end
context "generating an expiring url" do
it "generates the same url when using Times and Integer offsets" do
Timecop.freeze do
offset = 1234
rebuild_model(@options)
dummy = Dummy.new
dummy.avatar = StringIO.new('.')
assert_equal dummy.avatar.expiring_url(offset),
dummy.avatar.expiring_url(Time.now + offset )
end
end
it 'matches the default url if there is no assignment' do
dummy = Dummy.new
assert_equal dummy.avatar.url, dummy.avatar.expiring_url
end
it 'matches the default url when given a style if there is no assignment' do
dummy = Dummy.new
assert_equal dummy.avatar.url(:thumb), dummy.avatar.expiring_url(3600, :thumb)
end
end
context "with an invalid bucket name for a subdomain" do
before do
rebuild_model(@options.merge(fog_directory: "this_is_invalid"))
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "does not match the bucket-subdomain restrictions" do
invalid_subdomains = %w(this_is_invalid in iamareallylongbucketnameiamareallylongbucketnameiamareallylongbu invalid- inval..id inval-.id inval.-id -invalid 192.168.10.2)
invalid_subdomains.each do |name|
assert_no_match Paperclip::Storage::Fog::AWS_BUCKET_SUBDOMAIN_RESTRICTON_REGEX, name
end
end
it "provides an url in folder style" do
assert_match(/^https:\/\/s3.amazonaws.com\/this_is_invalid\/avatars\/5k.png\?\d*$/, @dummy.avatar.url)
end
it "provides a url that expires in folder style" do
assert_match(/^https:\/\/s3.amazonaws.com\/this_is_invalid\/avatars\/5k.png.+Expires=.+$/, @dummy.avatar.expiring_url)
end
end
context "with a proc for a bucket name evaluating a model method" do
before do
@dynamic_fog_directory = 'dynamicpaperclip'
rebuild_model(@options.merge(fog_directory: lambda { |attachment| attachment.instance.bucket_name }))
@dummy = Dummy.new
@dummy.stubs(:bucket_name).returns(@dynamic_fog_directory)
@dummy.avatar = @file
@dummy.save
end
it "has created the bucket" do
assert @connection.directories.get(@dynamic_fog_directory).inspect
end
it "provides an url using dynamic bucket name" do
assert_match(/^https:\/\/dynamicpaperclip.s3.amazonaws.com\/avatars\/5k.png\?\d*$/, @dummy.avatar.url)
end
end
context "with a proc for the fog_host evaluating a model method" do
before do
rebuild_model(@options.merge(fog_host: lambda { |attachment| attachment.instance.fog_host }))
@dummy = Dummy.new
@dummy.stubs(:fog_host).returns('http://dynamicfoghost.com')
@dummy.avatar = @file
@dummy.save
end
it "provides a public url" do
assert_match(/http:\/\/dynamicfoghost\.com/, @dummy.avatar.url)
end
end
context "with a custom fog_host" do
before do
rebuild_model(@options.merge(fog_host: "http://dynamicfoghost.com"))
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "provides a public url" do
assert_match(/http:\/\/dynamicfoghost\.com/, @dummy.avatar.url)
end
it "provides an expiring url" do
assert_match(/http:\/\/dynamicfoghost\.com/, @dummy.avatar.expiring_url)
end
context "with an invalid bucket name for a subdomain" do
before do
rebuild_model(@options.merge({fog_directory: "this_is_invalid", fog_host: "http://dynamicfoghost.com"}))
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "provides an expiring url" do
assert_match(/http:\/\/dynamicfoghost\.com/, @dummy.avatar.expiring_url)
end
end
end
context "with a proc for the fog_credentials evaluating a model method" do
before do
@dynamic_fog_credentials = {
provider: 'AWS',
aws_access_key_id: 'DYNAMIC_ID',
aws_secret_access_key: 'DYNAMIC_SECRET'
}
rebuild_model(@options.merge(fog_credentials: lambda { |attachment| attachment.instance.fog_credentials }))
@dummy = Dummy.new
@dummy.stubs(:fog_credentials).returns(@dynamic_fog_credentials)
@dummy.avatar = @file
@dummy.save
end
it "provides a public url" do
assert_equal @dummy.avatar.fog_credentials, @dynamic_fog_credentials
end
end
context "with custom fog_options" do
before do
rebuild_model(
@options.merge(fog_options: { multipart_chunk_size: 104857600 }),
)
@dummy = Dummy.new
@dummy.avatar = @file
end
it "applies the options to the fog #create call" do
files = stub
@dummy.avatar.stubs(:directory).returns stub(files: files)
files.expects(:create).with(
has_entries(multipart_chunk_size: 104857600),
)
@dummy.save
end
end
end
end
context "when using local storage" do
before do
Fog.unmock!
rebuild_model styles: { medium: "300x300>", thumb: "100x100>" },
storage: :fog,
url: '/:attachment/:filename',
fog_directory: "paperclip",
fog_credentials: { provider: :local, local_root: "." },
fog_host: 'localhost'
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.stubs(:new_record?).returns(false)
end
after do
@file.close
Fog.mock!
end
it "returns the public url in place of the expiring url" do
assert_match @dummy.avatar.public_url, @dummy.avatar.expiring_url
end
end
end
================================================
FILE: spec/paperclip/storage/s3_live_spec.rb
================================================
require 'spec_helper'
unless ENV["S3_BUCKET"].blank?
describe Paperclip::Storage::S3, 'Live S3' do
context "when assigning an S3 attachment directly to another model" do
before do
rebuild_model styles: { thumb: "100x100", square: "32x32#" },
storage: :s3,
bucket: ENV["S3_BUCKET"],
path: ":class/:attachment/:id/:style.:extension",
s3_region: ENV["S3_REGION"],
s3_credentials: {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
@file = File.new(fixture_file("5k.png"))
end
it "does not raise any error" do
@attachment = Dummy.new.avatar
@attachment.assign(@file)
@attachment.save
@attachment2 = Dummy.new.avatar
@attachment2.assign(@file)
@attachment2.save
end
it "allows assignment from another S3 object" do
@attachment = Dummy.new.avatar
@attachment.assign(@file)
@attachment.save
@attachment2 = Dummy.new.avatar
@attachment2.assign(@attachment)
@attachment2.save
end
after { @file.close }
end
context "Generating an expiring url on a nonexistant attachment" do
before do
rebuild_model styles: { thumb: "100x100", square: "32x32#" },
storage: :s3,
bucket: ENV["S3_BUCKET"],
path: ":class/:attachment/:id/:style.:extension",
s3_region: ENV["S3_REGION"],
s3_credentials: {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
@dummy = Dummy.new
end
it "returns a missing url" do
expect(@dummy.avatar.expiring_url).to eq @dummy.avatar.url
end
end
context "Using S3 for real, an attachment with S3 storage" do
before do
rebuild_model styles: { thumb: "100x100", square: "32x32#" },
storage: :s3,
bucket: ENV["S3_BUCKET"],
path: ":class/:attachment/:id/:style.:extension",
s3_region: ENV["S3_REGION"],
s3_credentials: {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
Dummy.delete_all
@dummy = Dummy.new
end
it "is extended by the S3 module" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy.avatar = @file
end
after do
@file.close
@dummy.destroy
end
context "and saved" do
before do
@dummy.save
end
it "is on S3" do
assert true
end
end
end
end
context "An attachment that uses S3 for storage and has spaces in file name" do
before do
rebuild_model styles: { thumb: "100x100", square: "32x32#" },
storage: :s3,
bucket: ENV["S3_BUCKET"],
s3_region: ENV["S3_REGION"],
url: ":s3_domain_url",
path: "/:class/:attachment/:id_partition/:style/:filename",
s3_credentials: {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
}
Dummy.delete_all
@file = File.new(fixture_file('spaced file.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
it "returns a replaced version for path" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.path
end
it "returns a replaced version for url" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.url
end
it "is accessible" do
assert_success_response @dummy.avatar.url
end
it "is reprocessable" do
assert @dummy.avatar.reprocess!
end
it "is destroyable" do
url = @dummy.avatar.url
@dummy.destroy
assert_forbidden_response url
end
end
context "An attachment that uses S3 for storage and uses AES256 encryption" do
before do
rebuild_model styles: { thumb: "100x100", square: "32x32#" },
storage: :s3,
bucket: ENV["S3_BUCKET"],
path: ":class/:attachment/:id/:style.:extension",
s3_region: ENV["S3_REGION"],
s3_credentials: {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
},
s3_server_side_encryption: "AES256"
Dummy.delete_all
@dummy = Dummy.new
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy.avatar = @file
end
after do
@file.close
@dummy.destroy
end
context "and saved" do
before do
@dummy.save
end
it "is encrypted on S3" do
assert @dummy.avatar.s3_object.server_side_encryption == "AES256"
end
end
end
end
end
end
================================================
FILE: spec/paperclip/storage/s3_spec.rb
================================================
require "spec_helper"
require "aws-sdk-s3"
describe Paperclip::Storage::S3 do
before do
Aws.config[:stub_responses] = true
end
def aws2_add_region
{ s3_region: 'us-east-1' }
end
context "Parsing S3 credentials" do
before do
@proxy_settings = {host: "127.0.0.1", port: 8888, user: "foo", password: "bar"}
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
http_proxy: @proxy_settings,
s3_credentials: {not: :important}
@dummy = Dummy.new
@avatar = @dummy.avatar
end
it "gets the correct credentials when RAILS_ENV is production" do
rails_env("production") do
assert_equal({key: "12345"},
@avatar.parse_credentials('production' => {key: '12345'},
development: {key: "54321"}))
end
end
it "gets the correct credentials when RAILS_ENV is development" do
rails_env("development") do
assert_equal({key: "54321"},
@avatar.parse_credentials('production' => {key: '12345'},
development: {key: "54321"}))
end
end
it "returns the argument if the key does not exist" do
rails_env("not really an env") do
assert_equal({test: "12345"}, @avatar.parse_credentials(test: "12345"))
end
end
it "supports HTTP proxy settings" do
rails_env("development") do
assert_equal(true, @avatar.using_http_proxy?)
assert_equal(@proxy_settings[:host], @avatar.http_proxy_host)
assert_equal(@proxy_settings[:port], @avatar.http_proxy_port)
assert_equal(@proxy_settings[:user], @avatar.http_proxy_user)
assert_equal(@proxy_settings[:password], @avatar.http_proxy_password)
end
end
end
context ":bucket option via :s3_credentials" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {bucket: 'testing'}
@dummy = Dummy.new
end
it "populates #bucket_name" do
assert_equal @dummy.avatar.bucket_name, 'testing'
end
end
context ":bucket option" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing", s3_credentials: {}
@dummy = Dummy.new
end
it "populates #bucket_name" do
assert_equal @dummy.avatar.bucket_name, 'testing'
end
end
context "missing :bucket option" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
http_proxy: @proxy_settings,
s3_credentials: {not: :important}
@dummy = Dummy.new
@dummy.avatar = stringy_file
end
it "raises an argument error" do
expect { @dummy.save }.to raise_error(ArgumentError, /missing required :bucket option/)
end
end
context "" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
url: ":s3_path_url"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an S3 path" do
assert_match %r{^//s3.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
it "uses the correct bucket" do
assert_equal "bucket", @dummy.avatar.s3_bucket.name
end
it "uses the correct key" do
assert_equal "avatars/data", @dummy.avatar.s3_object.key
end
end
context "s3_protocol" do
["http", :http, ""].each do |protocol|
context "as #{protocol.inspect}" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_protocol: protocol
@dummy = Dummy.new
end
it "returns the s3_protocol in string" do
assert_equal protocol.to_s, @dummy.avatar.s3_protocol
end
end
end
end
context "s3_protocol: 'https'" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
s3_protocol: 'https',
bucket: "bucket",
path: ":attachment/:basename:dotextension"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an S3 path" do
assert_match %r{^https://s3.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "s3_protocol: ''" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
s3_protocol: '',
bucket: "bucket",
path: ":attachment/:basename:dotextension"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a protocol-relative URL" do
assert_match %r{^//s3.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "s3_protocol: :https" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
s3_protocol: :https,
bucket: "bucket",
path: ":attachment/:basename:dotextension"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an S3 path" do
assert_match %r{^https://s3.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "s3_protocol: ''" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
s3_protocol: '',
bucket: "bucket",
path: ":attachment/:basename:dotextension"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an S3 path" do
assert_match %r{^//s3.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "An attachment that uses S3 for storage and has the style in the path" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
styles: {
thumb: "80x80>"
},
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
@dummy = Dummy.new
@dummy.avatar = stringy_file
@avatar = @dummy.avatar
end
it "uses an S3 object based on the correct path for the default style" do
assert_equal("avatars/original/data", @dummy.avatar.s3_object.key)
end
it "uses an S3 object based on the correct path for the custom style" do
assert_equal("avatars/thumb/data", @dummy.avatar.s3_object(:thumb).key)
end
end
# the s3_host_name will be defined by the s3_region
context "s3_host_name" do
before do
rebuild_model storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
s3_host_name: "s3-ap-northeast-1.amazonaws.com",
s3_region: "ap-northeast-1"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an :s3_host_name path" do
assert_match %r{^//s3-ap-northeast-1.amazonaws.com/bucket/avatars/data[^\.]}, @dummy.avatar.url
end
it "uses the S3 bucket with the correct host name" do
assert_equal "s3.ap-northeast-1.amazonaws.com",
@dummy.avatar.s3_bucket.client.config.endpoint.host
end
end
context "dynamic s3_host_name" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
s3_host_name: lambda {|a| a.instance.value }
@dummy = Dummy.new
class << @dummy
attr_accessor :value
end
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "uses s3_host_name as a proc if available" do
@dummy.value = "s3.something.com"
assert_equal "//s3.something.com/bucket/avatars/data", @dummy.avatar.url(:original, timestamp: false)
end
end
context "use_accelerate_endpoint" do
context "defaults to false" do
before do
rebuild_model(
storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
s3_host_name: "s3-ap-northeast-1.amazonaws.com",
s3_region: "ap-northeast-1",
)
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an :s3_host_name path" do
assert_match %r{^//s3-ap-northeast-1.amazonaws.com/bucket/avatars/data[^\.]},
@dummy.avatar.url
end
it "uses the S3 client with the use_accelerate_endpoint config is false" do
expect(@dummy.avatar.s3_bucket.client.config.use_accelerate_endpoint).to be(false)
end
end
context "set to true" do
before do
rebuild_model(
storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
s3_host_name: "s3-accelerate.amazonaws.com",
s3_region: "ap-northeast-1",
use_accelerate_endpoint: true,
)
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an :s3_host_name path" do
assert_match %r{^//s3-accelerate.amazonaws.com/bucket/avatars/data[^\.]},
@dummy.avatar.url
end
it "uses the S3 client with the use_accelerate_endpoint config is true" do
expect(@dummy.avatar.s3_bucket.client.config.use_accelerate_endpoint).to be(true)
end
end
end
context "An attachment that uses S3 for storage and has styles that return different file types" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
styles: { large: ['500x500#', :jpg] },
bucket: "bucket",
path: ":attachment/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
File.open(fixture_file('5k.png'), 'rb') do |file|
@dummy = Dummy.new
@dummy.avatar = file
@dummy.stubs(:new_record?).returns(false)
end
end
it "returns a url containing the correct original file mime type" do
assert_match /.+\/5k.png/, @dummy.avatar.url
end
it 'uses the correct key for the original file mime type' do
assert_match /.+\/5k.png/, @dummy.avatar.s3_object.key
end
it "returns a url containing the correct processed file mime type" do
assert_match /.+\/5k.jpg/, @dummy.avatar.url(:large)
end
it "uses the correct key for the processed file mime type" do
assert_match /.+\/5k.jpg/, @dummy.avatar.s3_object(:large).key
end
end
context "An attachment that uses S3 for storage and has a proc for styles" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
styles: lambda { |attachment| attachment.instance.counter
{thumbnail: { geometry: "50x50#",
s3_headers: {'Cache-Control' => 'max-age=31557600'}} }},
bucket: "bucket",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
@file = File.new(fixture_file('5k.png'), 'rb')
Dummy.class_eval do
def counter
@counter ||= 0
@counter += 1
@counter
end
end
@dummy = Dummy.new
@dummy.avatar = @file
object = stub
@dummy.avatar.stubs(:s3_object).with(:original).returns(object)
@dummy.avatar.stubs(:s3_object).with(:thumbnail).returns(object)
object.expects(:upload_file)
.with(anything, content_type: 'image/png',
acl: :"public-read")
object.expects(:upload_file)
.with(anything, content_type: 'image/png',
acl: :"public-read",
cache_control: 'max-age=31557600')
@dummy.save
end
after { @file.close }
it "succeeds" do
assert_equal @dummy.counter, 7
end
end
context "An attachment that uses S3 for storage and has styles" do
before do
rebuild_model(
(aws2_add_region).merge(
storage: :s3,
styles: { thumb: ["90x90#", :jpg] },
bucket: "bucket",
s3_credentials: {
"access_key_id" => "12345",
"secret_access_key" => "54321" }
)
)
@file = File.new(fixture_file("5k.png"), "rb")
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.save
end
context "reprocess" do
before do
@object = stub
@dummy.avatar.stubs(:s3_object).with(:original).returns(@object)
@dummy.avatar.stubs(:s3_object).with(:thumb).returns(@object)
@object.stubs(:get).yields(@file.read)
@object.stubs(:exists?).returns(true)
end
it "uploads original" do
@object.expects(:upload_file).with(
anything,
content_type: "image/png",
acl: :"public-read").returns(true)
@object.expects(:upload_file).with(
anything,
content_type: "image/jpeg",
acl: :"public-read").returns(true)
@dummy.avatar.reprocess!
end
it "doesn't upload original" do
@object.expects(:upload_file).with(
anything,
content_type: "image/jpeg",
acl: :"public-read").returns(true)
@dummy.avatar.reprocess!(:thumb)
end
end
after { @file.close }
end
context "An attachment that uses S3 for storage and has spaces in file name" do
before do
rebuild_model(
(aws2_add_region).merge storage: :s3,
styles: { large: ["500x500#", :jpg] },
bucket: "bucket",
s3_credentials: { "access_key_id" => "12345",
"secret_access_key" => "54321" }
)
File.open(fixture_file("spaced file.png"), "rb") do |file|
@dummy = Dummy.new
@dummy.avatar = file
@dummy.stubs(:new_record?).returns(false)
end
end
it "returns a replaced version for path" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.path
end
it "returns a replaced version for url" do
assert_match /.+\/spaced_file\.png/, @dummy.avatar.url
end
end
context "An attachment that uses S3 for storage and has a question mark in file name" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
styles: { large: ['500x500#', :jpg] },
bucket: "bucket",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
stringio = stringy_file
class << stringio
def original_filename
"question?mark.png"
end
end
file = Paperclip.io_adapters.for(stringio, hash_digest: Digest::MD5)
@dummy = Dummy.new
@dummy.avatar = file
@dummy.save
@dummy.stubs(:new_record?).returns(false)
end
it "returns a replaced version for path" do
assert_match /.+\/question_mark\.png/, @dummy.avatar.path
end
it "returns a replaced version for url" do
assert_match /.+\/question_mark\.png/, @dummy.avatar.url
end
end
context "" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
url: ":s3_domain_url"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on an S3 subdomain" do
assert_match %r{^//bucket.s3.amazonaws.com/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "" do
before do
rebuild_model(
(aws2_add_region).merge storage: :s3,
s3_credentials: {
production: { bucket: "prod_bucket" },
development: { bucket: "dev_bucket" }
},
bucket: "bucket",
s3_host_alias: "something.something.com",
path: ":attachment/:basename:dotextension",
url: ":s3_alias_url"
)
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on the host_alias" do
assert_match %r{^//something.something.com/avatars/data[^\.]}, @dummy.avatar.url
end
end
context "generating a url with a prefixed host alias" do
before do
rebuild_model(
aws2_add_region.merge(
storage: :s3,
s3_credentials: {
production: { bucket: "prod_bucket" },
development: { bucket: "dev_bucket" },
},
bucket: "bucket",
s3_host_alias: "something.something.com",
s3_prefixes_in_alias: 2,
path: "prefix1/prefix2/:attachment/:basename:dotextension",
url: ":s3_alias_url",
)
)
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url with the prefixes removed" do
assert_match %r{^//something.something.com/avatars/data[^\.]},
@dummy.avatar.url
end
end
context "generating a url with a proc as the host alias" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: { bucket: "prod_bucket" },
s3_host_alias: Proc.new{|atch| "cdn#{atch.instance.counter % 4}.example.com"},
path: ":attachment/:basename:dotextension",
url: ":s3_alias_url"
Dummy.class_eval do
def counter
@counter ||= 0
@counter += 1
@counter
end
end
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a url based on the host_alias" do
assert_match %r{^//cdn1.example.com/avatars/data[^\.]}, @dummy.avatar.url
assert_match %r{^//cdn2.example.com/avatars/data[^\.]}, @dummy.avatar.url
end
it "still returns the bucket name" do
assert_equal "prod_bucket", @dummy.avatar.bucket_name
end
end
context "" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {},
bucket: "bucket",
path: ":attachment/:basename:dotextension",
url: ":asset_host"
@dummy = Dummy.new
@dummy.avatar = stringy_file
@dummy.stubs(:new_record?).returns(false)
end
it "returns a relative URL for Rails to calculate assets host" do
assert_match %r{^avatars/data[^\.]}, @dummy.avatar.url
end
end
context "Generating a secure url with an expiration" do
before do
@build_model_with_options = lambda {|options|
base_options = {
storage: :s3,
s3_credentials: {
production: { bucket: "prod_bucket" },
development: { bucket: "dev_bucket" }
},
s3_host_alias: "something.something.com",
s3_permissions: "private",
path: ":attachment/:basename:dotextension",
url: ":s3_alias_url"
}
rebuild_model (aws2_add_region).merge base_options.merge(options)
}
end
it "uses default options" do
@build_model_with_options[{}]
rails_env("production") do
@dummy = Dummy.new
@dummy.avatar = stringy_file
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:presigned_url).with(:get, expires_in: 3600)
@dummy.avatar.expiring_url
end
end
it "allows overriding s3_url_options" do
@build_model_with_options[s3_url_options: { response_content_disposition: "inline" }]
rails_env("production") do
@dummy = Dummy.new
@dummy.avatar = stringy_file
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:presigned_url)
.with(:get, expires_in: 3600,
response_content_disposition: "inline")
@dummy.avatar.expiring_url
end
end
it "allows overriding s3_object options with a proc" do
@build_model_with_options[s3_url_options: lambda {|attachment| { response_content_type: attachment.avatar_content_type } }]
rails_env("production") do
@dummy = Dummy.new
@file = stringy_file
@file.stubs(:original_filename).returns("5k.png\n\n")
Paperclip.stubs(:run).returns('image/png')
@file.stubs(:content_type).returns("image/png\n\n")
@file.stubs(:to_tempfile).returns(@file)
@dummy.avatar = @file
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:presigned_url)
.with(:get, expires_in: 3600, response_content_type: "image/png")
@dummy.avatar.expiring_url
end
end
end
context "#expiring_url" do
before { @dummy = Dummy.new }
context "with no attachment" do
before { assert(!@dummy.avatar.exists?) }
it "returns the default URL" do
assert_equal(@dummy.avatar.url, @dummy.avatar.expiring_url)
end
it 'generates a url for a style when a file does not exist' do
assert_equal(@dummy.avatar.url(:thumb), @dummy.avatar.expiring_url(3600, :thumb))
end
end
it "generates the same url when using Times and Integer offsets" do
assert_equal @dummy.avatar.expiring_url(1234), @dummy.avatar.expiring_url(Time.now + 1234)
end
end
context "Generating a url with an expiration for each style" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {
production: { bucket: "prod_bucket" },
development: { bucket: "dev_bucket" }
},
s3_permissions: :private,
s3_host_alias: "something.something.com",
path: ":attachment/:style/:basename:dotextension",
url: ":s3_alias_url"
rails_env("production") do
@dummy = Dummy.new
@dummy.avatar = stringy_file
end
end
it "generates a url for the thumb" do
object = stub
@dummy.avatar.stubs(:s3_object).with(:thumb).returns(object)
object.expects(:presigned_url).with(:get, expires_in: 1800)
@dummy.avatar.expiring_url(1800, :thumb)
end
it "generates a url for the default style" do
object = stub
@dummy.avatar.stubs(:s3_object).with(:original).returns(object)
object.expects(:presigned_url).with(:get, expires_in: 1800)
@dummy.avatar.expiring_url(1800)
end
end
context "Parsing S3 credentials with a bucket in them" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: {
production: { bucket: "prod_bucket" },
development: { bucket: "dev_bucket" }
}
@dummy = Dummy.new
end
it "gets the right bucket in production" do
rails_env("production") do
assert_equal "prod_bucket", @dummy.avatar.bucket_name
assert_equal "prod_bucket", @dummy.avatar.s3_bucket.name
end
end
it "gets the right bucket in development" do
rails_env("development") do
assert_equal "dev_bucket", @dummy.avatar.bucket_name
assert_equal "dev_bucket", @dummy.avatar.s3_bucket.name
end
end
end
# the bucket.name is determined by the :s3_region
context "Parsing S3 credentials with a s3_host_name in them" do
before do
rebuild_model storage: :s3,
bucket: 'testing',
s3_credentials: {
production: {
s3_region: "world-end",
s3_host_name: "s3-world-end.amazonaws.com" },
development: {
s3_region: "ap-northeast-1",
s3_host_name: "s3-ap-northeast-1.amazonaws.com" },
test: {
s3_region: "" }
}
@dummy = Dummy.new
end
it "gets the right s3_host_name in production" do
rails_env("production") do
assert_match %r{^s3-world-end.amazonaws.com}, @dummy.avatar.s3_host_name
assert_match %r{^s3.world-end.amazonaws.com},
@dummy.avatar.s3_bucket.client.config.endpoint.host
end
end
it "gets the right s3_host_name in development" do
rails_env("development") do
assert_match %r{^s3.ap-northeast-1.amazonaws.com},
@dummy.avatar.s3_host_name
assert_match %r{^s3.ap-northeast-1.amazonaws.com},
@dummy.avatar.s3_bucket.client.config.endpoint.host
end
end
it "gets the right s3_host_name if the key does not exist" do
rails_env("test") do
assert_match %r{^s3.amazonaws.com}, @dummy.avatar.s3_host_name
assert_raises(Aws::Errors::MissingRegionError) do
@dummy.avatar.s3_bucket.client.config.endpoint.host
end
end
end
end
context "An attachment with S3 storage" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
access_key_id: "12345",
secret_access_key: "54321"
}
end
it "is extended by the S3 module" do
assert Dummy.new.avatar.is_a?(Paperclip::Storage::S3)
end
it "won't be extended by the Filesystem module" do
assert ! Dummy.new.avatar.is_a?(Paperclip::Storage::Filesystem)
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
@dummy.stubs(:new_record?).returns(false)
end
after { @file.close }
it "does not get a bucket to get a URL" do
@dummy.avatar.expects(:s3).never
@dummy.avatar.expects(:s3_bucket).never
assert_match %r{^//s3\.amazonaws\.com/testing/avatars/original/5k\.png}, @dummy.avatar.url
end
it "is rewound after flush_writes" do
@dummy.avatar.instance_eval "def after_flush_writes; end"
@dummy.avatar.stubs(:s3_object).returns(stub(upload_file: true))
files = @dummy.avatar.queued_for_write.values.each(&:read)
@dummy.save
assert files.none?(&:eof?), "Expect all the files to be rewound."
end
it "is removed after after_flush_writes" do
@dummy.avatar.stubs(:s3_object).returns(stub(upload_file: true))
paths = @dummy.avatar.queued_for_write.values.map(&:path)
@dummy.save
assert paths.none?{ |path| File.exist?(path) },
"Expect all the files to be deleted."
end
it "will retry to save again but back off on SlowDown" do
@dummy.avatar.stubs(:sleep)
Aws::S3::Object.any_instance.stubs(:upload_file).
raises(Aws::S3::Errors::SlowDown.new(stub,
stub(status: 503, body: "")))
expect {@dummy.save}.to raise_error(Aws::S3::Errors::SlowDown)
expect(@dummy.avatar).to have_received(:sleep).with(1)
expect(@dummy.avatar).to have_received(:sleep).with(2)
expect(@dummy.avatar).to have_received(:sleep).with(4)
expect(@dummy.avatar).to have_received(:sleep).with(8)
expect(@dummy.avatar).to have_received(:sleep).with(16)
end
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything, content_type: 'image/png', acl: :"public-read")
@dummy.save
end
it "succeeds" do
assert true
end
end
context "and saved without a bucket" do
before do
Aws::S3::Bucket.any_instance.expects(:create)
Aws::S3::Object.any_instance.stubs(:upload_file).
raises(Aws::S3::Errors::NoSuchBucket
.new(stub,
stub(status: 404, body: ""))).then.returns(nil)
@dummy.save
end
it "succeeds" do
assert true
end
end
context "and remove" do
before do
Aws::S3::Object.any_instance.stubs(:exists?).returns(true)
Aws::S3::Object.any_instance.stubs(:delete)
@dummy.destroy
end
it "succeeds" do
assert true
end
end
context 'that the file were missing' do
before do
Aws::S3::Object.any_instance.stubs(:exists?)
.raises(Aws::S3::Errors::ServiceError.new("rspec stub raises",
"object exists?"))
end
it 'returns false on exists?' do
assert !@dummy.avatar.exists?
end
end
end
end
context "An attachment with S3 storage and bucket defined as a Proc" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: lambda { |attachment| "bucket_#{attachment.instance.other}" },
s3_credentials: {not: :important}
end
it "gets the right bucket name" do
assert "bucket_a", Dummy.new(other: 'a').avatar.bucket_name
assert "bucket_a", Dummy.new(other: 'a').avatar.s3_bucket.name
assert "bucket_b", Dummy.new(other: 'b').avatar.bucket_name
assert "bucket_b", Dummy.new(other: 'b').avatar.s3_bucket.name
end
end
context "An attachment with S3 storage and S3 credentials defined as a Proc" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: {not: :important},
s3_credentials: lambda { |attachment|
Hash['access_key_id' => "access#{attachment.instance.other}", 'secret_access_key' => "secret#{attachment.instance.other}"]
}
end
it "gets the right credentials" do
assert "access1234", Dummy.new(other: '1234').avatar.s3_credentials[:access_key_id]
assert "secret1234", Dummy.new(other: '1234').avatar.s3_credentials[:secret_access_key]
end
end
context "An attachment with S3 storage and S3 credentials with a :credential_provider" do
before do
class DummyCredentialProvider; end
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
s3_credentials: {
credentials: DummyCredentialProvider.new
}
@dummy = Dummy.new
end
it "sets the credential-provider" do
expect(@dummy.avatar.s3_bucket.client.config.credentials).to be_a DummyCredentialProvider
end
end
context "An attachment with S3 storage and S3 credentials in an unsupported manor" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing", s3_credentials: ["unsupported"]
@dummy = Dummy.new
end
it "does not accept the credentials" do
assert_raises(ArgumentError) do
@dummy.avatar.s3_credentials
end
end
end
context "An attachment with S3 storage and S3 credentials not supplied" do
before do
rebuild_model (aws2_add_region).merge storage: :s3, bucket: "testing"
@dummy = Dummy.new
end
it "does not parse any credentials" do
assert_equal({}, @dummy.avatar.s3_credentials)
end
end
context "An attachment with S3 storage and specific s3 headers set" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_headers: {'Cache-Control' => 'max-age=31557600'}
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: 'image/png',
acl: :"public-read",
cache_control: 'max-age=31557600')
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "An attachment with S3 storage and metadata set using header names" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_headers: {'x-amz-meta-color' => 'red'}
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: 'image/png',
acl: :"public-read",
metadata: { "color" => "red" })
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "An attachment with S3 storage and metadata set using the :s3_metadata option" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_metadata: { "color" => "red" }
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: 'image/png',
acl: :"public-read",
metadata: { "color" => "red" })
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "An attachment with S3 storage and storage class set" do
context "using the header name" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_headers: { "x-amz-storage-class" => "reduced_redundancy" }
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: 'image/png',
acl: :"public-read",
storage_class: "reduced_redundancy")
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "using per style hash" do
before do
rebuild_model (aws2_add_region).merge :storage => :s3,
:bucket => "testing",
:path => ":attachment/:style/:basename.:extension",
:styles => {
:thumb => "80x80>"
},
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
:s3_storage_class => {
:thumb => :reduced_redundancy
}
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
[:thumb, :original].each do |style|
@dummy.avatar.stubs(:s3_object).with(style).returns(object)
expected_options = {
:content_type => "image/png",
acl: :"public-read"
}
expected_options.merge!(:storage_class => :reduced_redundancy) if style == :thumb
object.expects(:upload_file)
.with(anything, expected_options)
end
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "using global hash option" do
before do
rebuild_model (aws2_add_region).merge :storage => :s3,
:bucket => "testing",
:path => ":attachment/:style/:basename.:extension",
:styles => {
:thumb => "80x80>"
},
:s3_credentials => {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
:s3_storage_class => :reduced_redundancy
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
[:thumb, :original].each do |style|
@dummy.avatar.stubs(:s3_object).with(style).returns(object)
object.expects(:upload_file)
.with(anything, :content_type => "image/png",
acl: :"public-read",
:storage_class => :reduced_redundancy)
end
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
end
context "Can disable AES256 encryption multiple ways" do
[nil, false, ''].each do |tech|
before do
rebuild_model(
(aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"},
s3_server_side_encryption: tech)
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything, :content_type => "image/png", acl: :"public-read")
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
end
context "An attachment with S3 storage and using AES256 encryption" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_server_side_encryption: "AES256"
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything, content_type: "image/png",
acl: :"public-read",
server_side_encryption: "AES256")
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "An attachment with S3 storage and storage class set using the :storage_class option" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_storage_class: :reduced_redundancy
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: "image/png",
acl: :"public-read",
storage_class: :reduced_redundancy)
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "with S3 credentials supplied as Pathname" do
before do
ENV['S3_KEY'] = 'pathname_key'
ENV['S3_BUCKET'] = 'pathname_bucket'
ENV['S3_SECRET'] = 'pathname_secret'
rails_env('test') do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: Pathname.new(fixture_file('s3.yml'))
Dummy.delete_all
@dummy = Dummy.new
end
end
it "parses the credentials" do
assert_equal 'pathname_bucket', @dummy.avatar.bucket_name
assert_equal 'pathname_key',
@dummy.avatar.s3_bucket.client.config.access_key_id
assert_equal 'pathname_secret',
@dummy.avatar.s3_bucket.client.config.secret_access_key
end
end
context "with S3 credentials in a YAML file" do
before do
ENV['S3_KEY'] = 'env_key'
ENV['S3_BUCKET'] = 'env_bucket'
ENV['S3_SECRET'] = 'env_secret'
rails_env('test') do
rebuild_model (aws2_add_region).merge storage: :s3,
s3_credentials: File.new(fixture_file('s3.yml'))
Dummy.delete_all
@dummy = Dummy.new
end
end
it "runs the file through ERB" do
assert_equal 'env_bucket', @dummy.avatar.bucket_name
assert_equal 'env_key',
@dummy.avatar.s3_bucket.client.config.access_key_id
assert_equal 'env_secret',
@dummy.avatar.s3_bucket.client.config.secret_access_key
end
end
context "S3 Permissions" do
context "defaults to :public_read" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
}
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything, content_type: "image/png", acl: :"public-read")
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "string permissions set" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_permissions: :private
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
object = stub
@dummy.avatar.stubs(:s3_object).returns(object)
object.expects(:upload_file)
.with(anything, content_type: "image/png", acl: :private)
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "hash permissions set" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
styles: {
thumb: "80x80>"
},
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_permissions: {
original: :private,
thumb: :public_read
}
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
[:thumb, :original].each do |style|
object = stub
@dummy.avatar.stubs(:s3_object).with(style).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: "image/png",
acl: style == :thumb ? :public_read : :private)
end
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "proc permission set" do
before do
rebuild_model(
(aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
styles: {
thumb: "80x80>"
},
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_permissions: lambda {|attachment, style|
attachment.instance.private_attachment? && style.to_sym != :thumb ? :private : :"public-read"
}
)
end
end
end
context "An attachment with S3 storage and metadata set using a proc as headers" do
before do
rebuild_model(
(aws2_add_region).merge storage: :s3,
bucket: "testing",
path: ":attachment/:style/:basename:dotextension",
styles: {
thumb: "80x80>"
},
s3_credentials: {
'access_key_id' => "12345",
'secret_access_key' => "54321"
},
s3_headers: lambda {|attachment|
{'Content-Disposition' => "attachment; filename=\"#{attachment.name}\""}
}
)
end
context "when assigned" do
before do
@file = File.new(fixture_file('5k.png'), 'rb')
@dummy = Dummy.new
@dummy.stubs(name: 'Custom Avatar Name.png')
@dummy.avatar = @file
end
after { @file.close }
context "and saved" do
before do
[:thumb, :original].each do |style|
object = stub
@dummy.avatar.stubs(:s3_object).with(style).returns(object)
object.expects(:upload_file)
.with(anything,
content_type: "image/png",
acl: :"public-read",
content_disposition: 'attachment; filename="Custom Avatar Name.png"')
end
@dummy.save
end
it "succeeds" do
assert true
end
end
end
end
context "path is a proc" do
before do
rebuild_model (aws2_add_region).merge storage: :s3,
path: ->(attachment) { attachment.instance.attachment_path }
@dummy = Dummy.new
@dummy.class_eval do
def attachment_path
'/some/dynamic/path'
end
end
@dummy.avatar = stringy_file
end
it "returns a correct path" do
assert_match '/some/dynamic/path', @dummy.avatar.path
end
end
private
def rails_env(env)
stored_env, Rails.env = Rails.env, env
begin
yield
ensure
Rails.env = stored_env
end
end
end
================================================
FILE: spec/paperclip/style_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Style do
context "A style rule" do
before do
@attachment = attachment path: ":basename.:extension",
styles: { foo: {geometry: "100x100#", format: :png} },
whiny: true
@style = @attachment.styles[:foo]
end
it "is held as a Style object" do
expect(@style).to be_a Paperclip::Style
end
it "gets processors from the attachment definition" do
assert_equal [:thumbnail], @style.processors
end
it "has the right geometry" do
assert_equal "100x100#", @style.geometry
end
it "is whiny if the attachment is" do
assert @style.whiny?
end
it "responds to hash notation" do
assert_equal [:thumbnail], @style[:processors]
assert_equal "100x100#", @style[:geometry]
end
it "returns the name of the style in processor options" do
assert_equal :foo, @style.processor_options[:style]
end
end
context "A style rule with properties supplied as procs" do
before do
@attachment = attachment path: ":basename.:extension",
whiny_thumbnails: true,
processors: lambda {|a| [:test]},
styles: {
foo: lambda{|a| "300x300#"},
bar: {
geometry: lambda{|a| "300x300#"},
convert_options: lambda{|a| "-do_stuff"},
source_file_options: lambda{|a| "-do_extra_stuff"}
}
}
end
it "calls procs when they are needed" do
assert_equal "300x300#", @attachment.styles[:foo].geometry
assert_equal "300x300#", @attachment.styles[:bar].geometry
assert_equal [:test], @attachment.styles[:foo].processors
assert_equal [:test], @attachment.styles[:bar].processors
assert_equal "-do_stuff", @attachment.styles[:bar].convert_options
assert_equal "-do_extra_stuff", @attachment.styles[:bar].source_file_options
end
end
context "An attachment with style rules in various forms" do
before do
styles = {}
styles[:aslist] = ["100x100", :png]
styles[:ashash] = {geometry: "100x100", format: :png}
styles[:asstring] = "100x100"
@attachment = attachment path: ":basename.:extension",
styles: styles
end
it "has the right number of styles" do
expect(@attachment.styles).to be_a Hash
assert_equal 3, @attachment.styles.size
end
it "has styles as Style objects" do
[:aslist, :ashash, :aslist].each do |s|
expect(@attachment.styles[s]).to be_a Paperclip::Style
end
end
it "has the right geometries" do
[:aslist, :ashash, :aslist].each do |s|
assert_equal @attachment.styles[s].geometry, "100x100"
end
end
it "has the right formats" do
assert_equal @attachment.styles[:aslist].format, :png
assert_equal @attachment.styles[:ashash].format, :png
assert_nil @attachment.styles[:asstring].format
end
it "retains order" do
assert_equal [:aslist, :ashash, :asstring], @attachment.styles.keys
end
end
context "An attachment with :convert_options" do
it "does not have called extra_options_for(:thumb/:large) on initialization" do
@attachment = attachment path: ":basename.:extension",
styles: {thumb: "100x100", large: "400x400"},
convert_options: {all: "-do_stuff", thumb: "-thumbnailize"}
@attachment.expects(:extra_options_for).never
@style = @attachment.styles[:thumb]
end
it "calls extra_options_for(:thumb/:large) when convert options are requested" do
@attachment = attachment path: ":basename.:extension",
styles: {thumb: "100x100", large: "400x400"},
convert_options: {all: "-do_stuff", thumb: "-thumbnailize"}
@style = @attachment.styles[:thumb]
@file = StringIO.new("...")
@file.stubs(:original_filename).returns("file.jpg")
@attachment.expects(:extra_options_for).with(:thumb)
@attachment.styles[:thumb].convert_options
end
end
context "An attachment with :source_file_options" do
it "does not have called extra_source_file_options_for(:thumb/:large) on initialization" do
@attachment = attachment path: ":basename.:extension",
styles: {thumb: "100x100", large: "400x400"},
source_file_options: {all: "-density 400", thumb: "-depth 8"}
@attachment.expects(:extra_source_file_options_for).never
@style = @attachment.styles[:thumb]
end
it "calls extra_options_for(:thumb/:large) when convert options are requested" do
@attachment = attachment path: ":basename.:extension",
styles: {thumb: "100x100", large: "400x400"},
source_file_options: {all: "-density 400", thumb: "-depth 8"}
@style = @attachment.styles[:thumb]
@file = StringIO.new("...")
@file.stubs(:original_filename).returns("file.jpg")
@attachment.expects(:extra_source_file_options_for).with(:thumb)
@attachment.styles[:thumb].source_file_options
end
end
context "A style rule with its own :processors" do
before do
@attachment = attachment path: ":basename.:extension",
styles: {
foo: {
geometry: "100x100#",
format: :png,
processors: [:test]
}
},
processors: [:thumbnail]
@style = @attachment.styles[:foo]
end
it "does not get processors from the attachment" do
@attachment.expects(:processors).never
assert_not_equal [:thumbnail], @style.processors
end
it "reports its own processors" do
assert_equal [:test], @style.processors
end
end
context "A style rule with :processors supplied as procs" do
before do
@attachment = attachment path: ":basename.:extension",
styles: {
foo: {
geometry: "100x100#",
format: :png,
processors: lambda{|a| [:test]}
}
},
processors: [:thumbnail]
end
it "defers processing of procs until they are needed" do
expect(@attachment.styles[:foo].instance_variable_get("@processors")).to be_a Proc
end
it "calls procs when they are needed" do
assert_equal [:test], @attachment.styles[:foo].processors
end
end
context "An attachment with :convert_options and :source_file_options in :styles" do
before do
@attachment = attachment path: ":basename.:extension",
styles: {
thumb: "100x100",
large: {geometry: "400x400",
convert_options: "-do_stuff",
source_file_options: "-do_extra_stuff"
}
}
@file = StringIO.new("...")
@file.stubs(:original_filename).returns("file.jpg")
end
it "has empty options for :thumb style" do
assert_equal "", @attachment.styles[:thumb].processor_options[:convert_options]
assert_equal "", @attachment.styles[:thumb].processor_options[:source_file_options]
end
it "has the right options for :large style" do
assert_equal "-do_stuff", @attachment.styles[:large].processor_options[:convert_options]
assert_equal "-do_extra_stuff", @attachment.styles[:large].processor_options[:source_file_options]
end
end
context "A style rule supplied with default format" do
before do
@attachment = attachment default_format: :png,
styles: {
asstring: "300x300#",
aslist: ["300x300#", :jpg],
ashash: {
geometry: "300x300#",
convert_options: "-do_stuff"
}
}
end
it "has the right number of styles" do
expect(@attachment.styles).to be_a Hash
assert_equal 3, @attachment.styles.size
end
it "has styles as Style objects" do
[:aslist, :ashash, :aslist].each do |s|
expect(@attachment.styles[s]).to be_a Paperclip::Style
end
end
it "has the right geometries" do
[:aslist, :ashash, :aslist].each do |s|
assert_equal @attachment.styles[s].geometry, "300x300#"
end
end
it "has the right formats" do
assert_equal @attachment.styles[:aslist].format, :jpg
assert_equal @attachment.styles[:ashash].format, :png
assert_equal @attachment.styles[:asstring].format, :png
end
end
end
================================================
FILE: spec/paperclip/tempfile_factory_spec.rb
================================================
require 'spec_helper'
describe Paperclip::TempfileFactory do
it "is able to generate a tempfile with the right name" do
file = subject.generate("omg.png")
assert File.extname(file.path), "png"
end
it "is able to generate a tempfile with the right name with a tilde at the beginning" do
file = subject.generate("~omg.png")
assert File.extname(file.path), "png"
end
it "is able to generate a tempfile with the right name with a tilde at the end" do
file = subject.generate("omg.png~")
assert File.extname(file.path), "png"
end
it "is able to generate a tempfile from a file with a really long name" do
filename = "#{"longfilename" * 100}.png"
file = subject.generate(filename)
assert File.extname(file.path), "png"
end
it 'is able to take nothing as a parameter and not error' do
file = subject.generate
assert File.exist?(file.path)
end
it "does not throw Errno::ENAMETOOLONG when it has a really long name" do
expect { subject.generate("o" * 255) }.to_not raise_error
end
end
================================================
FILE: spec/paperclip/tempfile_spec.rb
================================================
require "spec_helper"
describe Paperclip::Tempfile do
context "A Paperclip Tempfile" do
before do
@tempfile = described_class.new(["file", ".jpg"])
end
after { @tempfile.close }
it "has its path contain a real extension" do
assert_equal ".jpg", File.extname(@tempfile.path)
end
it "is a real Tempfile" do
assert @tempfile.is_a?(::Tempfile)
end
end
context "Another Paperclip Tempfile" do
before do
@tempfile = described_class.new("file")
end
after { @tempfile.close }
it "does not have an extension if not given one" do
assert_equal "", File.extname(@tempfile.path)
end
it "is a real Tempfile" do
assert @tempfile.is_a?(::Tempfile)
end
end
end
================================================
FILE: spec/paperclip/thumbnail_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Thumbnail do
context "An image" do
before do
@file = File.new(fixture_file("5k.png"), 'rb')
end
after { @file.close }
[["600x600>", "434x66"],
["400x400>", "400x61"],
["32x32<", "434x66"],
[nil, "434x66"]
].each do |args|
context "being thumbnailed with a geometry of #{args[0]}" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: args[0])
end
it "starts with dimensions of 434x66" do
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
assert_equal "434x66", `#{cmd}`.chomp
end
it "reports the correct target geometry" do
assert_equal args[0].to_s, @thumb.target_geometry.to_s
end
context "when made" do
before do
@thumb_result = @thumb.make
end
it "is the size we expect it to be" do
cmd = %Q[identify -format "%wx%h" "#{@thumb_result.path}"]
assert_equal args[1], `#{cmd}`.chomp
end
end
end
end
context "being thumbnailed at 100x50 with cropping" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "100x50#")
end
it "lets us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
Terrapin::CommandLine.path = ''
Paperclip.options[:command_path] = ''
ENV['PATH'] = ''
assert_raises(Paperclip::Errors::CommandNotFoundError) do
silence_stream(STDERR) do
@thumb.make
end
end
ensure
ENV['PATH'] = old_path
end
end
it "reports its correct current and target geometries" do
assert_equal "100x50#", @thumb.target_geometry.to_s
assert_equal "434x66", @thumb.current_geometry.to_s
end
it "reports its correct format" do
assert_nil @thumb.format
end
it "has whiny turned on by default" do
assert @thumb.whiny
end
it "has convert_options set to nil by default" do
assert_equal nil, @thumb.convert_options
end
it "has source_file_options set to nil by default" do
assert_equal nil, @thumb.source_file_options
end
it "sends the right command to convert when sent #make" do
@thumb.expects(:convert).with do |*arg|
arg[0] == ':source -auto-orient -resize "x50" -crop "100x50+114+0" +repage :dest' &&
arg[1][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end
@thumb.make
end
it "creates the thumbnail when sent #make" do
dst = @thumb.make
assert_match /100x50/, `identify "#{dst.path}"`
end
end
it 'crops a EXIF-rotated image properly' do
file = File.new(fixture_file('rotated.jpg'))
thumb = Paperclip::Thumbnail.new(file, geometry: "50x50#")
output_file = thumb.make
command = Terrapin::CommandLine.new("identify", "-format %wx%h :file")
assert_equal "50x50", command.run(file: output_file.path).strip
end
context "being thumbnailed with source file options set" do
before do
@thumb = Paperclip::Thumbnail.new(@file,
geometry: "100x50#",
source_file_options: "-strip")
end
it "has source_file_options value set" do
assert_equal ["-strip"], @thumb.source_file_options
end
it "sends the right command to convert when sent #make" do
@thumb.expects(:convert).with do |*arg|
arg[0] == '-strip :source -auto-orient -resize "x50" -crop "100x50+114+0" +repage :dest' &&
arg[1][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end
@thumb.make
end
it "creates the thumbnail when sent #make" do
dst = @thumb.make
assert_match /100x50/, `identify "#{dst.path}"`
end
context "redefined to have bad source_file_options setting" do
before do
@thumb = Paperclip::Thumbnail.new(@file,
geometry: "100x50#",
source_file_options: "-this-aint-no-option")
end
it "errors when trying to create the thumbnail" do
assert_raises(Paperclip::Error) do
silence_stream(STDERR) do
@thumb.make
end
end
end
end
end
context "being thumbnailed with convert options set" do
before do
@thumb = Paperclip::Thumbnail.new(@file,
geometry: "100x50#",
convert_options: "-strip -depth 8")
end
it "has convert_options value set" do
assert_equal %w"-strip -depth 8", @thumb.convert_options
end
it "sends the right command to convert when sent #make" do
@thumb.expects(:convert).with do |*arg|
arg[0] == ':source -auto-orient -resize "x50" -crop "100x50+114+0" +repage -strip -depth 8 :dest' &&
arg[1][:source] == "#{File.expand_path(@thumb.file.path)}[0]"
end
@thumb.make
end
it "creates the thumbnail when sent #make" do
dst = @thumb.make
assert_match /100x50/, `identify "#{dst.path}"`
end
context "redefined to have bad convert_options setting" do
before do
@thumb = Paperclip::Thumbnail.new(@file,
geometry: "100x50#",
convert_options: "-this-aint-no-option")
end
it "errors when trying to create the thumbnail" do
silence_stream(STDERR) do
expect {
@thumb.make
}.to raise_error(
Paperclip::Error, /unrecognized option `-this-aint-no-option'/
)
end
end
it "lets us know when a command isn't found versus a processing error" do
old_path = ENV['PATH']
begin
Terrapin::CommandLine.path = ''
Paperclip.options[:command_path] = ''
ENV['PATH'] = ''
assert_raises(Paperclip::Errors::CommandNotFoundError) do
silence_stream(STDERR) do
@thumb.make
end
end
ensure
ENV['PATH'] = old_path
end
end
end
end
context "being thumbnailed with a blank geometry string" do
before do
@thumb = Paperclip::Thumbnail.new(@file,
geometry: "",
convert_options: "-gravity center -crop \"300x300+0-0\"")
end
it "does not get resized by default" do
assert !@thumb.transformation_command.include?("-resize")
end
end
context "being thumbnailed with default animated option (true)" do
it "calls identify to check for animated images when sent #make" do
thumb = Paperclip::Thumbnail.new(@file, geometry: "100x50#")
thumb.expects(:identify).at_least_once.with do |*arg|
arg[0] == '-format %m :file' &&
arg[1][:file] == "#{File.expand_path(thumb.file.path)}[0]"
end
thumb.make
end
end
context "passing a custom file geometry parser" do
after do
Object.send(:remove_const, :GeoParser) if Object.const_defined?(:GeoParser)
end
it "produces the appropriate transformation_command" do
GeoParser = Class.new do
def self.from_file(file)
new
end
def transformation_to(target, should_crop)
["SCALE", "CROP"]
end
end
thumb = Paperclip::Thumbnail.new(@file, geometry: '50x50', file_geometry_parser: ::GeoParser)
transformation_command = thumb.transformation_command
assert transformation_command.include?('-crop'),
%{expected #{transformation_command.inspect} to include '-crop'}
assert transformation_command.include?('"CROP"'),
%{expected #{transformation_command.inspect} to include '"CROP"'}
assert transformation_command.include?('-resize'),
%{expected #{transformation_command.inspect} to include '-resize'}
assert transformation_command.include?('"SCALE"'),
%{expected #{transformation_command.inspect} to include '"SCALE"'}
end
end
context "passing a custom geometry string parser" do
after do
Object.send(:remove_const, :GeoParser) if Object.const_defined?(:GeoParser)
end
it "produces the appropriate transformation_command" do
GeoParser = Class.new do
def self.parse(s)
new
end
def to_s
"151x167"
end
end
thumb = Paperclip::Thumbnail.new(@file, geometry: '50x50', string_geometry_parser: ::GeoParser)
transformation_command = thumb.transformation_command
assert transformation_command.include?('"151x167"'),
%{expected #{transformation_command.inspect} to include '151x167'}
end
end
end
context "A multipage PDF" do
before do
@file = File.new(fixture_file("twopage.pdf"), 'rb')
end
after { @file.close }
it "starts with two pages with dimensions 612x792" do
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
assert_equal "612x792"*2, `#{cmd}`.chomp
end
context "being thumbnailed at 100x100 with cropping" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "100x100#", format: :png)
end
it "reports its correct current and target geometries" do
assert_equal "100x100#", @thumb.target_geometry.to_s
assert_equal "612x792", @thumb.current_geometry.to_s
end
it "reports its correct format" do
assert_equal :png, @thumb.format
end
it "creates the thumbnail when sent #make" do
dst = @thumb.make
assert_match /100x100/, `identify "#{dst.path}"`
end
end
end
context "An animated gif" do
before do
@file = File.new(fixture_file("animated.gif"), 'rb')
end
after { @file.close }
it "starts with 12 frames with size 100x100" do
cmd = %Q[identify -format "%wx%h" "#{@file.path}"]
assert_equal "100x100"*12, `#{cmd}`.chomp
end
context "with static output" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :jpg)
end
it "creates the single frame thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50", `#{cmd}`.chomp
end
end
context "with animated output format" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :gif)
end
it "creates the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h," "#{dst.path}"]
frames = `#{cmd}`.chomp.split(',')
assert_equal 12, frames.size
assert_frame_dimensions (45..50), frames
end
it "uses the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
it "uses the -layers 'optimize' option" do
assert_equal @thumb.transformation_command.last, '-layers "optimize"'
end
end
context "with omitted output format" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50")
end
it "creates the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h," "#{dst.path}"]
frames = `#{cmd}`.chomp.split(',')
assert_equal 12, frames.size
assert_frame_dimensions (45..50), frames
end
it "uses the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
it "uses the -layers 'optimize' option" do
assert_equal @thumb.transformation_command.last, '-layers "optimize"'
end
end
context "with unidentified source format" do
before do
@unidentified_file = File.new(fixture_file("animated.unknown"), 'rb')
@thumb = Paperclip::Thumbnail.new(@file, geometry: "60x60")
end
it "creates the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h," "#{dst.path}"]
frames = `#{cmd}`.chomp.split(',')
assert_equal 12, frames.size
assert_frame_dimensions (55..60), frames
end
it "uses the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
it "uses the -layers 'optimize' option" do
assert_equal @thumb.transformation_command.last, '-layers "optimize"'
end
end
context "with no source format" do
before do
@unidentified_file = File.new(fixture_file("animated"), 'rb')
@thumb = Paperclip::Thumbnail.new(@file, geometry: "70x70")
end
it "creates the 12 frames thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h," "#{dst.path}"]
frames = `#{cmd}`.chomp.split(',')
assert_equal 12, frames.size
assert_frame_dimensions (60..70), frames
end
it "uses the -coalesce option" do
assert_equal @thumb.transformation_command.first, "-coalesce"
end
it "uses the -layers 'optimize' option" do
assert_equal @thumb.transformation_command.last, '-layers "optimize"'
end
end
context "with animated option set to false" do
before do
@thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", animated: false)
end
it "outputs the gif format" do
dst = @thumb.make
cmd = %Q[identify "#{dst.path}"]
assert_match /GIF/, `#{cmd}`.chomp
end
it "creates the single frame thumbnail when sent #make" do
dst = @thumb.make
cmd = %Q[identify -format "%wx%h" "#{dst.path}"]
assert_equal "50x50", `#{cmd}`.chomp
end
end
context "with a specified frame_index" do
before do
@thumb = Paperclip::Thumbnail.new(
@file,
geometry: "50x50",
frame_index: 5,
format: :jpg,
)
end
it "creates the thumbnail from the frame index when sent #make" do
@thumb.make
assert_equal 5, @thumb.frame_index
end
end
context "with a specified frame_index out of bounds" do
before do
@thumb = Paperclip::Thumbnail.new(
@file,
geometry: "50x50",
frame_index: 20,
format: :jpg,
)
end
it "errors when trying to create the thumbnail" do
assert_raises(Paperclip::Error) do
silence_stream(STDERR) do
@thumb.make
end
end
end
end
end
context "with a really long file name" do
before do
tempfile = Tempfile.new("f")
tempfile_additional_chars = tempfile.path.split("/")[-1].length + 15
image_file = File.new(fixture_file("5k.png"), "rb")
@file = Tempfile.new("f" * (255 - tempfile_additional_chars))
@file.write(image_file.read)
@file.rewind
end
it "does not throw Errno::ENAMETOOLONG" do
thumb = Paperclip::Thumbnail.new(@file, geometry: "50x50", format: :gif)
expect { thumb.make }.to_not raise_error
end
end
end
================================================
FILE: spec/paperclip/url_generator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::UrlGenerator do
it "uses the given interpolator" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
mock_attachment = MockAttachment.new(interpolator: mock_interpolator)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {})
assert_equal expected, result
assert mock_interpolator.has_interpolated_attachment?(mock_attachment)
assert mock_interpolator.has_interpolated_style_name?(:style_name)
end
it "uses the default URL when no file is assigned" do
mock_interpolator = MockInterpolator.new
default_url = "the default url"
options = { interpolator: mock_interpolator, default_url: default_url }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(default_url),
"expected the interpolator to be passed #{default_url.inspect} but it wasn't"
end
it "executes the default URL lambda when no file is assigned" do
mock_interpolator = MockInterpolator.new
default_url = lambda {|attachment| "the #{attachment.class.name} default url" }
options = { interpolator: mock_interpolator, default_url: default_url}
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?("the MockAttachment default url"),
%{expected the interpolator to be passed "the MockAttachment default url", but it wasn't}
end
it "executes the method named by the symbol as the default URL when no file is assigned" do
mock_model = FakeModel.new
default_url = :to_s
mock_interpolator = MockInterpolator.new
options = {
interpolator: mock_interpolator,
default_url: default_url,
model: mock_model,
}
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(mock_model.to_s),
%{expected the interpolator to be passed #{mock_model.to_s}, but it wasn't}
end
it "URL-escapes spaces if asked to" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {escape: true})
assert_equal "the%20expected%20result", result
end
it "escapes the result of the interpolator using a method on the object, if asked to escape" do
expected = Class.new do
def escape
"the escaped result"
end
end.new
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {escape: true})
assert_equal "the escaped result", result
end
it "leaves spaces unescaped as asked to" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {escape: false})
assert_equal "the expected result", result
end
it "defaults to leaving spaces unescaped" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {})
assert_equal "the expected result", result
end
it "produces URLs without the updated_at value when the object does not respond to updated_at" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator, responds_to_updated_at: false }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {timestamp: true})
assert_equal expected, result
end
it "produces URLs without the updated_at value when the updated_at value is nil" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new(result: expected)
options = {
responds_to_updated_at: true,
updated_at: nil,
interpolator: mock_interpolator,
}
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {timestamp: true})
assert_equal expected, result
end
it "produces URLs with the updated_at when it exists" do
expected = "the expected result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator, updated_at: updated_at }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {timestamp: true})
assert_equal "#{expected}?#{updated_at}", result
end
it "produces URLs with the updated_at when it exists, separated with a & if a ? follow by = already exists" do
expected = "the?expected=result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator, updated_at: updated_at }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {timestamp: true})
assert_equal "#{expected}{updated_at}", result
end
it "produces URLs without the updated_at when told to do as much" do
expected = "the expected result"
updated_at = 1231231234
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator, updated_at: updated_at }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
result = url_generator.for(:style_name, {timestamp: false})
assert_equal expected, result
end
it "produces the correct URL when the instance has a file name" do
expected = "the expected result"
mock_interpolator = MockInterpolator.new
options = {
interpolator: mock_interpolator,
url: expected,
original_filename: "exists",
}
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
url_generator.for(:style_name, {})
assert mock_interpolator.has_interpolated_pattern?(expected),
"expected the interpolator to be passed #{expected.inspect} but it wasn't"
end
describe "should be able to escape (, ), [, and ]." do
def generate(expected, updated_at=nil)
mock_interpolator = MockInterpolator.new(result: expected)
options = { interpolator: mock_interpolator, updated_at: updated_at }
mock_attachment = MockAttachment.new(options)
url_generator = Paperclip::UrlGenerator.new(mock_attachment)
def url_generator.respond_to(params)
false if params == :escape
end
url_generator.for(:style_name, {escape: true, timestamp: !!updated_at})
end
it "not timestamp" do
expected = "the(expected)result[]"
assert_equal "the%28expected%29result%5B%5D", generate(expected)
end
it "timestamp" do
expected = "the(expected)result[]"
updated_at = 1231231234
assert_equal "the%28expected%29result%5B%5D?#{updated_at}",
generate(expected, updated_at)
end
end
end
================================================
FILE: spec/paperclip/validators/attachment_content_type_validator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators::AttachmentContentTypeValidator do
before do
rebuild_model
@dummy = Dummy.new
end
def build_validator(options)
@validator = Paperclip::Validators::AttachmentContentTypeValidator.new(options.merge(
attributes: :avatar
))
end
context "with a nil content type" do
before do
build_validator content_type: "image/jpg"
@dummy.stubs(avatar_content_type: nil)
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "with :allow_nil option" do
context "as true" do
before do
build_validator content_type: "image/png", allow_nil: true
@dummy.stubs(avatar_content_type: nil)
@validator.validate(@dummy)
end
it "allows avatar_content_type as nil" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as false" do
before do
build_validator content_type: "image/png", allow_nil: false
@dummy.stubs(avatar_content_type: nil)
@validator.validate(@dummy)
end
it "does not allow avatar_content_type as nil" do
assert @dummy.errors[:avatar_content_type].present?
end
end
end
context "with a failing validation" do
before do
build_validator content_type: "image/png", allow_nil: false
@dummy.stubs(avatar_content_type: nil)
@validator.validate(@dummy)
end
it "adds error to the base object" do
assert @dummy.errors[:avatar].present?,
"Error not added to base attribute"
end
it "adds error to base object as a string" do
expect(@dummy.errors[:avatar].first).to be_a String
end
end
context "with a successful validation" do
before do
build_validator content_type: "image/png", allow_nil: false
@dummy.stubs(avatar_content_type: "image/png")
@validator.validate(@dummy)
end
it "does not add error to the base object" do
assert @dummy.errors[:avatar].blank?,
"Error was added to base attribute"
end
end
context "with :allow_blank option" do
context "as true" do
before do
build_validator content_type: "image/png", allow_blank: true
@dummy.stubs(avatar_content_type: "")
@validator.validate(@dummy)
end
it "allows avatar_content_type as blank" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as false" do
before do
build_validator content_type: "image/png", allow_blank: false
@dummy.stubs(avatar_content_type: "")
@validator.validate(@dummy)
end
it "does not allow avatar_content_type as blank" do
assert @dummy.errors[:avatar_content_type].present?
end
end
end
context "whitelist format" do
context "with an allowed type" do
context "as a string" do
before do
build_validator content_type: "image/jpg"
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as an regexp" do
before do
build_validator content_type: /^image\/.*/
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as a list" do
before do
build_validator content_type: ["image/png", "image/jpg", "image/jpeg"]
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
end
context "with a disallowed type" do
context "as a string" do
before do
build_validator content_type: "image/png"
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "sets a correct default error message" do
assert @dummy.errors[:avatar_content_type].present?
expect(@dummy.errors[:avatar_content_type]).to include "is invalid"
end
end
context "as a regexp" do
before do
build_validator content_type: /^text\/.*/
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "sets a correct default error message" do
assert @dummy.errors[:avatar_content_type].present?
expect(@dummy.errors[:avatar_content_type]).to include "is invalid"
end
end
context "with :message option" do
context "without interpolation" do
before do
build_validator content_type: "image/png", message: "should be a PNG image"
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "sets a correct error message" do
expect(@dummy.errors[:avatar_content_type]).to include "should be a PNG image"
end
end
context "with interpolation" do
before do
build_validator content_type: "image/png", message: "should have content type %{types}"
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "sets a correct error message" do
expect(@dummy.errors[:avatar_content_type]).to include "should have content type image/png"
end
end
end
end
end
context "blacklist format" do
context "with an allowed type" do
context "as a string" do
before do
build_validator not: "image/gif"
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as an regexp" do
before do
build_validator not: /^text\/.*/
@dummy.stubs(avatar_content_type: "image/jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
context "as a list" do
before do
build_validator not: ["image/png", "image/jpg", "image/jpeg"]
@dummy.stubs(avatar_content_type: "image/gif")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_content_type].blank?
end
end
end
context "with a disallowed type" do
context "as a string" do
before do
build_validator not: "image/png"
@dummy.stubs(avatar_content_type: "image/png")
@validator.validate(@dummy)
end
it "sets a correct default error message" do
assert @dummy.errors[:avatar_content_type].present?
expect(@dummy.errors[:avatar_content_type]).to include "is invalid"
end
end
context "as a regexp" do
before do
build_validator not: /^text\/.*/
@dummy.stubs(avatar_content_type: "text/plain")
@validator.validate(@dummy)
end
it "sets a correct default error message" do
assert @dummy.errors[:avatar_content_type].present?
expect(@dummy.errors[:avatar_content_type]).to include "is invalid"
end
end
context "with :message option" do
context "without interpolation" do
before do
build_validator not: "image/png", message: "should not be a PNG image"
@dummy.stubs(avatar_content_type: "image/png")
@validator.validate(@dummy)
end
it "sets a correct error message" do
expect(@dummy.errors[:avatar_content_type]).to include "should not be a PNG image"
end
end
context "with interpolation" do
before do
build_validator not: "image/png", message: "should not have content type %{types}"
@dummy.stubs(avatar_content_type: "image/png")
@validator.validate(@dummy)
end
it "sets a correct error message" do
expect(@dummy.errors[:avatar_content_type]).to include "should not have content type image/png"
end
end
end
end
end
context "using the helper" do
before do
Dummy.validates_attachment_content_type :avatar, content_type: "image/jpg"
end
it "adds the validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_content_type }
end
end
context "given options" do
it "raises argument error if no required argument was given" do
assert_raises(ArgumentError) do
build_validator message: "Some message"
end
end
it "does not raise argument error if :content_type was given" do
build_validator content_type: "image/jpg"
end
it "does not raise argument error if :not was given" do
build_validator not: "image/jpg"
end
end
end
================================================
FILE: spec/paperclip/validators/attachment_file_name_validator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators::AttachmentFileNameValidator do
before do
rebuild_model
@dummy = Dummy.new
end
def build_validator(options)
@validator = Paperclip::Validators::AttachmentFileNameValidator.new(options.merge(
attributes: :avatar
))
end
context "with a failing validation" do
before do
build_validator matches: /.*\.png$/, allow_nil: false
@dummy.stubs(avatar_file_name: "data.txt")
@validator.validate(@dummy)
end
it "adds error to the base object" do
assert @dummy.errors[:avatar].present?,
"Error not added to base attribute"
end
it "adds error to base object as a string" do
expect(@dummy.errors[:avatar].first).to be_a String
end
end
it "does not add error to the base object with a successful validation" do
build_validator matches: /.*\.png$/, allow_nil: false
@dummy.stubs(avatar_file_name: "image.png")
@validator.validate(@dummy)
assert @dummy.errors[:avatar].blank?, "Error was added to base attribute"
end
context "whitelist format" do
context "with an allowed type" do
context "as a single regexp" do
before do
build_validator matches: /.*\.jpg$/
@dummy.stubs(avatar_file_name: "image.jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
context "as a list" do
before do
build_validator matches: [/.*\.png$/, /.*\.jpe?g$/]
@dummy.stubs(avatar_file_name: "image.jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
end
context "with a disallowed type" do
it "sets a correct default error message" do
build_validator matches: /^text\/.*/
@dummy.stubs(avatar_file_name: "image.jpg")
@validator.validate(@dummy)
assert @dummy.errors[:avatar_file_name].present?
expect(@dummy.errors[:avatar_file_name]).to include "is invalid"
end
it "sets a correct custom error message" do
build_validator matches: /.*\.png$/, message: "should be a PNG image"
@dummy.stubs(avatar_file_name: "image.jpg")
@validator.validate(@dummy)
expect(@dummy.errors[:avatar_file_name]).to include "should be a PNG image"
end
end
end
context "blacklist format" do
context "with an allowed type" do
context "as a single regexp" do
before do
build_validator not: /^text\/.*/
@dummy.stubs(avatar_file_name: "image.jpg")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
context "as a list" do
before do
build_validator not: [/.*\.png$/, /.*\.jpe?g$/]
@dummy.stubs(avatar_file_name: "image.gif")
@validator.validate(@dummy)
end
it "does not set an error message" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
end
context "with a disallowed type" do
it "sets a correct default error message" do
build_validator not: /data.*/
@dummy.stubs(avatar_file_name: "data.txt")
@validator.validate(@dummy)
assert @dummy.errors[:avatar_file_name].present?
expect(@dummy.errors[:avatar_file_name]).to include "is invalid"
end
it "sets a correct custom error message" do
build_validator not: /.*\.png$/, message: "should not be a PNG image"
@dummy.stubs(avatar_file_name: "image.png")
@validator.validate(@dummy)
expect(@dummy.errors[:avatar_file_name]).to include "should not be a PNG image"
end
end
end
context "using the helper" do
before do
Dummy.validates_attachment_file_name :avatar, matches: /.*\.jpg$/
end
it "adds the validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_file_name }
end
end
context "given options" do
it "raises argument error if no required argument was given" do
assert_raises(ArgumentError) do
build_validator message: "Some message"
end
end
it "does not raise argument error if :matches was given" do
build_validator matches: /.*\.jpg$/
end
it "does not raise argument error if :not was given" do
build_validator not: /.*\.jpg$/
end
end
end
================================================
FILE: spec/paperclip/validators/attachment_presence_validator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators::AttachmentPresenceValidator do
before do
rebuild_model
@dummy = Dummy.new
end
def build_validator(options={})
@validator = Paperclip::Validators::AttachmentPresenceValidator.new(options.merge(
attributes: :avatar
))
end
context "nil attachment" do
before do
@dummy.avatar = nil
end
context "with default options" do
before do
build_validator
@validator.validate(@dummy)
end
it "adds error on the attachment" do
assert @dummy.errors[:avatar].present?
end
it "does not add an error on the file_name attribute" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
context "with :if option" do
context "returning true" do
before do
build_validator if: true
@validator.validate(@dummy)
end
it "performs a validation" do
assert @dummy.errors[:avatar].present?
end
end
context "returning false" do
before do
build_validator if: false
@validator.validate(@dummy)
end
it "performs a validation" do
assert @dummy.errors[:avatar].present?
end
end
end
end
context "with attachment" do
before do
build_validator
@dummy.avatar = StringIO.new('.\n')
@validator.validate(@dummy)
end
it "does not add error on the attachment" do
assert @dummy.errors[:avatar].blank?
end
it "does not add an error on the file_name attribute" do
assert @dummy.errors[:avatar_file_name].blank?
end
end
context "using the helper" do
before do
Dummy.validates_attachment_presence :avatar
end
it "adds the validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_presence }
end
end
end
================================================
FILE: spec/paperclip/validators/attachment_size_validator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators::AttachmentSizeValidator do
before do
rebuild_model
@dummy = Dummy.new
end
def build_validator(options)
@validator = Paperclip::Validators::AttachmentSizeValidator.new(options.merge(
attributes: :avatar
))
end
def self.should_allow_attachment_file_size(size)
context "when the attachment size is #{size}" do
it "adds error to dummy object" do
@dummy.stubs(:avatar_file_size).returns(size)
@validator.validate(@dummy)
assert @dummy.errors[:avatar_file_size].blank?,
"Expect an error message on :avatar_file_size, got none."
end
it "does not add error to the base dummy object" do
assert @dummy.errors[:avatar].blank?,
"Error added to base attribute"
end
end
end
def self.should_not_allow_attachment_file_size(size, options = {})
context "when the attachment size is #{size}" do
before do
@dummy.stubs(:avatar_file_size).returns(size)
@validator.validate(@dummy)
end
it "adds error to dummy object" do
assert @dummy.errors[:avatar_file_size].present?,
"Unexpected error message on :avatar_file_size"
end
it "adds error to the base dummy object" do
assert @dummy.errors[:avatar].present?,
"Error not added to base attribute"
end
it "adds error to base object as a string" do
expect(@dummy.errors[:avatar].first).to be_a String
end
if options[:message]
it "returns a correct error message" do
expect(@dummy.errors[:avatar_file_size]).to include options[:message]
end
end
end
end
context "with :in option" do
context "as a range" do
before do
build_validator in: (5.kilobytes..10.kilobytes)
end
should_allow_attachment_file_size(7.kilobytes)
should_not_allow_attachment_file_size(4.kilobytes)
should_not_allow_attachment_file_size(11.kilobytes)
end
context "as a proc" do
before do
build_validator in: lambda { |avatar| (5.kilobytes..10.kilobytes) }
end
should_allow_attachment_file_size(7.kilobytes)
should_not_allow_attachment_file_size(4.kilobytes)
should_not_allow_attachment_file_size(11.kilobytes)
end
end
context "with :greater_than option" do
context "as number" do
before do
build_validator greater_than: 10.kilobytes
end
should_allow_attachment_file_size 11.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
context "as a proc" do
before do
build_validator greater_than: lambda { |avatar| 10.kilobytes }
end
should_allow_attachment_file_size 11.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
end
context "with :less_than option" do
context "as number" do
before do
build_validator less_than: 10.kilobytes
end
should_allow_attachment_file_size 9.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
context "as a proc" do
before do
build_validator less_than: lambda { |avatar| 10.kilobytes }
end
should_allow_attachment_file_size 9.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
end
context "with :greater_than and :less_than option" do
context "as numbers" do
before do
build_validator greater_than: 5.kilobytes,
less_than: 10.kilobytes
end
should_allow_attachment_file_size 7.kilobytes
should_not_allow_attachment_file_size 5.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
context "as a proc" do
before do
build_validator greater_than: lambda { |avatar| 5.kilobytes },
less_than: lambda { |avatar| 10.kilobytes }
end
should_allow_attachment_file_size 7.kilobytes
should_not_allow_attachment_file_size 5.kilobytes
should_not_allow_attachment_file_size 10.kilobytes
end
end
context "with :message option" do
context "given a range" do
before do
build_validator in: (5.kilobytes..10.kilobytes),
message: "is invalid. (Between %{min} and %{max} please.)"
end
should_not_allow_attachment_file_size(
11.kilobytes,
message: "is invalid. (Between 5 KB and 10 KB please.)"
)
end
context "given :less_than and :greater_than" do
before do
build_validator less_than: 10.kilobytes,
greater_than: 5.kilobytes,
message: "is invalid. (Between %{min} and %{max} please.)"
end
should_not_allow_attachment_file_size(
11.kilobytes,
message: "is invalid. (Between 5 KB and 10 KB please.)"
)
end
end
context "default error messages" do
context "given :less_than and :greater_than" do
before do
build_validator greater_than: 5.kilobytes,
less_than: 10.kilobytes
end
should_not_allow_attachment_file_size(
11.kilobytes,
message: "must be less than 10 KB"
)
should_not_allow_attachment_file_size(
4.kilobytes,
message: "must be greater than 5 KB"
)
end
context "given a size range" do
before do
build_validator in: (5.kilobytes..10.kilobytes)
end
should_not_allow_attachment_file_size(
11.kilobytes,
message: "must be in between 5 KB and 10 KB"
)
should_not_allow_attachment_file_size(
4.kilobytes,
message: "must be in between 5 KB and 10 KB"
)
end
end
context "using the helper" do
before do
Dummy.validates_attachment_size :avatar, in: (5.kilobytes..10.kilobytes)
end
it "adds the validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_size }
end
end
context "given options" do
it "raises argument error if no required argument was given" do
assert_raises(ArgumentError) do
build_validator message: "Some message"
end
end
(Paperclip::Validators::AttachmentSizeValidator::AVAILABLE_CHECKS).each do |argument|
it "does not raise arguemnt error if #{argument} was given" do
build_validator argument => 5.kilobytes
end
end
it "does not raise argument error if :in was given" do
build_validator in: (5.kilobytes..10.kilobytes)
end
end
end
================================================
FILE: spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators::MediaTypeSpoofDetectionValidator do
before do
rebuild_model
@dummy = Dummy.new
end
def build_validator(options = {})
@validator = Paperclip::Validators::MediaTypeSpoofDetectionValidator.new(options.merge(
attributes: :avatar
))
end
it "is on the attachment without being explicitly added" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :media_type_spoof_detection }
end
it "is not on the attachment when explicitly rejected" do
rebuild_model validate_media_type: false
assert Dummy.validators_on(:avatar).none?{ |validator| validator.kind == :media_type_spoof_detection }
end
it "returns default error message for spoofed media type" do
build_validator
file = File.new(fixture_file("5k.png"), "rb")
@dummy.avatar.assign(file)
detector = mock("detector", :spoofed? => true)
Paperclip::MediaTypeSpoofDetector.stubs(:using).returns(detector)
@validator.validate(@dummy)
assert_equal I18n.t("errors.messages.spoofed_media_type"), @dummy.errors[:avatar].first
end
it "runs when attachment is dirty" do
build_validator
file = File.new(fixture_file("5k.png"), "rb")
@dummy.avatar.assign(file)
Paperclip::MediaTypeSpoofDetector.stubs(:using).returns(stub(:spoofed? => false))
@dummy.valid?
assert_received(Paperclip::MediaTypeSpoofDetector, :using){|e| e.once }
end
it "does not run when attachment is not dirty" do
Paperclip::MediaTypeSpoofDetector.stubs(:using).never
@dummy.valid?
assert_received(Paperclip::MediaTypeSpoofDetector, :using){|e| e.never }
end
end
================================================
FILE: spec/paperclip/validators_spec.rb
================================================
require 'spec_helper'
describe Paperclip::Validators do
context "using the helper" do
before do
rebuild_class
Dummy.validates_attachment :avatar, presence: true, content_type: { content_type: "image/jpeg" }, size: { in: 0..10240 }
end
it "adds the attachment_presence validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_presence }
end
it "adds the attachment_content_type validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_content_type }
end
it "adds the attachment_size validator to the class" do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_size }
end
it 'prevents you from attaching a file that violates that validation' do
Dummy.class_eval{ validate(:name) { raise "DO NOT RUN THIS" } }
dummy = Dummy.new(avatar: File.new(fixture_file("12k.png")))
expect(dummy.errors.keys).to match_array [:avatar_content_type, :avatar, :avatar_file_size]
assert_raises(RuntimeError){ dummy.valid? }
end
end
context 'using the helper with array of validations' do
before do
rebuild_class
Dummy.validates_attachment :avatar, file_type_ignorance: true, file_name: [
{ matches: /\A.*\.jpe?g\z/i, message: :invalid_extension },
{ matches: /\A.{,8}\..+\z/i, message: [:too_long, count: 8] },
]
end
it 'adds the attachment_file_name validator to the class' do
assert Dummy.validators_on(:avatar).any?{ |validator| validator.kind == :attachment_file_name }
end
it 'adds the attachment_file_name validator with two validations' do
assert_equal 2, Dummy.validators_on(:avatar).select{ |validator| validator.kind == :attachment_file_name }.size
end
it 'prevents you from attaching a file that violates all of these validations' do
Dummy.class_eval{ validate(:name) { raise 'DO NOT RUN THIS' } }
dummy = Dummy.new(avatar: File.new(fixture_file('spaced file.png')))
expect(dummy.errors.keys).to match_array [:avatar, :avatar_file_name]
assert_raises(RuntimeError){ dummy.valid? }
end
it 'prevents you from attaching a file that violates only first of these validations' do
Dummy.class_eval{ validate(:name) { raise 'DO NOT RUN THIS' } }
dummy = Dummy.new(avatar: File.new(fixture_file('5k.png')))
expect(dummy.errors.keys).to match_array [:avatar, :avatar_file_name]
assert_raises(RuntimeError){ dummy.valid? }
end
it 'prevents you from attaching a file that violates only second of these validations' do
Dummy.class_eval{ validate(:name) { raise 'DO NOT RUN THIS' } }
dummy = Dummy.new(avatar: File.new(fixture_file('spaced file.jpg')))
expect(dummy.errors.keys).to match_array [:avatar, :avatar_file_name]
assert_raises(RuntimeError){ dummy.valid? }
end
it 'allows you to attach a file that does not violate these validations' do
dummy = Dummy.new(avatar: File.new(fixture_file('rotated.jpg')))
expect(dummy.errors.full_messages).to be_empty
assert dummy.valid?
end
end
context "using the helper with a conditional" do
before do
rebuild_class
Dummy.validates_attachment :avatar, presence: true,
content_type: { content_type: "image/jpeg" },
size: { in: 0..10240 },
if: :title_present?
end
it "validates the attachment if title is present" do
Dummy.class_eval do
def title_present?
true
end
end
dummy = Dummy.new(avatar: File.new(fixture_file("12k.png")))
expect(dummy.errors.keys).to match_array [:avatar_content_type, :avatar, :avatar_file_size]
end
it "does not validate attachment if title is not present" do
Dummy.class_eval do
def title_present?
false
end
end
dummy = Dummy.new(avatar: File.new(fixture_file("12k.png")))
assert_equal [], dummy.errors.keys
end
end
context 'with no other validations on the Dummy#avatar attachment' do
before do
reset_class("Dummy")
Dummy.has_attached_file :avatar
Paperclip.reset_duplicate_clash_check!
end
it 'raises an error when no content_type validation exists' do
assert_raises(Paperclip::Errors::MissingRequiredValidatorError) do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
it 'does not raise an error when a content_type validation exists' do
Dummy.validates_attachment :avatar, content_type: { content_type: "image/jpeg" }
assert_nothing_raised do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
it 'does not raise an error when a content_type validation exists using validates_with' do
Dummy.validates_with Paperclip::Validators::AttachmentContentTypeValidator, attributes: :attachment, content_type: 'images/jpeg'
assert_nothing_raised do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
it 'does not raise an error when an inherited validator is used' do
class MyValidator < Paperclip::Validators::AttachmentContentTypeValidator
def initialize(options)
options[:content_type] = "images/jpeg" unless options.has_key?(:content_type)
super
end
end
Dummy.validates_with MyValidator, attributes: :attachment
assert_nothing_raised do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
it 'does not raise an error when a file_name validation exists' do
Dummy.validates_attachment :avatar, file_name: { matches: /png$/ }
assert_nothing_raised do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
it 'does not raise an error when a the validation has been explicitly rejected' do
Dummy.validates_attachment :avatar, file_type_ignorance: true
assert_nothing_raised do
Dummy.new(avatar: File.new(fixture_file("12k.png")))
end
end
end
end
================================================
FILE: spec/spec_helper.rb
================================================
require 'rubygems'
require 'rspec'
require 'active_record'
require 'active_record/version'
require 'active_support'
require 'active_support/core_ext'
require 'mocha/api'
require 'bourne'
require 'ostruct'
require 'pathname'
require 'activerecord-import'
ROOT = Pathname(File.expand_path(File.join(File.dirname(__FILE__), '..')))
puts "Testing against version #{ActiveRecord::VERSION::STRING}"
$LOAD_PATH << File.join(ROOT, 'lib')
$LOAD_PATH << File.join(ROOT, 'lib', 'paperclip')
require File.join(ROOT, 'lib', 'paperclip.rb')
FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures")
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config['test'])
if ActiveRecord::VERSION::STRING >= "4.2" &&
ActiveRecord::VERSION::STRING < "5.0"
ActiveRecord::Base.raise_in_transactional_callbacks = true
end
Paperclip.options[:logger] = ActiveRecord::Base.logger
Dir[File.join(ROOT, 'spec', 'support', '**', '*.rb')].each{|f| require f }
Rails = FakeRails.new('test', Pathname.new(ROOT).join('tmp'))
ActiveSupport::Deprecation.silenced = true
RSpec.configure do |config|
config.include Assertions
config.include ModelReconstruction
config.include TestData
config.include Reporting
config.extend VersionHelper
config.mock_framework = :mocha
config.before(:all) do
rebuild_model
end
end
================================================
FILE: spec/support/assertions.rb
================================================
module Assertions
def assert(truthy, message = nil)
expect(!!truthy).to(eq(true), message)
end
def assert_equal(expected, actual, message = nil)
expect(actual).to(eq(expected), message)
end
def assert_not_equal(expected, actual, message = nil)
expect(actual).to_not(eq(expected), message)
end
def assert_raises(exception_class, message = nil, &block)
expect(&block).to raise_error(exception_class, message)
end
def assert_nothing_raised(&block)
expect(&block).to_not raise_error
end
def assert_nil(thing)
expect(thing).to be_nil
end
def assert_contains(haystack, needle)
expect(haystack).to include(needle)
end
def assert_match(pattern, value)
expect(value).to match(pattern)
end
def assert_no_match(pattern, value)
expect(value).to_not match(pattern)
end
def assert_file_exists(path_to_file)
expect(path_to_file).to exist
end
def assert_file_not_exists(path_to_file)
expect(path_to_file).to_not exist
end
def assert_empty(object)
expect(object).to be_empty
end
def assert_success_response(url)
url = "http:#{url}" unless url =~ /http/
Net::HTTP.get_response(URI.parse(url)) do |response|
assert_equal "200",
response.code,
"Expected HTTP response code 200, got #{response.code}"
end
end
def assert_not_found_response(url)
url = "http:#{url}" unless url =~ /http/
Net::HTTP.get_response(URI.parse(url)) do |response|
assert_equal "404", response.code,
"Expected HTTP response code 404, got #{response.code}"
end
end
def assert_forbidden_response(url)
url = "http:#{url}" unless url =~ /http/
Net::HTTP.get_response(URI.parse(url)) do |response|
assert_equal "403", response.code,
"Expected HTTP response code 403, got #{response.code}"
end
end
def assert_frame_dimensions(range, frames)
frames.each_with_index do |frame, frame_index|
frame.split('x').each_with_index do |dimension, dimension_index |
assert range.include?(dimension.to_i), "Frame #{frame_index}[#{dimension_index}] should have been within #{range.inspect}, but was #{dimension}"
end
end
end
end
================================================
FILE: spec/support/fake_model.rb
================================================
class FakeModel
attr_accessor(
:avatar_file_name,
:avatar_file_size,
:avatar_updated_at,
:avatar_content_type,
:avatar_fingerprint,
:id
)
def errors
@errors ||= []
end
def run_paperclip_callbacks name, *args
end
def valid?
errors.empty?
end
def new_record?
false
end
end
================================================
FILE: spec/support/fake_rails.rb
================================================
class FakeRails
def initialize(env, root)
@env = env
@root = root
end
attr_accessor :env, :root
def const_defined?(const)
false
end
end
================================================
FILE: spec/support/fixtures/empty.html
================================================
================================================
FILE: spec/support/fixtures/fog.yml
================================================
development:
provider: AWS
aws_access_key_id: AWS_ID
aws_secret_access_key: AWS_SECRET
test:
provider: AWS
aws_access_key_id: AWS_ID
aws_secret_access_key: AWS_SECRET
================================================
FILE: spec/support/fixtures/s3.yml
================================================
development:
key: 54321
production:
key: 12345
test:
bucket: <%= ENV['S3_BUCKET'] %>
access_key_id: <%= ENV['S3_KEY'] %>
secret_access_key: <%= ENV['S3_SECRET'] %>
================================================
FILE: spec/support/fixtures/text.txt
================================================
paperclip!
================================================
FILE: spec/support/matchers/accept.rb
================================================
RSpec::Matchers.define :accept do |expected|
match do |actual|
actual.matches?(expected)
end
end
================================================
FILE: spec/support/matchers/exist.rb
================================================
RSpec::Matchers.define :exist do |expected|
match do |actual|
File.exist?(actual)
end
end
================================================
FILE: spec/support/matchers/have_column.rb
================================================
RSpec::Matchers.define :have_column do |column_name|
chain :with_default do |default|
@default = default
end
match do |columns|
column = columns.detect{|column| column.name == column_name }
column && column.default.to_s == @default.to_s
end
failure_message_method =
if RSpec::Version::STRING.to_i >= 3
:failure_message
else
:failure_message_for_should
end
send(failure_message_method) do |columns|
"expected to find '#{column_name}', " +
"default '#{@default}' " +
"in #{columns.map { |column| [column.name, column.default] }}"
end
end
================================================
FILE: spec/support/mock_attachment.rb
================================================
class MockAttachment
attr_accessor :updated_at, :original_filename
attr_reader :options
def initialize(options = {})
@options = options
@model = options[:model]
@responds_to_updated_at = options[:responds_to_updated_at]
@updated_at = options[:updated_at]
@original_filename = options[:original_filename]
end
def instance
@model
end
def respond_to?(meth)
if meth.to_s == "updated_at"
@responds_to_updated_at || @updated_at
else
super
end
end
end
================================================
FILE: spec/support/mock_interpolator.rb
================================================
class MockInterpolator
def initialize(options = {})
@options = options
end
def interpolate(pattern, attachment, style_name)
@interpolated_pattern = pattern
@interpolated_attachment = attachment
@interpolated_style_name = style_name
@options[:result]
end
def has_interpolated_pattern?(pattern)
@interpolated_pattern == pattern
end
def has_interpolated_style_name?(style_name)
@interpolated_style_name == style_name
end
def has_interpolated_attachment?(attachment)
@interpolated_attachment == attachment
end
end
================================================
FILE: spec/support/mock_url_generator_builder.rb
================================================
class MockUrlGeneratorBuilder
def initializer
end
def new(attachment)
@attachment = attachment
@attachment_options = @attachment.options
self
end
def for(style_name, options)
@generated_url_with_style_name = style_name
@generated_url_with_options = options
"hello"
end
def has_generated_url_with_options?(options)
# options.is_a_subhash_of(@generated_url_with_options)
options.inject(true) do |acc,(k,v)|
acc && @generated_url_with_options[k] == v
end
end
def has_generated_url_with_style_name?(style_name)
@generated_url_with_style_name == style_name
end
end
================================================
FILE: spec/support/model_reconstruction.rb
================================================
module ModelReconstruction
def reset_class class_name
ActiveRecord::Base.send(:include, Paperclip::Glue)
Object.send(:remove_const, class_name) rescue nil
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
klass.class_eval do
include Paperclip::Glue
end
klass.reset_column_information
klass.connection_pool.clear_table_cache!(klass.table_name) if klass.connection_pool.respond_to?(:clear_table_cache!)
if klass.connection.respond_to?(:schema_cache)
if ActiveRecord::VERSION::STRING >= "5.0"
klass.connection.schema_cache.clear_data_source_cache!(klass.table_name)
else
klass.connection.schema_cache.clear_table_cache!(klass.table_name)
end
end
klass
end
def reset_table table_name, &block
block ||= lambda { |table| true }
ActiveRecord::Base.connection.create_table :dummies, {force: true}, &block
end
def modify_table &block
ActiveRecord::Base.connection.change_table :dummies, &block
end
def rebuild_model options = {}
ActiveRecord::Base.connection.create_table :dummies, force: true do |table|
table.column :title, :string
table.column :other, :string
table.column :avatar_file_name, :string
table.column :avatar_content_type, :string
table.column :avatar_file_size, :bigint
table.column :avatar_updated_at, :datetime
table.column :avatar_fingerprint, :string
end
rebuild_class options
end
def rebuild_class options = {}
reset_class("Dummy").tap do |klass|
klass.has_attached_file :avatar, options
klass.do_not_validate_attachment_file_type :avatar
Paperclip.reset_duplicate_clash_check!
end
end
def rebuild_meta_class_of obj, options = {}
meta_class_of(obj).tap do |metaklass|
metaklass.has_attached_file :avatar, options
metaklass.do_not_validate_attachment_file_type :avatar
Paperclip.reset_duplicate_clash_check!
end
end
def meta_class_of(obj)
class << obj
self
end
end
end
================================================
FILE: spec/support/reporting.rb
================================================
module Reporting
def silence_stream(stream)
old_stream = stream.dup
stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
stream.sync = true
yield
ensure
stream.reopen(old_stream)
old_stream.close
end
end
================================================
FILE: spec/support/test_data.rb
================================================
module TestData
def attachment(options={})
Paperclip::Attachment.new(:avatar, FakeModel.new, options)
end
def stringy_file
StringIO.new('.\n')
end
def fixture_file(filename)
File.join(File.dirname(__FILE__), 'fixtures', filename)
end
end
================================================
FILE: spec/support/version_helper.rb
================================================
module VersionHelper
def active_support_version
ActiveSupport::VERSION::STRING
end
def ruby_version
RUBY_VERSION
end
end