saviour 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +491 -0
- data/Rakefile +6 -0
- data/lib/saviour.rb +170 -0
- data/lib/saviour/base_uploader.rb +81 -0
- data/lib/saviour/config.rb +13 -0
- data/lib/saviour/file.rb +124 -0
- data/lib/saviour/local_storage.rb +72 -0
- data/lib/saviour/processors/digest.rb +16 -0
- data/lib/saviour/s3_storage.rb +77 -0
- data/lib/saviour/string_source.rb +15 -0
- data/lib/saviour/uploader/element.rb +19 -0
- data/lib/saviour/uploader/processors_runner.rb +88 -0
- data/lib/saviour/uploader/store_dir_extractor.rb +41 -0
- data/lib/saviour/url_source.rb +55 -0
- data/lib/saviour/version.rb +3 -0
- data/saviour.gemspec +26 -0
- data/spec/feature/access_to_model_and_mounted_as.rb +30 -0
- data/spec/feature/crud_workflows_spec.rb +138 -0
- data/spec/feature/persisted_path_spec.rb +35 -0
- data/spec/feature/reload_model_spec.rb +25 -0
- data/spec/feature/validations_spec.rb +172 -0
- data/spec/feature/versions_spec.rb +186 -0
- data/spec/models/base_uploader_spec.rb +396 -0
- data/spec/models/config_spec.rb +16 -0
- data/spec/models/file_spec.rb +210 -0
- data/spec/models/local_storage_spec.rb +154 -0
- data/spec/models/processors/digest_spec.rb +22 -0
- data/spec/models/s3_storage_spec.rb +170 -0
- data/spec/models/saviour_spec.rb +80 -0
- data/spec/models/url_source_spec.rb +76 -0
- data/spec/spec_helper.rb +93 -0
- data/spec/support/data/camaloon.jpg +0 -0
- data/spec/support/data/example.xml +21 -0
- data/spec/support/data/text.txt +1 -0
- data/spec/support/models.rb +3 -0
- data/spec/support/schema.rb +9 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -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
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|