]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/status.py
webish: add more share information to upload status, including assisted uploads
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / status.py
1
2 import time
3 from twisted.internet import defer
4 from nevow import rend, tags as T
5 from allmydata.util import base32, idlib
6 from allmydata.web.common import IClient, getxmlfile
7 from allmydata.interfaces import IUploadStatus, IDownloadStatus, \
8      IPublishStatus, IRetrieveStatus
9
10 def plural(sequence_or_length):
11     if isinstance(sequence_or_length, int):
12         length = sequence_or_length
13     else:
14         length = len(sequence_or_length)
15     if length == 1:
16         return ""
17     return "s"
18
19 class RateAndTimeMixin:
20
21     def render_time(self, ctx, data):
22         # 1.23s, 790ms, 132us
23         if data is None:
24             return ""
25         s = float(data)
26         if s >= 1.0:
27             return "%.2fs" % s
28         if s >= 0.01:
29             return "%dms" % (1000*s)
30         if s >= 0.001:
31             return "%.1fms" % (1000*s)
32         return "%dus" % (1000000*s)
33
34     def render_rate(self, ctx, data):
35         # 21.8kBps, 554.4kBps 4.37MBps
36         if data is None:
37             return ""
38         r = float(data)
39         if r > 1000000:
40             return "%1.2fMBps" % (r/1000000)
41         if r > 1000:
42             return "%.1fkBps" % (r/1000)
43         return "%dBps" % r
44
45 class UploadResultsRendererMixin(RateAndTimeMixin):
46     # this requires a method named 'upload_results'
47
48     def render_pushed_shares(self, ctx, data):
49         d = self.upload_results()
50         d.addCallback(lambda res: res.pushed_shares)
51         return d
52
53     def render_preexisting_shares(self, ctx, data):
54         d = self.upload_results()
55         d.addCallback(lambda res: res.preexisting_shares)
56         return d
57
58     def render_sharemap(self, ctx, data):
59         d = self.upload_results()
60         d.addCallback(lambda res: res.sharemap)
61         def _render(sharemap):
62             if sharemap is None:
63                 return "None"
64             l = T.ul()
65             for shnum in sorted(sharemap.keys()):
66                 l[T.li["%d -> %s" % (shnum, sharemap[shnum])]]
67             return l
68         d.addCallback(_render)
69         return d
70
71     def render_servermap(self, ctx, data):
72         d = self.upload_results()
73         d.addCallback(lambda res: res.servermap)
74         def _render(servermap):
75             if servermap is None:
76                 return "None"
77             l = T.ul()
78             for peerid in sorted(servermap.keys()):
79                 peerid_s = idlib.shortnodeid_b2a(peerid)
80                 shares_s = ",".join(["#%d" % shnum
81                                      for shnum in servermap[peerid]])
82                 l[T.li["[%s] got share%s: %s" % (peerid_s,
83                                                  plural(servermap[peerid]),
84                                                  shares_s)]]
85             return l
86         d.addCallback(_render)
87         return d
88
89     def data_file_size(self, ctx, data):
90         d = self.upload_results()
91         d.addCallback(lambda res: res.file_size)
92         return d
93
94     def _get_time(self, name):
95         d = self.upload_results()
96         d.addCallback(lambda res: res.timings.get(name))
97         return d
98
99     def data_time_total(self, ctx, data):
100         return self._get_time("total")
101
102     def data_time_storage_index(self, ctx, data):
103         return self._get_time("storage_index")
104
105     def data_time_contacting_helper(self, ctx, data):
106         return self._get_time("contacting_helper")
107
108     def data_time_existence_check(self, ctx, data):
109         return self._get_time("existence_check")
110
111     def data_time_cumulative_fetch(self, ctx, data):
112         return self._get_time("cumulative_fetch")
113
114     def data_time_helper_total(self, ctx, data):
115         return self._get_time("helper_total")
116
117     def data_time_peer_selection(self, ctx, data):
118         return self._get_time("peer_selection")
119
120     def data_time_total_encode_and_push(self, ctx, data):
121         return self._get_time("total_encode_and_push")
122
123     def data_time_cumulative_encoding(self, ctx, data):
124         return self._get_time("cumulative_encoding")
125
126     def data_time_cumulative_sending(self, ctx, data):
127         return self._get_time("cumulative_sending")
128
129     def data_time_hashes_and_close(self, ctx, data):
130         return self._get_time("hashes_and_close")
131
132     def _get_rate(self, name):
133         d = self.upload_results()
134         def _convert(r):
135             file_size = r.file_size
136             time = r.timings.get(name)
137             if time is None:
138                 return None
139             try:
140                 return 1.0 * file_size / time
141             except ZeroDivisionError:
142                 return None
143         d.addCallback(_convert)
144         return d
145
146     def data_rate_total(self, ctx, data):
147         return self._get_rate("total")
148
149     def data_rate_storage_index(self, ctx, data):
150         return self._get_rate("storage_index")
151
152     def data_rate_encode(self, ctx, data):
153         return self._get_rate("cumulative_encoding")
154
155     def data_rate_push(self, ctx, data):
156         return self._get_rate("cumulative_sending")
157
158     def data_rate_encode_and_push(self, ctx, data):
159         d = self.upload_results()
160         def _convert(r):
161             file_size = r.file_size
162             if file_size is None:
163                 return None
164             time1 = r.timings.get("cumulative_encoding")
165             if time1 is None:
166                 return None
167             time2 = r.timings.get("cumulative_sending")
168             if time2 is None:
169                 return None
170             try:
171                 return 1.0 * file_size / (time1+time2)
172             except ZeroDivisionError:
173                 return None
174         d.addCallback(_convert)
175         return d
176
177     def data_rate_ciphertext_fetch(self, ctx, data):
178         d = self.upload_results()
179         def _convert(r):
180             fetch_size = r.ciphertext_fetched
181             if fetch_size is None:
182                 return None
183             time = r.timings.get("cumulative_fetch")
184             if time is None:
185                 return None
186             try:
187                 return 1.0 * fetch_size / time
188             except ZeroDivisionError:
189                 return None
190         d.addCallback(_convert)
191         return d
192
193 class UploadStatusPage(UploadResultsRendererMixin, rend.Page):
194     docFactory = getxmlfile("upload-status.xhtml")
195
196     def __init__(self, data):
197         rend.Page.__init__(self, data)
198         self.upload_status = data
199
200     def upload_results(self):
201         return defer.maybeDeferred(self.upload_status.get_results)
202
203     def render_results(self, ctx, data):
204         d = self.upload_results()
205         def _got_results(results):
206             if results:
207                 return ctx.tag
208             return ""
209         d.addCallback(_got_results)
210         return d
211
212     def render_started(self, ctx, data):
213         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
214         started_s = time.strftime(TIME_FORMAT,
215                                   time.localtime(data.get_started()))
216         return started_s
217
218     def render_si(self, ctx, data):
219         si_s = base32.b2a_or_none(data.get_storage_index())
220         if si_s is None:
221             si_s = "(None)"
222         return si_s
223
224     def render_helper(self, ctx, data):
225         return {True: "Yes",
226                 False: "No"}[data.using_helper()]
227
228     def render_total_size(self, ctx, data):
229         size = data.get_size()
230         if size is None:
231             size = "(unknown)"
232         return size
233
234     def render_progress_hash(self, ctx, data):
235         progress = data.get_progress()[0]
236         # TODO: make an ascii-art bar
237         return "%.1f%%" % (100.0 * progress)
238
239     def render_progress_ciphertext(self, ctx, data):
240         progress = data.get_progress()[1]
241         # TODO: make an ascii-art bar
242         return "%.1f%%" % (100.0 * progress)
243
244     def render_progress_encode_push(self, ctx, data):
245         progress = data.get_progress()[2]
246         # TODO: make an ascii-art bar
247         return "%.1f%%" % (100.0 * progress)
248
249     def render_status(self, ctx, data):
250         return data.get_status()
251
252 class DownloadResultsRendererMixin(RateAndTimeMixin):
253     # this requires a method named 'download_results'
254
255     def render_servermap(self, ctx, data):
256         d = self.download_results()
257         d.addCallback(lambda res: res.servermap)
258         def _render(servermap):
259             if servermap is None:
260                 return "None"
261             l = T.ul()
262             for peerid in sorted(servermap.keys()):
263                 peerid_s = idlib.shortnodeid_b2a(peerid)
264                 shares_s = ",".join(["#%d" % shnum
265                                      for shnum in servermap[peerid]])
266                 l[T.li["[%s] has share%s: %s" % (peerid_s,
267                                                  plural(servermap[peerid]),
268                                                  shares_s)]]
269             return l
270         d.addCallback(_render)
271         return d
272
273     def render_servers_used(self, ctx, data):
274         d = self.download_results()
275         d.addCallback(lambda res: res.servers_used)
276         def _got(servers_used):
277             if not servers_used:
278                 return ""
279             peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid)
280                                    for peerid in servers_used])
281             return T.li["Servers Used: ", peerids_s]
282         d.addCallback(_got)
283         return d
284
285     def render_problems(self, ctx, data):
286         d = self.download_results()
287         d.addCallback(lambda res: res.server_problems)
288         def _got(server_problems):
289             if not server_problems:
290                 return ""
291             l = T.ul()
292             for peerid in sorted(server_problems.keys()):
293                 peerid_s = idlib.shortnodeid_b2a(peerid)
294                 l[T.li["[%s]: %s" % (peerid_s, server_problems[peerid])]]
295             return T.li["Server Problems:", l]
296         d.addCallback(_got)
297         return d
298
299     def data_file_size(self, ctx, data):
300         d = self.download_results()
301         d.addCallback(lambda res: res.file_size)
302         return d
303
304     def _get_time(self, name):
305         d = self.download_results()
306         d.addCallback(lambda res: res.timings.get(name))
307         return d
308
309     def data_time_total(self, ctx, data):
310         return self._get_time("total")
311
312     def data_time_peer_selection(self, ctx, data):
313         return self._get_time("peer_selection")
314
315     def data_time_uri_extension(self, ctx, data):
316         return self._get_time("uri_extension")
317
318     def data_time_hashtrees(self, ctx, data):
319         return self._get_time("hashtrees")
320
321     def data_time_segments(self, ctx, data):
322         return self._get_time("segments")
323
324     def data_time_cumulative_fetch(self, ctx, data):
325         return self._get_time("cumulative_fetch")
326
327     def data_time_cumulative_decode(self, ctx, data):
328         return self._get_time("cumulative_decode")
329
330     def data_time_cumulative_decrypt(self, ctx, data):
331         return self._get_time("cumulative_decrypt")
332
333     def _get_rate(self, name):
334         d = self.download_results()
335         def _convert(r):
336             file_size = r.file_size
337             time = r.timings.get(name)
338             if time is None:
339                 return None
340             try:
341                 return 1.0 * file_size / time
342             except ZeroDivisionError:
343                 return None
344         d.addCallback(_convert)
345         return d
346
347     def data_rate_total(self, ctx, data):
348         return self._get_rate("total")
349
350     def data_rate_segments(self, ctx, data):
351         return self._get_rate("segments")
352
353     def data_rate_fetch(self, ctx, data):
354         return self._get_rate("cumulative_fetch")
355
356     def data_rate_decode(self, ctx, data):
357         return self._get_rate("cumulative_decode")
358
359     def data_rate_decrypt(self, ctx, data):
360         return self._get_rate("cumulative_decrypt")
361
362     def render_server_timings(self, ctx, data):
363         d = self.download_results()
364         d.addCallback(lambda res: res.timings.get("fetch_per_server"))
365         def _render(per_server):
366             if per_server is None:
367                 return ""
368             l = T.ul()
369             for peerid in sorted(per_server.keys()):
370                 peerid_s = idlib.shortnodeid_b2a(peerid)
371                 times_s = ", ".join([self.render_time(None, t)
372                                      for t in per_server[peerid]])
373                 l[T.li["[%s]: %s" % (peerid_s, times_s)]]
374             return T.li["Per-Server Segment Fetch Response Times: ", l]
375         d.addCallback(_render)
376         return d
377
378 class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page):
379     docFactory = getxmlfile("download-status.xhtml")
380
381     def __init__(self, data):
382         rend.Page.__init__(self, data)
383         self.download_status = data
384
385     def download_results(self):
386         return defer.maybeDeferred(self.download_status.get_results)
387
388     def render_results(self, ctx, data):
389         d = self.download_results()
390         def _got_results(results):
391             if results:
392                 return ctx.tag
393             return ""
394         d.addCallback(_got_results)
395         return d
396
397     def render_started(self, ctx, data):
398         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
399         started_s = time.strftime(TIME_FORMAT,
400                                   time.localtime(data.get_started()))
401         return started_s
402
403     def render_si(self, ctx, data):
404         si_s = base32.b2a_or_none(data.get_storage_index())
405         if si_s is None:
406             si_s = "(None)"
407         return si_s
408
409     def render_helper(self, ctx, data):
410         return {True: "Yes",
411                 False: "No"}[data.using_helper()]
412
413     def render_total_size(self, ctx, data):
414         size = data.get_size()
415         if size is None:
416             size = "(unknown)"
417         return size
418
419     def render_progress(self, ctx, data):
420         progress = data.get_progress()
421         # TODO: make an ascii-art bar
422         return "%.1f%%" % (100.0 * progress)
423
424     def render_status(self, ctx, data):
425         return data.get_status()
426
427 class RetrieveStatusPage(rend.Page, RateAndTimeMixin):
428     docFactory = getxmlfile("retrieve-status.xhtml")
429
430     def __init__(self, data):
431         rend.Page.__init__(self, data)
432         self.retrieve_status = data
433
434     def render_started(self, ctx, data):
435         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
436         started_s = time.strftime(TIME_FORMAT,
437                                   time.localtime(data.get_started()))
438         return started_s
439
440     def render_si(self, ctx, data):
441         si_s = base32.b2a_or_none(data.get_storage_index())
442         if si_s is None:
443             si_s = "(None)"
444         return si_s
445
446     def render_helper(self, ctx, data):
447         return {True: "Yes",
448                 False: "No"}[data.using_helper()]
449
450     def render_current_size(self, ctx, data):
451         size = data.get_size()
452         if size is None:
453             size = "(unknown)"
454         return size
455
456     def render_progress(self, ctx, data):
457         progress = data.get_progress()
458         # TODO: make an ascii-art bar
459         return "%.1f%%" % (100.0 * progress)
460
461     def render_status(self, ctx, data):
462         return data.get_status()
463
464     def render_encoding(self, ctx, data):
465         k, n = data.get_encoding()
466         return ctx.tag["Encoding: %s of %s" % (k, n)]
467
468     def render_search_distance(self, ctx, data):
469         d = data.get_search_distance()
470         return ctx.tag["Search Distance: %s peer%s" % (d, plural(d))]
471
472     def render_problems(self, ctx, data):
473         problems = data.problems
474         if not problems:
475             return ""
476         l = T.ul()
477         for peerid in sorted(problems.keys()):
478             peerid_s = idlib.shortnodeid_b2a(peerid)
479             l[T.li["[%s]: %s" % (peerid_s, problems[peerid])]]
480         return ctx.tag["Server Problems:", l]
481
482     def _get_rate(self, data, name):
483         file_size = self.retrieve_status.get_size()
484         time = self.retrieve_status.timings.get(name)
485         if time is None:
486             return None
487         try:
488             return 1.0 * file_size / time
489         except ZeroDivisionError:
490             return None
491
492     def data_time_total(self, ctx, data):
493         return self.retrieve_status.timings.get("total")
494     def data_rate_total(self, ctx, data):
495         return self._get_rate(data, "total")
496
497     def data_time_peer_selection(self, ctx, data):
498         return self.retrieve_status.timings.get("peer_selection")
499
500     def data_time_fetch(self, ctx, data):
501         return self.retrieve_status.timings.get("fetch")
502     def data_rate_fetch(self, ctx, data):
503         return self._get_rate(data, "fetch")
504
505     def data_time_cumulative_verify(self, ctx, data):
506         return self.retrieve_status.timings.get("cumulative_verify")
507
508     def data_time_decode(self, ctx, data):
509         return self.retrieve_status.timings.get("decode")
510     def data_rate_decode(self, ctx, data):
511         return self._get_rate(data, "decode")
512
513     def data_time_decrypt(self, ctx, data):
514         return self.retrieve_status.timings.get("decrypt")
515     def data_rate_decrypt(self, ctx, data):
516         return self._get_rate(data, "decrypt")
517
518     def render_server_timings(self, ctx, data):
519         per_server = self.retrieve_status.timings.get("fetch_per_server")
520         if not per_server:
521             return ""
522         l = T.ul()
523         for peerid in sorted(per_server.keys()):
524             peerid_s = idlib.shortnodeid_b2a(peerid)
525             times_s = ", ".join([self.render_time(None, t)
526                                  for t in per_server[peerid]])
527             l[T.li["[%s]: %s" % (peerid_s, times_s)]]
528         return T.li["Per-Server Fetch Response Times: ", l]
529
530
531 class PublishStatusPage(rend.Page, RateAndTimeMixin):
532     docFactory = getxmlfile("publish-status.xhtml")
533
534     def __init__(self, data):
535         rend.Page.__init__(self, data)
536         self.publish_status = data
537
538     def render_started(self, ctx, data):
539         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
540         started_s = time.strftime(TIME_FORMAT,
541                                   time.localtime(data.get_started()))
542         return started_s
543
544     def render_si(self, ctx, data):
545         si_s = base32.b2a_or_none(data.get_storage_index())
546         if si_s is None:
547             si_s = "(None)"
548         return si_s
549
550     def render_helper(self, ctx, data):
551         return {True: "Yes",
552                 False: "No"}[data.using_helper()]
553
554     def render_current_size(self, ctx, data):
555         size = data.get_size()
556         if size is None:
557             size = "(unknown)"
558         return size
559
560     def render_progress(self, ctx, data):
561         progress = data.get_progress()
562         # TODO: make an ascii-art bar
563         return "%.1f%%" % (100.0 * progress)
564
565     def render_status(self, ctx, data):
566         return data.get_status()
567
568     def render_encoding(self, ctx, data):
569         k, n = data.get_encoding()
570         return ctx.tag["Encoding: %s of %s" % (k, n)]
571
572     def render_peers_queried(self, ctx, data):
573         return ctx.tag["Peers Queried: ", data.peers_queried]
574
575     def render_sharemap(self, ctx, data):
576         sharemap = data.sharemap
577         if sharemap is None:
578             return ctx.tag["None"]
579         l = T.ul()
580         for shnum in sorted(sharemap.keys()):
581             l[T.li["%d -> Placed on " % shnum,
582                    ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid)
583                               for (peerid,seqnum,root_hash)
584                               in sharemap[shnum]])]]
585         return ctx.tag["Sharemap:", l]
586
587     def render_problems(self, ctx, data):
588         problems = data.problems
589         if not problems:
590             return ""
591         l = T.ul()
592         for peerid in sorted(problems.keys()):
593             peerid_s = idlib.shortnodeid_b2a(peerid)
594             l[T.li["[%s]: %s" % (peerid_s, problems[peerid])]]
595         return ctx.tag["Server Problems:", l]
596
597     def _get_rate(self, data, name):
598         file_size = self.publish_status.get_size()
599         time = self.publish_status.timings.get(name)
600         if time is None:
601             return None
602         try:
603             return 1.0 * file_size / time
604         except ZeroDivisionError:
605             return None
606
607     def data_time_total(self, ctx, data):
608         return self.publish_status.timings.get("total")
609     def data_rate_total(self, ctx, data):
610         return self._get_rate(data, "total")
611
612     def data_time_setup(self, ctx, data):
613         return self.publish_status.timings.get("setup")
614
615     def data_time_query(self, ctx, data):
616         return self.publish_status.timings.get("query")
617
618     def data_time_privkey(self, ctx, data):
619         return self.publish_status.timings.get("privkey")
620
621     def data_time_privkey_fetch(self, ctx, data):
622         return self.publish_status.timings.get("privkey_fetch")
623     def render_privkey_from(self, ctx, data):
624         peerid = data.privkey_from
625         if peerid:
626             return " (got from [%s])" % idlib.shortnodeid_b2a(peerid)
627         else:
628             return ""
629
630     def data_time_encrypt(self, ctx, data):
631         return self.publish_status.timings.get("encrypt")
632     def data_rate_encrypt(self, ctx, data):
633         return self._get_rate(data, "encrypt")
634
635     def data_time_encode(self, ctx, data):
636         return self.publish_status.timings.get("encode")
637     def data_rate_encode(self, ctx, data):
638         return self._get_rate(data, "encode")
639
640     def data_time_pack(self, ctx, data):
641         return self.publish_status.timings.get("pack")
642     def data_rate_pack(self, ctx, data):
643         return self._get_rate(data, "pack")
644     def data_time_sign(self, ctx, data):
645         return self.publish_status.timings.get("sign")
646
647     def data_time_push(self, ctx, data):
648         return self.publish_status.timings.get("push")
649     def data_rate_push(self, ctx, data):
650         return self._get_rate(data, "push")
651
652     def data_initial_read_size(self, ctx, data):
653         return self.publish_status.initial_read_size
654
655     def render_server_timings(self, ctx, data):
656         per_server = self.publish_status.timings.get("per_server")
657         if not per_server:
658             return ""
659         l = T.ul()
660         for peerid in sorted(per_server.keys()):
661             peerid_s = idlib.shortnodeid_b2a(peerid)
662             times = []
663             for op,t in per_server[peerid]:
664                 if op == "read":
665                     times.append( "(" + self.render_time(None, t) + ")" )
666                 else:
667                     times.append( self.render_time(None, t) )
668             times_s = ", ".join(times)
669             l[T.li["[%s]: %s" % (peerid_s, times_s)]]
670         return T.li["Per-Server Response Times: ", l]
671
672
673 class Status(rend.Page):
674     docFactory = getxmlfile("status.xhtml")
675     addSlash = True
676
677     def data_active_operations(self, ctx, data):
678         active =  (IClient(ctx).list_active_uploads() +
679                    IClient(ctx).list_active_downloads() +
680                    IClient(ctx).list_active_publish() +
681                    IClient(ctx).list_active_retrieve())
682         return active
683
684     def data_recent_operations(self, ctx, data):
685         recent = [o for o in (IClient(ctx).list_recent_uploads() +
686                               IClient(ctx).list_recent_downloads() +
687                               IClient(ctx).list_recent_publish() +
688                               IClient(ctx).list_recent_retrieve())
689                   if not o.get_active()]
690         recent.sort(lambda a,b: cmp(a.get_started(), b.get_started()))
691         recent.reverse()
692         return recent
693
694     def render_row(self, ctx, data):
695         s = data
696
697         TIME_FORMAT = "%H:%M:%S %d-%b-%Y"
698         started_s = time.strftime(TIME_FORMAT,
699                                   time.localtime(s.get_started()))
700         ctx.fillSlots("started", started_s)
701
702         si_s = base32.b2a_or_none(s.get_storage_index())
703         if si_s is None:
704             si_s = "(None)"
705         ctx.fillSlots("si", si_s)
706         ctx.fillSlots("helper", {True: "Yes",
707                                  False: "No"}[s.using_helper()])
708
709         size = s.get_size()
710         if size is None:
711             size = "(unknown)"
712         ctx.fillSlots("total_size", size)
713
714         progress = data.get_progress()
715         if IUploadStatus.providedBy(data):
716             link = "up-%d" % data.get_counter()
717             ctx.fillSlots("type", "upload")
718             # TODO: make an ascii-art bar
719             (chk, ciphertext, encandpush) = progress
720             progress_s = ("hash: %.1f%%, ciphertext: %.1f%%, encode: %.1f%%" %
721                           ( (100.0 * chk),
722                             (100.0 * ciphertext),
723                             (100.0 * encandpush) ))
724             ctx.fillSlots("progress", progress_s)
725         elif IDownloadStatus.providedBy(data):
726             link = "down-%d" % data.get_counter()
727             ctx.fillSlots("type", "download")
728             ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
729         elif IPublishStatus.providedBy(data):
730             link = "publish-%d" % data.get_counter()
731             ctx.fillSlots("type", "publish")
732             ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
733         else:
734             assert IRetrieveStatus.providedBy(data)
735             ctx.fillSlots("type", "retrieve")
736             link = "retrieve-%d" % data.get_counter()
737             ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress))
738         ctx.fillSlots("status", T.a(href=link)[s.get_status()])
739         return ctx.tag
740
741     def childFactory(self, ctx, name):
742         client = IClient(ctx)
743         stype,count_s = name.split("-")
744         count = int(count_s)
745         if stype == "up":
746             for s in client.list_recent_uploads():
747                 if s.get_counter() == count:
748                     return UploadStatusPage(s)
749             for s in client.list_all_uploads():
750                 if s.get_counter() == count:
751                     return UploadStatusPage(s)
752         if stype == "down":
753             for s in client.list_recent_downloads():
754                 if s.get_counter() == count:
755                     return DownloadStatusPage(s)
756             for s in client.list_all_downloads():
757                 if s.get_counter() == count:
758                     return DownloadStatusPage(s)
759         if stype == "publish":
760             for s in client.list_recent_publish():
761                 if s.get_counter() == count:
762                     return PublishStatusPage(s)
763             for s in client.list_all_publish():
764                 if s.get_counter() == count:
765                     return PublishStatusPage(s)
766         if stype == "retrieve":
767             for s in client.list_recent_retrieve():
768                 if s.get_counter() == count:
769                     return RetrieveStatusPage(s)
770             for s in client.list_all_retrieve():
771                 if s.get_counter() == count:
772                     return RetrieveStatusPage(s)
773
774