nochmal 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/main.yml +16 -0
- data/.gitignore +27 -0
- data/.rspec +5 -0
- data/.rubocop.yml +35 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/rake +28 -0
- data/bin/rspec +28 -0
- data/bin/setup +8 -0
- data/lib/nochmal/adapters/active_storage.rb +92 -0
- data/lib/nochmal/adapters/base.rb +104 -0
- data/lib/nochmal/adapters/carrierwave.rb +58 -0
- data/lib/nochmal/adapters/carrierwave_analyze.rb +222 -0
- data/lib/nochmal/adapters/carrierwave_migration.rb +97 -0
- data/lib/nochmal/adapters/carrierwave_resume.rb +39 -0
- data/lib/nochmal/migration_data/create_tables.rb +33 -0
- data/lib/nochmal/migration_data/incomplete.rb +18 -0
- data/lib/nochmal/migration_data/meta.rb +43 -0
- data/lib/nochmal/migration_data/status.rb +27 -0
- data/lib/nochmal/migration_data/status_exists.rb +31 -0
- data/lib/nochmal/output.rb +127 -0
- data/lib/nochmal/railtie.rb +10 -0
- data/lib/nochmal/reupload.rb +95 -0
- data/lib/nochmal/version.rb +5 -0
- data/lib/nochmal.rb +36 -0
- data/lib/tasks/nochmal.rake +51 -0
- data/nochmal.gemspec +52 -0
- metadata +290 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module Adapters
|
5
|
+
# collect common things for the carrierwave analysis and migration
|
6
|
+
class Carrierwave < Base
|
7
|
+
PREFIX = "carrierwave_"
|
8
|
+
|
9
|
+
def models_with_attachments
|
10
|
+
@models_with_attachments ||= begin
|
11
|
+
Rails.application.eager_load!
|
12
|
+
|
13
|
+
ActiveRecord::Base
|
14
|
+
.descendants
|
15
|
+
.reject(&:abstract_class?)
|
16
|
+
.select { |model| carrierwave?(model) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def collection(model, uploader)
|
21
|
+
maybe_sti_scope(model).where.not(db_column(uploader) => nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def blob(attachment)
|
25
|
+
Pathname.new(
|
26
|
+
attachment.file.file.gsub(
|
27
|
+
attachment.mounted_as.to_s,
|
28
|
+
not_prefixed(attachment.mounted_as).to_s
|
29
|
+
)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def db_column(uploader_name)
|
36
|
+
not_prefixed(uploader_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def not_prefixed(type)
|
40
|
+
type.to_s.delete_prefix(PREFIX).to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
def prefixed(type)
|
44
|
+
:"#{PREFIX}#{not_prefixed(type)}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def uploader(model, type)
|
48
|
+
@uploaders[model][type]
|
49
|
+
end
|
50
|
+
|
51
|
+
def carrierwave?(model)
|
52
|
+
model.uploaders.any? do |_name, uploader|
|
53
|
+
uploader.new.is_a? CarrierWave::Uploader::Base
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module Adapters
|
5
|
+
# Wrap ActiveStorageHelper and use carrierwave as simulated from_storage_service
|
6
|
+
class CarrierwaveAnalyze < Carrierwave # rubocop:disable Metrics/ClassLength
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
|
10
|
+
@carrierwave_changed = []
|
11
|
+
@variants_present = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def attachment_types_for(model)
|
15
|
+
@types[model] ||= model.uploaders.map do |uploader, uploader_class|
|
16
|
+
@uploaders[model] = { uploader => uploader_class }
|
17
|
+
model.has_one_attached prefixed(uploader), service: @to_storage_service.name
|
18
|
+
|
19
|
+
uploader
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def empty_collection?(_model, _uploader)
|
24
|
+
false # simulate that uploads exist
|
25
|
+
end
|
26
|
+
|
27
|
+
# actions
|
28
|
+
|
29
|
+
def reupload(_record, _type)
|
30
|
+
{ status: :ok } # like count
|
31
|
+
end
|
32
|
+
|
33
|
+
# hooks
|
34
|
+
|
35
|
+
def general_notes
|
36
|
+
[
|
37
|
+
display_helper_notes,
|
38
|
+
gemfile_additions
|
39
|
+
].join("\n")
|
40
|
+
end
|
41
|
+
|
42
|
+
def type_notes(model = nil, type = nil)
|
43
|
+
return nil if @carrierwave_changed.include?(model.base_class.sti_name)
|
44
|
+
|
45
|
+
@carrierwave_changed << model.base_class.sti_name
|
46
|
+
uploader = uploader(model, type)
|
47
|
+
|
48
|
+
[
|
49
|
+
carrierwave_change(model, type, uploader),
|
50
|
+
active_storage_change(type, uploader),
|
51
|
+
validation_notes(model, type, uploader),
|
52
|
+
uploader_change(uploader), "\n"
|
53
|
+
].join
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def carrierwave_change(model, type, uploader)
|
59
|
+
<<~TEXT
|
60
|
+
# replace #{model.name.underscore}.#{type}_url in your views
|
61
|
+
# replace #{model.name.underscore}.#{type} in your views
|
62
|
+
# maybe search for #{type} in your codebase to find everything...
|
63
|
+
#
|
64
|
+
# Change carrierwave-uploader in #{model.name}:
|
65
|
+
class #{model.name}
|
66
|
+
mount_uploader :#{prefixed(type)}, #{uploader.name}, mount_on: '#{type}'
|
67
|
+
TEXT
|
68
|
+
end
|
69
|
+
|
70
|
+
def active_storage_change(type, uploader)
|
71
|
+
versions = uploader.versions.map do |name, version|
|
72
|
+
"attachable.variant :#{name}, #{version.processors.map(&:compact).to_h}"
|
73
|
+
end
|
74
|
+
|
75
|
+
service = ", service: :#{@to}" unless @to.nil?
|
76
|
+
|
77
|
+
return " has_one_attached :#{type}#{service}" if versions.none?
|
78
|
+
|
79
|
+
@variants_present = true
|
80
|
+
|
81
|
+
<<~TEXT
|
82
|
+
has_one_attached :#{type}#{service} do |attachable|
|
83
|
+
#{versions.join}
|
84
|
+
end
|
85
|
+
# uploader #{type} has #{versions.size} versions
|
86
|
+
|
87
|
+
# allow removal, carrierwave-style
|
88
|
+
def remove_#{type}; false end
|
89
|
+
def remove_#{type}=(delete_it); #{type}.purge_later if delete_it; end
|
90
|
+
TEXT
|
91
|
+
end
|
92
|
+
|
93
|
+
def validation_notes(model, type, uploader)
|
94
|
+
<<~TEXT
|
95
|
+
# Check for #{type} validations in #{model} and #{uploader}
|
96
|
+
# Take a look at https://github.com/igorkasyanchuk/active_storage_validations
|
97
|
+
#
|
98
|
+
# Please make your validation switchable, the validation does not
|
99
|
+
# work properly for the migration.
|
100
|
+
if ENV['NOCHMAL_MIGRATION'].blank? # if not migrating RIGHT NOW, i.e. normal case
|
101
|
+
validates :picture, dimension: { width: { max: 8_000 }, height: { max: 8_000 } },
|
102
|
+
content_type: ['image/jpeg', 'image/gif', 'image/png']
|
103
|
+
end
|
104
|
+
end
|
105
|
+
TEXT
|
106
|
+
end
|
107
|
+
|
108
|
+
def uploader_change(uploader)
|
109
|
+
<<~TEXT
|
110
|
+
# Ensure that #{uploader}#store_dir does not have a prefix of
|
111
|
+
# "carrierwave_". To find the existing files, you need to add
|
112
|
+
# "mounted_as.to_s.delete_prefix('carrierwave_')" at the appropriate
|
113
|
+
# location. If there is no "carrierwave_"-prefix in the generated path,
|
114
|
+
# everything is fine.
|
115
|
+
TEXT
|
116
|
+
end
|
117
|
+
|
118
|
+
def display_helper_notes
|
119
|
+
<<~RUBY
|
120
|
+
module UploadDisplayHelper
|
121
|
+
# This method provides a facade to serve uploads either from ActiveStorage or
|
122
|
+
# CarrierWave
|
123
|
+
#
|
124
|
+
# Usage:
|
125
|
+
#
|
126
|
+
# upload_url(person, :picture)
|
127
|
+
# upload_url(person, :picture, size: '72x72')
|
128
|
+
# upload_url(person, :picture, size: '72x72')
|
129
|
+
# upload_url(person, :picture, variant: :thumb)
|
130
|
+
# upload_url(person, :picture, variant: :thumb, default: 'profil')
|
131
|
+
#
|
132
|
+
# could be
|
133
|
+
#
|
134
|
+
# person.picture or
|
135
|
+
# person.picture.variant(resize_to_limit: [72, 72]) or
|
136
|
+
# person.picture.variant(:thumb)
|
137
|
+
#
|
138
|
+
# This helper returns a suitable first argument for image_tag (the image location),
|
139
|
+
# but also for the second arg of link_to (the target).
|
140
|
+
def upload_url(model, name, size: nil, default: model.class.name.underscore, variant: nil) # rubocop:disable Metrics/MethodLength,Metrics/PerceivedComplexity
|
141
|
+
return upload_variant(model, name, variant, default: default) if variant.present?
|
142
|
+
|
143
|
+
if model.send(name.to_sym).attached?
|
144
|
+
model.send(name.to_sym).yield_self do |pic|
|
145
|
+
if size
|
146
|
+
# variant passes to mini_magick or vips, I assume mini_magick here
|
147
|
+
pic.variant(resize_to_limit: extract_image_dimensions(size))
|
148
|
+
else
|
149
|
+
pic
|
150
|
+
end
|
151
|
+
end
|
152
|
+
elsif model.respond_to?(:"carrierwave_#{name}") && model.send(:"carrierwave_#{name}")
|
153
|
+
model.send(:"carrierwave_#{name}_url")
|
154
|
+
else
|
155
|
+
upload_default(default)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# return the filename of the uploaded file
|
160
|
+
def upload_name(model, name)
|
161
|
+
if model.send(name.to_sym).attached?
|
162
|
+
model.send(name.to_sym).filename.to_s
|
163
|
+
elsif model.respond_to?(:"carrierwave_#{name}_identifier")
|
164
|
+
model.send(:"carrierwave_#{name}_identifier")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def upload_exists?(model, name)
|
169
|
+
return true if model.send(name.to_sym).attached?
|
170
|
+
|
171
|
+
if model.respond_to?(:"carrierwave_#{name}")
|
172
|
+
model.send(:"carrierwave_#{name}").present?
|
173
|
+
else
|
174
|
+
false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def upload_variant(model, name, variant, default: model.name.underscore)
|
181
|
+
if model.send(name.to_sym).attached?
|
182
|
+
model.send(name.to_sym).variant(variant.to_sym)
|
183
|
+
elsif model.respond_to?(:"carrierwave_#{name}")
|
184
|
+
model.send(:"carrierwave_#{name}").send(variant.to_sym).url
|
185
|
+
else
|
186
|
+
upload_default([default, variant].compact.map(&:to_s).join('_'))
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def upload_default(png_name = 'profil')
|
191
|
+
ActionController::Base.helpers.asset_pack_path("media/images/#{png_name}.png")
|
192
|
+
end
|
193
|
+
|
194
|
+
def extract_image_dimensions(width_x_height)
|
195
|
+
case width_x_height
|
196
|
+
when /^\d+x\d+$/ then width_x_height.split('x')
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
RUBY
|
201
|
+
end
|
202
|
+
|
203
|
+
def gemfile_additions
|
204
|
+
variants_dependencies = <<~TEXT
|
205
|
+
gem 'active_storage_variant' # provides person.avatar.variant(:thumb) for Rails < 7
|
206
|
+
TEXT
|
207
|
+
|
208
|
+
validation_dependencies = <<~TEXT
|
209
|
+
gem 'active_storage_validations' # validate filesize, dimensions and content-type
|
210
|
+
TEXT
|
211
|
+
|
212
|
+
<<~TEXT
|
213
|
+
The following gems are suggested to have in your Gemfile:
|
214
|
+
|
215
|
+
gem 'nochmal' # only needed until the migration to the desired ActiveStorage-Backend is complete
|
216
|
+
#{variants_dependencies if @variants_present}
|
217
|
+
#{validation_dependencies}
|
218
|
+
TEXT
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module Adapters
|
5
|
+
# Wrap ActiveStorageHelper and use carrierwave as simulated from_storage_service
|
6
|
+
class CarrierwaveMigration < Carrierwave
|
7
|
+
def attachment_types_for(model)
|
8
|
+
@types[model] ||= model.uploaders.map do |uploader, uploader_class|
|
9
|
+
@uploaders[model] = { uploader => uploader_class }
|
10
|
+
uploader
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def reupload(record, type)
|
15
|
+
pathname = blob(record.send(type))
|
16
|
+
|
17
|
+
if pathname.exist?
|
18
|
+
StringIO.open(pathname.read) do |temp|
|
19
|
+
record.send(not_prefixed(type)).attach(io: temp, filename: pathname.basename)
|
20
|
+
end
|
21
|
+
|
22
|
+
{ status: :ok }
|
23
|
+
else
|
24
|
+
{ status: :missing,
|
25
|
+
message: MigrationData::Status.new(filename: pathname, record: record).missing_message }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def collection(model, uploader)
|
30
|
+
super(model, uploader).tap do |scope|
|
31
|
+
type_started(model, uploader, scope.count)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# hooks
|
36
|
+
|
37
|
+
def setup(action)
|
38
|
+
@mode = action
|
39
|
+
|
40
|
+
return if @mode == :count
|
41
|
+
raise MigrationData::StatusExists if MigrationData::Status.table_exists?
|
42
|
+
|
43
|
+
MigrationData::CreateMigrationTables.new.up
|
44
|
+
end
|
45
|
+
|
46
|
+
def teardown
|
47
|
+
return if @mode == :count
|
48
|
+
raise MigrationData::Incomplete unless completely_done?
|
49
|
+
return if ENV["NOCHMAL_KEEP_METADATA"].present?
|
50
|
+
|
51
|
+
MigrationData::CreateMigrationTables.new.down
|
52
|
+
end
|
53
|
+
|
54
|
+
def item_completed(record, type, status)
|
55
|
+
return if @mode == :count
|
56
|
+
return unless %i[ok missing].include? status
|
57
|
+
|
58
|
+
MigrationData::Status.find_or_create_by(
|
59
|
+
record_id: record.id,
|
60
|
+
record_type: record.class.sti_name,
|
61
|
+
uploader_type: type,
|
62
|
+
filename: blob(record.send(type)).to_s
|
63
|
+
).update(status: status)
|
64
|
+
end
|
65
|
+
|
66
|
+
def type_completed(model, type)
|
67
|
+
return if @mode == :count
|
68
|
+
|
69
|
+
MigrationData::Meta
|
70
|
+
.find_by(record_type: model.sti_name, uploader_type: type)
|
71
|
+
.update(migrated: migrated(model, type))
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def type_started(model, uploader, count)
|
77
|
+
return if @mode == :count
|
78
|
+
|
79
|
+
MigrationData::Meta.find_or_create_by(
|
80
|
+
record_type: model.sti_name,
|
81
|
+
uploader_type: uploader
|
82
|
+
).update(expected: count)
|
83
|
+
end
|
84
|
+
|
85
|
+
def completely_done?
|
86
|
+
MigrationData::Meta.all.all?(&:done?)
|
87
|
+
end
|
88
|
+
|
89
|
+
def migrated(model, type)
|
90
|
+
MigrationData::Status
|
91
|
+
.where(record_type: model.sti_name, uploader_type: type)
|
92
|
+
.where.not(status: nil)
|
93
|
+
.count
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module Adapters
|
5
|
+
# Resume a started migration
|
6
|
+
class CarrierwaveResume < CarrierwaveMigration
|
7
|
+
# action
|
8
|
+
|
9
|
+
def reupload(record, type)
|
10
|
+
status = MigrationData::Status.find_by(
|
11
|
+
record_id: record.id, record_type: record.class.sti_name,
|
12
|
+
uploader_type: type, filename: blob(record.send(type)).to_s
|
13
|
+
)
|
14
|
+
|
15
|
+
if status&.migrated?
|
16
|
+
message = status.missing_message if status.missing?
|
17
|
+
{ status: :skip, message: message }
|
18
|
+
else
|
19
|
+
super(record, type)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# hooks
|
24
|
+
|
25
|
+
def setup(action)
|
26
|
+
if MigrationData::Status.table_exists? && MigrationData::Meta.table_exists?
|
27
|
+
@mode = action
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
|
31
|
+
Output.notes [
|
32
|
+
"It appears that no previous migration has been running.",
|
33
|
+
'Creating the needed tables and "resuming from square 1"...'
|
34
|
+
]
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module MigrationData
|
5
|
+
class CreateMigrationTables < ActiveRecord::Migration[6.0] # :nodoc:
|
6
|
+
def up # rubocop:disable Metrics/MethodLength
|
7
|
+
create_table :nochmal_migration_data_status do |t|
|
8
|
+
t.belongs_to :record, polymorphic: true
|
9
|
+
|
10
|
+
t.string :uploader_type
|
11
|
+
t.string :filename
|
12
|
+
|
13
|
+
t.string :status
|
14
|
+
end
|
15
|
+
|
16
|
+
return if table_exists?(:nochmal_migration_data_meta)
|
17
|
+
|
18
|
+
create_table :nochmal_migration_data_meta do |t|
|
19
|
+
t.string :record_type
|
20
|
+
t.string :uploader_type
|
21
|
+
t.integer :expected
|
22
|
+
t.integer :migrated
|
23
|
+
t.string :status
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def down
|
28
|
+
drop_table :nochmal_migration_data_status, if_exists: true
|
29
|
+
drop_table :nochmal_migration_data_meta, if_exists: true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module MigrationData
|
5
|
+
# A migration may not be complete...
|
6
|
+
class Incomplete < StandardError
|
7
|
+
def initialize(*_args)
|
8
|
+
super <<~MESSAGE
|
9
|
+
This did not end well...
|
10
|
+
|
11
|
+
#{Meta.all.map(&:to_s).join("\n ")}
|
12
|
+
|
13
|
+
Care to clean up the mess?
|
14
|
+
MESSAGE
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module MigrationData
|
5
|
+
# Track the status of the whole migration
|
6
|
+
class Meta < ActiveRecord::Base
|
7
|
+
self.table_name = :nochmal_migration_data_meta
|
8
|
+
|
9
|
+
before_save :update_status
|
10
|
+
|
11
|
+
def done?
|
12
|
+
status.to_s == "done"
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
[
|
17
|
+
record_type, "#", uploader_type, ": ",
|
18
|
+
migrated, "/", expected, " -> ", status
|
19
|
+
].join
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update_status
|
25
|
+
self.status = current_status
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_status # rubocop:disable Metrics/CyclomaticComplexity
|
29
|
+
return nil if migrated.nil? && expected.nil?
|
30
|
+
|
31
|
+
if expected.positive? && migrated.nil?
|
32
|
+
"not migrated"
|
33
|
+
else
|
34
|
+
case migrated.to_i <=> expected
|
35
|
+
when -1 then "partial"
|
36
|
+
when 0 then "done"
|
37
|
+
when 1 then "too much"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module MigrationData
|
5
|
+
# Track the status of individual uploads to be migrated
|
6
|
+
class Status < ActiveRecord::Base
|
7
|
+
self.table_name = :nochmal_migration_data_status
|
8
|
+
|
9
|
+
belongs_to :record, polymorphic: true
|
10
|
+
|
11
|
+
scope :missing, -> { where(status: "missing") }
|
12
|
+
scope :ok, -> { where(status: "ok") }
|
13
|
+
|
14
|
+
def migrated?
|
15
|
+
status.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
def missing?
|
19
|
+
status.to_s == "missing"
|
20
|
+
end
|
21
|
+
|
22
|
+
def missing_message
|
23
|
+
"#{filename} was not found, but was attached to #{record}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nochmal
|
4
|
+
module MigrationData
|
5
|
+
# Provide some help when a migration got aborted.
|
6
|
+
#
|
7
|
+
# This might happen if the OS or the user kills the process.
|
8
|
+
#
|
9
|
+
# I suspect an OOM-Kill by the OS or Container-Runtime (K8s/OCP).
|
10
|
+
class StatusExists < StandardError
|
11
|
+
def initialize
|
12
|
+
super <<~ERROR
|
13
|
+
It seems like the migration has already been started.
|
14
|
+
|
15
|
+
You may want to resume with
|
16
|
+
rails nochmal:carrierwave:resume
|
17
|
+
|
18
|
+
Alternatively, you can manually delete the tables
|
19
|
+
- #{Status.table_name}
|
20
|
+
- #{Meta.table_name}
|
21
|
+
and rerun the migration completely.
|
22
|
+
|
23
|
+
If you want, you can examine the status by interacting with
|
24
|
+
- Nochmal::MigrationData::Meta # class/attachment-level stats
|
25
|
+
- Nochmal::MigrationData::Status # individual upload stats
|
26
|
+
in your rails console.
|
27
|
+
ERROR
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pastel"
|
4
|
+
|
5
|
+
module Nochmal
|
6
|
+
# Handles output for the Reupload Task
|
7
|
+
class Output
|
8
|
+
class << self
|
9
|
+
def reupload(models)
|
10
|
+
puts reupload_header(models)
|
11
|
+
yield
|
12
|
+
puts reupload_footer
|
13
|
+
end
|
14
|
+
|
15
|
+
def model(model, skipping: false)
|
16
|
+
if skipping
|
17
|
+
puts "Skipping #{pastel.green(model)}"
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
|
21
|
+
puts model_header(model)
|
22
|
+
yield
|
23
|
+
puts model_footer
|
24
|
+
end
|
25
|
+
|
26
|
+
def type(type, count, action)
|
27
|
+
puts type_header(type)
|
28
|
+
print attachment_summary(count, action)
|
29
|
+
yield
|
30
|
+
puts type_footer
|
31
|
+
end
|
32
|
+
|
33
|
+
def attachment(filename)
|
34
|
+
print attachment_detail(filename)
|
35
|
+
end
|
36
|
+
|
37
|
+
def notes(notes)
|
38
|
+
notes = Array.wrap(notes)
|
39
|
+
return unless notes.any?
|
40
|
+
|
41
|
+
puts reupload_notes(notes)
|
42
|
+
end
|
43
|
+
|
44
|
+
def print_result_indicator(status)
|
45
|
+
case status
|
46
|
+
when :ok then print_progress_indicator
|
47
|
+
when :missing then print_failure_indicator
|
48
|
+
when :skip then print_skip_indicator
|
49
|
+
when :noop then nil
|
50
|
+
else print_unknown_indicator
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def print_progress_indicator
|
55
|
+
print pastel.green(".")
|
56
|
+
end
|
57
|
+
|
58
|
+
def print_failure_indicator
|
59
|
+
print pastel.red("F")
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_skip_indicator
|
63
|
+
print pastel.yellow("*")
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_unknown_indicator
|
67
|
+
print pastel.blue("?")
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def reupload_header(models)
|
73
|
+
model_text = "model".pluralize(models.count)
|
74
|
+
model_names = models.map { |model| pastel.green(model) }.join(", ")
|
75
|
+
|
76
|
+
<<~HEADER
|
77
|
+
|
78
|
+
|
79
|
+
================================================================================
|
80
|
+
I have found #{models.count} #{model_text} to process: #{model_names}
|
81
|
+
|
82
|
+
HEADER
|
83
|
+
end
|
84
|
+
|
85
|
+
def model_header(model)
|
86
|
+
"Model #{pastel.green(model)}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def type_header(type)
|
90
|
+
" Type #{pastel.green(type)}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def attachment_summary(count, action)
|
94
|
+
" Going to #{action} #{count} #{"attachment".pluralize(count)}: "
|
95
|
+
end
|
96
|
+
|
97
|
+
def attachment_detail(filename)
|
98
|
+
"\n - #{filename}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def type_footer
|
102
|
+
"\n Done!"
|
103
|
+
end
|
104
|
+
|
105
|
+
def model_footer
|
106
|
+
"Done!"
|
107
|
+
end
|
108
|
+
|
109
|
+
def reupload_footer
|
110
|
+
"\nAll attachments have been processed!"
|
111
|
+
end
|
112
|
+
|
113
|
+
def reupload_notes(notes)
|
114
|
+
<<~NOTES
|
115
|
+
|
116
|
+
================================================================================
|
117
|
+
#{notes.join("\n")}
|
118
|
+
================================================================================
|
119
|
+
NOTES
|
120
|
+
end
|
121
|
+
|
122
|
+
def pastel
|
123
|
+
Pastel.new
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|