saviour 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +491 -0
  7. data/Rakefile +6 -0
  8. data/lib/saviour.rb +170 -0
  9. data/lib/saviour/base_uploader.rb +81 -0
  10. data/lib/saviour/config.rb +13 -0
  11. data/lib/saviour/file.rb +124 -0
  12. data/lib/saviour/local_storage.rb +72 -0
  13. data/lib/saviour/processors/digest.rb +16 -0
  14. data/lib/saviour/s3_storage.rb +77 -0
  15. data/lib/saviour/string_source.rb +15 -0
  16. data/lib/saviour/uploader/element.rb +19 -0
  17. data/lib/saviour/uploader/processors_runner.rb +88 -0
  18. data/lib/saviour/uploader/store_dir_extractor.rb +41 -0
  19. data/lib/saviour/url_source.rb +55 -0
  20. data/lib/saviour/version.rb +3 -0
  21. data/saviour.gemspec +26 -0
  22. data/spec/feature/access_to_model_and_mounted_as.rb +30 -0
  23. data/spec/feature/crud_workflows_spec.rb +138 -0
  24. data/spec/feature/persisted_path_spec.rb +35 -0
  25. data/spec/feature/reload_model_spec.rb +25 -0
  26. data/spec/feature/validations_spec.rb +172 -0
  27. data/spec/feature/versions_spec.rb +186 -0
  28. data/spec/models/base_uploader_spec.rb +396 -0
  29. data/spec/models/config_spec.rb +16 -0
  30. data/spec/models/file_spec.rb +210 -0
  31. data/spec/models/local_storage_spec.rb +154 -0
  32. data/spec/models/processors/digest_spec.rb +22 -0
  33. data/spec/models/s3_storage_spec.rb +170 -0
  34. data/spec/models/saviour_spec.rb +80 -0
  35. data/spec/models/url_source_spec.rb +76 -0
  36. data/spec/spec_helper.rb +93 -0
  37. data/spec/support/data/camaloon.jpg +0 -0
  38. data/spec/support/data/example.xml +21 -0
  39. data/spec/support/data/text.txt +1 -0
  40. data/spec/support/models.rb +3 -0
  41. data/spec/support/schema.rb +9 -0
  42. metadata +196 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 58fead9b2cc6702062727d40824f65b016445a6d
