Commit aeebc047 authored by Vadim Vlasov's avatar Vadim Vlasov

Chunked Upload implemented

parent cfcf8c75
......@@ -2,42 +2,42 @@ module Fastlane
module Actions
class AppgateUploadAction < Action
def self.run(params)
require 'faraday'
require 'faraday_middleware'
Helper::AppGateHelper.verify_required_params(params)
api_key = params[:api_key]
app_id = params[:app_id]
file_path = params[:file_path]
branch = params[:branch]
api_key = "bearer " + params[:api_key]
include_branch = params[:include_branch]
skip_waiting_for_release = params[:skip_waiting_for_release]
url = "https://apps.furylion.net/api/app/#{app_id}/upload"
UI.message("Uploading binary...")
conn = Faraday.new(url: url) do |faraday|
faraday.request :multipart
faraday.request :url_encoded
faraday.adapter Faraday.default_adapter
faraday.options.timeout = 600
faraday.options.open_timeout = 10
if !include_branch
branch = ""
end
payload = {
file: Faraday::UploadIO.new(file_path, 'application/octet-stream')
}
payload[:branch] = branch if include_branch
UI.message("Starting release upload...")
release_data = Helper::AppGateHelper.create_release(api_key, app_id, branch, File.basename(file_path), File.size(file_path))
UI.abort_with_message!("Failed to create release") unless release_data
response = conn.post do |req|
req.headers['Authorization'] = api_key
req.body = payload
end
release_id = release_data[:release_id]
upload_token = release_data[:upload_token]
case response.status
when 200..205
UI.success("Upload complete!")
when 401
UI.user_error!("Auth Error, provided invalid token")
when 400...407, 409...428, 430...499
UI.user_error!("Client error: #{response.status}: #{response.body}")
uploaded = Helper::AppGateHelper.upload_file_parallel(release_id, file_path, upload_token)
UI.abort_with_message!("Failed to upload file") unless uploaded
UI.message("Finishing release...")
finished = Helper::AppGateHelper.finish_upload(release_id, upload_token)
UI.abort_with_message!("Failed to finish upload") unless finished
if skip_waiting_for_release
UI.success("Successfully uploaded release! Skipping wait for processing.")
else
UI.user_error!("Unexpected error: #{response.status}: #{response.body}")
UI.message("Waiting for release to be ready...")
release_ready = Helper::AppGateHelper.wait_for_release_to_be_ready(release_id, upload_token)
UI.abort_with_message!("Failed to wait for release to be ready") unless release_ready
UI.success("Successfully uploaded and finished release!")
end
end
......@@ -76,7 +76,13 @@ module Fastlane
FastlaneCore::ConfigItem.new(key: :api_key,
env_name: "APPGATE_API_KEY",
description: "API key for AppGate",
sensitive: true)
sensitive: true),
FastlaneCore::ConfigItem.new(key: :skip_waiting_for_release,
env_name: "APPGATE_SKIP_WAITING_FOR_RELEASE",
description: "Skip waiting for release processing",
optional: true,
is_string: false,
default_value: false)
]
end
......
module Fastlane
module Helper
class AppGateHelper
BASE_URL = "https://apps.furylion.net/api"
CHUNK_SIZE = 4 * 1024 * 1024 # 4MB
# Maximum number of retries for a request
MAX_RETRIES = 5
# Delay between retries in seconds
RETRY_DELAY = 5
# Time to wait between 2 status polls in seconds
RELEASE_UPLOAD_STATUS_POLL_INTERVAL = 1
def self.verify_required_params(params)
required_params = [:api_key, :app_id, :file_path]
required_params.each do |param|
UI.user_error!("Missing required parameter: #{param}") unless params[param]
end
end
def self.create_release(api_key, app_id, branch, file_name, file_size)
url = "#{BASE_URL}/app/#{app_id}/releases"
body = {
branch: branch,
size: file_size,
file_name: file_name
}
response = self.request_with_retry(:post, url, api_key, body)
return { release_id: response['release_id'], upload_token: response['upload_token'] } if response && response['release_id'] && response['upload_token']
nil
end
def self.upload_file(release_id, file, upload_token)
File.open(file, 'rb') do |f|
chunk_number = 0
until f.eof?
chunk = f.read(CHUNK_SIZE)
unless upload_chunk(release_id, upload_token, chunk, chunk_number)
return false
end
chunk_number += 1
UI.message("Uploaded chunk #{chunk_number}")
end
UI.message("Binary uploaded")
end
true
end
def self.human_readable_size(size_in_bytes)
units = ['B', 'KB', 'MB', 'GB', 'TB']
index = 0
size = size_in_bytes.to_f
while size >= 1024 && index < units.length - 1
size /= 1024
index += 1
end
return sprintf("%.2f %s", size, units[index])
end
def self.upload_file_parallel(release_id, file, upload_token, max_threads = 5)
file_size = File.size(file)
chunk_count = (file_size.to_f / CHUNK_SIZE).ceil
UI.message("Uploading release binary...")
UI.message("File size: #{human_readable_size(file_size)}")
UI.message("Number of chunks: #{chunk_count}")
UI.message("Using #{max_threads} threads for parallel upload")
queue = Queue.new
chunk_count.times { |i| queue << i }
mutex = Mutex.new # Добавляем мьютекс для синхронизации вывода
threads = max_threads.times.map do
Thread.new do
while (chunk_number = queue.pop(true) rescue nil)
File.open(file, 'rb') do |f|
f.seek(chunk_number * CHUNK_SIZE)
chunk = f.read(CHUNK_SIZE)
if upload_chunk(release_id, upload_token, chunk, chunk_number)
mutex.synchronize { UI.message("Uploaded chunk #{chunk_number}") }
else
mutex.synchronize { UI.error("Failed to upload chunk #{chunk_number}") }
end
end
end
end
end
threads.each(&:join)
UI.message("Binary uploaded")
true
end
def self.upload_chunk(release_id, upload_token, chunk, chunk_number)
url = "#{BASE_URL}/release/#{release_id}/upload/chunk"
query = {
token: upload_token,
block_number: chunk_number
}
response = self.request_with_retry(:post, url, nil, chunk, query)
response.success?
end
def self.finish_upload(release_id, upload_token)
url = "#{BASE_URL}/release/#{release_id}/upload/finish"
query = {
token: upload_token
}
response = self.request_with_retry(:post, url, nil, nil, query)
response.success?
end
def self.request_with_retry(method, url, api_key = nil, body = nil, query = nil)
retries = 0
begin
response = self.request(method, url, api_key, body, query)
if response.nil?
raise "Request failed with nil response"
end
return response
rescue => e
UI.error("Error occurred: #{e.message}")
if retries < MAX_RETRIES
retries += 1
UI.message("Retrying in #{RETRY_DELAY} seconds (attempt #{retries}/#{MAX_RETRIES})...")
sleep(RETRY_DELAY)
retry
else
UI.user_error!("Max retries reached. Request failed.")
end
end
end
def self.request(method, url, api_key = nil, body = nil, query = nil)
headers = {}
headers['Authorization'] = "Bearer #{api_key}" if api_key
headers['Content-Type'] = 'application/json' unless headers['Content-Type']
begin
response = Faraday.new.send(method) do |req|
req.url url
req.headers = headers
req.params = query if query
if body
if headers['Content-Type'] == 'application/json' && body.is_a?(Hash)
req.body = body.to_json
else
req.body = body
end
end
end
if response.success?
return JSON.parse(response.body) if response.body && !response.body.empty? && headers['Content-Type'] == 'application/json'
return response
else
UI.error("Error: #{response.status} - #{response.body}")
return nil
end
rescue Faraday::Error => e
UI.error("Network error: #{e}")
return nil
end
end
def self.wait_for_release_to_be_ready(release_id, upload_token)
url = "#{BASE_URL}/release/#{release_id}/status"
query = {
token: upload_token,
}
loop do
response = self.request_with_retry(:get, url, nil, nil, query)
if response && response['status']
case response['status']
when 'UploadFinished'
return true
when 'UploadFailed'
UI.user_error!("Upload failed: #{response['error_message']}")
return false
end
else
UI.error("Failed to get release status")
end
sleep(RELEASE_UPLOAD_STATUS_POLL_INTERVAL)
end
end
end
end
end
\ No newline at end of file
module Fastlane
module Furylion
VERSION = "1.2.1"
VERSION = "1.3.0"
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment