attachs 0.4.5 → 4.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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +83 -218
  4. data/Rakefile +1 -14
  5. data/lib/attachs.rb +98 -31
  6. data/lib/attachs/attachment.rb +301 -72
  7. data/lib/attachs/builder.rb +78 -0
  8. data/lib/attachs/collection.rb +84 -0
  9. data/lib/attachs/concern.rb +47 -0
  10. data/lib/attachs/configuration.rb +11 -0
  11. data/lib/attachs/console.rb +40 -0
  12. data/lib/attachs/extensions/active_record/base.rb +24 -0
  13. data/lib/attachs/extensions/active_record/validations/attachment_content_type_validator.rb +37 -0
  14. data/lib/attachs/extensions/active_record/validations/attachment_presence_validator.rb +27 -0
  15. data/lib/attachs/extensions/active_record/validations/attachment_size_validator.rb +55 -0
  16. data/lib/attachs/extensions/active_record/validations/attachment_validator.rb +24 -0
  17. data/lib/attachs/interpolations.rb +27 -0
  18. data/lib/attachs/jobs/base.rb +14 -0
  19. data/lib/attachs/jobs/delete_job.rb +15 -0
  20. data/lib/attachs/jobs/update_job.rb +11 -0
  21. data/lib/attachs/locales/en.yml +2 -6
  22. data/lib/attachs/locales/es.yml +2 -6
  23. data/lib/attachs/processors/base.rb +4 -14
  24. data/lib/attachs/processors/image.rb +45 -0
  25. data/lib/attachs/railtie.rb +7 -27
  26. data/lib/attachs/storages/base.rb +10 -59
  27. data/lib/attachs/storages/s3.rb +68 -63
  28. data/lib/attachs/version.rb +1 -1
  29. data/lib/generators/attachs/install/install_generator.rb +15 -0
  30. data/lib/generators/attachs/install/templates/initializer.rb +7 -0
  31. data/lib/generators/attachs/upload/templates/migration.rb +11 -0
  32. data/lib/generators/attachs/upload/templates/model.rb +19 -0
  33. data/lib/generators/attachs/upload/upload_generator.rb +24 -0
  34. data/lib/tasks/attachs.rake +12 -26
  35. data/test/attachment_test.rb +175 -12
  36. data/test/collection_test.rb +52 -0
  37. data/test/concern_test.rb +10 -0
  38. data/test/dummy/Rakefile +1 -2
  39. data/test/dummy/app/assets/javascripts/application.js +2 -2
  40. data/test/dummy/app/assets/stylesheets/application.css +6 -4
  41. data/test/dummy/app/models/product.rb +16 -0
  42. data/test/dummy/app/models/shop.rb +14 -0
  43. data/test/dummy/app/models/upload.rb +19 -0
  44. data/test/dummy/app/views/layouts/application.html.erb +9 -11
  45. data/test/dummy/bin/bundle +1 -0
  46. data/test/dummy/bin/rails +2 -1
  47. data/test/dummy/bin/rake +1 -0
  48. data/test/dummy/bin/setup +30 -0
  49. data/test/dummy/config.ru +1 -1
  50. data/test/dummy/config/application.rb +3 -2
  51. data/test/dummy/config/boot.rb +1 -1
  52. data/test/dummy/config/database.yml +4 -22
  53. data/test/dummy/config/database.yml.travis +3 -0
  54. data/test/dummy/config/environment.rb +1 -1
  55. data/test/dummy/config/environments/development.rb +15 -3
  56. data/test/dummy/config/environments/production.rb +21 -26
  57. data/test/dummy/config/environments/test.rb +10 -12
  58. data/test/dummy/config/initializers/assets.rb +11 -0
  59. data/test/dummy/config/initializers/attachs.rb +7 -21
  60. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  61. data/test/dummy/config/initializers/mime_types.rb +1 -2
  62. data/test/dummy/config/initializers/session_store.rb +1 -1
  63. data/test/dummy/config/routes.rb +54 -0
  64. data/test/dummy/config/secrets.yml +24 -0
  65. data/test/dummy/db/migrate/20161024234003_create_shops.rb +11 -0
  66. data/test/dummy/db/migrate/20161024234029_create_products.rb +12 -0
  67. data/test/dummy/db/migrate/20161031135453_create_uploads.rb +11 -0
  68. data/test/dummy/db/schema.rb +27 -16
  69. data/test/dummy/log/development.log +61 -0
  70. data/test/dummy/log/test.log +5873 -0
  71. data/test/dummy/public/404.html +58 -55
  72. data/test/dummy/public/422.html +58 -55
  73. data/test/dummy/public/500.html +57 -54
  74. data/test/fixtures/big_file.txt +50 -0
  75. data/test/fixtures/big_image.jpg +0 -0
  76. data/test/fixtures/file.txt +1 -0
  77. data/test/fixtures/image.jpg +0 -0
  78. data/test/{dummy/test/fixtures/file.txt → fixtures/small_file.txt} +0 -0
  79. data/test/fixtures/small_image.jpg +0 -0
  80. data/test/generator_test.rb +14 -6
  81. data/test/helper_test.rb +28 -0
  82. data/test/query_test.rb +26 -0
  83. data/test/support/storage_helper.rb +65 -0
  84. data/test/task_test.rb +69 -0
  85. data/test/test_helper.rb +6 -45
  86. data/test/upload_test.rb +27 -0
  87. data/test/validator_test.rb +160 -0
  88. metadata +114 -68
  89. data/lib/attachs/active_record/base.rb +0 -103
  90. data/lib/attachs/active_record/connection_adapters.rb +0 -40
  91. data/lib/attachs/active_record/migration.rb +0 -17
  92. data/lib/attachs/active_record/validators.rb +0 -7
  93. data/lib/attachs/active_record/validators/attachment_content_type_validator.rb +0 -30
  94. data/lib/attachs/active_record/validators/attachment_presence_validator.rb +0 -22
  95. data/lib/attachs/active_record/validators/attachment_size_validator.rb +0 -47
  96. data/lib/attachs/processors/thumbnail.rb +0 -69
  97. data/lib/attachs/storages/local.rb +0 -95
  98. data/lib/attachs/types/base.rb +0 -22
  99. data/lib/attachs/types/default.rb +0 -29
  100. data/lib/attachs/types/regular.rb +0 -21
  101. data/lib/generators/attachs/install_generator.rb +0 -13
  102. data/lib/generators/attachs/templates/attachs.rb +0 -2
  103. data/test/dummy/README.rdoc +0 -28
  104. data/test/dummy/app/models/medium.rb +0 -5
  105. data/test/dummy/app/models/picture.rb +0 -2
  106. data/test/dummy/config/initializers/secret_token.rb +0 -13
  107. data/test/dummy/config/s3.yml +0 -15
  108. data/test/dummy/db/migrate/20140808012639_create_media.rb +0 -11
  109. data/test/dummy/test/fixtures/image.gif +0 -0
  110. data/test/local_storage_test.rb +0 -72
  111. data/test/migration_test.rb +0 -65
  112. data/test/processor_test.rb +0 -49
  113. data/test/s3_storage_tes.rb +0 -66
  114. data/test/tasks_test.rb +0 -57
  115. data/test/validators_test.rb +0 -100
