1# frozen_string_literal: true
2
3module Storage
4  module LegacyNamespace
5    extend ActiveSupport::Concern
6
7    include Gitlab::ShellAdapter
8
9    def move_dir
10      proj_with_tags = first_project_with_container_registry_tags
11
12      if proj_with_tags
13        raise Gitlab::UpdatePathError, "Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry"
14      end
15
16      parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
17                     Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
18                   end
19
20      move_repositories
21
22      if saved_change_to_parent?
23        former_parent_full_path = parent_was&.full_path
24        parent_full_path = parent&.full_path
25        Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
26
27        if any_project_with_pages_deployed?
28          run_after_commit do
29            Gitlab::PagesTransfer.new.async.move_namespace(path, former_parent_full_path, parent_full_path)
30          end
31        end
32      else
33        Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
34
35        if any_project_with_pages_deployed?
36          full_path_was = full_path_before_last_save
37
38          run_after_commit do
39            Gitlab::PagesTransfer.new.async.rename_namespace(full_path_was, full_path)
40          end
41        end
42      end
43
44      # If repositories moved successfully we need to
45      # send update instructions to users.
46      # However we cannot allow rollback since we moved namespace dir
47      # So we basically we mute exceptions in next actions
48      begin
49        send_update_instructions
50        write_projects_repository_config
51      rescue StandardError => e
52        Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e,
53          full_path_before_last_save: full_path_before_last_save,
54          full_path: full_path,
55          action: 'move_dir')
56      end
57
58      true # false would cancel later callbacks but not rollback
59    end
60
61    # Hooks
62
63    # Save the storages before the projects are destroyed to use them on after destroy
64    def prepare_for_destroy
65      old_repository_storages
66    end
67
68    private
69
70    def move_repositories
71      # Move the namespace directory in all storages used by member projects
72      repository_storages(legacy_only: true).each do |repository_storage|
73        # Ensure old directory exists before moving it
74        Gitlab::GitalyClient::NamespaceService.allow do
75          gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
76
77          # Ensure new directory exists before moving it (if there's a parent)
78          gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
79
80          unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
81
82            Gitlab::AppLogger.error("Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}")
83
84            # if we cannot move namespace directory we should rollback
85            # db changes in order to prevent out of sync between db and fs
86            raise Gitlab::UpdatePathError, 'namespace directory cannot be moved'
87          end
88        end
89      end
90    end
91
92    def old_repository_storages
93      @old_repository_storage_paths ||= repository_storages(legacy_only: true)
94    end
95
96    def repository_storages(legacy_only: false)
97      # We need to get the storage paths for all the projects, even the ones that are
98      # pending delete. Unscoping also get rids of the default order, which causes
99      # problems with SELECT DISTINCT.
100      Project.unscoped do
101        namespace_projects = all_projects
102        namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
103        namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
104      end
105    end
106
107    def rm_dir
108      # Remove the namespace directory in all storages paths used by member projects
109      old_repository_storages.each do |repository_storage|
110        # Move namespace directory into trash.
111        # We will remove it later async
112        new_path = "#{full_path}+#{id}+deleted"
113
114        Gitlab::GitalyClient::NamespaceService.allow do
115          if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
116            Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
117
118            # Remove namespace directory async with delay so
119            # GitLab has time to remove all projects first
120            run_after_commit do
121              GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
122            end
123          end
124        end
125      end
126    end
127  end
128end
129