diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 15:26:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 15:26:25 +0300 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /app/models/packages | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'app/models/packages')
-rw-r--r-- | app/models/packages/build_info.rb | 6 | ||||
-rw-r--r-- | app/models/packages/composer/metadatum.rb | 14 | ||||
-rw-r--r-- | app/models/packages/conan.rb | 8 | ||||
-rw-r--r-- | app/models/packages/conan/file_metadatum.rb | 32 | ||||
-rw-r--r-- | app/models/packages/conan/metadatum.rb | 41 | ||||
-rw-r--r-- | app/models/packages/dependency.rb | 47 | ||||
-rw-r--r-- | app/models/packages/dependency_link.rb | 19 | ||||
-rw-r--r-- | app/models/packages/go/module.rb | 93 | ||||
-rw-r--r-- | app/models/packages/go/module_version.rb | 115 | ||||
-rw-r--r-- | app/models/packages/maven.rb | 8 | ||||
-rw-r--r-- | app/models/packages/maven/metadatum.rb | 28 | ||||
-rw-r--r-- | app/models/packages/nuget.rb | 8 | ||||
-rw-r--r-- | app/models/packages/nuget/dependency_link_metadatum.rb | 19 | ||||
-rw-r--r-- | app/models/packages/nuget/metadatum.rb | 27 | ||||
-rw-r--r-- | app/models/packages/package.rb | 195 | ||||
-rw-r--r-- | app/models/packages/package_file.rb | 56 | ||||
-rw-r--r-- | app/models/packages/pypi.rb | 8 | ||||
-rw-r--r-- | app/models/packages/pypi/metadatum.rb | 19 | ||||
-rw-r--r-- | app/models/packages/sem_ver.rb | 54 | ||||
-rw-r--r-- | app/models/packages/tag.rb | 18 |
20 files changed, 815 insertions, 0 deletions
diff --git a/app/models/packages/build_info.rb b/app/models/packages/build_info.rb new file mode 100644 index 00000000000..df8cf68490e --- /dev/null +++ b/app/models/packages/build_info.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Packages::BuildInfo < ApplicationRecord + belongs_to :package, inverse_of: :build_info + belongs_to :pipeline, class_name: 'Ci::Pipeline' +end diff --git a/app/models/packages/composer/metadatum.rb b/app/models/packages/composer/metadatum.rb new file mode 100644 index 00000000000..3026f5ea878 --- /dev/null +++ b/app/models/packages/composer/metadatum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Packages + module Composer + class Metadatum < ApplicationRecord + self.table_name = 'packages_composer_metadata' + self.primary_key = :package_id + + belongs_to :package, -> { where(package_type: :composer) }, inverse_of: :composer_metadatum + + validates :package, :target_sha, :composer_json, presence: true + end + end +end diff --git a/app/models/packages/conan.rb b/app/models/packages/conan.rb new file mode 100644 index 00000000000..01007c3fa78 --- /dev/null +++ b/app/models/packages/conan.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Conan + def self.table_name_prefix + 'packages_conan_' + end + end +end diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb new file mode 100644 index 00000000000..e1ef62b3959 --- /dev/null +++ b/app/models/packages/conan/file_metadatum.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Packages::Conan::FileMetadatum < ApplicationRecord + belongs_to :package_file, inverse_of: :conan_file_metadatum + + validates :package_file, presence: true + + validates :recipe_revision, + presence: true, + format: { with: Gitlab::Regex.conan_revision_regex } + + validates :package_revision, absence: true, if: :recipe_file? + validates :package_revision, format: { with: Gitlab::Regex.conan_revision_regex }, if: :package_file? + + validates :conan_package_reference, absence: true, if: :recipe_file? + validates :conan_package_reference, format: { with: Gitlab::Regex.conan_package_reference_regex }, if: :package_file? + validate :conan_package_type + + enum conan_file_type: { recipe_file: 1, package_file: 2 } + + RECIPE_FILES = ::Gitlab::Regex::Packages::CONAN_RECIPE_FILES + PACKAGE_FILES = ::Gitlab::Regex::Packages::CONAN_PACKAGE_FILES + PACKAGE_BINARY = 'conan_package.tgz' + + private + + def conan_package_type + unless package_file&.package&.conan? + errors.add(:base, _('Package type must be Conan')) + end + end +end diff --git a/app/models/packages/conan/metadatum.rb b/app/models/packages/conan/metadatum.rb new file mode 100644 index 00000000000..7ec2641177a --- /dev/null +++ b/app/models/packages/conan/metadatum.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class Packages::Conan::Metadatum < ApplicationRecord + belongs_to :package, -> { where(package_type: :conan) }, inverse_of: :conan_metadatum + + validates :package, presence: true + + validates :package_username, + presence: true, + format: { with: Gitlab::Regex.conan_recipe_component_regex } + + validates :package_channel, + presence: true, + format: { with: Gitlab::Regex.conan_recipe_component_regex } + + validate :conan_package_type + + def recipe + "#{package.name}/#{package.version}@#{package_username}/#{package_channel}" + end + + def recipe_path + recipe.tr('@', '/') + end + + def self.package_username_from(full_path:) + full_path.tr('/', '+') + end + + def self.full_path_from(package_username:) + package_username.tr('+', '/') + end + + private + + def conan_package_type + unless package&.conan? + errors.add(:base, _('Package type must be Conan')) + end + end +end diff --git a/app/models/packages/dependency.rb b/app/models/packages/dependency.rb new file mode 100644 index 00000000000..51b80934827 --- /dev/null +++ b/app/models/packages/dependency.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +class Packages::Dependency < ApplicationRecord + has_many :dependency_links, class_name: 'Packages::DependencyLink' + + validates :name, :version_pattern, presence: true + + validates :name, uniqueness: { scope: :version_pattern } + + NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'.freeze + MAX_STRING_LENGTH = 255.freeze + MAX_CHUNKED_QUERIES_COUNT = 10.freeze + + def self.ids_for_package_names_and_version_patterns(names_and_version_patterns = {}, chunk_size = 50, max_rows_limit = 200) + names_and_version_patterns.reject! { |key, value| key.size > MAX_STRING_LENGTH || value.size > MAX_STRING_LENGTH } + raise ArgumentError, 'Too many names_and_version_patterns' if names_and_version_patterns.size > MAX_CHUNKED_QUERIES_COUNT * chunk_size + + matched_ids = [] + names_and_version_patterns.each_slice(chunk_size) do |tuples| + where_statement = Array.new(tuples.size, NAME_VERSION_PATTERN_TUPLE_MATCHING) + .join(' OR ') + ids = where(where_statement, *tuples.flatten) + .limit(max_rows_limit + 1) + .pluck(:id) + matched_ids.concat(ids) + + raise ArgumentError, 'Too many Dependencies selected' if matched_ids.size > max_rows_limit + end + + matched_ids + end + + def self.for_package_names_and_version_patterns(names_and_version_patterns = {}, chunk_size = 50, max_rows_limit = 200) + ids = ids_for_package_names_and_version_patterns(names_and_version_patterns, chunk_size, max_rows_limit) + + return none if ids.empty? + + id_in(ids) + end + + def self.pluck_ids_and_names + pluck(:id, :name) + end + + def orphaned? + self.dependency_links.empty? + end +end diff --git a/app/models/packages/dependency_link.rb b/app/models/packages/dependency_link.rb new file mode 100644 index 00000000000..51018602bdc --- /dev/null +++ b/app/models/packages/dependency_link.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +class Packages::DependencyLink < ApplicationRecord + belongs_to :package, inverse_of: :dependency_links + belongs_to :dependency, inverse_of: :dependency_links, class_name: 'Packages::Dependency' + has_one :nuget_metadatum, inverse_of: :dependency_link, class_name: 'Packages::Nuget::DependencyLinkMetadatum' + + validates :package, :dependency, presence: true + + validates :dependency_type, + uniqueness: { scope: %i[package_id dependency_id] } + + enum dependency_type: { dependencies: 1, devDependencies: 2, bundleDependencies: 3, peerDependencies: 4 } + + scope :with_dependency_type, ->(dependency_type) { where(dependency_type: dependency_type) } + scope :includes_dependency, -> { includes(:dependency) } + scope :for_package, ->(package) { where(package_id: package.id) } + scope :preload_dependency, -> { preload(:dependency) } + scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) } +end diff --git a/app/models/packages/go/module.rb b/app/models/packages/go/module.rb new file mode 100644 index 00000000000..b38b691ed6c --- /dev/null +++ b/app/models/packages/go/module.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module Packages + module Go + class Module + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :name, :path + + def initialize(project, name, path) + @project = project + @name = name + @path = path + end + + def versions + strong_memoize(:versions) { Packages::Go::VersionFinder.new(self).execute } + end + + def version_by(ref: nil, commit: nil) + raise ArgumentError.new 'no filter specified' unless ref || commit + raise ArgumentError.new 'ref and commit are mutually exclusive' if ref && commit + + if commit + return version_by_sha(commit) if commit.is_a? String + + return version_by_commit(commit) + end + + return version_by_name(ref) if ref.is_a? String + + version_by_ref(ref) + end + + def path_valid?(major) + m = /\/v(\d+)$/i.match(@name) + + case major + when 0, 1 + m.nil? + else + !m.nil? && m[1].to_i == major + end + end + + def gomod_valid?(gomod) + if Feature.enabled?(:go_proxy_disable_gomod_validation, @project) + return gomod&.start_with?("module ") + end + + gomod&.split("\n", 2)&.first == "module #{@name}" + end + + private + + def version_by_name(name) + # avoid a Gitaly call if possible + if strong_memoized?(:versions) + v = versions.find { |v| v.name == ref } + return v if v + end + + ref = @project.repository.find_tag(name) || @project.repository.find_branch(name) + return unless ref + + version_by_ref(ref) + end + + def version_by_ref(ref) + # reuse existing versions + if strong_memoized?(:versions) + v = versions.find { |v| v.ref == ref } + return v if v + end + + commit = ref.dereferenced_target + semver = Packages::SemVer.parse(ref.name, prefixed: true) + Packages::Go::ModuleVersion.new(self, :ref, commit, ref: ref, semver: semver) + end + + def version_by_sha(sha) + commit = @project.commit_by(oid: sha) + return unless ref + + version_by_commit(commit) + end + + def version_by_commit(commit) + Packages::Go::ModuleVersion.new(self, :commit, commit) + end + end + end +end diff --git a/app/models/packages/go/module_version.rb b/app/models/packages/go/module_version.rb new file mode 100644 index 00000000000..a50c78f8e69 --- /dev/null +++ b/app/models/packages/go/module_version.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module Packages + module Go + class ModuleVersion + include Gitlab::Utils::StrongMemoize + + VALID_TYPES = %i[ref commit pseudo].freeze + + attr_reader :mod, :type, :ref, :commit + + delegate :major, to: :@semver, allow_nil: true + delegate :minor, to: :@semver, allow_nil: true + delegate :patch, to: :@semver, allow_nil: true + delegate :prerelease, to: :@semver, allow_nil: true + delegate :build, to: :@semver, allow_nil: true + + def initialize(mod, type, commit, name: nil, semver: nil, ref: nil) + raise ArgumentError.new("invalid type '#{type}'") unless VALID_TYPES.include? type + raise ArgumentError.new("mod is required") unless mod + raise ArgumentError.new("commit is required") unless commit + + if type == :ref + raise ArgumentError.new("ref is required") unless ref + elsif type == :pseudo + raise ArgumentError.new("name is required") unless name + raise ArgumentError.new("semver is required") unless semver + end + + @mod = mod + @type = type + @commit = commit + @name = name if name + @semver = semver if semver + @ref = ref if ref + end + + def name + @name || @ref&.name + end + + def full_name + "#{mod.name}@#{name || commit.sha}" + end + + def gomod + strong_memoize(:gomod) do + if strong_memoized?(:blobs) + blob_at(@mod.path + '/go.mod') + elsif @mod.path.empty? + @mod.project.repository.blob_at(@commit.sha, 'go.mod')&.data + else + @mod.project.repository.blob_at(@commit.sha, @mod.path + '/go.mod')&.data + end + end + end + + def archive + suffix_len = @mod.path == '' ? 0 : @mod.path.length + 1 + + Zip::OutputStream.write_buffer do |zip| + files.each do |file| + zip.put_next_entry "#{full_name}/#{file[suffix_len...]}" + zip.write blob_at(file) + end + end + end + + def files + strong_memoize(:files) do + ls_tree.filter { |e| !excluded.any? { |n| e.start_with? n } } + end + end + + def excluded + strong_memoize(:excluded) do + ls_tree + .filter { |f| f.end_with?('/go.mod') && f != @mod.path + '/go.mod' } + .map { |f| f[0..-7] } + end + end + + def valid? + @mod.path_valid?(major) && @mod.gomod_valid?(gomod) + end + + private + + def blob_at(path) + return if path.nil? || path.empty? + + path = path[1..] if path.start_with? '/' + + blobs.find { |x| x.path == path }&.data + end + + def blobs + strong_memoize(:blobs) { @mod.project.repository.batch_blobs(files.map { |x| [@commit.sha, x] }) } + end + + def ls_tree + strong_memoize(:ls_tree) do + path = + if @mod.path.empty? + '.' + else + @mod.path + end + + @mod.project.repository.gitaly_repository_client.search_files_by_name(@commit.sha, path) + end + end + end + end +end diff --git a/app/models/packages/maven.rb b/app/models/packages/maven.rb new file mode 100644 index 00000000000..5c1581ce0b7 --- /dev/null +++ b/app/models/packages/maven.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Maven + def self.table_name_prefix + 'packages_maven_' + end + end +end diff --git a/app/models/packages/maven/metadatum.rb b/app/models/packages/maven/metadatum.rb new file mode 100644 index 00000000000..b7f27fb9e06 --- /dev/null +++ b/app/models/packages/maven/metadatum.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +class Packages::Maven::Metadatum < ApplicationRecord + belongs_to :package, -> { where(package_type: :maven) } + + validates :package, presence: true + + validates :path, + presence: true, + format: { with: Gitlab::Regex.maven_path_regex } + + validates :app_group, + presence: true, + format: { with: Gitlab::Regex.maven_app_group_regex } + + validates :app_name, + presence: true, + format: { with: Gitlab::Regex.maven_app_name_regex } + + validate :maven_package_type + + private + + def maven_package_type + unless package&.maven? + errors.add(:base, _('Package type must be Maven')) + end + end +end diff --git a/app/models/packages/nuget.rb b/app/models/packages/nuget.rb new file mode 100644 index 00000000000..42c167e9b7f --- /dev/null +++ b/app/models/packages/nuget.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Nuget + def self.table_name_prefix + 'packages_nuget_' + end + end +end diff --git a/app/models/packages/nuget/dependency_link_metadatum.rb b/app/models/packages/nuget/dependency_link_metadatum.rb new file mode 100644 index 00000000000..b586b55d3f0 --- /dev/null +++ b/app/models/packages/nuget/dependency_link_metadatum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Packages::Nuget::DependencyLinkMetadatum < ApplicationRecord + self.primary_key = :dependency_link_id + + belongs_to :dependency_link, inverse_of: :nuget_metadatum + + validates :dependency_link, :target_framework, presence: true + + validate :ensure_nuget_package_type + + private + + def ensure_nuget_package_type + return if dependency_link&.package&.nuget? + + errors.add(:base, _('Package type must be NuGet')) + end +end diff --git a/app/models/packages/nuget/metadatum.rb b/app/models/packages/nuget/metadatum.rb new file mode 100644 index 00000000000..1db8c0eddbf --- /dev/null +++ b/app/models/packages/nuget/metadatum.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Packages::Nuget::Metadatum < ApplicationRecord + belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_metadatum + + validates :package, presence: true + validates :license_url, public_url: { allow_blank: true } + validates :project_url, public_url: { allow_blank: true } + validates :icon_url, public_url: { allow_blank: true } + + validate :ensure_at_least_one_field_supplied + validate :ensure_nuget_package_type + + private + + def ensure_at_least_one_field_supplied + return if license_url? || project_url? || icon_url? + + errors.add(:base, _('Nuget metadatum must have at least license_url, project_url or icon_url set')) + end + + def ensure_nuget_package_type + return if package&.nuget? + + errors.add(:base, _('Package type must be NuGet')) + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb new file mode 100644 index 00000000000..d6633456de4 --- /dev/null +++ b/app/models/packages/package.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true +class Packages::Package < ApplicationRecord + include Sortable + include Gitlab::SQL::Pattern + include UsageStatistics + + belongs_to :project + # package_files must be destroyed by ruby code in order to properly remove carrierwave uploads and update project statistics + has_many :package_files, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :dependency_links, inverse_of: :package, class_name: 'Packages::DependencyLink' + has_many :tags, inverse_of: :package, class_name: 'Packages::Tag' + has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum' + has_one :pypi_metadatum, inverse_of: :package, class_name: 'Packages::Pypi::Metadatum' + has_one :maven_metadatum, inverse_of: :package, class_name: 'Packages::Maven::Metadatum' + has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum' + has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' + has_one :build_info, inverse_of: :package + + accepts_nested_attributes_for :conan_metadatum + accepts_nested_attributes_for :maven_metadatum + + delegate :recipe, :recipe_path, to: :conan_metadatum, prefix: :conan + + validates :project, presence: true + validates :name, presence: true + + validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan? + + validates :name, + uniqueness: { scope: %i[project_id version package_type] }, unless: :conan? + + validate :valid_conan_package_recipe, if: :conan? + validate :valid_npm_package_name, if: :npm? + validate :valid_composer_global_name, if: :composer? + validate :package_already_taken, if: :npm? + validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? } + validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? + validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? + validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? } + + enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5, composer: 6 } + + scope :with_name, ->(name) { where(name: name) } + scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } + scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) } + scope :with_version, ->(version) { where(version: version) } + scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) } + scope :with_package_type, ->(package_type) { where(package_type: package_type) } + + scope :with_conan_channel, ->(package_channel) do + joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel }) + end + scope :with_conan_username, ->(package_username) do + joins(:conan_metadatum).where(packages_conan_metadata: { package_username: package_username }) + end + + scope :with_composer_target, -> (target) do + includes(:composer_metadatum) + .joins(:composer_metadatum) + .where(Packages::Composer::Metadatum.table_name => { target_sha: target }) + end + scope :preload_composer, -> { preload(:composer_metadatum) } + + scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + + scope :has_version, -> { where.not(version: nil) } + scope :processed, -> do + where.not(package_type: :nuget).or( + where.not(name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) + ) + end + scope :preload_files, -> { preload(:package_files) } + scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) } + scope :limit_recent, ->(limit) { order_created_desc.limit(limit) } + scope :select_distinct_name, -> { select(:name).distinct } + + # Sorting + scope :order_created, -> { reorder('created_at ASC') } + scope :order_created_desc, -> { reorder('created_at DESC') } + scope :order_name, -> { reorder('name ASC') } + scope :order_name_desc, -> { reorder('name DESC') } + scope :order_version, -> { reorder('version ASC') } + scope :order_version_desc, -> { reorder('version DESC') } + scope :order_type, -> { reorder('package_type ASC') } + scope :order_type_desc, -> { reorder('package_type DESC') } + scope :order_project_name, -> { joins(:project).reorder('projects.name ASC') } + scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') } + scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') } + scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') } + + def self.for_projects(projects) + return none unless projects.any? + + where(project_id: projects) + end + + def self.only_maven_packages_with_path(path) + joins(:maven_metadatum).where(packages_maven_metadata: { path: path }) + end + + def self.by_name_and_file_name(name, file_name) + with_name(name) + .joins(:package_files) + .where(packages_package_files: { file_name: file_name }).last! + end + + def self.by_file_name_and_sha256(file_name, sha256) + joins(:package_files) + .where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last! + end + + def self.pluck_names + pluck(:name) + end + + def self.pluck_versions + pluck(:version) + end + + def self.sort_by_attribute(method) + case method.to_s + when 'created_asc' then order_created + when 'created_at_asc' then order_created + when 'name_asc' then order_name + when 'name_desc' then order_name_desc + when 'version_asc' then order_version + when 'version_desc' then order_version_desc + when 'type_asc' then order_type + when 'type_desc' then order_type_desc + when 'project_name_asc' then order_project_name + when 'project_name_desc' then order_project_name_desc + when 'project_path_asc' then order_project_path + when 'project_path_desc' then order_project_path_desc + else + order_created_desc + end + end + + def versions + project.packages + .with_name(name) + .where.not(version: version) + .with_package_type(package_type) + .order(:version) + end + + def pipeline + build_info&.pipeline + end + + def tag_names + tags.pluck(:name) + end + + private + + def valid_conan_package_recipe + recipe_exists = project.packages + .conan + .includes(:conan_metadatum) + .with_name(name) + .with_version(version) + .with_conan_channel(conan_metadatum.package_channel) + .with_conan_username(conan_metadatum.package_username) + .id_not_in(id) + .exists? + + errors.add(:base, _('Package recipe already exists')) if recipe_exists + end + + def valid_composer_global_name + # .default_scoped is required here due to a bug in rails that leaks + # the scope and adds `self` to the query incorrectly + # See https://github.com/rails/rails/pull/35186 + if Packages::Package.default_scoped.composer.with_name(name).where.not(project_id: project_id).exists? + errors.add(:name, 'is already taken by another project') + end + end + + def valid_npm_package_name + return unless project&.root_namespace + + unless name =~ %r{\A@#{project.root_namespace.path}/[^/]+\z} + errors.add(:name, 'is not valid') + end + end + + def package_already_taken + return unless project + + if project.package_already_taken?(name) + errors.add(:base, _('Package already exists')) + end + end +end diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb new file mode 100644 index 00000000000..567b5a14603 --- /dev/null +++ b/app/models/packages/package_file.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +class Packages::PackageFile < ApplicationRecord + include UpdateProjectStatistics + + delegate :project, :project_id, to: :package + delegate :conan_file_type, to: :conan_file_metadatum + + belongs_to :package + + has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum' + + accepts_nested_attributes_for :conan_file_metadatum + + validates :package, presence: true + validates :file, presence: true + validates :file_name, presence: true + + scope :recent, -> { order(id: :desc) } + scope :with_file_name, ->(file_name) { where(file_name: file_name) } + scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) } + scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) } + scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) } + + scope :with_conan_file_type, ->(file_type) do + joins(:conan_file_metadatum) + .where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] }) + end + + scope :with_conan_package_reference, ->(conan_package_reference) do + joins(:conan_file_metadatum) + .where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference }) + end + + mount_uploader :file, Packages::PackageFileUploader + + after_save :update_file_metadata, if: :saved_change_to_file? + + update_project_statistics project_statistics_name: :packages_size + + def update_file_metadata + # The file.object_store is set during `uploader.store!` + # which happens after object is inserted/updated + self.update_column(:file_store, file.object_store) + self.update_column(:size, file.size) unless file.size == self.size + end + + def download_path + Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) if ::Gitlab.ee? + end + + def local? + file_store == ::Packages::PackageFileUploader::Store::LOCAL + end +end + +Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFileGeo') diff --git a/app/models/packages/pypi.rb b/app/models/packages/pypi.rb new file mode 100644 index 00000000000..fc8a55caa31 --- /dev/null +++ b/app/models/packages/pypi.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Pypi + def self.table_name_prefix + 'packages_pypi_' + end + end +end diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb new file mode 100644 index 00000000000..7e6456ad964 --- /dev/null +++ b/app/models/packages/pypi/metadatum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Packages::Pypi::Metadatum < ApplicationRecord + self.primary_key = :package_id + + belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum + + validates :package, presence: true + + validate :pypi_package_type + + private + + def pypi_package_type + unless package&.pypi? + errors.add(:base, _('Package type must be PyPi')) + end + end +end diff --git a/app/models/packages/sem_ver.rb b/app/models/packages/sem_ver.rb new file mode 100644 index 00000000000..b73d51b08b7 --- /dev/null +++ b/app/models/packages/sem_ver.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Packages::SemVer + attr_accessor :major, :minor, :patch, :prerelease, :build + + def initialize(major = 0, minor = 0, patch = 0, prerelease = nil, build = nil, prefixed: false) + @major = major + @minor = minor + @patch = patch + @prerelease = prerelease + @build = build + @prefixed = prefixed + end + + def prefixed? + @prefixed + end + + def ==(other) + self.class == other.class && + self.major == other.major && + self.minor == other.minor && + self.patch == other.patch && + self.prerelease == other.prerelease && + self.build == other.build + end + + def to_s + s = "#{prefixed? ? 'v' : ''}#{major || 0}.#{minor || 0}.#{patch || 0}" + s += "-#{prerelease}" if prerelease + s += "+#{build}" if build + + s + end + + def self.match(str, prefixed: false) + return unless str&.start_with?('v') == prefixed + + str = str[1..] if prefixed + + Gitlab::Regex.semver_regex.match(str) + end + + def self.match?(str, prefixed: false) + !match(str, prefixed: prefixed).nil? + end + + def self.parse(str, prefixed: false) + m = match str, prefixed: prefixed + return unless m + + new(m[1].to_i, m[2].to_i, m[3].to_i, m[4], m[5], prefixed: prefixed) + end +end diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb new file mode 100644 index 00000000000..771d016daed --- /dev/null +++ b/app/models/packages/tag.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +class Packages::Tag < ApplicationRecord + belongs_to :package, inverse_of: :tags + + validates :package, :name, presence: true + + FOR_PACKAGES_TAGS_LIMIT = 200.freeze + NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags + + scope :preload_package, -> { preload(:package) } + scope :with_name, -> (name) { where(name: name) } + + def self.for_packages(packages) + where(package_id: packages.select(:id)) + .order(updated_at: :desc) + .limit(FOR_PACKAGES_TAGS_LIMIT) + end +end |