thoughtbot-paperclip 2.1.5

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.
@@ -0,0 +1,207 @@
1
+ module Paperclip
2
+ module Storage
3
+
4
+ # The default place to store attachments is in the filesystem. Files on the local
5
+ # filesystem can be very easily served by Apache without requiring a hit to your app.
6
+ # They also can be processed more easily after they've been saved, as they're just
7
+ # normal files. There is one Filesystem-specific option for has_attached_file.
8
+ # * +path+: The location of the repository of attachments on disk. This can (and, in
9
+ # almost all cases, should) be coordinated with the value of the +url+ option to
10
+ # allow files to be saved into a place where Apache can serve them without
11
+ # hitting your app. Defaults to
12
+ # ":rails_root/public/:attachment/:id/:style/:basename.:extension"
13
+ # By default this places the files in the app's public directory which can be served
14
+ # directly. If you are using capistrano for deployment, a good idea would be to
15
+ # make a symlink to the capistrano-created system directory from inside your app's
16
+ # public directory.
17
+ # See Paperclip::Attachment#interpolate for more information on variable interpolaton.
18
+ # :path => "/var/app/attachments/:class/:id/:style/:basename.:extension"
19
+ module Filesystem
20
+ def self.extended base
21
+ end
22
+
23
+ def exists?(style = default_style)
24
+ if original_filename
25
+ File.exist?(path(style))
26
+ else
27
+ false
28
+ end
29
+ end
30
+
31
+ # Returns representation of the data of the file assigned to the given
32
+ # style, in the format most representative of the current storage.
33
+ def to_file style = default_style
34
+ @queued_for_write[style] || (File.new(path(style), 'rb') if exists?(style))
35
+ end
36
+ alias_method :to_io, :to_file
37
+
38
+ def flush_writes #:nodoc:
39
+ logger.info("[paperclip] Writing files for #{name}")
40
+ @queued_for_write.each do |style, file|
41
+ FileUtils.mkdir_p(File.dirname(path(style)))
42
+ logger.info("[paperclip] -> #{path(style)}")
43
+ FileUtils.mv(file.path, path(style))
44
+ FileUtils.chmod(0644, path(style))
45
+ file.close
46
+ end
47
+ @queued_for_write = {}
48
+ end
49
+
50
+ def flush_deletes #:nodoc:
51
+ logger.info("[paperclip] Deleting files for #{name}")
52
+ @queued_for_delete.each do |path|
53
+ begin
54
+ logger.info("[paperclip] -> #{path}")
55
+ FileUtils.rm(path) if File.exist?(path)
56
+ rescue Errno::ENOENT => e
57
+ # ignore file-not-found, let everything else pass
58
+ end
59
+ end
60
+ @queued_for_delete = []
61
+ end
62
+ end
63
+
64
+ # Amazon's S3 file hosting service is a scalable, easy place to store files for
65
+ # distribution. You can find out more about it at http://aws.amazon.com/s3
66
+ # There are a few S3-specific options for has_attached_file:
67
+ # * +s3_credentials+: Takes a path, a File, or a Hash. The path (or File) must point
68
+ # to a YAML file containing the +access_key_id+ and +secret_access_key+ that Amazon
69
+ # gives you. You can 'environment-space' this just like you do to your
70
+ # database.yml file, so different environments can use different accounts:
71
+ # development:
72
+ # access_key_id: 123...
73
+ # secret_access_key: 123...
74
+ # test:
75
+ # access_key_id: abc...
76
+ # secret_access_key: abc...
77
+ # production:
78
+ # access_key_id: 456...
79
+ # secret_access_key: 456...
80
+ # This is not required, however, and the file may simply look like this:
81
+ # access_key_id: 456...
82
+ # secret_access_key: 456...
83
+ # In which case, those access keys will be used in all environments. You can also
84
+ # put your bucket name in this file, instead of adding it to the code directly.
85
+ # This is useful when you want the same account but a different bucket for
86
+ # development versus production.
87
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
88
+ # policies that S3 provides (more information can be found here:
89
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
90
+ # The default for Paperclip is "public-read".
91
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
92
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
93
+ # default), and 'https' when your :s3_permissions are anything else.
94
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
95
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
96
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
97
+ # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
98
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
99
+ # Normally, this won't matter in the slightest and you can leave the default (which is
100
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
101
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
102
+ # * +path+: This is the key under the bucket in which the file will be stored. The
103
+ # URL will be constructed from the bucket and the path. This is what you will want
104
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
105
+ # S3 (strictly speaking) does not support directories, you can still use a / to
106
+ # separate parts of your file name.
107
+ module S3
108
+ def self.extended base
109
+ require 'right_aws'
110
+ base.instance_eval do
111
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
112
+ @bucket = @options[:bucket] || @s3_credentials[:bucket]
113
+ @s3_options = @options[:s3_options] || {}
114
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
115
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
116
+ @url = ":s3_path_url" unless @url.to_s.match(/^:s3.*url$/)
117
+ end
118
+ base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
119
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
120
+ end
121
+ base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
122
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
123
+ end
124
+ ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
125
+ end
126
+
127
+ def s3
128
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
129
+ @s3_credentials[:secret_access_key],
130
+ @s3_options)
131
+ end
132
+
133
+ def s3_bucket
134
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
135
+ end
136
+
137
+ def bucket_name
138
+ @bucket
139
+ end
140
+
141
+ def parse_credentials creds
142
+ creds = find_credentials(creds).stringify_keys
143
+ (creds[ENV['RAILS_ENV']] || creds).symbolize_keys
144
+ end
145
+
146
+ def exists?(style = default_style)
147
+ s3_bucket.key(path(style)) ? true : false
148
+ end
149
+
150
+ def s3_protocol
151
+ @s3_protocol
152
+ end
153
+
154
+ # Returns representation of the data of the file assigned to the given
155
+ # style, in the format most representative of the current storage.
156
+ def to_file style = default_style
157
+ @queued_for_write[style] || s3_bucket.key(path(style))
158
+ end
159
+ alias_method :to_io, :to_file
160
+
161
+ def flush_writes #:nodoc:
162
+ logger.info("[paperclip] Writing files for #{name}")
163
+ @queued_for_write.each do |style, file|
164
+ begin
165
+ logger.info("[paperclip] -> #{path(style)}")
166
+ key = s3_bucket.key(path(style))
167
+ key.data = file
168
+ key.put(nil, @s3_permissions, {'Content-type' => instance_read(:content_type)})
169
+ rescue RightAws::AwsError => e
170
+ raise
171
+ end
172
+ end
173
+ @queued_for_write = {}
174
+ end
175
+
176
+ def flush_deletes #:nodoc:
177
+ logger.info("[paperclip] Writing files for #{name}")
178
+ @queued_for_delete.each do |path|
179
+ begin
180
+ logger.info("[paperclip] -> #{path}")
181
+ if file = s3_bucket.key(path)
182
+ file.delete
183
+ end
184
+ rescue RightAws::AwsError
185
+ # Ignore this.
186
+ end
187
+ end
188
+ @queued_for_delete = []
189
+ end
190
+
191
+ def find_credentials creds
192
+ case creds
193
+ when File:
194
+ YAML.load_file(creds.path)
195
+ when String:
196
+ YAML.load_file(creds)
197
+ when Hash:
198
+ creds
199
+ else
200
+ raise ArgumentError, "Credentials are not a path, file, or hash."
201
+ end
202
+ end
203
+ private :find_credentials
204
+
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,88 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail
4
+
5
+ attr_accessor :file, :current_geometry, :target_geometry, :format, :whiny_thumbnails, :convert_options
6
+
7
+ # Creates a Thumbnail object set to work on the +file+ given. It
8
+ # will attempt to transform the image into one defined by +target_geometry+
9
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
10
+ # unless specified. Thumbnail creation will raise no errors unless
11
+ # +whiny_thumbnails+ is true (which it is, by default. If +convert_options+ is
12
+ # set, the options will be appended to the convert command upon image conversion
13
+ def initialize file, target_geometry, format = nil, convert_options = nil, whiny_thumbnails = true
14
+ @file = file
15
+ @crop = target_geometry[-1,1] == '#'
16
+ @target_geometry = Geometry.parse target_geometry
17
+ @current_geometry = Geometry.from_file file
18
+ @convert_options = convert_options
19
+ @whiny_thumbnails = whiny_thumbnails
20
+
21
+ @current_format = File.extname(@file.path)
22
+ @basename = File.basename(@file.path, @current_format)
23
+
24
+ @format = format
25
+ end
26
+
27
+ # Creates a thumbnail, as specified in +initialize+, +make+s it, and returns the
28
+ # resulting Tempfile.
29
+ def self.make file, dimensions, format = nil, convert_options = nil, whiny_thumbnails = true
30
+ new(file, dimensions, format, convert_options, whiny_thumbnails).make
31
+ end
32
+
33
+ # Returns true if the +target_geometry+ is meant to crop.
34
+ def crop?
35
+ @crop
36
+ end
37
+
38
+ # Returns true if the image is meant to make use of additional convert options.
39
+ def convert_options?
40
+ not @convert_options.blank?
41
+ end
42
+
43
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
44
+ # that contains the new image.
45
+ def make
46
+ src = @file
47
+ dst = Tempfile.new([@basename, @format].compact.join("."))
48
+ dst.binmode
49
+
50
+ command = <<-end_command
51
+ "#{ File.expand_path(src.path) }[0]"
52
+ #{ transformation_command }
53
+ "#{ File.expand_path(dst.path) }"
54
+ end_command
55
+
56
+ begin
57
+ success = Paperclip.run("convert", command.gsub(/\s+/, " "))
58
+ rescue PaperclipCommandLineError
59
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}" if @whiny_thumbnails
60
+ end
61
+
62
+ dst
63
+ end
64
+
65
+ # Returns the command ImageMagick's +convert+ needs to transform the image
66
+ # into the thumbnail.
67
+ def transformation_command
68
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
69
+ trans = "-resize \"#{scale}\""
70
+ trans << " -crop \"#{crop}\" +repage" if crop
71
+ trans << " #{convert_options}" if convert_options?
72
+ trans
73
+ end
74
+ end
75
+
76
+ # Due to how ImageMagick handles its image format conversion and how Tempfile
77
+ # handles its naming scheme, it is necessary to override how Tempfile makes
78
+ # its names so as to allow for file extensions. Idea taken from the comments
79
+ # on this blog post:
80
+ # http://marsorange.com/archives/of-mogrify-ruby-tempfile-dynamic-class-definitions
81
+ class Tempfile < ::Tempfile
82
+ # Replaces Tempfile's +make_tmpname+ with one that honors file extensions.
83
+ def make_tmpname(basename, n)
84
+ extension = File.extname(basename)
85
+ sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n, extension)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,48 @@
1
+ module Paperclip
2
+ # The Upfile module is a convenience module for adding uploaded-file-type methods
3
+ # to the +File+ class. Useful for testing.
4
+ # user.avatar = File.new("test/test_avatar.jpg")
5
+ module Upfile
6
+
7
+ # Infer the MIME-type of the file from the extension.
8
+ def content_type
9
+ type = (self.path.match(/\.(\w+)$/)[1] rescue "octet-stream").downcase
10
+ case type
11
+ when %r"jpe?g" then "image/jpeg"
12
+ when %r"tiff?" then "image/tiff"
13
+ when %r"png", "gif", "bmp" then "image/#{type}"
14
+ when "txt" then "text/plain"
15
+ when %r"html?" then "text/html"
16
+ when "csv", "xml", "css", "js" then "text/#{type}"
17
+ else "application/x-#{type}"
18
+ end
19
+ end
20
+
21
+ # Returns the file's normal name.
22
+ def original_filename
23
+ File.basename(self.path)
24
+ end
25
+
26
+ # Returns the size of the file.
27
+ def size
28
+ File.size(self)
29
+ end
30
+ end
31
+ end
32
+
33
+ if defined? StringIO
34
+ class StringIO
35
+ attr_accessor :original_filename, :content_type
36
+ def original_filename
37
+ @original_filename ||= "stringio.txt"
38
+ end
39
+ def content_type
40
+ @content_type ||= "text/plain"
41
+ end
42
+ end
43
+ end
44
+
45
+ class File #:nodoc:
46
+ include Paperclip::Upfile
47
+ end
48
+
@@ -0,0 +1,32 @@
1
+ module Paperclip
2
+ module Shoulda
3
+ def should_have_attached_file name, options = {}
4
+ klass = self.name.gsub(/Test$/, '').constantize
5
+ context "Class #{klass.name} with attachment #{name}" do
6
+ should "respond to all the right methods" do
7
+ ["#{name}", "#{name}=", "#{name}?"].each do |meth|
8
+ assert klass.instance_methods.include?(meth), "#{klass.name} does not respond to #{name}."
9
+ end
10
+ end
11
+
12
+ should "have the correct definition" do
13
+ expected = options
14
+ actual = klass.attachment_definitions[name]
15
+ expected.delete(:validations) if not options.key?(:validations)
16
+ expected.delete(:whiny_thumbnails) if not options.key?(:whiny_thumbnails)
17
+
18
+ assert_equal expected, actual
19
+ end
20
+
21
+ should "ensure that ImageMagick is available" do
22
+ %w( convert identify ).each do |command|
23
+ `#{Paperclip.path_for_command(command)}`
24
+ assert_equal 0, $?, "ImageMagick's #{command} returned with an error. Make sure that #{command} is available at #{Paperclip.path_for_command(command)}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ Test::Unit::TestCase.extend(Paperclip::Shoulda)
@@ -0,0 +1,79 @@
1
+ def obtain_class
2
+ class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
4
+ @klass = Object.const_get(class_name)
5
+ end
6
+
7
+ def obtain_attachments
8
+ name = ENV['ATTACHMENT'] || ENV['attachment']
9
+ raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
10
+ if !name.blank? && @klass.attachment_definitions.keys.include?(name)
11
+ [ name ]
12
+ else
13
+ @klass.attachment_definitions.keys
14
+ end
15
+ end
16
+
17
+ def for_all_attachments
18
+ klass = obtain_class
19
+ names = obtain_attachments
20
+ ids = klass.connection.select_values("SELECT #{klass.primary_key} FROM #{klass.table_name}")
21
+
22
+ ids.each do |id|
23
+ instance = klass.find(id)
24
+ names.each do |name|
25
+ result = if instance.send("#{ name }?")
26
+ yield(instance, name)
27
+ else
28
+ true
29
+ end
30
+ print result ? "." : "x"; $stdout.flush
31
+ end
32
+ end
33
+ puts " Done."
34
+ end
35
+
36
+ namespace :paperclip do
37
+ desc "Refreshes both metadata and thumbnails."
38
+ task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
39
+
40
+ namespace :refresh do
41
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
42
+ task :thumbnails => :environment do
43
+ errors = []
44
+ for_all_attachments do |instance, name|
45
+ result = instance.send(name).reprocess!
46
+ errors << [instance.id, instance.errors] unless instance.errors.blank?
47
+ result
48
+ end
49
+ errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
50
+ end
51
+
52
+ desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
53
+ task :metadata => :environment do
54
+ for_all_attachments do |instance, name|
55
+ if file = instance.send(name).to_file
56
+ instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
57
+ instance.send("#{name}_content_type=", file.content_type.strip)
58
+ instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
59
+ instance.save(false)
60
+ else
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ desc "Cleans out invalid attachments. Useful after you've added new validations."
68
+ task :clean => :environment do
69
+ for_all_attachments do |instance, name|
70
+ instance.send(name).send(:validate)
71
+ if instance.send(name).valid?
72
+ true
73
+ else
74
+ instance.send("#{name}=", nil)
75
+ instance.save
76
+ end
77
+ end
78
+ end
79
+ end