kt-paperclip 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +17 -0
  3. data/.github/issue_template.md +3 -0
  4. data/.gitignore +19 -0
  5. data/.hound.yml +1050 -0
  6. data/.rubocop.yml +1 -0
  7. data/.travis.yml +47 -0
  8. data/Appraisals +24 -0
  9. data/CONTRIBUTING.md +86 -0
  10. data/Gemfile +18 -0
  11. data/LICENSE +24 -0
  12. data/NEWS +515 -0
  13. data/README.md +1053 -0
  14. data/RELEASING.md +17 -0
  15. data/Rakefile +52 -0
  16. data/UPGRADING +17 -0
  17. data/features/basic_integration.feature +85 -0
  18. data/features/migration.feature +29 -0
  19. data/features/rake_tasks.feature +62 -0
  20. data/features/step_definitions/attachment_steps.rb +110 -0
  21. data/features/step_definitions/html_steps.rb +15 -0
  22. data/features/step_definitions/rails_steps.rb +257 -0
  23. data/features/step_definitions/s3_steps.rb +14 -0
  24. data/features/step_definitions/web_steps.rb +106 -0
  25. data/features/support/env.rb +12 -0
  26. data/features/support/fakeweb.rb +11 -0
  27. data/features/support/file_helpers.rb +34 -0
  28. data/features/support/fixtures/boot_config.txt +15 -0
  29. data/features/support/fixtures/gemfile.txt +5 -0
  30. data/features/support/fixtures/preinitializer.txt +20 -0
  31. data/features/support/paths.rb +28 -0
  32. data/features/support/rails.rb +39 -0
  33. data/features/support/selectors.rb +19 -0
  34. data/gemfiles/4.2.gemfile +20 -0
  35. data/gemfiles/5.0.gemfile +20 -0
  36. data/gemfiles/5.1.gemfile +20 -0
  37. data/gemfiles/5.2.gemfile +20 -0
  38. data/gemfiles/6.0.gemfile +20 -0
  39. data/lib/generators/paperclip/USAGE +8 -0
  40. data/lib/generators/paperclip/paperclip_generator.rb +36 -0
  41. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +15 -0
  42. data/lib/paperclip.rb +215 -0
  43. data/lib/paperclip/attachment.rb +617 -0
  44. data/lib/paperclip/attachment_registry.rb +60 -0
  45. data/lib/paperclip/callbacks.rb +42 -0
  46. data/lib/paperclip/content_type_detector.rb +80 -0
  47. data/lib/paperclip/errors.rb +34 -0
  48. data/lib/paperclip/file_command_content_type_detector.rb +28 -0
  49. data/lib/paperclip/filename_cleaner.rb +15 -0
  50. data/lib/paperclip/geometry.rb +157 -0
  51. data/lib/paperclip/geometry_detector_factory.rb +45 -0
  52. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  53. data/lib/paperclip/glue.rb +17 -0
  54. data/lib/paperclip/has_attached_file.rb +116 -0
  55. data/lib/paperclip/helpers.rb +60 -0
  56. data/lib/paperclip/interpolations.rb +201 -0
  57. data/lib/paperclip/interpolations/plural_cache.rb +18 -0
  58. data/lib/paperclip/io_adapters/abstract_adapter.rb +75 -0
  59. data/lib/paperclip/io_adapters/attachment_adapter.rb +47 -0
  60. data/lib/paperclip/io_adapters/data_uri_adapter.rb +22 -0
  61. data/lib/paperclip/io_adapters/empty_string_adapter.rb +19 -0
  62. data/lib/paperclip/io_adapters/file_adapter.rb +26 -0
  63. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +16 -0
  64. data/lib/paperclip/io_adapters/identity_adapter.rb +17 -0
  65. data/lib/paperclip/io_adapters/nil_adapter.rb +37 -0
  66. data/lib/paperclip/io_adapters/registry.rb +36 -0
  67. data/lib/paperclip/io_adapters/stringio_adapter.rb +36 -0
  68. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +44 -0
  69. data/lib/paperclip/io_adapters/uri_adapter.rb +68 -0
  70. data/lib/paperclip/locales/en.yml +18 -0
  71. data/lib/paperclip/logger.rb +21 -0
  72. data/lib/paperclip/matchers.rb +64 -0
  73. data/lib/paperclip/matchers/have_attached_file_matcher.rb +54 -0
  74. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +101 -0
  75. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +59 -0
  76. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +97 -0
  77. data/lib/paperclip/media_type_spoof_detector.rb +90 -0
  78. data/lib/paperclip/missing_attachment_styles.rb +84 -0
  79. data/lib/paperclip/processor.rb +56 -0
  80. data/lib/paperclip/processor_helpers.rb +52 -0
  81. data/lib/paperclip/rails_environment.rb +21 -0
  82. data/lib/paperclip/railtie.rb +31 -0
  83. data/lib/paperclip/schema.rb +81 -0
  84. data/lib/paperclip/storage.rb +3 -0
  85. data/lib/paperclip/storage/filesystem.rb +99 -0
  86. data/lib/paperclip/storage/fog.rb +252 -0
  87. data/lib/paperclip/storage/s3.rb +461 -0
  88. data/lib/paperclip/style.rb +106 -0
  89. data/lib/paperclip/tempfile.rb +42 -0
  90. data/lib/paperclip/tempfile_factory.rb +22 -0
  91. data/lib/paperclip/thumbnail.rb +131 -0
  92. data/lib/paperclip/url_generator.rb +76 -0
  93. data/lib/paperclip/validators.rb +73 -0
  94. data/lib/paperclip/validators/attachment_content_type_validator.rb +88 -0
  95. data/lib/paperclip/validators/attachment_file_name_validator.rb +75 -0
  96. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +28 -0
  97. data/lib/paperclip/validators/attachment_presence_validator.rb +28 -0
  98. data/lib/paperclip/validators/attachment_size_validator.rb +109 -0
  99. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +29 -0
  100. data/lib/paperclip/version.rb +3 -0
  101. data/lib/tasks/paperclip.rake +140 -0
  102. data/paperclip.gemspec +50 -0
  103. data/shoulda_macros/paperclip.rb +134 -0
  104. data/spec/database.yml +4 -0
  105. data/spec/paperclip/attachment_definitions_spec.rb +13 -0
  106. data/spec/paperclip/attachment_processing_spec.rb +79 -0
  107. data/spec/paperclip/attachment_registry_spec.rb +158 -0
  108. data/spec/paperclip/attachment_spec.rb +1590 -0
  109. data/spec/paperclip/content_type_detector_spec.rb +47 -0
  110. data/spec/paperclip/file_command_content_type_detector_spec.rb +40 -0
  111. data/spec/paperclip/filename_cleaner_spec.rb +13 -0
  112. data/spec/paperclip/geometry_detector_spec.rb +38 -0
  113. data/spec/paperclip/geometry_parser_spec.rb +73 -0
  114. data/spec/paperclip/geometry_spec.rb +255 -0
  115. data/spec/paperclip/glue_spec.rb +42 -0
  116. data/spec/paperclip/has_attached_file_spec.rb +78 -0
  117. data/spec/paperclip/integration_spec.rb +702 -0
  118. data/spec/paperclip/interpolations_spec.rb +270 -0
  119. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +160 -0
  120. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +140 -0
  121. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +88 -0
  122. data/spec/paperclip/io_adapters/empty_string_adapter_spec.rb +17 -0
  123. data/spec/paperclip/io_adapters/file_adapter_spec.rb +131 -0
  124. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +137 -0
  125. data/spec/paperclip/io_adapters/identity_adapter_spec.rb +8 -0
  126. data/spec/paperclip/io_adapters/nil_adapter_spec.rb +25 -0
  127. data/spec/paperclip/io_adapters/registry_spec.rb +35 -0
  128. data/spec/paperclip/io_adapters/stringio_adapter_spec.rb +64 -0
  129. data/spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb +146 -0
  130. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +221 -0
  131. data/spec/paperclip/matchers/have_attached_file_matcher_spec.rb +19 -0
  132. data/spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb +108 -0
  133. data/spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb +69 -0
  134. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +88 -0
  135. data/spec/paperclip/media_type_spoof_detector_spec.rb +120 -0
  136. data/spec/paperclip/meta_class_spec.rb +30 -0
  137. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +88 -0
  138. data/spec/paperclip/paperclip_spec.rb +196 -0
  139. data/spec/paperclip/plural_cache_spec.rb +37 -0
  140. data/spec/paperclip/processor_helpers_spec.rb +57 -0
  141. data/spec/paperclip/processor_spec.rb +26 -0
  142. data/spec/paperclip/rails_environment_spec.rb +30 -0
  143. data/spec/paperclip/rake_spec.rb +103 -0
  144. data/spec/paperclip/schema_spec.rb +252 -0
  145. data/spec/paperclip/storage/filesystem_spec.rb +79 -0
  146. data/spec/paperclip/storage/fog_spec.rb +560 -0
  147. data/spec/paperclip/storage/s3_live_spec.rb +188 -0
  148. data/spec/paperclip/storage/s3_spec.rb +1695 -0
  149. data/spec/paperclip/style_spec.rb +251 -0
  150. data/spec/paperclip/tempfile_factory_spec.rb +33 -0
  151. data/spec/paperclip/tempfile_spec.rb +35 -0
  152. data/spec/paperclip/thumbnail_spec.rb +504 -0
  153. data/spec/paperclip/url_generator_spec.rb +221 -0
  154. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +322 -0
  155. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +159 -0
  156. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +85 -0
  157. data/spec/paperclip/validators/attachment_size_validator_spec.rb +235 -0
  158. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +48 -0
  159. data/spec/paperclip/validators_spec.rb +164 -0
  160. data/spec/spec_helper.rb +45 -0
  161. data/spec/support/assertions.rb +84 -0
  162. data/spec/support/fake_model.rb +24 -0
  163. data/spec/support/fake_rails.rb +12 -0
  164. data/spec/support/fixtures/12k.png +0 -0
  165. data/spec/support/fixtures/50x50.png +0 -0
  166. data/spec/support/fixtures/5k.png +0 -0
  167. data/spec/support/fixtures/animated +0 -0
  168. data/spec/support/fixtures/animated.gif +0 -0
  169. data/spec/support/fixtures/animated.unknown +0 -0
  170. data/spec/support/fixtures/bad.png +1 -0
  171. data/spec/support/fixtures/empty.html +1 -0
  172. data/spec/support/fixtures/empty.xlsx +0 -0
  173. data/spec/support/fixtures/fog.yml +8 -0
  174. data/spec/support/fixtures/rotated.jpg +0 -0
  175. data/spec/support/fixtures/s3.yml +8 -0
  176. data/spec/support/fixtures/spaced file.jpg +0 -0
  177. data/spec/support/fixtures/spaced file.png +0 -0
  178. data/spec/support/fixtures/text.txt +1 -0
  179. data/spec/support/fixtures/twopage.pdf +0 -0
  180. data/spec/support/fixtures/uppercase.PNG +0 -0
  181. data/spec/support/matchers/accept.rb +5 -0
  182. data/spec/support/matchers/exist.rb +5 -0
  183. data/spec/support/matchers/have_column.rb +23 -0
  184. data/spec/support/mock_attachment.rb +24 -0
  185. data/spec/support/mock_interpolator.rb +24 -0
  186. data/spec/support/mock_url_generator_builder.rb +26 -0
  187. data/spec/support/model_reconstruction.rb +72 -0
  188. data/spec/support/reporting.rb +11 -0
  189. data/spec/support/test_data.rb +13 -0
  190. data/spec/support/version_helper.rb +9 -0
  191. metadata +586 -0
