attachs 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +98 -0
- data/Rakefile +34 -0
- data/app/controllers/attachs/presets_controller.rb +17 -0
- data/config/locales/en.yml +8 -0
- data/config/locales/es.yml +8 -0
- data/config/routes.rb +5 -0
- data/lib/attachs.rb +16 -0
- data/lib/attachs/active_record/base.rb +145 -0
- data/lib/attachs/engine.rb +7 -0
- data/lib/attachs/magick/image.rb +85 -0
- data/lib/attachs/railtie.rb +20 -0
- data/lib/attachs/storages/local.rb +61 -0
- data/lib/attachs/storages/s3.rb +77 -0
- data/lib/attachs/types/file.rb +113 -0
- data/lib/attachs/types/image.rb +40 -0
- data/lib/attachs/validators/attachment_content_type_validator.rb +11 -0
- data/lib/attachs/validators/attachment_presence_validator.rb +9 -0
- data/lib/attachs/validators/attachment_size_validator.rb +27 -0
- data/lib/attachs/validators/base.rb +13 -0
- data/lib/attachs/version.rb +5 -0
- data/lib/generators/attachs/s3/config_generator.rb +13 -0
- data/lib/generators/attachs/s3/templates/s3.yml +16 -0
- data/lib/tasks/attachs_tasks.rake +71 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/all_attached.rb +16 -0
- data/test/dummy/app/models/file_attached.rb +3 -0
- data/test/dummy/app/models/image_attached.rb +3 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +29 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +3 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20130820222342_create_file_attacheds.rb +9 -0
- data/test/dummy/db/migrate/20130820222355_create_image_attacheds.rb +9 -0
- data/test/dummy/db/migrate/20130820222534_create_all_attacheds.rb +19 -0
- data/test/dummy/db/schema.rb +44 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +28 -0
- data/test/dummy/log/test.log +8545 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/uploads/files/file.txt +1 -0
- data/test/dummy/public/uploads/images/big/image.jpg +0 -0
- data/test/dummy/public/uploads/images/original/image.jpg +0 -0
- data/test/dummy/public/uploads/images/small/image.jpg +0 -0
- data/test/dummy/test/fixtures/file.txt +1 -0
- data/test/dummy/test/fixtures/file_big.txt +1 -0
- data/test/dummy/test/fixtures/file_empty.txt +1 -0
- data/test/dummy/test/fixtures/image.jpg +0 -0
- data/test/dummy/test/fixtures/image_big.jpg +0 -0
- data/test/dummy/test/fixtures/image_empty.jpg +0 -0
- data/test/local_file_record_test.rb +20 -0
- data/test/local_file_string_test.rb +23 -0
- data/test/local_file_upload_test.rb +30 -0
- data/test/local_generate_test.rb +25 -0
- data/test/local_image_record_test.rb +25 -0
- data/test/local_image_string_test.rb +34 -0
- data/test/local_image_upload_test.rb +27 -0
- data/test/local_records_test.rb +52 -0
- data/test/local_validators_test.rb +71 -0
- data/test/s3_file_record_tes.rb +21 -0
- data/test/s3_file_string_tes.rb +24 -0
- data/test/s3_file_upload_tes.rb +31 -0
- data/test/s3_image_record_tes.rb +26 -0
- data/test/s3_image_string_tes.rb +31 -0
- data/test/s3_image_upload_tes.rb +28 -0
- data/test/s3_records_tes.rb +56 -0
- data/test/s3_validators_tes.rb +72 -0
- data/test/test_helper.rb +49 -0
- metadata +250 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module Attachs
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
|
4
|
+
config.attachs = ActiveSupport::OrderedOptions.new
|
5
|
+
config.attachs.presets = {}
|
6
|
+
config.attachs.default_presets = []
|
7
|
+
config.attachs.default_validations = {}
|
8
|
+
config.attachs.base_url = ''
|
9
|
+
config.attachs.storage = :local
|
10
|
+
|
11
|
+
initializer 'attachs' do
|
12
|
+
::ActiveRecord::Base.send :include, Attachs::ActiveRecord::Base
|
13
|
+
if config.attachs.storage == :s3
|
14
|
+
require 'aws-sdk'
|
15
|
+
Attachs::Storages::S3.config = YAML.load_file(Rails.root.join('config', 's3.yml'))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Attachs
|
2
|
+
module Storages
|
3
|
+
class Local
|
4
|
+
|
5
|
+
def initialize(default)
|
6
|
+
@tmp = (Rails.env.test? and !default)
|
7
|
+
end
|
8
|
+
|
9
|
+
def exists?(path)
|
10
|
+
::File.exists? realpath(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def size(path)
|
14
|
+
::File.size realpath(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def url(path)
|
18
|
+
::File.join Rails.application.config.attachs.base_url, path
|
19
|
+
end
|
20
|
+
|
21
|
+
def store(upload, path)
|
22
|
+
create_dir realpath(path)
|
23
|
+
upload.rewind # Hack to avoid empty files
|
24
|
+
::File.open(realpath(path), 'wb') do |file|
|
25
|
+
while chunk = upload.read(16 * 1024)
|
26
|
+
file.write(chunk)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete(path)
|
32
|
+
::File.delete realpath(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
def magick(source, output, upload)
|
36
|
+
create_dir realpath(output)
|
37
|
+
yield Attachs::Magick::Image.new(realpath(source), realpath(output))
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def tmp?
|
43
|
+
@tmp == true
|
44
|
+
end
|
45
|
+
|
46
|
+
def realpath(path)
|
47
|
+
base_path.join path
|
48
|
+
end
|
49
|
+
|
50
|
+
def base_path
|
51
|
+
Rails.root.join tmp? ? 'tmp' : 'public'
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_dir(path)
|
55
|
+
dir = base_path.join('uploads', path).dirname
|
56
|
+
FileUtils.mkdir_p dir unless ::File.directory? dir
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Attachs
|
2
|
+
module Storages
|
3
|
+
class S3
|
4
|
+
|
5
|
+
cattr_accessor :config
|
6
|
+
|
7
|
+
def initialize(default)
|
8
|
+
if (Rails.env.test? and !default)
|
9
|
+
env = 'test'
|
10
|
+
elsif default
|
11
|
+
env = 'production'
|
12
|
+
else
|
13
|
+
env = Rails.env
|
14
|
+
end
|
15
|
+
config = self.class.config[env]
|
16
|
+
AWS.config access_key_id: config['access_key_id'], secret_access_key: config['secret_access_key']
|
17
|
+
@bucket = AWS::S3.new.buckets[config['bucket']]
|
18
|
+
end
|
19
|
+
|
20
|
+
def exists?(path)
|
21
|
+
object(path).exists?
|
22
|
+
end
|
23
|
+
|
24
|
+
def size(path)
|
25
|
+
object(path).content_length
|
26
|
+
end
|
27
|
+
|
28
|
+
def url(path)
|
29
|
+
base_url = Rails.application.config.attachs.base_url
|
30
|
+
base_url.present? ? ::File.join(base_url, path) : object(path).public_url(secure: false)
|
31
|
+
end
|
32
|
+
|
33
|
+
def store(upload, path)
|
34
|
+
bucket.objects.create(path, Pathname.new(upload.path)).acl = :public_read
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(path)
|
38
|
+
object(path).delete
|
39
|
+
end
|
40
|
+
|
41
|
+
def magick(source, output, upload)
|
42
|
+
if cache[source].blank?
|
43
|
+
if upload.present?
|
44
|
+
cache[source] = upload.path
|
45
|
+
else
|
46
|
+
remote = create_tmp_file
|
47
|
+
remote.binmode
|
48
|
+
object(source).read { |chunk| remote.write(chunk) }
|
49
|
+
remote.close
|
50
|
+
remote.open
|
51
|
+
cache[source] = remote.path
|
52
|
+
end
|
53
|
+
end
|
54
|
+
tmp = create_tmp_file
|
55
|
+
yield Attachs::Magick::Image.new(cache[source], tmp.path)
|
56
|
+
store tmp, output
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
attr_reader :bucket
|
62
|
+
|
63
|
+
def create_tmp_file
|
64
|
+
Tempfile.new('s3')
|
65
|
+
end
|
66
|
+
|
67
|
+
def cache
|
68
|
+
@cache ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def object(path)
|
72
|
+
bucket.objects[path]
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Attachs
|
2
|
+
module Types
|
3
|
+
class File
|
4
|
+
|
5
|
+
def initialize(source, options={})
|
6
|
+
if source.is_a? ActionDispatch::Http::UploadedFile or source.is_a? Rack::Test::UploadedFile
|
7
|
+
@upload = source
|
8
|
+
@stored = false
|
9
|
+
@default = false
|
10
|
+
@storage = build_storage(:local)
|
11
|
+
elsif source.is_a? String
|
12
|
+
@upload = false
|
13
|
+
@filename = source
|
14
|
+
@stored = true
|
15
|
+
@default = false
|
16
|
+
@storage = build_storage
|
17
|
+
elsif options.has_key? :default
|
18
|
+
@upload = false
|
19
|
+
@filename = options[:default]
|
20
|
+
@stored = true
|
21
|
+
@default = true
|
22
|
+
@storage = build_storage
|
23
|
+
end
|
24
|
+
@deleted = false
|
25
|
+
@options = options
|
26
|
+
end
|
27
|
+
|
28
|
+
def default?
|
29
|
+
@default == true
|
30
|
+
end
|
31
|
+
|
32
|
+
def stored?
|
33
|
+
@stored == true
|
34
|
+
end
|
35
|
+
|
36
|
+
def deleted?
|
37
|
+
@deleted == true
|
38
|
+
end
|
39
|
+
|
40
|
+
def exists?(*args)
|
41
|
+
return false if deleted?
|
42
|
+
storage.exists? path(*args)
|
43
|
+
end
|
44
|
+
|
45
|
+
def size(*args)
|
46
|
+
return 0 if deleted?
|
47
|
+
storage.size path(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def extname
|
51
|
+
return nil if deleted?
|
52
|
+
@extname ||= ::File.extname(filename)
|
53
|
+
end
|
54
|
+
|
55
|
+
def filename
|
56
|
+
return nil if deleted?
|
57
|
+
@filename ||= "#{(Time.now.to_f * 10000000).to_i}#{::File.extname upload.original_filename}".downcase
|
58
|
+
end
|
59
|
+
|
60
|
+
def path(*args)
|
61
|
+
return nil if deleted?
|
62
|
+
stored? ? destination_path(*args) : upload.path
|
63
|
+
end
|
64
|
+
|
65
|
+
def url(*args)
|
66
|
+
return nil if deleted? or not stored?
|
67
|
+
storage.url path(*args)
|
68
|
+
end
|
69
|
+
|
70
|
+
def store
|
71
|
+
if not stored? and upload?
|
72
|
+
@storage = build_storage
|
73
|
+
storage.store upload, destination_path
|
74
|
+
yield if block_given?
|
75
|
+
@stored = true
|
76
|
+
@deleted = false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def delete
|
81
|
+
if not default? and stored? and exists?
|
82
|
+
storage.delete path
|
83
|
+
yield if block_given?
|
84
|
+
@storage = build_storage(:local) if upload?
|
85
|
+
@stored = false
|
86
|
+
@deleted = true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
attr_reader :upload, :storage, :options
|
93
|
+
|
94
|
+
def build_storage(type=nil)
|
95
|
+
type ||= Rails.application.config.attachs.storage
|
96
|
+
Attachs::Storages.const_get(type.to_s.classify).new default?
|
97
|
+
end
|
98
|
+
|
99
|
+
def upload?
|
100
|
+
upload.present?
|
101
|
+
end
|
102
|
+
|
103
|
+
def destination_path(*args)
|
104
|
+
::File.join 'uploads', store_path(*args), filename
|
105
|
+
end
|
106
|
+
|
107
|
+
def store_path(*args)
|
108
|
+
'files'
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Attachs
|
2
|
+
module Types
|
3
|
+
class Image < File
|
4
|
+
|
5
|
+
def presets
|
6
|
+
Rails.application.config.attachs.default_presets | (options[:presets] || [])
|
7
|
+
end
|
8
|
+
|
9
|
+
def store
|
10
|
+
super { presets.each { |name| generate_preset name } if presets.any? }
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete
|
14
|
+
super { presets.each { |name| storage.delete path(name) if exists?(name) } if presets.any? }
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_preset(name)
|
18
|
+
storage.magick destination_path, destination_path(name), upload do |image|
|
19
|
+
settings = Rails.application.config.attachs.presets[name]
|
20
|
+
if settings.respond_to? :call
|
21
|
+
settings.call image
|
22
|
+
else
|
23
|
+
image.send "resize_to_#{settings[:method] || 'fill'}", settings[:width], settings[:height]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_preset(name)
|
29
|
+
storage.delete path(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def store_path(*args)
|
35
|
+
::File.join('images', (args[0] ? args[0].to_s.gsub('_', '-') : 'original'))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class AttachmentContentTypeValidator < Attachs::Validators::Base
|
2
|
+
|
3
|
+
def validate_each(record, attribute, value)
|
4
|
+
if value.present? and not value.default?
|
5
|
+
unless options[:in].include? value.extname.from(1)
|
6
|
+
add_error record, attribute, 'errors.messages.attachment_content_type', types: options[:in].to_sentence
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class AttachmentSizeValidator < Attachs::Validators::Base
|
2
|
+
include ActionView::Helpers::NumberHelper
|
3
|
+
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
if value.present? and not value.default?
|
6
|
+
if options.has_key? :in
|
7
|
+
unless options[:in].include? value.size
|
8
|
+
add_error record, attribute, 'attachment_size_in', min: number_to_human_size(options[:in].begin), max: number_to_human_size(options[:in].end)
|
9
|
+
end
|
10
|
+
else
|
11
|
+
if options.has_key? :less_than and value.size > options[:less_than]
|
12
|
+
add_error record, attribute, 'attachment_size_less_than', count: number_to_human_size(options[:less_than])
|
13
|
+
end
|
14
|
+
if options.has_key? :greater_than and value.size < options[:greater_than]
|
15
|
+
add_error record, attribute, 'attachment_size_greater_than', count: number_to_human_size(options[:greater_than])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def add_error(record, attribute, type, options)
|
24
|
+
super(record, attribute, "errors.messages.#{type}", options)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
development:
|
3
|
+
bucket: development-bucket
|
4
|
+
access_key_id: development_access_key_id
|
5
|
+
secret_access_key: development_secret_access_key
|
6
|
+
|
7
|
+
test:
|
8
|
+
bucket: test-bucket
|
9
|
+
access_key_id: test_access_key_id
|
10
|
+
secret_access_key: test_secret_access_key
|
11
|
+
|
12
|
+
production:
|
13
|
+
bucket: production-bucket
|
14
|
+
access_key_id: production_access_key_id
|
15
|
+
secret_access_key: production_secret_access_key
|
16
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Attachs
|
2
|
+
module Task
|
3
|
+
|
4
|
+
def self.iterate_images
|
5
|
+
model = ENV['MODEL'].classify.constantize
|
6
|
+
presets = ENV['PRESETS'].split(',').map(&:to_sym)
|
7
|
+
model.find_each do |record|
|
8
|
+
model.attachments.each do |attr, options|
|
9
|
+
if options[:type] == :image
|
10
|
+
presets.each do |preset|
|
11
|
+
image = record.send(attr)
|
12
|
+
yield image, preset
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
namespace :uploads do
|
23
|
+
|
24
|
+
namespace :presets do
|
25
|
+
desc 'Refresh preset'
|
26
|
+
task refresh: :clean do
|
27
|
+
Attachs::Task.iterate_images do |image, preset|
|
28
|
+
puts "Generating preset #{image.url(preset)}."
|
29
|
+
image.generate_preset preset
|
30
|
+
end
|
31
|
+
puts "Presets refreshed successfully."
|
32
|
+
end
|
33
|
+
desc 'Clean preset'
|
34
|
+
task clean: :environment do
|
35
|
+
Attachs::Task.iterate_images do |image, preset|
|
36
|
+
puts "Deleting preset #{image.url(preset)}."
|
37
|
+
image.delete_preset preset
|
38
|
+
end
|
39
|
+
puts "Presets cleaned successfully."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
namespace :s3 do
|
44
|
+
namespace :buckets do
|
45
|
+
desc 'Create buckets'
|
46
|
+
task :create do
|
47
|
+
config = YAML.load_file(Rails.root.join('config', 's3.yml'))
|
48
|
+
config.each do |env, options|
|
49
|
+
require 'aws-sdk'
|
50
|
+
AWS.config access_key_id: options['access_key_id'], secret_access_key: options['secret_access_key']
|
51
|
+
bucket = options['bucket']
|
52
|
+
begin
|
53
|
+
AWS::S3.new.buckets.create bucket
|
54
|
+
rescue AWS::S3::Errors::InvalidAccessKeyId
|
55
|
+
puts "Invalid credentials in #{env} environment."
|
56
|
+
next
|
57
|
+
rescue AWS::S3::Errors::BucketAlreadyExists
|
58
|
+
puts "Bucket #{bucket} already exists."
|
59
|
+
next
|
60
|
+
rescue AWS::S3::Errors::BucketAlreadyOwnedByYou
|
61
|
+
puts "You already own bucket #{bucket}."
|
62
|
+
next
|
63
|
+
end
|
64
|
+
puts "Bucket #{bucket} created successfully."
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|