166 lines
5 KiB
Ruby
166 lines
5 KiB
Ruby
# frozen_string_literal: true
|
|
module Packages
|
|
module Npm
|
|
class CreatePackageService < ::Packages::CreatePackageService
|
|
include Gitlab::Utils::StrongMemoize
|
|
include ExclusiveLeaseGuard
|
|
|
|
PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename licenseText].freeze
|
|
DEFAULT_LEASE_TIMEOUT = 1.hour.to_i
|
|
|
|
def execute
|
|
return error('Version is empty.', 400) if version.blank?
|
|
return error('Attachment data is empty.', 400) if attachment['data'].blank?
|
|
return error('Package already exists.', 403) if current_package_exists?
|
|
return error('File is too large.', 400) if file_size_exceeded?
|
|
|
|
package = try_obtain_lease do
|
|
ApplicationRecord.transaction { create_npm_package! }
|
|
end
|
|
|
|
return error('Could not obtain package lease.', 400) unless package
|
|
|
|
package
|
|
end
|
|
|
|
private
|
|
|
|
def create_npm_package!
|
|
package = create_package!(:npm, name: name, version: version)
|
|
|
|
::Packages::CreatePackageFileService.new(package, file_params).execute
|
|
::Packages::CreateDependencyService.new(package, package_dependencies).execute
|
|
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
|
|
|
|
create_npm_metadatum!(package)
|
|
|
|
package
|
|
end
|
|
|
|
def create_npm_metadatum!(package)
|
|
package.create_npm_metadatum!(package_json: package_json)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
if package.npm_metadatum && package.npm_metadatum.errors.added?(:package_json, 'structure is too large')
|
|
Gitlab::ErrorTracking.track_exception(e, field_sizes: field_sizes_for_error_tracking)
|
|
end
|
|
|
|
raise
|
|
end
|
|
|
|
def current_package_exists?
|
|
project.packages
|
|
.npm
|
|
.with_name(name)
|
|
.with_version(version)
|
|
.not_pending_destruction
|
|
.exists?
|
|
end
|
|
|
|
def name
|
|
params[:name]
|
|
end
|
|
|
|
def version
|
|
strong_memoize(:version) do
|
|
params[:versions].each_key.first
|
|
end
|
|
end
|
|
|
|
def version_data
|
|
params[:versions][version]
|
|
end
|
|
|
|
def package_json
|
|
version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
|
|
end
|
|
|
|
def dist_tag
|
|
params['dist-tags'].each_key.first
|
|
end
|
|
|
|
def package_file_name
|
|
strong_memoize(:package_file_name) do
|
|
"#{name}-#{version}.tgz"
|
|
end
|
|
end
|
|
|
|
def attachment
|
|
strong_memoize(:attachment) do
|
|
params['_attachments'][package_file_name]
|
|
end
|
|
end
|
|
|
|
# TODO (technical debt): Extract the package size calculation to its own component and unit test it separately.
|
|
def calculated_package_file_size
|
|
strong_memoize(:calculated_package_file_size) do
|
|
# This calculation is based on:
|
|
# 1. 4 chars in a Base64 encoded string are 3 bytes in the original string. Meaning 1 char is 0.75 bytes.
|
|
# 2. The encoded string may have 1 or 2 extra '=' chars used for padding. Each padding char means 1 byte less in the original string.
|
|
# Reference:
|
|
# - https://blog.aaronlenoir.com/2017/11/10/get-original-length-from-base-64-string/
|
|
# - https://en.wikipedia.org/wiki/Base64#Decoding_Base64_with_padding
|
|
encoded_data = attachment['data']
|
|
((encoded_data.length * 0.75) - encoded_data[-2..].count('=')).to_i
|
|
end
|
|
end
|
|
|
|
def file_params
|
|
{
|
|
file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])),
|
|
size: calculated_package_file_size,
|
|
file_sha1: version_data[:dist][:shasum],
|
|
file_name: package_file_name,
|
|
build: params[:build]
|
|
}
|
|
end
|
|
|
|
def package_dependencies
|
|
_version, versions_data = params[:versions].first
|
|
versions_data
|
|
end
|
|
|
|
def file_size_exceeded?
|
|
project.actual_limits.exceeded?(:npm_max_file_size, calculated_package_file_size)
|
|
end
|
|
|
|
# used by ExclusiveLeaseGuard
|
|
def lease_key
|
|
"packages:npm:create_package_service:packages:#{project.id}_#{name}_#{version}"
|
|
end
|
|
|
|
# used by ExclusiveLeaseGuard
|
|
def lease_timeout
|
|
DEFAULT_LEASE_TIMEOUT
|
|
end
|
|
|
|
def field_sizes
|
|
strong_memoize(:field_sizes) do
|
|
package_json.transform_values do |value|
|
|
value.to_s.size
|
|
end
|
|
end
|
|
end
|
|
|
|
def filtered_field_sizes
|
|
strong_memoize(:filtered_field_sizes) do
|
|
field_sizes.select do |_, size|
|
|
size >= ::Packages::Npm::Metadatum::MIN_PACKAGE_JSON_FIELD_SIZE_FOR_ERROR_TRACKING
|
|
end
|
|
end
|
|
end
|
|
|
|
def largest_fields
|
|
strong_memoize(:largest_fields) do
|
|
field_sizes
|
|
.sort_by { |a| a[1] }
|
|
.reverse[0..::Packages::Npm::Metadatum::NUM_FIELDS_FOR_ERROR_TRACKING - 1]
|
|
.to_h
|
|
end
|
|
end
|
|
|
|
def field_sizes_for_error_tracking
|
|
filtered_field_sizes.empty? ? largest_fields : filtered_field_sizes
|
|
end
|
|
end
|
|
end
|
|
end
|