echo_uploads 0.0.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.
- 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: []
|