coryodaniel-milton 0.3.7

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,49 @@
1
+ module Milton
2
+ # Generic view of an "Image", or rather, something with a width and a
3
+ # height we care about =).
4
+ class Image
5
+ attr_accessor :width
6
+ attr_accessor :height
7
+
8
+ class << self
9
+ # Instantiates a new image from the given path. Uses ImageMagick's
10
+ # identify method to determine the width and height of the image with
11
+ # the given path and returns a new Image with those dimensions.
12
+ #
13
+ # Raises a MissingFileError if the given path could not be identify'd
14
+ # by ImageMagick (resulting in a height and width).
15
+ def from_path(path)
16
+ raise Milton::MissingFileError.new("Could not identify #{path} as an image, does the file exist?") unless Milton.syscall("identify #{path}") =~ /.*? (\d+)x(\d+)\+\d+\+\d+/
17
+ new($1, $2)
18
+ end
19
+
20
+ # Instantiates a new image from the given geometry string. A geometry
21
+ # string is just something like 50x40. The first number is the width
22
+ # and the second is the height.
23
+ def from_geometry(geometry)
24
+ new(*(geometry.split("x").collect(&:to_i)))
25
+ end
26
+ end
27
+
28
+ # Instantiates a new Image with the given width and height
29
+ def initialize(width=nil, height=nil)
30
+ @width = width.to_i
31
+ @height = height.to_i
32
+ end
33
+
34
+ # Returns the larger dimension of the Image
35
+ def larger_dimension
36
+ width > height ? width : height
37
+ end
38
+
39
+ # Returns true if the Image is wider than it is tall
40
+ def wider?
41
+ width > height
42
+ end
43
+
44
+ # Returns true if the Image is square
45
+ def square?
46
+ width == height
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
1
+ require File.join(File.dirname(__FILE__), 'stored_file')
2
+
3
+ module Milton
4
+ module Storage
5
+ # How Milton deals with a file stored on disk...
6
+ class DiskFile < StoredFile
7
+ class << self
8
+ # Creates the given directory and sets it to the mode given in
9
+ # options[:chmod]
10
+ def recreate_directory(directory, options)
11
+ return true if File.exists?(directory)
12
+ FileUtils.mkdir_p(directory)
13
+ FileUtils.chmod(options[:storage_options][:chmod], directory)
14
+ end
15
+ end
16
+
17
+ # Returns the full path and filename to the file with the given options.
18
+ # If no options are given then returns the path and filename to the
19
+ # original file.
20
+ def path
21
+ File.join(dirname, filename)
22
+ end
23
+
24
+ # Returns the full directory path up to the file, w/o the filename.
25
+ def dirname
26
+ File.join(root_path, partitioned_path)
27
+ end
28
+
29
+ # Returns true if the file exists on the underlying file system.
30
+ def exists?
31
+ File.exist?(path)
32
+ end
33
+
34
+ # Writes the given source file to this file's path.
35
+ def store(source)
36
+ Milton.log "storing #{source} to disk at #{path}"
37
+ self.class.recreate_directory(dirname, options)
38
+ File.cp(source, path)
39
+ FileUtils.chmod(options[:storage_options][:chmod], path)
40
+ end
41
+
42
+ # Removes the file from the underlying file system and any derivatives of
43
+ # the file.
44
+ def destroy
45
+ Milton.log "destroying path #{dirname}"
46
+ FileUtils.rm_rf dirname if File.exists?(dirname)
47
+ end
48
+
49
+ # Copies this file to the given location on disk.
50
+ def copy(destination)
51
+ Milton.log "copying #{path} to #{destination}"
52
+ FileUtils.cp(path, destination)
53
+ end
54
+
55
+ # Returns the mime type of the file.
56
+ def mime_type
57
+ Milton.syscall("file -Ib #{path}").gsub(/\n/,"")
58
+ end
59
+
60
+ protected
61
+
62
+ # Partitioner that takes an id, pads it up to 12 digits then splits
63
+ # that into 4 folders deep, each 3 digits long.
64
+ #
65
+ # i.e.
66
+ # 000/000/012/139
67
+ #
68
+ # Scheme allows for 1000 billion files while never storing more than
69
+ # 1000 files in a single folder.
70
+ #
71
+ # Can overwrite this method to provide your own partitioning scheme.
72
+ def partitioned_path
73
+ # TODO: there's probably some fancy 1-line way to do this...
74
+ padded = ("0"*(12-id.to_s.size)+id.to_s).split('')
75
+ File.join(*[0, 3, 6, 9].collect{ |i| padded.slice(i, 3).join })
76
+ end
77
+
78
+ # The full path to the root of where files will be stored on disk.
79
+ def root_path
80
+ options[:storage_options][:root]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,103 @@
1
+ require 'milton/storage/stored_file'
2
+ require 'right_aws'
3
+ # these are required to generate HMAC:SHA1 signature for retrieving private
4
+ # files from S3
5
+ require 'base64'
6
+ require 'openssl'
7
+ require 'digest/sha1'
8
+
9
+ # TODO: Raise helpful errors on missing required options instead of letting
10
+ # right_aws fail cryptically
11
+
12
+ module Milton
13
+ module Storage
14
+ class S3File < StoredFile
15
+ def path
16
+ "http://#{options[:storage_options][:bucket]}.s3.amazonaws.com/#{key}"
17
+ end
18
+
19
+ def dirname
20
+ id
21
+ end
22
+
23
+ def exists?
24
+ bucket.key(key).exists?
25
+ end
26
+
27
+ def store(source)
28
+ Milton.log "storing #{source} to #{path} (#{options[:storage_options][:permissions]})"
29
+ bucket.put(key, File.open(source), {}, options[:storage_options][:permissions])
30
+ end
31
+
32
+ def destroy
33
+ Milton.log "destroying #{path}"
34
+ bucket.key(key).try(:delete)
35
+ end
36
+
37
+ # Copies this file to the given location on disk.
38
+ # Note that this copies to a LOCAL location, not to another place on S3!
39
+ def copy(destination)
40
+ Milton.log "copying #{path} to #{destination}"
41
+
42
+ s3 = RightAws::S3Interface.new(options[:storage_options][:access_key_id], options[:storage_options][:secret_access_key], :logger => Rails.logger)
43
+ file = File.new(destination, 'wb')
44
+
45
+ # stream the download as opposed to downloading the whole thing and reading
46
+ # it all into memory at once since it might be gigantic...
47
+ s3.get(options[:storage_options][:bucket], key) { |chunk| file.write(chunk) }
48
+ file.close
49
+ end
50
+
51
+ def mime_type
52
+ # TODO: implement
53
+ end
54
+
55
+ # Generates a signed url to this resource on S3.
56
+ #
57
+ # See doc for +signature+.
58
+ def signed_url(expires_at=nil)
59
+ "#{path}?AWSAccessKeyId=#{options[:storage_options][:access_key_id]}" +
60
+ (expires_at ? "&Expires=#{expires_at.to_i}" : '') +
61
+ "&Signature=#{signature(expires_at)}"
62
+ end
63
+
64
+ # Generates a signature for passing authorization for this file on to
65
+ # another user without having to proxy the file.
66
+ #
67
+ # See http://docs.amazonwebservices.com/AmazonS3/latest/index.html?RESTAuthentication.html
68
+ #
69
+ # Optionally pass +expires_at+ to make the signature valid only until
70
+ # given expiration date/time -- useful for temporary secure access to
71
+ # files.
72
+ def signature(expires_at=nil)
73
+ CGI.escape(Base64.encode64(OpenSSL::HMAC.digest(
74
+ OpenSSL::Digest::Digest.new('sha1'),
75
+ options[:storage_options][:secret_access_key],
76
+ "GET\n\n\n#{expires_at ? expires_at.to_i : ''}\n/#{options[:storage_options][:bucket]}/#{key}"
77
+ )).chomp.gsub(/\n/, ''))
78
+ end
79
+
80
+ protected
81
+
82
+ def key
83
+ "#{dirname}/#{filename}"
84
+ end
85
+
86
+ def s3
87
+ @s3 ||= RightAws::S3.new(
88
+ options[:storage_options][:access_key_id],
89
+ options[:storage_options][:secret_access_key],
90
+ { :protocol => http? ? 'http' : 'https', :port => http? ? 80 : 443, :logger => Rails.logger }
91
+ )
92
+ end
93
+
94
+ def http?
95
+ options[:storage_options].has_key?(:protocol) && options[:storage_options][:protocol] == 'http'
96
+ end
97
+
98
+ def bucket
99
+ @bucket ||= s3.bucket(options[:storage_options][:bucket], true, options[:storage_options][:permissions])
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,46 @@
1
+ module Milton
2
+ module Storage
3
+ class StoredFile
4
+ class << self
5
+ # Sanitizes the given filename, removes pathnames and the special chars
6
+ # needed for options seperation for derivatives
7
+ def sanitize_filename(filename, options)
8
+ File.basename(filename, File.extname(filename)).gsub(/^.*(\\|\/)/, '').
9
+ gsub(/[^\w]|#{Regexp.escape(options[:separator])}/, options[:replacement]).
10
+ strip + File.extname(filename)
11
+ end
12
+
13
+ def create(filename, id, source, options)
14
+ returning new(filename, id, options) do |file|
15
+ file.store(source)
16
+ end
17
+ end
18
+
19
+ # Returns the adapter class specified by the given type (by naming
20
+ # convention)
21
+ #
22
+ # Storage::StoredFile.adapter(:s3) => Storage::S3File
23
+ # Storage::StoredFile.adapter(:disk) => Storage::DiskFile
24
+ #
25
+ def adapter(type)
26
+ "Milton::Storage::#{type.to_s.classify}File".constantize
27
+ end
28
+ end
29
+
30
+ attr_accessor :filename, :id, :options
31
+
32
+ def initialize(filename, id, options)
33
+ self.filename = filename
34
+ self.id = id
35
+ self.options = options
36
+ end
37
+
38
+ # Creates a clone of this StoredFile of the same type with the same id
39
+ # and options but using the given filename. Doesn't actually do any
40
+ # copying of the underlying file data.
41
+ def clone(filename)
42
+ self.class.new(filename, self.id, self.options)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ module Milton
2
+ module Uploading
3
+ module ClassMethods
4
+ def self.extended(base)
5
+ base.setup_callbacks
6
+ end
7
+
8
+ def setup_callbacks
9
+ # Rails 2.1 fix for callbacks
10
+ define_callbacks(:before_file_saved, :after_file_saved) if defined?(::ActiveSupport::Callbacks)
11
+ after_save :save_uploaded_file
12
+ after_file_saved :create_derivatives if @after_create_callbacks.delete(:create_derivatives)
13
+ end
14
+
15
+ unless defined?(::ActiveSupport::Callbacks)
16
+ def before_file_saved(&block)
17
+ write_inheritable_array(:before_file_saved, [block])
18
+ end
19
+
20
+ def after_file_saved(&block)
21
+ write_inheritable_array(:after_file_saved, [block])
22
+ end
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+ def self.included(base)
28
+ # Rails 2.1 fix for callbacks
29
+ base.define_callbacks *[:before_file_saved, :after_file_saved] if base.respond_to?(:define_callbacks)
30
+ end
31
+
32
+ # Set file=<uploaded file> on your model to handle an uploaded file.
33
+ def file=(file)
34
+ return nil if file.nil? || file.size == 0
35
+ @upload = Upload.new(file, self.class.milton_options)
36
+ self.filename = @upload.filename
37
+ self.size = @upload.size if respond_to?(:size=)
38
+ self.content_type = Milton::File.mime_type?(@upload) if respond_to?(:content_type=)
39
+ end
40
+
41
+ protected
42
+
43
+ def save_uploaded_file
44
+ if @upload && !@upload.stored?
45
+ callback :before_file_saved
46
+ @attached_file = @upload.store(id)
47
+ callback :after_file_saved
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ class Upload
54
+ attr_reader :content_type, :filename, :size, :options
55
+
56
+ def initialize(data_or_path, options)
57
+ @stored = false
58
+ @tempfile = Milton::Tempfile.create(data_or_path, options[:tempfile_path])
59
+ @content_type = data_or_path.content_type
60
+ @filename = Storage::StoredFile.sanitize_filename(data_or_path.original_filename, options) if respond_to?(:filename)
61
+ @size = File.size(self.temp_path)
62
+ @options = options
63
+ end
64
+
65
+ def stored?
66
+ @stored
67
+ end
68
+
69
+ def store(id)
70
+ return true if stored?
71
+ returning Storage::StoredFile.adapter(options[:storage]).create(filename, id, temp_path, options) do
72
+ @stored = true
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ def temp_path
79
+ @tempfile.respond_to?(:path) ? @tempfile.path : @tempfile.to_s
80
+ end
81
+ end
82
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,359 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class AttachmentTest < ActiveSupport::TestCase
4
+ context "being included into a model" do
5
+ class NotAnAttachment < ActiveRecord::Base
6
+ end
7
+
8
+ context "NotAnAttachment" do
9
+ should "not have milton_options" do
10
+ assert !NotAnAttachment.respond_to?(:milton_options)
11
+ end
12
+
13
+ should "not have attachment methods" do
14
+ assert !NotAnAttachment.respond_to?(:has_attachment_methods)
15
+ end
16
+ end
17
+
18
+ context "Attachment" do
19
+ should "have milton_options on Attachment" do
20
+ assert Attachment.respond_to?(:milton_options)
21
+ end
22
+
23
+ should "have attachment methods" do
24
+ assert Attachment.respond_to?(:has_attachment_methods)
25
+ end
26
+
27
+ should "have a hash of options" do
28
+ assert Attachment.milton_options.is_a?(Hash)
29
+ end
30
+ end
31
+ end
32
+
33
+ context "setting options" do
34
+ context "defaults" do
35
+ class DefaultAttachment < ActiveRecord::Base
36
+ is_attachment
37
+ end
38
+
39
+ should "use :disk as default storage" do
40
+ assert_equal :disk, Attachment.milton_options[:storage]
41
+ end
42
+
43
+ should "use #{Rails.root}/public/default_attachments as default disk storage root" do
44
+ assert_equal File.join(Rails.root, 'public', 'default_attachments'), DefaultAttachment.milton_options[:storage_options][:root]
45
+ end
46
+
47
+ should "use 0755 as default mode for new disk files" do
48
+ assert_equal 0755, DefaultAttachment.milton_options[:storage_options][:chmod]
49
+ end
50
+
51
+ should "raise LoadError if storage engine could not be required" do
52
+ assert_raise LoadError do
53
+ class BadStorageAttachment < ActiveRecord::Base
54
+ is_attachment :storage => :foo
55
+ end
56
+ end
57
+ end
58
+
59
+ should "raise helpful LoadError if storage engine could not be required" do
60
+ begin
61
+ class BadStorageAttachment < ActiveRecord::Base
62
+ is_attachment :storage => :foo
63
+ end
64
+ rescue LoadError => e
65
+ assert_equal "No 'foo' storage found for Milton (failed to require milton/storage/foo_file)", e.message
66
+ end
67
+ end
68
+ end
69
+
70
+ context "inheritence" do
71
+ class FooImage < Image
72
+ is_attachment :resizeable => { :sizes => { :foo => { :size => '10x10' } } }
73
+ end
74
+
75
+ class BarImage < FooImage # note that BarImage < FooImage < Image
76
+ is_attachment :resizeable => { :sizes => { } }
77
+ end
78
+
79
+ should "inherit settings from Image" do
80
+ assert_equal Image.milton_options[:storage_options][:root], FooImage.milton_options[:storage_options][:root]
81
+ end
82
+
83
+ should "overwrite settings from Image when redefined in FooImage" do
84
+ assert_equal({ :foo => { :size => '10x10' } }, FooImage.milton_options[:resizeable][:sizes])
85
+ end
86
+
87
+ should "overwrite settings from FooImage when redefined in BarImage" do
88
+ assert_equal({}, BarImage.milton_options[:resizeable][:sizes])
89
+ end
90
+ end
91
+
92
+ context "encapsulation" do
93
+ class FooRootImage < Image
94
+ is_attachment :storage_options => { :root => '/foo' }
95
+ end
96
+
97
+ class BarRootImage < Image
98
+ is_attachment :storage_options => { :root => '/bar' }
99
+ end
100
+
101
+ should "not overwrite FooRootImage's root setting with BarRootImage's" do
102
+ assert_equal '/foo', FooRootImage.milton_options[:storage_options][:root]
103
+ end
104
+
105
+ should "not overwrite BarRootImage's root setting with FooRootImage's" do
106
+ assert_equal '/bar', BarRootImage.milton_options[:storage_options][:root]
107
+ end
108
+ end
109
+ end
110
+
111
+ context "getting mime-type" do
112
+ setup do
113
+ @attachment = Attachment.new :file => upload('milton.jpg')
114
+ end
115
+
116
+ context "from freshly uploaded file" do
117
+ should "recognize it as an image/jpg" do
118
+ assert_equal 'image/jpg', @attachment.content_type
119
+ end
120
+ end
121
+
122
+ context "from existing file" do
123
+ setup do
124
+ @attachment.save
125
+ @attachment.reload
126
+ end
127
+
128
+ should "recognize it as an image/jpg" do
129
+ assert_equal 'image/jpg', @attachment.content_type
130
+ end
131
+ end
132
+
133
+ context "from file with no content_type set" do
134
+ setup do
135
+ @attachment.update_attribute(:content_type, nil)
136
+ @attachment.save
137
+ @attachment.reload
138
+ end
139
+
140
+ should "attempt to determine mime_type from file" do
141
+ # this is implemented w/ unix file cmd so is system dependent currently...
142
+ assert_equal 'image/jpeg', @attachment.content_type
143
+ end
144
+ end
145
+ end
146
+
147
+ context "creating attachment folder" do
148
+ raise "Failed to create #{File.join(output_path, 'exists')}" unless FileUtils.mkdir_p(File.join(output_path, 'exists'))
149
+ FileUtils.ln_s 'exists', File.join(output_path, 'linked')
150
+ raise "Failed to symlink #{File.join(output_path, 'linked')}" unless File.symlink?(File.join(output_path, 'linked'))
151
+
152
+ class NoRootAttachment < Attachment
153
+ is_attachment :storage_options => { :root => File.join(ActiveSupport::TestCase.output_path, 'nonexistant') }
154
+ end
155
+
156
+ class RootExistsAttachment < Attachment
157
+ is_attachment :storage_options => { :root => File.join(ActiveSupport::TestCase.output_path, 'exists') }
158
+ end
159
+
160
+ class SymlinkAttachment < Attachment
161
+ is_attachment :storage_options => { :root => File.join(ActiveSupport::TestCase.output_path, 'linked') }
162
+ end
163
+
164
+ should "create root path when root path does not exist" do
165
+ @attachment = NoRootAttachment.create :file => upload('milton.jpg')
166
+ assert File.exists?(@attachment.path)
167
+ assert File.exists?(File.join(output_path, 'nonexistant'))
168
+ assert_match /nonexistant/, @attachment.path
169
+ end
170
+
171
+ should "work when root path already exists" do
172
+ @attachment = RootExistsAttachment.create :file => upload('milton.jpg')
173
+ assert File.exists?(@attachment.path)
174
+ assert_match /exists/, @attachment.path
175
+ end
176
+
177
+ should "work when root path is a symlink" do
178
+ @attachment = SymlinkAttachment.create :file => upload('milton.jpg')
179
+ assert File.exists?(@attachment.path)
180
+ assert_match /linked/, @attachment.path
181
+ end
182
+ end
183
+
184
+ context "being destroyed" do
185
+ setup do
186
+ @attachment = Attachment.create :file => upload('milton.jpg')
187
+ end
188
+
189
+ should "delete the underlying file from the filesystem" do
190
+ @attachment.destroy
191
+ assert !File.exists?(@attachment.path)
192
+ end
193
+
194
+ # the partitioning algorithm ensures that each attachment model has its own
195
+ # folder, so we can safely delete the folder, if you write a new
196
+ # partitioner this might change!
197
+ should "delete the directory containing the file and all derivatives from the filesystem" do
198
+ @attachment.destroy
199
+ assert !File.exists?(File.dirname(@attachment.path))
200
+ end
201
+ end
202
+
203
+ context "instantiating" do
204
+ setup do
205
+ @image = Image.new :file => upload('milton.jpg')
206
+ end
207
+
208
+ should "have a file= method" do
209
+ assert @image.respond_to?(:file=)
210
+ end
211
+
212
+ should "set the filename from the uploaded file" do
213
+ assert_equal 'milton.jpg', @image.filename
214
+ end
215
+
216
+ should "strip seperator (.) from the filename and replace them with replacement (-)" do
217
+ @image.filename = 'foo.bar.baz.jpg'
218
+ assert_equal 'foo-bar-baz.jpg', @image.filename
219
+ end
220
+ end
221
+
222
+ context "path partitioning" do
223
+ setup do
224
+ @image = Image.new :file => upload('milton.jpg')
225
+ end
226
+
227
+ should "be stored in a partitioned folder based on its id" do
228
+ assert_match /^.*\/0*#{@image.id}\/#{@image.filename}$/, @image.path
229
+ end
230
+ end
231
+
232
+ context "public path helper" do
233
+ setup do
234
+ @image = Image.new(:file => upload('milton.jpg'))
235
+ end
236
+
237
+ should "give the path from public/ on to the filename" do
238
+ flexmock(@image, :path => '/root/public/assets/1/milton.jpg')
239
+ assert_equal "/assets/1/milton.jpg", @image.public_path
240
+ end
241
+
242
+ should "give the path from foo/ on to the filename" do
243
+ flexmock(@image, :path => '/root/foo/assets/1/milton.jpg')
244
+ assert_equal "/assets/1/milton.jpg", @image.public_path({}, 'foo')
245
+ end
246
+ end
247
+
248
+ context "handling uploads" do
249
+ context "filename column" do
250
+ should "raise an exception if there is no filename column" do
251
+ assert_raise RuntimeError do
252
+ class NotUploadable < ActiveRecord::Base # see schema.rb, there is a not_uploadables table
253
+ is_attachment
254
+ end
255
+ end
256
+ end
257
+
258
+ should "not raise an exception if the underlying table doesn't exist" do
259
+ assert_nothing_raised do
260
+ class NoTable < ActiveRecord::Base
261
+ is_attachment
262
+ end
263
+ end
264
+ end
265
+ end
266
+
267
+ context "class extensions" do
268
+ context "class methods" do
269
+ should "add before_file_saved callback" do
270
+ assert Attachment.respond_to?(:before_file_saved)
271
+ end
272
+
273
+ should "add after_file_saved callback" do
274
+ assert Attachment.respond_to?(:after_file_saved)
275
+ end
276
+ end
277
+ end
278
+
279
+ context "handling file upload" do
280
+ context "saving upload" do
281
+ setup do
282
+ @attachment = Attachment.new :file => upload('milton.jpg')
283
+ end
284
+
285
+ should "save the upload to the filesystem on save" do
286
+ @attachment.save
287
+ assert File.exists?(@attachment.path)
288
+ end
289
+
290
+ should "have the same filesize as original file when large enough not to be a StringIO" do
291
+ # FIXME: this doesn't actually upload as a StringIO, figure out how to
292
+ # force that
293
+ @attachment.save
294
+ assert_equal File.size(File.join(File.dirname(__FILE__), '..', 'fixtures', 'milton.jpg')), File.size(@attachment.path)
295
+ end
296
+
297
+ should "have the same filesize as original file when small enough to be a StringIO" do
298
+ assert_equal File.size(File.join(File.dirname(__FILE__), '..', 'fixtures', 'mini-milton.jpg')), File.size(Attachment.create(:file => upload('mini-milton.jpg')).path)
299
+ end
300
+ end
301
+
302
+ context "stored full filename" do
303
+ setup do
304
+ @attachment = Attachment.create! :file => upload('milton.jpg')
305
+ end
306
+
307
+ should "use set root" do
308
+ assert_match /^#{@attachment.milton_options[:storage_options][:root]}.*$/, @attachment.path
309
+ end
310
+
311
+ should "use uploaded filename" do
312
+ assert_match /^.*#{@attachment.filename}$/, @attachment.path
313
+ end
314
+ end
315
+
316
+ context "sanitizing filename" do
317
+ setup do
318
+ @attachment = Attachment.create! :file => upload('unsanitary .milton.jpg')
319
+ end
320
+
321
+ should "strip the space and . and replace them with -" do
322
+ assert_match /^.*\/unsanitary--milton.jpg$/, @attachment.path
323
+ end
324
+
325
+ should "exist with sanitized filename" do
326
+ assert File.exists?(@attachment.path)
327
+ end
328
+ end
329
+
330
+ context "saving attachment after upload" do
331
+ setup do
332
+ @attachment = Attachment.create! :file => upload('unsanitary .milton.jpg')
333
+ end
334
+
335
+ should "save the file again" do
336
+ assert_nothing_raised do
337
+ Attachment.find(@attachment.id).save!
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
343
+
344
+ context "updating an existing attachment" do
345
+ setup do
346
+ @attachment = Attachment.create! :file => upload('milton.jpg')
347
+ @original_path = @attachment.path
348
+ @attachment.update_attributes! :file => upload('big-milton.jpg')
349
+ end
350
+
351
+ should "store the path to the updated upload" do
352
+ assert_equal 'big-milton.jpg', File.basename(@attachment.path)
353
+ end
354
+
355
+ should "save the updated upload" do
356
+ assert File.exists?(@attachment.path)
357
+ end
358
+ end
359
+ end