4
+ data.tar.gz: 5a6a218dae027315fce70761c2b39035cd243bcb
5
+ SHA512:
6
+ metadata.gz: 58f13bfb42bb55782bcade9e8fefb67e5d697eb614cd181cca89eaa6443b3166dbbd73d70a53ce24af864de2d9d5f918c8dd48a13f1eb1a96fd821d1d8267a95
7
+ data.tar.gz: c1d3737b8414cae6610d4c33232514aa74e99b1a5e948b887f2aa32de0b9bd5629b644478e269726053901590e16452af717486fcdb95f4d6bc1fb43ff393495
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ spec/support/test_data_dir/*
19
+ .idea
20
+ .rbenv-gemsets
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.8
5
+ - 2.2.4
6
+ - 2.3.0
7
+
8
+ addons:
9
+ code_climate:
10
+ repo_token: abb288da5fac3efc45be30ffb37085314b9189ddccedf2cc68282777477e21c5
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in saviour.gemspec
4
+ gemspec
5
+
6
+ gem "codeclimate-test-reporter", group: :test, require: nil
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Roger Campos
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,491 @@
1
+ [![Build Status](https://travis-ci.org/rogercampos/saviour.svg?branch=master)](https://travis-ci.org/rogercampos/saviour)
2
+ [![Code Climate](https://codeclimate.com/github/rogercampos/saviour/badges/gpa.svg)](https://codeclimate.com/github/rogercampos/saviour)
3
+ [![Test Coverage](https://codeclimate.com/github/rogercampos/saviour/badges/coverage.svg)](https://codeclimate.com/github/rogercampos/saviour/coverage)
4
+
5
+ # Saviour
6
+
7
+ This is a small library that handles file uploads and nothing more. It integrates with ActiveRecord and manages file
8
+ storage following the active record instance lifecycle.
9
+
10
+
11
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
12
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
13
+
14
+
15
+ - [Intro](#intro)
16
+ - [Basic usage example](#basic-usage-example)
17
+ - [File API](#file-api)
18
+ - [Storage abstraction](#storage-abstraction)
19
+ - [public_url](#public_url)
20
+ - [LocalStorage](#localstorage)
21
+ - [S3Storage](#s3storage)
22
+ - [Source abstraction](#source-abstraction)
23
+ - [StringSource](#stringsource)
24
+ - [UrlSource](#urlsource)
25
+ - [Uploader classes and Processors](#uploader-classes-and-processors)
26
+ - [store_dir](#store_dir)
27
+ - [Accessing model and attached_as](#accessing-model-and-attached_as)
28
+ - [Processors](#processors)
29
+ - [Versions](#versions)
30
+ - [Validations](#validations)
31
+ - [Active Record Lifecycle integration](#active-record-lifecycle-integration)
32
+ - [FAQ](#faq)
33
+ - [Digested filename](#digested-filename)
34
+ - [Getting metadata from the file](#getting-metadata-from-the-file)
35
+ - [How to recreate versions](#how-to-recreate-versions)
36
+ - [Caching across redisplays in normal forms](#caching-across-redisplays-in-normal-forms)
37
+ - [Introspection (Class.attached_files)](#introspection-classattached_files)
38
+ - [Processing in background](#processing-in-background)
39
+
40
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
41
+
42
+
43
+ ## Intro
44
+
45
+ The goal of this library is to be as minimal as possible, including as less features and code the better. This library's
46
+ responsibility is to handle the storage of a file related to an ActiveRecord object, persisting the file on save and
47
+ deleting it on destroy. Therefore, there is no code included to handle images, integration with rails views or any
48
+ other related feature. There is however a FAQ section later on in this README that can help you implement those things
49
+ using Saviour and your own code.
50
+
51
+
52
+ ## Basic usage example
53
+
54
+ This library is inspired api-wise by carrierwave, sharing the same way of declaring "attachments" (file storages related
55
+ to an ActiveRecord object) and processings. See the following example of a model including a file:
56
+
57
+ ```
58
+ class Post < ActiveRecord::Base
59
+ # The posts table must have an `image` String column.
60
+ attach_file :image, PostImageUploader
61
+ end
62
+
63
+ class PostImageUploader < Saviour::BaseUploader
64
+ store_dir! { "/default/path/#{model.id}/#{attached_as}" }
65
+
66
+ process :resize, width: 500, height: 500
67
+
68
+ version(:thumb) do
69
+ process :resize, width: 100, height: 100
70
+ end
71
+
72
+ def resize(contents, filename, opts)
73
+ width = opts[:width]
74
+ height = opts[:height]
75
+
76
+ # modify contents in memory here
77
+ contents = user_implementation_of_resize(contents, width, height)
78
+
79
+ [contents, filename]
80
+ end
81
+ end
82
+ ```
83
+
84
+ In this example we have posts that have an image. That image will be stored in a path like `/default/path/<id>/image`
85
+ and also a resize operation will be performed before persisting the file.
86
+
87
+ There's one version declared with the name `thumb` that will be created by resizing the file to 100x100. The version
88
+ filename will be by default `<original_filename>_thumb` but it can be changed if you want.
89
+
90
+ Filenames (both for the original image and for the versions) can be changed in a processor just by returning a different
91
+ second argument.
92
+
93
+ Here the resize manipulation is done in-memory, but there're also a way to handle manipulations done at the file level if
94
+ you need to use external binaries like imagemagick, image optimization tools (pngquant, jpegotim, etc...) or others.
95
+
96
+
97
+ ## File API
98
+
99
+ `Saviour::File` is the type of object you'll get when accessing the attribute over which a file is attached in the
100
+ ActiveRecord object. The public api you can use on those objects is:
101
+
102
+ - assign
103
+ - exists?
104
+ - read
105
+ - write
106
+ - delete
107
+ - public_url
108
+ - url
109
+ - changed?
110
+ - filename
111
+ - with_copy
112
+ - blank?
113
+
114
+ Use `assign` to assign a file to be stored. You can use any object that responds to `read`. See below the section
115
+ about Sources abstraction for further info.
116
+
117
+ `exists?`, `read`, `write`, `delete` and `public_url` are delegated to the storage, with the exception of `write` that
118
+ is channeled with the uploader first to handle processings. `url` is just an alias for `public_url`.
119
+
120
+ `changed?` indicates if the file has changed, in memory, regarding it's initial value. It's equivalent to the `changed?`
121
+ method that ActiveRecord implements on database columns.
122
+
123
+ `filename` is the filename of the currently stored file. Only works for files that have been already stored, not assigned.
124
+
125
+ `blank?` indicates if the file is present either in the persistence layer or in memory. It provides api-compatibility with
126
+ default rails validations like `validates_presence_of`.
127
+
128
+ `with_copy` is a helper method that will read the persisted file, create a copy using a `Tempfile` and call the block
129
+ passed to the method with that Tempfile. Will clean afterwards.
130
+
131
+ As mentioned before, you can access a `File` object via the name of the attached_as, from the previous example you could do:
132
+
133
+ ```
134
+ post = Post.find(123)
135
+ post.image # => <Saviour::File>
136
+ ```
137
+
138
+ You can also get the `File` instance of version by using an argument matching the version name:
139
+
140
+ ```
141
+ post = Post.find(123)
142
+ post.image # => <Saviour::File>
143
+ post.image(:thumb) # => <Saviour::File>
144
+ ```
145
+
146
+ Finally, a couple of convenient methods are also added to the ActiveRecord object that just delegate to the `File` object:
147
+
148
+ ```
149
+ post = Post.find(123)
150
+ post.image = File.open("/my/image.jpg") # This is equivalent to post.image.assign(File.open(...))
151
+ post.image_changed? # This is equivalent to post.image.changed?
152
+ ```
153
+
154
+ ## Storage abstraction
155
+
156
+ Storages are classes responsible for handling the persistence layer with the underlying persistence provider, whatever
157
+ that is. Storages are considered public API and anyone can write a new one. Included in the Library there are two of them,
158
+ LocalStorage and S3Storage. To be an Storage, a class must implement the following api:
159
+
160
+ ```
161
+ def write(contents, path)
162
+ end
163
+
164
+ def read(path)
165
+ end
166
+
167
+ def exists?(path)
168
+ end
169
+
170
+ def delete(path)
171
+ end
172
+ ```
173
+
174
+ The convention here is that a file consist of a raw content and a path representing its location within the underlying
175
+ persistence layer.
176
+
177
+ You must configure Saviour by providing the storage to use:
178
+
179
+ ```
180
+ Saviour::Config.storage = MyStorageImplementation.new
181
+ ```
182
+
183
+ The provided storage object will be used for all the lifespan of the running application, for all the file uploads
184
+ handled by Saviour.
185
+
186
+
187
+ ### public_url
188
+
189
+ Storages can optionally also implement this method, in order to provide a public URL to the stored file without going
190
+ through the application code.
191
+
192
+ For example, if you're storing files in a machine with a webserver, you may want this method to convert from a local
193
+ path to an external URL, adding the domain and protocol parts. As an ilustrative example:
194
+
195
+ ```
196
+ def public_url(path)
197
+ "http://mydomain.com/files/#{path}"
198
+ end
199
+ ```
200
+
201
+
202
+ ### LocalStorage
203
+
204
+ You can use this storage to store files in the local machine running the code. Example:
205
+
206
+ ```
207
+ Saviour::Config.storage = Saviour::LocalStorage.new(
208
+ local_prefix: "/var/www/app_name/current/files",
209
+ public_url_prefix: "http://mydomain.com/uploads"
210
+ )
211
+ ```
212
+
213
+ The `local_prefix` option is mandatory, and defines the base prefix under which the storage will store files in the
214
+ machine. You need to configure this accordingly to your use case and deployment strategies, for example, for rails
215
+ and capistrano with default settings you'll need to set it to `Rails.root.join("public/system")`.
216
+
217
+ The `public_url_prefix` is optional and should represent the public endpoint from which you'll serve the assets.
218
+ Same as before, you'll need to configure this accordingly to your deployment specifics.
219
+ You can also assign a Proc instead of a String to dynamically manage this (for multiple asset hosts for example).
220
+
221
+ This storage will take care of removing empty folders after removing files.
222
+
223
+ This storage includes a feature of overwrite protection, raising an exception if an attempt is made of writing something
224
+ on a path that already exists. This behaviour in enabled by default, but you can turn it off by passing an additional
225
+ argument when instantiating the storage: `overwrite_protection: false`.
226
+
227
+
228
+ ### S3Storage
229
+
230
+ An storage implementation using `Fog::AWS` to talk with Amazon S3. Example:
231
+
232
+ ```
233
+ Saviour::Config.storage = Saviour::S3Storage.new(
234
+ bucket: "my-bucket-name",
235
+ aws_access_key_id: "stub",
236
+ aws_secret_access_key: "stub"
237
+ )
238
+ ```
239
+
240
+ All passed options except for `bucket` will be directly forwarded to the initialization of `Fog::Storage.new(opts)`,
241
+ so please refer to Fog/AWS [source](https://github.com/fog/fog-aws/blob/master/lib/fog/aws/storage.rb) for extra options.
242
+
243
+ The `public_url` method just delegates to the Fog implementation, which will provide the default path to the file,
244
+ for example `https://fake-bucket.s3.amazonaws.com/dest/file.txt`. Custom domains can be configured directly in Fog via
245
+ the `host` option, as well as `region`, etc.
246
+
247
+ The `exists?` method uses a head request to verify existence, so it doesn't actually download the file.
248
+
249
+ All files will be created as public.
250
+
251
+ This storage includes a feature of overwrite protection, raising an exception if an attempt is made of writing something
252
+ on a path that already exists. This behaviour in enabled by default, but you can turn it off by passing an additional
253
+ argument when instantiating the storage: `overwrite_protection: false`.
254
+
255
+
256
+ ## Source abstraction
257
+
258
+ As mentioned before, you can use `File#assign` with any object that responds to `read`. This is already the case for `::File`,
259
+ `Tempfile` or `IO`. Since a file requires also a filename, however, in those cases a random filename will be assigned
260
+ (you can always set the filename using a processor later on).
261
+
262
+ Additionally, if the object responds to `#original_filename` then that will be used as a filename instead of generating
263
+ a random one.
264
+
265
+ You can create your own classes implementing this API to extend functionality. This library includes two of them: StringSource
266
+ and UrlSource.
267
+
268
+
269
+ ### StringSource
270
+
271
+ This is just a wrapper class that gives no additional behaviour except for implementing the required API. Use it as:
272
+
273
+ ```
274
+ foo = Saviour::StringSource.new("my raw contents", "filename.jpg")
275
+ post = Post.find(123)
276
+ post.image = foo
277
+ ```
278
+
279
+ ### UrlSource
280
+
281
+ This class implements the source abstraction from a URL. The `read` method will download the given URL and use those
282
+ contents. The filename will be guessed as well from the URL. Redirects will be followed (max 10) and connection retried
283
+ 3 times before raising an exception. Example:
284
+
285
+ ```
286
+ foo = Saviour::UrlSource.new("http://server.com/path/image.jpg")
287
+ post = Post.find(123)
288
+ post.image = foo
289
+ ```
290
+
291
+
292
+ ## Uploader classes and Processors
293
+
294
+ Uploaders are the classes responsible for managing what happens when a file is uploaded into an storage. Use them to define
295
+ the path that will be used to store the file, additional processings that you want to run and versions. See a complete
296
+ example:
297
+
298
+ ```
299
+ class ExampleUploader < Saviour::BaseUploader
300
+ store_dir! { "/default/path/#{model.id}" }
301
+
302
+ process :resize, width: 50, height: 50
303
+
304
+ process_with_file do |local_file, filename|
305
+ `mogrify -resize 40x40 #{local_file.path}`
306
+ [local_file, filename]
307
+ end
308
+
309
+ process do |contents, filename|
310
+ [contents, "new-#{filename}"]
311
+ end
312
+
313
+ version(:thumb) do
314
+ store_dir! { "/default/path/#{model.id}/versions" }
315
+ process :resize, with: 10, height: 10
316
+ end
317
+
318
+ version(:just_a_copy)
319
+
320
+ def resize(contents, filename, opts)
321
+ # User RMagick to modify contents in memory here
322
+ [contents, filename]
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### store_dir
328
+
329
+ Use `store_dir` to indicate the default directory under which the file will be stored. You can also use it under a
330
+ `version` to change the default directory for that specific version.
331
+
332
+
333
+ ### Accessing model and attached_as
334
+
335
+ Both `store_dir` and `process` / `process_with_file` declarations can be expressed passing a block or passing a symbol
336
+ representing a method. In both cases, you can directly access there a method called `model` and a method called
337
+ `attached_as`, representing the original model and the name under which the file is attached to the model.
338
+
339
+ Use this to get info form the model to compose the store_dir, for example, or even to create a processor that
340
+ extracts information from the file and passes this info back to the model to store it in additional db columns.
341
+
342
+ ### Processors
343
+
344
+ Processors are the methods (or blocks) that will modify either the file contents or the filename before actually
345
+ upload the file into the storage. You can declare them via the `process` or the `process_with_file` method.
346
+
347
+ They work as a stack, chaining the response from the previous one as input for the next one, and are executed in the
348
+ same order you declare them. Each processor will receive the raw contents and the filename, and must return an array
349
+ with two values, the new contents and the new filename.
350
+
351
+ As described in the example before, processors can be declared in two ways:
352
+
353
+ - As a symbol or a string, it will be interpreted as a method that will be called in the current uploader.
354
+ You can optionally set an extra Hash of options that will be forwarded to the method, so it becomes easier to reuse processors.
355
+
356
+ - As a Proc, for inline use cases.
357
+
358
+ By default processors work with the full raw contents of the file, and that's what you will get and must return when
359
+ using the `process` method. However, since there are use cases for which is more convenient to have a File object
360
+ instead of the raw contents, you can also use the `process_with_file` method, which will give you a Tempfile object,
361
+ and from which you must return a File object as well.
362
+
363
+ You can combine both and Saviour will take care of synchronization, however take into account that every time you
364
+ switch from one to another there will be a penalty for having to either read or write from/to disk.
365
+ Internally Saviour works with raw contents, so even if you only use `process_with_file`, there will be a penalty at the
366
+ beginning and at the end, for writing and reading to and from a file.
367
+
368
+ When using `process_with_file`, the last file instance you return from your last processor defined as
369
+ `process_with_file` will be automatically deleted by Saviour. Be aware of this if you return
370
+ some File instance different than the one you received pointing to a file.
371
+
372
+ Finally, processors can be disabled entirely via a configuration parameter. Example:
373
+
374
+ ```
375
+ Saviour::Config.processing_enabled = false
376
+ Saviour::Config.processing_enabled = true
377
+ ```
378
+
379
+ You can use this when running tests, for example, or if you want processors to not execute for some reason. The flag can be
380
+ changed in real time.
381
+
382
+
383
+ ## Versions
384
+
385
+ Versions in Saviour are treated just like an additional attachment. They require you an additional database column to
386
+ persist the file path, and this means you can work with them completely independently of the main file. They can be
387
+ assigned, deleted, etc... independently. You just need to work with the versioned `Saviour::File` instance instead of the main
388
+ one, so for example when assigning a file you'll need to do `object.file(:thumb).assign(my_file)`.
389
+
390
+ You must create an additional database String column for each version, with the following convention:
391
+
392
+ `<attached_as>_<version_name>`
393
+
394
+ The only feature versions gives you is following their main file: A version will be assigned automatically if you assign the
395
+ main file, and all versions will be deleted when deleting the main file.
396
+
397
+ In case of conflict, the versioned assignation will be preserved. For example, if you assign both the main file and the version,
398
+ both of them will be respected and the main file will not propagate to the version in this case.
399
+
400
+ Defined processors in the Uploader will execute when assigning a version directly. Validations will also execute when assigning
401
+ a version directly (see validation section for details).
402
+
403
+ When you open a `version` block within an uploader, you can declare some processors (or change the store dir) only for
404
+ that version. Note that all processors will be executed for every version that exists, plus one time for the base file.
405
+ There are no optimizations done, if your uploader declares one processors first, and from there you open 2 versions,
406
+ the first processors will be executed 3 times.
407
+
408
+
409
+ ## Validations
410
+
411
+ You can declare validations on your model to implement specific checkings over the contents or the filename of an attachment.
412
+
413
+ Take note that validations are executed over the contents given as they are, before any processing. For example you can
414
+ have a validation declaring "max file size is 1Mb", assign a file right below the limit, but then process it in a way that
415
+ increases its size. You'll be left with a file bigger than 1Mb.
416
+
417
+ Example of validations:
418
+
419
+ ```
420
+ class Post < ActiveRecord::Base
421
+ attach_file :image, PostImageUploader
422
+
423
+ attach_validation(:image) do |contents, filename|
424
+ errors.add(:image, "must be smaller than 10Mb") if contents.bytesize >= 10.megabytes
425
+ errors.add(:image, "must be a jpeg file") if File.extname(filename) != ".jpg" # naive, don't copy paste
426
+ end
427
+ end
428
+ ```
429
+
430
+ Validations will always receive the raw contents of the file. If you need to work with a `File` object you'll need to implement
431
+ the necessary conversions.
432
+
433
+ Validations can also be declared passing a method name instead of a block, like this:
434
+
435
+ ```
436
+ class Post < ActiveRecord::Base
437
+ attach_file :image, PostImageUploader
438
+ attach_validation :image, :check_size
439
+
440
+ private
441
+
442
+ def check_size(contents, filename)
443
+ errors.add(:image, "must be smaller than 10Mb") if contents.bytesize >= 10.megabytes
444
+ end
445
+ end
446
+ ```
447
+
448
+ To improve reusability, validation blocks or methods will also receive a third argument (only if declared in your
449
+ implementation). This third argument is a hash containing `attached_as` and `version` of the validating file.
450
+
451
+
452
+ ## Active Record Lifecycle integration
453
+
454
+ On `after_save` Saviour will upload the changed files attached to the current model, executing the processors as needed.
455
+
456
+ On `after_destroy` Saviour will delete all the attached files and versions.
457
+
458
+ On `validate` Saviour will execute the validations defined.
459
+
460
+ When validations are defined, the assigned source will be readed only once. On validation time, it will be readed, passed
461
+ to the validation blocks and cached. If the model is valid, the upload will happen from those cached contents. If there
462
+ are no validations, the source will be readed only on upload time, after validating the model.
463
+
464
+
465
+ ## FAQ
466
+
467
+ This is a compilation of common questions or features regarding file uploads.
468
+
469
+ ### Digested filename
470
+
471
+ A common use case is to create a processor to include a digest of the file in the filename. The implementation is left
472
+ for the user, but a simple example of such processor is this:
473
+
474
+ ```
475
+ def digest_filename(contents, filename, opts = {})
476
+ separator = opts.fetch(:separator, "-")
477
+
478
+ digest = ::Digest::MD5.hexdigest(contents)
479
+ extension = ::File.extname(filename)
480
+
481
+ new_filename = "#{[::File.basename(filename, ".*"), digest].join(separator)}#{extension}"
482
+
483
+ [contents, new_filename]
484
+ end
485
+ ```
486
+
487
+ ### Getting metadata from the file
488
+ ### How to recreate versions
489
+ ### Caching across redisplays in normal forms
490
+ ### Introspection (Class.attached_files)
491
+ ### Processing in background