saviour 0.5.10 → 0.5.11

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5512cd3673652315ed3ddc008ab718fe055d8a2771dd9505a3bfca27fdc4b14
4
- data.tar.gz: ed92ca2f0112bfb8ff1c0c6e5464452bc282710b8168b4acc19f9dacfe870e36
3
+ metadata.gz: 8506a47d7861dc92875e3dca040bc958ef442f4622464c4604545b7d2d255e36
4
+ data.tar.gz: 483e8eb3fe97c38feb64cdc2e7f7cc20580070e533759fc991fddd4ecb3ea054
5
5
  SHA512:
6
- metadata.gz: 3a6f13bc2d0b64f53b98232564715461708e4d6f0eac4966e4bbad5049e5e118c7a6c0c48a7cc08fa1b2a82e199640e17689ef5caf564a3b09d2cd40b41a7fe7
7
- data.tar.gz: ef4713c314cfdfc59c6d4bb9551281b896c506303522316162594a8094f95966bc0fe4aad6f8c81117407bc58b95c713f4dfaea719831344764a28686cd5fce5
6
+ metadata.gz: b751a08547b441ebebe4483d880ddd7c5a302c1f8eb0c040a65b282ab7d9a9ce0a61c7a54d677098c99a91f67f4b6bf81b58fe4e453807b02514490d85f95e09
7
+ data.tar.gz: e0157df1f371809d1d8894bcfe6af390a3ce6a8ab649e16b7a6b9bd982f17ff95c7786ddfd330a9654625c6956cf2f029e68c9a6a8cc858f5950206317546bc6
@@ -8,8 +8,8 @@ rvm:
8
8
  - 2.5.0
9
9
 
10
10
  gemfile:
11
- - gemfiles/5.0.gemfile
12
11
  - gemfiles/5.1.gemfile
12
+ - gemfiles/5.2.gemfile
13
13
 
14
14
  addons:
15
15
  code_climate:
data/Gemfile CHANGED
@@ -4,4 +4,4 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "codeclimate-test-reporter", group: :test, require: nil
7
- gem "simplecov", group: :test, require: nil
7
+ gem "simplecov", group: :test, require: nil
data/README.md CHANGED
@@ -4,569 +4,1162 @@
4
4
 
5
5
  # Saviour
