bits 0.1.1-mswin32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []