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: ``` ``` 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 ``` ``` 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 --- [![Build Status](https://secure.travis-ci.org/thoughtbot/paperclip.svg?branch=master)](http://travis-ci.org/thoughtbot/paperclip) [![Dependency Status](https://gemnasium.com/thoughtbot/paperclip.svg?travis)](https://gemnasium.com/thoughtbot/paperclip) [![Code Climate](https://codeclimate.com/github/thoughtbot/paperclip.svg)](https://codeclimate.com/github/thoughtbot/paperclip) [![Inline docs](http://inch-ci.org/github/thoughtbot/paperclip.svg)](http://inch-ci.org/github/thoughtbot/paperclip) [![Security](https://hakiri.io/github/thoughtbot/paperclip/master.svg)](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: ![untitled](https://cloud.githubusercontent.com/assets/1104431/4524452/a1f8cce4-4d44-11e4-872e-17adb96f79c9.png) 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 ---------------- ![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg) 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