@@ -1,118 +1,347 @@
1
1
  module Attachs
2
2
  class Attachment
3
+ include ActiveModel::Validations
3
4
 
4
- attr_reader :record, :attribute, :filename, :content_type, :size, :updated_at, :upload, :options
5
+ attr_reader :record, :record_attribute, :options, :attributes, :value
5
6
 
6
- delegate :basename, :extension, :image?, :url, :process, :destroy, :update, to: :type
7
-
8
- def initialize(record, attribute, options, source=false)
7
+ def initialize(record, record_attribute, options={}, attributes={})
9
8
  @record = record
10
- @attribute = attribute
9
+ @record_attribute = record_attribute
11
10
  @options = options
12
- case source
13
- when nil
14
- %w(filename content_type size updated_at).each do |name|
15
- record.send "#{attribute}_#{name}=", nil
11
+ @original_attributes = @attributes = normalize_attributes(attributes)
12
+ end
13
+
14
+ %i(id size filename content_type uploaded_at).each do |name|
15
+ define_method name do
16
+ attributes[name]
17
+ end
18
+ end
19
+
20
+ def basename
21
+ if filename
22
+ File.basename filename, extension
23
+ end
24
+ end
25
+
26
+ def extension
27
+ if filename
28
+ File.extname filename
29
+ end
30
+ end
31
+
32
+ def present?
33
+ filename.present? && content_type.present? && size.present?
34
+ end
35
+
36
+ def blank?
37
+ !present?
38
+ end
39
+
40
+ def persisted?
41
+ record.persisted? && paths.any? && uploaded_at
42
+ end
43
+
44
+ def changed?
45
+ @original_attributes != @attributes
46
+ end
47
+
48
+ def type
49
+ if content_type
50
+ if content_type.starts_with?('image/')
51
+ 'image'
52
+ else
53
+ 'file'
54
+ end
55
+ end
56
+ end
57
+
58
+ def url(style=:original)
59
+ paths = attributes[:paths]
60
+ if paths.has_key?(style)
61
+ storage.url paths[style]
62
+ elsif options.has_key?(:default_path)
63
+ template = options[:default_path]
64
+ path = generate_path(template, style)
65
+ storage.url path
66
+ end
67
+ end
68
+
69
+ def position
70
+ if options[:multiple]
71
+ attributes[:position]
72
+ end
73
+ end
74
+
75
+ def position=(value)
76
+ if options[:multiple]
77
+ attributes[:position] = value
78
+ write_record
79
+ end
80
+ end
81
+
82
+ def assign(value)
83
+ source, new_attributes = process_value(value)
84
+ if new_attributes
85
+ unless changed?
86
+ @original_attributes = attributes
16
87
  end
