uploadcolumn 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+