@@ -0,0 +1,15 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
2
+ def self.up
3
+ change_table :<%= table_name %> do |t|
4
+ <% attachment_names.each do |attachment| -%>
5
+ t.attachment :<%= attachment %>
6
+ <% end -%>
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ <% attachment_names.each do |attachment| -%>
12
+ remove_attachment :<%= table_name %>, :<%= attachment %>
13
+ <% end -%>
14
+ end
15
+ end
@@ -0,0 +1,215 @@
1
+ # Paperclip allows file attachments that are stored in the filesystem. All graphical
2
+ # transformations are done using the Graphics/ImageMagick command line utilities and
3
+ # are stored in Tempfiles until the record is saved. Paperclip does not require a
4
+ # separate model for storing the attachment's information, instead adding a few simple
5
+ # columns to your table.
6
+ #
7
+ # Author:: Jon Yurek
8
+ # Copyright:: Copyright (c) 2008-2011 thoughtbot, inc.
9
+ # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
10
+ #
11
+ # Paperclip defines an attachment as any file, though it makes special considerations
12
+ # for image files. You can declare that a model has an attached file with the
13
+ # +has_attached_file+ method:
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # has_attached_file :avatar, :styles => { :thumb => "100x100" }
17
+ # end
18
+ #
19
+ # user = User.new
20
+ # user.avatar = params[:user][:avatar]
21
+ # user.avatar.url
22
+ # # => "/users/avatars/4/original_me.jpg"
23
+ # user.avatar.url(:thumb)
24
+ # # => "/users/avatars/4/thumb_me.jpg"
25
+ #
26
+ # See the +has_attached_file+ documentation for more details.
27
+
28
+ require "erb"
29
+ require "digest"
30
+ require "tempfile"
31
+ require "paperclip/version"
32
+ require "paperclip/geometry_parser_factory"
33
+ require "paperclip/geometry_detector_factory"
34
+ require "paperclip/geometry"
35
+ require "paperclip/processor"
36
+ require "paperclip/processor_helpers"
37
+ require "paperclip/tempfile"
38
+ require "paperclip/thumbnail"
39
+ require "paperclip/interpolations/plural_cache"
40
+ require "paperclip/interpolations"
41
+ require "paperclip/tempfile_factory"
42
+ require "paperclip/style"
43
+ require "paperclip/attachment"
44
+ require "paperclip/storage"
45
+ require "paperclip/callbacks"
46
+ require "paperclip/file_command_content_type_detector"
47
+ require "paperclip/media_type_spoof_detector"
48
+ require "paperclip/content_type_detector"
49
+ require "paperclip/glue"
50
+ require "paperclip/errors"
51
+ require "paperclip/missing_attachment_styles"
52
+ require "paperclip/validators"
53
+ require "paperclip/logger"
54
+ require "paperclip/helpers"
55
+ require "paperclip/has_attached_file"
56
+ require "paperclip/attachment_registry"
57
+ require "paperclip/filename_cleaner"
58
+ require "paperclip/rails_environment"
59
+
60
+ begin
61
+ # Use mime/types/columnar if available, for reduced memory usage
62
+ require "mime/types/columnar"
63
+ rescue LoadError
64
+ require "mime/types"
65
+ end
66
+
67
+ require "mimemagic"
68
+ require "mimemagic/overlay"
69
+ require "logger"
70
+ require "terrapin"
71
+
72
+ require "paperclip/railtie" if defined?(Rails::Railtie)
73
+
74
+ # The base module that gets included in ActiveRecord::Base. See the
75
+ # documentation for Paperclip::ClassMethods for more useful information.
76
+ module Paperclip
77
+ extend Helpers
78
+ extend Logger
79
+ extend ProcessorHelpers
80
+
81
+ # Provides configurability to Paperclip. The options available are:
82
+ # * whiny: Will raise an error if Paperclip cannot process thumbnails of
83
+ # an uploaded image. Defaults to true.
84
+ # * log: Logs progress to the Rails log. Uses ActiveRecord's logger, so honors
85
+ # log levels, etc. Defaults to true.
86
+ # * command_path: Defines the path at which to find the command line
87
+ # programs if they are not visible to Rails the system's search path. Defaults to
88
+ # nil, which uses the first executable found in the user's search path.
89
+ # * use_exif_orientation: Whether to inspect EXIF data to determine an
90
+ # image's orientation. Defaults to true.
91
+ def self.options
92
+ @options ||= {
93
+ command_path: nil,
94
+ content_type_mappings: {},
95
+ log: true,
96
+ log_command: true,
97
+ read_timeout: nil,
98
+ swallow_stderr: true,
99
+ use_exif_orientation: true,
100
+ whiny: true,
101
+ is_windows: Gem.win_platform?
102
+ }
103
+ end
104
+
105
+ def self.io_adapters=(new_registry)
106
+ @io_adapters = new_registry
107
+ end
108
+
109
+ def self.io_adapters
110
+ @io_adapters ||= Paperclip::AdapterRegistry.new
111
+ end
112
+
113
+ module ClassMethods
114
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
115
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
116
+ # The attribute returns a Paperclip::Attachment object which handles the management of
117
+ # that file. The intent is to make the attachment as much like a normal attribute. The
118
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
119
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
120
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
121
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
122
+ # you can set to change the behavior of a Paperclip attachment:
123
+ # * +url+: The full URL of where the attachment is publicly accessible. This can just
124
+ # as easily point to a directory served directly through Apache as it can to an action
125
+ # that can control permissions. You can specify the full domain and path, but usually
126
+ # just an absolute path is sufficient. The leading slash *must* be included manually for
127
+ # absolute paths. The default value is
128
+ # "/system/:class/:attachment/:id_partition/:style/:filename". See
129
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
130
+ # :url => "/:class/:attachment/:id/:style_:filename"
131
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
132
+ # Note: When using the +s3+ storage option, the +url+ option expects
133
+ # particular values. See the Paperclip::Storage::S3#url documentation for
134
+ # specifics.
135
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
136
+ # This field is interpolated just as the url is. The default value is
137
+ # "/:attachment/:style/missing.png"
138
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
139
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
140
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
141
+ # geometry strings at the ImageMagick website
142
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
143
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
144
+ # inside the dimensions and then crop the rest off (weighted at the center). The
145
+ # default value is to generate no thumbnails.
146
+ # * +default_style+: The thumbnail style that will be used by default URLs.
147
+ # Defaults to +original+.
148
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
149
+ # :default_style => :normal
150
+ # user.avatar.url # => "/avatars/23/normal_me.png"
151
+ # * +keep_old_files+: Keep the existing attachment files (original + resized) from
152
+ # being automatically deleted when an attachment is cleared or updated. Defaults to +false+.
153
+ # * +preserve_files+: Keep the existing attachment files in all cases, even if the parent
154
+ # record is destroyed. Defaults to +false+.
155
+ # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
156
+ # to a command line error. This will override the global setting for this attachment.
157
+ # Defaults to true.
158
+ # * +convert_options+: When creating thumbnails, use this free-form options
159
+ # array to pass in various convert command options. Typical options are "-strip" to
160
+ # remove all Exif data from the image (save space for thumbnails and avatars) or
161
+ # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
162
+ # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
163
+ # Note that this option takes a hash of options, each of which correspond to the style
164
+ # of thumbnail being generated. You can also specify :all as a key, which will apply
165
+ # to all of the thumbnails being generated. If you specify options for the :original,
166
+ # it would be best if you did not specify destructive options, as the intent of keeping
167
+ # the original around is to regenerate all the thumbnails when requirements change.
168
+ # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
169
+ # :convert_options => {
170
+ # :all => "-strip",
171
+ # :negative => "-negate"
172
+ # }
173
+ # NOTE: While not deprecated yet, it is not recommended to specify options this way.
174
+ # It is recommended that :convert_options option be included in the hash passed to each
175
+ # :styles for compatibility with future versions.
176
+ # NOTE: Strings supplied to :convert_options are split on space in order to undergo
177
+ # shell quoting for safety. If your options require a space, please pre-split them
178
+ # and pass an array to :convert_options instead.
179
+ # * +storage+: Chooses the storage backend where the files will be stored. The current
180
+ # choices are :filesystem, :fog and :s3. The default is :filesystem. Make sure you read the
181
+ # documentation for Paperclip::Storage::Filesystem, Paperclip::Storage::Fog and Paperclip::Storage::S3
182
+ # for backend-specific options.
183
+ #
184
+ # It's also possible for you to dynamically define your interpolation string for :url,
185
+ # :default_url, and :path in your model by passing a method name as a symbol as a argument
186
+ # for your has_attached_file definition:
187
+ #
188
+ # class Person
189
+ # has_attached_file :avatar, :default_url => :default_url_by_gender
190
+ #
191
+ # private
192
+ #
193
+ # def default_url_by_gender
194
+ # "/assets/avatars/default_#{gender}.png"
195
+ # end
196
+ # end
197
+ def has_attached_file(name, options = {})
198
+ HasAttachedFile.define_on(self, name, options)
199
+ end
200
+ end
201
+ end
202
+
203
+ # This stuff needs to be run after Paperclip is defined.
204
+ require "paperclip/io_adapters/registry"
205
+ require "paperclip/io_adapters/abstract_adapter"
206
+ require "paperclip/io_adapters/empty_string_adapter"
207
+ require "paperclip/io_adapters/identity_adapter"
208
+ require "paperclip/io_adapters/file_adapter"
209
+ require "paperclip/io_adapters/stringio_adapter"
210
+ require "paperclip/io_adapters/data_uri_adapter"
211
+ require "paperclip/io_adapters/nil_adapter"
212
+ require "paperclip/io_adapters/attachment_adapter"
213
+ require "paperclip/io_adapters/uploaded_file_adapter"
214
+ require "paperclip/io_adapters/uri_adapter"
215
+ require "paperclip/io_adapters/http_url_proxy_adapter"
@@ -0,0 +1,617 @@
1
+ require "uri"
2
+ require "paperclip/url_generator"
3
+ require "active_support/deprecation"
4
+ require "active_support/core_ext/string/inflections"
5
+
6
+ module Paperclip
7
+ # The Attachment class manages the files for a given attachment. It saves
8
+ # when the model saves, deletes when the model is destroyed, and processes
9
+ # the file upon assignment.
10
+ class Attachment
11
+ def self.default_options
12
+ @default_options ||= {
13
+ convert_options: {},
14
+ default_style: :original,
15
+ default_url: "/:attachment/:style/missing.png",
16
+ escape_url: true,
17
+ restricted_characters: /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
18
+ filename_cleaner: nil,
19
+ hash_data: ":class/:attachment/:id/:style/:updated_at",
20
+ hash_digest: "SHA1",
21
+ interpolator: Paperclip::Interpolations,
22
+ only_process: [],
23
+ path: ":rails_root/public:url",
24
+ preserve_files: false,
25
+ processors: [:thumbnail],
26
+ source_file_options: {},
27
+ storage: :filesystem,
28
+ styles: {},
29
+ url: "/system/:class/:attachment/:id_partition/:style/:filename",
30
+ url_generator: Paperclip::UrlGenerator,
31
+ use_default_time_zone: true,
32
+ use_timestamp: true,
33
+ whiny: Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
34
+ validate_media_type: true,
35
+ adapter_options: { hash_digest: Digest::MD5 },
36
+ check_validity_before_processing: true
37
+ }
38
+ end
39
+
40
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny,
41
+ :options, :interpolator, :source_file_options
42
+ attr_accessor :post_processing
43
+
44
+ # Creates an Attachment object. +name+ is the name of the attachment,
45
+ # +instance+ is the model object instance it's attached to, and
46
+ # +options+ is the same as the hash passed to +has_attached_file+.
47
+ #
48
+ # Options include:
49
+ #
50
+ # +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
51
+ # +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
52
+ # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
53
+ # +only_process+ - style args to be run through the post-processor. This defaults to the empty list (which is
54
+ # a special case that indicates all styles should be processed)
55
+ # +default_url+ - a URL for the missing image
56
+ # +default_style+ - the style to use when an argument is not specified e.g. #url, #path
57
+ # +storage+ - the storage mechanism. Defaults to :filesystem
58
+ # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
59
+ # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
60
+ # +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
61
+ # +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
62
+ # +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
63
+ # +hash_secret+ - a secret passed to the +hash_digest+
64
+ # +convert_options+ - flags passed to the +convert+ command for processing
65
+ # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
66
+ # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
67
+ # +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
68
+ # +filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
69
+ # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
70
+ # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
71
+ # +escape_url+ - Perform URI escaping to URLs. Defaults to true
72
+ def initialize(name, instance, options = {})
73
+ @name = name.to_sym
74
+ @name_string = name.to_s
75
+ @instance = instance
76
+
77
+ options = self.class.default_options.deep_merge(options)
78
+
79
+ @options = options
80
+ @post_processing = true
81
+ @queued_for_delete = []
82
+ @queued_for_write = {}
83
+ @errors = {}
84
+ @dirty = false
85
+ @interpolator = options[:interpolator]
86
+ @url_generator = options[:url_generator].new(self)
87
+ @source_file_options = options[:source_file_options]
88
+ @whiny = options[:whiny]
89
+
90
+ initialize_storage
91
+ end
92
+
93
+ # What gets called when you call instance.attachment = File. It clears
94
+ # errors, assigns attributes, and processes the file. It also queues up the
95
+ # previous file for deletion, to be flushed away on #save of its host. In
96
+ # addition to form uploads, you can also assign another Paperclip
97
+ # attachment:
98
+ # new_user.avatar = old_user.avatar
99
+ def assign(uploaded_file)
100
+ @file = Paperclip.io_adapters.for(uploaded_file,
101
+ @options[:adapter_options])
102
+ ensure_required_accessors!
103
+ ensure_required_validations!
104
+
105
+ if @file.assignment?
106
+ clear(*only_process)
107
+
108
+ if @file.nil?
109
+ nil
110
+ else
111
+ assign_attributes
112
+ post_process_file
113
+ reset_file_if_original_reprocessed
114
+ end
115
+ end
116
+ end
117
+
118
+ # Returns the public URL of the attachment with a given style. This does
119
+ # not necessarily need to point to a file that your Web server can access
120
+ # and can instead point to an action in your app, for example for fine grained
121
+ # security; this has a serious performance tradeoff.
122
+ #
123
+ # Options:
124
+ #
125
+ # +timestamp+ - Add a timestamp to the end of the URL. Default: true.
126
+ # +escape+ - Perform URI escaping to the URL. Default: true.
127
+ #
128
+ # Global controls (set on has_attached_file):
129
+ #
130
+ # +interpolator+ - The object that fills in a URL pattern's variables.
131
+ # +default_url+ - The image to show when the attachment has no image.
132
+ # +url+ - The URL for a saved image.
133
+ # +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
134
+ #
135
+ # As mentioned just above, the object that generates this URL can be passed
136
+ # in, for finer control. This object must respond to two methods:
137
+ #
138
+ # +#new(Paperclip::Attachment, options_hash)+
139
+ # +#for(style_name, options_hash)+
140
+
141
+ def url(style_name = default_style, options = {})
142
+ if options == true || options == false # Backwards compatibility.
143
+ @url_generator.for(style_name, default_options.merge(timestamp: options))
144
+ else
145
+ @url_generator.for(style_name, default_options.merge(options))
146
+ end
147
+ end
148
+
149
+ def default_options
150
+ {
151
+ timestamp: @options[:use_timestamp],
152
+ escape: @options[:escape_url]
153
+ }
154
+ end
155
+
156
+ # Alias to +url+ that allows using the expiring_url method provided by the cloud
157
+ # storage implementations, but keep using filesystem storage for development and
158
+ # testing.
159
+ def expiring_url(_time = 3600, style_name = default_style)
160
+ url(style_name)
161
+ end
162
+
163
+ # Returns the path of the attachment as defined by the :path option. If the
164
+ # file is stored in the filesystem the path refers to the path of the file
165
+ # on disk. If the file is stored in S3, the path is the "key" part of the
166
+ # URL, and the :bucket option refers to the S3 bucket.
167
+ def path(style_name = default_style)
168
+ path = original_filename.nil? ? nil : interpolate(path_option, style_name)
169
+ path.respond_to?(:unescape) ? path.unescape : path
170
+ end
171
+
172
+ # :nodoc:
173
+ def staged_path(style_name = default_style)
174
+ @queued_for_write[style_name].path if staged?
175
+ end
176
+
177
+ # :nodoc:
178
+ def staged?
179
+ !@queued_for_write.empty?
180
+ end
181
+
182
+ # Alias to +url+
183
+ def to_s(style_name = default_style)
184
+ url(style_name)
185
+ end
186
+
187
+ def as_json(options = nil)
188
+ to_s((options && options[:style]) || default_style)
189
+ end
190
+
191
+ def default_style
192
+ @options[:default_style]
193
+ end
194
+
195
+ def styles
196
+ if @options[:styles].respond_to?(:call) || @normalized_styles.nil?
197
+ styles = @options[:styles]
198
+ styles = styles.call(self) if styles.respond_to?(:call)
199
+
200
+ @normalized_styles = styles.dup
201
+ styles.each_pair do |name, options|
202
+ @normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
203
+ end
204
+ end
205
+ @normalized_styles
206
+ end
207
+
208
+ def only_process
209
+ only_process = @options[:only_process].dup
210
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
211
+ only_process.map(&:to_sym)
212
+ end
213
+
214
+ def processors
215
+ processing_option = @options[:processors]
216
+
217
+ if processing_option.respond_to?(:call)
218
+ processing_option.call(instance)
219
+ else
220
+ processing_option
221
+ end
222
+ end
223
+
224
+ # Returns an array containing the errors on this attachment.
225
+ def errors
226
+ @errors
227
+ end
228
+
229
+ # Returns true if there are changes that need to be saved.
230
+ def dirty?
231
+ @dirty
232
+ end
233
+
234
+ # Saves the file, if there are no errors. If there are, it flushes them to
235
+ # the instance's errors and returns false, cancelling the save.
236
+ def save
237
+ flush_deletes unless @options[:keep_old_files]
238
+ process = only_process
239
+ @queued_for_write.except!(:original) if process.any? && !process.include?(:original)
240
+ flush_writes
241
+ @dirty = false
242
+ true
243
+ end
244
+
245
+ # Clears out the attachment. Has the same effect as previously assigning
246
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
247
+ # use #destroy.
248
+ def clear(*styles_to_clear)
249
+ if styles_to_clear.any?
250
+ queue_some_for_delete(*styles_to_clear)
251
+ else
252
+ queue_all_for_delete
253
+ @queued_for_write = {}
254
+ @errors = {}
255
+ end
256
+ end
257
+
258
+ # Destroys the attachment. Has the same effect as previously assigning
259
+ # nil to the attachment *and saving*. This is permanent. If you wish to
260
+ # wipe out the existing attachment but not save, use #clear.
261
+ def destroy
262
+ clear
263
+ save
264
+ end
265
+
266
+ # Returns the uploaded file if present.
267
+ def uploaded_file
268
+ instance_read(:uploaded_file)
269
+ end
270
+
271
+ # Returns the name of the file as originally assigned, and lives in the
272
+ # <attachment>_file_name attribute of the model.
273
+ def original_filename
274
+ instance_read(:file_name)
275
+ end
276
+
277
+ # Returns the size of the file as originally assigned, and lives in the
278
+ # <attachment>_file_size attribute of the model.
279
+ def size
280
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
281
+ end
282
+
283
+ # Returns the fingerprint of the file, if one's defined. The fingerprint is
284
+ # stored in the <attachment>_fingerprint attribute of the model.
285
+ def fingerprint
286
+ instance_read(:fingerprint)
287
+ end
288
+
289
+ # Returns the content_type of the file as originally assigned, and lives
290
+ # in the <attachment>_content_type attribute of the model.
291
+ def content_type
292
+ instance_read(:content_type)
293
+ end
294
+
295
+ # Returns the creation time of the file as originally assigned, and
296
+ # lives in the <attachment>_created_at attribute of the model.
297
+ def created_at
298
+ if able_to_store_created_at?
299
+ time = instance_read(:created_at)
300
+ time && time.to_f.to_i
301
+ end
302
+ end
303
+
304
+ # Returns the last modified time of the file as originally assigned, and
305
+ # lives in the <attachment>_updated_at attribute of the model.
306
+ def updated_at
307
+ time = instance_read(:updated_at)
308
+ time && time.to_f.to_i
309
+ end
310
+
311
+ # The time zone to use for timestamp interpolation. Using the default
312
+ # time zone ensures that results are consistent across all threads.
313
+ def time_zone
314
+ @options[:use_default_time_zone] ? Time.zone_default : Time.zone
315
+ end
316
+
317
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
318
+ # publicly viewable attachment.
319
+ def hash_key(style_name = default_style)
320
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret]
321
+
322
+ require "openssl" unless defined?(OpenSSL)
323
+ data = interpolate(@options[:hash_data], style_name)
324
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options[:hash_digest]).new, @options[:hash_secret], data)
325
+ end
326
+
327
+ # This method really shouldn't be called that often. Its expected use is
328
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
329
+ # thumbnails forcefully, by reobtaining the original file and going through
330
+ # the post-process again.
331
+ # NOTE: Calling reprocess WILL NOT delete existing files. This is due to
332
+ # inconsistencies in timing of S3 commands. It's possible that calling
333
+ # #reprocess! will lose data if the files are not kept.
334
+ def reprocess!(*style_args)
335
+ saved_flags = @options.slice(
336
+ :only_process,
337
+ :preserve_files,
338
+ :check_validity_before_processing
339
+ )
340
+ @options[:only_process] = style_args
341
+ @options[:preserve_files] = true
342
+ @options[:check_validity_before_processing] = false
343
+
344
+ begin
345
+ assign(self)
346
+ save
347
+ instance.save
348
+ rescue Errno::EACCES => e
349
+ warn "#{e} - skipping file."
350
+ false
351
+ ensure
352
+ @options.merge!(saved_flags)
353
+ end
354
+ end
355
+
356
+ # Returns true if a file has been assigned.
357
+ def file?
358
+ original_filename.present?
359
+ end
360
+
361
+ alias :present? :file?
362
+
363
+ def blank?
364
+ not present?
365
+ end
366
+
367
+ # Determines whether the instance responds to this attribute. Used to prevent
368
+ # calculations on fields we won't even store.
369
+ def instance_respond_to?(attr)
370
+ instance.respond_to?(:"#{name}_#{attr}")
371
+ end
372
+
373
+ # Writes the attachment-specific attribute on the instance. For example,
374
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
375
+ # "avatar_file_name" field (assuming the attachment is called avatar).
376
+ def instance_write(attr, value)
377
+ setter = :"#{@name_string}_#{attr}="
378
+ instance.send(setter, value) if instance.respond_to?(setter)
379
+ end
380
+
381
+ # Reads the attachment-specific attribute on the instance. See instance_write
382
+ # for more details.
383
+ def instance_read(attr)
384
+ getter = :"#{@name_string}_#{attr}"
385
+ instance.send(getter) if instance.respond_to?(getter)
386
+ end
387
+
388
+ private
389
+
390
+ def path_option
391
+ @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
392
+ end
393
+
394
+ def active_validator_classes
395
+ @instance.class.validators.map(&:class)
396
+ end
397
+
398
+ def missing_required_validator?
399
+ (active_validator_classes.flat_map(&:ancestors) & Paperclip::REQUIRED_VALIDATORS).empty?
400
+ end
401
+
402
+ def ensure_required_validations!
403
+ raise Paperclip::Errors::MissingRequiredValidatorError if missing_required_validator?
404
+ end
405
+
406
+ def ensure_required_accessors! #:nodoc:
407
+ %w(file_name).each do |field|
408
+ unless @instance.respond_to?("#{@name_string}_#{field}") && @instance.respond_to?("#{@name_string}_#{field}=")
409
+ raise Paperclip::Error.new("#{@instance.class} model missing required attr_accessor for '#{@name_string}_#{field}'")
410
+ end
411
+ end
412
+ end
413
+
414
+ def log(message) #:nodoc:
415
+ Paperclip.log(message)
416
+ end
417
+
418
+ def initialize_storage #:nodoc:
419
+ storage_class_name = @options[:storage].to_s.downcase.camelize
420
+ begin
421
+ storage_module = Paperclip::Storage.const_get(storage_class_name)
422
+ rescue NameError
423
+ raise Errors::StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
424
+ end
425
+ extend(storage_module)
426
+ end
427
+
428
+ def assign_attributes
429
+ @queued_for_write[:original] = @file
430
+ assign_file_information
431
+ assign_fingerprint { @file.fingerprint }
432
+ assign_timestamps
433
+ end
434
+
435
+ def assign_file_information
436
+ instance_write(:file_name, cleanup_filename(@file.original_filename))
437
+ instance_write(:content_type, @file.content_type.to_s.strip)
438
+ instance_write(:file_size, @file.size)
439
+ end
440
+
441
+ def assign_fingerprint
442
+ instance_write(:fingerprint, yield) if instance_respond_to?(:fingerprint)
443
+ end
444
+
445
+ def assign_timestamps
446
+ instance_write(:created_at, Time.now) if has_enabled_but_unset_created_at?
447
+
448
+ instance_write(:updated_at, Time.now)
449
+ end
450
+
451
+ def post_process_file
452
+ dirty!
453
+
454
+ post_process(*only_process) if post_processing
455
+ end
456
+
457
+ def dirty!
458
+ @dirty = true
459
+ end
460
+
461
+ def reset_file_if_original_reprocessed
462
+ instance_write(:file_size, @queued_for_write[:original].size)
463
+ assign_fingerprint { @queued_for_write[:original].fingerprint }
464
+ reset_updater
465
+ end
466
+
467
+ def reset_updater
468
+ instance.send(updater) if instance.respond_to?(updater)
469
+ end
470
+
471
+ def updater
472
+ :"#{name}_file_name_will_change!"
473
+ end
474
+
475
+ def extra_options_for(style) #:nodoc:
476
+ process_options(:convert_options, style)
477
+ end
478
+
479
+ def extra_source_file_options_for(style) #:nodoc:
480
+ process_options(:source_file_options, style)
481
+ end
482
+
483
+ def process_options(options_type, style) #:nodoc:
484
+ all_options = @options[options_type][:all]
485
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
486
+ style_options = @options[options_type][style]
487
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
488
+
489
+ [style_options, all_options].compact.join(" ")
490
+ end
491
+
492
+ def post_process(*style_args) #:nodoc:
493
+ return if @queued_for_write[:original].nil?
494
+ if !@options[:check_validity_before_processing] || check_validity?
495
+ instance.run_paperclip_callbacks(:post_process) do
496
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
497
+ post_process_styles(*style_args)
498
+ end
499
+ end
500
+ end
501
+ end
502
+
503
+ def check_validity?
504
+ instance.run_paperclip_callbacks(:"#{name}_validate")
505
+ instance.errors.none?
506
+ end
507
+
508
+ def post_process_styles(*style_args) #:nodoc:
509
+ if styles.include?(:original) && process_style?(:original, style_args)
510
+ post_process_style(:original, styles[:original])
511
+ end
512
+ styles.reject { |name, _style| name == :original }.each do |name, style|
513
+ post_process_style(name, style) if process_style?(name, style_args)
514
+ end
515
+ end
516
+
517
+ def post_process_style(name, style) #:nodoc:
518
+ raise "Style #{name} has no processors defined." if style.processors.blank?
519
+
520
+ intermediate_files = []
521
+ original = @queued_for_write[:original]
522
+
523
+ @queued_for_write[name] = style.processors.
524
+ inject(original) do |file, processor|
525
+ file = Paperclip.processor(processor).make(file, style.processor_options, self)
526
+ intermediate_files << file unless file == @queued_for_write[:original]
527
+ # if we're processing the original, close + unlink the source tempfile
528
+ @queued_for_write[:original].close(true) if name == :original
529
+ file
530
+ end
531
+
532
+ unadapted_file = @queued_for_write[name]
533
+ @queued_for_write[name] = Paperclip.io_adapters.
534
+ for(@queued_for_write[name], @options[:adapter_options])
535
+ unadapted_file.close if unadapted_file.respond_to?(:close)
536
+ @queued_for_write[name]
537
+ rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e
538
+ log("An error was received while processing: #{e.inspect}")
539
+ (@errors[:processing] ||= []) << e.message if @options[:whiny]
540
+ ensure
541
+ unlink_files(intermediate_files)
542
+ end
543
+
544
+ def process_style?(style_name, style_args) #:nodoc:
545
+ style_args.empty? || style_args.include?(style_name)
546
+ end
547
+
548
+ def interpolate(pattern, style_name = default_style) #:nodoc:
549
+ interpolator.interpolate(pattern, self, style_name)
550
+ end
551
+
552
+ def queue_some_for_delete(*styles)
553
+ @queued_for_delete += styles.uniq.map do |style|
554
+ path(style) if exists?(style)
555
+ end.compact
556
+ end
557
+
558
+ def queue_all_for_delete #:nodoc:
559
+ return if !file?
560
+
561
+ unless @options[:preserve_files]
562
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
563
+ path(style) if exists?(style)
564
+ end.compact
565
+ end
566
+ instance_write(:file_name, nil)
567
+ instance_write(:content_type, nil)
568
+ instance_write(:file_size, nil)
569
+ instance_write(:fingerprint, nil)
570
+ instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
571
+ instance_write(:updated_at, nil)
572
+ end
573
+
574
+ def flush_errors #:nodoc:
575
+ @errors.each do |_error, message|
576
+ [message].flatten.each { |m| instance.errors.add(name, m) }
577
+ end
578
+ end
579
+
580
+ # called by storage after the writes are flushed and before @queued_for_write is cleared
581
+ def after_flush_writes
582
+ unlink_files(@queued_for_write.values)
583
+ end
584
+
585
+ def unlink_files(files)
586
+ Array(files).each do |file|
587
+ file.close unless file.closed?
588
+
589
+ begin
590
+ file.unlink if file.respond_to?(:unlink)
591
+ rescue Errno::ENOENT
592
+ end
593
+ end
594
+ end
595
+
596
+ # You can either specifiy :restricted_characters or you can define your own
597
+ # :filename_cleaner object. This object needs to respond to #call and takes
598
+ # the filename that will be cleaned. It should return the cleaned filename.
599
+ def filename_cleaner
600
+ @options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
601
+ end
602
+
603
+ def cleanup_filename(filename)
604
+ filename_cleaner.call(filename)
605
+ end
606
+
607
+ # Check if attachment database table has a created_at field
608
+ def able_to_store_created_at?
609
+ @instance.respond_to?("#{name}_created_at".to_sym)
610
+ end
611
+
612
+ # Check if attachment database table has a created_at field which is not yet set
613
+ def has_enabled_but_unset_created_at?
614
+ able_to_store_created_at? && !instance_read(:created_at)
615
+ end
616
+ end
617
+ end