17
- when false
18
- %w(filename content_type size updated_at).each do |name|
19
- instance_variable_set :"@#{name}", record.send("#{attribute}_#{name}")
88
+ @attributes = new_attributes
89
+ write_record
90
+ @source = source
91
+ @value = value
92
+ end
93
+ end
94
+ alias_method :value=, :assign
95
+
96
+ def _destroy=(value)
97
+ if ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
98
+ assign nil
99
+ end
100
+ end
101
+
102
+ def save
103
+ if changed?
104
+ if original_attributes[:paths].any? || original_attributes[:old_paths].any?
105
+ Jobs::DeleteJob.perform_later(
106
+ original_attributes[:paths].values +
107
+ original_attributes[:old_paths]
108
+ )
109
+ end
110
+ case source.class.name
111
+ when 'Attachs::Attachment'
112
+ file = storage.get(source.paths[:original])
113
+ storage.process file, paths, styles
114
+ when 'Upload'
115
+ source.file.paths.each do |style, path|
116
+ storage.copy path, paths[style]
117
+ end
118
+ when 'ActionDispatch::Http::UploadedFile'
119
+ storage.process source, paths, styles
20
120
  end
121
+ @source = @value = nil
122
+ @original_attributes = @attributes
123
+ elsif present?
124
+ if paths != generate_paths
125
+ Jobs::UpdateJob.perform_later record, record_attribute.to_s
126
+ end
127
+ end
128
+ end
129
+
130
+ def update_paths
131
+ if changed?
132
+ raise 'Save attachment before update paths'
21
133
  else
22
- load source
23
- %w(filename content_type size updated_at).each do |name|
24
- record.send "#{attribute}_#{name}=", send(name)
134
+ new_paths = generate_paths
135
+ if paths != new_paths
136
+ new_paths.each do |style, new_path|
137
+ original_path = paths[style]
138
+ if original_path != new_path
139
+ unless storage.exist?(new_path)
140
+ storage.copy original_path, new_path
141
+ end
142
+ attributes[:paths][style] = new_path
143
+ attributes[:old_paths] |= [original_path]
144
+ end
145
+ end
146
+ update_record
25
147
  end
26
148
  end
27
149
  end
28
150
 
29
- def uploaded!
30
- @upload = nil
151
+ def reprocess
152
+ if changed?
153
+ raise 'Save attachment before reprocess'
154
+ elsif present? && styles
155
+ file = storage.get(paths[:original])
156
+ paths.each do |style, path|
157
+ if storage.exist?(path)
158
+ storage.delete path
159
+ end
160
+ Rails.logger.info "Regenerating: #{style} => #{path}"
161
+ end
162
+ new_paths = generate_paths
163
+ storage.process file, new_paths, styles
164
+ attributes[:paths] = new_paths
165
+ update_record
166
+ end
31
167
  end
32
168
 
33
- def styles
34
- @styles ||= ((options[:styles] || []) | Attachs.config.global_styles)
169
+ def fix_missings
170
+ if changed?
171
+ raise 'Save attachment before fix missings'
172
+ elsif present? && styles
173
+ missings = paths.slice(:original)
174
+ paths.except(:original).each do |style, path|
175
+ unless storage.exist?(path)
176
+ missings[style] = path
177
+ Rails.logger.info "Generating: #{style} => #{path}"
178
+ end
179
+ end
180
+ if missings.size > 1
181
+ file = storage.get(paths[:original])
182
+ storage.process file, missings, styles
183
+ end
184
+ end
35
185
  end
