Skip to content

Commit 1a3ba97

Browse files
authored
Add option to overwrite imported data (mastodon#9962)
* Add option to overwrite imported data Fix mastodon#7465 * Add import for domain blocks
1 parent 1da3a0d commit 1a3ba97

12 files changed

Lines changed: 148 additions & 43 deletions

File tree

app/models/account_domain_block.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
class AccountDomainBlock < ApplicationRecord
1414
include Paginable
15+
include DomainNormalizable
1516

1617
belongs_to :account
1718
validates :domain, presence: true, uniqueness: { scope: :account_id }

app/models/concerns/domain_normalizable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ module DomainNormalizable
1010
private
1111

1212
def normalize_domain
13-
self.domain = TagManager.instance.normalize_domain(domain)
13+
self.domain = TagManager.instance.normalize_domain(domain&.strip)
1414
end
1515
end

app/models/export.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: true
2+
23
require 'csv'
34

45
class Export

app/models/import.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,30 @@
1313
# data_file_size :integer
1414
# data_updated_at :datetime
1515
# account_id :bigint(8) not null
16+
# overwrite :boolean default(FALSE), not null
1617
#
1718

1819
class Import < ApplicationRecord
19-
FILE_TYPES = ['text/plain', 'text/csv'].freeze
20+
FILE_TYPES = %w(text/plain text/csv).freeze
21+
MODES = %i(merge overwrite).freeze
2022

2123
self.inheritance_column = false
2224

2325
belongs_to :account
2426

25-
enum type: [:following, :blocking, :muting]
27+
enum type: [:following, :blocking, :muting, :domain_blocking]
2628

2729
validates :type, presence: true
2830

2931
has_attached_file :data
3032
validates_attachment_content_type :data, content_type: FILE_TYPES
3133
validates_attachment_presence :data
34+
35+
def mode
36+
overwrite? ? :overwrite : :merge
37+
end
38+
39+
def mode=(str)
40+
self.overwrite = str.to_sym == :overwrite
41+
end
3242
end

app/services/import_service.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# frozen_string_literal: true
2+
3+
require 'csv'
4+
5+
class ImportService < BaseService
6+
ROWS_PROCESSING_LIMIT = 20_000
7+
8+
def call(import)
9+
@import = import
10+
@account = @import.account
11+
@data = CSV.new(import_data).reject(&:blank?)
12+
13+
case @import.type
14+
when 'following'
15+
import_follows!
16+
when 'blocking'
17+
import_blocks!
18+
when 'muting'
19+
import_mutes!
20+
when 'domain_blocking'
21+
import_domain_blocks!
22+
end
23+
end
24+
25+
private
26+
27+
def import_follows!
28+
import_relationships!('follow', 'unfollow', @account.following, follow_limit)
29+
end
30+
31+
def import_blocks!
32+
import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
33+
end
34+
35+
def import_mutes!
36+
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT)
37+
end
38+
39+
def import_domain_blocks!
40+
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row.first.strip }
41+
42+
if @import.overwrite?
43+
presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
44+
45+
@account.domain_blocks.find_each do |domain_block|
46+
if presence_hash[domain_block.domain]
47+
items.delete(domain_block.domain)
48+
else
49+
@account.unblock_domain!(domain_block.domain)
50+
end
51+
end
52+
end
53+
54+
items.each do |domain|
55+
@account.block_domain!(domain)
56+
end
57+
58+
AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
59+
[@account.id, domain]
60+
end
61+
end
62+
63+
def import_relationships!(action, undo_action, overwrite_scope, limit)
64+
items = @data.take(limit).map { |row| row.first.strip }
65+
66+
if @import.overwrite?
67+
presence_hash = items.each_with_object({}) { |id, mapping| mapping[id] = true }
68+
69+
overwrite_scope.find_each do |target_account|
70+
if presence_hash[target_account.acct]
71+
items.delete(target_account.acct)
72+
else
73+
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
74+
end
75+
end
76+
end
77+
78+
Import::RelationshipWorker.push_bulk(items) do |acct|
79+
[@account.id, acct, action]
80+
end
81+
end
82+
83+
def import_data
84+
Paperclip.io_adapters.for(@import.data).read
85+
end
86+
87+
def follow_limit
88+
FollowLimitValidator.limit_for_account(@account)
89+
end
90+
end

