namespace.rb 7.2 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 Routable
8

9 10
  cache_markdown_field :description, pipeline: :description

11
  has_many :projects, dependent: :destroy
M
Markus Koller 已提交
12
  has_many :project_statistics
13 14
  belongs_to :owner, class_name: "User"

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

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

25
  validates :description, length: { maximum: 255 }
26
  validates :path,
R
Robert Speicher 已提交
27
    presence: true,
28 29
    length: { maximum: 255 },
    namespace: true
30 31 32

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

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

  # 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 }
38
  after_destroy :rm_dir
39

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

M
Markus Koller 已提交
42 43 44 45 46 47 48 49 50 51 52 53
  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

54 55
  class << self
    def by_path(path)
G
Gabriel Mazetto 已提交
56
      find_by('lower(path) = :value', value: path.downcase)
57 58 59 60 61 62 63
    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

64 65 66 67 68 69 70
    # 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
71
    def search(query)
72 73 74 75
      t = arel_table
      pattern = "%#{query}%"

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

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

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

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

      path
    end
102 103
  end

104
  def to_param
105
    full_path
106
  end
107 108 109 110

  def human_name
    owner_name
  end
111

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

117 118 119 120 121 122
    # 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 已提交
123 124
        Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"

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

    Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)

133 134
    remove_exports!

135 136 137 138 139 140 141 142 143 144
    # 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
145
    end
146
  end
147

148
  def any_project_has_container_registry_tags?
K
Kamil Trzcinski 已提交
149
    projects.any?(&:has_container_registry_tags?)
150 151
  end

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

  def kind
    type == 'Group' ? 'group' : 'user'
  end
161 162

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

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

171 172 173 174 175 176 177 178
  def full_path
    if parent
      parent.full_path + '/' + path
    else
      path
    end
  end

179
  def full_name
180 181 182 183 184 185 186 187 188
    @full_name ||=
      if parent
        parent.full_name + ' / ' + name
      else
        name
      end
  end

  def parents
189
    @parents ||= parent ? parent.parents + [parent] : []
190 191
  end

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
  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
219 220

    remove_exports!
221
  end
222 223 224 225 226 227 228

  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
229 230 231 232

  def full_path_changed?
    path_changed? || parent_id_changed?
  end
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248

  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
249
end