36
186
 
37
- def private?
38
- @private ||= options[:private] == true
187
+ def destroy
188
+ assign nil
189
+ save
39
190
  end
40
191
 
41
- def processors
42
- @processors ||= (options[:processors] || Attachs.config.default_processors).map do |processor|
43
- "Attachs::Processors::#{processor.to_s.classify}".constantize
192
+ def method_missing(name, *args, &block)
193
+ if attributes.has_key?(name)
194
+ attributes[name]
195
+ else
196
+ super
44
197
  end
45
198
  end
46
199
 
47
- def public?
48
- !private?
200
+ def respond_to_missing?(name, include_private=false)
201
+ attributes.has_key?(name) || super
49
202
  end
50
203
 
51
- def upload?
52
- !upload.nil?
204
+ def persist
205
+ if changed? && present?
206
+ new_paths = generate_paths
207
+ if paths != new_paths
208
+ attributes[:paths] = new_paths
209
+ attributes[:uploaded_at] = Time.now
210
+ write_record
211
+ end
212
+ end
53
213
  end
54
214
 
55
- def processed?
56
- exists? and record.persisted? and !(
57
- record.send("#{attribute}_filename_changed?") or
58
- record.send("#{attribute}_content_type_changed?") or
59
- record.send("#{attribute}_size_changed?") or
60
- record.send("#{attribute}_updated_at_changed?")
61
- )
215
+ def unpersist
216
+ if changed? && present?
217
+ attributes[:paths] = original_attributes[:paths]
218
+ attributes[:uploaded_at] = original_attributes[:paths]
219
+ write_record
220
+ end
62
221
  end
63
222
 
64
- def exists?
65
- filename and content_type and size and updated_at
223
+ def styles
224
+ case value = options[:styles]
225
+ when Proc
226
+ value.call(record) || {}
227
+ when Hash
228
+ value
229
+ else
230
+ {}
231
+ end
66
232
  end
67
233
 
68
- def blank?
69
- !exists?
234
+ private
235
+
236
+ attr_reader :original_attributes, :source
237
+
238
+ def storage
239
+ Attachs.storage
70
240
  end
71
241
 
72
- def default?
73
- !options[:default_path].nil?
242
+ def normalize_attributes(attributes)
243
+ attributes.reverse_merge(paths: {}, old_paths: []).deep_symbolize_keys
74
244
  end
75
245
 
76
- def url?
77
- public? and (default? or processed?)
246
+ def write_record(value=nil)
247
+ unless record.destroyed?
248
+ record.send "#{record_attribute}_will_change!"
249
+ record.send :write_attribute, record_attribute, (value || raw_attributes)
250
+ end
78
251
  end
79
252
 
80
- protected
253
+ def raw_attributes
254
+ if options[:multiple]
255
+ record.send(:read_attribute, record_attribute).reject{ |h| h['id'] == id }.append attributes
256
+ else
257
+ attributes
258
+ end
259
+ end
81
260
 
82
- def load(source)
83
- if source.is_a? URI
84
- download = Tempfile.new('external')
85
- File.open(download.path, 'wb') do |file|
86
- Net::HTTP.start(source.host) do |http|
87
- file.write http.get(source.path).body
88
- end
261
+ def update_record(value=nil)
262
+ unless record.destroyed?
263
+ record.update_column record_attribute, (value || raw_attributes)
264
+ end
265
+ end
266
+
267
+ def generate_path(template, style)
268
+ if path = template.try(:dup)
269
+ path.gsub! ':style', style.to_s.gsub('_', '-')
270
+ path.scan(/:[a-z_]+/).each do |token|
271
+ name = token.from(1).to_sym
272
+ path.gsub! token, interpolate(name).to_s.parameterize
89
273
  end
90
- type = `file --mime-type -b '#{download.path}'`.strip
91
- source = ActionDispatch::Http::UploadedFile.new(
92
- tempfile: download,
93
- filename: File.basename(source.path),
94
- type: type
95
- )
96
- end
97
- if source.is_a? Attachs::Attachment
98
- @filename = source.filename
99
- @updated_at = source.updated_at
274
+ path.squeeze! '-'
275
+ path.squeeze! '/'
276
+ path.gsub! '-.', '.'
277
+ path.gsub! '/-', '/'
278
+ path.gsub! '-/', '/'
279
+ path.sub! /^\//, ''
280
+ path
281
+ end
282
+ end
283
+
284
+ def generate_paths
285
+ template = options[:path]
286
+ paths = { original: generate_path(template, :original) }
287
+ styles.each do |style, geometry|
288
+ paths[style] = generate_path(template, style)
289
+ end
290
+ paths
291
+ end
292
+
293
+ def interpolate(name)
294
+ if %i(basename extension).include?(name)
295
+ send name
296
+ elsif name == :attribute
297
+ record_attribute
298
+ elsif attributes.except(:upload_at, :paths, :old_paths).has_key?(name)
299
+ attributes[name]
100
300
  else
