attachs 0.3.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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +98 -0
  4. data/Rakefile +34 -0
  5. data/app/controllers/attachs/presets_controller.rb +17 -0
  6. data/config/locales/en.yml +8 -0
  7. data/config/locales/es.yml +8 -0
  8. data/config/routes.rb +5 -0
  9. data/lib/attachs.rb +16 -0
  10. data/lib/attachs/active_record/base.rb +145 -0
  11. data/lib/attachs/engine.rb +7 -0
  12. data/lib/attachs/magick/image.rb +85 -0
  13. data/lib/attachs/railtie.rb +20 -0
  14. data/lib/attachs/storages/local.rb +61 -0
  15. data/lib/attachs/storages/s3.rb +77 -0
  16. data/lib/attachs/types/file.rb +113 -0
  17. data/lib/attachs/types/image.rb +40 -0
  18. data/lib/attachs/validators/attachment_content_type_validator.rb +11 -0
  19. data/lib/attachs/validators/attachment_presence_validator.rb +9 -0
  20. data/lib/attachs/validators/attachment_size_validator.rb +27 -0
  21. data/lib/attachs/validators/base.rb +13 -0
  22. data/lib/attachs/version.rb +5 -0
  23. data/lib/generators/attachs/s3/config_generator.rb +13 -0
  24. data/lib/generators/attachs/s3/templates/s3.yml +16 -0
  25. data/lib/tasks/attachs_tasks.rake +71 -0
  26. data/test/dummy/README.rdoc +28 -0
  27. data/test/dummy/Rakefile +6 -0
  28. data/test/dummy/app/assets/javascripts/application.js +13 -0
  29. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  30. data/test/dummy/app/controllers/application_controller.rb +5 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/models/all_attached.rb +16 -0
  33. data/test/dummy/app/models/file_attached.rb +3 -0
  34. data/test/dummy/app/models/image_attached.rb +3 -0
  35. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  36. data/test/dummy/bin/bundle +3 -0
  37. data/test/dummy/bin/rails +4 -0
  38. data/test/dummy/bin/rake +4 -0
  39. data/test/dummy/config.ru +4 -0
  40. data/test/dummy/config/application.rb +29 -0
  41. data/test/dummy/config/boot.rb +5 -0
  42. data/test/dummy/config/database.yml +25 -0
  43. data/test/dummy/config/environment.rb +5 -0
  44. data/test/dummy/config/environments/development.rb +29 -0
  45. data/test/dummy/config/environments/production.rb +80 -0
  46. data/test/dummy/config/environments/test.rb +36 -0
  47. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  48. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  49. data/test/dummy/config/initializers/inflections.rb +16 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +12 -0
  52. data/test/dummy/config/initializers/session_store.rb +3 -0
  53. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  54. data/test/dummy/config/locales/en.yml +23 -0
  55. data/test/dummy/config/routes.rb +3 -0
  56. data/test/dummy/db/development.sqlite3 +0 -0
  57. data/test/dummy/db/migrate/20130820222342_create_file_attacheds.rb +9 -0
  58. data/test/dummy/db/migrate/20130820222355_create_image_attacheds.rb +9 -0
  59. data/test/dummy/db/migrate/20130820222534_create_all_attacheds.rb +19 -0
  60. data/test/dummy/db/schema.rb +44 -0
  61. data/test/dummy/db/test.sqlite3 +0 -0
  62. data/test/dummy/log/development.log +28 -0
  63. data/test/dummy/log/test.log +8545 -0
  64. data/test/dummy/public/404.html +58 -0
  65. data/test/dummy/public/422.html +58 -0
  66. data/test/dummy/public/500.html +57 -0
  67. data/test/dummy/public/favicon.ico +0 -0
  68. data/test/dummy/public/uploads/files/file.txt +1 -0
  69. data/test/dummy/public/uploads/images/big/image.jpg +0 -0
  70. data/test/dummy/public/uploads/images/original/image.jpg +0 -0
  71. data/test/dummy/public/uploads/images/small/image.jpg +0 -0
  72. data/test/dummy/test/fixtures/file.txt +1 -0
  73. data/test/dummy/test/fixtures/file_big.txt +1 -0
  74. data/test/dummy/test/fixtures/file_empty.txt +1 -0
  75. data/test/dummy/test/fixtures/image.jpg +0 -0
  76. data/test/dummy/test/fixtures/image_big.jpg +0 -0
  77. data/test/dummy/test/fixtures/image_empty.jpg +0 -0
  78. data/test/local_file_record_test.rb +20 -0
  79. data/test/local_file_string_test.rb +23 -0
  80. data/test/local_file_upload_test.rb +30 -0
  81. data/test/local_generate_test.rb +25 -0
  82. data/test/local_image_record_test.rb +25 -0
  83. data/test/local_image_string_test.rb +34 -0
  84. data/test/local_image_upload_test.rb +27 -0
  85. data/test/local_records_test.rb +52 -0
  86. data/test/local_validators_test.rb +71 -0
  87. data/test/s3_file_record_tes.rb +21 -0
  88. data/test/s3_file_string_tes.rb +24 -0
  89. data/test/s3_file_upload_tes.rb +31 -0
  90. data/test/s3_image_record_tes.rb +26 -0
  91. data/test/s3_image_string_tes.rb +31 -0
  92. data/test/s3_image_upload_tes.rb +28 -0
  93. data/test/s3_records_tes.rb +56 -0
  94. data/test/s3_validators_tes.rb +72 -0
  95. data/test/test_helper.rb +49 -0
  96. 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,9 @@
1
+ class AttachmentPresenceValidator < Attachs::Validators::Base
2
+
3
+ def validate_each(record, attribute, value)
4
+ unless value.present? and value.exists? and not value.default?
5
+ add_error record, attribute, 'errors.messages.attachment_presence'
6
+ end
7
+ end
8
+
9
+ 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,13 @@
1
+ module Attachs
2
+ module Validators
3
+ class Base < ActiveModel::EachValidator
4
+
5
+ protected
6
+
7
+ def add_error(record, attribute, type, options={})
8
+ record.errors[attribute] << (options[:message] || I18n.t(type, options))
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module Attachs
2
+
3
+ VERSION = '0.3.0'
4
+
5
+ end
@@ -0,0 +1,13 @@
1
+ module Attachs
2
+ module S3
3
+ class ConfigGenerator < Rails::Generators::Base
4
+
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ def install
8
+ copy_file 's3.yml', 'config/s3.yml'
9
+ end
10
+
11
+ end
12
+ end
13
+ 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
+