~cytrogen/masto-fe

ref: d7fcd70023d481cb6a53aef7a58dabfd6a2fa7b2 masto-fe/lib/mastodon/cli/upgrade.rb -rw-r--r-- 5.4 KiB
d7fcd700 — Claire Merge commit '2016c5d912f400ae98ee03ce269112de2f9ec62d' into glitch-soc/merge-upstream 2 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# frozen_string_literal: true

require_relative 'base'

module Mastodon::CLI
  class Upgrade < Base
    CURRENT_STORAGE_SCHEMA_VERSION = 1

    option :dry_run, type: :boolean, default: false
    option :verbose, type: :boolean, default: false, aliases: [:v]
    desc 'storage-schema', 'Upgrade storage schema of various file attachments to the latest version'
    long_desc <<~LONG_DESC
      Iterates over every file attachment of every record and, if its storage schema is outdated, performs the
      necessary upgrade to the latest one. In practice this means e.g. moving files to different directories.

      Will most likely take a long time.
    LONG_DESC
    def storage_schema
      progress = create_progress_bar(nil)
      records  = 0

      klasses = [
        Account,
        CustomEmoji,
        MediaAttachment,
        PreviewCard,
      ]

      klasses.each do |klass|
        attachment_names = klass.attachment_definitions.keys

        klass.find_each do |record|
          attachment_names.each do |attachment_name|
            attachment = record.public_send(attachment_name)
            upgraded   = false

            next if attachment.blank? || attachment.storage_schema_version >= CURRENT_STORAGE_SCHEMA_VERSION

            styles = attachment.styles.keys

            styles << :original unless styles.include?(:original)

            styles.each do |style|
              success = case Paperclip::Attachment.default_options[:storage]
                        when :s3
                          upgrade_storage_s3(progress, attachment, style)
                        when :fog
                          upgrade_storage_fog(progress, attachment, style)
                        when :azure
                          upgrade_storage_azure(progress, attachment, style)
                        when :filesystem
                          upgrade_storage_filesystem(progress, attachment, style)
                        end

              upgraded = true if style == :original && success

              progress.increment
            end

            attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION) if upgraded
          end

          if record.changed?
            record.save unless dry_run?
            records += 1
          end
        end
      end

      progress.total = progress.progress
      progress.finish

      say("Upgraded storage schema of #{records} records#{dry_run_mode_suffix}", :green, true)
    end

    private

    def upgrade_storage_s3(progress, attachment, style)
      previous_storage_schema_version = attachment.storage_schema_version
      object                          = attachment.s3_object(style)
      success                         = true

      attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION)

      new_object = attachment.s3_object(style)

      if new_object.key != object.key && object.exists?
        progress.log("Moving #{object.key} to #{new_object.key}") if options[:verbose]

        begin
          object.move_to(new_object, acl: attachment.s3_permissions(style)) unless dry_run?
        rescue => e
          progress.log(pastel.red("Error processing #{object.key}: #{e}"))
          success = false
        end
      end

      # Because we move files style-by-style, it's important to restore
      # previous version at the end. The upgrade will be recorded after
      # all styles are updated
      attachment.instance_write(:storage_schema_version, previous_storage_schema_version)
      success
    end

    def upgrade_storage_fog(_progress, _attachment, _style)
      say('The fog storage driver is not supported for this operation at this time', :red)
      exit(1)
    end

    def upgrade_storage_azure(_progress, _attachment, _style)
      say('The azure storage driver is not supported for this operation at this time', :red)
      exit(1)
    end

    def upgrade_storage_filesystem(progress, attachment, style)
      previous_storage_schema_version = attachment.storage_schema_version
      previous_path                   = attachment.path(style)
      success                         = true

      attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION)

      upgraded_path = attachment.path(style)

      if upgraded_path != previous_path && File.exist?(previous_path)
        progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]

        begin
          move_previous_to_upgraded
        rescue => e
          progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
          success = false

          remove_directory
        end
      end

      # Because we move files style-by-style, it's important to restore
      # previous version at the end. The upgrade will be recorded after
      # all styles are updated
      attachment.instance_write(:storage_schema_version, previous_storage_schema_version)
      success
    end

    def move_previous_to_upgraded(previous_path, upgraded_path)
      return if dry_run?

      FileUtils.mkdir_p(File.dirname(upgraded_path))
      FileUtils.mv(previous_path, upgraded_path)

      begin
        FileUtils.rmdir(File.dirname(previous_path), parents: true)
      rescue Errno::ENOTEMPTY
        # OK
      end
    end

    def remove_directory(path)
      return if dry_run?

      begin
        FileUtils.rmdir(File.dirname(path), parents: true)
      rescue Errno::ENOTEMPTY
        # OK
      end
    end
  end
end