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.
- checksums.yaml +5 -5
- data/.github/issue_template.md +3 -0
- data/MIGRATING-ES.md +317 -0
- data/MIGRATING.md +375 -0
- data/NEWS +17 -0
- data/README.md +26 -4
- data/UPGRADING +3 -3
- data/features/step_definitions/attachment_steps.rb +10 -10
- data/lib/paperclip.rb +1 -0
- data/lib/paperclip/attachment.rb +19 -6
- data/lib/paperclip/filename_cleaner.rb +0 -1
- data/lib/paperclip/geometry_detector_factory.rb +1 -1
- data/lib/paperclip/interpolations.rb +6 -1
- data/lib/paperclip/io_adapters/abstract_adapter.rb +11 -10
- data/lib/paperclip/io_adapters/attachment_adapter.rb +7 -1
- data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +2 -1
- data/lib/paperclip/io_adapters/uri_adapter.rb +8 -6
- data/lib/paperclip/logger.rb +1 -1
- data/lib/paperclip/media_type_spoof_detector.rb +8 -5
- data/lib/paperclip/processor.rb +10 -2
- data/lib/paperclip/schema.rb +1 -1
- data/lib/paperclip/storage/fog.rb +1 -1
- data/lib/paperclip/style.rb +0 -1
- data/lib/paperclip/thumbnail.rb +4 -1
- data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +4 -0
- data/lib/paperclip/version.rb +1 -1
- data/spec/paperclip/attachment_processing_spec.rb +0 -1
- data/spec/paperclip/attachment_spec.rb +17 -2
- data/spec/paperclip/filename_cleaner_spec.rb +0 -1
- data/spec/paperclip/integration_spec.rb +41 -5
- data/spec/paperclip/interpolations_spec.rb +9 -0
- data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +28 -0
- data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +33 -16
- data/spec/paperclip/io_adapters/uri_adapter_spec.rb +56 -8
- data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +1 -1
- data/spec/paperclip/media_type_spoof_detector_spec.rb +26 -0
- data/spec/paperclip/schema_spec.rb +46 -46
- data/spec/paperclip/style_spec.rb +0 -1
- data/spec/paperclip/thumbnail_spec.rb +5 -3
- data/spec/paperclip/url_generator_spec.rb +0 -1
- data/spec/support/model_reconstruction.rb +2 -2
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 898cd227405301490ff021b70120159d15482f1bcf07357f32bc4f14c92a5a46
|
4
|
+
data.tar.gz: 48290676c056a90077da05f9906e8621ab2e8593634606b8bcf8e7beee28ef15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/MIGRATING-ES.md
ADDED
@@ -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!
|
data/MIGRATING.md
ADDED
@@ -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!
|