peterpunk-merb_paperclip 0.9.3

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,287 @@
1
+ module Paperclip
2
+ # The Attachment class manages the files for a given attachment. It saves when the model saves,
3
+ # deletes when the model is destroyed, and processes the file upon assignment.
4
+ class Attachment
5
+
6
+ def self.default_options
7
+ @default_options ||= {
8
+ :url => "/:attachment/:id/:style/:basename.:extension",
9
+ :path => ":merb_root/public/:attachment/:id/:style/:basename.:extension",
10
+ :styles => {},
11
+ :default_url => "/:attachment/:style/missing.png",
12
+ :default_style => :original,
13
+ :validations => [],
14
+ :storage => :filesystem
15
+ }
16
+ end
17
+
18
+ attr_reader :name, :instance, :styles, :default_style
19
+
20
+ # Creates an Attachment object. +name+ is the name of the attachment, +instance+ is the
21
+ # ActiveRecord object instance it's attached to, and +options+ is the same as the hash
22
+ # passed to +has_attached_file+.
23
+ def initialize name, instance, options = {}
24
+ @name = name
25
+ @instance = instance
26
+
27
+ options = self.class.default_options.merge(options)
28
+
29
+ @url = options[:url]
30
+ @path = options[:path]
31
+ @styles = options[:styles]
32
+ @default_url = options[:default_url]
33
+ @validations = options[:validations]
34
+ @default_style = options[:default_style]
35
+ @storage = options[:storage]
36
+ @whiny_thumbnails = options[:whiny_thumbnails]
37
+ @options = options
38
+ @queued_for_delete = []
39
+ @queued_for_write = {}
40
+ @errors = []
41
+ @validation_errors = nil
42
+ @dirty = false
43
+
44
+ normalize_style_definition
45
+ initialize_storage
46
+
47
+ logger.info("[paperclip] Paperclip attachment #{name} on #{instance.class} initialized.")
48
+ end
49
+
50
+ # What gets called when you call instance.attachment = File. It clears errors,
51
+ # assigns attributes, processes the file, and runs validations. It also queues up
52
+ # the previous file for deletion, to be flushed away on #save of its host.
53
+ # In addition to form uploads, you can also assign another Paperclip attachment:
54
+ # new_user.avatar = old_user.avatar
55
+ def assign uploaded_file
56
+ %w(file_name).each do |field|
57
+ unless @instance.class.column_names.include?("#{name}_#{field}")
58
+ raise PaperclipError.new("#{self} model does not have required column '#{name}_#{field}'")
59
+ end
60
+ end
61
+
62
+ if uploaded_file.is_a?(Paperclip::Attachment)
63
+ uploaded_file = uploaded_file.to_file(:original)
64
+ end
65
+
66
+ return nil unless valid_assignment?(uploaded_file)
67
+ logger.info("[paperclip] Assigning #{uploaded_file.inspect} to #{name}")
68
+
69
+ queue_existing_for_delete
70
+ @errors = []
71
+ @validation_errors = nil
72
+
73
+ return nil if uploaded_file.nil?
74
+
75
+ logger.info("[paperclip] Writing attributes for #{name}")
76
+ @queued_for_write[:original] = uploaded_file['tempfile']
77
+ @instance[:"#{@name}_file_name"] = uploaded_file['tempfile'].original_filename.strip.gsub /[^\w\d\.\-]+/, '_'
78
+ @instance[:"#{@name}_content_type"] = uploaded_file['tempfile'].content_type.strip
79
+ @instance[:"#{@name}_file_size"] = uploaded_file['tempfile'].size.to_i
80
+ @instance[:"#{@name}_updated_at"] = Time.now
81
+
82
+ @dirty = true
83
+
84
+ post_process
85
+ ensure
86
+ validate
87
+ end
88
+
89
+ # Returns the public URL of the attachment, with a given style. Note that this
90
+ # does not necessarily need to point to a file that your web server can access
91
+ # and can point to an action in your app, if you need fine grained security.
92
+ # This is not recommended if you don't need the security, however, for
93
+ # performance reasons.
94
+ def url style = default_style
95
+ url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
96
+ updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
97
+ end
98
+
99
+ # Returns the path of the attachment as defined by the :path option. If the
100
+ # file is stored in the filesystem the path refers to the path of the file on
101
+ # disk. If the file is stored in S3, the path is the "key" part of the URL,
102
+ # and the :bucket option refers to the S3 bucket.
103
+ def path style = nil #:nodoc:
104
+ interpolate(@path, style)
105
+ end
106
+
107
+ # Alias to +url+
108
+ def to_s style = nil
109
+ url(style)
110
+ end
111
+
112
+ # Returns true if there are no errors on this attachment.
113
+ def valid?
114
+ validate
115
+ errors.length == 0
116
+ end
117
+
118
+ # Returns an array containing the errors on this attachment.
119
+ def errors
120
+ @errors.compact.uniq
121
+ end
122
+
123
+ # Returns true if there are changes that need to be saved.
124
+ def dirty?
125
+ @dirty
126
+ end
127
+
128
+ # Saves the file, if there are no errors. If there are, it flushes them to
129
+ # the instance's errors and returns false, cancelling the save.
130
+ def save
131
+ if valid?
132
+ logger.info("[paperclip] Saving files for #{name}")
133
+ flush_deletes
134
+ flush_writes
135
+ @dirty = false
136
+ true
137
+ else
138
+ logger.info("[paperclip] Errors on #{name}. Not saving.")
139
+ flush_errors
140
+ false
141
+ end
142
+ end
143
+
144
+ # Returns the name of the file as originally assigned, and as lives in the
145
+ # <attachment>_file_name attribute of the model.
146
+ def original_filename
147
+ instance[:"#{name}_file_name"]
148
+ end
149
+
150
+ def updated_at
151
+ time = instance[:"#{name}_updated_at"]
152
+ time && time.to_i
153
+ end
154
+
155
+ # A hash of procs that are run during the interpolation of a path or url.
156
+ # A variable of the format :name will be replaced with the return value of
157
+ # the proc named ":name". Each lambda takes the attachment and the current
158
+ # style as arguments. This hash can be added to with your own proc if
159
+ # necessary.
160
+ def self.interpolations
161
+ @interpolations ||= {
162
+ :merb_root => lambda{|attachment,style| Merb.root },
163
+ :class => lambda do |attachment,style|
164
+ attachment.instance.class.name.underscore.pluralize
165
+ end,
166
+ :basename => lambda do |attachment,style|
167
+ attachment.original_filename.gsub(File.extname(attachment.original_filename), "")
168
+ end,
169
+ :extension => lambda do |attachment,style|
170
+ ((style = attachment.styles[style]) && style.last) ||
171
+ File.extname(attachment.original_filename).gsub(/^\.+/, "")
172
+ end,
173
+ :id => lambda{|attachment,style| attachment.instance.id },
174
+ :id_partition => lambda do |attachment, style|
175
+ ("%09d" % attachment.instance.id).scan(/\d{3}/).join("/")
176
+ end,
177
+ :attachment => lambda{|attachment,style| attachment.name.to_s.downcase.pluralize },
178
+ :style => lambda{|attachment,style| style || attachment.default_style },
179
+ }
180
+ end
181
+
182
+ # This method really shouldn't be called that often. It's expected use is in the
183
+ # paperclip:refresh rake task and that's it. It will regenerate all thumbnails
184
+ # forcefully, by reobtaining the original file and going through the post-process
185
+ # again.
186
+ def reprocess!
187
+ new_original = Tempfile.new("paperclip-reprocess")
188
+ if old_original = to_file(:original)
189
+ new_original.write( old_original.read )
190
+ new_original.rewind
191
+
192
+ @queued_for_write = { :original => new_original }
193
+ post_process
194
+
195
+ old_original.close if old_original.respond_to?(:close)
196
+
197
+ save
198
+ else
199
+ true
200
+ end
201
+ end
202
+
203
+ def file?
204
+ !original_filename.blank?
205
+ end
206
+
207
+ private
208
+
209
+ def logger
210
+ instance.logger
211
+ end
212
+
213
+ def valid_assignment? file #:nodoc:
214
+ file.nil? || (file.is_a?(Mash) && file.has_key?(:tempfile))
215
+ end
216
+
217
+ def validate #:nodoc:
218
+ unless @validation_errors
219
+ @validation_errors = @validations.collect do |v|
220
+ v.call(self, instance)
221
+ end.flatten.compact.uniq
222
+ @errors += @validation_errors
223
+ end
224
+ @validation_errors
225
+ end
226
+
227
+ def normalize_style_definition
228
+ @styles.each do |name, args|
229
+ dimensions, format = [args, nil].flatten[0..1]
230
+ format = nil if format == ""
231
+ @styles[name] = [dimensions, format]
232
+ end
233
+ end
234
+
235
+ def initialize_storage
236
+ @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
237
+ self.extend(@storage_module)
238
+ end
239
+
240
+ def post_process #:nodoc:
241
+ return if @queued_for_write[:original].nil?
242
+ logger.info("[paperclip] Post-processing #{name}")
243
+ @styles.each do |name, args|
244
+ begin
245
+ dimensions, format = args
246
+ dimensions = dimensions.call(instance) if dimensions.respond_to? :call
247
+ @queued_for_write[name] = Thumbnail.make(@queued_for_write[:original],
248
+ dimensions,
249
+ format,
250
+ @whiny_thumnails)
251
+ rescue PaperclipError => e
252
+ @errors << e.message if @whiny_thumbnails
253
+ end
254
+ end
255
+ end
256
+
257
+ def interpolate pattern, style = default_style #:nodoc:
258
+ interpolations = self.class.interpolations.sort{|a,b| a.first.to_s <=> b.first.to_s }
259
+ interpolations.reverse.inject( pattern.dup ) do |result, interpolation|
260
+ tag, blk = interpolation
261
+ result.gsub(/:#{tag}/) do |match|
262
+ blk.call( self, style )
263
+ end
264
+ end
265
+ end
266
+
267
+ def queue_existing_for_delete #:nodoc:
268
+ return unless file?
269
+ logger.info("[paperclip] Queueing the existing files for #{name} for deletion.")
270
+ @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
271
+ path(style) if exists?(style)
272
+ end.compact
273
+ @instance[:"#{@name}_file_name"] = nil
274
+ @instance[:"#{@name}_content_type"] = nil
275
+ @instance[:"#{@name}_file_size"] = nil
276
+ @instance[:"#{@name}_updated_at"] = nil
277
+ end
278
+
279
+ def flush_errors #:nodoc:
280
+ @errors.each do |error|
281
+ instance.errors.add(name, error)
282
+ end
283
+ end
284
+
285
+ end
286
+ end
287
+
@@ -0,0 +1,109 @@
1
+ module Paperclip
2
+
3
+ # Defines the geometry of an image.
4
+ class Geometry
5
+ attr_accessor :height, :width, :modifier
6
+
7
+ # Gives a Geometry representing the given height and width
8
+ def initialize width = nil, height = nil, modifier = nil
9
+ height = nil if height == ""
10
+ width = nil if width == ""
11
+ @height = (height || width).to_f
12
+ @width = (width || height).to_f
13
+ @modifier = modifier
14
+ end
15
+
16
+ # Uses ImageMagick to determing the dimensions of a file, passed in as either a
17
+ # File or path.
18
+ def self.from_file file
19
+ file = file.path if file.respond_to? "path"
20
+ parse(`#{Paperclip.path_for_command('identify')} "#{file}"`) ||
21
+ raise(NotIdentifiedByImageMagickError.new("#{file} is not recognized by the 'identify' command."))
22
+ end
23
+
24
+ # Parses a "WxH" formatted string, where W is the width and H is the height.
25
+ def self.parse string
26
+ if match = (string && string.match(/\b(\d*)x(\d*)\b([\>\<\#\@\%^!])?/))
27
+ Geometry.new(*match[1,3])
28
+ end
29
+ end
30
+
31
+ # True if the dimensions represent a square
32
+ def square?
33
+ height == width
34
+ end
35
+
36
+ # True if the dimensions represent a horizontal rectangle
37
+ def horizontal?
38
+ height < width
39
+ end
40
+
41
+ # True if the dimensions represent a vertical rectangle
42
+ def vertical?
43
+ height > width
44
+ end
45
+
46
+ # The aspect ratio of the dimensions.
47
+ def aspect
48
+ width / height
49
+ end
50
+
51
+ # Returns the larger of the two dimensions
52
+ def larger
53
+ [height, width].max
54
+ end
55
+
56
+ # Returns the smaller of the two dimensions
57
+ def smaller
58
+ [height, width].min
59
+ end
60
+
61
+ # Returns the width and height in a format suitable to be passed to Geometry.parse
62
+ def to_s
63
+ "%dx%d%s" % [width, height, modifier]
64
+ end
65
+
66
+ # Same as to_s
67
+ def inspect
68
+ to_s
69
+ end
70
+
71
+ # Returns the scaling and cropping geometries (in string-based ImageMagick format)
72
+ # neccessary to transform this Geometry into the Geometry given. If crop is true,
73
+ # then it is assumed the destination Geometry will be the exact final resolution.
74
+ # In this case, the source Geometry is scaled so that an image containing the
75
+ # destination Geometry would be completely filled by the source image, and any
76
+ # overhanging image would be cropped. Useful for square thumbnail images. The cropping
77
+ # is weighted at the center of the Geometry.
78
+ def transformation_to dst, crop = false
79
+
80
+ if crop
81
+ ratio = Geometry.new( dst.width / self.width, dst.height / self.height )
82
+ scale_geometry, scale = scaling(dst, ratio)
83
+ crop_geometry = cropping(dst, ratio, scale)
84
+ else
85
+ scale_geometry = dst.to_s
86
+ end
87
+
88
+ [ scale_geometry, crop_geometry ]
89
+ end
90
+
91
+ private
92
+
93
+ def scaling dst, ratio
94
+ if ratio.horizontal? || ratio.square?
95
+ [ "%dx" % dst.width, ratio.width ]
96
+ else
97
+ [ "x%d" % dst.height, ratio.height ]
98
+ end
99
+ end
100
+
101
+ def cropping dst, ratio, scale
102
+ if ratio.horizontal? || ratio.square?
103
+ "%dx%d+%d+%d" % [ dst.width, dst.height, 0, (self.height * scale - dst.height) / 2 ]
104
+ else
105
+ "%dx%d+%d+%d" % [ dst.width, dst.height, (self.width * scale - dst.width) / 2, 0 ]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,47 @@
1
+ # Provides method that can be included on File-type objects (IO, StringIO, Tempfile, etc) to allow stream copying
2
+ # and Tempfile conversion.
3
+ module IOStream
4
+
5
+ # Returns a Tempfile containing the contents of the readable object.
6
+ def to_tempfile
7
+ tempfile = Tempfile.new("stream")
8
+ tempfile.binmode
9
+ self.stream_to(tempfile)
10
+ end
11
+
12
+ # Copies one read-able object from one place to another in blocks, obviating the need to load
13
+ # the whole thing into memory. Defaults to 8k blocks. If this module is included in both
14
+ # StringIO and Tempfile, then either can have its data copied anywhere else without typing
15
+ # worries or memory overhead worries. Returns a File if a String is passed in as the destination
16
+ # and returns the IO or Tempfile as passed in if one is sent as the destination.
17
+ def stream_to path_or_file, in_blocks_of = 8192
18
+ dstio = case path_or_file
19
+ when String then File.new(path_or_file, "wb+")
20
+ when IO then path_or_file
21
+ when Tempfile then path_or_file
22
+ end
23
+ buffer = ""
24
+ self.rewind
25
+ while self.read(in_blocks_of, buffer) do
26
+ dstio.write(buffer)
27
+ end
28
+ dstio.rewind
29
+ dstio
30
+ end
31
+ end
32
+
33
+ class IO
34
+ include IOStream
35
+ end
36
+
37
+ class Mash
38
+ include IOStream
39
+ end
40
+
41
+ %w( Tempfile StringIO ).each do |klass|
42
+ if Object.const_defined? klass
43
+ Object.const_get(klass).class_eval do
44
+ include IOStream
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,204 @@
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
+ # ":merb_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/:filename"
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)) 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
+ result = file.stream_to(path(style))
44
+ file.close
45
+ result.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.
84
+ # * +s3_permissions+: This is a String that should be one of the "canned" access
85
+ # policies that S3 provides (more information can be found here:
86
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAccessPolicy.html#RESTCannedAccessPolicies)
87
+ # The default for Paperclip is "public-read".
88
+ # * +s3_protocol+: The protocol for the URLs generated to your S3 assets. Can be either
89
+ # 'http' or 'https'. Defaults to 'http' when your :s3_permissions are 'public-read' (the
90
+ # default), and 'https' when your :s3_permissions are anything else.
91
+ # * +bucket+: This is the name of the S3 bucket that will store your files. Remember
92
+ # that the bucket must be unique across all of Amazon S3. If the bucket does not exist
93
+ # Paperclip will attempt to create it. The bucket name will not be interpolated.
94
+ # * +url+: There are two options for the S3 url. You can choose to have the bucket's name
95
+ # placed domain-style (bucket.s3.amazonaws.com) or path-style (s3.amazonaws.com/bucket).
96
+ # Normally, this won't matter in the slightest and you can leave the default (which is
97
+ # path-style, or :s3_path_url). But in some cases paths don't work and you need to use
98
+ # the domain-style (:s3_domain_url). Anything else here will be treated like path-style.
99
+ # * +path+: This is the key under the bucket in which the file will be stored. The
100
+ # URL will be constructed from the bucket and the path. This is what you will want
101
+ # to interpolate. Keys should be unique, like filenames, and despite the fact that
102
+ # S3 (strictly speaking) does not support directories, you can still use a / to
103
+ # separate parts of your file name.
104
+ module S3
105
+ def self.extended base
106
+ require 'right_aws'
107
+ base.instance_eval do
108
+ @bucket = @options[:bucket]
109
+ @s3_credentials = parse_credentials(@options[:s3_credentials])
110
+ @s3_options = @options[:s3_options] || {}
111
+ @s3_permissions = @options[:s3_permissions] || 'public-read'
112
+ @s3_protocol = @options[:s3_protocol] || (@s3_permissions == 'public-read' ? 'http' : 'https')
113
+ @url = ":s3_path_url" unless @url.to_s.match(/^s3.*url$/)
114
+ end
115
+ base.class.interpolations[:s3_path_url] = lambda do |attachment, style|
116
+ "#{attachment.s3_protocol}://s3.amazonaws.com/#{attachment.bucket_name}/#{attachment.path(style).gsub(%r{^/}, "")}"
117
+ end
118
+ base.class.interpolations[:s3_domain_url] = lambda do |attachment, style|
119
+ "#{attachment.s3_protocol}://#{attachment.bucket_name}.s3.amazonaws.com/#{attachment.path(style).gsub(%r{^/}, "")}"
120
+ end
121
+ ActiveRecord::Base.logger.info("[paperclip] S3 Storage Initalized.")
122
+ end
123
+
124
+ def s3
125
+ @s3 ||= RightAws::S3.new(@s3_credentials[:access_key_id],
126
+ @s3_credentials[:secret_access_key],
127
+ @s3_options)
128
+ end
129
+
130
+ def s3_bucket
131
+ @s3_bucket ||= s3.bucket(@bucket, true, @s3_permissions)
132
+ end
133
+
134
+ def bucket_name
135
+ @bucket
136
+ end
137
+
138
+ def parse_credentials creds
139
+ creds = find_credentials(creds).stringify_keys
140
+ (creds[Merb.env] || creds).symbolize_keys
141
+ end
142
+
143
+ def exists?(style = default_style)
144
+ s3_bucket.key(path(style)) ? true : false
145
+ end
146
+
147
+ def s3_protocol
148
+ @s3_protocol
149
+ end
150
+
151
+ # Returns representation of the data of the file assigned to the given
152
+ # style, in the format most representative of the current storage.
153
+ def to_file style = default_style
154
+ @queued_for_write[style] || s3_bucket.key(path(style))
155
+ end
156
+ alias_method :to_io, :to_file
157
+
158
+ def flush_writes #:nodoc:
159
+ logger.info("[paperclip] Writing files for #{name}")
160
+ @queued_for_write.each do |style, file|
161
+ begin
162
+ logger.info("[paperclip] -> #{path(style)}")
163
+ key = s3_bucket.key(path(style))
164
+ key.data = file
165
+ key.put(nil, @s3_permissions)
166
+ rescue RightAws::AwsError => e
167
+ raise
168
+ end
169
+ end
170
+ @queued_for_write = {}
171
+ end
172
+
173
+ def flush_deletes #:nodoc:
174
+ logger.info("[paperclip] Writing files for #{name}")
175
+ @queued_for_delete.each do |path|
176
+ begin
177
+ logger.info("[paperclip] -> #{path}")
178
+ if file = s3_bucket.key(path)
179
+ file.delete
180
+ end
181
+ rescue RightAws::AwsError
182
+ # Ignore this.
183
+ end
184
+ end
185
+ @queued_for_delete = []
186
+ end
187
+
188
+ def find_credentials creds
189
+ case creds
190
+ when File:
191
+ YAML.load_file(creds.path)
192
+ when String:
193
+ YAML.load_file(creds)
194
+ when Hash:
195
+ creds
196
+ else
197
+ raise ArgumentError, "Credentials are not a path, file, or hash."
198
+ end
199
+ end
200
+ private :find_credentials
201
+
202
+ end
203
+ end
204
+ end