6
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.
7
+ Saviour is a tool to help you manage files attached to Active Record models. It tries to be minimal about the
8
+ use cases it covers, but with a deep and complete coverage on the ones it does. For example, it offers
9
+ no support for image manipulation, but it does implement dirty tracking and transactional-aware behavior.
10
+
11
+ It also tries to have a flexible design, so that additional features can be added by the user on top of it.
12
+ You can see an example of such typical features on the [FAQ section at the end of this document](#faq).
13
+
14
+
15
+ ## Motivation
16
+
17
+ This project started in 2015 as an attempt to replace Carrierwave. Since then other solutions have appeared
18
+ to solve the same problem, like [shrine](https://github.com/shrinerb/shrine), [refile](https://github.com/refile/refile)
19
+ and even more recently rails own solution [activestorage](https://github.com/rails/rails/tree/master/activestorage).
20
+
21
+ The main difference between those solutions and Saviour is about the broadness and scope of the problem
22
+ that wants to be solved.
23
+
24
+ They offer a complete out-of-the-box solution that covers many different needs:
25
+ image management, caching of files for seamless integration with html forms, direct uploads to s3, metadata
26
+ extraction, background jobs integration or support for different ORMs are some of the features you can find on
27
+ those libraries.
28
+
29
+ If you need those functionalities and they suit your needs, they can be perfect solutions for you.
30
+
31
+ The counterpart, however, is that they have more dependencies and, as they cover a broader spectrum of
32
+ use cases, they tend to impose more conventions that are expected to be followed as is. If you don't want,
33
+ or can't follow some of those conventions then you're out of luck.
34
+
35
+ Saviour provides a battle-tested infrastructure for storing files following an AR model
36
+ life-cycle which can be easily extended to suit your custom needs.
37
+
38
+
39
+
40
+ ## Installation
41
+
42
+ Add this line to your application's Gemfile:
43
+
44
+ ```ruby
45
+ gem 'saviour'
46
+ ```
47
+
48
+ And then execute:
49
+
50
+ $ bundle
9
51
 
10
52
 
11
53
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
12
54
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
13
55
 
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)
56
+ - [Quick start](#quick-start)
57
+ - [General Usage](#general-usage)
58
+ - [Api on attachment](#api-on-attachment)
59
+ - [Additional api on the model](#additional-api-on-the-model)
60
+ - [Storages](#storages)
61
+ - [Local Storage](#local-storage)
62
+ - [S3 Storage](#s3-storage)
63
+ - [Uploader classes](#uploader-classes)
64
+ - [store_dir](#store_dir)
65
+ - [Processors](#processors)
66
+ - [halt_process](#halt_process)
67
+ - [Versions](#versions)
68
+ - [Transactional behavior](#transactional-behavior)
69
+ - [Concurrency](#concurrency)
70
+ - [stash](#stash)
71
+ - [Dirty tracking](#dirty-tracking)
72
+ - [AR Validations](#ar-validations)
73
+ - [Introspection](#introspection)
74
+ - [Extras & Advance usage](#extras--advance-usage)
75
+ - [Skip processors](#skip-processors)
76
+ - [Testing](#testing)
77
+ - [Sources: url and string](#sources-url-and-string)
78
+ - [Custom Storages](#custom-storages)
79
+ - [Bypassing Saviour](#bypassing-saviour)
80
+ - [Bypass example: Nested Cloning](#bypass-example-nested-cloning)
32
81
  - [FAQ](#faq)
33
- - [Digested filename](#digested-filename)
34
- - [Getting metadata from the file](#getting-metadata-from-the-file)
82
+ - [how to reuse code in your app, attachment with defaults](#how-to-reuse-code-in-your-app-attachment-with-defaults)
83
+ - [How to manage file removal from forms](#how-to-manage-file-removal-from-forms)
84
+ - [How to extract metadata from files](#how-to-extract-metadata-from-files)
85
+ - [How to process files in background / delayed](#how-to-process-files-in-background--delayed)
35
86
  - [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)
87
+ - [How to digest the filename](#how-to-digest-the-filename)
88
+ - [License](#license)
39
89
 
40
90
  <!-- END doctoc generated TOC please keep comment here to allow auto update -->
41
91
 
42
92
 
43
- ## Intro
93
+ ## Quick start
44
94
 
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.
95
+ First, you'll need to configure Saviour to indicate what type of storage you'll want to use. For example,
96
+ to use local storage:
97
+
98
+ ```ruby
99
+ # config/initializers/saviour.rb
50
100
 
101
+ Saviour::Config.storage = Saviour::LocalStorage.new(
102
+ local_prefix: Rails.root.join('public/system/uploads/'),
103
+ public_url_prefix: "https://mywebsite.com/system/"
104
+ )
105
+ ```
51
106
 
52
- ## Basic usage example
107
+ A local storage will persist the files on the server running the ruby code and will require settings to
108
+ indicate precisely where to store those files locally and how to build a public url to them. Those settings
109
+ depend on your server and deployment configurations. Saviour ships with local storage and Amazon's S3 storage
110
+ capabilities, see the section on [Storages](#storages) for more details.
53
111
 
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:
112
+ Saviour will also require a text column for each attachment in an ActiveRecord model. This column will be used to
113
+ persist a file's "path" across the storage. For example:
56
114
 
115
+ ```ruby
116
+ create_table "users" do |t|
117
+ # other columns...
118
+ t.text "avatar"
119
+ end
57
120
  ```
58
- class Post < ActiveRecord::Base
121
+
122
+ Then include the mixin `Saviour::Model` in your AR model and declare the attachment:
123
+
124
+ ```ruby
125
+ class User < ApplicationRecord
59
126
  include Saviour::Model
60
127
 
61
- # The posts table must have an `image` string column.
62
- attach_file :image, PostImageUploader
128
+ attach_file(:avatar) do
129
+ store_dir { "uploads/avatars/#{model.id}/" }
130
+ end
63
131
  end
132
+ ```
64
133
 
65
- class PostImageUploader < Saviour::BaseUploader
66
- store_dir { "/default/path/#{model.id}/#{attached_as}" }
67
-
68
- process :resize, width: 500, height: 500
134
+ Declaring a `store_dir` is mandatory and indicates at what base path the assigned files must be stored. More
135
+ on this later at the [Uploaders section](#uploader-classes).
69
136
 
70
- version(:thumb) do
71
- process :resize, width: 100, height: 100
72
- end
137
+ Now you can use it:
73
138
 
74
- def resize(contents, filename, opts)
75
- width = opts[:width]
76
- height = opts[:height]
139
+ ```ruby
140
+ user = User.create! avatar: File.open("/path/to/cowboy.jpg")
141
+ user.avatar.read # => binary contents
77
142
 
78
- # modify contents in memory here
79
- contents = user_implementation_of_resize(contents, width, height)
143
+ # Url generation depends on how the storage is configured
144
+ user.avatar.url # => "https://mywebsite.com/system/uploads/avatars/1/cowboy.jpg"
80
145
 
81
- [contents, filename]
82
- end
83
- end
146
+ # Using local storage, the persisted column will have the path to the file
147
+ user[:avatar] # => "uploads/avatars/1/cowboy.jpg"
84
148
  ```
85
149
 
86
- In this example we have posts that have an image. That image will be stored in a path like `/default/path/<id>/image`
87
- and also a resize operation will be performed before persisting the file.
88
150
 
89
- There's one version declared with the name `thumb` that will be created by resizing the file to 100x100. The version
90
- filename will be by default `<original_filename>_thumb` but it can be changed if you want.
151
+ ### General Usage
152
+
153
+ You can assign to an attachment any object that responds to `read`. This includes `File`, `StringIO` and many others.
154
+
155
+ The filename given to the file will be obtained by following this process:
156
+
157
+ - First, trying to call `original_filename` on the given object.
158
+ - Second, trying to call `filename` on the given object.
159
+ - Finally, if that object responds to `path`, it will be extracted as the basename of that path.
160
+
161
+ If none of that works, a random filename will be assigned.
91
162
 
92
- Filenames (both for the original image and for the versions) can be changed in a processor just by returning a different
93
- second argument.
163
+ The actual storing of the file and any possible related processing (more on this [later](#processors)) will
164
+ happen on after save, not on assignation. You can assign and re-assign different values to an attachment at no
165
+ cost.
94
166
 
95
- Here the resize manipulation is done in-memory, but there're also a way to handle manipulations done at the file level if
96
- you need to use external binaries like imagemagick, image optimization tools (pngquant, jpegotim, etc...) or others.
97
167
 
168
+ #### Api on attachment
98
169
 
99
- ## File API
170
+ Given the previous example of a User with an avatar attachment, the following methods are available to you on the attachment object:
100
171
 
101
- `Saviour::File` is the type of object you'll get when accessing the attribute over which a file is attached in the
102
- ActiveRecord object. The public api you can use on those objects is:
172
+ - `user.avatar.present?` && `.blank?`: Indicates if the attachment has an associated file or not, even if it has not been persisted yet. This methods allow you for a transparent use of rails `validates_presence_of :avatar`, as the object responds to `blank?`.
173
+ - `user.avatar.persisted?`: Indicates if the attachment has an associated file and this file is persisted. Is false after assignation and before save.
174
+ - `user.avatar?`: Same as `user.avatar.present?`
175
+ - `user.avatar.exists?`: If the attachment is `persisted?`, it checks with the storage to verify the existence of the associated path. Use it to check for situations where the database has a persisted path but the storage may not have the file, due to any other reasons (direct manipulation by other means).
176
+ - `user.avatar.with_copy {|f| ... }`: Utility method that fetches the file and gives it to you in the form of a `Tempfile`. Will forward the return value of your block. The tempfile will be cleaned up on block termination.
177
+ - `user.avatar.read`: Returns binary raw contents of the stored file.
178
+ - `user.avatar.url`: Returns the url to the stored file, based on the storage configurations.
179
+ - `user.avatar.reload`: If the contents of the storage were directly manipulated, you can use this method to force a reload of the attachment state from the storage.
180
+ - `user.avatar.filename`: Returns the filename of the stored file.
181
+ - `user.avatar.persisted_path`: If persisted, returns the path of the file as stored in the storage, otherwise nil. It's the same as the db column value.
182
+ - `user.avatar.changed?`: Returns true/false if the attachment has been assigned but not yet saved.
183
+
184
+ Usage example:
103
185
 
104
- - assign
105
- - exists?
106
- - read
107
- - write
108
- - delete
109
- - public_url
110
- - url
111
- - changed?
112
- - filename
113
- - with_copy
114
- - blank?
186
+ ```ruby
187
+ user = User.new
188
+
189
+ user.avatar? # => false
190
+ user.avatar.present? # => false
191
+ user.avatar.blank? # => true
115
192
 
116
- Use `assign` to assign a file to be stored. You can use any object that responds to `read`. See below the section
117
- about Sources abstraction for further info.
193
+ user.avatar.read # => nil, same for #url, #filename, #persisted_path
118
194
 
119
- `exists?`, `read`, `write`, `delete` and `public_url` are delegated to the storage, with the exception of `write` that
120
- is channeled with the uploader first to handle processings. `url` is just an alias for `public_url`.
195
+ user.avatar = File.open("image.jpg")
121
196
 
122
- `changed?` indicates if the file has changed, in memory, regarding it's initial value. It's equivalent to the `changed?`
123
- method that ActiveRecord implements on database columns.
197
+ user.avatar.changed? # => true
198
+ user.avatar? # => true, same as #present?
199
+ user.avatar.persisted? # => false
124
200
 
125
- `filename` is the filename of the currently stored file. Only works for files that have been already stored, not assigned.
201
+ user.avatar.url # => nil, not yet persisted
202
+ user.avatar.exists? # => false, not yet persisted
203
+ user.avatar.filename # => "image.jpg"
204
+ user.avatar.read # => nil, not yet persisted
126
205
 
127
- `blank?` indicates if the file is present either in the persistence layer or in memory. It provides api-compatibility with
128
- default rails validations like `validates_presence_of`.
206
+ user.avatar.with_copy # => nil, not yet persisted
129
207
 
130
- `with_copy` is a helper method that will read the persisted file, create a copy using a `Tempfile` and call the block
131
- passed to the method with that Tempfile. Will clean afterwards.
208
+ user.save!
132
209
 
133
- As mentioned before, you can access a `File` object via the name of the attached_as, from the previous example you could do:
210
+ user.avatar.changed? # => false
211
+ user.avatar? # => true
212
+ user.avatar.exists? # => true
213
+ user.avatar.persisted? # => true
134
214
 
215
+ user.avatar.read # => bytecontents
216
+ user.avatar.url # => "https://somedomain.com/path/image.jpg"
217
+ user.avatar.with_copy # => yields a tempfile with the image
218
+ user.avatar.read # => bytecontents
135
219
  ```
136
- post = Post.find(123)
137
- post.image # => <Saviour::File>
220
+
221
+
222
+ #### Additional api on the model
223
+
224
+ When you declare an attachment in an AR model, the model is extended with:
225
+
226
+ - `#dup`: The `dup` method over the AR instance will also take care of dupping any possible attachment with associated files if any. If the new instance returned by dup is saved, the attachments will be saved as well normally, generating a copy of the files present on the original instance.
227
+
228
+ - `#remove_<attached_as>!`: This new method will be added for each attachment. For example, `user.remove_avatar!`. Use this method to remove the associated file.
229
+
230
+ Usage example:
231
+
232
+ ```ruby
233
+ user = User.create! avatar: File.open("image.jpg")
234
+
235
+ user.avatar.url # => "https://somedomain.com/uploads_path/users/1/avatar/image.jpg"
236
+
237
+ new_user = user.dup
238
+ new_user.save!
239
+
240
+ new_user.avatar.url # => "https://somedomain.com/uploads_path/users/2/avatar/image.jpg"
241
+
242
+ new_user.remove_avatar!
243
+ new_user.avatar? # => false
138
244
  ```
139
245
 
140
- You can also get the `File` instance of version by using an argument matching the version name:
141
246
 
247
+
248
+ ### Storages
249
+
250
+ Storages are the Saviour's components responsible for file persistence. Local storage and Amazon's S3 storage
251
+ are available by default, but more can be built, as they are designed as independent components and any class
252
+ that follows the expected public api can be used as one. More on this on the [Custom storage section](custom-storages).
253
+
254
+ We'll review now how to use the two provided storages.
255
+
256
+ #### Local Storage
257
+
258
+ You can use this storage to store files in the local machine running the ruby code. Example:
259
+
260
+ ```ruby
261
+ # config/initializers/saviour.rb
262
+
263
+ Saviour::Config.storage = Saviour::LocalStorage.new(
264
+ local_prefix: Rails.root.join('public/system/uploads/'),
265
+ public_url_prefix: "http://mydomain.com/uploads"
266
+ )
142
267
  ```
143
- post = Post.find(123)
144
- post.image # => <Saviour::File>
145
- post.image(:thumb) # => <Saviour::File>
268
+
269
+ The `local_prefix` is the base prefix under which the storage will store files in the
270
+ machine. You need to configure this accordingly to your use case and deployment strategies, for example, for rails
271
+ and capistrano with default settings you'll have to store the files under `Rails.root.join("public/system")`,
272
+ as this is by default the shared directory between deployments.
273
+
274
+ The `public_url_prefix` is the base prefix to build the public endpoint from which you'll serve the assets.
275
+ Same as before, you'll need to configure this accordingly to your deployment specifics.
276
+
277
+ You can also assign a Proc instead of a String to dynamically calculate the value, useful when you have multiple
278
+ asset hosts:
279
+
280
+ `public_url_prefix: -> { https://media-#{rand(4)}.mywebsite.com/system/uploads/" }`
281
+
282
+ This storage will take care of removing folders after they become empty.
283
+
284
+ The optional extra argument `permissions` will allow you to set what permissions the files should have locally.
285
+ This value defaults to '0644' and can be changed when creating the storage instance:
286
+
287
+ ```ruby
288
+ Saviour::Config.storage = Saviour::LocalStorage.new(
289
+ local_prefix: Rails.root.join('public/system/uploads/'),
290
+ public_url_prefix: "http://mydomain.com/uploads",
291
+ permissions: '0600'
292
+ )
146
293
  ```
147
294
 
148
- Finally, a couple of convenient methods are also added to the ActiveRecord object that just delegate to the `File` object:
295
+ #### S3 Storage
296
+
297
+ This storage will store files on Amazon S3, using the `aws-sdk-s3` gem. Example:
149
298
 
299
+ ```ruby
300
+ Saviour::Config.storage = Saviour::S3Storage.new(
301
+ bucket: "my-bucket-name",
302
+ aws_access_key_id: "stub",
303
+ aws_secret_access_key: "stub",
304
+ region: "my-region",
305
+ public_url_prefix: "https://s3-eu-west-1.amazonaws.com/my-bucket/"
306
+ )
150
307
  ```
151
- post = Post.find(123)
152
- post.image = File.open("/my/image.jpg") # This is equivalent to post.image.assign(File.open(...))
153
- post.image_changed? # This is equivalent to post.image.changed?
308
+
309
+ The first 4 options (`bucket`, `aws_access_key_id`, `aws_secret_access_key` and `region`) are required for the
310
+ connection and usage of your s3 bucket.
311
+
312
+ The `public_url_prefix` is the base prefix to build the public endpoint from which the files are available.
313
+ Normally you'll set it as in the example provided, or you can also change it accordingly to any CDN you may be
314
+ using.
315
+
316
+ You can also assign a Proc instead of a String to dynamically calculate the value, which is useful when you have multiple
317
+ asset hosts:
318
+
319
+ `public_url_prefix: -> { https://media-#{rand(4)}.mywebsite.com/system/uploads/" }`
320
+
321
+ The optional argument `create_options` can be given to establishing extra parameters to use when creating files. For
322
+ example you might want to set up a large cache control value so that the files become cacheable:
323
+
324
+ ```ruby
325
+ create_options: {
326
+ cache_control: 'max-age=31536000' # 1 year
327
+ }
154
328
  ```
155
329
 
156
- ## Storage abstraction
330
+ Those options will be forwarded directly to aws-sdk, you can see the complete reference here:
331
+
332
+ https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_object-instance_method
157
333
 
158
- Storages are classes responsible for handling the persistence layer with the underlying persistence provider, whatever
159
- that is. Storages are considered public API and anyone can write a new one. Included in the Library there are two of them,
160
- LocalStorage and S3Storage. To be an Storage, a class must implement the following api:
334
+ Currently, there's no support for different create options on a per-file basis. All stored files will be created
335
+ using the same options. If you want a public access on those files, you can make them public with a general
336
+ rule at the bucket level or using the `acl` create option:
161
337
 
338
+ ```ruby
339
+ create_options: {
340
+ acl: 'public-read'
341
+ }
162
342
  ```
163
- def write(contents, path)
164
- end
165
343
 
166
- def read(path)
344
+ NOTE: Be aware that S3 has a limit of 1024 bytes for the keys (paths) used. Trying to store a file with a
345
+ larger path will result in a `Saviour::KeyTooLarge` exception.
346
+
347
+
348
+ ### Uploader classes
349
+
350
+ Uploader classes are responsible to make changes to an attachment byte contents or filename, as well as indicating
351
+ what base path that file should have.
352
+
353
+ An uploader class can be provided explicitly, for example:
354
+
355
+ ```ruby
356
+ # app/uploaders/post_image_uploader.rb
357
+ class PostImageUploader < Saviour::BaseUploader
358
+ store_dir { "uploads/posts/images/#{model.id}/" }
167
359
  end
168
360
 
169
- def exists?(path)
361
+ # app/models/post.rb
362
+ class Post < ApplicationRecord
363
+ include Saviour::Model
364
+
365
+ attach_file :image, PostImageUploader
170
366
  end
367
+ ```
368
+
369
+ Or you can also provide a `&block` to the `attach_file` method to declare the uploader class implicitly. This
370
+ syntax is usually more convenient if you don't have a lot of code in your uploaders:
171
371
 
172
- def delete(path)
372
+ ```ruby
373
+ class Post < ApplicationRecord
374
+ include Saviour::Model
375
+
376
+ attach_file :image do
377
+ store_dir { "uploads/posts/images/#{model.id}/" }
378
+ end
173
379
  end
174
380
  ```
175
381
 
176
- The convention here is that a file consist of a raw content and a path representing its location within the underlying
177
- persistence layer.
178
382
 
179
- You must configure Saviour by providing the storage to use:
383
+ #### store_dir
180
384
 
181
- ```
182
- Saviour::Config.storage = MyStorageImplementation.new
385
+ Declaring a `store_dir` is mandatory for each uploader class. It can be provided directly as a block or as a symbol,
386
+ in which case it has to match with a method you define on the uploader class.
387
+
388
+ Its returning value must be a string representing the base path under which the files will be stored.
389
+
390
+ At runtime the model is available as `model`, and the name of the attachment as `attached_as`. For example:
391
+
392
+ ```ruby
393
+ class PostImageUploader < Saviour::BaseUploader
394
+ store_dir { "uploads/posts/images/#{model.id}/" }
395
+
396
+ # or
397
+ store_dir { "uploads/posts/#{model.id}/#{attached_as}" }
398
+
399
+ # or more generic
400
+ store_dir { "uploads/#{model.class.name.parameterize}/#{model.id}/#{attached_as}" }
401
+
402
+ # or with a method
403
+ store_dir :calculate_dir
404
+
405
+ def calculate_dir
406
+ "uploads/posts/images/#{model.id}/"
407
+ end
408
+ end
183
409
  ```
184
410
 
185
- The provided storage object is considered a global configuration state that will be used by Saviour for all mounters.
186
- However, this configuration is thread-safe and can be changed at runtime, allowing you in practice to work with different
187
- storages by swapping them depending on your use case.
411
+ Since attachment processing and storing happens on after save, at the time `store_dir` is called the model
412
+ has already been saved, so the database `id` is available.
188
413
 
414
+ The user is expected to configure such store_dirs appropriately so that path collisions cannot happen across
415
+ the whole application. To that end, the use of `model.id` and `attached_as` as part of
416
+ the store dir is a common approach to ensure there will be no collisions. Other options could involve
417
+ random token generation.
189
418
 
190
- ### public_url
191
419
 
192
- Storages can optionally also implement this method, in order to provide a public URL to the stored file without going
193
- through the application code.
420
+ #### Processors
194
421
 
195
- For example, if you're storing files in a machine with a webserver, you may want this method to convert from a local
196
- path to an external URL, adding the domain and protocol parts. As an ilustrative example:
422
+ Processors are methods (or lambdas) that receive the contents of the file being saved and its filename,
423
+ and in turn return file contents and filename. You can use them to change both values, for example:
197
424
 
198
- ```
199
- def public_url(path)
200
- "http://mydomain.com/files/#{path}"
425
+ ```ruby
426
+ class PostImageUploader < Saviour::BaseUploader
427
+ store_dir { "uploads/posts/#{model.id}/#{attached_as}" }
428
+
429
+ process do |contents, filename|
430
+ new_filename = "#{Digest::MD5.hexdigest(contents)}-#{filename}"
431
+ new_contents = Zlib::Deflate.deflate(contents)
432
+
433
+ [new_contents, new_filename]
434
+ end
201
435
  end
202
436
  ```
203
437
 
438
+ Here we're compressing the contents with ruby's zlib and adding a checksum to the filename for caching purposes. The returning
439
+ value must be always an array of two values, a pair of contents/filename.
204
440
 
205
- ### LocalStorage
441
+ If you want to reuse processors and make them more generic with variables, you can also define them as methods
442
+ and share them via a ruby module or via inheritance. In this form, you can pass arbitrary arguments.
443
+
444
+ ```ruby
445
+ module ProcessorsHelpers
446
+ def resize(contents, filename, width:, height:)
447
+ new_contents = SomeImageManipulationImplementation.new(contents).resize_to(width, height)
448
+
449
+ [new_contents, filename]
450
+ end
451
+ end
206
452
 
207
- You can use this storage to store files in the local machine running the code. Example:
453
+ class PostImageUploader < Saviour::BaseUploader
454
+ include ProcessorsHelpers
455
+ store_dir { "uploads/posts/#{model.id}/#{attached_as}" }
208
456
 
457
+ process :resize, width: 100, height: 100
458
+ end
209
459
  ```
210
- Saviour::Config.storage = Saviour::LocalStorage.new(
211
- local_prefix: "/var/www/app_name/current/files",
212
- public_url_prefix: "http://mydomain.com/uploads"
213
- )
460
+
461
+ You may declare as many processors as you want in an uploader class, they will be executed in the same order as
462
+ you define them and they will be chained: the output of the first processor will be the input of the second one, etc.
463
+
464
+ Inside a processor you also have access to the following variables:
465
+
466
+ - `model`: The model owner of the file being saved.
467
+ - `attached_as`: The name of the attachment being processed.
468
+ - `store_dir`: The computed value of the store dir this file will have.
469
+
470
+ When you use `process` to declare processors as seen before, you're given the raw byte contents that were originally
471
+ assigned to the attachment. This may be convenient if you have a use case when you generate those contents yourself
472
+ or want to manipulate them directly with ruby, but that's normally not the case. Usually, you assign files to
473
+ the attachments and modify them via third party binaries (like imagemagick). In that scenario, in order to reduce
474
+ memory usage, you can use instead `process_with_file`.
475
+
476
+ This is essentially the same but instead of raw byte contents you're given a `Tempfile` instance:
477
+
478
+ ```ruby
479
+ class PostImageUploader < Saviour::BaseUploader
480
+ store_dir { "uploads/posts/#{model.id}/#{attached_as}" }
481
+
482
+ process_with_file do |file, filename|
483
+ `convert -thumbnail 100x100^ #{Shellwords.escape(file.path)}`
484
+
485
+ [file, filename]
486
+ end
487
+ end
214
488
  ```
215
489
 
216
- The `local_prefix` option is mandatory, and defines the base prefix under which the storage will store files in the
217
- machine. You need to configure this accordingly to your use case and deployment strategies, for example, for rails
218
- and capistrano with default settings you'll need to set it to `Rails.root.join("public/system")`.
490
+ *Note that when escaping to the shell you need to check for safety in case there's an injection in the filename.*
219
491
 
220
- The `public_url_prefix` is optional and should represent the public endpoint from which you'll serve the assets.
221
- Same as before, you'll need to configure this accordingly to your deployment specifics.
222
- You can also assign a Proc instead of a String to dynamically manage this (for multiple asset hosts for example).
492
+ You can modify directly the contents of the given file in the filesystem, or you could also delete the given file and
493
+ return a new one. If you return a different file instance, you're expected to clean up the one that was given to you.
494
+
495
+ You can mix `process` with `process_with_file` but you should try to avoid it, as it will be a performance penalty
496
+ having to convert between formats.
223
497
 
224
- This storage will take care of removing empty folders after removing files.
498
+ Also, even if there's just one `process`, the whole contents of the file will be loaded into memory. Avoid that usage
499
+ if you're conservative about memory usage or take care of restricting the allowed file size you can work with on
500
+ any file upload you accept across your application.
225
501
 
226
502
 
227
- ### S3Storage
503
+ ##### halt_process
228
504
 
229
- An storage implementation using `Fog::AWS` to talk with Amazon S3. Example:
505
+ `halt_process` is a method you can call from inside a processor in order to abort the processing and storing
506
+ of the current file. You can use this to conditionally store a file or not based on runtime decisions.
230
507
 
508
+ For example, you may be storing media files that can be audio, video or images, and you want to generate a
509
+ thumbnail for videos and images but not for audio files.
510
+
511
+ ```ruby
512
+ class ThumbImageUploader < Saviour::BaseUploader
513
+ store_dir { "uploads/thumbs/#{model.id}/#{attached_as}" }
514
+
515
+ process_with_file do |file, filename|
516
+ halt_process unless can_generate_thumb?(file)
517
+ `convert -thumbnail 100x100^ #{Shellwords.escape(file.path)}`
518
+
519
+ [file, filename]
520
+ end
521
+
522
+ def can_generate_thumb?(file)
523
+ # Some mime type checking
524
+ end
525
+ end
231
526
  ```
232
- Saviour::Config.storage = Saviour::S3Storage.new(
233
- bucket: "my-bucket-name",
234
- aws_access_key_id: "stub",
235
- aws_secret_access_key: "stub"
236
- )
527
+
528
+
529
+ ### Versions
530
+
531
+ Versions is a common and popular feature on other file management libraries, however, they're usually implemented
532
+ in a way that makes the "versioned" attachments behave differently than normal attachments.
533
+
534
+ Saviour takes another approach: there's no such concept as a "versioned attachment", there're only attachments.
535
+ The way this works with Saviour is by making one attachment "follow" another one, so that whatever is assigned on
536
+ the main attachment is also assigned automatically to the follower, and when the main attachment is deleted
537
+ also is the follower.
538
+
539
+ For example:
540
+
541
+ ```ruby
542
+ class Post < ApplicationRecord
543
+ include Saviour::Model
544
+
545
+ attach_file :image do
546
+ store_dir { "uploads/posts/images/#{model.id}/" }
547
+ end
548
+
549
+ attach_file :image_thumb, follow: :image, dependent: :destroy do
550
+ store_dir { "uploads/posts/image_thumbs/#{model.id}/" }
551
+ process_with_file :resize, width: 100, height: 100
552
+ end
553
+ end
237
554
  ```
238
555
 
239
- All passed options except for `bucket` will be directly forwarded to the initialization of `Fog::Storage.new(opts)`,
240
- so please refer to Fog/AWS [source](https://github.com/fog/fog-aws/blob/master/lib/fog/aws/storage.rb) for extra options.
556
+ Using the `follow: :image` syntax you declare that the `image_thumb` attachment has to be automatically assigned
557
+ to the same contents as `image` every time `image` is assigned.
241
558
 
242
- The `public_url` method just delegates to the Fog implementation, which will provide the default path to the file,
243
- for example `https://fake-bucket.s3.amazonaws.com/dest/file.txt`. Custom domains can be configured directly in Fog via
244
- the `host` option, as well as `region`, etc.
559
+ The `:dependent` part is mandatory and indicates if the `image_thumb` attachment has to be removed when the
560
+ `image` is removed (with `dependent: :destroy`) or not (with `dependent: :ignore`).
245
561
 
246
- The `exists?` method uses a head request to verify existence, so it doesn't actually download the file.
562
+ ```ruby
563
+ a = Post.create! image: File.open("/path/image.png")
564
+ a.image # => original file assigned
565
+ a.image_thumb # => a thumb over the image assigned
566
+ ```
247
567
 
248
- All files will be created as public by default, but you can set an additional argument when initializing the storage to
249
- declare options to be used when creating files to S3, and those options will take precedence. Use this for example to
250
- set an expiration time for the asset. Example:
568
+ Now, both attachments are independent:
251
569
 
570
+ ```ruby
571
+ # `image_thumb` can be changed independently
572
+ a.update_attributes! image_thumb: File.open("/path/another_file.png")
573
+
574
+ # or removed
575
+ a.remove_file_thumb!
252
576
  ```
253
- Saviour::Config.storage = Saviour::S3Storage.new(
254
- bucket: "my-bucket-name",
255
- aws_access_key_id: "stub",
256
- aws_secret_access_key: "stub",
257
- create_options: {public: false, 'Cache-Control' => 'max-age=31536000'}
258
- )
577
+
578
+ If `dependent: :destroy` has been choosed, then removing `image` will remove `image_thumb` as well:
579
+
580
+ ```ruby
581
+ a.remove_image!
582
+ a.image? # => false
583
+ a.image_thumb? # => false
584
+ ````
585
+
586
+ If the "versioned attachment" is assigned at the same time as the main one, the provided files will be preserved:
587
+
588
+ ```ruby
589
+ a = Post.create! image: File.open("/path/image.png"), image_thumb: File.open("/path/thumb.jpg")
590
+ a.image # => 'image.png' file
591
+ a.image_thumb # => 'thumb.jpg' file
592
+
593
+ # The same happens when assignations and db saving are separated:
594
+
595
+ a = Post.find(42)
596
+
597
+ # other code ...
598
+ a.image_thumb = File.open("/path/thumb.jpg")
599
+
600
+ # other code ...
601
+ a.image = File.open("/path/image.png")
602
+
603
+ # other code ...
604
+ a.save!
605
+ a.image # => 'image.png' file
606
+ a.image_thumb # => 'thumb.jpg' file
259
607
  ```
260
608
 
261
- NOTE: Be aware that S3 has a limit of 1024 bytes for the keys (paths) used. Be sure to truncate to that maximum length
262
- if you're using an s3 storage, for example with a processor like this:
609
+ Finally, even if you selected to use `dependent: :destroy` you may choose to not remove the "versions" when
610
+ removing the main attachment using an extra argument when removing:
263
611
 
264
612
  ```ruby
265
- # http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html
266
- # Max 1024 bytes for keys in S3
267
- def truncate_at_max_key_size(contents, filename)
268
- # Left 20 bytes of margin (up to 1024) to have some room for a name
269
- if store_dir.bytesize > 1004
270
- raise "The store_dir used is already bigger than 1004 bytes, must be reduced!"
271
- end
613
+ a = Post.create! image: File.open("/path/image.png")
614
+ a.remove_image!(dependent: :ignore)
615
+ a.image? # => false
616
+ a.image_thumb? # => true
617
+ ```
272
618
 
273
- key = "#{store_dir}#{filename}"
274
- new_filename = if key > 1024
275
- # note mb_chars is an active support's method
276
- filename.mb_chars.limit(1024 - store_dir.bytesize).to_s
277
- else
278
- filename
279
- end
619
+ The same is true for the opposite, you could use `remove_image!(dependent: :destroy)` if the attachment was
620
+ configured as `dependent: :ignore`.
280
621
 
281
- [contents, new_filename]
282
- end
283
- ```
284
622
 
623
+ ### Transactional behavior
285
624
 
286
- ## Source abstraction
625
+ When working with attachments inside a database transaction (using Active Record), all the changes made will be
626
+ reverted if the transaction is rolled back.
287
627
 
288
- As mentioned before, you can use `File#assign` with any io like object that responds to `read` and `rewind`. This is already the case for `::File`,
289
- `Tempfile` or `IO`. Since a file requires also a filename, however, in those cases a random filename will be assigned
290
- (you can always set the filename using a processor later on).
628
+ On file creation (either creating a new AR model or assigning a file for the first time), the file will be
629
+ available on after save, but will be removed on after rollback.
291
630
 
292
- Additionally, if the object responds to `#original_filename` then that will be used as a filename instead of generating
293
- a random one, or, if the object responds to `#path` then `File.basename(path)` will be used as a name.
631
+ On file update, changes will be available on after save, but the original file will be restored on after rollback.
294
632
 
295
- You can create your own classes implementing this API to extend functionality. This library includes two of them: StringSource
296
- and UrlSource.
633
+ On file deletion, the file will be no longer available (via Saviour public api) on after save, but the actual deletion
634
+ will happen on after commit (so in case of rollback the file is never removed).
297
635
 
298
636
 
299
- ### StringSource
637
+ ### Concurrency
300
638
 
301
- This is just a wrapper class that gives no additional behavior except for implementing the required API. Use it as:
639
+ Saviour will run all processors and storage operations concurrently for all attachments present in a model. For example:
640
+
641
+ ```ruby
642
+ class Product < ApplicationRecord
643
+ include Saviour::Model
644
+
645
+ attach_file :image, SomeUploader
646
+ attach_file :image_thumb, SomeUploader, follow: :image
647
+ attach_file :cover, SomeUploader
648
+ end
302
649
 
650
+ a = Product.new image: File.open('...'), cover: File.open('...')
651
+ a.save!
303
652
  ```
304
- foo = Saviour::StringSource.new("my raw contents", "filename.jpg")
305
- post = Post.find(123)
306
- post.image = foo
653
+
654
+ At the time that `save!` is executed, 3 threads will be opened. In each one, the processors you defined will be
655
+ executed for that file, and then the result will be written to the storage.
656
+
657
+ In case you have so many attachments that processing them concurrently would be undesired you can limit the
658
+ max concurrency with:
659
+
660
+ ```ruby
661
+ Saviour::Config.concurrent_workers = 2
307
662
  ```
308
663
 
309
- ### UrlSource
664
+ The default value is 4.
665
+
666
+
667
+ #### stash
668
+
669
+ Note that this means **your processor's code must be thread-safe**. Do not issue db queries from processors
670
+ directly, for example. They would be executed in a new connection by AR and you may not be expecting that.
671
+
672
+ Saviour comes with a simple mechanism to gather data from processors so that you can use it later from
673
+ the main thread: `stash`. For example:
674
+
675
+
676
+ ```ruby
677
+ class ImageUploader < Saviour::BaseUploader
678
+ store_dir { "uploads/thumbs/#{model.id}/#{attached_as}" }
679
+
680
+ process_with_file do |file, filename|
681
+ width, height = `identify -format "%wx%h" #{Shellwords.escape(file.path)}`.strip.split(/x/).map(&:to_i)
682
+
683
+ stash(
684
+ width: width,
685
+ height: height,
686
+ size: File.size(file.path)
687
+ )
310
688
 
311
- This class implements the source abstraction from a URL. The `read` method will download the given URL and use those
312
- contents. The filename will be guessed as well from the URL. Redirects will be followed (max 10) and connection retried
313
- 3 times before raising an exception. Example:
689
+ [file, filename]
690
+ end
314
691
 
692
+ after_upload do |stash|
693
+ model.update_attributes!(size: stash[:size], width: stash[:width], height: stash[:height])
694
+ end
695
+ end
315
696
  ```
316
- foo = Saviour::UrlSource.new("http://server.com/path/image.jpg")
317
- post = Post.find(123)
318
- post.image = foo
697
+
698
+ Use `stash(hash)` to push a hash of data from a processor. You can call this multiple times from different processors,
699
+ the hashes you stash will be deep merged. You can then declare an `after_upload` block that will run in the main
700
+ thread once all attachments have been saved to the storage. The block will simply receive the stash hash, and from
701
+ there you can run arbitrary code to persist the info.
702
+
703
+
704
+ ### Dirty tracking
705
+
706
+ Saviour implements dirty tracking for the attachments. Given the following example:
707
+
708
+ ```ruby
709
+ class User < ApplicationRecord
710
+ include Saviour::Model
711
+
712
+ attach_file(:avatar) do
713
+ store_dir { "uploads/avatars/#{model.id}/" }
714
+ end
715
+ end
319
716
  ```
320
717
 
718
+ You can now use:
719
+
720
+ ```ruby
721
+ a = User.create! avatar: File.open("avatar.jpg")
722
+
723
+ a.avatar = File.open("avatar_2.jpg")
724
+
725
+ a.changed? # => true
726
+
727
+ a.avatar_changed? # => true
321
728
 
322
- ## Uploader classes and Processors
729
+ a.avatar_was.url # => url pointing to the original avatar.jpg file
730
+ a.avatar_was.read # => previous byte contents
323
731
 
324
- Uploaders are the classes responsible for managing what happens when a file is uploaded into an storage. Use them to define
325
- the path that will be used to store the file, additional processings that you want to run and versions. See a complete
326
- example:
732
+ a.changed_attributes # => { avatar: <Saviour::File instance of avatar.jpg>}
733
+ a.avatar_change # => [<Saviour::File instance of avatar.jpg>, <Saviour::File instance of avatar_2.jpg>]
734
+ a.changes # => { avatar: [<Saviour::File instance of avatar.jpg>, <Saviour::File instance of avatar_2.jpg>] }
327
735
 
736
+ a.save!
737
+
738
+ a.avatar_changed? # => false
328
739
  ```
329
- class ExampleUploader < Saviour::BaseUploader
330
- store_dir { "/default/path/#{model.id}" }
331
740
 
332
- process :resize, width: 50, height: 50
333
741
 
334
- process_with_file do |local_file, filename|
335
- `mogrify -resize 40x40 #{local_file.path}`
336
- [local_file, filename]
742
+
743
+ ### AR Validations
744
+
745
+ You can use `attach_validation` in an Active Record model to declare validations over attachments, for example:
746
+
747
+ ```ruby
748
+ class User < ApplicationRecord
749
+ include Saviour::Model
750
+
751
+ attach_file(:avatar) do
752
+ store_dir { "uploads/avatars/#{model.id}/" }
337
753
  end
338
754
 
339
- process do |contents, filename|
340
- [contents, "new-#{filename}"]
755
+ attach_validation :avatar do |contents, filename|
756
+ errors.add(:avatar, "max 10 Mb") if contents.bytesize > 10.megabytes
757
+ errors.add(:avatar, "invalid format") unless %w(jpg jpeg).include?(File.extname(filename))
341
758
  end
759
+ end
760
+ ```
761
+
762
+ Similar as with processors, your block will receive the raw byte contents of the assigned file (or object) and the
763
+ filename. Adding errors is up to the logic you want to have.
342
764
 
343
- version(:thumb) do
344
- store_dir { "/default/path/#{model.id}/versions" }
345
- process :resize, with: 10, height: 10
765
+ Validations can also be expressed as methods in the model:
766
+
767
+ ```ruby
768
+ class User < ApplicationRecord
769
+ include Saviour::Model
770
+
771
+ attach_file(:avatar) do
772
+ store_dir { "uploads/avatars/#{model.id}/" }
346
773
  end
347
774
 
348
- version(:just_a_copy)
775
+ attach_validation :avatar, :check_format
349
776
 
350
- def resize(contents, filename, opts)
351
- # User RMagick to modify contents in memory here
352
- [contents, filename]
777
+ def check_format(contents, filename)
778
+ errors.add(:avatar, "invalid format") unless %w(jpg jpeg).include?(File.extname(filename))
353
779
  end
354
780
  end
355
781
  ```
356
782
 
357
- ### store_dir
783
+ In both forms (block or method) an additional 3rd argument will be provided as a hash of `{attached_as: "avatar"}`
784
+ in this example. You can use this to apply different logic per attachment in case of shared validations.
785
+
786
+ Those validations will run on before save, so none of the processors you may have defined did run yet. The contents
787
+ and filename provided in the validation are the ones originally assigned to the attachment.
358
788
 
359
- Use `store_dir` to indicate the default directory under which the file will be stored. You can also use it under a
360
- `version` to change the default directory for that specific version.
789
+ You can also use the variation `attach_validation_with_file`, which is the same but instead of raw contents you're
790
+ given a `File` object to work with. Use this to preserve memory if that's your use case, same considerations apply
791
+ as in the processor's case.
361
792
 
362
- Note that it's very important that the full path to any attached file to any model is unique. This is typically
363
- accomplished by using `model.id` and `attached_as` as part of either the `store_dir` or the `filename`, in any
364
- combination you may want. If this is not satisfied, you may experience unexpected overwrite of files or files
365
- having unexpected contents, for example if two different models write to the same storage path, and then one
366
- of them is deleted.
367
793
 
794
+ ### Introspection
368
795
 
369
- ### Accessing model and attached_as
796
+ Two methods are added to any class including `Saviour::Model` to give you information about what attachments
797
+ have been defined in that class.
370
798
 
371
- Both `store_dir` and `process` / `process_with_file` declarations can be expressed passing a block or passing a symbol
372
- representing a method. In both cases, you can directly access there a method called `model` and a method called
373
- `attached_as`, representing the original model and the name under which the file is attached to the model.
799
+ `Model.attached_files` will give an array of symbols, representing all the attachments declared in that class.
374
800
 
375
- Use this to get info form the model to compose the store_dir, for example, or even to create a processor that
376
- extracts information from the file and passes this info back to the model to store it in additional db columns.
801
+ `Model.attached_followers_per_leader` will give a hash where the keys are attachments that have versions
802
+ assigned, and the values being an array of symbols, representing the attachments that are following that attachment.
377
803
 
378
- ### Processors
804
+ ```ruby
805
+ class Post < ApplicationRecord
806
+ include Saviour::Model
379
807
 
380
- Processors are the methods (or blocks) that will modify either the file contents or the filename before actually
381
- upload the file into the storage. You can declare them via the `process` or the `process_with_file` method.
808
+ attach_file :image, SomeUploader
809
+ attach_file :image_thumb, SomeUploader, follow: :image, dependent: :destroy
810
+ attach_file :image_thumb_2, SomeUploader, follow: :image, dependent: :destroy
811
+ attach_file :cover, SomeUploader
812
+ end
813
+
814
+ Post.attached_files # => [:image, :image_thumb, :image_thumb_2, :cover]
815
+ Post.attached_followers_per_leader # => { image: [:image_thumb, :image_thumb_2] }
816
+ ```
382
817
 
383
- They work as a stack, chaining the response from the previous one as input for the next one, and are executed in the
384
- same order you declare them. Each processor will receive the raw contents and the filename, and must return an array
385
- with two values, the new contents and the new filename.
386
818
 
387
- As described in the example before, processors can be declared in two ways:
819
+ ## Extras & Advance usage
388
820
 
389
- - As a symbol or a string, it will be interpreted as a method that will be called in the current uploader.
390
- You can optionally set an extra Hash of options that will be forwarded to the method, so it becomes easier to reuse processors.
821
+ ### Skip processors
391
822
 
392
- - As a Proc, for inline use cases.
823
+ Saviour has a configuration flag called `processing_enabled` that controls whether or not to execute processors.
824
+ You can set it:
393
825
 
394
- By default processors work with the full raw contents of the file, and that's what you will get and must return when
395
- using the `process` method. However, since there are use cases for which is more convenient to have a File object
396
- instead of the raw contents, you can also use the `process_with_file` method, which will give you a Tempfile object,
397
- and from which you must return a File object as well.
826
+ `Saviour::Config.processing_enabled = false`
398
827
 
399
- You can combine both and Saviour will take care of synchronization, however take into account that every time you
400
- switch from one to another there will be a penalty for having to either read or write from/to disk.
401
- Internally Saviour works with raw contents, so even if you only use `process_with_file`, there will be a penalty at the
402
- beginning and at the end, for writing and reading to and from a file.
828
+ It's thread-safe and can be changed on the fly. Use it if you, for some reason, need to skip processing in a general
829
+ way.
403
830
 
404
- When using `process_with_file`, the last file instance you return from your last processor defined as
405
- `process_with_file` will be automatically deleted by Saviour. Be aware of this if you return
406
- some File instance different than the one you received pointing to a file.
831
+ ### Testing
407
832
 
408
- From inside a process you can also access the current store dir with `store_dir`.
833
+ As file management is an expensive operation if you're working with a remote storage like s3, there
834
+ are some things that you might want to change during test execution.
409
835
 
410
- From inside a process, you can also call `halt_process` to abort the current processing and upload of the file.
411
- This can be useful for example for an "image_thumb" attachment that can be generic. If you're able to generate a
412
- thumbnail image for the given file, then it works normally, otherwise halt:
836
+ First of all, you can use a local storage on tests instead of s3, only this will speed up your suite a lot.
837
+ If you have some tests that must run against s3, you can use an s3 spec flag to conditionally
838
+ swap storages on the fly:
413
839
 
414
840
  ```ruby
415
- process_with_file do |file, filename|
416
- if can_generate_thumbnail?(file) # Only if jpg, png or pdf file
417
- create_thumbnail(file, filename)
418
- else
419
- halt_process
841
+ # config/env/test.rb
842
+ Saviour::Config.storage = ::LocalStorage.new(...)
843
+
844
+ # spec/support/saviour.rb
845
+ module S3Stub
846
+ mattr_accessor :storage
847
+
848
+ self.storage = Saviour::S3Storage.new(...)
849
+ end
850
+
851
+ RSpec.configure do |config|
852
+ config.around(:example, s3_storage: true) do |example|
853
+ previous_storage = Saviour::Config.storage
854
+ Saviour::Config.storage = S3Stub.storage
855
+
856
+ example.call
857
+
858
+ Saviour::Config.storage = previous_storage
420
859
  end
421
860
  end
422
- ```
423
861
 
424
- Finally, processors can be disabled entirely via a configuration parameter. Example:
862
+ it "some regular test" do
863
+ # local storage here
864
+ end
425
865
 
866
+ it "some test with s3", s3_storage: true do
867
+ # s3 storage here
868
+ end
426
869
  ```
870
+
871
+ Finally, you can also choose to disable execution of all processors during tests:
872
+
873
+ ```ruby
874
+ # spec/support/saviour.rb
875
+
427
876
  Saviour::Config.processing_enabled = false
428
- Saviour::Config.processing_enabled = true
429
877
  ```
430
878
 
431
- You can use this when running tests, for example, or if you want processors to not execute for some reason. The flag can be
432
- changed in real time and is thread-safe.
879
+ This will skip all processors, so you'll avoid image manipulations, etc. If you have a more complex application
880
+ and you can't disable all processors, but still would want to skip only the ones related to image manipulation,
881
+ I would recommend to delegate image manipulation to a specialized class and then stub all of their methods.
882
+
433
883
 
884
+ ### Sources: url and string
434
885
 
435
- ## Versions
886
+ Saviour comes with two small utility classes to encapsulate values to assign as attachments.
436
887
 
437
- Versions in Saviour are treated just like an additional attachment. They require you an additional database column to
438
- persist the file path, and this means you can work with them completely independently of the main file. They can be
439
- assigned, deleted, etc... independently. You just need to work with the versioned `Saviour::File` instance instead of the main
440
- one, so for example when assigning a file you'll need to do `object.file(:thumb).assign(my_file)`.
888
+ If you want to provide directly the contents and filename, you can use `Saviour::StringSource`:
441
889
 
442
- You must create an additional database String column for each version, with the following convention:
890
+ `Post.create! image: Saviour::StringSource.new("hello world", "file.txt")`
443
891
 
444
- `<attached_as>_<version_name>`
892
+ If you want to assign a file stored in an http endpoint, you can use `Saviour::UrlSource`:
445
893
 
446
- The only feature versions gives you is following their main file: A version will be assigned automatically if you assign the
447
- main file, and all versions will be deleted when deleting the main file.
894
+ `Post.create! image: Saviour::UrlSource.new("https://dummyimage.com/600x400/000/fff")`
448
895
 
449
- In case of conflict, the versioned assignation will be preserved. For example, if you assign both the main file and the version,
450
- both of them will be respected and the main file will not propagate to the version in this case.
451
896
 
452
- Defined processors in the Uploader will execute when assigning a version directly. Validations will also execute when assigning
453
- a version directly (see validation section for details).
897
+ ### Custom Storages
454
898
 
455
- When you open a `version` block within an uploader, you can declare some processors (or change the store dir) only for
456
- that version. Note that all processors will be executed for every version that exists, plus one time for the base file.
457
- There are no optimizations done, if your uploader declares one processors first, and from there you open 2 versions,
458
- the first processors will be executed 3 times.
899
+ An storage is a class that implements the public api expected by Saviour. The abstraction expected
900
+ by Saviour is that, whatever the underlying platform or technology, the storage is able to persist
901
+ the given file using the given path as a unique identifier.
459
902
 
903
+ The complete public api that must be satisfied is:
460
904
 
461
- ## Validations
905
+ - write(raw_contents, path): Given raw byte contents and a full path, the storage is expected to
906
+ persist those contents indexed by the given path, so that later on can be retrieved by the same path.
907
+ The return value is ignored.
462
908
 
463
- You can declare validations on your model to implement specific checkings over the contents or the filename of an attachment.
909
+ - read(path): Returns the raw contents stored in the given path.
464
910
 
465
- Take note that validations are executed over the contents given as they are, before any processing. For example you can
466
- have a validation declaring "max file size is 1Mb", assign a file right below the limit, but then process it in a way that
467
- increases its size. You'll be left with a file bigger than 1Mb.
911
+ - write_from_file(file, path): Same as write, but providing a file object rather than raw contents. The storage
912
+ has the opportunity to implement this operation in a more performant way, if possible (local storage does here
913
+ a `cp`, for example). The return value is ignored.
468
914
 
469
- Example of validations:
915
+ - read_to_file(path, file): Same as read, but writing to the given file directly instead of returning raw values.
916
+ The storage has the opportunity to implement this operation in a more performant way, if possible.
917
+ The return value is ignored.
470
918
 
471
- ```
472
- class Post < ActiveRecord::Base
473
- include Saviour::Model
474
- attach_file :image, PostImageUploader
919
+ - delete(path): Removes the file stored at the given path. The return value is ignored.
475
920
 
476
- attach_validation(:image) do |contents, filename|
477
- errors.add(:image, "must be smaller than 10Mb") if contents.bytesize >= 10.megabytes
478
- errors.add(:image, "must be a jpeg file") if File.extname(filename) != ".jpg" # naive, don't copy paste
479
- end
921
+ - exists?(path): Returns a boolean true/false, depending if the given path is present in the storage or not.
922
+ S3 storage implements this with a HEAD request, for example.
923
+
924
+ - public_url(path): Returns a string corresponding to an URL under which the file represented by the given
925
+ path is available.
926
+
927
+ - cp(source_path, destination_path): Copies the file from "source_path" into "destination_path". Overwrites
928
+ "destination_path" if necessary.
929
+
930
+ - mv(source_path, destination_path): Moves the file from "source_path" into "destination_path". Overwrites
931
+ "destination_path" if necessary, and removes the file at "source_path".
932
+
933
+ `cp` and `mv` are explicitly created in order to give a chance to the storage to implement the feature in a more
934
+ performant way, for example, s3 implements `cp` as direct copy inside s3 without downloading/uploading the file.
935
+
936
+ If the given path does not correspond with an existing file, in the case of `read`, `read_to_file`, `delete`, `cp` or
937
+ `mv`, the storage is expected to raise the `Saviour::FileNotPresent` exception.
938
+
939
+ Any additional information the storage may require can be provided on instance creation (on `initialize`) since
940
+ this is not used by Saviour.
941
+
942
+
943
+ ### Bypassing Saviour
944
+
945
+ The only reference to stored files Saviour holds and uses is the path persisted in the database. If you want to,
946
+ you can directly manipulate the storage contents and the database in any custom way and Saviour will just pick
947
+ the changes and work from there.
948
+
949
+ Since Saviour is by design model-based, there may be use cases when this becomes a performance issue, for example:
950
+
951
+ ##### Bypass example: Nested Cloning
952
+
953
+ Say that you have a model `Post` that has many `Image`s, and you're working with S3. `Post` has 3 attachments and
954
+ `Image` has 2 attachments. If you want to do a feature to "clone" a post, a simple implementation would be to
955
+ basically `dup` the instances and save them.
956
+
957
+ However, for a post with many related images, this would represent many api calls and roundtrips to download
958
+ contents and re-upload them. It would be a lot faster to work with s3 directly, issue api calls to copy the
959
+ files inside s3 directly (no download/upload, and even you could issue those api calls concurrently),
960
+ and then assign manually crafted paths directly to the new instances.
961
+
962
+
963
+ ## FAQ
964
+
965
+ ### how to reuse code in your app, attachment with defaults
966
+
967
+ If your application manages many file attachments and you want certain things to apply to all of them, you can
968
+ extract common behaviors into a module:
969
+
970
+ ```ruby
971
+ module FileAttachmentHelpers
972
+ # Shared processors
480
973
  end
481
- ```
482
974
 
483
- Validations will always receive the raw contents of the file. If you need to work with a `File` object you'll need to implement
484
- the necessary conversions.
975
+ module FileAttachment
976
+ extend ActiveSupport::Concern
977
+
978
+ included do
979
+ include Saviour::Model
980
+ end
485
981
 
486
- Validations can also be declared passing a method name instead of a block, like this:
982
+ class_methods do
983
+ def attach_file_with_defaults(*args, &block)
984
+ attached_as = args[0]
487
985
 
488
- ```
489
- class Post < ActiveRecord::Base
490
- include Saviour::Model
491
- attach_file :image, PostImageUploader
492
- attach_validation :image, :check_size
986
+ attach_file(*args) do
987
+ include FileAttachmentHelpers
988
+
989
+ store_dir { "uploads/#{model.class.name.parameterize}/#{model.id}/#{attached_as}" }
493
990
 
494
- private
991
+ instance_eval(&block) if block
992
+ process_with_file :sanitize_filename
993
+ process_with_file :digest_filename
994
+ process_with_file :truncate_at_max_key_size
995
+ end
495
996
 
496
- def check_size(contents, filename)
497
- errors.add(:image, "must be smaller than 10Mb") if contents.bytesize >= 10.megabytes
997
+ attach_validation_with_file(attached_as) do |file, _|
998
+ errors.add(attached_as, 'is an empty file') if ::File.size(file.path).zero?
999
+ end
1000
+ end
1001
+
1002
+ def validate_extension(*validated_attachments, as:)
1003
+ formats = Array.wrap(as).map(&:to_s)
1004
+
1005
+ validated_attachments.each do |attached_as|
1006
+ attach_validation_with_file(attached_as) do |_, filename|
1007
+ ext = ::File.extname(filename)
1008
+ unless formats.include?(ext.downcase.delete('.'))
1009
+ errors.add(attached_as, "must have any of the following extensions: '#{formats}'")
1010
+ end
1011
+ end
1012
+ end
1013
+ end
1014
+ end
1015
+ end
1016
+
1017
+ class Post < ApplicationRecord
1018
+ include FileAttachment
1019
+
1020
+ attach_file_with_defaults :cover # Nothing extra needed
1021
+
1022
+ attach_file_with_defaults :image do
1023
+ process_with_file :some_extra_thing
498
1024
  end
499
1025
  end
500
1026
  ```
501
1027
 
502
- To improve reusability, validation blocks or methods will also receive a third argument (only if declared in your
503
- implementation). This third argument is a hash containing `attached_as` and `version` of the validating file.
1028
+ In this example we're encapsulating many behaviors that will be given for free to any declared attachments:
504
1029
 
1030
+ - `store_dir` computed by default into a path that will be different for each class / id / attached_as.
1031
+ - 3 generic processors are always run, `sanitize_filename` to ensure we'll have a sane url in the end, `digest_filename` to append a digest and `truncate_at_max_key_size` to ensure we don't reach the 1024 bytes imposed by S3.
1032
+ - All attachments will validate that the assigned file must not be empty (0 bytes file).
1033
+ - An utility method is added to allow for validations against the filename extension with `validate_extension :image, as: %w[jpg jpeg png]`
505
1034
 
506
- ## Active Record Lifecycle integration
507
1035
 
508
- On `after_save` Saviour will upload the changed files attached to the current model, executing the processors as needed.
1036
+ ### How to manage file removal from forms
509
1037
 
510
- On `after_destroy` Saviour will delete all the attached files and versions.
1038
+ This feature can be implemented with a temporal flag in the model, which is exposed in the forms and passed via
1039
+ controllers, and a `before_update` to read the value and delete the attachment if present. For example, the
1040
+ `FileAttachment` module exposed in the previous point could be extended as such:
511
1041
 
512
- On `validate` Saviour will execute the validations defined.
1042
+ ```ruby
1043
+ module FileAttachment
1044
+ # ...
1045
+ class_methods do
1046
+ def attach_file_with_defaults(*args, &block)
1047
+ attached_as = args[0]
1048
+ # ...
1049
+
1050
+ define_method("remove_#{attached_as}") do
1051
+ instance_variable_get("@remove_#{attached_as}")
1052
+ end
1053
+
1054
+ alias_method "remove_#{attached_as}?", "remove_#{attached_as}"
1055
+
1056
+ define_method("remove_#{attached_as}=") do |value|
1057
+ instance_variable_set "@remove_#{attached_as}", ActiveRecord::Type::Boolean.new.cast(value)
1058
+ end
1059
+
1060
+ before_update do
1061
+ send("remove_#{attached_as}!") if send("remove_#{attached_as}?")
1062
+ end
1063
+ end
1064
+ end
1065
+ end
1066
+ ```
513
1067
 
514
- When validations are defined, the assigned source will be readed only once. On validation time, it will be readed, passed
515
- to the validation blocks and cached. If the model is valid, the upload will happen from those cached contents. If there
516
- are no validations, the source will be readed only on upload time, after validating the model.
1068
+ Then it can be used as:
517
1069
 
1070
+ ```ruby
1071
+ # This would be a controller code
1072
+ a = Post.find(42)
518
1073
 
519
- ## FAQ
1074
+ # Params received from a form
1075
+ a.update_attributes(remove_image: "t")
1076
+ ```
520
1077
 
521
- This is a compilation of common questions or features regarding file uploads.
522
1078
 
523
- ### Digested filename
1079
+ ### How to extract metadata from files
524
1080
 
525
- A common use case is to create a processor to include a digest of the file in the filename, in order to automatically
526
- expire caches. The implementation is left for the user, but a simple example of such processor is this:
1081
+ You can use processors to accomplish this. Just be aware that processors run concurrently, so if you want to
1082
+ persist you extracted information in the database probably you'll want to use `stash`, see [the section
1083
+ about stash feature for examples](#stash).
527
1084
 
528
- ```
529
- def digest_filename(contents, filename, opts = {})
530
- separator = opts.fetch(:separator, "-")
531
1085
 
532
- digest = ::Digest::MD5.hexdigest(contents)
533
- extension = ::File.extname(filename)
1086
+ ### How to process files in background / delayed
534
1087
 
535
- new_filename = "#{[::File.basename(filename, ".*"), digest].join(separator)}#{extension}"
1088
+ As a previous warning note, pushing logic to be run in the background, when they have visible consequences for the application, may
1089
+ have undesired side effects and added complexity. For example, as you can't be sure about when the delayed job
1090
+ will be completed, your application now needs to handle the uncertainty about the situation: The file processing may
1091
+ or may not have run yet.
536
1092
 
537
- [contents, new_filename]
1093
+ Implementing a delayed processor means that Saviour is no longer involved in the process. You could add the
1094
+ enqueuing of the job when you detect a change in the attachment:
1095
+
1096
+ ```ruby
1097
+ class Post < ApplicationRecord
1098
+ include Saviour::Model
1099
+ attach_file :image
1100
+
1101
+ before_save do
1102
+ if image_changed?
1103
+ # On after commit, enqueue the job
1104
+ end
538
1105
  end
1106
+ end
539
1107
  ```
540
1108
 
1109
+ The job then should take the model and the attachment to process and run the processings directly:
1110
+
1111
+ ```ruby
1112
+ a = Post.find(42)
1113
+ a.image.with_copy do |f|
1114
+ # manipulate f as desired
1115
+ a.update_attributes! image: f
1116
+ end
1117
+ ```
1118
+
1119
+
541
1120
  ### How to recreate versions
542
1121
 
543
- Recreating a version based on the master file can be easily done by just assigning the master file to the version and
544
- saving the model. You just need a little bit more code in order to preserve the current version filename, for example,
545
- if that's something you want.
1122
+ As "versions" are just regular attachments, you only need to assign to it the contents of the main attachment. You can
1123
+ also directly assign attachments between themselves. For example:
546
1124
 
547
- An example service that can do that is the following:
1125
+ ```ruby
1126
+ class Post < ApplicationRecord
1127
+ include Saviour::Model
1128
+
1129
+ attach_file :image, SomeUploader
1130
+ attach_file :image_thumb, SomeUploader, follow: :image, dependent: :destroy
1131
+ end
548
1132
 
1133
+ post = Post.find 42
1134
+ post.image_thumb = post.image
1135
+ post.save!
549
1136
  ```
550
- class SaviourRecreateVersionsService
551
- def initialize(model)
552
- @model = model
553
- end
554
1137
 
555
- def recreate!(attached_as, *versions)
556
- base = @model.send(attached_as).read
1138
+ ### How to digest the filename
1139
+
1140
+ You can use a processor like this one:
1141
+
1142
+ ```ruby
1143
+ def digest_filename(file, filename, opts = {})
1144
+ separator = opts.fetch(:separator, '-')
1145
+
1146
+ digest = ::Digest::MD5.file(file.path).hexdigest
1147
+ extension = ::File.extname(filename)
1148
+
1149
+ previous_filename = ::File.basename(filename, '.*')
557
1150
 
558
- versions.each do |version|
559
- current_filename = @model.send(attached_as, version).filename
560
- @model.send(attached_as, version).assign(Saviour::StringSource.new(base, current_filename))
1151
+ if Regexp.new("[0-9a-f]{32}#{Regexp.escape(extension)}$").match(filename)
1152
+ # Remove the previous digest if found
1153
+ previous_filename = previous_filename.split(separator)[0...-1].join(separator)
561
1154
  end
562
1155
 
563
- @model.save!
1156
+ new_filename = "#{previous_filename}#{separator}#{digest}#{extension}"
1157
+
1158
+ [file, new_filename]
564
1159
  end
565
- end
566
1160
  ```
567
1161
 
568
- ### Getting metadata from the file
1162
+ ## License
1163
+
1164
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
569
1165
 
570
- ### Caching across redisplays in normal forms
571
- ### Introspection (Class.attached_files)
572
- ### Processing in background