paperclip 6.0.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.github/issue_template.md +3 -0
  3. data/MIGRATING-ES.md +317 -0
  4. data/MIGRATING.md +375 -0
  5. data/NEWS +17 -0
  6. data/README.md +26 -4
  7. data/UPGRADING +3 -3
  8. data/features/step_definitions/attachment_steps.rb +10 -10
  9. data/lib/paperclip.rb +1 -0
  10. data/lib/paperclip/attachment.rb +19 -6
  11. data/lib/paperclip/filename_cleaner.rb +0 -1
  12. data/lib/paperclip/geometry_detector_factory.rb +1 -1
  13. data/lib/paperclip/interpolations.rb +6 -1
  14. data/lib/paperclip/io_adapters/abstract_adapter.rb +11 -10
  15. data/lib/paperclip/io_adapters/attachment_adapter.rb +7 -1
  16. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +2 -1
  17. data/lib/paperclip/io_adapters/uri_adapter.rb +8 -6
  18. data/lib/paperclip/logger.rb +1 -1
  19. data/lib/paperclip/media_type_spoof_detector.rb +8 -5
  20. data/lib/paperclip/processor.rb +10 -2
  21. data/lib/paperclip/schema.rb +1 -1
  22. data/lib/paperclip/storage/fog.rb +1 -1
  23. data/lib/paperclip/style.rb +0 -1
  24. data/lib/paperclip/thumbnail.rb +4 -1
  25. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +4 -0
  26. data/lib/paperclip/version.rb +1 -1
  27. data/spec/paperclip/attachment_processing_spec.rb +0 -1
  28. data/spec/paperclip/attachment_spec.rb +17 -2
  29. data/spec/paperclip/filename_cleaner_spec.rb +0 -1
  30. data/spec/paperclip/integration_spec.rb +41 -5
  31. data/spec/paperclip/interpolations_spec.rb +9 -0
  32. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +28 -0
  33. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +33 -16
  34. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +56 -8
  35. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +1 -1
  36. data/spec/paperclip/media_type_spoof_detector_spec.rb +26 -0
  37. data/spec/paperclip/schema_spec.rb +46 -46
  38. data/spec/paperclip/style_spec.rb +0 -1
  39. data/spec/paperclip/thumbnail_spec.rb +5 -3
  40. data/spec/paperclip/url_generator_spec.rb +0 -1
  41. data/spec/support/model_reconstruction.rb +2 -2
  42. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 610c7d5a2752fdd632262c73bcfbdf2cfc170663
4
- data.tar.gz: 4c8484cdd5426d92a272d45e0bd9ee46e77e9c07
2
+ SHA256:
3
+ metadata.gz: 898cd227405301490ff021b70120159d15482f1bcf07357f32bc4f14c92a5a46
4
+ data.tar.gz: 48290676c056a90077da05f9906e8621ab2e8593634606b8bcf8e7beee28ef15
5
5
  SHA512:
6
- metadata.gz: 9d03df62f808fe6cb43b50389ed78071a592bd5b72ad73477ccebd08b12bba0f3e6810209e8d00bcb15128c9f359efccf04f874dddc84e1c89c7adcb16fb6ad0
7
- data.tar.gz: e8c3f8a23d4d42d75460a1204f37e5ba75fd4e3a6b58859d1d3e33929a46e253e60d5cbd923e11dda6856ec43d5c19fff3cc57f83a3e08dbe8d2f6574c059286
6
+ metadata.gz: 589b966a2cdf5ccaafa13faa9fb9298e5501274d282aafb098260ffc5268892828ea3f8ddbe4f5ee8de8dcc365031f417577d69b3320cc558eda6b8917ede710
7
+ data.tar.gz: 1b1559aa80cac274756a923a7b943cc4c50e06d984c362764ceeda86f75d9eae2833428dffa0959eee56984e2034e091fd2811aea30b9e933713bfb0070cb107
@@ -0,0 +1,3 @@
1
+ ## Deprecation notice
2
+
3
+ Paperclip is currently undergoing [deprecation in favor of ActiveStorage](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md). Maintainers of this repository will no longer be tending to new issues. We're leaving the issues page open so Paperclip users can still see & search through old issues, and continue existing discussions if they wish.
@@ -0,0 +1,317 @@
1
+ # Migrando de Paperclip a ActiveStorage
2
+
3
+ Paperclip y ActiveStorage resuelven problemas similares con soluciones
4
+ similares, por lo que pasar de uno a otro es simple.
5
+
6
+ El proceso de ir desde Paperclip hacia ActiveStorage es como sigue:
7
+
8
+ 1. Implementa las migraciones a la base de datos de ActiveStorage.
9
+ 2. Configura el almacenamiento.
10
+ 3. Copia la base de datos.
11
+ 4. Copia los archivos.
12
+ 5. Actualiza tus pruebas.
13
+ 6. Actualiza tus vistas.
14
+ 7. Actualiza tus controladores.
15
+ 8. Actualiza tus modelos.
16
+
17
+ ## Implementa las migraciones a la base de datos de ActiveStorage
18
+
19
+ Sigue [las instrucciones para instalar ActiveStorage]. Muy probablemente vas a
20
+ querer agregar la gema `mini_magick` a tu Gemfile.
21
+
22
+
23
+ ```sh
24
+ rails active_storage:install
25
+ ```
26
+
27
+ [las instrucciones para instalar ActiveStorage]: https://github.com/rails/rails/blob/master/activestorage/README.md#installation
28
+
29
+ ## Configura el almacenamiento
30
+
31
+ De nuevo, sigue [las instrucciones para configurar ActiveStorage].
32
+
33
+ [las instrucciones para configurar ActiveStorage]: http://edgeguides.rubyonrails.org/active_storage_overview.html#setup
34
+
35
+ ## Copia la base de datos.
36
+
37
+ Las tablas `active_storage_blobs` y`active_storage_attachments` son en donde
38
+ ActiveStorage espera encontrar los metadatos del archivo. Paperclip almacena los
39
+ metadatos del archivo directamente en en la tabla del objeto asociado.
40
+
41
+ Vas a necesitar escribir una migración para esta conversión. Proveer un script
42
+ simple, es complicado porque están involucrados tus modelos. ¡Pero lo
43
+ intentaremos!
44
+
45
+ Así sería para un `User` con un `avatar` en Paperclip:
46
+
47
+ ```ruby
48
+ class User < ApplicationRecord
49
+ has_attached_file :avatar
50
+ end
51
+ ```
52
+
53
+ Tus migraciones de Paperclip producirán una tabla como la siguiente:
54
+
55
+ ```ruby
56
+ create_table "users", force: :cascade do |t|
57
+ t.string "avatar_file_name"
58
+ t.string "avatar_content_type"
59
+ t.integer "avatar_file_size"
60
+ t.datetime "avatar_updated_at"
61
+ end
62
+ ```
63
+
64
+ Y tu la convertirás en estas tablas:
65
+
66
+ ```ruby
67
+ create_table "active_storage_attachments", force: :cascade do |t|
68
+ t.string "name", null: false
69
+ t.string "record_type", null: false
70
+ t.integer "record_id", null: false
71
+ t.integer "blob_id", null: false
72
+ t.datetime "created_at", null: false
73
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
74
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
75
+ end
76
+ ```
77
+
78
+ ```ruby
79
+ create_table "active_storage_blobs", force: :cascade do |t|
80
+ t.string "key", null: false
81
+ t.string "filename", null: false
82
+ t.string "content_type"
83
+ t.text "metadata"
84
+ t.bigint "byte_size", null: false
85
+ t.string "checksum", null: false
86
+ t.datetime "created_at", null: false
87
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
88
+ end
89
+ ```
90
+
91
+ Así que asumiendo que quieres dejar los archivos en el mismo lugar, _esta es tu
92
+ migración_. De otra forma, ve la siguiente sección primero y modifica la
93
+ migración como corresponda.
94
+
95
+ ```ruby
96
+ Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }
97
+
98
+ class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
99
+ require 'open-uri'
100
+
101
+ def up
102
+ # postgres
103
+ get_blob_id = 'LASTVAL()'
104
+ # mariadb
105
+ # get_blob_id = 'LAST_INSERT_ID()'
106
+ # sqlite
107
+ # get_blob_id = 'LAST_INSERT_ROWID()'
108
+
109
+ active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
110
+ INSERT INTO active_storage_blobs (
111
+ key, filename, content_type, metadata, byte_size,
112
+ checksum, created_at
113
+ ) VALUES (?, ?, ?, '{}', ?, ?, ?)
114
+ SQL
115
+
116
+ active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
117
+ INSERT INTO active_storage_attachments (
118
+ name, record_type, record_id, blob_id, created_at
119
+ ) VALUES (?, ?, ?, #{get_blob_id}, ?)
120
+ SQL
121
+
122
+ models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
123
+
124
+ transaction do
125
+ models.each do |model|
126
+ attachments = model.column_names.map do |c|
127
+ if c =~ /(.+)_file_name$/
128
+ $1
129
+ end
130
+ end.compact
131
+
132
+ model.find_each.each do |instance|
133
+ attachments.each do |attachment|
134
+ active_storage_blob_statement.execute(
135
+ key(instance, attachment),
136
+ instance.send("#{attachment}_file_name"),
137
+ instance.send("#{attachment}_content_type"),
138
+ instance.send("#{attachment}_file_size"),
139
+ checksum(instance.send(attachment)),
140
+ instance.updated_at.iso8601
141
+ )
142
+
143
+ active_storage_attachment_statement.
144
+ execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
145
+ end
146
+ end
147
+ end
148
+ end
149
+
150
+ active_storage_attachment_statement.close
151
+ active_storage_blob_statement.close
152
+ end
153
+
154
+ def down
155
+ raise ActiveRecord::IrreversibleMigration
156
+ end
157
+
158
+ private
159
+
160
+ def key(instance, attachment)
161
+ SecureRandom.uuid
162
+ # Alternativamente:
163
+ # instance.send("#{attachment}_file_name")
164
+ end
165
+
166
+ def checksum(attachment)
167
+ # archivos locales almacenados en disco:
168
+ url = attachment.path
169
+ Digest::MD5.base64digest(File.read(url))
170
+
171
+ # archivos remotos almacenados en la computadora de alguién más:
172
+ # url = attachment.url
173
+ # Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
174
+ end
175
+ end
176
+ ```
177
+
178
+ ## Copia los archivos
179
+
180
+ La migración de arriba deja los archivos como estaban. Sin embargo,
181
+ los servicios de Paperclip y ActiveStorage utilizan diferentes ubicaciones.
182
+
183
+ Por defecto, Paperclip se ve así:
184
+
185
+ ```
186
+ public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
187
+ ```
188
+
189
+ Y ActiveStorage se ve así:
190
+
191
+ ```
192
+ storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
193
+ ```
194
+
195
+ Ese `xMRXuT6nqpoiConJFQJFt6c9` es el valor de `active_storage_blobs.key`. En la
196
+ migración de arriba usamos simplemente el nombre del archivo, pero tal vez
197
+ quieras usar un UUID.
198
+
199
+ Migrando los archivos en un hospedaje externo (S3, Azure Storage, GCS, etc.)
200
+ está fuera del alcance de este documento inicial. Así es como se vería para un
201
+ almacenamiento local:
202
+
203
+ ```ruby
204
+ #!bin/rails runner
205
+
206
+ class ActiveStorageBlob < ActiveRecord::Base
207
+ end
208
+
209
+ class ActiveStorageAttachment < ActiveRecord::Base
210
+ belongs_to :blob, class_name: 'ActiveStorageBlob'
211
+ belongs_to :record, polymorphic: true
212
+ end
213
+
214
+ ActiveStorageAttachment.find_each do |attachment|
215
+ name = attachment.name
216
+
217
+ source = attachment.record.send(name).path
218
+ dest_dir = File.join(
219
+ "storage",
220
+ attachment.blob.key.first(2),
221
+ attachment.blob.key.first(4).last(2))
222
+ dest = File.join(dest_dir, attachment.blob.key)
223
+
224
+ FileUtils.mkdir_p(dest_dir)
225
+ puts "Moving #{source} to #{dest}"
226
+ FileUtils.cp(source, dest)
227
+ end
228
+ ```
229
+
230
+ ## Actualiza tus pruebas
231
+
232
+ En lugar de utilizar `have_attached_file`, será necesario que escribas tu propio
233
+ matcher. Aquí hay un matcher similar _en espíritu_ al que Paperclip provee:
234
+
235
+
236
+ ```ruby
237
+ RSpec::Matchers.define :have_attached_file do |name|
238
+ matches do |record|
239
+ file = record.send(name)
240
+ file.respond_to?(:variant) && file.respond_to?(:attach)
241
+ end
242
+ end
243
+ ```
244
+
245
+ ## Actualiza tus vistas
246
+
247
+ En Paperclip se ven así:
248
+
249
+ ```ruby
250
+ image_tag @user.avatar.url(:medium)
251
+ ```
252
+
253
+ En ActiveStorage se ven así:
254
+
255
+ ```ruby
256
+ image_tag @user.avatar.variant(resize: "250x250")
257
+ ```
258
+
259
+ ## Actualiza tus controladores
260
+
261
+ Esto no debería _requerir_ ningúna actualización. Sin embargo, si te fijas en
262
+ el schema de tu base de datos, notaras un join.
263
+
264
+ Por ejemplo si tu controlador tiene:
265
+
266
+ ```ruby
267
+ def index
268
+ @users = User.all.order(:name)
269
+ end
270
+ ```
271
+
272
+ Y tu vista tiene:
273
+
274
+ ```
275
+ <ul>
276
+ <% @users.each do |user| %>
277
+ <li><%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %></li>
278
+ <% end %>
279
+ </ul>
280
+ ```
281
+
282
+ Vas a terminar con un n+1, ya que descargas cada archivo adjunto dentro del
283
+ bucle.
284
+
285
+ Así que mientras que el controlador y el modelo funcionarán sin ningún cambio,
286
+ tal vez quieras revisar dos veces tus bucles y agregar `includes` en dónde haga
287
+ falta.
288
+
289
+ ActiveStorage agrega `avatar_attachment` y `avatar_blob` a las relaciones del
290
+ tipo `has-one`, así como `avatar_attachments` y `avatar_blobs` a las relaciones
291
+ de tipo `has-many`:
292
+
293
+ ```ruby
294
+ def index
295
+ @users = User.all.order(:name).includes(:avatar_attachment)
296
+ end
297
+ ```
298
+
299
+ ## Actualiza tus modelos
300
+
301
+ Sigue [la guía sobre cómo adjuntar archivos a los registros]. Por ejemplo, un
302
+ `User` con un `avatar` se representa como:
303
+
304
+ ```ruby
305
+ class User < ApplicationRecord
306
+ has_one_attached :avatar
307
+ end
308
+ ```
309
+
310
+ Cualquier cambio de tamaño se hace en la vista como un `variant`.
311
+
312
+ [la guía sobre cómo adjuntar archivos a los registros]: http://edgeguides.rubyonrails.org/active_storage_overview.html#attaching-files-to-records
313
+
314
+ ## Quita Paperclip
315
+
316
+ Quita la gema de tu `Gemfile` y corre `bundle`. Corre tus pruebas porque ya
317
+ terminaste!
@@ -0,0 +1,375 @@
1
+ # Migrating from Paperclip to ActiveStorage
2
+
3
+ Paperclip and ActiveStorage solve similar problems with similar solutions, so
4
+ transitioning from one to the other is straightforward data re-writing.
5
+
6
+ The process of going from Paperclip to ActiveStorage is as follows:
7
+
8
+ 1. Apply the ActiveStorage database migrations.
9
+ 2. Configure storage.
10
+ 3. Copy the database data over.
11
+ 4. Copy the files over.
12
+ 5. Update your tests.
13
+ 6. Update your views.
14
+ 7. Update your controllers.
15
+ 8. Update your models.
16
+
17
+ ## Apply the ActiveStorage database migrations
18
+
19
+ Follow [the instructions for installing ActiveStorage]. You'll very likely want
20
+ to add the `mini_magick` gem to your Gemfile.
21
+
22
+ ```sh
23
+ rails active_storage:install
24
+ ```
25
+
26
+ [the instructions for installing ActiveStorage]: https://github.com/rails/rails/blob/master/activestorage/README.md#installation
27
+
28
+ ## Configure storage
29
+
30
+ Again, follow [the instructions for configuring ActiveStorage].
31
+
32
+ [the instructions for configuring ActiveStorage]: http://edgeguides.rubyonrails.org/active_storage_overview.html#setup
33
+
34
+ ## Copy the database data over
35
+
36
+ The `active_storage_blobs` and `active_storage_attachments` tables are where
37
+ ActiveStorage expects to find file metadata. Paperclip stores the file metadata
38
+ directly on the associated object's table.
39
+
40
+ You'll need to write a migration for this conversion. Because the models for
41
+ your domain are involved, it's tricky to supply a simple script. But we'll try!
42
+
43
+ Here's how it would go for a `User` with an `avatar`, that is this in
44
+ Paperclip:
45
+
46
+ ```ruby
47
+ class User < ApplicationRecord
48
+ has_attached_file :avatar
49
+ end
50
+ ```
51
+
52
+ Your Paperclip migrations will produce a table like so:
53
+
54
+ ```ruby
55
+ create_table "users", force: :cascade do |t|
56
+ t.string "avatar_file_name"
57
+ t.string "avatar_content_type"
58
+ t.integer "avatar_file_size"
59
+ t.datetime "avatar_updated_at"
60
+ end
61
+ ```
62
+
63
+ And you'll be converting into these tables:
64
+
65
+ ```ruby
66
+ create_table "active_storage_attachments", force: :cascade do |t|
67
+ t.string "name", null: false
68
+ t.string "record_type", null: false
69
+ t.integer "record_id", null: false
70
+ t.integer "blob_id", null: false
71
+ t.datetime "created_at", null: false
72
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
73
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
74
+ end
75
+ ```
76
+
77
+ ```ruby
78
+ create_table "active_storage_blobs", force: :cascade do |t|
79
+ t.string "key", null: false
80
+ t.string "filename", null: false
81
+ t.string "content_type"
82
+ t.text "metadata"
83
+ t.bigint "byte_size", null: false
84
+ t.string "checksum", null: false
85
+ t.datetime "created_at", null: false
86
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
87
+ end
88
+ ```
89
+
90
+ So, assuming you want to leave the files in the exact same place, _this is
91
+ your migration_. Otherwise, see the next section first and modify the migration
92
+ to taste.
93
+
94
+ ```ruby
95
+ Dir[Rails.root.join("app/models/**/*.rb")].sort.each { |file| require file }
96
+
97
+ class ConvertToActiveStorage < ActiveRecord::Migration[5.2]
98
+ require 'open-uri'
99
+
100
+ def up
101
+ # postgres
102
+ get_blob_id = 'LASTVAL()'
103
+ # mariadb
104
+ # get_blob_id = 'LAST_INSERT_ID()'
105
+ # sqlite
106
+ # get_blob_id = 'LAST_INSERT_ROWID()'
107
+
108
+ active_storage_blob_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
109
+ INSERT INTO active_storage_blobs (
110
+ `key`, filename, content_type, metadata, byte_size, checksum, created_at
111
+ ) VALUES (?, ?, ?, '{}', ?, ?, ?)
112
+ SQL
113
+
114
+ active_storage_attachment_statement = ActiveRecord::Base.connection.raw_connection.prepare(<<-SQL)
115
+ INSERT INTO active_storage_attachments (
116
+ name, record_type, record_id, blob_id, created_at
117
+ ) VALUES (?, ?, ?, #{get_blob_id}, ?)
118
+ SQL
119
+
120
+ models = ActiveRecord::Base.descendants.reject(&:abstract_class?)
121
+
122
+ transaction do
123
+ models.each do |model|
124
+ attachments = model.column_names.map do |c|
125
+ if c =~ /(.+)_file_name$/
126
+ $1
127
+ end
128
+ end.compact
129
+
130
+ model.find_each.each do |instance|
131
+ attachments.each do |attachment|
132
+ active_storage_blob_statement.execute(
133
+ key(instance, attachment),
134
+ instance.send("#{attachment}_file_name"),
135
+ instance.send("#{attachment}_content_type"),
136
+ instance.send("#{attachment}_file_size"),
137
+ checksum(instance.send(attachment)),
138
+ instance.updated_at.iso8601
139
+ )
140
+
141
+ active_storage_attachment_statement.
142
+ execute(attachment, model.name, instance.id, instance.updated_at.iso8601)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ active_storage_attachment_statement.close
149
+ active_storage_blob_statement.close
150
+ end
151
+
152
+ def down
153
+ raise ActiveRecord::IrreversibleMigration
154
+ end
155
+
156
+ private
157
+
158
+ def key(instance, attachment)
159
+ SecureRandom.uuid
160
+ # Alternatively:
161
+ # instance.send("#{attachment}_file_name")
162
+ end
163
+
164
+ def checksum(attachment)
165
+ # local files stored on disk:
166
+ url = attachment.path
167
+ Digest::MD5.base64digest(File.read(url))
168
+
169
+ # remote files stored on another person's computer:
170
+ # url = attachment.url
171
+ # Digest::MD5.base64digest(Net::HTTP.get(URI(url)))
172
+ end
173
+ end
174
+ ```
175
+
176
+ ## Copy the files over
177
+
178
+ The above migration leaves the files as they are. However, the default
179
+ Paperclip and ActiveStorage storage services use different locations.
180
+
181
+ By default, Paperclip looks like this:
182
+
183
+ ```
184
+ public/system/users/avatars/000/000/004/original/the-mystery-of-life.png
185
+ ```
186
+
187
+ And ActiveStorage looks like this:
188
+
189
+ ```
190
+ storage/xM/RX/xMRXuT6nqpoiConJFQJFt6c9
191
+ ```
192
+
193
+ That `xMRXuT6nqpoiConJFQJFt6c9` is the `active_storage_blobs.key` value. In the
194
+ migration above we simply used the filename but you may wish to use a UUID
195
+ instead.
196
+
197
+
198
+ ### Moving local storage files
199
+
200
+ ```ruby
201
+ #!bin/rails runner
202
+
203
+ class ActiveStorageBlob < ActiveRecord::Base
204
+ end
205
+
206
+ class ActiveStorageAttachment < ActiveRecord::Base
207
+ belongs_to :blob, class_name: 'ActiveStorageBlob'
208
+ belongs_to :record, polymorphic: true
209
+ end
210
+
211
+ ActiveStorageAttachment.find_each do |attachment|
212
+ name = attachment.name
213
+
214
+ source = attachment.record.send(name).path
215
+ dest_dir = File.join(
216
+ "storage",
217
+ attachment.blob.key.first(2),
218
+ attachment.blob.key.first(4).last(2))
219
+ dest = File.join(dest_dir, attachment.blob.key)
220
+
221
+ FileUtils.mkdir_p(dest_dir)
222
+ puts "Moving #{source} to #{dest}"
223
+ FileUtils.cp(source, dest)
224
+ end
225
+ ```
226
+
227
+ ### Moving files on a remote host (S3, Azure Storage, GCS, etc.)
228
+
229
+ One of the most straightforward ways to move assets stored on a remote host is
230
+ to use a rake task that regenerates the file names and places them in the
231
+ proper file structure/hierarchy.
232
+
233
+ Assuming you have a model configured similarly to the example below:
234
+
235
+ ```ruby
236
+ class Organization < ApplicationRecord
237
+ # New ActiveStorage declaration
238
+ has_one_attached :logo
239
+
240
+ # Old Paperclip config
241
+ # must be removed BEFORE to running the rake task so that
242
+ # all of the new ActiveStorage goodness can be used when
243
+ # calling organization.logo
244
+ has_attached_file :logo,
245
+ path: "/organizations/:id/:basename_:style.:extension",
246
+ default_url: "https://s3.amazonaws.com/xxxxx/organizations/missing_:style.jpg",
247
+ default_style: :normal,
248
+ styles: { thumb: "64x64#", normal: "400x400>" },
249
+ convert_options: { thumb: "-quality 100 -strip", normal: "-quality 75 -strip" }
250
+ end
251
+ ```
252
+
253
+ The following rake task would migrate all of your assets:
254
+
255
+ ```ruby
256
+ namespace :organizations do
257
+ task migrate_to_active_storage: :environment do
258
+ Organization.where.not(logo_file_name: nil).find_each do |organization|
259
+ # This step helps us catch any attachments we might have uploaded that
260
+ # don't have an explicit file extension in the filename
261
+ image = organization.logo_file_name
262
+ ext = File.extname(image)
263
+ image_original = URI.unescape(image.gsub(ext, "_original#{ext}"))
264
+
265
+ # this url pattern can be changed to reflect whatever service you use
266
+ logo_url = "https://s3.amazonaws.com/xxxxx/organizations/#{organization.id}/#{image_original}"
267
+ organization.logo.attach(io: open(logo_url),
268
+ filename: organization.logo_file_name,
269
+ content_type: organization.logo_content_type)
270
+ end
271
+ end
272
+ end
273
+ ```
274
+
275
+ An added advantage of this method is that you're creating a copy of all assets,
276
+ which is handy in the event you need to rollback your deploy.
277
+
278
+ This also means that you can run the rake task from your development machine
279
+ and completely migrate the assets before your deploy, minimizing the chances
280
+ that you'll have a timed-out deployment.
281
+
282
+ The main drawback of this method is the same as its benefit - you are
283
+ essentially duplicating all of your assets. These days storage and bandwidth
284
+ are relatively cheap, but in some instances where you have a huge volume of
285
+ files, or very large file sizes, this might get a little less feasible.
286
+
287
+ In my experience I was able to move tens of thousands of images in a matter of
288
+ a couple of hours, just by running the migration overnight on my MacBook Pro.
289
+
290
+ Once you've confirmed that the migration and deploy have gone successfully you
291
+ can safely delete the old assets from your remote host.
292
+
293
+ ## Update your tests
294
+
295
+ Instead of the `have_attached_file` matcher, you'll need to write your own.
296
+ Here's one that is similar in spirit to the Paperclip-supplied matcher:
297
+
298
+ ```ruby
299
+ RSpec::Matchers.define :have_attached_file do |name|
300
+ matches do |record|
301
+ file = record.send(name)
302
+ file.respond_to?(:variant) && file.respond_to?(:attach)
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Update your views
308
+
309
+ In Paperclip it looks like this:
310
+
311
+ ```ruby
312
+ image_tag @user.avatar.url(:medium)
313
+ ```
314
+
315
+ In ActiveStorage it looks like this:
316
+
317
+ ```ruby
318
+ image_tag @user.avatar.variant(resize: "250x250")
319
+ ```
320
+
321
+ ## Update your controllers
322
+
323
+ This should _require_ no update. However, if you glance back at the database
324
+ schema above, you may notice a join.
325
+
326
+ For example, if your controller has
327
+
328
+ ```ruby
329
+ def index
330
+ @users = User.all.order(:name)
331
+ end
332
+ ```
333
+
334
+ And your view has
335
+
336
+ ```
337
+ <ul>
338
+ <% @users.each do |user| %>
339
+ <li><%= image_tag user.avatar.variant(resize: "10x10"), alt: user.name %></li>
340
+ <% end %>
341
+ </ul>
342
+ ```
343
+
344
+ Then you'll end up with an n+1 as you load each attachment in the loop.
345
+
346
+ So while the controller and model will work without change, you will want to
347
+ double-check your loops and add `includes` as needed. ActiveStorage adds an
348
+ `avatar_attachment` and `avatar_blob` relationship to has-one relations, and
349
+ `avatar_attachments` and `avatar_blobs` to has-many:
350
+
351
+ ```ruby
352
+ def index
353
+ @users = User.all.order(:name).includes(:avatar_attachment)
354
+ end
355
+ ```
356
+
357
+ ## Update your models
358
+
359
+ Follow [the guide on attaching files to records]. For example, a `User` with an
360
+ `avatar` is represented as:
361
+
362
+ ```ruby
363
+ class User < ApplicationRecord
364
+ has_one_attached :avatar
365
+ end
366
+ ```
367
+
368
+ Any resizing is done in the view as a variant.
369
+
370
+ [the guide on attaching files to records]: http://edgeguides.rubyonrails.org/active_storage_overview.html#attaching-files-to-records
371
+
372
+ ## Remove Paperclip
373
+
374
+ Remove the Gem from your `Gemfile` and run `bundle`. Run your tests because
375
+ you're done!