namespace.rb 8.0 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 12 13 14
  # Prevent users from creating unreasonably deep level of nesting.
  # The number 20 was taken based on maximum nesting level of
  # Android repo (15) + some extra backup.
  NUMBER_OF_ANCESTORS_ALLOWED = 20

15 16
  cache_markdown_field :description, pipeline: :description

17
  has_many :projects, dependent: :destroy
M
Markus Koller 已提交
18
  has_many :project_statistics
19 20
  belongs_to :owner, class_name: "User"

21 22 23
  belongs_to :parent, class_name: "Namespace"
  has_many :children, class_name: "Namespace", foreign_key: :parent_id

24
  validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
25
  validates :name,
26
    presence: true,
27
    uniqueness: { scope: :parent_id },
28 29
    length: { maximum: 255 },
    namespace_name: true
30

31
  validates :description, length: { maximum: 255 }
32
  validates :path,
R
Robert Speicher 已提交
33
    presence: true,
34 35
    length: { maximum: 255 },
    namespace: true
36

37 38
  validate :nesting_level_allowed

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

41
  after_update :move_dir, if: :path_changed?
42
  after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
43 44 45

  # 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 }
46
  after_destroy :rm_dir
47

A
Andrew8xx8 已提交
48
  scope :root, -> { where('type IS NULL') }
49

M
Markus Koller 已提交
50 51 52 53 54 55 56 57 58 59 60 61
  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

62 63
  class << self
    def by_path(path)
G
Gabriel Mazetto 已提交
64
      find_by('lower(path) = :value', value: path.downcase)
65 66 67 68 69 70 71
    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

72 73 74 75 76 77 78
    # 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
79
    def search(query)
80 81 82 83
      t = arel_table
      pattern = "%#{query}%"

      where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
84 85 86
    end

    def clean_path(path)
87
      path = path.dup
D
Douwe Maan 已提交
88
      # Get the email username by removing everything after an `@` sign.
89
      path.gsub!(/@.*\z/,                "")
D
Douwe Maan 已提交
90
      # Remove everything that's not in the list of allowed characters.
91 92 93 94 95
      path.gsub!(/[^a-zA-Z0-9_\-\.]/,    "")
      # Remove trailing violations ('.atom', '.git', or '.')
      path.gsub!(/(\.atom|\.git|\.)*\z/, "")
      # Remove leading violations ('-')
      path.gsub!(/\A\-+/,                "")
96

97
      # Users with the great usernames of "." or ".." would end up with a blank username.
98
      # Work around that by setting their username to "blank", followed by a counter.
99 100
      path = "blank" if path.blank?

101 102
      counter = 0
      base = path
103
      while Namespace.find_by_path_or_name(path)
104 105 106 107 108 109
        counter += 1
        path = "#{base}#{counter}"
      end

      path
    end
110 111
  end

112
  def to_param
113
    full_path
114
  end
115 116 117 118

  def human_name
    owner_name
  end
119

120
  def move_dir
121
    if any_project_has_container_registry_tags?
122
      raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
123 124
    end

125 126 127 128 129 130
    # 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)
C
Chris Wilson 已提交
131 132
        Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"

133 134
        # if we cannot move namespace directory we should rollback
        # db changes in order to prevent out of sync between db and fs
135
        raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
136
      end
137 138 139
    end

    Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
140
    Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
141

142 143
    remove_exports!

144 145 146 147 148 149 150 151 152 153
    # 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
154
    end
155
  end
156

157
  def any_project_has_container_registry_tags?
K
Kamil Trzcinski 已提交
158
    projects.any?(&:has_container_registry_tags?)
159 160
  end

161
  def send_update_instructions
162 163 164
    projects.each do |project|
      project.send_move_instructions("#{path_was}/#{project.path}")
    end
165
  end
166 167 168 169

  def kind
    type == 'Group' ? 'group' : 'user'
  end
170 171

  def find_fork_of(project)
G
Gabriel Mazetto 已提交
172
    projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
173
  end
174

175 176 177 178 179
  def lfs_enabled?
    # User namespace will always default to the global setting
    Gitlab.config.lfs.enabled
  end

180 181 182 183
  def shared_runners_enabled?
    projects.with_shared_runners.any?
  end

184 185 186
  # Scopes the model on ancestors of the record
  def ancestors
    if parent_id
187
      path = route ? route.path : full_path
188 189 190 191 192 193 194
      paths = []

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

195
      self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
196 197 198 199 200 201 202
    else
      self.class.none
    end
  end

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

206 207 208 209
  def user_ids_for_project_authorizations
    [owner_id]
  end

210 211 212 213
  def parent_changed?
    parent_id_changed?
  end

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  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
241 242

    remove_exports!
243
  end
244 245 246 247 248 249 250

  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
251

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  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
267 268 269 270 271 272

  def nesting_level_allowed
    if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
      errors.add(:parent_id, "has too deep level of nesting")
    end
  end
273
end