paperclip 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +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!