BigW Consortium Gitlab

rename_base.rb 6.84 KB
Newer Older
1 2 3 4 5 6 7 8
module Gitlab
  module Database
    module RenameReservedPathsMigration
      module V1
        class RenameBase
          attr_reader :paths, :migration

          delegate :update_column_in_batches,
9
                   :execute,
10
                   :replace_sql,
11
                   :quote_string,
12
                   :say,
13 14 15 16 17 18 19 20
                   to: :migration

          def initialize(paths, migration)
            @paths = paths
            @migration = migration
          end

          def path_patterns
James Lopez committed
21
            @path_patterns ||= paths.flat_map { |path| ["%/#{path}", path] }
22 23 24 25 26 27 28 29 30 31
          end

          def rename_path_for_routable(routable)
            old_path = routable.path
            old_full_path = routable.full_path
            # Only remove the last occurrence of the path name to get the parent namespace path
            namespace_path = remove_last_occurrence(old_full_path, old_path)
            new_path = rename_path(namespace_path, old_path)
            new_full_path = join_routable_path(namespace_path, new_path)

32 33 34 35 36 37
            perform_rename(routable, old_full_path, new_full_path)

            [old_full_path, new_full_path]
          end

          def perform_rename(routable, old_full_path, new_full_path)
38
            # skips callbacks & validations
39
            new_path = new_full_path.split('/').last
40 41
            routable.class.where(id: routable)
              .update_all(path: new_path)
42 43 44 45 46

            rename_routes(old_full_path, new_full_path)
          end

          def rename_routes(old_full_path, new_full_path)
47
            routes = Route.arel_table
48 49 50 51 52 53 54 55 56 57 58 59

            quoted_old_full_path = quote_string(old_full_path)
            quoted_old_wildcard_path = quote_string("#{old_full_path}/%")

            filter = if Database.mysql?
                       "lower(routes.path) = lower('#{quoted_old_full_path}') "\
                       "OR routes.path LIKE '#{quoted_old_wildcard_path}'"
                     else
                       "routes.id IN "\
                       "( SELECT routes.id FROM routes WHERE lower(routes.path) = lower('#{quoted_old_full_path}') "\
                       "UNION SELECT routes.id FROM routes WHERE routes.path ILIKE '#{quoted_old_wildcard_path}' )"
                     end
60

61 62 63 64
            replace_statement = replace_sql(Route.arel_table[:path],
                                            old_full_path,
                                            new_full_path)

65 66 67
            update = Arel::UpdateManager.new(ActiveRecord::Base)
                       .table(routes)
                       .set([[routes[:path], replace_statement]])
68 69
                       .where(Arel::Nodes::SqlLiteral.new(filter))

70
            execute(update.to_sql)
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
          end

          def rename_path(namespace_path, path_was)
            counter = 0
            path = "#{path_was}#{counter}"

            while route_exists?(join_routable_path(namespace_path, path))
              counter += 1
              path = "#{path_was}#{counter}"
            end

            path
          end

          def remove_last_occurrence(string, pattern)
            string.reverse.sub(pattern.reverse, "").reverse
          end

          def join_routable_path(namespace_path, top_level)
            if namespace_path.present?
              File.join(namespace_path, top_level)
            else
              top_level
            end
          end

          def route_exists?(full_path)
            MigrationClasses::Route.where(Route.arel_table[:path].matches(full_path)).any?
          end

          def move_pages(old_path, new_path)
            move_folders(pages_dir, old_path, new_path)
          end

          def move_uploads(old_path, new_path)
            return unless file_storage?

            move_folders(uploads_dir, old_path, new_path)
          end

          def move_folders(directory, old_relative_path, new_relative_path)
            old_path = File.join(directory, old_relative_path)
113 114 115 116
            unless File.directory?(old_path)
              say "#{old_path} doesn't exist, skipping"
              return
            end
117 118 119 120 121 122

            new_path = File.join(directory, new_relative_path)
            FileUtils.mv(old_path, new_path)
          end

          def remove_cached_html_for_projects(project_ids)
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
            project_ids.each do |project_id|
              update_column_in_batches(:projects, :description_html, nil) do |table, query|
                query.where(table[:id].eq(project_id))
              end

              update_column_in_batches(:issues, :description_html, nil) do |table, query|
                query.where(table[:project_id].eq(project_id))
              end

              update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
                query.where(table[:target_project_id].eq(project_id))
              end

              update_column_in_batches(:notes, :note_html, nil) do |table, query|
                query.where(table[:project_id].eq(project_id))
              end

              update_column_in_batches(:milestones, :description_html, nil) do |table, query|
                query.where(table[:project_id].eq(project_id))
              end
143
            end
144 145
          end

146
          def track_rename(type, old_path, new_path)
147
            key = redis_key_for_type(type)
148
            Gitlab::Redis::SharedState.with do |redis|
149 150 151 152
              redis.lpush(key, [old_path, new_path].to_json)
              redis.expire(key, 2.weeks.to_i)
            end
            say "tracked rename: #{key}: #{old_path} -> #{new_path}"
153 154
          end

155 156
          def reverts_for_type(type)
            key = redis_key_for_type(type)
157

158
            Gitlab::Redis::SharedState.with do |redis|
159 160
              failed_reverts = []

161 162 163
              while rename_info = redis.lpop(key)
                path_before_rename, path_after_rename = JSON.parse(rename_info)
                say "renaming #{type} from #{path_after_rename} back to #{path_before_rename}"
164 165 166 167
                begin
                  yield(path_before_rename, path_after_rename)
                rescue StandardError => e
                  failed_reverts << rename_info
168 169 170
                  say "Renaming #{type} from #{path_after_rename} back to "\
                      "#{path_before_rename} failed. Review the error and try "\
                      "again by running the `down` action. \n"\
171 172
                      "#{e.message}: \n #{e.backtrace.join("\n")}"
                end
173
              end
174 175

              failed_reverts.each { |rename_info| redis.lpush(key, rename_info) }
176 177 178 179
            end
          end

          def redis_key_for_type(type)
180
            "rename:#{migration.name}:#{type}"
181 182
          end

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
          def file_storage?
            CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
          end

          def uploads_dir
            File.join(CarrierWave.root, "uploads")
          end

          def pages_dir
            Settings.pages.path
          end
        end
      end
    end
  end
end