shenzhen_fir 0.14.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,94 @@
1
+ require 'plist'
2
+ require 'tempfile'
3
+ require 'zip'
4
+ require 'zip/filesystem'
5
+
6
+ command :info do |c|
7
+ c.syntax = 'ipa info [options]'
8
+ c.summary = 'Show mobile provisioning information about an .ipa file'
9
+ c.description = ''
10
+
11
+ c.action do |args, options|
12
+ say_error "`security` command not found in $PATH" and abort if `which security` == ""
13
+ say_error "`codesign` command not found in $PATH" and abort if `which codesign` == ""
14
+
15
+ determine_file! unless @file = args.pop
16
+ say_error "Missing or unspecified .ipa file" and abort unless @file and ::File.exist?(@file)
17
+
18
+ Zip::File.open(@file) do |zipfile|
19
+ app_entry = zipfile.find_entry("Payload/#{File.basename(@file, File.extname(@file))}.app")
20
+ provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
21
+
22
+ if (!provisioning_profile_entry)
23
+ zipfile.dir.entries("Payload").each do |dir_entry|
24
+ if dir_entry =~ /.app$/
25
+ say "Using .app: #{dir_entry}"
26
+ app_entry = zipfile.find_entry("Payload/#{dir_entry}")
27
+ provisioning_profile_entry = zipfile.find_entry("#{app_entry.name}embedded.mobileprovision") if app_entry
28
+ break
29
+ end
30
+ end
31
+ end
32
+
33
+ say_error "Embedded mobile provisioning file not found in #{@file}" and abort unless provisioning_profile_entry
34
+
35
+ tempdir = ::File.new(Dir.mktmpdir)
36
+ begin
37
+ zipfile.each do |zip_entry|
38
+ temp_entry_path = ::File.join(tempdir.path, zip_entry.name)
39
+
40
+ FileUtils.mkdir_p(::File.dirname(temp_entry_path))
41
+ zipfile.extract(zip_entry, temp_entry_path) unless ::File.exist?(temp_entry_path)
42
+ end
43
+
44
+ temp_provisioning_profile = ::File.new(::File.join(tempdir.path, provisioning_profile_entry.name))
45
+ temp_app_directory = ::File.new(::File.join(tempdir.path, app_entry.name))
46
+
47
+ plist = Plist::parse_xml(`security cms -D -i #{temp_provisioning_profile.path}`)
48
+
49
+ codesign = `codesign -dv "#{temp_app_directory.path}" 2>&1`
50
+ codesigned = /Signed Time/ === codesign
51
+
52
+ table = Terminal::Table.new do |t|
53
+ plist.each do |key, value|
54
+ next if key == "DeveloperCertificates"
55
+
56
+ columns = []
57
+ columns << key
58
+ columns << case value
59
+ when Hash
60
+ value.collect{|k, v| "#{k}: #{v}"}.join("\n")
61
+ when Array
62
+ value.join("\n")
63
+ else
64
+ value.to_s
65
+ end
66
+
67
+ t << columns
68
+ end
69
+
70
+ t << ["Codesigned", codesigned.to_s.capitalize]
71
+ end
72
+
73
+ puts table
74
+
75
+ rescue => e
76
+ say_error e.message
77
+ ensure
78
+ FileUtils.remove_entry_secure tempdir
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def determine_file!
86
+ files = Dir['*.ipa']
87
+ @file ||= case files.length
88
+ when 0 then nil
89
+ when 1 then files.first
90
+ else
91
+ @file = choose "Select an .ipa File:", *files
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+ $:.push File.expand_path('../', __FILE__)
2
+
3
+ require 'plugins/rivierabuild'
4
+ require 'plugins/hockeyapp'
5
+ require 'plugins/testfairy'
6
+ require 'plugins/deploygate'
7
+ require 'plugins/itunesconnect'
8
+ require 'plugins/ftp'
9
+ require 'plugins/s3'
10
+ require 'plugins/crashlytics'
11
+ require 'plugins/fir'
12
+ require 'plugins/pgyer'
13
+
14
+ require 'commands/build'
15
+ require 'commands/distribute'
16
+ require 'commands/info'
@@ -0,0 +1,9 @@
1
+ module Shenzhen::PlistBuddy
2
+ class << self
3
+ def print(file, key)
4
+ output = `/usr/libexec/PlistBuddy -c "Print :#{key}" "#{file}" 2> /dev/null`
5
+
6
+ !output || output.empty? || /Does Not Exist/ === output ? nil : output.strip
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,82 @@
1
+ require 'pathname'
2
+
3
+ module Shenzhen::Plugins
4
+ module Crashlytics
5
+ class Client
6
+
7
+ def initialize(crashlytics_path, api_token, build_secret)
8
+ @api_token, @build_secret = api_token, build_secret
9
+
10
+ @crashlytics_path = Pathname.new("#{crashlytics_path}/submit").cleanpath.to_s
11
+ say_error "Path to Crashlytics.framework/submit is invalid" and abort unless File.exists?(@crashlytics_path)
12
+ end
13
+
14
+ def upload_build(ipa, options)
15
+ command = "#{@crashlytics_path} #{@api_token} #{@build_secret} -ipaPath '#{options[:file]}'"
16
+ command += " -notesPath '#{options[:notes]}'" if options[:notes]
17
+ command += " -emails #{options[:emails]}" if options[:emails]
18
+ command += " -groupAliases #{options[:groups]}" if options[:groups]
19
+ command += " -notifications #{options[:notifications] ? 'YES' : 'NO'}"
20
+
21
+ system command
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ command :'distribute:crashlytics' do |c|
28
+ c.syntax = "ipa distribute:crashlytics [options]"
29
+ c.summary = "Distribute an .ipa file over Crashlytics"
30
+ c.description = ""
31
+ c.option '-c', '--crashlytics_path PATH', "/path/to/Crashlytics.framework/"
32
+ c.option '-f', '--file FILE', ".ipa file for the build"
33
+ c.option '-a', '--api_token TOKEN', "API Token. Available at https://www.crashlytics.com/settings/organizations"
34
+ c.option '-s', '--build_secret SECRET', "Build Secret. Available at https://www.crashlytics.com/settings/organizations"
35
+ c.option '-m', '--notes PATH', "Path to release notes file"
36
+ c.option '-e', '--emails EMAIL1,EMAIL2', "Emails of users for access"
37
+ c.option '-g', '--groups GROUPS', "Groups for users for access"
38
+ c.option '-n', '--notifications [YES | NO]', "Should send notification email to testers?"
39
+
40
+ c.action do |args, options|
41
+ determine_file! unless @file = options.file
42
+ say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
43
+
44
+ determine_crashlytics_path! unless @crashlytics_path = options.crashlytics_path || ENV['CRASHLYTICS_FRAMEWORK_PATH']
45
+ say_error "Missing path to Crashlytics.framework" and abort unless @crashlytics_path
46
+
47
+ determine_crashlytics_api_token! unless @api_token = options.api_token || ENV['CRASHLYTICS_API_TOKEN']
48
+ say_error "Missing API Token" and abort unless @api_token
49
+
50
+ determine_crashlytics_build_secret! unless @build_secret = options.build_secret || ENV['CRASHLYTICS_BUILD_SECRET']
51
+ say_error "Missing Build Secret" and abort unless @build_secret
52
+
53
+ parameters = {}
54
+ parameters[:file] = @file
55
+ parameters[:notes] = options.notes if options.notes
56
+ parameters[:emails] = options.emails if options.emails
57
+ parameters[:groups] = options.groups if options.groups
58
+ parameters[:notifications] = options.notifications == 'YES' if options.notifications
59
+
60
+ client = Shenzhen::Plugins::Crashlytics::Client.new(@crashlytics_path, @api_token, @build_secret)
61
+
62
+ if client.upload_build(@file, parameters)
63
+ say_ok "Build successfully uploaded to Crashlytics"
64
+ else
65
+ say_error "Error uploading to Crashlytics" and abort
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def determine_crashlytics_path!
72
+ @crashlytics_path ||= ask "Path to Crashlytics.framework:"
73
+ end
74
+
75
+ def determine_crashlytics_api_token!
76
+ @api_token ||= ask "API Token:"
77
+ end
78
+
79
+ def determine_crashlytics_build_secret!
80
+ @build_secret ||= ask "Build Secret:"
81
+ end
82
+ end
@@ -0,0 +1,97 @@
1
+ require 'json'
2
+ require 'openssl'
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ module Shenzhen::Plugins
7
+ module DeployGate
8
+ class Client
9
+ HOSTNAME = 'deploygate.com'
10
+
11
+ def initialize(api_token, user_name)
12
+ @api_token, @user_name = api_token, user_name
13
+ @connection = Faraday.new(:url => "https://#{HOSTNAME}", :request => { :timeout => 120 }) do |builder|
14
+ builder.request :multipart
15
+ builder.request :json
16
+ builder.response :json, :content_type => /\bjson$/
17
+ builder.use FaradayMiddleware::FollowRedirects
18
+ builder.adapter :net_http
19
+ end
20
+ end
21
+
22
+ def upload_build(ipa, options)
23
+ options.update({
24
+ :token => @api_token,
25
+ :file => Faraday::UploadIO.new(ipa, 'application/octet-stream'),
26
+ :message => options[:message] || ''
27
+ })
28
+
29
+ @connection.post("/api/users/#{@user_name}/apps", options).on_complete do |env|
30
+ yield env[:status], env[:body] if block_given?
31
+ end
32
+
33
+ rescue Faraday::Error::TimeoutError
34
+ say_error "Timed out while uploading build. Check https://deploygate.com/ to see if the upload was completed." and abort
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ command :'distribute:deploygate' do |c|
41
+ c.syntax = "ipa distribute:deploygate [options]"
42
+ c.summary = "Distribute an .ipa file over deploygate"
43
+ c.description = ""
44
+ c.option '-f', '--file FILE', ".ipa file for the build"
45
+ c.option '-a', '--api_token TOKEN', "API Token. Available at https://deploygate.com/settings"
46
+ c.option '-u', '--user_name USER_NAME', "User Name. Available at https://deploygate.com/settings"
47
+ c.option '-m', '--message MESSAGE', "Release message for the build"
48
+ c.option '-d', '--distribution_key DESTRIBUTION_KEY', "distribution key for distribution page"
49
+ c.option '-n', '--disable_notify', "disable notification"
50
+ c.option '-r', '--release_note RELEASE_NOTE', "release note for distribution page"
51
+ c.option '-v', '--visibility (private|public)', "privacy setting ( require public for personal free account)"
52
+
53
+ c.action do |args, options|
54
+ determine_file! unless @file = options.file
55
+ say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
56
+
57
+ determine_deploygate_api_token! unless @api_token = options.api_token || ENV['DEPLOYGATE_API_TOKEN']
58
+ say_error "Missing API Token" and abort unless @api_token
59
+
60
+ determine_deploygate_user_name! unless @user_name = options.user_name || ENV['DEPLOYGATE_USER_NAME']
61
+ say_error "Missing User Name" and abort unless @api_token
62
+
63
+ @message = options.message
64
+ @distribution_key = options.distribution_key || ENV['DEPLOYGATE_DESTRIBUTION_KEY']
65
+ @release_note = options.release_note
66
+ @disable_notify = ! options.disable_notify.nil? ? "yes" : nil
67
+ @visibility = options.visibility
68
+ @message = options.message
69
+
70
+ parameters = {}
71
+ parameters[:file] = @file
72
+ parameters[:message] = @message
73
+ parameters[:distribution_key] = @distribution_key if @distribution_key
74
+ parameters[:release_note] = @release_note if @release_note
75
+ parameters[:disable_notify] = @disable_notify if @disable_notify
76
+ parameters[:visibility] = @visibility if @visibility
77
+ parameters[:replace] = "true" if options.replace
78
+
79
+ client = Shenzhen::Plugins::DeployGate::Client.new(@api_token, @user_name)
80
+ response = client.upload_build(@file, parameters)
81
+ if (200...300) === response.status and not response.body["error"]
82
+ say_ok "Build successfully uploaded to DeployGate"
83
+ else
84
+ say_error "Error uploading to DeployGate: #{response.body["error"] || "(Unknown Error)"}" and abort
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def determine_deploygate_api_token!
91
+ @api_token ||= ask "API Token:"
92
+ end
93
+
94
+ def determine_deploygate_user_name!
95
+ @user_name ||= ask "User Name:"
96
+ end
97
+ end
@@ -0,0 +1,151 @@
1
+ require 'json'
2
+ require 'openssl'
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ module Shenzhen::Plugins
7
+ module Fir
8
+ class Client
9
+ HOSTNAME = 'api.fir.im'
10
+ VERSION = 'v2'
11
+
12
+
13
+ def initialize(user_token)
14
+ @user_token = user_token
15
+
16
+ @connection = Faraday.new(:url => "http://#{HOSTNAME}") do |builder|
17
+ builder.request :url_encoded
18
+ builder.response :json
19
+ builder.use FaradayMiddleware::FollowRedirects
20
+ builder.adapter :net_http
21
+ end
22
+ end
23
+
24
+
25
+ #
26
+ #get upload ticket
27
+ #
28
+ def get_upload_ticket bundle_id
29
+ options = {
30
+ :type => 'ios',
31
+ :bundle_id => bundle_id,
32
+ :api_token => @user_token
33
+ }
34
+
35
+ response = @connection.post('/apps', options) do |env|
36
+ yield env[:status], env[:body] if block_given?
37
+ end
38
+
39
+ rescue Faraday::Error::TimeoutError
40
+ say_error "Timed out while geting upload ticket." and abort
41
+ end
42
+
43
+ #
44
+ #upload file
45
+ #
46
+ def upload_file_and_update_app_info ipa, options
47
+ connection = Faraday.new(:url => options['upload_url'], :request => { :timeout => 360 }) do |builder|
48
+ builder.request :multipart
49
+ builder.response :json
50
+ builder.use FaradayMiddleware::FollowRedirects
51
+ builder.adapter :net_http
52
+ end
53
+
54
+ form_options = {
55
+ :key => options['key'],
56
+ :token => options['token'],
57
+ :file => Faraday::UploadIO.new(ipa, 'application/octet-stream'),
58
+ "x:name" => options[:name],
59
+ "x:version" => options[:version],
60
+ "x:build" => options[:build],
61
+ "x:release_type" => options[:release_type],
62
+ "x:changelog" => options[:changelog]
63
+ }
64
+ p "=================uploading====================="
65
+ connection.post('/', form_options).on_complete do |env|
66
+ yield env[:status], env[:body] if block_given?
67
+ end
68
+ rescue Errno::EPIPE
69
+ say_error "Upload failed. Check internet connection is ok." and abort
70
+ rescue Faraday::Error::TimeoutError
71
+ say_error "Timed out while uploading build. Check https://fir.im// to see if the upload was completed." and abort
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ command :'distribute:fir' do |c|
78
+ c.syntax = "ipa distribute:fir [options]"
79
+ c.summary = "请使用新版api_token => http://fir.im/user/info 获取 \n Distribute an .ipa file over fir.im"
80
+ c.description = ""
81
+ c.option '-f', '--file FILE', ".ipa file for the build"
82
+ c.option '-u', '--user_token 即fir.im 的 api_token ', "fir.im 的 api_token 在 http://fir.im/user/info 获取"
83
+ c.option '-a', '--app_id APPID', "App Id (iOS Bundle identifier)"
84
+ c.option '-n', '--notes NOTES', "Release notes for the build"
85
+ c.option '-N', '--app_name APP_NAME', "the name for app"
86
+ c.option '-R', '--release_type RELEASE_TYPE', "release_type for app default adhoc"
87
+ c.option '-V', '--app_version VERSION', "应用编译号 build"
88
+ c.option '-S', '--short_version SHORT', "App Short Version"
89
+
90
+ c.action do |args, options|
91
+ determine_file! unless @file = options.file
92
+ say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
93
+
94
+ determine_fir_user_token! unless @user_token = options.user_token || ENV['FIR_USER_TOKEN']
95
+ say_error "Missing User Token" and abort unless @user_token
96
+ determine_fir_app_id! unless @app_id = options.app_id || ENV['FIR_APP_ID']
97
+ say_error "Missing App Id" and abort unless @app_id
98
+
99
+ determine_notes! unless @notes = options.notes
100
+ say_error "Missing release notes" and abort unless @notes
101
+
102
+ determine_app_version! unless @app_version = options.app_version
103
+
104
+ determine_short_version! unless @short_version = options.short_version
105
+
106
+ client = Shenzhen::Plugins::Fir::Client.new(@user_token)
107
+ #get upload ticket
108
+ app_response = client.get_upload_ticket(@app_id)
109
+ if app_response.status == 201
110
+
111
+ upload_app_options = app_response.body['cert']['binary']
112
+ app_short_uri = app_response.body['short']
113
+ if options.app_name || ENV['APP_NAME']
114
+ upload_app_options[:name] = options.app_name || ENV['APP_NAME']
115
+ end
116
+ upload_app_options[:release_type] = options.release_type || "adhoc"
117
+ upload_app_options[:version] = @short_version
118
+ upload_app_options[:build] = @app_version
119
+ upload_app_options[:changelog] = @notes
120
+
121
+ #upload file
122
+ upload_response = client.upload_file_and_update_app_info(@file, upload_app_options)
123
+
124
+ if upload_response.status == 200
125
+ say_ok "Build successfully uploaded to Fir, visit url: http://fir.im/#{app_short_uri}"
126
+ else
127
+ say_error "Error uploading to Fir: #{upload_response.body[:error]}" and abort
128
+ end
129
+ else
130
+ say_error "Error getting app information: #{app_response.body[:error]}"
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def determine_fir_user_token!
137
+ @user_token ||= ask "User Token:"
138
+ end
139
+
140
+ def determine_fir_app_id!
141
+ @app_id ||= ask "App Id:"
142
+ end
143
+
144
+ def determine_app_version!
145
+ @app_version ||= ask "App Version:"
146
+ end
147
+
148
+ def determine_short_version!
149
+ @short_version ||= ask "Short Version:"
150
+ end
151
+ end
@@ -0,0 +1,181 @@
1
+ require 'net/ftp'
2
+ require 'net/sftp'
3
+
4
+ module Shenzhen::Plugins
5
+ module FTP
6
+ class Client
7
+
8
+ def initialize(host, port, user, password)
9
+ @host, @port, @user, @password = host, port, user, password
10
+ end
11
+
12
+ def upload(ipa, options = {})
13
+ connection = Net::FTP.new
14
+ connection.passive = true
15
+ connection.connect(@host, @port)
16
+
17
+ path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
18
+
19
+ begin
20
+ connection.login(@user, @password) rescue raise "Login authentication failed"
21
+
22
+ if options[:mkdir]
23
+ components, pwd = path.split(/\//).reject(&:empty?), nil
24
+ components.each do |component|
25
+ pwd = File.join(*[pwd, component].compact)
26
+
27
+ begin
28
+ connection.mkdir pwd
29
+ rescue => exception
30
+ raise exception unless /File exists/ === exception.message
31
+ end
32
+ end
33
+ end
34
+
35
+ connection.chdir path unless path.empty?
36
+ connection.putbinaryfile ipa, File.basename(ipa)
37
+ connection.putbinaryfile(options[:dsym], File.basename(options[:dsym])) if options[:dsym]
38
+ ensure
39
+ connection.close
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def expand_path_with_substitutions_from_ipa_plist(ipa, path)
46
+ substitutions = path.scan(/\{CFBundle[^}]+\}/)
47
+ return path if substitutions.empty?
48
+
49
+ Dir.mktmpdir do |dir|
50
+ system "unzip -q #{ipa} -d #{dir} 2> /dev/null"
51
+
52
+ plist = Dir["#{dir}/**/*.app/Info.plist"].last
53
+
54
+ substitutions.uniq.each do |substitution|
55
+ key = substitution[1...-1]
56
+ value = Shenzhen::PlistBuddy.print(plist, key)
57
+
58
+ path.gsub!(Regexp.new(substitution), value) if value
59
+ end
60
+ end
61
+
62
+ return path
63
+ end
64
+ end
65
+ end
66
+
67
+ module SFTP
68
+ class Client < Shenzhen::Plugins::FTP::Client
69
+ def upload(ipa, options = {})
70
+ session = Net::SSH.start(@host, @user, :password => @password, :port => @port)
71
+ connection = Net::SFTP::Session.new(session).connect!
72
+
73
+ path = expand_path_with_substitutions_from_ipa_plist(ipa, options[:path])
74
+
75
+ begin
76
+ connection.stat!(path) do |response|
77
+ connection.mkdir! path if options[:mkdir] and not response.ok?
78
+
79
+ connection.upload! ipa, determine_file_path(File.basename(ipa), path)
80
+ connection.upload! options[:dsym], determine_file_path(File.basename(options[:dsym]), path) if options[:dsym]
81
+ end
82
+ ensure
83
+ connection.close_channel
84
+ session.shutdown!
85
+ end
86
+ end
87
+
88
+ def determine_file_path(file_name, path)
89
+ if path.empty?
90
+ file_name
91
+ else
92
+ "#{path}/#{file_name}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ command :'distribute:ftp' do |c|
100
+ c.syntax = "ipa distribute:ftp [options]"
101
+ c.summary = "Distribute an .ipa file over FTP"
102
+ c.description = ""
103
+
104
+ c.example '', '$ ipa distribute:ftp --host 127.0.0.1 -f ./file.ipa -u username --path "/path/to/folder/{CFBundleVersion}/" --mkdir'
105
+
106
+ c.option '-f', '--file FILE', ".ipa file for the build"
107
+ c.option '-d', '--dsym FILE', "zipped .dsym package for the build"
108
+ c.option '-h', '--host HOST', "FTP host"
109
+ c.option '-u', '--user USER', "FTP user"
110
+ c.option '-p', '--password PASS', "FTP password"
111
+ c.option '-P', '--path PATH', "FTP path. Values from Info.plist will be substituted for keys wrapped in {} \n\t\t e.g. \"/path/to/folder/{CFBundleVersion}/\" would be evaluated as \"/path/to/folder/1.0.0/\""
112
+ c.option '--port PORT', "FTP port"
113
+ c.option '--protocol [PROTOCOL]', [:ftp, :sftp], "Protocol to use (ftp, sftp)"
114
+ c.option '--[no-]mkdir', "Create directories on FTP if they don't already exist"
115
+
116
+ c.action do |args, options|
117
+ options.default :mkdir => true
118
+
119
+ determine_file! unless @file = options.file
120
+ say_error "Missing or unspecified .ipa file" and abort unless @file and File.exist?(@file)
121
+
122
+ determine_dsym! unless @dsym = options.dsym
123
+ say_warning "Specified dSYM.zip file doesn't exist" unless @dsym and File.exist?(@dsym)
124
+
125
+ determine_host! unless @host = options.host
126
+ say_error "Missing FTP host" and abort unless @host
127
+
128
+ determine_port!(options.protocol) unless @port = options.port
129
+
130
+ determine_user! unless @user = options.user
131
+ say_error "Missing FTP user" and abort unless @user
132
+
133
+ @password = options.password
134
+ if !@password && options.protocol != :sftp
135
+ determine_password!
136
+ say_error "Missing FTP password" and abort unless @password
137
+ end
138
+
139
+ @path = options.path || ""
140
+
141
+ client = case options.protocol
142
+ when :sftp
143
+ Shenzhen::Plugins::SFTP::Client.new(@host, @port, @user, @password)
144
+ else
145
+ Shenzhen::Plugins::FTP::Client.new(@host, @port, @user, @password)
146
+ end
147
+
148
+ begin
149
+ client.upload @file, {:path => @path, :dsym => @dsym, :mkdir => !!options.mkdir}
150
+ say_ok "Build successfully uploaded to FTP"
151
+ rescue => exception
152
+ say_error "Error while uploading to FTP: #{exception}"
153
+ raise if options.trace
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def determine_host!
160
+ @host ||= ask "FTP Host:"
161
+ end
162
+
163
+ def determine_port!(protocol)
164
+ @port = case protocol
165
+ when :sftp
166
+ Net::SSH::Transport::Session::DEFAULT_PORT
167
+ else
168
+ Net::FTP::FTP_PORT
169
+ end
170
+ end
171
+
172
+ def determine_user!
173
+ @user ||= ask "Username:"
174
+ end
175
+
176
+ def determine_password!
177
+ @password ||= password "Password:"
178
+ end
179
+ end
180
+
181
+ alias_command :'distribute:sftp', :'distribute:ftp', '--protocol', 'sftp'