jnicklas-carrierwave 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Generators +4 -0
- data/LICENSE +20 -0
- data/README.md +211 -0
- data/Rakefile +96 -0
- data/TODO +0 -0
- data/lib/carrierwave/mount.rb +93 -0
- data/lib/carrierwave/orm/activerecord.rb +20 -0
- data/lib/carrierwave/orm/datamapper.rb +20 -0
- data/lib/carrierwave/processing/image_science.rb +70 -0
- data/lib/carrierwave/processing/rmagick.rb +161 -0
- data/lib/carrierwave/sanitized_file.rb +231 -0
- data/lib/carrierwave/storage/abstract.rb +80 -0
- data/lib/carrierwave/storage/file.rb +40 -0
- data/lib/carrierwave/storage/s3.rb +83 -0
- data/lib/carrierwave/uploader.rb +420 -0
- data/lib/carrierwave.rb +63 -0
- data/lib/generators/templates/uploader.rbt +32 -0
- data/lib/generators/uploader_generator.rb +20 -0
- data/spec/fixtures/bork.txt +1 -0
- data/spec/fixtures/test.jpeg +1 -0
- data/spec/fixtures/test.jpg +1 -0
- data/spec/mount_spec.rb +180 -0
- data/spec/orm/activerecord_spec.rb +168 -0
- data/spec/orm/datamapper_spec.rb +133 -0
- data/spec/sanitized_file_spec.rb +618 -0
- data/spec/spec_helper.rb +122 -0
- data/spec/uploader_spec.rb +709 -0
- metadata +89 -0
@@ -0,0 +1,420 @@
|
|
1
|
+
module CarrierWave
|
2
|
+
class Uploader
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
##
|
7
|
+
# Returns a list of processor callbacks which have been declared for this uploader
|
8
|
+
#
|
9
|
+
# @return [String]
|
10
|
+
#
|
11
|
+
def processors
|
12
|
+
@processors ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Adds a processor callback which applies operations as a file is uploaded.
|
17
|
+
# The argument may be the name of any method of the uploader, expressed as a symbol,
|
18
|
+
# or a list of such methods, or a hash where the key is a method and the value is
|
19
|
+
# an array of arguments to call the method with
|
20
|
+
#
|
21
|
+
# @param [*Symbol, Hash{Symbol => Array[]}] args
|
22
|
+
# @example
|
23
|
+
# class MyUploader < CarrierWave::Uploader
|
24
|
+
# process :sepiatone, :vignette
|
25
|
+
# process :scale => [200, 200]
|
26
|
+
#
|
27
|
+
# def sepiatone
|
28
|
+
# ...
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def vignette
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def scale(height, width)
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
def process(*args)
|
41
|
+
args.each do |arg|
|
42
|
+
if arg.is_a?(Hash)
|
43
|
+
arg.each do |method, args|
|
44
|
+
processors.push([method, args])
|
45
|
+
end
|
46
|
+
else
|
47
|
+
processors.push([arg, []])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Sets the storage engine to be used when storing files with this uploader.
|
54
|
+
# Can be any class that implements a #store!(CarrierWave::SanitizedFile) and a #retrieve!
|
55
|
+
# method. See lib/carrierwave/storage/file.rb for an example. Storage engines should
|
56
|
+
# be added to CarrierWave.config[:storage_engines] so they can be referred
|
57
|
+
# to by a symbol, which should be more convenient
|
58
|
+
#
|
59
|
+
# If no argument is given, it will simply return the currently used storage engine.
|
60
|
+
#
|
61
|
+
# @param [Symbol, Class] storage The storage engine to use for this uploader
|
62
|
+
# @return [Class] the storage engine to be used with this uploader
|
63
|
+
# @example
|
64
|
+
# storage :file
|
65
|
+
# storage CarrierWave::Storage::File
|
66
|
+
# storage MyCustomStorageEngine
|
67
|
+
#
|
68
|
+
def storage(storage = nil)
|
69
|
+
if storage.is_a?(Symbol)
|
70
|
+
@storage = get_storage_by_symbol(storage)
|
71
|
+
@storage.setup!
|
72
|
+
elsif storage
|
73
|
+
@storage = storage
|
74
|
+
@storage.setup!
|
75
|
+
elsif @storage.nil?
|
76
|
+
# Get the storage from the superclass if there is one
|
77
|
+
@storage = superclass.storage rescue nil
|
78
|
+
end
|
79
|
+
if @storage.nil?
|
80
|
+
# If we were not able to find a store any other way, setup the default store
|
81
|
+
@storage ||= get_storage_by_symbol(CarrierWave.config[:storage])
|
82
|
+
@storage.setup!
|
83
|
+
end
|
84
|
+
return @storage
|
85
|
+
end
|
86
|
+
|
87
|
+
alias_method :storage=, :storage
|
88
|
+
|
89
|
+
attr_accessor :version_name
|
90
|
+
|
91
|
+
##
|
92
|
+
# Adds a new version to this uploader
|
93
|
+
#
|
94
|
+
# @param [#to_sym] name name of the version
|
95
|
+
# @param [Proc] &block a block to eval on this version of the uploader
|
96
|
+
#
|
97
|
+
def version(name, &block)
|
98
|
+
name = name.to_sym
|
99
|
+
klass = Class.new(self)
|
100
|
+
klass.version_name = name
|
101
|
+
klass.class_eval(&block) if block
|
102
|
+
versions[name] = klass
|
103
|
+
class_eval <<-RUBY
|
104
|
+
def #{name}
|
105
|
+
versions[:#{name}]
|
106
|
+
end
|
107
|
+
RUBY
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# @return [Hash{Symbol => Class}] a list of versions available for this uploader
|
112
|
+
#
|
113
|
+
def versions
|
114
|
+
@versions ||= {}
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Generates a unique cache id for use in the caching system
|
119
|
+
#
|
120
|
+
# @return [String] a cache if in the format YYYYMMDD-HHMM-PID-RND
|
121
|
+
#
|
122
|
+
def generate_cache_id
|
123
|
+
Time.now.strftime('%Y%m%d-%H%M') + '-' + Process.pid.to_s + '-' + ("%04d" % rand(9999))
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def get_storage_by_symbol(symbol)
|
129
|
+
CarrierWave.config[:storage_engines][symbol]
|
130
|
+
end
|
131
|
+
|
132
|
+
end # class << self
|
133
|
+
|
134
|
+
attr_reader :file, :model, :mounted_as
|
135
|
+
|
136
|
+
##
|
137
|
+
# If a model is given as the first parameter, it will stored in the uploader, and
|
138
|
+
# available throught +#model+. Likewise, mounted_as stores the name of the column
|
139
|
+
# where this instance of the uploader is mounted. These values can then be used inside
|
140
|
+
# your uploader.
|
141
|
+
#
|
142
|
+
# If you do not wish to mount your uploaders with the ORM extensions in -more then you
|
143
|
+
# can override this method inside your uploader.
|
144
|
+
#
|
145
|
+
# @param [Object] model Any kind of model object
|
146
|
+
# @param [Symbol] mounted_as The name of the column where this uploader is mounted
|
147
|
+
# @example
|
148
|
+
# class MyUploader < CarrierWave::Uploader
|
149
|
+
# def store_dir
|
150
|
+
# File.join('public', 'files', mounted_as, model.permalink)
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
#
|
154
|
+
def initialize(model=nil, mounted_as=nil)
|
155
|
+
@model = model
|
156
|
+
@mounted_as = mounted_as
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Apply all process callbacks added through CarrierWaveer.process
|
161
|
+
#
|
162
|
+
def process!
|
163
|
+
self.class.processors.each do |method, args|
|
164
|
+
self.send(method, *args)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
##
|
169
|
+
# @return [String] the path where the file is currently located.
|
170
|
+
#
|
171
|
+
def current_path
|
172
|
+
file.path if file.respond_to?(:path)
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Returns a hash mapping the name of each version of the uploader to an instance of it
|
177
|
+
#
|
178
|
+
# @return [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances
|
179
|
+
#
|
180
|
+
def versions
|
181
|
+
return @versions if @versions
|
182
|
+
@versions = {}
|
183
|
+
self.class.versions.each do |name, klass|
|
184
|
+
@versions[name] = klass.new(model, mounted_as)
|
185
|
+
end
|
186
|
+
@versions
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# @return [String] the location where this file is accessible via a url
|
191
|
+
#
|
192
|
+
def url
|
193
|
+
if file.respond_to?(:url) and not file.url.blank?
|
194
|
+
file.url
|
195
|
+
elsif current_path
|
196
|
+
File.expand_path(current_path).gsub(File.expand_path(CarrierWave.config[:public]), '')
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
alias_method :to_s, :url
|
201
|
+
|
202
|
+
##
|
203
|
+
# Returns a string that uniquely identifies the last stored file
|
204
|
+
#
|
205
|
+
# @return [String] uniquely identifies a file
|
206
|
+
#
|
207
|
+
def identifier
|
208
|
+
file.identifier if file.respond_to?(:identifier)
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Override this in your Uploader to change the filename.
|
213
|
+
#
|
214
|
+
# Be careful using record ids as filenames. If the filename is stored in the database
|
215
|
+
# the record id will be nil when the filename is set. Don't use record ids unless you
|
216
|
+
# understand this limitation.
|
217
|
+
#
|
218
|
+
# Do not use the version_name in the filename, as it will prevent versions from being
|
219
|
+
# loaded correctly.
|
220
|
+
#
|
221
|
+
# @return [String] a filename
|
222
|
+
#
|
223
|
+
def filename
|
224
|
+
@filename
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# @return [String] the name of this version of the uploader
|
229
|
+
#
|
230
|
+
def version_name
|
231
|
+
self.class.version_name
|
232
|
+
end
|
233
|
+
|
234
|
+
##
|
235
|
+
# @return [String] the directory relative to which we will upload
|
236
|
+
#
|
237
|
+
def root
|
238
|
+
CarrierWave.config[:root]
|
239
|
+
end
|
240
|
+
|
241
|
+
####################
|
242
|
+
## Cache
|
243
|
+
####################
|
244
|
+
|
245
|
+
##
|
246
|
+
# Override this in your Uploader to change the directory where files are cached.
|
247
|
+
#
|
248
|
+
# @return [String] a directory
|
249
|
+
#
|
250
|
+
def cache_dir
|
251
|
+
CarrierWave.config[:cache_dir]
|
252
|
+
end
|
253
|
+
|
254
|
+
##
|
255
|
+
# Returns a String which uniquely identifies the currently cached file for later retrieval
|
256
|
+
#
|
257
|
+
# @return [String] a cache name, in the format YYYYMMDD-HHMM-PID-RND/filename.txt
|
258
|
+
#
|
259
|
+
def cache_name
|
260
|
+
File.join(cache_id, [version_name, original_filename].compact.join('_')) if cache_id and original_filename
|
261
|
+
end
|
262
|
+
|
263
|
+
##
|
264
|
+
# Caches the given file unless a file has already been cached, stored or retrieved.
|
265
|
+
#
|
266
|
+
# @param [File, IOString, Tempfile] new_file any kind of file object
|
267
|
+
# @raise [CarrierWave::FormNotMultipart] if the assigned parameter is a string
|
268
|
+
#
|
269
|
+
def cache(new_file)
|
270
|
+
cache!(new_file) unless file
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# Caches the given file. Calls process! to trigger any process callbacks.
|
275
|
+
#
|
276
|
+
# @param [File, IOString, Tempfile] new_file any kind of file object
|
277
|
+
# @raise [CarrierWave::FormNotMultipart] if the assigned parameter is a string
|
278
|
+
#
|
279
|
+
def cache!(new_file)
|
280
|
+
self.cache_id = CarrierWave::Uploader.generate_cache_id unless cache_id
|
281
|
+
new_file = CarrierWave::SanitizedFile.new(new_file)
|
282
|
+
raise CarrierWave::FormNotMultipart, "check that your upload form is multipart encoded" if new_file.string?
|
283
|
+
|
284
|
+
@file = new_file
|
285
|
+
|
286
|
+
@filename = new_file.filename
|
287
|
+
self.original_filename = new_file.filename
|
288
|
+
|
289
|
+
@file = @file.copy_to(cache_path)
|
290
|
+
process!
|
291
|
+
|
292
|
+
versions.each do |name, v|
|
293
|
+
v.send(:cache_id=, cache_id)
|
294
|
+
v.cache!(new_file)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
##
|
299
|
+
# Retrieves the file with the given cache_name from the cache, unless a file has
|
300
|
+
# already been cached, stored or retrieved.
|
301
|
+
#
|
302
|
+
# @param [String] cache_name uniquely identifies a cache file
|
303
|
+
#
|
304
|
+
def retrieve_from_cache(cache_name)
|
305
|
+
retrieve_from_cache!(cache_name) unless file
|
306
|
+
rescue CarrierWave::InvalidParameter
|
307
|
+
end
|
308
|
+
|
309
|
+
##
|
310
|
+
# Retrieves the file with the given cache_name from the cache.
|
311
|
+
#
|
312
|
+
# @param [String] cache_name uniquely identifies a cache file
|
313
|
+
# @raise [CarrierWave::InvalidParameter] if the cache_name is incorrectly formatted.
|
314
|
+
#
|
315
|
+
def retrieve_from_cache!(cache_name)
|
316
|
+
self.cache_id, self.original_filename = cache_name.split('/', 2)
|
317
|
+
@filename = original_filename
|
318
|
+
@file = CarrierWave::SanitizedFile.new(cache_path)
|
319
|
+
versions.each { |name, v| v.retrieve_from_cache!(cache_name) }
|
320
|
+
end
|
321
|
+
|
322
|
+
####################
|
323
|
+
## STORE
|
324
|
+
####################
|
325
|
+
|
326
|
+
##
|
327
|
+
# Override this in your Uploader to change the directory where the file backend stores files.
|
328
|
+
#
|
329
|
+
# Other backends may or may not use this method, depending on their specific needs.
|
330
|
+
#
|
331
|
+
# @return [String] a directory
|
332
|
+
#
|
333
|
+
def store_dir
|
334
|
+
[CarrierWave.config[:store_dir], version_name].compact.join(File::Separator)
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Stores the file by passing it to this Uploader's storage engine, unless a file has
|
339
|
+
# already been cached, stored or retrieved.
|
340
|
+
#
|
341
|
+
# If CarrierWave.config[:use_cache] is true, it will first cache the file
|
342
|
+
# and apply any process callbacks before uploading it.
|
343
|
+
#
|
344
|
+
# @param [File, IOString, Tempfile] new_file any kind of file object
|
345
|
+
#
|
346
|
+
def store(new_file)
|
347
|
+
store!(new_file) unless file
|
348
|
+
end
|
349
|
+
|
350
|
+
##
|
351
|
+
# Stores the file by passing it to this Uploader's storage engine.
|
352
|
+
#
|
353
|
+
# If new_file is omitted, a previously cached file will be stored.
|
354
|
+
#
|
355
|
+
# If CarrierWave.config[:use_cache] is true, it will first cache the file
|
356
|
+
# and apply any process callbacks before uploading it.
|
357
|
+
#
|
358
|
+
# @param [File, IOString, Tempfile] new_file any kind of file object
|
359
|
+
#
|
360
|
+
def store!(new_file=nil)
|
361
|
+
if CarrierWave.config[:use_cache]
|
362
|
+
cache!(new_file) if new_file
|
363
|
+
@file = storage.store!(self, @file)
|
364
|
+
@cache_id = nil
|
365
|
+
else
|
366
|
+
new_file = CarrierWave::SanitizedFile.new(new_file)
|
367
|
+
|
368
|
+
@filename = new_file.filename
|
369
|
+
self.original_filename = filename
|
370
|
+
|
371
|
+
@file = storage.store!(self, new_file)
|
372
|
+
end
|
373
|
+
versions.each { |name, v| v.store!(new_file) }
|
374
|
+
end
|
375
|
+
|
376
|
+
##
|
377
|
+
# Retrieves the file from the storage, unless a file has
|
378
|
+
# already been cached, stored or retrieved.
|
379
|
+
#
|
380
|
+
# @param [String] filename uniquely identifies the file to retrieve
|
381
|
+
#
|
382
|
+
def retrieve_from_store(filename)
|
383
|
+
retrieve_from_store!(filename) unless file
|
384
|
+
rescue CarrierWave::InvalidParameter
|
385
|
+
end
|
386
|
+
|
387
|
+
##
|
388
|
+
# Retrieves the file from the storage.
|
389
|
+
#
|
390
|
+
# @param [String] identifier uniquely identifies the file to retrieve
|
391
|
+
#
|
392
|
+
def retrieve_from_store!(identifier)
|
393
|
+
@file = storage.retrieve!(self, identifier)
|
394
|
+
versions.each { |name, v| v.retrieve_from_store!(identifier) }
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
def cache_path
|
400
|
+
File.expand_path(File.join(cache_dir, cache_name), root)
|
401
|
+
end
|
402
|
+
|
403
|
+
def storage
|
404
|
+
self.class.storage
|
405
|
+
end
|
406
|
+
|
407
|
+
attr_reader :cache_id, :original_filename
|
408
|
+
|
409
|
+
def cache_id=(cache_id)
|
410
|
+
raise CarrierWave::InvalidParameter, "invalid cache id" unless cache_id =~ /^[\d]{8}\-[\d]{4}\-[\d]+\-[\d]{4}$/
|
411
|
+
@cache_id = cache_id
|
412
|
+
end
|
413
|
+
|
414
|
+
def original_filename=(filename)
|
415
|
+
raise CarrierWave::InvalidParameter, "invalid filename" unless filename =~ /^[a-z0-9\.\-\+_]+$/i
|
416
|
+
@original_filename = filename
|
417
|
+
end
|
418
|
+
|
419
|
+
end # Uploader
|
420
|
+
end # CarrierWave
|
data/lib/carrierwave.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module CarrierWave
|
4
|
+
class << self
|
5
|
+
attr_accessor :config
|
6
|
+
end
|
7
|
+
|
8
|
+
class UploadError < StandardError; end
|
9
|
+
class NoFileError < UploadError; end
|
10
|
+
class FormNotMultipart < UploadError; end
|
11
|
+
class InvalidParameter < UploadError; end
|
12
|
+
# Should be used by methods used as process callbacks.
|
13
|
+
class ProcessingError < UploadError; end
|
14
|
+
end
|
15
|
+
|
16
|
+
dir = File.join(File.dirname(__FILE__), 'carrierwave')
|
17
|
+
|
18
|
+
require File.join(dir, 'sanitized_file')
|
19
|
+
require File.join(dir, 'uploader')
|
20
|
+
require File.join(dir, 'mount')
|
21
|
+
require File.join(dir, 'storage', 'abstract')
|
22
|
+
require File.join(dir, 'storage', 'file')
|
23
|
+
require File.join(dir, 'storage', 's3')
|
24
|
+
|
25
|
+
CarrierWave.config = {
|
26
|
+
:storage => :file,
|
27
|
+
:use_cache => true,
|
28
|
+
:storage_engines => {
|
29
|
+
:file => CarrierWave::Storage::File,
|
30
|
+
:s3 => CarrierWave::Storage::S3
|
31
|
+
},
|
32
|
+
:s3 => {
|
33
|
+
:access => :public_read
|
34
|
+
},
|
35
|
+
:store_dir => 'public/uploads',
|
36
|
+
:cache_dir => 'public/uploads/tmp'
|
37
|
+
}
|
38
|
+
|
39
|
+
if defined?(Merb)
|
40
|
+
CarrierWave.config[:root] = Merb.root
|
41
|
+
CarrierWave.config[:public] = Merb.dir_for(:public)
|
42
|
+
|
43
|
+
orm_path = File.dirname(__FILE__) / 'carrierwave' / 'orm' / Merb.orm
|
44
|
+
require orm_path if File.exist?(orm_path + '.rb')
|
45
|
+
|
46
|
+
Merb.push_path(:model, Merb.root / "app" / "uploaders")
|
47
|
+
|
48
|
+
Merb.add_generators File.dirname(__FILE__) / 'generators' / 'uploader_generator'
|
49
|
+
end
|
50
|
+
|
51
|
+
if defined?(Rails)
|
52
|
+
CarrierWave.config[:root] = Rails.root
|
53
|
+
CarrierWave.config[:public] = File.join(Rails.root, 'public')
|
54
|
+
|
55
|
+
require File.join(File.dirname(__FILE__), "carrierwave", "orm", 'activerecord')
|
56
|
+
|
57
|
+
ActiveSupport::Dependencies.load_paths << File.join(Rails.root, "app", "uploaders")
|
58
|
+
end
|
59
|
+
|
60
|
+
if defined?(Sinatra)
|
61
|
+
CarrierWave.config[:root] = Sinatra::Application.root
|
62
|
+
CarrierWave.config[:public] = Sinatra::Application.public
|
63
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class <%= class_name %>Uploader < CarrierWave::Uploader
|
2
|
+
|
3
|
+
# Include RMagick or ImageScience support
|
4
|
+
# include CarrierWave::RMagick
|
5
|
+
# include CarrierWave::ImageScience
|
6
|
+
|
7
|
+
# Choose what kind of storage to use for this uploader
|
8
|
+
storage :file
|
9
|
+
|
10
|
+
# Process files as they are uploaded.
|
11
|
+
# process :scale => [200, 300]
|
12
|
+
#
|
13
|
+
# def scale(width, height)
|
14
|
+
# # do something
|
15
|
+
# end
|
16
|
+
|
17
|
+
# Create different verions of your uploaded files
|
18
|
+
# version :thumb do
|
19
|
+
# process :scale => [50, 50]
|
20
|
+
# end
|
21
|
+
|
22
|
+
# Override the filename of the uploaded files
|
23
|
+
# def filename
|
24
|
+
# "something"
|
25
|
+
# end
|
26
|
+
|
27
|
+
# Override the directory where uploaded files will be stored
|
28
|
+
# def store_dir
|
29
|
+
# "something"
|
30
|
+
# end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Merb
|
2
|
+
module Generators
|
3
|
+
class UploaderGenerator < NamedGenerator
|
4
|
+
|
5
|
+
def self.source_root
|
6
|
+
File.join(File.dirname(__FILE__), 'templates')
|
7
|
+
end
|
8
|
+
|
9
|
+
first_argument :name, :required => true, :desc => "The name of this uploader"
|
10
|
+
|
11
|
+
template :uploader do |t|
|
12
|
+
t.source = 'uploader.rbt'
|
13
|
+
t.destination = "app/uploaders/#{file_name}_uploader.rb"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
add :uploader, UploaderGenerator
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
bork bork bork
|
@@ -0,0 +1 @@
|
|
1
|
+
this is stuff
|
@@ -0,0 +1 @@
|
|
1
|
+
this is stuff
|