diff --git a/libs/ruby/warp_engine/app/controllers/warp_engine/build/publish_controller.rb b/libs/ruby/warp_engine/app/controllers/warp_engine/build/publish_controller.rb index cfb8915..e057c99 100644 --- a/libs/ruby/warp_engine/app/controllers/warp_engine/build/publish_controller.rb +++ b/libs/ruby/warp_engine/app/controllers/warp_engine/build/publish_controller.rb @@ -10,7 +10,7 @@ module WarpEngine api :POST, "/build/publish", "Register an uploaded build as a release" header "X-Update-Secret", "Shared secret or application token (update scope)", required: true param :name, String, required: true, desc: "Software name" - param :platform, String, required: true, desc: "Platform (tic80, love, ebitengine, c64, godot, bevy, phaser)" + param :platform, String, required: true, desc: "Platform (one of WarpEngine::Platform::NAMES)" param :version, String, required: true, desc: "Version string" returns code: 200, desc: "JSON with the published name/platform/version" error code: 401, desc: "Invalid secret" @@ -21,24 +21,21 @@ module WarpEngine return render json: { error: "Unauthorized" }, status: :unauthorized end - %i[name platform version].each do |key| - return render json: { error: "#{key.to_s.capitalize} not provided" }, status: :bad_request if params[key].blank? - end - - unless software_ownership_authorized?(params[:name]) - return render json: { error: "Forbidden" }, status: :forbidden - end - input = WarpEngine::PublishInputDto.new( platform: params[:platform], name: params[:name], version: params[:version] ) + return render json: { error: input.error_message }, status: :bad_request unless input.valid? + + unless software_ownership_authorized?(input.name) + return render json: { error: "Forbidden" }, status: :forbidden + end WarpEngine::PublishService.new.publish(input) - claim_software_ownership(params[:name]) + claim_software_ownership(input.name) - render json: { published: true, name: params[:name], platform: params[:platform], version: params[:version] } + render json: { published: true, name: input.name, platform: input.platform, version: input.version } end end end diff --git a/libs/ruby/warp_engine/app/controllers/warp_engine/build/uploads_controller.rb b/libs/ruby/warp_engine/app/controllers/warp_engine/build/uploads_controller.rb index d44337a..de388ab 100644 --- a/libs/ruby/warp_engine/app/controllers/warp_engine/build/uploads_controller.rb +++ b/libs/ruby/warp_engine/app/controllers/warp_engine/build/uploads_controller.rb @@ -9,8 +9,6 @@ module WarpEngine short "Build artifact upload" end - NAME_FORMAT = /\A[A-Za-z0-9._-]+\z/ - api :POST, "/build/upload", "Upload a build artifact into the artifact directory" header "X-Update-Secret", "Shared secret or application token (upload scope)", required: true param :name, String, required: true, desc: "Software name (filename must be prefixed with -)" @@ -28,35 +26,30 @@ module WarpEngine return render json: { error: "Unauthorized" }, status: :unauthorized end - name = params[:name].to_s - version = params[:version].to_s - file = params[:file] + input = WarpEngine::UploadInputDto.new( + name: params[:name].to_s, + version: params[:version].to_s, + file: params[:file], + sha256: params[:sha256] + ) + return render json: { error: input.error_message }, status: :bad_request unless input.valid? - return render json: { error: "Invalid name" }, status: :bad_request unless name.match?(NAME_FORMAT) - return render json: { error: "Invalid version" }, status: :bad_request unless version.match?(NAME_FORMAT) - return render json: { error: "File not provided" }, status: :bad_request unless file.respond_to?(:original_filename) - - unless software_ownership_authorized?(name) + unless software_ownership_authorized?(input.name) return render json: { error: "Forbidden" }, status: :forbidden end - filename = File.basename(file.original_filename.to_s) - unless filename.start_with?("#{name}-#{version}.", "#{name}-#{version}-") - return render json: { error: "Filename must be prefixed with #{name}-#{version}" }, status: :bad_request - end - max = WarpEngine.config.max_upload_size - if file.size > max + if input.file.size > max return render json: { error: "File too large (max #{max / (1024 * 1024)}MB)" }, status: :payload_too_large end - digest = Digest::SHA256.file(file.tempfile.path).hexdigest - if params[:sha256].present? && !ActiveSupport::SecurityUtils.secure_compare(params[:sha256].downcase, digest) + digest = Digest::SHA256.file(input.file.tempfile.path).hexdigest + if input.sha256.present? && !ActiveSupport::SecurityUtils.secure_compare(input.sha256.downcase, digest) return render json: { error: "SHA256 mismatch" }, status: :unprocessable_entity end - stored = WarpEngine::FileManagerService.new.upload("", file) - render json: { file: stored, size: file.size, sha256: digest } + stored = WarpEngine::FileManagerService.new.upload("", input.file) + render json: { file: stored, size: input.file.size, sha256: digest } end end end diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/publish_input_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/publish_input_dto.rb index b142a52..80fa1f3 100644 --- a/libs/ruby/warp_engine/app/dtos/warp_engine/publish_input_dto.rb +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/publish_input_dto.rb @@ -1,7 +1,14 @@ module WarpEngine - PublishInputDto = Struct.new(:platform, :name, :version, keyword_init: true) do - def initialize(platform:, name:, version: nil) - super - end + class PublishInputDto + include ActiveModel::Model + + attr_accessor :platform, :name, :version + + validates :name, presence: true + validates :version, presence: true + validates :platform, presence: true + validates :platform, inclusion: { in: WarpEngine::Platform::NAMES }, allow_blank: true + + def error_message = errors.full_messages.to_sentence end end diff --git a/libs/ruby/warp_engine/app/dtos/warp_engine/upload_input_dto.rb b/libs/ruby/warp_engine/app/dtos/warp_engine/upload_input_dto.rb new file mode 100644 index 0000000..b9e340f --- /dev/null +++ b/libs/ruby/warp_engine/app/dtos/warp_engine/upload_input_dto.rb @@ -0,0 +1,37 @@ +module WarpEngine + class UploadInputDto + include ActiveModel::Model + + NAME_FORMAT = /\A[A-Za-z0-9._-]+\z/ + + attr_accessor :name, :version, :file, :sha256 + + validates :name, format: { with: NAME_FORMAT } + validates :version, format: { with: NAME_FORMAT } + validate :file_present + validate :filename_prefixed + + def filename + return nil unless file.respond_to?(:original_filename) + + File.basename(file.original_filename.to_s) + end + + def error_message = errors.full_messages.to_sentence + + private + + def file_present + return if file.respond_to?(:original_filename) + + errors.add(:file, "not provided") + end + + def filename_prefixed + return if filename.nil? || errors.include?(:name) || errors.include?(:version) + return if filename.start_with?("#{name}-#{version}.", "#{name}-#{version}-") + + errors.add(:file, "name must be prefixed with #{name}-#{version}") + end + end +end