bits 0.1.1-mswin32

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/bits.rb +410 -0
  2. data/test/test_bits.rb +132 -0
  3. metadata +41 -0
data/lib/bits.rb ADDED
@@ -0,0 +1,410 @@
1
+ #
2
+ # An interface for the "Background Intelligent Transfer Service" (BITS) in MS Windows
3
+ #
4
+ # A more decent way to interface with BITS would be the COM interface it provides. I did not get it to work, so I had to revert to pasing the bitsadmin command.
5
+ #
6
+ # Prereqs (formally stated in ../bits.gemspec):
7
+ # - "Background Intelligent Transfer Service" (BITS) f�r Windows (KB842773)
8
+ # - bitsadmin tool, available at http://www.microsoft.com/downloads/details.aspx?amp;displaylang=en&familyid=49AE8576-9BB9-4126-9761-BA8011FABF38&displaylang=en
9
+ #
10
+ # Author:: Nicolas E. Rabenau (mailto:nerab@gmx.at)
11
+ # Copyright:: Copyright (c) 2005 Nicolas E. Rabenau
12
+ # License:: Distributed under the same terms as Ruby
13
+ #
14
+ module BITS
15
+ #
16
+ # the basic bitsadmin string that is used in all methods
17
+ #
18
+ BITSADMIN = "bitsadmin /rawreturn /nowrap"
19
+
20
+ #
21
+ # The BITS Manager bundles all static BITS operations that are independent from an individual job
22
+ #
23
+ class Manager
24
+
25
+ #
26
+ # returns a hash of all BITS jobs where the job's name is the key and the Job itself is the value. If there is more
27
+ # than one job with the same name the value is an array of all jobs with that name.
28
+ #
29
+ def Manager.jobs
30
+ jobs = Hash.new
31
+
32
+ for jobString in `#{BITS::BITSADMIN} /list`.split(/\n/)
33
+ job = Job.new(jobString)
34
+
35
+ # a job's name is not guaranteed to be unique
36
+ if jobs[job.name].nil?
37
+ # value has not been assigned before
38
+ jobs[job.name] = job
39
+ else
40
+ if jobs[job.name].is_a? Array
41
+ # value already contains an array
42
+ jobs[job.name] << job
43
+ elsif jobs[job.name].is_a? Job
44
+ # value contains a Job
45
+ existingValue = jobs[job.name]
46
+ jobs[job.name] = Array.new
47
+ jobs[job.name] << existingValue
48
+ jobs[job.name] << job
49
+ else
50
+ # should never happen. If it does, there is something terribly wrong, so we bail out fast
51
+ raise "Unexpected class of value (#{jobs[job.name].class}) for jobs['#{job.name}']"
52
+ end
53
+ end
54
+ end
55
+
56
+ jobs
57
+ end
58
+
59
+ #
60
+ # returns a hash of all BITS jobs where the job's id is the key and the Job itself is the value
61
+ #
62
+ def Manager.jobs_by_id
63
+ jobs = Hash.new
64
+
65
+ for jobString in `#{BITS::BITSADMIN} /list`.split(/\n/)
66
+ job = Job.new(jobString)
67
+ jobs[job.id] = job
68
+ end
69
+
70
+ jobs
71
+ end
72
+
73
+ #
74
+ # create a new job with name
75
+ #
76
+ # It is arguable whether this method should be Job.create or Manager.createJob. I decided to use the latter.
77
+ #
78
+ def Manager.createJob(name)
79
+ if "" == name
80
+ raise "Job name missing"
81
+ end
82
+
83
+ Job.new(`#{BITS::BITSADMIN} /create "#{name}"`)
84
+ end
85
+
86
+ #
87
+ # deletes all jobs
88
+ #
89
+ def Manager.cancelAllJobs
90
+ `#{BITS::BITSADMIN} /reset`
91
+ end
92
+
93
+ #
94
+ # returns the version number of the underlying BITS
95
+ #
96
+ # For troubleshooting, we could use "bitsadmin /util /version /verbose"
97
+ #
98
+ def Manager.version
99
+ parsed = `bitsadmin`.scan /\d\.\d/
100
+ parsed[0]
101
+ end
102
+ end
103
+
104
+ #
105
+ # Models the description of a downloadable file, consisting of an URL and the local name where the result is going to be stored
106
+ #
107
+ class FileDescriptor
108
+ attr_accessor :remote_url, :local_name, :bytes_transferred, :bytes_total
109
+
110
+ #
111
+ # builds a new FileDescription with the URL and the optional local name
112
+ #
113
+ def initialize(url, local_name = nil)
114
+ @remote_url = url
115
+ @local_name = local_name
116
+ @bytes_transferred = 0
117
+ @bytes_total = 'UNKNOWN'
118
+ end
119
+
120
+ def to_s
121
+ "#{@remote_url} -> #{@local_name}"
122
+ end
123
+ end
124
+
125
+ #
126
+ # Models a BITS job
127
+ #
128
+ # A job can be modified through set methods, changes will be effective immediately.
129
+ #
130
+ class Job
131
+ #
132
+ # used to refer to the job in BITS (primary key)
133
+ #
134
+ attr_reader :id
135
+
136
+ #
137
+ # builds a new Job from the supplied id. The job must already exist in BITS
138
+ #
139
+ # Ideally this method would have only module visibility in order to prevent instantiations of a job outside Manager.createJob
140
+ #
141
+ def initialize(job_id)
142
+ if %r!([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12})! =~ job_id
143
+ @id = $1
144
+ else
145
+ raise JobParseException, "Unable to parse '#{job_id}' into a job description. Maybe the format has changed?"
146
+ end
147
+ end
148
+
149
+ def files
150
+ output = `#{BITS::BITSADMIN} /listfiles {#{@id}}`
151
+
152
+ # bitsadmin typically produces something like this:
153
+ # 0 / UNKNOWN WORKING http://remote -> c:\temp\local.file
154
+
155
+ files = Array.new
156
+
157
+ for line in output.split(/\n/)
158
+ if %r!(\d+) / ((\d+)|UNKNOWN) WORKING (.*) -> (.*)! =~ line
159
+ # print "found 1: >#{$1}< 2: >#{$2}< 3: >#{$3}< 4: >#{$4}< 5: >#{$5}<"
160
+ p = FileDescriptor.new($4, $5)
161
+ p.bytes_transferred = $1
162
+ p.bytes_total = $2
163
+ files << p
164
+ else
165
+ raise FileDescriptorParseException, "Unable to parse '#{output}' into a file descriptor. Maybe the format has changed?"
166
+ end
167
+ end
168
+
169
+ files
170
+ end
171
+
172
+ #
173
+ # adds a file to be downloaded and the local name for the file to the job
174
+ #
175
+ def addFile(url, local_name)
176
+ `bitsadmin /rawreturn /addfile {#{@id}} #{url} #{local_name}`
177
+ end
178
+
179
+ #
180
+ # suspends the job
181
+ #
182
+ def suspend
183
+ `#{BITS::BITSADMIN} /suspend {#{@id}}`
184
+ end
185
+
186
+ #
187
+ # resumes the job
188
+ #
189
+ def resume
190
+ `#{BITS::BITSADMIN} /resume {#{@id}}`
191
+ end
192
+
193
+ #
194
+ # cancels the job
195
+ #
196
+ def cancel
197
+ `#{BITS::BITSADMIN} /cancel {#{@id}}`
198
+ end
199
+
200
+ #
201
+ # completes the job
202
+ #
203
+ def complete
204
+ `#{BITS::BITSADMIN} /complete {#{@id}}`
205
+ end
206
+
207
+ # returns the job's type
208
+ def type
209
+ `#{BITS::BITSADMIN} /gettype {#{@id}}`
210
+ end
211
+
212
+ # returns the job's ACL propagation flags
213
+ def aclflags
214
+ `#{BITS::BITSADMIN} /getaclflags {#{@id}}`
215
+ end
216
+
217
+ # returns the size of the job
218
+ def bytestotal
219
+ `#{BITS::BITSADMIN} /getbytestotal {#{@id}}`
220
+ end
221
+
222
+ # returns the number of bytes transferred
223
+ def bytestransferred
224
+ `#{BITS::BITSADMIN} /getbytestransferred {#{@id}}`
225
+ end
226
+
227
+ # returns the number of files in the job
228
+ def filestotal
229
+ `#{BITS::BITSADMIN} /getfilestotal {#{@id}}`
230
+ end
231
+
232
+ # returns the number of files transferred
233
+ def filestransferred
234
+ `#{BITS::BITSADMIN} /getfilestransferred {#{@id}}`
235
+ end
236
+
237
+ # returns the job's creation time
238
+ def creationtime
239
+ `#{BITS::BITSADMIN} /getcreationtime {#{@id}}`
240
+ end
241
+
242
+ # returns the job's modification time
243
+ def modificationtime
244
+ `#{BITS::BITSADMIN} /getmodificationtime {#{@id}}`
245
+ end
246
+
247
+ # returns the job's completion time
248
+ def completiontime
249
+ `#{BITS::BITSADMIN} /getcompletiontime {#{@id}}`
250
+ end
251
+
252
+ # returns the job's state
253
+ def state
254
+ `#{BITS::BITSADMIN} /getstate {#{@id}}`
255
+ end
256
+
257
+ # returns detailed error information
258
+ def error
259
+ `#{BITS::BITSADMIN} /geterror {#{@id}}`
260
+ end
261
+
262
+ # returns the job's owner
263
+ def owner
264
+ `#{BITS::BITSADMIN} /getowner {#{@id}}`
265
+ end
266
+
267
+ # returns the job's display name
268
+ def name
269
+ `#{BITS::BITSADMIN} /getdisplayname {#{@id}}`
270
+ end
271
+
272
+ # returns the job's description
273
+ def description
274
+ `#{BITS::BITSADMIN} /getdescription {#{@id}}`
275
+ end
276
+
277
+ # returns the job's priority
278
+ def priority
279
+ `#{BITS::BITSADMIN} /getpriority {#{@id}}`
280
+ end
281
+
282
+ # returns the notify flags
283
+ def notifyflags
284
+ `#{BITS::BITSADMIN} /getnotifyflags {#{@id}}`
285
+ end
286
+
287
+ # Determines if notify interface is registered
288
+ def notifyinterface
289
+ `#{BITS::BITSADMIN} /getnotifyinterface {#{@id}}`
290
+ end
291
+
292
+ # returns the retry delay in seconds
293
+ def minretrydelay
294
+ `#{BITS::BITSADMIN} /getminretrydelay {#{@id}}`
295
+ end
296
+
297
+ # returns the no progress timeout in seconds
298
+ def noprogresstimeout
299
+ `#{BITS::BITSADMIN} /getnoprogresstimeout {#{@id}}`
300
+ end
301
+
302
+ # returns an error count for the job
303
+ def errorcount
304
+ `#{BITS::BITSADMIN} /geterrorcount {#{@id}}`
305
+ end
306
+
307
+ # returns the proxy usage setting
308
+ def proxyusage
309
+ `#{BITS::BITSADMIN} /getproxyusage {#{@id}}`
310
+ end
311
+
312
+ # returns the proxy list
313
+ def proxylist
314
+ `#{BITS::BITSADMIN} /getproxylist {#{@id}}`
315
+ end
316
+
317
+ # returns the proxy bypass list
318
+ def proxybypasslist
319
+ `#{BITS::BITSADMIN} /getproxybypasslist {#{@id}}`
320
+ end
321
+
322
+ # returns the job's notification command line
323
+ def notifycmdline
324
+ `#{BITS::BITSADMIN} /getnotifycmdline {#{@id}}`
325
+ end
326
+
327
+ # returns a String representation of the job
328
+ def to_s
329
+ `#{BITS::BITSADMIN} /info {#{@id}}`
330
+ end
331
+
332
+ # attribute writers
333
+
334
+ # sets the ACL propagation flags for the job
335
+ def aclflags=(acl_flags)
336
+ `#{BITS::BITSADMIN} /setaclflags {#{@id}} #{acl_flags}`
337
+ end
338
+
339
+ # sets the job display name
340
+ def name=(display_name)
341
+ `#{BITS::BITSADMIN} /setdisplayname {#{@id}} \"#{display_name}\"`
342
+ end
343
+
344
+ # sets the job description
345
+ def description=(description)
346
+ `#{BITS::BITSADMIN} /setdescription {#{@id}} \"#{description}\"`
347
+ end
348
+
349
+ # sets the job priority
350
+ def priority=(priority)
351
+ `#{BITS::BITSADMIN} /setpriority {#{@id}} #{priority}`
352
+ end
353
+
354
+ # sets the notify flags
355
+ def notifyflags=(notify_flags)
356
+ `#{BITS::BITSADMIN} /setnotifyflags {#{@id}} #{notify_flags}`
357
+ end
358
+
359
+ # sets the retry delay in seconds
360
+ def minretrydelay=(retry_delay)
361
+ `#{BITS::BITSADMIN} /setminretrydelay {#{@id}} #{retry_delay}`
362
+ end
363
+
364
+ # sets the no progress timeout in seconds
365
+ def noprogresstimeout=(timeout)
366
+ `#{BITS::BITSADMIN} /setnoprogresstimeout {#{@id}} #{timeout}`
367
+ end
368
+
369
+ # sets the proxy usage
370
+ def proxysettings=(usage)
371
+ `#{BITS::BITSADMIN} /setproxysettings {#{@id}} #{usage}`
372
+ end
373
+
374
+ # sets a program to execute for notification, and optionally parameters. The program name and parameters can be NULL.
375
+ # IMPORTANT: if parameters are non-NULL, then the program name should be the first parameter.
376
+ #
377
+ # For some reason I didn't get this assignment operation to work with more than a single parameter, therefore I made
378
+ # it a "setXxx" operation. Not elegant, but it works.
379
+ #
380
+ def setnotifycmdline(program_name, program_parameters = "")
381
+ `#{BITS::BITSADMIN} /setnotifycmdline {#{@id}} #{program_name} \"#{program_parameters}\"`
382
+ end
383
+
384
+ # adds credentials to a job. <target> may be either SERVER or PROXY, <scheme> may be BASIC, DIGEST, NTLM, NEGOTIATE, or PASSPORT.
385
+ def credentials=(target, scheme, username, password)
386
+ `#{BITS::BITSADMIN} /setcredentials {#{@id}} #{target} #{scheme} #{username} #{password}`
387
+ end
388
+ end
389
+
390
+ #
391
+ # Generic parse exception, base class for concrete parse exceptions
392
+ #
393
+ class ParseException < Exception
394
+
395
+ end
396
+
397
+ #
398
+ # Exception that occurs in case parsing a Job specification fails
399
+ #
400
+ class JobParseException < ParseException
401
+
402
+ end
403
+
404
+ #
405
+ # Exception that occurs in case parsing a FileDescriptor specification fails
406
+ #
407
+ class FileDescriptorParseException < ParseException
408
+
409
+ end
410
+ end
data/test/test_bits.rb ADDED
@@ -0,0 +1,132 @@
1
+ require 'test/unit'
2
+ require "bits"
3
+ include BITS
4
+
5
+ #
6
+ # Unit tests for bits.rb
7
+ #
8
+ # Author:: Nicolas E. Rabenau (mailto:nerab@gmx.at)
9
+ # Copyright:: Copyright (c) 2005 Nicolas E. Rabenau
10
+ # License:: Distributed under the same terms as Ruby
11
+ #
12
+ class BITS_Test < Test::Unit::TestCase
13
+
14
+ @@RND_CHARS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
15
+
16
+ #
17
+ # create job with random name
18
+ #
19
+ def setup
20
+ @jobName = self.class.name + "_" + BITS_Test.randomString(8)
21
+ @job = Manager.createJob(@jobName)
22
+ end
23
+
24
+ #
25
+ # remove test job
26
+ #
27
+ def teardown
28
+ @job.cancel
29
+ end
30
+
31
+ #
32
+ # test setting and reading the job's name
33
+ #
34
+ def test_name
35
+ assert_equal(@jobName, @job.name)
36
+ end
37
+
38
+ #
39
+ # test setting and reading the job's description
40
+ #
41
+ def test_description
42
+ description = "A test job for bits.rb"
43
+ @job.description = description
44
+ assert_equal(description, @job.description)
45
+ end
46
+
47
+ #
48
+ # tests the Manager's job listing by name
49
+ #
50
+ def test_joblist_by_name
51
+ assert(0 < Manager.jobs.size)
52
+ assert(Manager.jobs.has_key?(@jobName))
53
+ end
54
+
55
+ #
56
+ # tests that the Manager's job listing by name returns all Jobs with duplicate names in an array
57
+ #
58
+ def test_joblist_by_name_with_duplicate_jobnames
59
+ assert(Manager.jobs.has_key?(@jobName))
60
+ j2 = Manager.createJob(@jobName)
61
+ assert_equal(Array, Manager.jobs[@jobName].class)
62
+ assert_equal(2, Manager.jobs[@jobName].size)
63
+ j2.cancel
64
+ end
65
+
66
+ #
67
+ # tests the Manager's job listing by id
68
+ #
69
+ def test_joblist_by_id
70
+ assert(0 < Manager.jobs_by_id.size)
71
+ assert(Manager.jobs_by_id.has_key?(@job.id))
72
+ end
73
+
74
+ #
75
+ # test setting and reading the job's notify commandline
76
+ #
77
+ def test_notifycmdline
78
+ notifycmdline = "net"
79
+ notifycmdlineparams = "net send * BITS job #{@job.name} (#{@job.id}) successfully completed."
80
+ @job.setnotifycmdline(notifycmdline, notifycmdlineparams)
81
+ @job.description = "Testing notifycmdline"
82
+ assert_equal("the notification command line is '#{notifycmdline}' '#{notifycmdline}'", @job.notifycmdline)
83
+ end
84
+
85
+ def test_status_created
86
+ assert_equal("SUSPENDED", @job.state)
87
+ end
88
+
89
+ #
90
+ # test resuming a job without an URL attached, must fail
91
+ #
92
+ def test_resume_empty
93
+ @job.resume
94
+ assert_equal("SUSPENDED", @job.state)
95
+ end
96
+
97
+ #
98
+ # test resuming a job with an URL attached, must not be in suspended or cancelled state
99
+ #
100
+ def test_resume_nonempty
101
+ @job.addFile("http://www.google.com", "c:\\temp\\google.com.html")
102
+ @job.resume
103
+ assert_not_equal("SUSPENDED", @job.state)
104
+ assert_not_equal("CANCELLED", @job.state)
105
+ end
106
+
107
+ #
108
+ # test attaching files to the job
109
+ #
110
+ def test_files
111
+ @job.addFile("http://www.google.com", "c:\\temp\\google.com.html")
112
+ @job.addFile("http://www.heise.de", "c:\\temp\\heise.de.html")
113
+ assert_equal(@job.files[0].remote_url, "http://www.google.com")
114
+ assert_equal(@job.files[0].local_name, "c:\\temp\\google.com.html")
115
+ assert_equal(@job.files[1].remote_url, "http://www.heise.de")
116
+ assert_equal(@job.files[1].local_name, "c:\\temp\\heise.de.html")
117
+ end
118
+
119
+ private
120
+
121
+ #
122
+ # returns a String of defined length, made of random characters and numbers
123
+ #
124
+ def BITS_Test.randomString(length)
125
+ retVal = Array.new
126
+ for i in 0..length
127
+ retVal[i] = @@RND_CHARS[rand(@@RND_CHARS.length)]
128
+ end
129
+
130
+ retVal.to_s
131
+ end
132
+ end
metadata ADDED
@@ -0,0 +1,41 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: bits
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.1
7
+ date: 2005-10-12
8
+ summary: A ruby interface for the Background Intelligent Transfer Service (BITS)
9
+ require_paths:
10
+ - lib
11
+ email: nerab@gmx.at
12
+ homepage: http://bits.rubyforge.org
13
+ rubyforge_project: bits
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: mswin32
27
+ authors:
28
+ - Nicolas E. Rabenau
29
+ files:
30
+ - lib/bits.rb
31
+ test_files:
32
+ - test/test_bits.rb
33
+ rdoc_options: []
34
+ extra_rdoc_files: []
35
+ executables: []
36
+ extensions: []
37
+ requirements:
38
+ - Background Intelligent Transfer Service (BITS) f�r Windows (KB842773)
39
+ - "bitsadmin tool, available at
40
+ http://www.microsoft.com/downloads/details.aspx?amp;displaylang=en&familyid=49AE8576-9BB9-4126-9761-BA8011FABF38&displaylang=en"
41
+ dependencies: []