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.
- data/MIT-LICENSE +20 -0
- data/README.markdown +350 -0
- data/Rakefile +9 -0
- data/init.rb +1 -0
- data/lib/milton.rb +111 -0
- data/lib/milton/attachment.rb +202 -0
- data/lib/milton/core/file.rb +22 -0
- data/lib/milton/core/tempfile.rb +44 -0
- data/lib/milton/derivatives/derivative.rb +106 -0
- data/lib/milton/derivatives/thumbnail.rb +39 -0
- data/lib/milton/derivatives/thumbnail/crop_calculator.rb +64 -0
- data/lib/milton/derivatives/thumbnail/image.rb +49 -0
- data/lib/milton/storage/disk_file.rb +84 -0
- data/lib/milton/storage/s3_file.rb +103 -0
- data/lib/milton/storage/stored_file.rb +46 -0
- data/lib/milton/uploading.rb +82 -0
- data/test/fixtures/big-milton.jpg +0 -0
- data/test/fixtures/milton.jpg +0 -0
- data/test/fixtures/mini-milton.jpg +0 -0
- data/test/fixtures/unsanitary .milton.jpg +0 -0
- data/test/milton/attachment_test.rb +359 -0
- data/test/milton/milton_test.rb +21 -0
- data/test/milton/resizing_test.rb +70 -0
- data/test/s3_helper.rb +59 -0
- data/test/schema.rb +13 -0
- data/test/test_helper.rb +78 -0
- metadata +87 -0
|
@@ -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
|
|
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
|