Read about our upcoming Code of Conduct on this issue

namespace.rb 7.63 KB
Newer Older
1
class Namespace < ActiveRecord::Base
2
3
  acts_as_paranoid

4
  include CacheMarkdownField
5
  include Sortable
6
  include Gitlab::ShellAdapter
7
  include Gitlab::CurrentSettings
8
  include Routable
9

10
11
  cache_markdown_field :description, pipeline: :description

12
  has_many :projects, dependent: :destroy
Markus Koller's avatar
Markus Koller committed
13
  has_many :project_statistics
14
15
  belongs_to :owner, class_name: "User"

16
17
18
  belongs_to :parent, class_name: "Namespace"
  has_many :children, class_name: "Namespace", foreign_key: :parent_id

19
  validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
20
  validates :name,
21
    presence: true,
22
    uniqueness: { scope: :parent_id },
23
24
    length: { maximum: 255 },
    namespace_name: true
25

26
  validates :description, length: { maximum: 255 }
27
  validates :path,
28
    presence: true,
29
30
    length: { maximum: 255 },
    namespace: true
31
32
33

  delegate :name, to: :owner, allow_nil: true, prefix: true

34
  after_update :move_dir, if: :path_changed?
35
  after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
36
37
38

  # Save the storage paths before the projects are destroyed to use them on after destroy
  before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
39
  after_destroy :rm_dir
40

Andrew8xx8's avatar
Andrew8xx8 committed
41
  scope :root, -> { where('type IS NULL') }
42

Markus Koller's avatar
Markus Koller committed
43
44
45
46
47
48
49
50
51
52
53
54
  scope :with_statistics, -> do
    joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
      .group('namespaces.id')
      .select(
        'namespaces.*',
        'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
        'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
        'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
        'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
      )
  end

55
56
  class << self
    def by_path(path)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
57
      find_by('lower(path) = :value', value: path.downcase)
58
59
60
61
62
63
64
    end

    # Case insensetive search for namespace by path or name
    def find_by_path_or_name(path)
      find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
    end

65
66
67
68
69
70
71
    # Searches for namespaces matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation
72
    def search(query)
73
74
75
76
      t = arel_table
      pattern = "%#{query}%"

      where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
77
78
79
    end

    def clean_path(path)
80
      path = path.dup
Douwe Maan's avatar
Douwe Maan committed
81
      # Get the email username by removing everything after an `@` sign.
82
      path.gsub!(/@.*\z/,                "")
Douwe Maan's avatar
Douwe Maan committed
83
      # Remove everything that's not in the list of allowed characters.
84
85
86
87
88
      path.gsub!(/[^a-zA-Z0-9_\-\.]/,    "")
      # Remove trailing violations ('.atom', '.git', or '.')
      path.gsub!(/(\.atom|\.git|\.)*\z/, "")
      # Remove leading violations ('-')
      path.gsub!(/\A\-+/,                "")
89

90
      # Users with the great usernames of "." or ".." would end up with a blank username.
91
      # Work around that by setting their username to "blank", followed by a counter.
92
93
      path = "blank" if path.blank?

94
95
      counter = 0
      base = path
96
      while Namespace.find_by_path_or_name(path)
97
98
99
100
101
102
        counter += 1
        path = "#{base}#{counter}"
      end

      path
    end
103
104
  end

105
  def to_param
106
    full_path
107
  end
108
109
110
111

  def human_name
    owner_name
  end
112

113
  def move_dir
114
    if any_project_has_container_registry_tags?
115
      raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
116
117
    end

118
119
120
121
122
123
    # Move the namespace directory in all storages paths used by member projects
    repository_storage_paths.each do |repository_storage_path|
      # Ensure old directory exists before moving it
      gitlab_shell.add_namespace(repository_storage_path, path_was)

      unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Chris Wilson's avatar
Chris Wilson committed
124
125
        Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"

126
127
        # if we cannot move namespace directory we should rollback
        # db changes in order to prevent out of sync between db and fs
128
        raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
129
      end
130
131
132
    end

    Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
133
    Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
134

135
136
    remove_exports!

137
138
139
140
141
142
143
144
145
146
    # If repositories moved successfully we need to
    # send update instructions to users.
    # However we cannot allow rollback since we moved namespace dir
    # So we basically we mute exceptions in next actions
    begin
      send_update_instructions
    rescue
      # Returning false does not rollback after_* transaction but gives
      # us information about failing some of tasks
      false
147
    end
148
  end
149

150
  def any_project_has_container_registry_tags?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
151
    projects.any?(&:has_container_registry_tags?)
152
153
  end

154
  def send_update_instructions
155
156
157
    projects.each do |project|
      project.send_move_instructions("#{path_was}/#{project.path}")
    end
158
  end
159
160
161
162

  def kind
    type == 'Group' ? 'group' : 'user'
  end
163
164

  def find_fork_of(project)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
165
    projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
166
  end
167

168
169
170
171
172
  def lfs_enabled?
    # User namespace will always default to the global setting
    Gitlab.config.lfs.enabled
  end

173
174
175
176
  def shared_runners_enabled?
    projects.with_shared_runners.any?
  end

177
178
179
180
181
182
183
184
185
186
187
  # Scopes the model on ancestors of the record
  def ancestors
    if parent_id
      path = route.path
      paths = []

      until path.blank?
        path = path.rpartition('/').first
        paths << path
      end

188
      self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
189
190
191
192
193
194
195
    else
      self.class.none
    end
  end

  # Scopes the model on direct and indirect children of the record
  def descendants
196
    self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
197
198
  end

199
200
201
202
  def user_ids_for_project_authorizations
    [owner_id]
  end

203
204
205
206
  def parent_changed?
    parent_id_changed?
  end

207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
  private

  def repository_storage_paths
    # We need to get the storage paths for all the projects, even the ones that are
    # pending delete. Unscoping also get rids of the default order, which causes
    # problems with SELECT DISTINCT.
    Project.unscoped do
      projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
    end
  end

  def rm_dir
    # Remove the namespace directory in all storages paths used by member projects
    @old_repository_storage_paths.each do |repository_storage_path|
      # Move namespace directory into trash.
      # We will remove it later async
      new_path = "#{path}+#{id}+deleted"

      if gitlab_shell.mv_namespace(repository_storage_path, path, new_path)
        message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
        Gitlab::AppLogger.info message

        # Remove namespace directroy async with delay so
        # GitLab has time to remove all projects first
        GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
      end
    end
234
235

    remove_exports!
236
  end
237
238
239
240
241
242
243

  def refresh_access_of_projects_invited_groups
    Group.
      joins(project_group_links: :project).
      where(projects: { namespace_id: id }).
      find_each(&:refresh_members_authorized_projects)
  end
244

245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
  def remove_exports!
    Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
  end

  def export_path
    File.join(Gitlab::ImportExport.storage_path, full_path_was)
  end

  def full_path_was
    if parent
      parent.full_path + '/' + path_was
    else
      path_was
    end
  end
260
end