saviour 0.2.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 (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