voltron-upload 0.2.1
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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +166 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/dropzone.js +1769 -0
- data/app/assets/javascripts/voltron-upload.js +264 -0
- data/app/assets/stylesheets/dropzone.scss +595 -0
- data/app/assets/stylesheets/voltron-upload.scss +446 -0
- data/app/views/voltron/upload/preview/_horizontal_tile.html.erb +18 -0
- data/app/views/voltron/upload/preview/_progress.html.erb +8 -0
- data/app/views/voltron/upload/preview/_vertical_tile.html.erb +18 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/generators/voltron/upload/install/assets_generator.rb +25 -0
- data/lib/generators/voltron/upload/install/views_generator.rb +21 -0
- data/lib/generators/voltron/upload/install_generator.rb +57 -0
- data/lib/voltron/config/upload.rb +18 -0
- data/lib/voltron/upload/action_dispatch/routes.rb +17 -0
- data/lib/voltron/upload/action_view/field.rb +152 -0
- data/lib/voltron/upload/active_record/base.rb +87 -0
- data/lib/voltron/upload/carrierwave/uploader/base.rb +66 -0
- data/lib/voltron/upload/engine.rb +20 -0
- data/lib/voltron/upload/error.rb +20 -0
- data/lib/voltron/upload/version.rb +5 -0
- data/lib/voltron/upload.rb +51 -0
- data/lib/voltron/uploader.rb +58 -0
- data/voltron-upload.gemspec +34 -0
- metadata +243 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
module Voltron
|
2
|
+
module Upload
|
3
|
+
module Field
|
4
|
+
|
5
|
+
include ActionView::Helpers::TagHelper
|
6
|
+
|
7
|
+
def file_field(method, options={})
|
8
|
+
if Voltron.config.upload.enabled && !options[:default]
|
9
|
+
template = instance_variable_get('@template')
|
10
|
+
field = UploadField.new(@object, method, template, options)
|
11
|
+
|
12
|
+
# +merge+ is because options is a hash with_indifferent_access, and will therefore have an 'object' attribute when converted to html
|
13
|
+
#super method, {}.merge(field.options)
|
14
|
+
content_tag 'v-upload', nil, field.options
|
15
|
+
else
|
16
|
+
options.delete(:default)
|
17
|
+
super method, options
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class UploadField
|
22
|
+
|
23
|
+
include ::ActionDispatch::Routing::PolymorphicRoutes
|
24
|
+
|
25
|
+
include ::Rails.application.routes.url_helpers
|
26
|
+
|
27
|
+
include ::ActionView::Helpers::TextHelper
|
28
|
+
|
29
|
+
attr_reader :options, :template
|
30
|
+
|
31
|
+
def initialize(model, method, template, options)
|
32
|
+
@model = model
|
33
|
+
@method = method.to_sym
|
34
|
+
@template = template
|
35
|
+
@options = options.with_indifferent_access
|
36
|
+
prepare
|
37
|
+
end
|
38
|
+
|
39
|
+
def prepare
|
40
|
+
#add_preview_class if has_preview_template?
|
41
|
+
|
42
|
+
options.merge!({
|
43
|
+
':multiple' => multiple?,
|
44
|
+
':files' => files.to_json,
|
45
|
+
':cached' => caches.to_json,
|
46
|
+
':removed' => removals.to_json,
|
47
|
+
':options' => preview_options.to_json,
|
48
|
+
'accept' => accept,
|
49
|
+
'preview' => preview_name,
|
50
|
+
'param' => input_name,
|
51
|
+
'url' => polymorphic_path(@model.class, action: :upload)
|
52
|
+
})
|
53
|
+
|
54
|
+
#options[:data] ||= {}
|
55
|
+
#options[:data].merge!({
|
56
|
+
# upload_files: files,
|
57
|
+
# upload_cache: caches,
|
58
|
+
# upload_remove: removals,
|
59
|
+
# upload_options: preview_options
|
60
|
+
#})
|
61
|
+
end
|
62
|
+
|
63
|
+
def preview_options
|
64
|
+
previews = Voltron.config.upload.previews || {}
|
65
|
+
opts = previews.with_indifferent_access.try(:[], preview_name) || {}
|
66
|
+
opts.merge!({
|
67
|
+
preview_template: preview_markup,
|
68
|
+
})
|
69
|
+
opts.merge!(options.delete(:options) || {})
|
70
|
+
opts.map { |k,v| { k.to_s.camelize(:lower) => v } }.reduce(Hash.new, :merge).compact
|
71
|
+
end
|
72
|
+
|
73
|
+
def preview_markup
|
74
|
+
if has_preview_template?
|
75
|
+
# Fetch the html found in the partial provided
|
76
|
+
ActionController::Base.new.render_to_string(partial: "voltron/upload/preview/#{preview_name}").squish
|
77
|
+
elsif has_preview_markup?
|
78
|
+
# If not blank, value of +preview+ is likely (should be) raw html, in which case, just return that markup
|
79
|
+
preview.squish
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Strip tags, they cause problems in the lookup_context +exists?+ and +render_to_string+
|
84
|
+
def preview_name
|
85
|
+
strip_tags(preview)
|
86
|
+
end
|
87
|
+
|
88
|
+
def preview
|
89
|
+
@preview ||= options.delete(:preview).to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def accept
|
93
|
+
@accept ||= options.delete(:accept).to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
def has_preview_template?
|
97
|
+
preview_name.present? && template.lookup_context.exists?(preview_name, 'voltron/upload/preview', true)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Eventually, consider utilizing Nokogiri to detect whether content also is actually HTML markup
|
101
|
+
# Right now the overhead and frustration of that gem is not worth it
|
102
|
+
def has_preview_markup?
|
103
|
+
preview.present?
|
104
|
+
end
|
105
|
+
|
106
|
+
#def add_preview_class
|
107
|
+
# options[:class] ||= ''
|
108
|
+
# classes = options[:class].split(/\s+/)
|
109
|
+
# classes << "dz-layout-#{preview_name}"
|
110
|
+
# options[:class] = classes.join(' ')
|
111
|
+
#end
|
112
|
+
|
113
|
+
def input_name
|
114
|
+
ActionView::Helpers::Tags::Base.new(ActiveModel::Naming.param_key(@model), @method, nil).send(:tag_name) + (multiple? ? '[]' : '')
|
115
|
+
end
|
116
|
+
|
117
|
+
def multiple?
|
118
|
+
@model.respond_to?("#{@method}_urls")
|
119
|
+
end
|
120
|
+
|
121
|
+
def files
|
122
|
+
# If set to not preserve files, return an empty array so nothing is shown
|
123
|
+
return [] if options[:preserve] === false
|
124
|
+
|
125
|
+
# Return an array of files' json data
|
126
|
+
uploads = Array.wrap(@model.send(@method)).map(&:to_upload_json).reject(&:blank?)
|
127
|
+
|
128
|
+
# Remove all uploads from the array that have been flagged for removal
|
129
|
+
removals.each do |removal|
|
130
|
+
uploads.reject! { |upload| upload[:id] == removal }
|
131
|
+
end
|
132
|
+
|
133
|
+
uploads
|
134
|
+
end
|
135
|
+
|
136
|
+
def caches
|
137
|
+
if multiple?
|
138
|
+
cache = @model.send("cache_#{@method}") rescue []
|
139
|
+
Array.wrap(cache)
|
140
|
+
else
|
141
|
+
Array.wrap(@model.send("cache_#{@method}"))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def removals
|
146
|
+
Array.wrap(@model.send("remove_#{@method}")).compact.reject { |i| !i }
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Voltron
|
2
|
+
module Upload
|
3
|
+
module Base
|
4
|
+
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
def mount_uploader(*args)
|
10
|
+
super *args
|
11
|
+
|
12
|
+
column = args.first.to_sym
|
13
|
+
|
14
|
+
attr_accessor "cache_#{column}"
|
15
|
+
|
16
|
+
before_validation do
|
17
|
+
uploader = self.class.uploaders[column]
|
18
|
+
|
19
|
+
begin
|
20
|
+
cache_id = send("cache_#{column}")
|
21
|
+
send(column).retrieve_from_cache!(cache_id) if cache_id.present?
|
22
|
+
rescue ::CarrierWave::InvalidParameter => e
|
23
|
+
# Invalid cache id, we don't need to do anything but skip it
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def mount_uploaders(*args)
|
29
|
+
super *args
|
30
|
+
|
31
|
+
column = args.first.to_sym
|
32
|
+
|
33
|
+
attr_accessor "cache_#{column}"
|
34
|
+
|
35
|
+
before_validation do
|
36
|
+
uploader = self.class.uploaders[column]
|
37
|
+
cache_ids = (send("cache_#{column}") rescue []) || []
|
38
|
+
|
39
|
+
# Store the existing files
|
40
|
+
files = send(column)
|
41
|
+
|
42
|
+
cache_ids.each do |cache_id|
|
43
|
+
begin
|
44
|
+
# Retrieve files from the cache and add them to the list of files
|
45
|
+
file = uploader.new(self, column)
|
46
|
+
file.retrieve_from_cache!(cache_id)
|
47
|
+
files << file
|
48
|
+
rescue ::CarrierWave::InvalidParameter => e
|
49
|
+
# Invalid cache id, we don't need to do anything but skip it
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the files
|
54
|
+
send("#{column}=", files)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Only required for multiple uploads. Since Carrierwave does not have any way to remove individual files
|
58
|
+
# we must identify each file to be removed individually, remove it from the array of existing files,
|
59
|
+
# then reset the value of our mounted files
|
60
|
+
after_validation do
|
61
|
+
# Only attempt to remove files if there are no validation errors
|
62
|
+
if errors.empty?
|
63
|
+
# Merge any new uploads with the pre-existing uploads (so we can "add" new files, instead of overwriting)
|
64
|
+
uploads = Array.wrap(send(column))
|
65
|
+
|
66
|
+
# Get the ids of uploads we want to remove
|
67
|
+
removals = Array.wrap(send("remove_#{column}"))
|
68
|
+
|
69
|
+
removals.each do |removal|
|
70
|
+
uploads.reject! { |upload| upload.id == removal }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Initially ensure carrierwave DOESN'T think we want to remove ALL files, we're just going to change the files (i.e. - remove some, not all)
|
74
|
+
send("remove_#{column}=", false)
|
75
|
+
|
76
|
+
# Ensure that nil is assigned as the value if we indeed have no more files
|
77
|
+
send("remove_#{column}!") if uploads.empty?
|
78
|
+
|
79
|
+
assign_attributes(column => uploads)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Voltron
|
2
|
+
module Upload
|
3
|
+
module CarrierWave
|
4
|
+
module Uploader
|
5
|
+
module Base
|
6
|
+
|
7
|
+
def initialize(*args)
|
8
|
+
self.class.send(:before, :store, :save_timestamp)
|
9
|
+
self.class.send(:after, :store, :apply_timestamp)
|
10
|
+
super(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_upload_json
|
14
|
+
if present?
|
15
|
+
{
|
16
|
+
id: id,
|
17
|
+
url: url,
|
18
|
+
name: file.original_filename,
|
19
|
+
size: file.size,
|
20
|
+
type: file.content_type
|
21
|
+
}
|
22
|
+
else
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
if stored?
|
29
|
+
[File.mtime(full_store_path).to_i, file.original_filename].join('/')
|
30
|
+
elsif cached? && File.exists?(Rails.root.join('public', cache_path))
|
31
|
+
[cached?, file.original_filename].join('/')
|
32
|
+
else
|
33
|
+
file.original_filename
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def stored?
|
38
|
+
File.exists?(full_store_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def full_store_path
|
42
|
+
Rails.root.join('public', store_path(file.filename))
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Before we store the file for good, grab the offset number
|
48
|
+
# so it can be used to create a unique timestamp after storing
|
49
|
+
def save_timestamp(*args)
|
50
|
+
id_components = File.basename(File.expand_path('..', file.path)).split('-')
|
51
|
+
@offset = id_components[2].to_i + 1000
|
52
|
+
end
|
53
|
+
|
54
|
+
# Update the modified time of the file to a unique timestamp
|
55
|
+
# This timestamp will later be used to help identify the file,
|
56
|
+
# as it will be part of the generated id
|
57
|
+
def apply_timestamp(*args)
|
58
|
+
@offset ||= rand(1..1000)
|
59
|
+
FileUtils.touch file.path, mtime: Time.now + @offset.seconds
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Voltron
|
2
|
+
module Upload
|
3
|
+
class Engine < Rails::Engine
|
4
|
+
|
5
|
+
isolate_namespace Voltron
|
6
|
+
|
7
|
+
initializer 'voltron.upload.initialize' do
|
8
|
+
::ActionDispatch::Routing::Mapper.send :include, ::Voltron::Upload::Routes
|
9
|
+
::ActionController::Base.send :extend, ::Voltron::Upload
|
10
|
+
|
11
|
+
ActiveSupport.on_load :active_record do
|
12
|
+
require 'voltron/upload/action_view/field'
|
13
|
+
::ActionView::Helpers::FormBuilder.send :prepend, ::Voltron::Upload::Field
|
14
|
+
::CarrierWave::Uploader::Base.send :prepend, ::Voltron::Upload::CarrierWave::Uploader::Base
|
15
|
+
::ActiveRecord::Base.send :include, ::Voltron::Upload::Base
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Voltron
|
2
|
+
module Upload
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
attr_accessor :messages
|
6
|
+
|
7
|
+
def initialize(*messages)
|
8
|
+
@messages = messages.flatten
|
9
|
+
end
|
10
|
+
|
11
|
+
def response
|
12
|
+
{ success: false, error: @messages }
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
:not_acceptable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'voltron'
|
2
|
+
require 'carrierwave'
|
3
|
+
require 'voltron/upload/version'
|
4
|
+
require 'voltron/config/upload'
|
5
|
+
require 'voltron/upload/error'
|
6
|
+
require 'voltron/uploader'
|
7
|
+
require 'voltron/upload/carrierwave/uploader/base'
|
8
|
+
require 'voltron/upload/active_record/base'
|
9
|
+
require 'voltron/upload/action_dispatch/routes'
|
10
|
+
|
11
|
+
module Voltron
|
12
|
+
module Upload
|
13
|
+
|
14
|
+
LOG_COLOR = :light_cyan
|
15
|
+
|
16
|
+
def uploadable(resource = nil)
|
17
|
+
include ControllerMethods
|
18
|
+
|
19
|
+
resource ||= controller_name
|
20
|
+
@uploader ||= Voltron::Uploader.new(resource)
|
21
|
+
|
22
|
+
rescue_from ActionController::InvalidAuthenticityToken do |e|
|
23
|
+
raise unless action_name == 'upload'
|
24
|
+
render json: { success: false, error: 'Invalid authenticity token provided' }, status: :unauthorized
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module ControllerMethods
|
29
|
+
|
30
|
+
def upload
|
31
|
+
begin
|
32
|
+
render json: uploader.process!(upload_params), status: :created
|
33
|
+
rescue Voltron::Upload::Error => e
|
34
|
+
render json: e.response, status: e.status
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def uploader
|
39
|
+
self.class.instance_variable_get('@uploader')
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload_params
|
43
|
+
request.parameters[uploader.resource_name].slice(*uploader.permitted_params)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
require "voltron/upload/engine" if defined?(Rails)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Voltron
|
2
|
+
class Uploader
|
3
|
+
|
4
|
+
attr_accessor :resource
|
5
|
+
|
6
|
+
def initialize(resource)
|
7
|
+
@resource = resource.to_s.classify.safe_constantize
|
8
|
+
end
|
9
|
+
|
10
|
+
# Resource name as it would appear in the params hash
|
11
|
+
def resource_name
|
12
|
+
resource.name.singularize.underscore.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
# List of permitted parameters needed for upload action
|
16
|
+
def permitted_params
|
17
|
+
columns.keys.map(&:to_sym)
|
18
|
+
#.map { |name, multiple| multiple ? { name => [] } : name }
|
19
|
+
end
|
20
|
+
|
21
|
+
def process!(params)
|
22
|
+
params = params.map { |column, value| { column => multiple?(column) && value.is_a?(Array) ? value.map(&:values).flatten : value } }.reduce(Hash.new, :merge)
|
23
|
+
model = resource.new(params)
|
24
|
+
|
25
|
+
# Test the validity, get the errors if any
|
26
|
+
model.valid?
|
27
|
+
|
28
|
+
# Remove all errors that were not related to an uploader, they're expected in this case
|
29
|
+
(model.errors.keys - resource.uploaders.keys).each { |k| model.errors.delete k }
|
30
|
+
|
31
|
+
if model.errors.any?
|
32
|
+
# If any errors, return the messages
|
33
|
+
raise ::Voltron::Upload::Error.new(model.errors.full_messages)
|
34
|
+
else
|
35
|
+
{ uploads: files_from(model) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def files_from(model)
|
40
|
+
model.slice(columns.keys).values.flatten.reject(&:blank?).map(&:to_upload_json)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get a hash of uploader columns and whether or not it accepts multiple uploads
|
44
|
+
# i.e. - { column => multiple_uploads? }
|
45
|
+
# i.e. - { avatar: false }
|
46
|
+
def columns
|
47
|
+
@instance ||= resource.new
|
48
|
+
uploaders = resource.uploaders.keys.map(&:to_s)
|
49
|
+
resource.uploaders.map { |k,v| { k.to_s => multiple?(k) } }.reduce(Hash.new, :merge)
|
50
|
+
end
|
51
|
+
|
52
|
+
def multiple?(column)
|
53
|
+
@instance ||= resource.new
|
54
|
+
@instance.respond_to?("#{column}_urls")
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'voltron/upload/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'voltron-upload'
|
8
|
+
spec.version = Voltron::Upload::VERSION
|
9
|
+
spec.authors = ['Eric Hainer']
|
10
|
+
spec.email = ['eric@commercekitchen.com']
|
11
|
+
|
12
|
+
spec.summary = %q{Adds logic to handle drag and drop file uploads with Dropzone.js}
|
13
|
+
spec.homepage = 'https://github.com/ehainer/voltron-upload'
|
14
|
+
spec.license = 'GNU GPL v3'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'rails', '>= 4.2'
|
22
|
+
spec.add_dependency 'voltron', '>= 0.2.5'
|
23
|
+
spec.add_dependency 'carrierwave', '~> 1.0'
|
24
|
+
spec.add_dependency 'sass-rails', '>= 5.0'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'byebug'
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
28
|
+
spec.add_development_dependency 'rake', '~> 11.3'
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
30
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.4'
|
31
|
+
spec.add_development_dependency 'factory_girl_rails', '~> 4.0'
|
32
|
+
spec.add_development_dependency 'jquery-rails', '~> 4'
|
33
|
+
spec.add_development_dependency 'sqlite3', '~> 1.2'
|
34
|
+
end
|