app/views/settings/imports/show.html.haml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
.field-group
66
= f.input :type, collection: Import.types.keys, wrapper: :with_block_label, include_blank: false, label_method: lambda { |type| I18n.t("imports.types.#{type}") }, hint: t('imports.preface')
77

8-
.field-group
9-
= f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
8+
.fields-row
9+
.fields-group.fields-row__column.fields-row__column-6
10+
= f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data')
11+
.fields-group.fields-row__column.fields-row__column-6
12+
= f.input :mode, as: :radio_buttons, collection: Import::MODES, label_method: lambda { |mode| safe_join([I18n.t("imports.modes.#{mode}"), content_tag(:span, I18n.t("imports.modes.#{mode}_long"), class: 'hint')]) }, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
1013

1114
.actions
1215
= f.button :button, t('imports.upload'), type: :submit

app/workers/import/relationship_worker.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ def perform(account_id, target_account_uri, relationship)
1313

1414
case relationship
1515
when 'follow'
16-
FollowService.new.call(from_account, target_account.acct)
16+
FollowService.new.call(from_account, target_account)
17+
when 'unfollow'
18+
UnfollowService.new.call(from_account, target_account)
1719
when 'block'
1820
BlockService.new.call(from_account, target_account)
21+
when 'unblock'
22+
UnblockService.new.call(from_account, target_account)
1923
when 'mute'
2024
MuteService.new.call(from_account, target_account)
25+
when 'unmute'
26+
UnmuteService.new.call(from_account, target_account)
2127
end
2228
rescue ActiveRecord::RecordNotFound
2329
true

app/workers/import_worker.rb

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,14 @@
11
# frozen_string_literal: true
22

3-
require 'csv'
4-
53
class ImportWorker
64
include Sidekiq::Worker
75

86
sidekiq_options queue: 'pull', retry: false
97

10-
attr_reader :import
11-
128
def perform(import_id)
13-
@import = Import.find(import_id)
14-
15-
Import::RelationshipWorker.push_bulk(import_rows) do |row|
16-
[@import.account_id, row.first, relationship_type]
17-
end
18-
19-
@import.destroy
20-
end
21-
22-
private
23-
24-
def import_contents
25-
Paperclip.io_adapters.for(@import.data).read
26-
end
27-
28-
def relationship_type
29-
case @import.type
30-
when 'following'
31-
'follow'
32-
when 'blocking'
33-
'block'
34-
when 'muting'
35-
'mute'
36-
end
37-
end
38-
39-
def import_rows
40-
rows = CSV.new(import_contents).reject(&:blank?)
41-
rows = rows.take(FollowLimitValidator.limit_for_account(@import.account)) if @import.type == 'following'
42-
rows
9+
import = Import.find(import_id)
10+
ImportService.new.call(import)
11+
ensure
12+
import&.destroy
4313
end
4414
end

config/locales/en.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,10 +628,16 @@ en:
628628
one: Something isn't quite right yet! Please review the error below
629629
other: Something isn't quite right yet! Please review %{count} errors below
630630
imports:
631+
modes:
632+
merge: Merge
633+
merge_long: Keep existing records and add new ones
634+
overwrite: Overwrite
635+
overwrite_long: Replace current records with the new ones
631636
preface: You can import data that you have exported from another instance, such as a list of the people you are following or blocking.
632637
success: Your data was successfully uploaded and will now be processed in due time
633638
types:
634639
blocking: Blocking list
640+
domain_blocking: Domain blocking list
635641
following: Following list
636642
muting: Muting list
637643
upload: Upload
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
2+
3+
class AddOverwriteToImports < ActiveRecord::Migration[5.2]
4+
include Mastodon::MigrationHelpers
5+
6+
disable_ddl_transaction!
7+
8+
def up
9+
safety_assured do
10+
add_column_with_default :imports, :overwrite, :boolean, default: false, allow_null: false
11+
end
12+
end
13+
14+
def down
15+
remove_column :imports, :overwrite, :boolean
16+
end
17+
end

0 commit comments

Comments
 (0)