101
- @upload = source
102
- @filename = source.original_filename.downcase
103
- @updated_at = Time.zone.now
301
+ Attachs.interpolations.find(name).call record
104
302
  end
105
- @content_type = source.content_type
106
- @size = source.size
107
303
  end
108
304
 
109
- def type
110
- @type ||= begin
111
- if exists?
112
- Attachs::Types::Regular.new(self)
113
- else
114
- Attachs::Types::Default.new(self)
305
+ def generate_id
306
+ SecureRandom.uuid.gsub '-', ''
307
+ end
308
+
309
+ def process_value(value)
310
+ case value.class.name
311
+ when 'NilClass'
312
+ [nil, {}]
313
+ when 'Attachs::Attachment'
314
+ attributes = value.attributes.merge(id: generate_id)
315
+ [value, attributes]
316
+ when 'Upload'
317
+ attributes = value.file.attributes.merge(id: generate_id)
318
+ [value.file, attributes]
319
+ when 'String','Fixnum','Bignum'
320
+ if Rails.configuration.cache_classes == false
321
+ Rails.application.eager_load!
322
+ end
323
+ if defined?(Upload)
324
+ upload = Upload.find(value)
325
+ attributes = upload.file.attributes.merge(id: generate_id)
326
+ [upload, attributes]
327
+ end
328
+ when 'ActionDispatch::Http::UploadedFile'
329
+ attributes = {
330
+ id: generate_id,
331
+ filename: value.original_filename,
332
+ content_type: value.content_type,
333
+ size: value.size.to_i,
334
+ uploaded_at: Time.now,
335
+ paths: {},
336
+ old_paths: []
337
+ }
338
+ if value.content_type.starts_with?('image/')
339
+ width, height = Console.find_dimensions(value.path)
340
+ attributes[:width] = width
341
+ attributes[:height] = height
342
+ attributes[:ratio] = (height.to_d / width.to_d).to_f
115
343
  end
344
+ [value, attributes]
116
345
  end
117
346
  end
118
347
 
@@ -0,0 +1,78 @@
1
+ module Attachs
2
+ class Builder
3
+
4
+ attr_reader :model, :concern
5
+
6
+ def initialize(model)
7
+ @model = model
8
+ @concern = Module.new
9
+ end
10
+
11
+ def define(attribute, options={})
12
+ unless options.has_key?(:path)
13
+ raise 'Path required'
14
+ end
15
+ model.include Attachs::Concern
16
+ model.attachments[attribute] = options
17
+ define_writer attribute
18
+ define_reader attribute, options
19
+ define_attributes_writer attribute, options
20
+ model.include concern
21
+ end
22
+
23
+ private
24
+
25
+ def define_writer(attribute)
26
+ concern.class_eval do
27
+ define_method "#{attribute}=" do |value|
28
+ send(attribute).assign value
29
+ end
30
+ end
31
+ end
32
+
33
+ def define_reader(attribute, options)
34
+ concern.class_eval do
35
+ define_method attribute do
36
+ variable = "@#{attribute}"
37
+ if instance = instance_variable_get(variable)
38
+ instance
39
+ else
40
+ if options[:multiple] == true
41
+ klass = Attachs::Collection
42
+ else
43
+ klass = Attachs::Attachment
44
+ end
45
+ instance_variable_set(
46
+ variable,
47
+ klass.new(self, attribute, options, super())
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def define_attributes_writer(attribute, options)
55
+ concern.class_eval do
56
+ define_method "#{attribute}_attributes=" do |collection_or_attributes|
57
+ if options[:multiple] == true
58
+ collection_or_attributes.each do |attributes|
59
+ if id = attributes.delete(:id)
60
+ attachment = send(attribute).find(id)
61
+ else
62
+ attachment = send(attribute).new
63
+ end
64
+ attributes.each do |name, value|
65
+ attachment.send "#{name}=", value
66
+ end
67
+ end
68
+ else
69
+ collection_or_attributes.each do |name, value|
70
+ send(attribute).send "#{name}=", value
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end