paperclip_jk 5.0.0.beta2

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 (190) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +21 -0
  3. data/.hound.yml +1066 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +26 -0
  6. data/Appraisals +27 -0
  7. data/CONTRIBUTING.md +86 -0
  8. data/Gemfile +15 -0
  9. data/LICENSE +24 -0
  10. data/NEWS +424 -0
  11. data/README.md +999 -0
  12. data/RELEASING.md +17 -0
  13. data/Rakefile +44 -0
  14. data/UPGRADING +17 -0
  15. data/features/basic_integration.feature +81 -0
  16. data/features/migration.feature +70 -0
  17. data/features/rake_tasks.feature +62 -0
  18. data/features/step_definitions/attachment_steps.rb +110 -0
  19. data/features/step_definitions/html_steps.rb +15 -0
  20. data/features/step_definitions/rails_steps.rb +230 -0
  21. data/features/step_definitions/s3_steps.rb +14 -0
  22. data/features/step_definitions/web_steps.rb +107 -0
  23. data/features/support/env.rb +11 -0
  24. data/features/support/fakeweb.rb +13 -0
  25. data/features/support/file_helpers.rb +34 -0
  26. data/features/support/fixtures/boot_config.txt +15 -0
  27. data/features/support/fixtures/gemfile.txt +5 -0
  28. data/features/support/fixtures/preinitializer.txt +20 -0
  29. data/features/support/paths.rb +28 -0
  30. data/features/support/rails.rb +63 -0
  31. data/features/support/selectors.rb +19 -0
  32. data/gemfiles/4.2.awsv2.0.gemfile +17 -0
  33. data/gemfiles/4.2.awsv2.1.gemfile +17 -0
  34. data/gemfiles/4.2.awsv2.gemfile +20 -0
  35. data/gemfiles/5.0.awsv2.0.gemfile +17 -0
  36. data/gemfiles/5.0.awsv2.1.gemfile +17 -0
  37. data/gemfiles/5.0.awsv2.gemfile +25 -0
  38. data/lib/generators/paperclip/USAGE +8 -0
  39. data/lib/generators/paperclip/paperclip_generator.rb +30 -0
  40. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +15 -0
  41. data/lib/paperclip.rb +211 -0
  42. data/lib/paperclip/attachment.rb +610 -0
  43. data/lib/paperclip/attachment_registry.rb +60 -0
  44. data/lib/paperclip/callbacks.rb +42 -0
  45. data/lib/paperclip/content_type_detector.rb +80 -0
  46. data/lib/paperclip/errors.rb +34 -0
  47. data/lib/paperclip/file_command_content_type_detector.rb +30 -0
  48. data/lib/paperclip/filename_cleaner.rb +16 -0
  49. data/lib/paperclip/geometry.rb +158 -0
  50. data/lib/paperclip/geometry_detector_factory.rb +48 -0
  51. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  52. data/lib/paperclip/glue.rb +17 -0
  53. data/lib/paperclip/has_attached_file.rb +115 -0
  54. data/lib/paperclip/helpers.rb +60 -0
  55. data/lib/paperclip/interpolations.rb +197 -0
  56. data/lib/paperclip/interpolations/plural_cache.rb +18 -0
  57. data/lib/paperclip/io_adapters/abstract_adapter.rb +47 -0
  58. data/lib/paperclip/io_adapters/attachment_adapter.rb +36 -0
  59. data/lib/paperclip/io_adapters/data_uri_adapter.rb +22 -0
  60. data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
  61. data/lib/paperclip/io_adapters/file_adapter.rb +22 -0
  62. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +15 -0
  63. data/lib/paperclip/io_adapters/identity_adapter.rb +12 -0
  64. data/lib/paperclip/io_adapters/nil_adapter.rb +34 -0
  65. data/lib/paperclip/io_adapters/registry.rb +32 -0
  66. data/lib/paperclip/io_adapters/stringio_adapter.rb +33 -0
  67. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +42 -0
  68. data/lib/paperclip/io_adapters/uri_adapter.rb +44 -0
  69. data/lib/paperclip/locales/en.yml +18 -0
  70. data/lib/paperclip/logger.rb +21 -0
  71. data/lib/paperclip/matchers.rb +64 -0
  72. data/lib/paperclip/matchers/have_attached_file_matcher.rb +54 -0
  73. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +100 -0
  74. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +59 -0
  75. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +96 -0
  76. data/lib/paperclip/media_type_spoof_detector.rb +89 -0
  77. data/lib/paperclip/missing_attachment_styles.rb +79 -0
  78. data/lib/paperclip/processor.rb +48 -0
  79. data/lib/paperclip/processor_helpers.rb +50 -0
  80. data/lib/paperclip/rails_environment.rb +25 -0
  81. data/lib/paperclip/railtie.rb +31 -0
  82. data/lib/paperclip/schema.rb +77 -0
  83. data/lib/paperclip/storage.rb +4 -0
  84. data/lib/paperclip/storage/database.rb +140 -0
  85. data/lib/paperclip/storage/filesystem.rb +90 -0
  86. data/lib/paperclip/storage/fog.rb +244 -0
  87. data/lib/paperclip/storage/s3.rb +442 -0
  88. data/lib/paperclip/style.rb +109 -0
  89. data/lib/paperclip/tempfile.rb +43 -0
  90. data/lib/paperclip/tempfile_factory.rb +23 -0
  91. data/lib/paperclip/thumbnail.rb +121 -0
  92. data/lib/paperclip/url_generator.rb +72 -0
  93. data/lib/paperclip/validators.rb +74 -0
  94. data/lib/paperclip/validators/attachment_content_type_validator.rb +88 -0
  95. data/lib/paperclip/validators/attachment_file_name_validator.rb +80 -0
  96. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +29 -0
  97. data/lib/paperclip/validators/attachment_presence_validator.rb +30 -0
  98. data/lib/paperclip/validators/attachment_size_validator.rb +109 -0
  99. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +27 -0
  100. data/lib/paperclip/version.rb +3 -0
  101. data/lib/tasks/paperclip.rake +127 -0
  102. data/paperclip.gemspec +54 -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 +80 -0
  107. data/spec/paperclip/attachment_registry_spec.rb +158 -0
  108. data/spec/paperclip/attachment_spec.rb +1517 -0
  109. data/spec/paperclip/content_type_detector_spec.rb +48 -0
  110. data/spec/paperclip/file_command_content_type_detector_spec.rb +26 -0
  111. data/spec/paperclip/filename_cleaner_spec.rb +14 -0
  112. data/spec/paperclip/geometry_detector_spec.rb +39 -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 +44 -0
  116. data/spec/paperclip/has_attached_file_spec.rb +158 -0
  117. data/spec/paperclip/integration_spec.rb +668 -0
  118. data/spec/paperclip/interpolations_spec.rb +262 -0
  119. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +78 -0
  120. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +139 -0
  121. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +83 -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 +113 -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 +102 -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 +109 -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 +70 -0
  136. data/spec/paperclip/meta_class_spec.rb +30 -0
  137. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +84 -0
  138. data/spec/paperclip/paperclip_spec.rb +192 -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 +33 -0
  143. data/spec/paperclip/rake_spec.rb +103 -0
  144. data/spec/paperclip/schema_spec.rb +248 -0
  145. data/spec/paperclip/storage/filesystem_spec.rb +79 -0
  146. data/spec/paperclip/storage/fog_spec.rb +545 -0
  147. data/spec/paperclip/storage/s3_live_spec.rb +186 -0
  148. data/spec/paperclip/storage/s3_spec.rb +1583 -0
  149. data/spec/paperclip/style_spec.rb +255 -0
  150. data/spec/paperclip/tempfile_factory_spec.rb +33 -0
  151. data/spec/paperclip/thumbnail_spec.rb +500 -0
  152. data/spec/paperclip/url_generator_spec.rb +211 -0
  153. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +322 -0
  154. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +160 -0
  155. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +85 -0
  156. data/spec/paperclip/validators/attachment_size_validator_spec.rb +235 -0
  157. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +52 -0
  158. data/spec/paperclip/validators_spec.rb +164 -0
  159. data/spec/spec_helper.rb +45 -0
  160. data/spec/support/assertions.rb +78 -0
  161. data/spec/support/fake_model.rb +25 -0
  162. data/spec/support/fake_rails.rb +12 -0
  163. data/spec/support/fixtures/12k.png +0 -0
  164. data/spec/support/fixtures/50x50.png +0 -0
  165. data/spec/support/fixtures/5k.png +0 -0
  166. data/spec/support/fixtures/animated +0 -0
  167. data/spec/support/fixtures/animated.gif +0 -0
  168. data/spec/support/fixtures/animated.unknown +0 -0
  169. data/spec/support/fixtures/bad.png +1 -0
  170. data/spec/support/fixtures/empty.html +1 -0
  171. data/spec/support/fixtures/empty.xlsx +0 -0
  172. data/spec/support/fixtures/fog.yml +8 -0
  173. data/spec/support/fixtures/rotated.jpg +0 -0
  174. data/spec/support/fixtures/s3.yml +8 -0
  175. data/spec/support/fixtures/spaced file.jpg +0 -0
  176. data/spec/support/fixtures/spaced file.png +0 -0
  177. data/spec/support/fixtures/text.txt +1 -0
  178. data/spec/support/fixtures/twopage.pdf +0 -0
  179. data/spec/support/fixtures/uppercase.PNG +0 -0
  180. data/spec/support/matchers/accept.rb +5 -0
  181. data/spec/support/matchers/exist.rb +5 -0
  182. data/spec/support/matchers/have_column.rb +23 -0
  183. data/spec/support/mock_attachment.rb +22 -0
  184. data/spec/support/mock_interpolator.rb +24 -0
  185. data/spec/support/mock_url_generator_builder.rb +27 -0
  186. data/spec/support/model_reconstruction.rb +68 -0
  187. data/spec/support/reporting.rb +11 -0
  188. data/spec/support/test_data.rb +13 -0
  189. data/spec/support/version_helper.rb +9 -0
  190. metadata +713 -0
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate paperclip Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,30 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ class PaperclipGenerator < ActiveRecord::Generators::Base
4
+ desc "Create a migration to add paperclip-specific fields to your model. " +
5
+ "The NAME argument is the name of your model, and the following " +
6
+ "arguments are the name of the attachments"
7
+
8
+ argument :attachment_names, :required => true, :type => :array, :desc => "The names of the attachment(s) to add.",
9
+ :banner => "attachment_one attachment_two attachment_three ..."
10
+
11
+ def self.source_root
12
+ @source_root ||= File.expand_path('../templates', __FILE__)
13
+ end
14
+
15
+ def generate_migration
16
+ migration_template "paperclip_migration.rb.erb", "db/migrate/#{migration_file_name}"
17
+ end
18
+
19
+ def migration_name
20
+ "add_attachment_#{attachment_names.join("_")}_to_#{name.underscore.pluralize}"
21
+ end
22
+
23
+ def migration_file_name
24
+ "#{migration_name}.rb"
25
+ end
26
+
27
+ def migration_class_name
28
+ migration_name.camelize
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
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,211 @@
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 'cocaine'
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
+ :whiny => true,
94
+ :image_magick_path => nil,
95
+ :command_path => nil,
96
+ :log => true,
97
+ :log_command => true,
98
+ :swallow_stderr => true,
99
+ :content_type_mappings => {},
100
+ :use_exif_orientation => true
101
+ }
102
+ end
103
+
104
+ def self.io_adapters=(new_registry)
105
+ @io_adapters = new_registry
106
+ end
107
+
108
+ def self.io_adapters
109
+ @io_adapters ||= Paperclip::AdapterRegistry.new
110
+ end
111
+
112
+ module ClassMethods
113
+ # +has_attached_file+ gives the class it is called on an attribute that maps to a file. This
114
+ # is typically a file stored somewhere on the filesystem and has been uploaded by a user.
115
+ # The attribute returns a Paperclip::Attachment object which handles the management of
116
+ # that file. The intent is to make the attachment as much like a normal attribute. The
117
+ # thumbnails will be created when the new file is assigned, but they will *not* be saved
118
+ # until +save+ is called on the record. Likewise, if the attribute is set to +nil+ is
119
+ # called on it, the attachment will *not* be deleted until +save+ is called. See the
120
+ # Paperclip::Attachment documentation for more specifics. There are a number of options
121
+ # you can set to change the behavior of a Paperclip attachment:
122
+ # * +url+: The full URL of where the attachment is publically accessible. This can just
123
+ # as easily point to a directory served directly through Apache as it can to an action
124
+ # that can control permissions. You can specify the full domain and path, but usually
125
+ # just an absolute path is sufficient. The leading slash *must* be included manually for
126
+ # absolute paths. The default value is
127
+ # "/system/:class/:attachment/:id_partition/:style/:filename". See
128
+ # Paperclip::Attachment#interpolate for more information on variable interpolaton.
129
+ # :url => "/:class/:attachment/:id/:style_:filename"
130
+ # :url => "http://some.other.host/stuff/:class/:id_:extension"
131
+ # * +default_url+: The URL that will be returned if there is no attachment assigned.
132
+ # This field is interpolated just as the url is. The default value is
133
+ # "/:attachment/:style/missing.png"
134
+ # has_attached_file :avatar, :default_url => "/images/default_:style_avatar.png"
135
+ # User.new.avatar_url(:small) # => "/images/default_small_avatar.png"
136
+ # * +styles+: A hash of thumbnail styles and their geometries. You can find more about
137
+ # geometry strings at the ImageMagick website
138
+ # (http://www.imagemagick.org/script/command-line-options.php#resize). Paperclip
139
+ # also adds the "#" option (e.g. "50x50#"), which will resize the image to fit maximally
140
+ # inside the dimensions and then crop the rest off (weighted at the center). The
141
+ # default value is to generate no thumbnails.
142
+ # * +default_style+: The thumbnail style that will be used by default URLs.
143
+ # Defaults to +original+.
144
+ # has_attached_file :avatar, :styles => { :normal => "100x100#" },
145
+ # :default_style => :normal
146
+ # user.avatar.url # => "/avatars/23/normal_me.png"
147
+ # * +keep_old_files+: Keep the existing attachment files (original + resized) from
148
+ # being automatically deleted when an attachment is cleared or updated. Defaults to +false+.
149
+ # * +preserve_files+: Keep the existing attachment files in all cases, even if the parent
150
+ # record is destroyed. Defaults to +false+.
151
+ # * +whiny+: Will raise an error if Paperclip cannot post_process an uploaded file due
152
+ # to a command line error. This will override the global setting for this attachment.
153
+ # Defaults to true.
154
+ # * +convert_options+: When creating thumbnails, use this free-form options
155
+ # array to pass in various convert command options. Typical options are "-strip" to
156
+ # remove all Exif data from the image (save space for thumbnails and avatars) or
157
+ # "-depth 8" to specify the bit depth of the resulting conversion. See ImageMagick
158
+ # convert documentation for more options: (http://www.imagemagick.org/script/convert.php)
159
+ # Note that this option takes a hash of options, each of which correspond to the style
160
+ # of thumbnail being generated. You can also specify :all as a key, which will apply
161
+ # to all of the thumbnails being generated. If you specify options for the :original,
162
+ # it would be best if you did not specify destructive options, as the intent of keeping
163
+ # the original around is to regenerate all the thumbnails when requirements change.
164
+ # has_attached_file :avatar, :styles => { :large => "300x300", :negative => "100x100" }
165
+ # :convert_options => {
166
+ # :all => "-strip",
167
+ # :negative => "-negate"
168
+ # }
169
+ # NOTE: While not deprecated yet, it is not recommended to specify options this way.
170
+ # It is recommended that :convert_options option be included in the hash passed to each
171
+ # :styles for compatibility with future versions.
172
+ # NOTE: Strings supplied to :convert_options are split on space in order to undergo
173
+ # shell quoting for safety. If your options require a space, please pre-split them
174
+ # and pass an array to :convert_options instead.
175
+ # * +storage+: Chooses the storage backend where the files will be stored. The current
176
+ # choices are :filesystem, :fog and :s3. The default is :filesystem. Make sure you read the
177
+ # documentation for Paperclip::Storage::Filesystem, Paperclip::Storage::Fog and Paperclip::Storage::S3
178
+ # for backend-specific options.
179
+ #
180
+ # It's also possible for you to dynamically define your interpolation string for :url,
181
+ # :default_url, and :path in your model by passing a method name as a symbol as a argument
182
+ # for your has_attached_file definition:
183
+ #
184
+ # class Person
185
+ # has_attached_file :avatar, :default_url => :default_url_by_gender
186
+ #
187
+ # private
188
+ #
189
+ # def default_url_by_gender
190
+ # "/assets/avatars/default_#{gender}.png"
191
+ # end
192
+ # end
193
+ def has_attached_file(name, options = {})
194
+ HasAttachedFile.define_on(self, name, options)
195
+ end
196
+ end
197
+ end
198
+
199
+ # This stuff needs to be run after Paperclip is defined.
200
+ require 'paperclip/io_adapters/registry'
201
+ require 'paperclip/io_adapters/abstract_adapter'
202
+ require 'paperclip/io_adapters/empty_string_adapter'
203
+ require 'paperclip/io_adapters/identity_adapter'
204
+ require 'paperclip/io_adapters/file_adapter'
205
+ require 'paperclip/io_adapters/stringio_adapter'
206
+ require 'paperclip/io_adapters/data_uri_adapter'
207
+ require 'paperclip/io_adapters/nil_adapter'
208
+ require 'paperclip/io_adapters/attachment_adapter'
209
+ require 'paperclip/io_adapters/uploaded_file_adapter'
210
+ require 'paperclip/io_adapters/uri_adapter'
211
+ require 'paperclip/io_adapters/http_url_proxy_adapter'
@@ -0,0 +1,610 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+ require 'paperclip/url_generator'
4
+ require 'active_support/deprecation'
5
+ require 'active_support/core_ext/string/inflections'
6
+
7
+ module Paperclip
8
+ # The Attachment class manages the files for a given attachment. It saves
9
+ # when the model saves, deletes when the model is destroyed, and processes
10
+ # the file upon assignment.
11
+ class Attachment
12
+ def self.default_options
13
+ @default_options ||= {
14
+ :convert_options => {},
15
+ :default_style => :original,
16
+ :default_url => "/:attachment/:style/missing.png",
17
+ :escape_url => true,
18
+ :restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
19
+ :filename_cleaner => nil,
20
+ :hash_data => ":class/:attachment/:id/:style/:updated_at",
21
+ :hash_digest => "SHA1",
22
+ :interpolator => Paperclip::Interpolations,
23
+ :only_process => [],
24
+ :path => ":rails_root/public:url",
25
+ :preserve_files => false,
26
+ :processors => [:thumbnail],
27
+ :source_file_options => {},
28
+ :storage => :filesystem,
29
+ :styles => {},
30
+ :url => "/system/:class/:attachment/:id_partition/:style/:filename",
31
+ :url_generator => Paperclip::UrlGenerator,
32
+ :use_default_time_zone => true,
33
+ :use_timestamp => true,
34
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
35
+ :validate_media_type => true,
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, @options)
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
+ ensure_required_accessors!
102
+ ensure_required_validations!
103
+
104
+ if @file.assignment?
105
+ clear(*only_process)
106
+
107
+ if @file.nil?
108
+ nil
109
+ else
110
+ assign_attributes
111
+ post_process_file
112
+ reset_file_if_original_reprocessed
113
+ end
114
+ else
115
+ nil
116
+ end
117
+ end
118
+
119
+ # Returns the public URL of the attachment with a given style. This does
120
+ # not necessarily need to point to a file that your Web server can access
121
+ # and can instead point to an action in your app, for example for fine grained
122
+ # security; this has a serious performance tradeoff.
123
+ #
124
+ # Options:
125
+ #
126
+ # +timestamp+ - Add a timestamp to the end of the URL. Default: true.
127
+ # +escape+ - Perform URI escaping to the URL. Default: true.
128
+ #
129
+ # Global controls (set on has_attached_file):
130
+ #
131
+ # +interpolator+ - The object that fills in a URL pattern's variables.
132
+ # +default_url+ - The image to show when the attachment has no image.
133
+ # +url+ - The URL for a saved image.
134
+ # +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
135
+ #
136
+ # As mentioned just above, the object that generates this URL can be passed
137
+ # in, for finer control. This object must respond to two methods:
138
+ #
139
+ # +#new(Paperclip::Attachment, options_hash)+
140
+ # +#for(style_name, options_hash)+
141
+
142
+ def url(style_name = default_style, options = {})
143
+ if options == true || options == false # Backwards compatibility.
144
+ @url_generator.for(style_name, default_options.merge(:timestamp => options))
145
+ else
146
+ @url_generator.for(style_name, default_options.merge(options))
147
+ end
148
+ end
149
+
150
+ def default_options
151
+ {
152
+ :timestamp => @options[:use_timestamp],
153
+ :escape => @options[:escape_url]
154
+ }
155
+ end
156
+
157
+ # Alias to +url+ that allows using the expiring_url method provided by the cloud
158
+ # storage implementations, but keep using filesystem storage for development and
159
+ # testing.
160
+ def expiring_url(time = 3600, style_name = default_style)
161
+ url(style_name)
162
+ end
163
+
164
+ # Returns the path of the attachment as defined by the :path option. If the
165
+ # file is stored in the filesystem the path refers to the path of the file
166
+ # on disk. If the file is stored in S3, the path is the "key" part of the
167
+ # URL, and the :bucket option refers to the S3 bucket.
168
+ def path(style_name = default_style)
169
+ path = original_filename.nil? ? nil : interpolate(path_option, style_name)
170
+ path.respond_to?(:unescape) ? path.unescape : path
171
+ end
172
+
173
+ # :nodoc:
174
+ def staged_path(style_name = default_style)
175
+ if staged?
176
+ @queued_for_write[style_name].path
177
+ end
178
+ end
179
+
180
+ # :nodoc:
181
+ def staged?
182
+ ! @queued_for_write.empty?
183
+ end
184
+
185
+ # Alias to +url+
186
+ def to_s style_name = default_style
187
+ url(style_name)
188
+ end
189
+
190
+ def as_json(options = nil)
191
+ to_s((options && options[:style]) || default_style)
192
+ end
193
+
194
+ def default_style
195
+ @options[:default_style]
196
+ end
197
+
198
+ def styles
199
+ if @options[:styles].respond_to?(:call) || @normalized_styles.nil?
200
+ styles = @options[:styles]
201
+ styles = styles.call(self) if styles.respond_to?(:call)
202
+
203
+ @normalized_styles = styles.dup
204
+ styles.each_pair do |name, options|
205
+ @normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
206
+ end
207
+ end
208
+ @normalized_styles
209
+ end
210
+
211
+ def only_process
212
+ only_process = @options[:only_process].dup
213
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
214
+ only_process.map(&:to_sym)
215
+ end
216
+
217
+ def processors
218
+ processing_option = @options[:processors]
219
+
220
+ if processing_option.respond_to?(:call)
221
+ processing_option.call(instance)
222
+ else
223
+ processing_option
224
+ end
225
+ end
226
+
227
+ # Returns an array containing the errors on this attachment.
228
+ def errors
229
+ @errors
230
+ end
231
+
232
+ # Returns true if there are changes that need to be saved.
233
+ def dirty?
234
+ @dirty
235
+ end
236
+
237
+ # Saves the file, if there are no errors. If there are, it flushes them to
238
+ # the instance's errors and returns false, cancelling the save.
239
+ def save
240
+ flush_deletes unless @options[:keep_old_files]
241
+ flush_writes
242
+ @dirty = false
243
+ true
244
+ end
245
+
246
+ # Clears out the attachment. Has the same effect as previously assigning
247
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
248
+ # use #destroy.
249
+ def clear(*styles_to_clear)
250
+ if styles_to_clear.any?
251
+ queue_some_for_delete(*styles_to_clear)
252
+ else
253
+ queue_all_for_delete
254
+ @queued_for_write = {}
255
+ @errors = {}
256
+ end
257
+ end
258
+
259
+ # Destroys the attachment. Has the same effect as previously assigning
260
+ # nil to the attachment *and saving*. This is permanent. If you wish to
261
+ # wipe out the existing attachment but not save, use #clear.
262
+ def destroy
263
+ clear
264
+ save
265
+ end
266
+
267
+ # Returns the uploaded file if present.
268
+ def uploaded_file
269
+ instance_read(:uploaded_file)
270
+ end
271
+
272
+ # Returns the name of the file as originally assigned, and lives in the
273
+ # <attachment>_file_name attribute of the model.
274
+ def original_filename
275
+ instance_read(:file_name)
276
+ end
277
+
278
+ # Returns the size of the file as originally assigned, and lives in the
279
+ # <attachment>_file_size attribute of the model.
280
+ def size
281
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
282
+ end
283
+
284
+ # Returns the fingerprint of the file, if one's defined. The fingerprint is
285
+ # stored in the <attachment>_fingerprint attribute of the model.
286
+ def fingerprint
287
+ instance_read(:fingerprint)
288
+ end
289
+
290
+ # Returns the content_type of the file as originally assigned, and lives
291
+ # in the <attachment>_content_type attribute of the model.
292
+ def content_type
293
+ instance_read(:content_type)
294
+ end
295
+
296
+ # Returns the creation time of the file as originally assigned, and
297
+ # lives in the <attachment>_created_at attribute of the model.
298
+ def created_at
299
+ if able_to_store_created_at?
300
+ time = instance_read(:created_at)
301
+ time && time.to_f.to_i
302
+ end
303
+ end
304
+
305
+ # Returns the last modified time of the file as originally assigned, and
306
+ # lives in the <attachment>_updated_at attribute of the model.
307
+ def updated_at
308
+ time = instance_read(:updated_at)
309
+ time && time.to_f.to_i
310
+ end
311
+
312
+ # The time zone to use for timestamp interpolation. Using the default
313
+ # time zone ensures that results are consistent across all threads.
314
+ def time_zone
315
+ @options[:use_default_time_zone] ? Time.zone_default : Time.zone
316
+ end
317
+
318
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
319
+ # publicly viewable attachment.
320
+ def hash_key(style_name = default_style)
321
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret]
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_only_process, @options[:only_process] = @options[:only_process], style_args
336
+ saved_preserve_files, @options[:preserve_files] = @options[:preserve_files], true
337
+ begin
338
+ assign(self)
339
+ save
340
+ instance.save
341
+ rescue Errno::EACCES => e
342
+ warn "#{e} - skipping file."
343
+ false
344
+ ensure
345
+ @options[:only_process] = saved_only_process
346
+ @options[:preserve_files] = saved_preserve_files
347
+ end
348
+ end
349
+
350
+ # Returns true if a file has been assigned.
351
+ def file?
352
+ !original_filename.blank?
353
+ end
354
+
355
+ alias :present? :file?
356
+
357
+ def blank?
358
+ not present?
359
+ end
360
+
361
+ # Determines whether the instance responds to this attribute. Used to prevent
362
+ # calculations on fields we won't even store.
363
+ def instance_respond_to?(attr)
364
+ instance.respond_to?(:"#{name}_#{attr}")
365
+ end
366
+
367
+ # Writes the attachment-specific attribute on the instance. For example,
368
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
369
+ # "avatar_file_name" field (assuming the attachment is called avatar).
370
+ def instance_write(attr, value)
371
+ setter = :"#{@name_string}_#{attr}="
372
+ if instance.respond_to?(setter)
373
+ instance.send(setter, value)
374
+ end
375
+ end
376
+
377
+ # Reads the attachment-specific attribute on the instance. See instance_write
378
+ # for more details.
379
+ def instance_read(attr)
380
+ getter = :"#{@name_string}_#{attr}"
381
+ if instance.respond_to?(getter)
382
+ instance.send(getter)
383
+ end
384
+ end
385
+
386
+ private
387
+
388
+ def path_option
389
+ @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
390
+ end
391
+
392
+ def active_validator_classes
393
+ @instance.class.validators.map(&:class)
394
+ end
395
+
396
+ def missing_required_validator?
397
+ (active_validator_classes.flat_map(&:ancestors) & Paperclip::REQUIRED_VALIDATORS).empty?
398
+ end
399
+
400
+ def ensure_required_validations!
401
+ if missing_required_validator?
402
+ raise Paperclip::Errors::MissingRequiredValidatorError
403
+ end
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
+ self.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
+ if instance_respond_to?(:fingerprint)
443
+ instance_write(:fingerprint, yield)
444
+ end
445
+ end
446
+
447
+ def assign_timestamps
448
+ if has_enabled_but_unset_created_at?
449
+ instance_write(:created_at, Time.now)
450
+ end
451
+
452
+ instance_write(:updated_at, Time.now)
453
+ end
454
+
455
+ def post_process_file
456
+ dirty!
457
+
458
+ if post_processing
459
+ post_process(*only_process)
460
+ end
461
+ end
462
+
463
+ def dirty!
464
+ @dirty = true
465
+ end
466
+
467
+ def reset_file_if_original_reprocessed
468
+ instance_write(:file_size, @queued_for_write[:original].size)
469
+ assign_fingerprint { @queued_for_write[:original].fingerprint }
470
+ reset_updater
471
+ end
472
+
473
+ def reset_updater
474
+ if instance.respond_to?(updater)
475
+ instance.send(updater)
476
+ end
477
+ end
478
+
479
+ def updater
480
+ :"#{name}_file_name_will_change!"
481
+ end
482
+
483
+ def extra_options_for(style) #:nodoc:
484
+ process_options(:convert_options, style)
485
+ end
486
+
487
+ def extra_source_file_options_for(style) #:nodoc:
488
+ process_options(:source_file_options, style)
489
+ end
490
+
491
+ def process_options(options_type, style) #:nodoc:
492
+ all_options = @options[options_type][:all]
493
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
494
+ style_options = @options[options_type][style]
495
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
496
+
497
+ [ style_options, all_options ].compact.join(" ")
498
+ end
499
+
500
+ def post_process(*style_args) #:nodoc:
501
+ return if @queued_for_write[:original].nil?
502
+
503
+ instance.run_paperclip_callbacks(:post_process) do
504
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
505
+ if !@options[:check_validity_before_processing] || !instance.errors.any?
506
+ post_process_styles(*style_args)
507
+ end
508
+ end
509
+ end
510
+ end
511
+
512
+ def post_process_styles(*style_args) #:nodoc:
513
+ post_process_style(:original, styles[:original]) if styles.include?(:original) && process_style?(:original, style_args)
514
+ styles.reject{ |name, style| name == :original }.each do |name, style|
515
+ post_process_style(name, style) if process_style?(name, style_args)
516
+ end
517
+ end
518
+
519
+ def post_process_style(name, style) #:nodoc:
520
+ begin
521
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
522
+ intermediate_files = []
523
+
524
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[: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
+ file
528
+ end
529
+
530
+ unadapted_file = @queued_for_write[name]
531
+ @queued_for_write[name] = Paperclip.io_adapters.for(@queued_for_write[name])
532
+ unadapted_file.close if unadapted_file.respond_to?(:close)
533
+ @queued_for_write[name]
534
+ rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e
535
+ log("An error was received while processing: #{e.inspect}")
536
+ (@errors[:processing] ||= []) << e.message if @options[:whiny]
537
+ ensure
538
+ unlink_files(intermediate_files)
539
+ end
540
+ end
541
+
542
+ def process_style?(style_name, style_args) #:nodoc:
543
+ style_args.empty? || style_args.include?(style_name)
544
+ end
545
+
546
+ def interpolate(pattern, style_name = default_style) #:nodoc:
547
+ interpolator.interpolate(pattern, self, style_name)
548
+ end
549
+
550
+ def queue_some_for_delete(*styles)
551
+ @queued_for_delete += styles.uniq.map do |style|
552
+ path(style) if exists?(style)
553
+ end.compact
554
+ end
555
+
556
+ def queue_all_for_delete #:nodoc:
557
+ return if !file?
558
+ unless @options[:preserve_files]
559
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
560
+ path(style) if exists?(style)
561
+ end.compact
562
+ end
563
+ instance_write(:file_name, nil)
564
+ instance_write(:content_type, nil)
565
+ instance_write(:file_size, nil)
566
+ instance_write(:fingerprint, nil)
567
+ instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
568
+ instance_write(:updated_at, nil)
569
+ end
570
+
571
+ def flush_errors #:nodoc:
572
+ @errors.each do |error, message|
573
+ [message].flatten.each {|m| instance.errors.add(name, m) }
574
+ end
575
+ end
576
+
577
+ # called by storage after the writes are flushed and before @queued_for_write is cleared
578
+ def after_flush_writes
579
+ unlink_files(@queued_for_write.values)
580
+ end
581
+
582
+ def unlink_files(files)
583
+ Array(files).each do |file|
584
+ file.close unless file.closed?
585
+ file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
586
+ end
587
+ end
588
+
589
+ # You can either specifiy :restricted_characters or you can define your own
590
+ # :filename_cleaner object. This object needs to respond to #call and takes
591
+ # the filename that will be cleaned. It should return the cleaned filename.
592
+ def filename_cleaner
593
+ @options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
594
+ end
595
+
596
+ def cleanup_filename(filename)
597
+ filename_cleaner.call(filename)
598
+ end
599
+
600
+ # Check if attachment database table has a created_at field
601
+ def able_to_store_created_at?
602
+ @instance.respond_to?("#{name}_created_at".to_sym)
603
+ end
604
+
605
+ # Check if attachment database table has a created_at field which is not yet set
606
+ def has_enabled_but_unset_created_at?
607
+ able_to_store_created_at? && !instance_read(:created_at)
608
+ end
609
+ end
610
+ end