attacheable 1.1 → 1.2
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/Rakefile +2 -2
- data/lib/attacheable.rb +239 -0
- data/lib/attacheable/file_naming.rb +48 -0
- data/lib/attacheable/photo_handler.rb +48 -0
- data/lib/attacheable/uploading.rb +93 -0
- data/pkg/attacheable-1.1.gem +0 -0
- data/test/attacheable_test.rb +194 -0
- data/test/fixtures/life.jpg +0 -0
- data/test/fixtures/test.doc +0 -0
- data/test/fixtures/wrong_type +0 -0
- data/test/test_helper.rb +16 -0
- metadata +13 -1
data/Rakefile
CHANGED
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
|
|
5
5
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
7
7
|
s.name = 'attacheable'
|
8
|
-
s.version = '1.
|
8
|
+
s.version = '1.2'
|
9
9
|
s.summary = 'Library to handle image uploads'
|
10
10
|
s.autorequire = 'attacheable'
|
11
11
|
s.author = "Max Lapshin"
|
@@ -25,7 +25,7 @@ spec = Gem::Specification.new do |s|
|
|
25
25
|
5. create only one row in table for one image. No separate rows for each thumbnail."
|
26
26
|
s.rubyforge_project = "attacheable"
|
27
27
|
s.has_rdoc = false
|
28
|
-
s.files = FileList["
|
28
|
+
s.files = FileList["**/**"].exclude(".git").to_a
|
29
29
|
|
30
30
|
end
|
31
31
|
|
data/lib/attacheable.rb
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
require File.dirname(__FILE__)+"/attacheable/file_naming"
|
2
|
+
require File.dirname(__FILE__)+"/attacheable/uploading"
|
3
|
+
class ActiveRecord::Base
|
4
|
+
#
|
5
|
+
# In model write has_attachment (conflicts with acts_as_attachment) with options:
|
6
|
+
#
|
7
|
+
# :thumbnails => list of thumbnails, i.e. {:medium => "120x", :large => "800x600"}
|
8
|
+
# :croppable_thumbnails => list of thumbnails, which must be cropped to center, i.e.: [:large, :preview]
|
9
|
+
# :path_prefix => path, where to store photos, i.e.: "public/system/photos"
|
10
|
+
# :replicas => [{:user => "user1", :host => "host1"}], list of hosts, where to clone all loaded originals
|
11
|
+
# :autocreate => true/false, whether to autocreate thumbnails on requesting thumbnail
|
12
|
+
#
|
13
|
+
# After this, add to routes:
|
14
|
+
# map.assets 'system/photos/*path_info', :controller => "photos", :action => "show"
|
15
|
+
# and add to PhotosController:
|
16
|
+
# def show
|
17
|
+
# photo, data = Photo.data_by_path_info(params[:path_info])
|
18
|
+
# render :text => data, :content_type => photo && photo.content_type
|
19
|
+
# end
|
20
|
+
# This will enable creation on demand
|
21
|
+
#
|
22
|
+
# You can also add
|
23
|
+
# uri "/system/photos/", :handler => Attacheable::PhotoHandler.new("/system/photos"), :in_front => true
|
24
|
+
# to any mongrel scripts for nonblocking image creation
|
25
|
+
#
|
26
|
+
# Table for this plugin should have fields:
|
27
|
+
# filename : string
|
28
|
+
# content_type : string (optional)
|
29
|
+
# width : integer (optional)
|
30
|
+
# heigth : integer (optional)
|
31
|
+
#
|
32
|
+
#
|
33
|
+
def self.has_attachment(options = {})
|
34
|
+
class_inheritable_accessor :attachment_options
|
35
|
+
self.attachment_options = options
|
36
|
+
|
37
|
+
options.with_indifferent_access
|
38
|
+
options[:autocreate] ||= false
|
39
|
+
options[:thumbnails] ||= {}
|
40
|
+
options[:thumbnails].symbolize_keys!.with_indifferent_access
|
41
|
+
options[:croppable_thumbnails] ||= []
|
42
|
+
options[:croppable_thumbnails] = options[:croppable_thumbnails].map(&:to_sym)
|
43
|
+
options[:path_prefix] ||= "public/system/#{table_name}"
|
44
|
+
options[:valid_filetypes] ||= %w(jpeg gif png psd)
|
45
|
+
include(Attacheable)
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Currently it will check valid filetype (unless option valid_filetypes set to :all)
|
50
|
+
#
|
51
|
+
def self.validates_as_attachment(options = {})
|
52
|
+
options[:message] ||= "Incorrect file type. Valid file types include: #{attachment_options[:valid_filetypes].to_sentence}"
|
53
|
+
self.attachment_options[:validation_message] = options[:message]
|
54
|
+
|
55
|
+
validate :valid_filetype?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
module Attacheable
|
61
|
+
include Attacheable::Uploading
|
62
|
+
include Attacheable::FileNaming
|
63
|
+
|
64
|
+
def self.included(base) #:nodoc:
|
65
|
+
base.before_update :rename_file
|
66
|
+
base.after_save :save_to_storage
|
67
|
+
base.after_destroy :remove_files
|
68
|
+
base.extend(ClassMethods)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def self.root
|
73
|
+
return RAILS_ROOT if defined?(RAILS_ROOT)
|
74
|
+
return Merb.root if defined?(Merb)
|
75
|
+
return File.dirname(__FILE__)+"/../.."
|
76
|
+
end
|
77
|
+
|
78
|
+
module ClassMethods
|
79
|
+
|
80
|
+
#
|
81
|
+
# You can delete all thumbnails or with selected type
|
82
|
+
#
|
83
|
+
def regenerate_thumbnails!(thumbnail = nil)
|
84
|
+
connection.select_values("select id from #{table_name}").each do |object_id|
|
85
|
+
object = find_by_id(object_id)
|
86
|
+
if object && object.filename
|
87
|
+
if thumbnail
|
88
|
+
FileUtils.rm_f(object.full_filename_without_creation(thumbnail))
|
89
|
+
else
|
90
|
+
to_remove = Dir["#{File.dirname(object.full_filename_without_creation)}/*"] - [object.full_filename_without_creation]
|
91
|
+
FileUtils.rm_f(to_remove)
|
92
|
+
end
|
93
|
+
#object.full_filename_with_creation(thumbnail)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# It is designed to read params[:path_info], or splitted PATH_INFO in mongrel handler
|
100
|
+
# It assumes, that path_info is of the following format ["0000", "0001", "file_medium.jpg"]
|
101
|
+
def data_by_path_info(path_info)
|
102
|
+
id1, id2, path = path_info
|
103
|
+
return [nil, nil] unless id1 && id2 && path
|
104
|
+
object = find(id1.to_i*1000 + id2.to_i)
|
105
|
+
if path = object.full_filename_by_path(path)
|
106
|
+
return [object, File.read(path)] if File.exists?(path)
|
107
|
+
end
|
108
|
+
[object, nil]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def attachment_options #:nodoc:
|
113
|
+
self.class.attachment_options
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Returns real path to original file if thumbnail is nil or path with thumbnail part inserted
|
118
|
+
# If options[:autocreate] is set to true, this method will autogenerate thumbnail
|
119
|
+
#
|
120
|
+
def full_filename(thumbnail = nil)
|
121
|
+
return "" if filename.blank?
|
122
|
+
attachment_options[:autocreate] ? full_filename_with_creation(thumbnail) : full_filename_without_creation(thumbnail)
|
123
|
+
end
|
124
|
+
|
125
|
+
def full_filename_by_path(path) #:nodoc:
|
126
|
+
return if filename.blank?
|
127
|
+
thumbnail = path.gsub(%r((^#{Regexp.escape(attachment_basename)}_)(\w+)(#{Regexp.escape(attachment_extname)})$), '\2')
|
128
|
+
return unless thumbnail
|
129
|
+
return unless attachment_options[:thumbnails][thumbnail.to_sym]
|
130
|
+
full_filename_with_creation(thumbnail.to_sym)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Gets the public path to the file, visible to browser
|
134
|
+
# The optional thumbnail argument will output the thumbnail's filename.
|
135
|
+
# If options[:autocreate] is set to true, this method will autogenerate thumbnail
|
136
|
+
def public_filename(thumbnail = nil)
|
137
|
+
return "" if filename.blank?
|
138
|
+
full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
# overrwrite this to do your own app-specific partitioning.
|
144
|
+
# you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php
|
145
|
+
def partitioned_path(*args)
|
146
|
+
("%08d" % id).scan(/..../) + args
|
147
|
+
end
|
148
|
+
|
149
|
+
def create_thumbnail_if_required(thumbnail)
|
150
|
+
thumbnail_path = full_filename_without_creation(thumbnail)
|
151
|
+
return thumbnail_path unless thumbnail
|
152
|
+
return thumbnail_path if File.exists?(thumbnail_path)
|
153
|
+
return unless /image\//.match(content_type)
|
154
|
+
if attachment_options[:croppable_thumbnails].include?(thumbnail.to_sym)
|
155
|
+
crop_and_thumbnail(thumbnail, thumbnail_path)
|
156
|
+
else
|
157
|
+
create_thumbnail(thumbnail, thumbnail_path)
|
158
|
+
end
|
159
|
+
thumbnail_path
|
160
|
+
end
|
161
|
+
|
162
|
+
def create_thumbnail(thumbnail, thumbnail_path)
|
163
|
+
return nil unless File.exists?(full_filename)
|
164
|
+
return nil unless attachment_options[:thumbnails][thumbnail.to_sym]
|
165
|
+
`convert -thumbnail #{attachment_options[:thumbnails][thumbnail.to_sym]} "#{full_filename}" "#{thumbnail_path}"`
|
166
|
+
thumbnail_path
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
public
|
171
|
+
|
172
|
+
def valid_filetype? #:nodoc:
|
173
|
+
errors.add("uploaded_data", attachment_options[:validation_message]) if @save_new_attachment && !@valid_filetype
|
174
|
+
end
|
175
|
+
|
176
|
+
# Main method, that accepts uploaded data
|
177
|
+
#
|
178
|
+
def uploaded_data=(file_data)
|
179
|
+
prepare_uploaded_file(file_data)
|
180
|
+
file_type = identify_uploaded_file_type
|
181
|
+
if accepts_file_type_for_upload?(file_type)
|
182
|
+
handle_uploaded_file
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def image_size
|
187
|
+
[width.to_s, height.to_s] * 'x'
|
188
|
+
end
|
189
|
+
|
190
|
+
def filename=(value)
|
191
|
+
@old_filename = full_filename unless filename.nil? || @old_filename
|
192
|
+
write_attribute :filename, sanitize_filename(value)
|
193
|
+
end
|
194
|
+
|
195
|
+
protected
|
196
|
+
|
197
|
+
|
198
|
+
def crop_and_thumbnail(thumbnail, thumbnail_path)
|
199
|
+
if !respond_to?(:width) || !respond_to?(:height)
|
200
|
+
file_type, width, height = identify_image_properties(full_filename)
|
201
|
+
end
|
202
|
+
album_x, album_y = attachment_options[:thumbnails][thumbnail.to_sym].split("x").map &:to_i
|
203
|
+
scale_x = width.to_f / album_x
|
204
|
+
scale_y = height.to_f / album_y
|
205
|
+
if scale_x > scale_y
|
206
|
+
x, y = (album_x*scale_y).floor, height
|
207
|
+
shift_x, shift_y = (width.to_i - x)/2, 0
|
208
|
+
else
|
209
|
+
x, y = width, (album_y*scale_x).floor
|
210
|
+
shift_x, shift_y = 0, (height.to_i - y)/2
|
211
|
+
end
|
212
|
+
# FileUtils.cp(full_filename_without_creation, thumbnail_path)
|
213
|
+
`convert -crop #{x}x#{y}+#{shift_x}+#{shift_y} "#{full_filename}" "#{thumbnail_path}"`
|
214
|
+
`mogrify -geometry #{album_x}x#{album_y} "#{thumbnail_path}"`
|
215
|
+
thumbnail_path
|
216
|
+
end
|
217
|
+
|
218
|
+
# Destroys the file. Called in the after_destroy callback
|
219
|
+
def remove_files
|
220
|
+
return unless filename
|
221
|
+
FileUtils.rm_rf(File.dirname(full_filename_without_creation))
|
222
|
+
rescue
|
223
|
+
logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}"
|
224
|
+
logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n")
|
225
|
+
end
|
226
|
+
|
227
|
+
# Renames the given file before saving
|
228
|
+
def rename_file
|
229
|
+
return unless @old_filename && @old_filename != full_filename
|
230
|
+
if @save_new_attachment && File.exists?(@old_filename)
|
231
|
+
FileUtils.rm @old_filename
|
232
|
+
elsif File.exists?(@old_filename)
|
233
|
+
FileUtils.mv @old_filename, full_filename
|
234
|
+
end
|
235
|
+
@old_filename = nil
|
236
|
+
true
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Attacheable
|
2
|
+
module FileNaming
|
3
|
+
def full_filename_with_creation(thumbnail = nil) #:nodoc:
|
4
|
+
create_thumbnail_if_required(thumbnail)
|
5
|
+
end
|
6
|
+
|
7
|
+
def full_filename_without_creation(thumbnail = nil) #:nodoc:
|
8
|
+
file_system_path = attachment_options[:path_prefix]
|
9
|
+
File.join(Attacheable.root, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail)))
|
10
|
+
end
|
11
|
+
|
12
|
+
def thumbnail_name_for(thumbnail = nil) #:nodoc:
|
13
|
+
return filename if thumbnail.blank?
|
14
|
+
ext = nil
|
15
|
+
basename = filename.gsub /\.\w+$/ do |s|
|
16
|
+
ext = s; ''
|
17
|
+
end
|
18
|
+
"#{basename}_#{thumbnail}#{ext}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def public_filename_without_creation(thumbnail = nil)
|
22
|
+
full_filename_without_creation(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), ''
|
23
|
+
end
|
24
|
+
|
25
|
+
def base_path #:nodoc:
|
26
|
+
@base_path ||= File.join(Attacheable.root, 'public')
|
27
|
+
end
|
28
|
+
|
29
|
+
def attachment_basename
|
30
|
+
filename && filename.gsub(/\.[^\.]+$/, '')
|
31
|
+
end
|
32
|
+
|
33
|
+
def attachment_extname
|
34
|
+
filename && filename.gsub(/^(.*)(\.[^\.]+)$/, '\2')
|
35
|
+
end
|
36
|
+
|
37
|
+
def sanitize_filename(filename)
|
38
|
+
returning filename.strip do |name|
|
39
|
+
# NOTE: File.basename doesn't work right with Windows paths on Unix
|
40
|
+
# get only the filename, not the whole path
|
41
|
+
name.gsub! /^.*(\\|\/)/, ''
|
42
|
+
|
43
|
+
# Finally, replace all non alphanumeric, underscore or periods with underscore
|
44
|
+
name.gsub! /[^\w\.\-]/, '_'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Attacheable
|
2
|
+
class PhotoHandler < Mongrel::HttpHandler
|
3
|
+
def initialize(prefix, options)
|
4
|
+
@prefix = prefix
|
5
|
+
@class_name = options[:class_name]
|
6
|
+
end
|
7
|
+
|
8
|
+
def logger
|
9
|
+
ActiveRecord::Base.logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def klass
|
13
|
+
@class_name.constantize
|
14
|
+
end
|
15
|
+
|
16
|
+
def process(request, response)
|
17
|
+
if File.exists?(Attacheable.root+"/public#{@prefix}/#{request.params["PATH_INFO"]}")
|
18
|
+
response.start(200) do |headers, out|
|
19
|
+
headers["Content-Type"] = "image/jpeg"
|
20
|
+
out.write(File.read(Attacheable.root+"/public#{@prefix}/#{request.params["PATH_INFO"]}"))
|
21
|
+
end
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
start_time = Time.now
|
26
|
+
photo, data = klass.data_by_path_info(request.params["PATH_INFO"].split("/"))
|
27
|
+
if photo
|
28
|
+
response.start(200) do |headers, out|
|
29
|
+
headers["Content-Type"] = photo.content_type
|
30
|
+
out.write(data)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
response.start(404) do |headers, out|
|
34
|
+
headers["Content-Type"] = "text/plain"
|
35
|
+
out.write("No such image\n")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
logger.info "Processed request in #{Time.now - start_time} seconds\n URI: #{request.params["REQUEST_URI"]}\n\n"
|
39
|
+
rescue Exception => e
|
40
|
+
logger.info "!! Internal server error\nURI: #{request.params["REQUEST_URI"]}\n#{e}\n#{e.backtrace.join("\n")}"
|
41
|
+
logger.flush
|
42
|
+
response.start(200) do |headers, out|
|
43
|
+
headers["Content-Type"] = "text/plain"
|
44
|
+
out.write("Some error on server\n")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Attacheable
|
2
|
+
module Uploading
|
3
|
+
def prepare_uploaded_file(file_data)
|
4
|
+
return prepare_merb_uploaded_file(file_data) if file_data.is_a?(Hash) && file_data["content_type"] && file_data["tempfile"]
|
5
|
+
return nil if file_data.nil? || !file_data.respond_to?(:original_filename) || !respond_to?(:filename=)
|
6
|
+
|
7
|
+
self.filename = file_data.original_filename
|
8
|
+
if file_data.is_a?(StringIO)
|
9
|
+
file_data.rewind
|
10
|
+
@tempfile = Tempfile.new(filename)
|
11
|
+
@tempfile.write(file_data.read)
|
12
|
+
@tempfile.close
|
13
|
+
else
|
14
|
+
@tempfile = file_data
|
15
|
+
end
|
16
|
+
if respond_to?(:size=)
|
17
|
+
self.size = file_data.respond_to?(:size) ? file_data.size : @tempfile.respond_to?(:size) ? @tempfile.size : File.size(@tempfile.path)
|
18
|
+
end
|
19
|
+
@save_new_attachment = true
|
20
|
+
@valid_filetype = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepare_merb_uploaded_file(file_data)
|
24
|
+
return nil if file_data["tempfile"].blank? || file_data["filename"].blank? || file_data["content_type"].blank?
|
25
|
+
self.filename = file_data["filename"]
|
26
|
+
self.size = file_data["size"] if respond_to?(:size=)
|
27
|
+
@tempfile = file_data["tempfile"]
|
28
|
+
@save_new_attachment = true
|
29
|
+
@valid_filetype = false
|
30
|
+
end
|
31
|
+
|
32
|
+
def identify_uploaded_file_type
|
33
|
+
return unless @tempfile
|
34
|
+
self.content_type = @tempfile.content_type if @tempfile.respond_to?(:content_type)
|
35
|
+
|
36
|
+
if content_type.blank? || content_type =~ /image\//
|
37
|
+
file_type, width, height = identify_image_properties(@tempfile.path)
|
38
|
+
if file_type
|
39
|
+
self.width = width if(respond_to?(:width=))
|
40
|
+
self.height = height if(respond_to?(:height=))
|
41
|
+
self.content_type = "image/#{file_type}"
|
42
|
+
file_type
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def identify_image_properties(path)
|
48
|
+
return [nil,nil,nil] if path.blank?
|
49
|
+
|
50
|
+
output = nil
|
51
|
+
silence_stderr do
|
52
|
+
output = `identify "#{path}"`
|
53
|
+
end
|
54
|
+
if output && match_data = / (\w+) (\d+)x(\d+) /.match(output)
|
55
|
+
file_type = match_data[1].to_s.downcase
|
56
|
+
width = match_data[2]
|
57
|
+
height = match_data[3]
|
58
|
+
return [file_type, width, height]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def accepts_file_type_for_upload?(file_type)
|
63
|
+
return false unless @tempfile
|
64
|
+
return true if attachment_options[:valid_filetypes] == :all
|
65
|
+
return true if attachment_options[:valid_filetypes].include?(file_type)
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle_uploaded_file
|
69
|
+
@save_new_attachment = true
|
70
|
+
@valid_filetype = true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Saves the file to the file system
|
74
|
+
def save_to_storage
|
75
|
+
if @save_new_attachment
|
76
|
+
FileUtils.mkdir_p(File.dirname(full_filename))
|
77
|
+
FileUtils.cp(@tempfile.path, full_filename)
|
78
|
+
File.chmod(0644, full_filename)
|
79
|
+
save_to_replicas
|
80
|
+
end
|
81
|
+
@save_new_attachment = false
|
82
|
+
@tempfile = nil
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
def save_to_replicas
|
87
|
+
attachment_options[:replicas].each do |replica|
|
88
|
+
system("ssh #{replica[:user]}@#{replica[:host]} mkdir -p \"#{File.dirname(full_filename)}\"")
|
89
|
+
system("scp \"#{full_filename}\" \"#{replica[:user]}@#{replica[:host]}:#{full_filename}\"")
|
90
|
+
end if attachment_options[:replicas]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
Binary file
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require File.dirname(__FILE__)+'/test_helper'
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
4
|
+
|
5
|
+
def setup_db
|
6
|
+
ActiveRecord::Migration.verbose = false
|
7
|
+
ActiveRecord::Schema.define(:version => 1) do
|
8
|
+
create_table :images do |t|
|
9
|
+
t.string :something
|
10
|
+
t.string :filename
|
11
|
+
t.string :content_type
|
12
|
+
#t.integer :width
|
13
|
+
#t.integer :height
|
14
|
+
t.string :type
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown_db
|
20
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
21
|
+
ActiveRecord::Base.connection.drop_table(table)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Image < ActiveRecord::Base
|
26
|
+
has_attachment :thumbnails => {:medium => "120x", :large => "800x600", :preview => "100x100"},
|
27
|
+
:croppable_thumbnails => %w(preview)
|
28
|
+
validates_as_attachment :message => "Please upload an image file."
|
29
|
+
end
|
30
|
+
|
31
|
+
class Photo < Image
|
32
|
+
end
|
33
|
+
|
34
|
+
module TestUploadExtension
|
35
|
+
attr_accessor :content_type
|
36
|
+
def original_filename
|
37
|
+
File.basename(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def size
|
41
|
+
File.stat(path).size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class AttacheableTest < Test::Unit::TestCase
|
46
|
+
def setup
|
47
|
+
setup_db
|
48
|
+
end
|
49
|
+
|
50
|
+
def teardown
|
51
|
+
teardown_db
|
52
|
+
FileUtils.rm_rf(File.dirname(__FILE__)+"/public")
|
53
|
+
Image.attachment_options[:autocreate] = false
|
54
|
+
Image.attachment_options[:valid_filetypes] = %w(jpeg gif png psd)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_image_autoconf
|
58
|
+
assert_equal "public/system/images", Image.attachment_options[:path_prefix], "Should guess path prefix right"
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_image_creation
|
62
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/life.jpg")
|
63
|
+
input.extend(TestUploadExtension)
|
64
|
+
assert_equal "life.jpg", input.original_filename, "should look like uploaded file"
|
65
|
+
image = Image.new(:uploaded_data => input)
|
66
|
+
assert_equal "life_medium.jpg", image.send(:thumbnail_name_for, :medium), "should generate right thumbnail filename"
|
67
|
+
assert image.save, "Image should be saved"
|
68
|
+
assert_equal "life", image.attachment_basename
|
69
|
+
assert_equal ".jpg", image.attachment_extname
|
70
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life.jpg"), "File should be saved"
|
71
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should not be generated"
|
72
|
+
image.destroy
|
73
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_merb_image_creation
|
77
|
+
path = File.dirname(__FILE__)+"/fixtures/life.jpg"
|
78
|
+
input = {"content_type"=>"image/jpeg", "size"=>File.size(path), "tempfile"=>File.open(path), "filename"=>"life.jpg"}
|
79
|
+
image = Image.new(:uploaded_data => input)
|
80
|
+
assert image.save, "Image should be saved"
|
81
|
+
assert_equal "life", image.attachment_basename
|
82
|
+
assert_equal ".jpg", image.attachment_extname
|
83
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life.jpg"), "File should be saved"
|
84
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should not be generated"
|
85
|
+
image.destroy
|
86
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_image_with_autocreation
|
90
|
+
Image.attachment_options[:autocreate] = true
|
91
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/life.jpg")
|
92
|
+
input.extend(TestUploadExtension)
|
93
|
+
image = Image.new(:uploaded_data => input)
|
94
|
+
assert image.save, "Image should be saved"
|
95
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life.jpg"), "File should be saved"
|
96
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should not be generated"
|
97
|
+
assert_equal "life_medium.jpg", image.send(:thumbnail_name_for, :medium), "should generate right thumbnail filename"
|
98
|
+
image.public_filename(:medium)
|
99
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should be generated on demand"
|
100
|
+
image.destroy
|
101
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_autocrop_image
|
105
|
+
Image.attachment_options[:autocreate] = true
|
106
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/life.jpg")
|
107
|
+
input.extend(TestUploadExtension)
|
108
|
+
image = Image.new(:uploaded_data => input)
|
109
|
+
assert image.save, "Image should be saved"
|
110
|
+
image.public_filename(:preview)
|
111
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_preview.jpg"), "Thumbnails should be generated on demand"
|
112
|
+
identify = `identify "#{File.dirname(__FILE__)+"/public/system/images/0000/0001/life_preview.jpg"}"`
|
113
|
+
assert Regexp.new(" JPEG 100x100 ").match(identify), "Image should be cropped to format"
|
114
|
+
image.destroy
|
115
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_image_with_wrong_type
|
119
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/wrong_type")
|
120
|
+
input.extend(TestUploadExtension)
|
121
|
+
image = Image.new(:uploaded_data => input)
|
122
|
+
assert !image.save, "Image should not be saved"
|
123
|
+
assert image.errors.on(:uploaded_data).size > 0, "Uploaded data is of wrong type"
|
124
|
+
assert_equal "Please upload an image file.", image.errors.on(:uploaded_data), "Validation message is the one used with :message"
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_with_sti
|
128
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/life.jpg")
|
129
|
+
input.extend(TestUploadExtension)
|
130
|
+
image = Photo.new(:uploaded_data => input)
|
131
|
+
assert image.save, "Image should be saved"
|
132
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life.jpg"), "File should be uploaded as for Image"
|
133
|
+
image.destroy
|
134
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_save_raw_binary
|
138
|
+
Image.attachment_options[:valid_filetypes] = :all
|
139
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/wrong_type")
|
140
|
+
input.extend(TestUploadExtension)
|
141
|
+
image = Image.new(:uploaded_data => input)
|
142
|
+
assert image.save, "Image should be saved"
|
143
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/wrong_type"), "File should be uploaded"
|
144
|
+
assert !image.send(:full_filename_with_creation, :preview)
|
145
|
+
image.destroy
|
146
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_save_msword
|
150
|
+
Image.attachment_options[:valid_filetypes] = :all
|
151
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/test.doc")
|
152
|
+
input.extend(TestUploadExtension)
|
153
|
+
input.content_type = "application/ms-word"
|
154
|
+
image = Image.new(:uploaded_data => input)
|
155
|
+
assert image.save, "Image should be saved"
|
156
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/test.doc"), "File should be uploaded"
|
157
|
+
assert !image.send(:full_filename_with_creation, :preview)
|
158
|
+
assert_equal "/system/images/0000/0001/test.doc", image.public_filename
|
159
|
+
image.destroy
|
160
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_create_thumbnail
|
164
|
+
input = File.open(File.dirname(__FILE__)+"/fixtures/life.jpg")
|
165
|
+
input.extend(TestUploadExtension)
|
166
|
+
assert_equal "life.jpg", input.original_filename, "should look like uploaded file"
|
167
|
+
image = Image.new(:uploaded_data => input)
|
168
|
+
assert_equal "life_medium.jpg", image.send(:thumbnail_name_for, :medium), "should generate right thumbnail filename"
|
169
|
+
assert image.save, "Image should be saved"
|
170
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life.jpg"), "File should be saved"
|
171
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should not be autogenerated"
|
172
|
+
photo, data = Image.data_by_path_info(%w(0000 0001 life_medium.jpg))
|
173
|
+
assert photo, "Image should be guessed"
|
174
|
+
assert File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001/life_medium.jpg"), "Thumbnails should be generated on demand"
|
175
|
+
assert data, "New file should be generated"
|
176
|
+
image.destroy
|
177
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000/0001"), "Directory should be cleaned"
|
178
|
+
end
|
179
|
+
|
180
|
+
def test_nil_upload
|
181
|
+
image = Image.new(:uploaded_data => nil)
|
182
|
+
assert image.save, "Image should be saved with empty file"
|
183
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000"), "nothing should be created"
|
184
|
+
assert image.update_attributes(:something => "empty"), "should be updateable"
|
185
|
+
assert image.destroy, "should be destroyable"
|
186
|
+
end
|
187
|
+
|
188
|
+
def test_string_upload
|
189
|
+
image = Image.new(:uploaded_data => "life.jpg")
|
190
|
+
assert image.save, "Image should be saved with empty file"
|
191
|
+
assert !File.exists?(File.dirname(__FILE__)+"/public/system/images/0000"), "nothing should be created"
|
192
|
+
assert image.destroy, "should be destroyable"
|
193
|
+
end
|
194
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_record'
|
5
|
+
require 'action_pack'
|
6
|
+
$KCODE = 'u'
|
7
|
+
|
8
|
+
$:.unshift File.join(File.dirname(__FILE__), '../lib')
|
9
|
+
if defined?(RAILS_ROOT)
|
10
|
+
RAILS_ROOT.replace(File.dirname(__FILE__))
|
11
|
+
else
|
12
|
+
RAILS_ROOT = File.dirname(__FILE__)
|
13
|
+
end
|
14
|
+
require 'attacheable'
|
15
|
+
|
16
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attacheable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "1.
|
4
|
+
version: "1.2"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Lapshin
|
@@ -24,12 +24,24 @@ extra_rdoc_files: []
|
|
24
24
|
files:
|
25
25
|
- init.rb
|
26
26
|
- lib
|
27
|
+
- lib/attacheable
|
28
|
+
- lib/attacheable/file_naming.rb
|
29
|
+
- lib/attacheable/photo_handler.rb
|
30
|
+
- lib/attacheable/uploading.rb
|
31
|
+
- lib/attacheable.rb
|
27
32
|
- MIT-LICENSE
|
28
33
|
- pkg
|
34
|
+
- pkg/attacheable-1.1.gem
|
29
35
|
- Rakefile
|
30
36
|
- README
|
31
37
|
- README.ru
|
32
38
|
- test
|
39
|
+
- test/attacheable_test.rb
|
40
|
+
- test/fixtures
|
41
|
+
- test/fixtures/life.jpg
|
42
|
+
- test/fixtures/test.doc
|
43
|
+
- test/fixtures/wrong_type
|
44
|
+
- test/test_helper.rb
|
33
45
|
has_rdoc: false
|
34
46
|
homepage:
|
35
47
|
post_install_message:
|