attachs 0.4.5 → 4.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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