echo_uploads 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/echo_uploads.rb +10 -0
- data/lib/echo_uploads/abstract_store.rb +7 -0
- data/lib/echo_uploads/file.rb +76 -0
- data/lib/echo_uploads/filesystem_store.rb +46 -0
- data/lib/echo_uploads/model.rb +139 -0
- data/lib/echo_uploads/perm_file_saving.rb +46 -0
- data/lib/echo_uploads/railtie.rb +7 -0
- data/lib/echo_uploads/temp_file_saving.rb +79 -0
- data/lib/echo_uploads/validation.rb +89 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e1c16aedf066542375c4917d100a2b80e76f552f
|
4
|
+
data.tar.gz: 8245c8fb20b7b6f4f5a5d706fd9f88577c8fe494
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b4b033b96e3d39697160a5edbee7aaf95265bf3209fd949975db05aa5a7f003936efb6e38280ce11159609830cd1e9d6dded6a7509324277eb52d9a2be8a8267
|
7
|
+
data.tar.gz: ecc573409a3bfb5ac9532ce8b8cd91d24e1e74f3a1c6051bbbe4079efde93c37ba52e4fc15bb869af8709564d0f459cefbee39d25a938086c4ddd6be89fe7dc9
|
data/lib/echo_uploads.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module EchoUploads; end
|
2
|
+
|
3
|
+
require 'echo_uploads/railtie'
|
4
|
+
require 'echo_uploads/validation'
|
5
|
+
require 'echo_uploads/perm_file_saving'
|
6
|
+
require 'echo_uploads/temp_file_saving'
|
7
|
+
require 'echo_uploads/model'
|
8
|
+
require 'echo_uploads/file'
|
9
|
+
require 'echo_uploads/abstract_store'
|
10
|
+
require 'echo_uploads/filesystem_store'
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'mime/types'
|
3
|
+
|
4
|
+
module EchoUploads
|
5
|
+
class File < ActiveRecord::Base
|
6
|
+
self.table_name = 'echo_uploads_files'
|
7
|
+
|
8
|
+
belongs_to :owner, polymorphic: true
|
9
|
+
|
10
|
+
before_destroy :delete_file_conditionally
|
11
|
+
|
12
|
+
def compute_mime!
|
13
|
+
type = MIME::Types.type_for(original_filename).first
|
14
|
+
self.mime_type = type ? type.content_type : 'application/octet-stream'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a proc that takes as its only argument an ActionDispatch::UploadedFile
|
18
|
+
# and returns a key string.
|
19
|
+
def self.default_key_proc
|
20
|
+
->(file) do
|
21
|
+
digest = Digest::SHA512.new
|
22
|
+
file.rewind
|
23
|
+
until file.eof?
|
24
|
+
digest.update file.read(1000)
|
25
|
+
end
|
26
|
+
digest.hexdigest
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deletes the file on disk if and only if no other instances of EchoUpload::File
|
31
|
+
# reference it.
|
32
|
+
def delete_file_conditionally
|
33
|
+
unless self.class.where(key: key).where(['id != ?', id]).exists?
|
34
|
+
storage.delete key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def original_filename
|
39
|
+
original_basename + original_extension
|
40
|
+
end
|
41
|
+
|
42
|
+
# Pass in an attribute name, an ActionDispatch::UploadedFile, and an options hash.
|
43
|
+
def persist!(attr, file, options)
|
44
|
+
# Configure and save the metadata object.
|
45
|
+
self.key = options[:key].call file
|
46
|
+
self.owner_attr = attr
|
47
|
+
self.original_extension = ::File.extname(file.original_filename)
|
48
|
+
self.original_basename = ::File.basename(file.original_filename, original_extension)
|
49
|
+
compute_mime!
|
50
|
+
self.storage_type = options[:storage].name
|
51
|
+
save!
|
52
|
+
|
53
|
+
# Write the file to the filestore.
|
54
|
+
storage.write key, file
|
55
|
+
|
56
|
+
# Prune any expired temporary files. (Unless automatic pruning was turned off in
|
57
|
+
# the app config.)
|
58
|
+
unless (
|
59
|
+
Rails.configuration.echo_uploads.respond_to?(:prune_tmp_files_on_upload) and
|
60
|
+
!Rails.configuration.echo_uploads.prune_tmp_files_on_upload
|
61
|
+
)
|
62
|
+
self.class.prune_temporary!
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.prune_temporary!
|
67
|
+
where(temporary: true).where(['expires_at < ?', Time.now]).each do |file_meta|
|
68
|
+
file_meta.destroy
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def storage
|
73
|
+
Object.const_get(storage_type).new
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EchoUploads
|
2
|
+
class FilesystemStore < ::EchoUploads::AbstractStore
|
3
|
+
def write(key, file)
|
4
|
+
_path = path key
|
5
|
+
unless ::File.exists?(_path)
|
6
|
+
unless ::File.exists?(folder)
|
7
|
+
begin
|
8
|
+
FileUtils.mkdir_p folder
|
9
|
+
rescue Errno::EACCES
|
10
|
+
raise "Permission denied trying to create #{folder}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
FileUtils.cp file.path, _path
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def read(key)
|
18
|
+
File.read path(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(key)
|
22
|
+
_path = path(key)
|
23
|
+
::File.delete(_path) if ::File.exists?(_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def open(key)
|
27
|
+
::File.open(path(key), 'rb', &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def path(key)
|
31
|
+
::File.join folder, key
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Can be customized in your per-environment config like this:
|
37
|
+
# config.echo_uploads.folder = File.join(Rails.root, 'my_uploads_folder', 'development')
|
38
|
+
def folder
|
39
|
+
if Rails.configuration.respond_to?(:echo_uploads) and Rails.configuration.echo_uploads.folder
|
40
|
+
Rails.configuration.echo_uploads.folder
|
41
|
+
else
|
42
|
+
::File.join Rails.root, 'echo_uploads', Rails.env
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'json'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module EchoUploads
|
6
|
+
module Model
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
class_attribute :echo_uploads_config
|
10
|
+
|
11
|
+
include ::EchoUploads::Validation
|
12
|
+
include ::EchoUploads::PermFileSaving
|
13
|
+
include ::EchoUploads::TempFileSaving
|
14
|
+
|
15
|
+
extend ClassMethods
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def echo_uploads_data
|
20
|
+
Base64.encode64(JSON.dump(self.class.echo_uploads_config.inject({}) do |hash, (attr, cfg)|
|
21
|
+
meta = send("#{attr}_tmp_metadata")
|
22
|
+
if meta
|
23
|
+
hash[attr] = {'id' => meta.id, 'key' => meta.key}
|
24
|
+
end
|
25
|
+
hash
|
26
|
+
end)).strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# Pass in a hash that's been encoded as JSON and then Base64.
|
30
|
+
def echo_uploads_data=(data)
|
31
|
+
parsed = JSON.parse Base64.decode64(data)
|
32
|
+
parsed.each do |attr, attr_data|
|
33
|
+
# Must verify that the metadata record is temporary. If not, an attacker could
|
34
|
+
# pass the ID of a permanent record and change its owner.
|
35
|
+
meta = ::EchoUploads::File.where(key: attr_data['key'], temporary: true).find(attr_data['id'])
|
36
|
+
send("#{attr}_tmp_metadata=", meta)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Helper method used internally Echo Uploads.
|
41
|
+
def map_metadata(attr)
|
42
|
+
meta = send("#{attr}_metadata")
|
43
|
+
meta ? yield(meta) : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
# Options:
|
48
|
+
# - +key+: A Proc that takes an ActionDispatch::UploadedFile and returns a key
|
49
|
+
# uniquely identifying the file. If this option is not specified, the key is
|
50
|
+
# computed as the SHA-512 hash of the file contents. A digest of the file's
|
51
|
+
# contents should always be at least a part of the key.
|
52
|
+
# - +expires+: Length of time temporary files will be persisted. Defaults to
|
53
|
+
# +1.day+.
|
54
|
+
# - +storage+: A class that persists uploaded files to disk, to the cloud, or to
|
55
|
+
# wherever else you want. Defaults to +EchoUploads::FilesystemStore+.
|
56
|
+
def echo_upload(attr, options = {})
|
57
|
+
options = {
|
58
|
+
expires: 1.day,
|
59
|
+
storage: ::EchoUploads::FilesystemStore,
|
60
|
+
key: ::EchoUploads::File.default_key_proc
|
61
|
+
}.merge(options)
|
62
|
+
|
63
|
+
# Init the config object. We can't use [] syntax to set the hash key because
|
64
|
+
# class_attribute expects you to call the setter method every time the
|
65
|
+
# attribute value changes. (Merely calling [] would just mutate the referenced
|
66
|
+
# object, and wouldn't invoke the setter.)
|
67
|
+
self.echo_uploads_config ||= {}
|
68
|
+
self.echo_uploads_config = echo_uploads_config.merge attr => {}
|
69
|
+
|
70
|
+
# Define reader and writer methods for the file attribute.
|
71
|
+
attr_accessor attr
|
72
|
+
|
73
|
+
# Define the path method. This method will raise if the given storage
|
74
|
+
# class doesn't support the #path method.
|
75
|
+
define_method("#{attr}_path") do
|
76
|
+
map_metadata(attr) do |meta|
|
77
|
+
meta.storage.path meta.key
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define the MIME type method.
|
82
|
+
define_method("#{attr}_mime") do
|
83
|
+
map_metadata(attr, &:mime_type)
|
84
|
+
end
|
85
|
+
alias_method "#{attr}_mime_type", "#{attr}_mime"
|
86
|
+
|
87
|
+
# Define the original filename method.
|
88
|
+
define_method("#{attr}_original_filename") do
|
89
|
+
map_metadata(attr, &:original_filename)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Define the key method
|
93
|
+
define_method("#{attr}_key") do
|
94
|
+
map_metadata(attr, &:key)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Define the has_x? method. Returns true if a permanent or temporary file has been
|
98
|
+
# persisted, or if a file (which may not be valid) has been uploaded this request
|
99
|
+
# cycle.
|
100
|
+
define_method("has_#{attr}?") do
|
101
|
+
# Does this record have a permanent file?
|
102
|
+
send("has_prm_#{attr}?") or
|
103
|
+
|
104
|
+
# Did the submitted form "remember" a previously saved metadata record?
|
105
|
+
send("has_tmp_#{attr}?") or
|
106
|
+
|
107
|
+
# Has a new file been uploaded in this request cycle?
|
108
|
+
send(attr).present?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Define the has_prm_x? method. Returns true if the permanent metadata record
|
112
|
+
# exists and has its owner set to this object.
|
113
|
+
define_method("has_prm_#{attr}?") do
|
114
|
+
send("#{attr}_metadata").present? and send("#{attr}_metadata").persisted?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Define the has_tmp_x? method. Returns true if the record "remembers"
|
118
|
+
# a a temporary metadata record. (Typically because validation errors caused
|
119
|
+
# the form to be redisplayed.)
|
120
|
+
define_method("has_tmp_#{attr}?") do
|
121
|
+
send("#{attr}_tmp_metadata").present?
|
122
|
+
end
|
123
|
+
|
124
|
+
# Define the association with the metadata model.
|
125
|
+
has_one("#{attr}_metadata".to_sym,
|
126
|
+
->() { where(owner_attr: attr) },
|
127
|
+
as: :owner, dependent: :destroy, class_name: '::EchoUploads::File'
|
128
|
+
)
|
129
|
+
|
130
|
+
# Define the temp attribute for the metadata model.
|
131
|
+
attr_accessor "#{attr}_tmp_metadata"
|
132
|
+
|
133
|
+
configure_temp_file_saving attr, options
|
134
|
+
|
135
|
+
configure_perm_file_saving attr, options
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EchoUploads
|
2
|
+
module PermFileSaving
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval { extend ClassMethods }
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def configure_perm_file_saving(attr, options)
|
9
|
+
# Save the file and the metadata after this model saves.
|
10
|
+
after_save do |model|
|
11
|
+
if (file = send(attr)).present?
|
12
|
+
# A file is being uploaded during this request cycle.
|
13
|
+
if meta = send("#{attr}_metadata")
|
14
|
+
# A previous permanent file exists. This is a new version being uploaded.
|
15
|
+
# Delete the old version from the disk if no other metadata record
|
16
|
+
# references it.
|
17
|
+
meta.delete_file_conditionally
|
18
|
+
else
|
19
|
+
# No previous permanent file exists.
|
20
|
+
meta = ::EchoUploads::File.new(owner: model, temporary: false)
|
21
|
+
send("#{attr}_metadata=", meta)
|
22
|
+
end
|
23
|
+
meta.persist! attr, file, options
|
24
|
+
elsif meta = send("#{attr}_tmp_metadata") and meta.temporary
|
25
|
+
# A file has not been uploaded during this request cycle. However, the
|
26
|
+
# submitted form "remembered" a temporary metadata record that was previously
|
27
|
+
# saved. We mark it as permanent and set its owner.
|
28
|
+
#
|
29
|
+
# But first, we must delete any existing metadata record. (It's possible we
|
30
|
+
# were trying to replace an old version of the file, and there were validation
|
31
|
+
# errors on the first attempt.)
|
32
|
+
if old = model.send("#{attr}_metadata")
|
33
|
+
old.destroy
|
34
|
+
end
|
35
|
+
|
36
|
+
meta.owner = model
|
37
|
+
send("#{attr}_metadata=", meta)
|
38
|
+
meta.temporary = false
|
39
|
+
meta.expires_at = nil
|
40
|
+
meta.save!
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module EchoUploads
|
2
|
+
module TempFileSaving
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval { extend ClassMethods }
|
5
|
+
end
|
6
|
+
|
7
|
+
# On a failed attempt to save (typically due to validation errors), save the file
|
8
|
+
# and metadata. Metadata record will be given the temporary flag.
|
9
|
+
#
|
10
|
+
# To deal with the various persistence methods (#save, #create,
|
11
|
+
# #update_attributes), and the fact that ActiveRecord rolls back the transaction
|
12
|
+
# on validation failure, we can't just use a convenient after_validation callback.
|
13
|
+
# Instead, we have to do some trickery with .alias_method_chain.
|
14
|
+
def maybe_save_temp_file(attr, options)
|
15
|
+
success = yield
|
16
|
+
|
17
|
+
# Because of the tangled way ActiveRecord's persistence methods delegate to each
|
18
|
+
# other, maybe_save_temp_file sometimes gets called twice. That's unavoidable. To
|
19
|
+
# workaround that issue, we check whether we're calling #save from within #update.
|
20
|
+
@echo_uploads_saving ||= {}
|
21
|
+
@echo_uploads_updating ||= {}
|
22
|
+
unless @echo_uploads_saving[attr] and @echo_uploads_updating[attr]
|
23
|
+
if (file = send(attr)).present? and !success and errors[attr].empty?
|
24
|
+
# A file has been uploaded. Validation failed, but the file itself was valid.
|
25
|
+
# Thus, we must persist a temporary file.
|
26
|
+
#
|
27
|
+
# It's possible at this point that the record already has a permanent file.
|
28
|
+
# That's fine. We'll now have a permanent and a temporary one. The temporary
|
29
|
+
# one will replace the permanent one if and when the user resubmits with
|
30
|
+
# valid data.
|
31
|
+
meta = ::EchoUploads::File.new(
|
32
|
+
owner: nil, temporary: true, expires_at: options[:expires].from_now
|
33
|
+
)
|
34
|
+
meta.persist! attr, file, options
|
35
|
+
send("#{attr}_tmp_metadata=", meta)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
success
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
# Wraps ActiveRecord's persistence methods. We can't use a callback for this. See
|
44
|
+
# the comment above for an explanation of why.
|
45
|
+
def configure_temp_file_saving(attr, options)
|
46
|
+
# Wrap the #save method. This also suffices for #create.
|
47
|
+
define_method("save_with_#{attr}_temp_file") do |*args|
|
48
|
+
@echo_uploads_saving ||= {}
|
49
|
+
@echo_uploads_saving[attr] = true
|
50
|
+
begin
|
51
|
+
success = maybe_save_temp_file(attr, options) do
|
52
|
+
send "save_without_#{attr}_temp_file", *args
|
53
|
+
end
|
54
|
+
success
|
55
|
+
ensure
|
56
|
+
@echo_uploads_saving.delete attr
|
57
|
+
end
|
58
|
+
end
|
59
|
+
alias_method_chain :save, "#{attr}_temp_file".to_sym
|
60
|
+
|
61
|
+
# Wrap the #update and #update_attributes methods.
|
62
|
+
define_method("update_with_#{attr}_temp_file") do |*args|
|
63
|
+
@echo_uploads_updating ||= {}
|
64
|
+
@echo_uploads_updating[attr] = true
|
65
|
+
begin
|
66
|
+
success = maybe_save_temp_file(attr, options) do
|
67
|
+
send "update_without_#{attr}_temp_file", *args
|
68
|
+
end
|
69
|
+
success
|
70
|
+
ensure
|
71
|
+
@echo_uploads_updating.delete attr
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias_method_chain :update, "#{attr}_temp_file".to_sym
|
75
|
+
alias_method :update_attributes, "update_with_#{attr}_temp_file".to_sym
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# If you want to validate the presence of a file, be sure to use:
|
2
|
+
# validates :attr, uploads: {presence: true}
|
3
|
+
# instead of:
|
4
|
+
# validates :attr, presence: true
|
5
|
+
# The former takes into account files that have already been persisted, whereas the
|
6
|
+
# latter does not and will cause false validation errors.
|
7
|
+
|
8
|
+
module EchoUploads
|
9
|
+
module Validation
|
10
|
+
class UploadValidator < ActiveModel::EachValidator
|
11
|
+
def validate_each(record, attr, val)
|
12
|
+
# Presence validation
|
13
|
+
if options[:presence]
|
14
|
+
unless record.send("has_#{attr}?")
|
15
|
+
record.errors[attr] << (
|
16
|
+
options[:message] ||
|
17
|
+
'must be uploaded'
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# File size validation
|
23
|
+
if options[:max_size]
|
24
|
+
unless options[:max_size].is_a? Numeric
|
25
|
+
raise(ArgumentError,
|
26
|
+
"validates :#{attr}, :upload called with invalid :max_size option. " +
|
27
|
+
":max_size must be a number, e.g. 1.megabyte"
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
if val.present?
|
32
|
+
unless val.respond_to?(:size)
|
33
|
+
raise ArgumentError, "Expected ##{attr} to respond to #size"
|
34
|
+
end
|
35
|
+
|
36
|
+
if val.size > options[:max_size]
|
37
|
+
record.errors[attr] << (
|
38
|
+
options[:message] ||
|
39
|
+
"must be smaller than #{options[:max_size].to_i} bytes"
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extension validation
|
46
|
+
if options[:extension]
|
47
|
+
unless options[:extension].is_a? Array
|
48
|
+
raise(ArgumentError,
|
49
|
+
"validates :#{attr}, :upload called with invalid :extension option. " +
|
50
|
+
":extension must be an array of extensions like ['.jpg', '.png']"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
if val.present?
|
55
|
+
unless val.respond_to?(:original_filename)
|
56
|
+
raise ArgumentError, "Expected ##{attr} to respond to #original_filename"
|
57
|
+
end
|
58
|
+
|
59
|
+
ext = ::File.extname val.original_filename
|
60
|
+
unless options[:extension].include?(ext)
|
61
|
+
record.errors[attr] << (
|
62
|
+
options[:message] ||
|
63
|
+
"must have one of the following extensions: #{options[:extension].join(',')}"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# If you pass in the presence: true option, this validator will assume you're using the
|
74
|
+
# CachedUploads module. It will look at the has_cached_upload config for the given
|
75
|
+
# attribute and check if a) the upload is present, b) the temporary MD5 hash is present,
|
76
|
+
# or c) the record has already been saved.
|
77
|
+
class UploadValidator < ActiveModel::EachValidator
|
78
|
+
def validate_each(record, attribute, value)
|
79
|
+
if options[:presence]
|
80
|
+
config = record.class.cached_uploads[attribute.to_sym]
|
81
|
+
if record.new_record? and value.blank? and record.send(config[:md5_attr]).blank?
|
82
|
+
record.errors[attribute] << (options[:message] || "can't be blank")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
if value.present? and value.size > options[:max_size]
|
86
|
+
record.errors[attribute] << (options[:message] || "is too large (max is #{options[:max_size]} bytes)")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: echo_uploads
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jarrett Colby
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mime-types
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: turn
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: ''
|
42
|
+
email: jarrett@madebyhq.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- lib/echo_uploads.rb
|
48
|
+
- lib/echo_uploads/abstract_store.rb
|
49
|
+
- lib/echo_uploads/file.rb
|
50
|
+
- lib/echo_uploads/filesystem_store.rb
|
51
|
+
- lib/echo_uploads/model.rb
|
52
|
+
- lib/echo_uploads/perm_file_saving.rb
|
53
|
+
- lib/echo_uploads/railtie.rb
|
54
|
+
- lib/echo_uploads/temp_file_saving.rb
|
55
|
+
- lib/echo_uploads/validation.rb
|
56
|
+
homepage: https://github.com/jarrett/echo_uploads
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata: {}
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 2.2.2
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Uploaded files for Rails
|
80
|
+
test_files: []
|