itunes_store_transporter 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Changes +12 -0
- data/README.rdoc +18 -11
- data/bin/itms +72 -45
- data/lib/itunes/store/transporter.rb +0 -1
- data/lib/itunes/store/transporter/command.rb +21 -22
- data/lib/itunes/store/transporter/command/lookup.rb +4 -3
- data/lib/itunes/store/transporter/command/status.rb +23 -9
- data/lib/itunes/store/transporter/command/verify.rb +8 -3
- data/lib/itunes/store/transporter/output_parser.rb +53 -54
- data/lib/itunes/store/transporter/shell.rb +5 -1
- data/lib/itunes/store/transporter/version.rb +1 -1
- data/spec/command_spec.rb +72 -39
- data/spec/fixtures/errors_and_warnings.yml +45 -0
- data/spec/fixtures/providers.yml +5 -0
- data/spec/fixtures/status.yml +29 -0
- data/spec/fixtures/stderr.yml +20 -0
- data/spec/fixtures/stdout.yml +5 -0
- data/spec/shell_spec.rb +17 -10
- data/spec/spec_helper.rb +14 -15
- metadata +27 -12
data/Changes
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
v0.0.2 2012-09-23
|
2
|
+
--------------------
|
3
|
+
Enhancements:
|
4
|
+
* `itms` added --no-config option
|
5
|
+
* `itms` allow boolean options to be set to false via --no-XXXX, e.g., --no-print-stderr
|
6
|
+
|
7
|
+
Bug Fixes:
|
8
|
+
* `itms status` passed the wrong arguments to the underlying method
|
9
|
+
* `itms` failed when the config file was empty
|
10
|
+
* Verify command :verify_assets => true would disable asset verification
|
11
|
+
* Status command ignored multiple status lines
|
12
|
+
* Lookup command failed to create tempdir
|
data/README.rdoc
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
= iTunes::Store::Transporter
|
2
2
|
|
3
|
+
{<img src="https://secure.travis-ci.org/sshaw/itunes_store_transporter.png"/>}[http://travis-ci.org/sshaw/itunes_store_transporter]
|
4
|
+
{<img src="https://codeclimate.com/badge.png"/>}[https://codeclimate.com/github/sshaw/itunes_store_transporter]
|
5
|
+
|
3
6
|
Upload and manage your assets in the iTunes Store using the iTunes Store's Transporter (+iTMSTransporter+).
|
4
7
|
|
5
8
|
=== Overview
|
@@ -36,7 +39,7 @@ supports the following operations:
|
|
36
39
|
* List providers
|
37
40
|
* Retrieve iTunes metadata schemas
|
38
41
|
|
39
|
-
It also includes +itms+, an executable that's sorta like using +iTMSTransporter+ directly
|
42
|
+
It also includes +itms+, an executable that's sorta like using +iTMSTransporter+ directly except
|
40
43
|
that it can send email notifications and allows one to set global/per-command defaults via <code>$HOME/.itms</code>.
|
41
44
|
|
42
45
|
=== Requirements
|
@@ -47,7 +50,7 @@ that it can send email notifications and allows one to set global/per-command de
|
|
47
50
|
=== Running on Windows
|
48
51
|
|
49
52
|
On Windows +iTMSTransporter+ is called via the +iTMSTransporter.CMD+ batch file. This file does not handle the
|
50
|
-
+iTMSTransporter+'s exit status correctly, causing <code>iTunes::Store::Transporter</code> to report everything as a success.
|
53
|
+
+iTMSTransporter+'s exit status correctly, causing <code>iTunes::Store::Transporter</code> to report everything as a success.
|
51
54
|
|
52
55
|
This can be fixed by modifying +iTMSTransporter.CMD+ (note that the following does not mimic the batch file exactly):
|
53
56
|
|
@@ -68,18 +71,18 @@ This can be fixed by modifying +iTMSTransporter.CMD+ (note that the following do
|
|
68
71
|
<code>itms COMMAND [OPTIONS]</code>
|
69
72
|
|
70
73
|
* +COMMAND+ - The command (<code>iTunes::Store::Transporter</code> method) to run
|
71
|
-
* +OPTIONS+ - These are
|
74
|
+
* +OPTIONS+ - These are quivalent to the given +COMMAND+'s options except they must be given in long option format. For example <code>:apple_id => "X123"</code> would be <code>--apple-id=X123</code>. Boolean options can be negated with the <code>--no-</code> prefix.
|
72
75
|
|
73
76
|
==== Config file
|
74
77
|
|
75
|
-
Default options and email notifications can be placed in a YAML file at <code>$HOME/.itms</code>.
|
78
|
+
Default options and email notifications can be placed in a YAML file at <code>$HOME/.itms</code>. To skip loading the config file use the <code>--no-config</code> option.
|
76
79
|
|
77
80
|
# Global command defaults
|
78
81
|
path: /usr/bin
|
79
82
|
username: sshaw
|
80
83
|
password: Pa55W0rd!
|
81
84
|
|
82
|
-
# Global email defaults
|
85
|
+
# Global email defaults
|
83
86
|
email:
|
84
87
|
to: everyone@example.com
|
85
88
|
from: no-reply@example.com
|
@@ -93,30 +96,34 @@ Default options and email notifications can be placed in a YAML file at <code>$H
|
|
93
96
|
upload:
|
94
97
|
shortname: enc0d3rz
|
95
98
|
transport: Aspera
|
96
|
-
rate:
|
99
|
+
rate: 750000
|
97
100
|
# Email notifications for the upload command
|
98
101
|
email:
|
99
102
|
success:
|
100
103
|
cc: assets@example.com
|
101
104
|
subject: iTunes Upload <%= @apple_id %>
|
102
|
-
message: |
|
103
|
-
|
105
|
+
message: |
|
106
|
+
<%= @username %> uploaded it using <%= @transport %>
|
104
107
|
|
105
108
|
Bye!
|
106
109
|
failure:
|
107
110
|
to: support@example.com
|
108
111
|
subject: Upload Failed!
|
109
112
|
message: |
|
110
|
-
Here's the problem:
|
113
|
+
Here's the problem:
|
111
114
|
|
112
|
-
<%= @error %>
|
115
|
+
<%= @error %>
|
113
116
|
|
114
117
|
Fix it!
|
115
118
|
|
119
|
+
As you can see, command options are turned into template variables.
|
120
|
+
|
116
121
|
=== More Info
|
117
122
|
|
118
|
-
*
|
123
|
+
* Docs: http://ruby-doc.org/gems/docs/i/itunes_store_transporter-0.0.2/README_rdoc.html
|
119
124
|
* Bugs: http://github.com/sshaw/itunes_store_transporter/issues
|
125
|
+
* Source Code: http://github.com/sshaw/itunes_store_transporter
|
126
|
+
* Web Based GUI: http://github.com/sshaw/itunes_store_transporter_web
|
120
127
|
|
121
128
|
=== Author
|
122
129
|
|
data/bin/itms
CHANGED
@@ -5,7 +5,7 @@ require "yaml"
|
|
5
5
|
require "net/smtp"
|
6
6
|
require "itunes/store/transporter"
|
7
7
|
|
8
|
-
# Command line interface to the ITunes::Store::Transporter library.
|
8
|
+
# Command line interface to the ITunes::Store::Transporter library.
|
9
9
|
# Using this is sorta like using iTMSTransporter except it can send email notifications and allows
|
10
10
|
# one to set global/per-command defaults via $HOME/.itms
|
11
11
|
|
@@ -14,13 +14,13 @@ module Command
|
|
14
14
|
class << self
|
15
15
|
def execute(name, options, argv)
|
16
16
|
name = name.capitalize
|
17
|
-
# Avoid Ruby 1.8/1.9 String/Symbol/const_defined? differences
|
17
|
+
# Avoid Ruby 1.8/1.9 String/Symbol/const_defined? differences
|
18
18
|
unless constants.include?(name) || constants.include?(name.to_sym)
|
19
19
|
raise ArgumentError, "unknown command '#{name}'"
|
20
20
|
end
|
21
21
|
|
22
22
|
command = const_get(name).new(options)
|
23
|
-
command.execute(argv)
|
23
|
+
command.execute(argv)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -30,7 +30,7 @@ module Command
|
|
30
30
|
@options = options
|
31
31
|
end
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
class Providers < Base
|
35
35
|
def initialize(options)
|
36
36
|
# Let iTMSTransporter print the providers
|
@@ -51,7 +51,7 @@ module Command
|
|
51
51
|
puts "Metadata saved to #{filename}"
|
52
52
|
end
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
class Schema < Base
|
56
56
|
def execute(args = [])
|
57
57
|
filename = "#{@options[:version]}-#{@options[:type]}.rng"
|
@@ -60,22 +60,41 @@ module Command
|
|
60
60
|
puts "Schema saved to #{filename}"
|
61
61
|
end
|
62
62
|
end
|
63
|
-
|
63
|
+
|
64
64
|
class Status < Base
|
65
65
|
def initialize(options)
|
66
|
-
#
|
67
|
-
|
66
|
+
# It's preferable to let iTMSTransporter output the status but unlike other commands
|
67
|
+
# it summarizes errors on stdout. This results in redundant error messages since we summarize
|
68
|
+
# errors too. To avoid this, and keep our error messages consistant, we reprint the status below.
|
69
|
+
options[:print_stdout] = false unless options.include?(:print_stdout)
|
68
70
|
super
|
69
71
|
end
|
70
72
|
|
71
|
-
def execute(args = [])
|
72
|
-
@itms.status
|
73
|
+
def execute(args = [])
|
74
|
+
info = @itms.status
|
75
|
+
info.each do |k, v|
|
76
|
+
next if k == :status
|
77
|
+
say(k, v, 21)
|
78
|
+
end
|
79
|
+
|
80
|
+
info[:status].each_with_index do |status, i|
|
81
|
+
pos = info[:status].size - i
|
82
|
+
puts "\n#{'-' * 15} Upload ##{pos} #{'-' * 15}"
|
83
|
+
status.each do |k, v|
|
84
|
+
say(k, v, 18)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def say(k, v, width)
|
90
|
+
k = k.to_s.capitalize.gsub("_", " ")
|
91
|
+
printf "%-#{width}s %s\n", k, v
|
73
92
|
end
|
74
93
|
end
|
75
94
|
|
76
95
|
class Upload < Base
|
77
96
|
def initialize(options)
|
78
|
-
# These can take a while so we let the user know what's going on
|
97
|
+
# These can take a while so we let the user know what's going on
|
79
98
|
options[:print_stderr] = true unless options.include?(:print_stderr)
|
80
99
|
super
|
81
100
|
end
|
@@ -98,9 +117,9 @@ module Command
|
|
98
117
|
puts @itms.version
|
99
118
|
end
|
100
119
|
end
|
101
|
-
end
|
120
|
+
end
|
102
121
|
|
103
|
-
class Email
|
122
|
+
class Email
|
104
123
|
Binding = Class.new do
|
105
124
|
def initialize(options = {})
|
106
125
|
options.each do |k, v|
|
@@ -109,19 +128,19 @@ class Email
|
|
109
128
|
end
|
110
129
|
end
|
111
130
|
end
|
112
|
-
|
131
|
+
|
113
132
|
def initialize(config = {})
|
114
133
|
unless config["to"]
|
115
|
-
raise "No email recipeints provided, you must specify at least one"
|
134
|
+
raise "No email recipeints provided, you must specify at least one"
|
116
135
|
end
|
117
136
|
|
118
137
|
@config = config
|
119
138
|
end
|
120
|
-
|
121
|
-
def send(params = {})
|
139
|
+
|
140
|
+
def send(params = {})
|
122
141
|
to = @config["to"].to_s.split /,/
|
123
142
|
host = @config["host"] || "localhost"
|
124
|
-
from = @config["from"] || "#{
|
143
|
+
from = @config["from"] || "#{user}@#{host}"
|
125
144
|
params = params.merge(@config)
|
126
145
|
message = ::ERB.new(build_template).def_class(Binding).new(params).result
|
127
146
|
|
@@ -132,6 +151,10 @@ class Email
|
|
132
151
|
end
|
133
152
|
|
134
153
|
protected
|
154
|
+
def user
|
155
|
+
ENV["USER"] || ENV["USERNAME"]
|
156
|
+
end
|
157
|
+
|
135
158
|
def build_template
|
136
159
|
%w[to from subject cc bcc].inject("") do |t, key|
|
137
160
|
t << "#{key}: #{@config[key]}\n" if @config[key]
|
@@ -141,7 +164,7 @@ class Email
|
|
141
164
|
end
|
142
165
|
|
143
166
|
COMMANDS = ITunes::Store::Transporter.instance_methods(false).map(&:to_s)
|
144
|
-
|
167
|
+
CONFIG_FILE_NAME = ".itms"
|
145
168
|
|
146
169
|
def home
|
147
170
|
ENV["HOME"] || ENV["USERPROFILE"]
|
@@ -149,28 +172,28 @@ end
|
|
149
172
|
|
150
173
|
# Should probably create a class for the options
|
151
174
|
def load_config(command)
|
152
|
-
|
153
|
-
|
175
|
+
return {} unless home
|
176
|
+
|
177
|
+
path = File.join(home, CONFIG_FILE_NAME)
|
178
|
+
return {} unless File.file?(path)
|
154
179
|
|
155
|
-
|
156
|
-
return
|
180
|
+
config = YAML.load_file(path)
|
181
|
+
return {} unless config
|
157
182
|
|
158
|
-
config = YAML.load_file(path)
|
159
|
-
|
160
183
|
# Get the global defaults. select() returns an aray on Ruby < 1.9
|
161
|
-
defaults = config.select { |k,v| !v.is_a?(Hash) }
|
162
|
-
defaults = Hash[defaults] unless defaults.is_a?(Hash)
|
184
|
+
defaults = config.select { |k,v| !v.is_a?(Hash) }
|
185
|
+
defaults = Hash[defaults] unless defaults.is_a?(Hash)
|
163
186
|
|
164
187
|
config[command] = defaults.merge(config[command] || {})
|
165
|
-
|
188
|
+
|
166
189
|
# Normalize the email config
|
167
190
|
email = Hash.new { |h, k| h[k] = {} }
|
168
191
|
|
169
|
-
%w[success failure].each do |type|
|
170
|
-
email[type] = (config[command]["email"] ||= {})[type]
|
192
|
+
%w[success failure].each do |type|
|
193
|
+
email[type] = (config[command]["email"] ||= {})[type]
|
171
194
|
next unless email[type]
|
172
195
|
|
173
|
-
# Merge the global email options & the command's "global" options with the success/failure options
|
196
|
+
# Merge the global email options & the command's "global" options with the success/failure options
|
174
197
|
settings = (config["email"].to_a + config[command]["email"].to_a).reject { |k, v| k == "success" or k == "failure" }
|
175
198
|
settings.each do |k, v|
|
176
199
|
email[type][k] = email[type][k] ? "#{email[type][k]}, #{v}" : v
|
@@ -178,9 +201,9 @@ def load_config(command)
|
|
178
201
|
end
|
179
202
|
|
180
203
|
# ITunes::Store::Transporter uses Symbols for options
|
181
|
-
config[command] = config[command].inject({}) do |cfg, (k,v)|
|
204
|
+
config[command] = config[command].inject({}) do |cfg, (k,v)|
|
182
205
|
cfg[k.to_sym] = v unless k.empty? # Avoid intern empty string errors in 1.8
|
183
|
-
cfg
|
206
|
+
cfg
|
184
207
|
end
|
185
208
|
|
186
209
|
config[command][:email] = email
|
@@ -206,19 +229,24 @@ command = ARGV.shift
|
|
206
229
|
abort("usage: itms command [options]") unless command
|
207
230
|
abort("invalid command '#{command}', valid commands are: #{COMMANDS.sort.join(', ')}") unless COMMANDS.include?(command)
|
208
231
|
|
209
|
-
options = load_config(command)
|
232
|
+
options = ARGV.delete("--no-config") ? {} : load_config(command)
|
210
233
|
|
211
|
-
while ARGV.any?
|
234
|
+
while ARGV.any?
|
212
235
|
opt = ARGV.first.dup
|
213
236
|
break unless opt.sub!(/\A--(?=\w)/, "")
|
214
|
-
|
237
|
+
|
215
238
|
key, val = opt.split(/=/, 2)
|
216
239
|
key.gsub!(/-/, "_")
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
240
|
+
|
241
|
+
if val
|
242
|
+
val = val.to_i if val =~ /\A\d+\z/
|
243
|
+
else
|
244
|
+
# Boolean option
|
245
|
+
val = key.sub!(/\Ano_(?=\w)/, "") ? false : true
|
246
|
+
end
|
247
|
+
|
248
|
+
options[key.to_sym] = val
|
249
|
+
ARGV.shift
|
222
250
|
end
|
223
251
|
|
224
252
|
# Keys for this are strings
|
@@ -227,11 +255,11 @@ command_options = options.dup
|
|
227
255
|
options[:argv] = ARGV.dup
|
228
256
|
options[:command] = command
|
229
257
|
|
230
|
-
begin
|
231
|
-
puts "Running command '#{command}'"
|
258
|
+
begin
|
259
|
+
puts "Running command '#{command}'\n\n"
|
232
260
|
Command.execute(command, command_options, ARGV)
|
233
261
|
send_email(email_options["success"], options)
|
234
|
-
rescue ITunes::Store::Transporter::ExecutionError => e
|
262
|
+
rescue ITunes::Store::Transporter::ExecutionError => e
|
235
263
|
print_errors(e)
|
236
264
|
options[:error] = e
|
237
265
|
send_email(email_options["failure"], options)
|
@@ -240,4 +268,3 @@ rescue ITunes::Store::Transporter::TransporterError => e
|
|
240
268
|
$stderr.puts e
|
241
269
|
exit 2
|
242
270
|
end
|
243
|
-
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require "optout"
|
2
|
-
require "itunes/store/transporter"
|
3
2
|
require "itunes/store/transporter/shell"
|
4
3
|
require "itunes/store/transporter/errors"
|
5
4
|
require "itunes/store/transporter/output_parser"
|
@@ -9,7 +8,7 @@ module ITunes
|
|
9
8
|
module Store
|
10
9
|
class Transporter
|
11
10
|
module Command # :nodoc: all
|
12
|
-
|
11
|
+
|
13
12
|
class Base
|
14
13
|
include Option
|
15
14
|
|
@@ -18,13 +17,13 @@ module ITunes
|
|
18
17
|
@shell = Shell.new(@config[:path])
|
19
18
|
@default_options = default_options
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
def run(options = {})
|
23
22
|
options = default_options.merge(options)
|
24
23
|
argv = create_transporter_options(options)
|
25
24
|
stdout_lines = []
|
26
25
|
stderr_lines = []
|
27
|
-
|
26
|
+
|
28
27
|
# TODO: hooks
|
29
28
|
exitcode = @shell.exec(argv) do |line, name|
|
30
29
|
if name == :stdout
|
@@ -46,13 +45,13 @@ module ITunes
|
|
46
45
|
protected
|
47
46
|
attr :config
|
48
47
|
attr :default_options
|
49
|
-
|
48
|
+
|
50
49
|
def options
|
51
50
|
@options ||= Optout.options do
|
52
51
|
# On Windows we must include this else the Transporter batch script will call `pause` after the Transporter exits
|
53
|
-
on :windows, "-WONoPause"
|
52
|
+
on :windows, "-WONoPause"
|
54
53
|
# Optout can't do this: [a, b, c] => -X a -X b -X c
|
55
|
-
on :jvm, "-X" #, :multiple => true
|
54
|
+
on :jvm, "-X" #, :multiple => true
|
56
55
|
end
|
57
56
|
end
|
58
57
|
|
@@ -60,38 +59,38 @@ module ITunes
|
|
60
59
|
def handle_success(stdout_lines, stderr_lines, options)
|
61
60
|
stdout_lines.join
|
62
61
|
end
|
63
|
-
|
62
|
+
|
64
63
|
def handle_error(stdout_lines, stderr_lines, options, exitcode)
|
65
|
-
parser =
|
64
|
+
parser = OutputParser.new(stderr_lines)
|
66
65
|
errors = parser.errors.any? ? parser.errors : [ TransporterMessage.new(stderr_lines.join) ]
|
67
|
-
raise
|
66
|
+
raise ExecutionError.new(errors, exitcode)
|
68
67
|
end
|
69
|
-
|
68
|
+
|
70
69
|
def create_transporter_options(optz)
|
71
|
-
optz[:windows] = "true" if
|
70
|
+
optz[:windows] = "true" if Shell.windows?
|
72
71
|
options.argv(optz)
|
73
72
|
rescue Optout::OptionError => e
|
74
|
-
raise
|
73
|
+
raise OptionError, e.message
|
75
74
|
end
|
76
75
|
end
|
77
|
-
|
76
|
+
|
78
77
|
class Mode < Base
|
79
|
-
def initialize(*config)
|
80
|
-
super
|
78
|
+
def initialize(*config)
|
79
|
+
super
|
81
80
|
options.on :log, "-o", Optout::File
|
82
|
-
options.on :verbose, "-v", %w|informational critical detailed eXtreme| # Since log output is critical to determining what's going on we can't include "off"
|
81
|
+
options.on :verbose, "-v", %w|informational critical detailed eXtreme| # Since log output is critical to determining what's going on we can't include "off"
|
82
|
+
options.on :summary, "-summaryFile", Optout::File
|
83
|
+
options.on :mode, "-m", /\w+/, :required => true
|
83
84
|
options.on :username, "-u", :required => true
|
84
85
|
options.on :password, "-p", :required => true
|
85
|
-
options.on :summary, "-summaryFile", Optout::File
|
86
|
-
options.on :mode, "-m", /\w+/, :required => true
|
87
86
|
options.on *SHORTNAME
|
88
87
|
end
|
89
|
-
|
88
|
+
|
90
89
|
def create_transporter_options(optz)
|
91
90
|
optz[:mode] = mode
|
92
91
|
super
|
93
92
|
end
|
94
|
-
|
93
|
+
|
95
94
|
def mode
|
96
95
|
self.class.to_s.split("::")[-1].gsub(/([a-z])([A-Z])/, "\1_\2").downcase
|
97
96
|
end
|
@@ -100,4 +99,4 @@ module ITunes
|
|
100
99
|
end
|
101
100
|
end
|
102
101
|
end
|
103
|
-
end
|
102
|
+
end
|