uploadcolumn 0.3.0
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/.gitignore +3 -0
- data/CHANGELOG +123 -0
- data/LICENSE +22 -0
- data/README.rdoc +206 -0
- data/Rakefile +90 -0
- data/VERSION +1 -0
- data/init.rb +15 -0
- data/lib/upload_column/active_record_extension.rb +154 -0
- data/lib/upload_column/configuration.rb +49 -0
- data/lib/upload_column/magic_columns.rb +50 -0
- data/lib/upload_column/manipulators/image_science.rb +86 -0
- data/lib/upload_column/manipulators/rmagick.rb +75 -0
- data/lib/upload_column/rails/action_controller_extension.rb +61 -0
- data/lib/upload_column/rails/asset_tag_extension.rb +17 -0
- data/lib/upload_column/rails/upload_column_helper.rb +45 -0
- data/lib/upload_column/sanitized_file.rb +176 -0
- data/lib/upload_column/uploaded_file.rb +299 -0
- data/lib/upload_column.rb +12 -0
- data/spec/active_record_extension_spec.rb +514 -0
- data/spec/custom_matchers.rb +148 -0
- data/spec/fixtures/animated.gif +0 -0
- data/spec/fixtures/animated_solarized.gif +0 -0
- data/spec/fixtures/invalid-image.jpg +1 -0
- data/spec/fixtures/kerb.jpg +0 -0
- data/spec/fixtures/kerb_solarized.jpg +0 -0
- data/spec/fixtures/netscape.gif +0 -0
- data/spec/fixtures/skanthak.png +0 -0
- data/spec/image_science_manipulator_spec.rb +195 -0
- data/spec/integration_spec.rb +668 -0
- data/spec/magic_columns_spec.rb +120 -0
- data/spec/rmagick_manipulator_spec.rb +186 -0
- data/spec/sanitized_file_spec.rb +496 -0
- data/spec/spec_helper.rb +90 -0
- data/spec/upload_column_spec.rb +65 -0
- data/spec/uploaded_file_spec.rb +1053 -0
- metadata +108 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
module UploadColumn
|
2
|
+
|
3
|
+
class UploadError < StandardError #:nodoc:
|
4
|
+
end
|
5
|
+
class IntegrityError < UploadError #:nodoc:
|
6
|
+
end
|
7
|
+
class TemporaryPathMalformedError < UploadError #:nodoc:
|
8
|
+
end
|
9
|
+
class UploadNotMultipartError < UploadError #:nodoc:
|
10
|
+
end
|
11
|
+
|
12
|
+
TempValueRegexp = %r{^((?:\d+\.)+\d+)/([^/;]+)(?:;([^/;]+))?$}
|
13
|
+
|
14
|
+
|
15
|
+
# When you call an upload_column field, an instance of this class will be returned.
|
16
|
+
#
|
17
|
+
# Suppose a +User+ model has a +picture+ upload_column, like so:
|
18
|
+
# class User < ActiveRecord::Base
|
19
|
+
# upload_column :picture
|
20
|
+
# end
|
21
|
+
# Now in our controller we did:
|
22
|
+
# @user = User.find(params[:id])
|
23
|
+
# We could then access the file:
|
24
|
+
# @user.picture.url
|
25
|
+
# Which would output the url to the file (assuming it is stored in /public/)
|
26
|
+
# = Versions
|
27
|
+
# If we had instead added different versions in our model
|
28
|
+
# upload_column :picture, :versions => [:thumb, :large]
|
29
|
+
# Then we could access them like so:
|
30
|
+
# @user.picture.thumb.url
|
31
|
+
# See the +README+ for more detaills.
|
32
|
+
class UploadedFile < SanitizedFile
|
33
|
+
|
34
|
+
attr_reader :instance, :attribute, :options, :versions
|
35
|
+
attr_accessor :suffix
|
36
|
+
|
37
|
+
class << self
|
38
|
+
|
39
|
+
# upload a file. In most cases you want to pass the ActiveRecord instance and the attribute
|
40
|
+
# name as well as the file. For a more bare-bones approach, check out SanitizedFile.
|
41
|
+
def upload(file, instance = nil, attribute = nil, options = {}) #:nodoc:
|
42
|
+
uf = self.new(:upload, file, instance, attribute, options)
|
43
|
+
return uf.empty? ? nil : uf
|
44
|
+
end
|
45
|
+
|
46
|
+
# Retrieve a file from the filesystem, based on the calculated store_dir and the filename
|
47
|
+
# stored in the database.
|
48
|
+
def retrieve(filename, instance = nil, attribute = nil, options = {}) #:nodoc:
|
49
|
+
self.new(:retrieve, filename, instance, attribute, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Retreieve a file that was stored as a temp file
|
53
|
+
def retrieve_temp(path, instance = nil, attribute = nil, options = {}) #:nodoc:
|
54
|
+
self.new(:retrieve_temp, path, instance, attribute, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(mode, file, instance, attribute, options={})
|
60
|
+
# TODO: the options are always reverse merged in here, in case UploadedFile has
|
61
|
+
# been initialized outside UploadColumn proper, this is not a very elegant solution, imho.
|
62
|
+
@options = options.reverse_merge(UploadColumn.configuration)
|
63
|
+
@instance = instance
|
64
|
+
@attribute = attribute
|
65
|
+
@suffix = options[:suffix]
|
66
|
+
|
67
|
+
load_manipulator
|
68
|
+
|
69
|
+
case mode
|
70
|
+
when :upload
|
71
|
+
if file and file.is_a?(String) and not file.empty?
|
72
|
+
raise UploadNotMultipartError.new("Do not know how to handle a string with value '#{file}' that was uploaded. Check if the form's encoding has been set to 'multipart/form-data'.")
|
73
|
+
end
|
74
|
+
|
75
|
+
super(file, @options)
|
76
|
+
|
77
|
+
unless empty?
|
78
|
+
if options[:validate_integrity]
|
79
|
+
raise UploadError.new("No list of valid extensions supplied.") unless options[:extensions]
|
80
|
+
raise IntegrityError.new("has an extension that is not allowed.") unless options[:extensions].include?(extension)
|
81
|
+
end
|
82
|
+
|
83
|
+
@temp_name = generate_tmpname
|
84
|
+
@new_file = true
|
85
|
+
|
86
|
+
move_to_directory(File.join(tmp_dir, @temp_name))
|
87
|
+
|
88
|
+
# The original is processed before versions are initialized.
|
89
|
+
self.process!(@options[:process]) if @options[:process] and self.respond_to?(:process!)
|
90
|
+
|
91
|
+
initialize_versions do |version|
|
92
|
+
copy_to_version(version)
|
93
|
+
end
|
94
|
+
|
95
|
+
apply_manipulations_to_versions
|
96
|
+
|
97
|
+
# trigger the _after_upload callback
|
98
|
+
self.instance.send("#{self.attribute}_after_upload", self) if self.instance.respond_to?("#{self.attribute}_after_upload")
|
99
|
+
end
|
100
|
+
when :retrieve
|
101
|
+
@path = File.join(store_dir, file)
|
102
|
+
@basename, @extension = split_extension(file)
|
103
|
+
initialize_versions
|
104
|
+
when :retrieve_temp
|
105
|
+
if file and not file.empty?
|
106
|
+
@temp_name, name, original_filename = file.scan( ::UploadColumn::TempValueRegexp ).first
|
107
|
+
|
108
|
+
if @temp_name and name
|
109
|
+
@path = File.join(tmp_dir, @temp_name, name)
|
110
|
+
@basename, @extension = split_extension(name)
|
111
|
+
@original_filename = original_filename
|
112
|
+
initialize_versions
|
113
|
+
else
|
114
|
+
raise TemporaryPathMalformedError.new("#{file} is not a valid temporary path!")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
else
|
118
|
+
super(file, @options)
|
119
|
+
initialize_versions
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the directory where tmp files are stored for this UploadedFile, relative to :root_dir
|
124
|
+
def relative_tmp_dir
|
125
|
+
parse_dir_options(:tmp_dir)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns the directory where tmp files are stored for this UploadedFile
|
129
|
+
def tmp_dir
|
130
|
+
File.expand_path(self.relative_tmp_dir, @options[:root_dir])
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns the directory where files are stored for this UploadedFile, relative to :root_dir
|
134
|
+
def relative_store_dir
|
135
|
+
parse_dir_options(:store_dir)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the directory where files are stored for this UploadedFile
|
139
|
+
def store_dir
|
140
|
+
File.expand_path(self.relative_store_dir, @options[:root_dir])
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the path of the file relative to :root_dir
|
144
|
+
def relative_path
|
145
|
+
self.path.sub(File.expand_path(options[:root_dir]) + '/', '')
|
146
|
+
end
|
147
|
+
|
148
|
+
# returns the full path of the file.
|
149
|
+
def path; super; end
|
150
|
+
|
151
|
+
# returns the directory where the file is currently stored.
|
152
|
+
def dir
|
153
|
+
File.dirname(self.path)
|
154
|
+
end
|
155
|
+
|
156
|
+
# return true if the file has just been uploaded.
|
157
|
+
def new_file?
|
158
|
+
@new_file
|
159
|
+
end
|
160
|
+
|
161
|
+
# returns the url of the file, by merging the relative path with the web_root option.
|
162
|
+
def public_path
|
163
|
+
# TODO: this might present an attack vector if the file is outside the web_root
|
164
|
+
options[:web_root].to_s + '/' + self.relative_path.gsub("\\", "/")
|
165
|
+
end
|
166
|
+
|
167
|
+
alias_method :to_s, :public_path
|
168
|
+
alias_method :url, :public_path
|
169
|
+
|
170
|
+
# this is the value returned when avatar_temp is called, where avatar is an upload_column
|
171
|
+
def temp_value #:nodoc:
|
172
|
+
if tempfile?
|
173
|
+
if original_filename
|
174
|
+
%(#{@temp_name}/#{filename};#{original_filename})
|
175
|
+
else
|
176
|
+
%(#{@temp_name}/#{filename})
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def inspect #:nodoc:
|
182
|
+
"<UploadedFile: #{self.path}>"
|
183
|
+
end
|
184
|
+
|
185
|
+
def tempfile?
|
186
|
+
@temp_name
|
187
|
+
end
|
188
|
+
|
189
|
+
alias_method :actual_filename, :filename
|
190
|
+
|
191
|
+
def filename
|
192
|
+
unless bn = parse_dir_options(:filename)
|
193
|
+
bn = [self.basename, self.suffix].compact.join('-')
|
194
|
+
bn += ".#{self.extension}" unless self.extension.blank?
|
195
|
+
end
|
196
|
+
return bn
|
197
|
+
end
|
198
|
+
|
199
|
+
# TODO: this is a public method, should be specced
|
200
|
+
def move_to_directory(dir)
|
201
|
+
p = File.join(dir, self.filename)
|
202
|
+
if copy_file(p)
|
203
|
+
@path = p
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
|
209
|
+
def copy_to_version(version)
|
210
|
+
copy = self.clone
|
211
|
+
copy.suffix = version
|
212
|
+
|
213
|
+
if copy_file(File.join(self.dir, copy.filename))
|
214
|
+
return copy
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def initialize_versions
|
219
|
+
if self.options[:versions]
|
220
|
+
@versions = {}
|
221
|
+
|
222
|
+
version_keys = options[:versions].is_a?(Hash) ? options[:versions].keys : options[:versions]
|
223
|
+
|
224
|
+
version_keys.each do |version|
|
225
|
+
|
226
|
+
version = version.to_sym
|
227
|
+
|
228
|
+
# Raise an error if the version name is a method on this class
|
229
|
+
raise ArgumentError.new("#{version} is an illegal name for an UploadColumn version.") if self.respond_to?(version)
|
230
|
+
|
231
|
+
if block_given?
|
232
|
+
@versions[version] = yield(version)
|
233
|
+
else
|
234
|
+
# Copy the file and store it in the versions array
|
235
|
+
# TODO: this might result in the manipulator not being loaded.
|
236
|
+
@versions[version] = self.clone #class.new(:open, File.join(self.dir, "#{self.basename}-#{version}.#{self.extension}"), instance, attribute, options.merge(:versions => nil, :suffix => version))
|
237
|
+
@versions[version].suffix = version
|
238
|
+
end
|
239
|
+
|
240
|
+
@versions[version].instance_eval { @path = File.join(self.dir, self.filename) } # ensure path is not cached
|
241
|
+
|
242
|
+
# Add the version methods to the instance
|
243
|
+
self.instance_eval <<-SRC
|
244
|
+
def #{version}
|
245
|
+
self.versions[:#{version}]
|
246
|
+
end
|
247
|
+
SRC
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def load_manipulator
|
253
|
+
if options[:manipulator]
|
254
|
+
self.extend(options[:manipulator])
|
255
|
+
self.load_manipulator_dependencies if self.respond_to?(:load_manipulator_dependencies)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def apply_manipulations_to_versions
|
260
|
+
@versions.each do |k, v|
|
261
|
+
v.process! @options[:versions][k]
|
262
|
+
end if @options[:versions].is_a?(Hash)
|
263
|
+
end
|
264
|
+
|
265
|
+
def save
|
266
|
+
self.move_to_directory(self.store_dir)
|
267
|
+
self.versions.each { |version, file| file.move_to_directory(self.store_dir) } if self.versions
|
268
|
+
@new_file = false
|
269
|
+
@temp_name = nil
|
270
|
+
true
|
271
|
+
end
|
272
|
+
|
273
|
+
def parse_dir_options(option)
|
274
|
+
if self.instance.respond_to?("#{self.attribute}_#{option}")
|
275
|
+
self.instance.send("#{self.attribute}_#{option}", self)
|
276
|
+
else
|
277
|
+
option = @options[option]
|
278
|
+
if option.is_a?(Proc)
|
279
|
+
case option.arity
|
280
|
+
when 2
|
281
|
+
option.call(self.instance, self)
|
282
|
+
when 1
|
283
|
+
option.call(self.instance)
|
284
|
+
else
|
285
|
+
option.call
|
286
|
+
end
|
287
|
+
else
|
288
|
+
option
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def generate_tmpname
|
294
|
+
now = Time.now
|
295
|
+
"#{now.to_i}.#{now.usec}.#{Process.pid}"
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'sanitized_file.rb')
|
2
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'uploaded_file.rb')
|
3
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'magic_columns.rb')
|
4
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'active_record_extension.rb')
|
5
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'manipulators', 'rmagick.rb')
|
6
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'manipulators', 'image_science.rb')
|
7
|
+
require File.join(File.dirname(__FILE__), 'upload_column', 'configuration.rb')
|
8
|
+
|
9
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), "..", "init")
|
10
|
+
|
11
|
+
ActiveRecord::Base.send(:include, UploadColumn::ActiveRecordExtension)
|
12
|
+
|